diff options
author | Shauren <shauren.trinity@gmail.com> | 2024-08-26 15:02:22 +0200 |
---|---|---|
committer | Ovahlord <dreadkiller@gmx.de> | 2024-08-27 17:20:43 +0200 |
commit | 18ae1540b53072c5fe10eaa80d96fed3a2e2c9a2 (patch) | |
tree | 52e3aa4ad56cdbd908fec5c9b4d239eacbf4df9d | |
parent | a3c66fe0b870f9ccb0225d307b2b190a60d5c0ef (diff) |
Core/Loot: Implemented currency loot
(cherry picked from commit 3e28ee080a1cf3c7cd332a8d1e0808505b4ea9d4)
# Conflicts:
# sql/base/characters_database.sql
# sql/updates/auth/cata_classic/2024_08_26_00_characters.sql
# sql/updates/world/cata_classic/2024_08_26_00_world.sql
# src/server/game/Entities/Player/Player.cpp
# src/server/game/Loot/Loot.cpp
# src/server/scripts/ExilesReach/zone_exiles_reach.cpp
-rw-r--r-- | sql/base/characters_database.sql | 6 | ||||
-rw-r--r-- | sql/updates/characters/cata_classic/2024_08_27_00_characters_2024_08_26_00_characters.sql | 3 | ||||
-rw-r--r-- | sql/updates/world/cata_classic/2024_08_27_01_world_2024_08_26_00_world.sql | 3 | ||||
-rw-r--r-- | src/server/database/Database/Implementation/CharacterDatabase.cpp | 6 | ||||
-rw-r--r-- | src/server/game/Entities/Player/Player.cpp | 121 | ||||
-rw-r--r-- | src/server/game/Entities/Player/Player.h | 1 | ||||
-rw-r--r-- | src/server/game/Handlers/LootHandler.cpp | 6 | ||||
-rw-r--r-- | src/server/game/Loot/Loot.cpp | 234 | ||||
-rw-r--r-- | src/server/game/Loot/Loot.h | 35 | ||||
-rw-r--r-- | src/server/game/Loot/LootItemStorage.cpp | 66 | ||||
-rw-r--r-- | src/server/game/Loot/LootItemStorage.h | 5 | ||||
-rw-r--r-- | src/server/game/Loot/LootMgr.cpp | 119 | ||||
-rw-r--r-- | src/server/game/Loot/LootMgr.h | 1 | ||||
-rw-r--r-- | src/server/game/Miscellaneous/Language.h | 2 | ||||
-rw-r--r-- | src/server/scripts/Commands/cs_modify.cpp | 20 | ||||
-rw-r--r-- | src/server/scripts/Commands/cs_npc.cpp | 114 |
16 files changed, 526 insertions, 216 deletions
diff --git a/sql/base/characters_database.sql b/sql/base/characters_database.sql index 35e3138b430..c4d6e126c41 100644 --- a/sql/base/characters_database.sql +++ b/sql/base/characters_database.sql @@ -2702,6 +2702,7 @@ DROP TABLE IF EXISTS `item_loot_items`; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `item_loot_items` ( `container_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'guid of container (item_instance.guid)', + `item_type` tinyint NOT NULL DEFAULT '0' COMMENT 'item or currency', `item_id` int unsigned NOT NULL DEFAULT '0' COMMENT 'loot item entry (item_instance.itemEntry)', `item_count` int NOT NULL DEFAULT '0' COMMENT 'stack size', `item_index` int unsigned NOT NULL DEFAULT '0', @@ -2714,7 +2715,7 @@ CREATE TABLE `item_loot_items` ( `rnd_bonus` int unsigned NOT NULL DEFAULT '0' COMMENT 'random bonus list added when originally rolled', `context` tinyint unsigned NOT NULL DEFAULT '0', `bonus_list_ids` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 'Space separated list of bonus list ids', - PRIMARY KEY (`container_id`,`item_id`) + PRIMARY KEY (`container_id`,`item_type`,`item_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; @@ -3434,7 +3435,8 @@ INSERT INTO `updates` VALUES ('2024_06_05_00_characters.sql','3BE1839524918827D4C0C8F9FBE9890CDF759FB4','RELEASED','2024-06-05 21:44:36',0), ('2024_06_25_00_characters.sql','046AC59E8B828B0C81A1A3C79860E464D96228B8','RELEASED','2024-06-25 17:56:15',0), ('2024_07_27_00_characters.sql','912ADCAC4948C75386387251D6FAEFB841574606','RELEASED','2024-07-27 01:03:05',0), -('2024_07_28_00_characters.sql','22C94D231C155686691780B65C12FCC2B4C3A6AA','RELEASED','2024-07-28 00:59:04',0); +('2024_07_28_00_characters.sql','22C94D231C155686691780B65C12FCC2B4C3A6AA','RELEASED','2024-07-28 00:59:04',0), +('2024_08_27_00_characters_2024_08_26_00_characters.sql','68EEBE1D639D59B24F5121008C2D103CA67FFC9A','RELEASED','2024-08-27 17:15:49',0); /*!40000 ALTER TABLE `updates` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/updates/characters/cata_classic/2024_08_27_00_characters_2024_08_26_00_characters.sql b/sql/updates/characters/cata_classic/2024_08_27_00_characters_2024_08_26_00_characters.sql new file mode 100644 index 00000000000..dedde7070ec --- /dev/null +++ b/sql/updates/characters/cata_classic/2024_08_27_00_characters_2024_08_26_00_characters.sql @@ -0,0 +1,3 @@ +ALTER TABLE `item_loot_items` ADD `item_type` tinyint NOT NULL DEFAULT 0 COMMENT 'item or currency' AFTER `container_id`; +ALTER TABLE `item_loot_items` DROP PRIMARY KEY; +ALTER TABLE `item_loot_items` ADD PRIMARY KEY (`container_id`,`item_type`,`item_id`); diff --git a/sql/updates/world/cata_classic/2024_08_27_01_world_2024_08_26_00_world.sql b/sql/updates/world/cata_classic/2024_08_27_01_world_2024_08_26_00_world.sql new file mode 100644 index 00000000000..1a466256697 --- /dev/null +++ b/sql/updates/world/cata_classic/2024_08_27_01_world_2024_08_26_00_world.sql @@ -0,0 +1,3 @@ +DELETE FROM `trinity_string` WHERE `entry`=296; +INSERT INTO `trinity_string` (`entry`,`content_default`,`content_loc1`,`content_loc2`,`content_loc3`,`content_loc4`,`content_loc5`,`content_loc6`,`content_loc7`,`content_loc8`) VALUES +(296,'├%.*s %dx |c%08x|Hcurrency:%d:%d|h[%s]|h|r (#%05d)',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 3e0c15c42af..1466894c248 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -644,10 +644,10 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_DEL_CHAR_CUF_PROFILES, "DELETE FROM character_cuf_profiles WHERE guid = ?", CONNECTION_ASYNC); // Items that hold loot or money - PrepareStatement(CHAR_SEL_ITEMCONTAINER_ITEMS, "SELECT container_id, item_id, item_count, item_index, follow_rules, ffa, blocked, counted, under_threshold, needs_quest, rnd_bonus, context, bonus_list_ids FROM item_loot_items", CONNECTION_SYNCH); + PrepareStatement(CHAR_SEL_ITEMCONTAINER_ITEMS, "SELECT container_id, item_type, item_id, item_count, item_index, follow_rules, ffa, blocked, counted, under_threshold, needs_quest, rnd_bonus, context, bonus_list_ids FROM item_loot_items", CONNECTION_SYNCH); PrepareStatement(CHAR_DEL_ITEMCONTAINER_ITEMS, "DELETE FROM item_loot_items WHERE container_id = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_DEL_ITEMCONTAINER_ITEM, "DELETE FROM item_loot_items WHERE container_id = ? AND item_id = ? AND item_count = ? AND item_index = ?", CONNECTION_ASYNC); - PrepareStatement(CHAR_INS_ITEMCONTAINER_ITEMS, "INSERT INTO item_loot_items (container_id, item_id, item_count, item_index, follow_rules, ffa, blocked, counted, under_threshold, needs_quest, rnd_bonus, context, bonus_list_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); + PrepareStatement(CHAR_DEL_ITEMCONTAINER_ITEM, "DELETE FROM item_loot_items WHERE container_id = ? AND item_type = ? AND item_id = ? AND item_count = ? AND item_index = ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_INS_ITEMCONTAINER_ITEMS, "INSERT INTO item_loot_items (container_id, item_type, item_id, item_count, item_index, follow_rules, ffa, blocked, counted, under_threshold, needs_quest, rnd_bonus, context, bonus_list_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_ITEMCONTAINER_MONEY, "SELECT container_id, money FROM item_loot_money", CONNECTION_SYNCH); PrepareStatement(CHAR_DEL_ITEMCONTAINER_MONEY, "DELETE FROM item_loot_money WHERE container_id = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_INS_ITEMCONTAINER_MONEY, "INSERT INTO item_loot_money (container_id, money) VALUES (?, ?)", CONNECTION_ASYNC); diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index b49a30f3c37..432700be071 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -15565,6 +15565,44 @@ int32 Player::GetQuestSlotObjectiveData(uint16 slot, QuestObjective const& objec return uint8((GetQuestSlotState(slot) & objective.StorageIndex) != 0); } +bool Player::HasQuestForCurrency(uint32 currencyId) const +{ + auto isCompletableObjective = [this](QuestObjectiveStatusData const& objectiveStatus) + { + Quest const* qInfo = sObjectMgr->GetQuestTemplate(objectiveStatus.QuestStatusItr->first); + QuestObjective const* objective = sObjectMgr->GetQuestObjective(objectiveStatus.ObjectiveId); + if (!qInfo || !objective || !IsQuestObjectiveCompletable(objectiveStatus.QuestStatusItr->second.Slot, qInfo, *objective)) + return false; + + // hide quest if player is in raid-group and quest is no raid quest + if (GetGroup() && GetGroup()->isRaidGroup() && !qInfo->IsAllowedInRaid(GetMap()->GetDifficultyID())) + if (!InBattleground()) //there are two ways.. we can make every bg-quest a raidquest, or add this code here.. i don't know if this can be exploited by other quests, but i think all other quests depend on a specific area.. but keep this in mind, if something strange happens later + return false; + + if (!IsQuestObjectiveComplete(objectiveStatus.QuestStatusItr->second.Slot, qInfo, *objective)) + return true; + + return false; + }; + + auto hasObjectiveTypeForCurrency = [&](QuestObjectiveType type) + { + return std::ranges::any_of(Trinity::Containers::MapEqualRange(m_questObjectiveStatus, { type, currencyId }), + isCompletableObjective, &QuestObjectiveStatusMap::value_type::second); + }; + + if (hasObjectiveTypeForCurrency(QUEST_OBJECTIVE_CURRENCY)) + return true; + + if (hasObjectiveTypeForCurrency(QUEST_OBJECTIVE_HAVE_CURRENCY)) + return true; + + if (hasObjectiveTypeForCurrency(QUEST_OBJECTIVE_OBTAIN_CURRENCY)) + return true; + + return false; +} + int32 Player::GetQuestObjectiveData(uint32 questId, uint32 objectiveId) const { uint16 slot = FindQuestSlot(questId); @@ -25462,51 +25500,62 @@ void Player::StoreLootItem(ObjectGuid lootWorldObjectGuid, uint8 lootSlot, Loot* return; } - ItemPosCountVec dest; - InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, item->itemid, item->count); - if (msg == EQUIP_ERR_OK) + switch (item->type) { - Item* newitem = StoreNewItem(dest, item->itemid, true, item->randomBonusListId, item->GetAllowedLooters(), item->context); - - if (ffaItem) + case LootItemType::Item: { - //freeforall case, notify only one player of the removal - ffaItem->is_looted = true; - SendNotifyLootItemRemoved(loot->GetGUID(), loot->GetOwnerGUID(), lootSlot); - } - else //not freeforall, notify everyone - loot->NotifyItemRemoved(lootSlot, GetMap()); + ItemPosCountVec dest; + InventoryResult msg = CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, item->itemid, item->count); + if (msg != EQUIP_ERR_OK) + { + SendEquipError(msg, nullptr, nullptr, item->itemid); + return; + } - //if only one person is supposed to loot the item, then set it to looted - if (!item->freeforall) - item->is_looted = true; + Item* newitem = StoreNewItem(dest, item->itemid, true, item->randomBonusListId, item->GetAllowedLooters(), item->context); - --loot->unlootedCount; + if (sObjectMgr->GetItemTemplate(item->itemid)) + if (newitem->GetQuality() > ITEM_QUALITY_EPIC || (newitem->GetQuality() == ITEM_QUALITY_EPIC && newitem->GetItemLevel(this) >= MinNewsItemLevel)) + if (Guild* guild = GetGuild()) + guild->AddGuildNews(GUILD_NEWS_ITEM_LOOTED, GetGUID(), 0, item->itemid); - if (sObjectMgr->GetItemTemplate(item->itemid)) - if (newitem->GetQuality() > ITEM_QUALITY_EPIC || (newitem->GetQuality() == ITEM_QUALITY_EPIC && newitem->GetItemLevel(this) >= MinNewsItemLevel)) - if (Guild* guild = GetGuild()) - guild->AddGuildNews(GUILD_NEWS_ITEM_LOOTED, GetGUID(), 0, item->itemid); + // if aeLooting then we must delay sending out item so that it appears properly stacked in chat + if (!aeResult) + { + SendNewItem(newitem, uint32(item->count), false, false, true, loot->GetDungeonEncounterId()); + UpdateCriteria(CriteriaType::LootItem, item->itemid, item->count); + UpdateCriteria(CriteriaType::GetLootByType, item->itemid, item->count, GetLootTypeForClient(loot->loot_type)); + UpdateCriteria(CriteriaType::LootAnyItem, item->itemid, item->count); + } + else + aeResult->Add(newitem, item->count, GetLootTypeForClient(loot->loot_type), loot->GetDungeonEncounterId()); - // if aeLooting then we must delay sending out item so that it appears properly stacked in chat - if (!aeResult) - { - SendNewItem(newitem, uint32(item->count), false, false, true, loot->GetDungeonEncounterId()); - UpdateCriteria(CriteriaType::LootItem, item->itemid, item->count); - UpdateCriteria(CriteriaType::GetLootByType, item->itemid, item->count, GetLootTypeForClient(loot->loot_type)); - UpdateCriteria(CriteriaType::LootAnyItem, item->itemid, item->count); + ApplyItemLootedSpell(newitem, true); + break; } - else - aeResult->Add(newitem, item->count, GetLootTypeForClient(loot->loot_type), loot->GetDungeonEncounterId()); - - // LootItem is being removed (looted) from the container, delete it from the DB. - if (loot->loot_type == LOOT_ITEM) - sLootItemStorage->RemoveStoredLootItemForContainer(lootWorldObjectGuid.GetCounter(), item->itemid, item->count, item->LootListId); + case LootItemType::Currency: + ModifyCurrency(item->itemid, item->count, CurrencyGainSource::Loot); + break; + } - ApplyItemLootedSpell(newitem, true); + if (ffaItem) + { + //freeforall case, notify only one player of the removal + ffaItem->is_looted = true; + SendNotifyLootItemRemoved(loot->GetGUID(), loot->GetOwnerGUID(), lootSlot); } - else - SendEquipError(msg, nullptr, nullptr, item->itemid); + else //not freeforall, notify everyone + loot->NotifyItemRemoved(lootSlot, GetMap()); + + //if only one person is supposed to loot the item, then set it to looted + if (!item->freeforall) + item->is_looted = true; + + --loot->unlootedCount; + + // LootItem is being removed (looted) from the container, delete it from the DB. + if (loot->loot_type == LOOT_ITEM) + sLootItemStorage->RemoveStoredLootItemForContainer(lootWorldObjectGuid.GetCounter(), item->type, item->itemid, item->count, item->LootListId); } void Player::_LoadSkills(PreparedQueryResult result) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 3e2842651f1..936756c80bf 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1643,6 +1643,7 @@ class TC_GAME_API Player final : public Unit, public GridObject<Player> bool HasQuestForItem(uint32 itemId) const; QuestObjective const* GetQuestObjectiveForItem(uint32 itemId, bool onlyIncomplete) const; bool HasQuestForGO(int32 goId) const; + bool HasQuestForCurrency(uint32 currencyId) const; void UpdateVisibleObjectInteractions(bool allUnits, bool onlySpellClicks, bool gameObjectQuestGiverStatus, bool questObjectiveGameObjects); bool CanShareQuest(uint32 questId) const; diff --git a/src/server/game/Handlers/LootHandler.cpp b/src/server/game/Handlers/LootHandler.cpp index a810b5a0b33..42174715278 100644 --- a/src/server/game/Handlers/LootHandler.cpp +++ b/src/server/game/Handlers/LootHandler.cpp @@ -441,12 +441,18 @@ void WorldSession::HandleLootMasterGiveOpcode(WorldPackets::Loot::MasterLootItem if (req.LootListID >= loot->items.size()) { + _player->SendLootError(req.Object, loot->GetOwnerGUID(), LOOT_ERROR_MASTER_OTHER); TC_LOG_DEBUG("loot", "MasterLootItem: Player {} might be using a hack! (slot {}, size {})", GetPlayer()->GetName(), req.LootListID, loot->items.size()); return; } LootItem& item = loot->items[req.LootListID]; + if (item.type != LootItemType::Item) + { + _player->SendLootError(req.Object, loot->GetOwnerGUID(), LOOT_ERROR_MASTER_OTHER); + return; + } ItemPosCountVec dest; InventoryResult msg = target->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, item.itemid, item.count); diff --git a/src/server/game/Loot/Loot.cpp b/src/server/game/Loot/Loot.cpp index 85c831eed59..ab022379792 100644 --- a/src/server/game/Loot/Loot.cpp +++ b/src/server/game/Loot/Loot.cpp @@ -17,8 +17,8 @@ #include "Loot.h" #include "Containers.h" -#include "DatabaseEnv.h" #include "DB2Stores.h" +#include "DatabaseEnv.h" #include "GameTime.h" #include "Group.h" #include "Item.h" @@ -40,26 +40,26 @@ // // Constructor, copies most fields from LootStoreItem and generates random count -LootItem::LootItem(LootStoreItem const& li) +LootItem::LootItem(LootStoreItem const& li) : itemid(li.itemid), conditions(li.conditions), needs_quest(li.needs_quest) { - itemid = li.itemid; - LootListId = 0; - conditions = li.conditions; - - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemid); - freeforall = proto && proto->HasFlag(ITEM_FLAG_MULTI_DROP); - follow_loot_rules = !li.needs_quest || (proto && proto->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES)); - - needs_quest = li.needs_quest; - - randomBonusListId = GenerateItemRandomBonusListId(itemid); - context = ItemContext::NONE; - count = 0; - is_looted = false; - is_blocked = false; - is_underthreshold = false; - is_counted = false; - rollWinnerGUID = ObjectGuid::Empty; + switch (li.type) + { + case LootStoreItem::Type::Item: + { + randomBonusListId = GenerateItemRandomBonusListId(itemid); + type = LootItemType::Item; + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemid); + freeforall = proto && proto->HasFlag(ITEM_FLAG_MULTI_DROP); + follow_loot_rules = !li.needs_quest || (proto && proto->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES)); + break; + } + case LootStoreItem::Type::Currency: + type = LootItemType::Currency; + freeforall = true; + break; + default: + break; + } } LootItem::LootItem(LootItem const&) = default; @@ -71,10 +71,35 @@ LootItem::~LootItem() = default; // Basic checks for player/item compatibility - if false no chance to see the item in the loot bool LootItem::AllowedForPlayer(Player const* player, Loot const* loot) const { - return AllowedForPlayer(player, loot, itemid, needs_quest, follow_loot_rules, false, conditions); + switch (type) + { + case LootItemType::Item: + return ItemAllowedForPlayer(player, loot, itemid, needs_quest, follow_loot_rules, false, conditions); + case LootItemType::Currency: + return CurrencyAllowedForPlayer(player, itemid, needs_quest, conditions); + default: + break; + } + return false; +} + +bool LootItem::AllowedForPlayer(Player const* player, LootStoreItem const& lootStoreItem, bool strictUsabilityCheck) +{ + switch (lootStoreItem.type) + { + case LootStoreItem::Type::Item: + return ItemAllowedForPlayer(player, nullptr, lootStoreItem.itemid, lootStoreItem.needs_quest, + !lootStoreItem.needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(lootStoreItem.itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), + strictUsabilityCheck, lootStoreItem.conditions); + case LootStoreItem::Type::Currency: + return CurrencyAllowedForPlayer(player, lootStoreItem.itemid, lootStoreItem.needs_quest, lootStoreItem.conditions); + default: + break; + } + return false; } -bool LootItem::AllowedForPlayer(Player const* player, Loot const* loot, uint32 itemid, bool needs_quest, bool follow_loot_rules, bool strictUsabilityCheck, +bool LootItem::ItemAllowedForPlayer(Player const* player, Loot const* loot, uint32 itemid, bool needs_quest, bool follow_loot_rules, bool strictUsabilityCheck, ConditionsReference const& conditions) { // DB conditions check @@ -128,14 +153,38 @@ bool LootItem::AllowedForPlayer(Player const* player, Loot const* loot, uint32 i return true; } -void LootItem::AddAllowedLooter(const Player* player) +bool LootItem::CurrencyAllowedForPlayer(Player const* player, uint32 currencyId, bool needs_quest, ConditionsReference const& conditions) +{ + // DB conditions check + if (!conditions.Meets(player)) + return false; + + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(currencyId); + if (!currency) + return false; + + // not show loot for not own team + if (currency->GetFlags().HasFlag(CurrencyTypesFlags::IsHordeOnly) && player->GetTeam() != HORDE) + return false; + + if (currency->GetFlags().HasFlag(CurrencyTypesFlags::IsAllianceOnly) && player->GetTeam() != ALLIANCE) + return false; + + // check quest requirements + if (needs_quest && !player->HasQuestForCurrency(currencyId)) + return false; + + return true; +} + +void LootItem::AddAllowedLooter(Player const* player) { allowedGUIDs.insert(player->GetGUID()); } bool LootItem::HasAllowedLooter(ObjectGuid const& looter) const { - return allowedGUIDs.find(looter) != allowedGUIDs.end(); + return allowedGUIDs.contains(looter); } Optional<LootSlotType> LootItem::GetUiTypeForPlayer(Player const* player, Loot const& loot) const @@ -143,7 +192,7 @@ Optional<LootSlotType> LootItem::GetUiTypeForPlayer(Player const* player, Loot c if (is_looted) return {}; - if (allowedGUIDs.find(player->GetGUID()) == allowedGUIDs.end()) + if (!allowedGUIDs.contains(player->GetGUID())) return {}; if (freeforall) @@ -604,7 +653,8 @@ void LootRoll::Finish(RollVoteMap::const_iterator winnerItr) { for (uint32 i = 0; i < loot.items.size(); ++i) if (LootItem* disenchantLoot = loot.LootItemInSlot(i, player)) - player->SendItemRetrievalMail(disenchantLoot->itemid, disenchantLoot->count, disenchantLoot->context); + if (disenchantLoot->type == LootItemType::Item) + player->SendItemRetrievalMail(disenchantLoot->itemid, disenchantLoot->count, disenchantLoot->context); } else m_loot->NotifyItemRemoved(m_lootItem->LootListId, m_map); @@ -791,7 +841,7 @@ bool Loot::FillLoot(uint32 lootId, LootStore const& store, Player* lootOwner, bo for (LootItem& item : items) { - if (!item.follow_loot_rules || item.freeforall) + if (!item.follow_loot_rules || item.freeforall || item.type != LootItemType::Item) continue; if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(item.itemid)) @@ -826,23 +876,40 @@ bool Loot::FillLoot(uint32 lootId, LootStore const& store, Player* lootOwner, bo // Inserts the item into the loot (called by LootTemplate processors) void Loot::AddItem(LootStoreItem const& item) { - ItemTemplate const* proto = sObjectMgr->GetItemTemplate(item.itemid); - if (!proto) - return; + switch (item.type) + { + case LootStoreItem::Type::Item: + { + ItemTemplate const* proto = sObjectMgr->GetItemTemplate(item.itemid); + if (!proto) + return; - uint32 count = urand(item.mincount, item.maxcount); - uint32 stacks = count / proto->GetMaxStackSize() + ((count % proto->GetMaxStackSize()) ? 1 : 0); + uint32 count = urand(item.mincount, item.maxcount); + uint32 stacks = count / proto->GetMaxStackSize() + ((count % proto->GetMaxStackSize()) ? 1 : 0); - for (uint32 i = 0; i < stacks && items.size() < MAX_NR_LOOT_ITEMS; ++i) - { - LootItem generatedLoot(item); - generatedLoot.context = _itemContext; - generatedLoot.count = std::min(count, proto->GetMaxStackSize()); - generatedLoot.LootListId = items.size(); - generatedLoot.BonusListIDs = ItemBonusMgr::GetBonusListsForItem(generatedLoot.itemid, _itemContext); - - items.push_back(generatedLoot); - count -= proto->GetMaxStackSize(); + for (uint32 i = 0; i < stacks && items.size() < MAX_NR_LOOT_ITEMS; ++i) + { + LootItem generatedLoot(item); + generatedLoot.context = _itemContext; + generatedLoot.count = std::min(count, proto->GetMaxStackSize()); + generatedLoot.LootListId = items.size(); + generatedLoot.BonusListIDs = ItemBonusMgr::GetBonusListsForItem(generatedLoot.itemid, _itemContext); + + items.push_back(generatedLoot); + count -= proto->GetMaxStackSize(); + } + break; + } + case LootStoreItem::Type::Currency: + { + LootItem generatedLoot(item); + generatedLoot.count = urand(item.mincount, item.maxcount); + generatedLoot.LootListId = items.size(); + items.push_back(generatedLoot); + break; + } + default: + break; } } @@ -867,17 +934,34 @@ bool Loot::AutoStore(Player* player, uint8 bag, uint8 slot, bool broadcast, bool if (!lootItem->rollWinnerGUID.IsEmpty() && lootItem->rollWinnerGUID != GetGUID()) continue; - ItemPosCountVec dest; - InventoryResult msg = player->CanStoreNewItem(bag, slot, dest, lootItem->itemid, lootItem->count); - if (msg != EQUIP_ERR_OK && slot != NULL_SLOT) - msg = player->CanStoreNewItem(bag, NULL_SLOT, dest, lootItem->itemid, lootItem->count); - if (msg != EQUIP_ERR_OK && bag != NULL_BAG) - msg = player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, lootItem->itemid, lootItem->count); - if (msg != EQUIP_ERR_OK) + switch (lootItem->type) { - player->SendEquipError(msg, nullptr, nullptr, lootItem->itemid); - allLooted = false; - continue; + case LootItemType::Item: + { + ItemPosCountVec dest; + InventoryResult msg = player->CanStoreNewItem(bag, slot, dest, lootItem->itemid, lootItem->count); + if (msg != EQUIP_ERR_OK && slot != NULL_SLOT) + msg = player->CanStoreNewItem(bag, NULL_SLOT, dest, lootItem->itemid, lootItem->count); + if (msg != EQUIP_ERR_OK && bag != NULL_BAG) + msg = player->CanStoreNewItem(NULL_BAG, NULL_SLOT, dest, lootItem->itemid, lootItem->count); + if (msg != EQUIP_ERR_OK) + { + player->SendEquipError(msg, nullptr, nullptr, lootItem->itemid); + allLooted = false; + continue; + } + + if (Item* pItem = player->StoreNewItem(dest, lootItem->itemid, true, lootItem->randomBonusListId, GuidSet(), lootItem->context, &lootItem->BonusListIDs)) + { + player->SendNewItem(pItem, lootItem->count, false, createdByPlayer, broadcast, GetDungeonEncounterId()); + player->ApplyItemLootedSpell(pItem, true); + } + + break; + } + case LootItemType::Currency: + player->ModifyCurrency(lootItem->itemid, lootItem->count, CurrencyGainSource::Loot); + break; } if (ffaitem) @@ -887,10 +971,6 @@ bool Loot::AutoStore(Player* player, uint8 bag, uint8 slot, bool broadcast, bool lootItem->is_looted = true; --unlootedCount; - - Item* pItem = player->StoreNewItem(dest, lootItem->itemid, true, lootItem->randomBonusListId, GuidSet(), lootItem->context, &lootItem->BonusListIDs); - player->SendNewItem(pItem, lootItem->count, false, createdByPlayer, broadcast); - player->ApplyItemLootedSpell(pItem, true); } return allLooted; @@ -962,18 +1042,12 @@ bool Loot::hasItemFor(Player const* player) const { // quest items for (LootItem const& lootItem : items) - if (!lootItem.is_looted && !lootItem.follow_loot_rules && lootItem.GetAllowedLooters().find(player->GetGUID()) != lootItem.GetAllowedLooters().end()) + if (!lootItem.is_looted && !lootItem.follow_loot_rules && lootItem.GetAllowedLooters().contains(player->GetGUID())) return true; if (NotNormalLootItemList const* ffaItems = Trinity::Containers::MapGetValuePtr(GetPlayerFFAItems(), player->GetGUID())) - { - bool hasFfaItem = std::ranges::any_of(*ffaItems, [&](NotNormalLootItem const& ffaItem) - { - return !ffaItem.is_looted; - }); - if (hasFfaItem) + if (std::ranges::any_of(*ffaItems, std::identity(), &NotNormalLootItem::is_looted)) return true; - } return false; } @@ -1000,11 +1074,33 @@ void Loot::BuildLootResponse(WorldPackets::Loot::LootResponse& packet, Player co if (!uiType) continue; - WorldPackets::Loot::LootItemData& lootItem = packet.Items.emplace_back(); - lootItem.LootListID = item.LootListId; - lootItem.UIType = *uiType; - lootItem.Quantity = item.count; - lootItem.Loot.Initialize(item); + switch (item.type) + { + case LootItemType::Item: + { + WorldPackets::Loot::LootItemData& lootItem = packet.Items.emplace_back(); + lootItem.LootListID = item.LootListId; + lootItem.UIType = *uiType; + lootItem.Quantity = item.count; + lootItem.Loot.Initialize(item); + break; + } + case LootItemType::Currency: + { + WorldPackets::Loot::LootCurrency& lootCurrency = packet.Currencies.emplace_back(); + lootCurrency.CurrencyID = item.itemid; + lootCurrency.Quantity = item.count; + lootCurrency.LootListID = item.LootListId; + lootCurrency.UIType = *uiType; + + // fake visible quantity for SPELL_AURA_MOD_CURRENCY_CATEGORY_GAIN_PCT - handled in Player::ModifyCurrency + lootCurrency.Quantity = float(lootCurrency.Quantity) * viewer->GetTotalAuraMultiplierByMiscValue(SPELL_AURA_MOD_CURRENCY_CATEGORY_GAIN_PCT, sCurrencyTypesStore.AssertEntry(item.itemid)->CategoryID); + break; + } + default: + break; + } + } } diff --git a/src/server/game/Loot/Loot.h b/src/server/game/Loot/Loot.h index 7534b23c672..3b5cbca7fb5 100644 --- a/src/server/game/Loot/Loot.h +++ b/src/server/game/Loot/Loot.h @@ -23,6 +23,7 @@ #include "Define.h" #include "Duration.h" #include "ItemEnchantmentMgr.h" +#include "LootItemType.h" #include "ObjectGuid.h" #include "Optional.h" #include "SharedDefines.h" @@ -174,30 +175,30 @@ enum class LootRollIneligibilityReason : uint32 struct TC_GAME_API LootItem { - uint32 itemid; - uint32 LootListId; - ItemRandomBonusListId randomBonusListId; + uint32 itemid = 0; + uint32 LootListId = 0; + ItemRandomBonusListId randomBonusListId = 0; std::vector<int32> BonusListIDs; - ItemContext context; + ItemContext context = ItemContext::NONE; ConditionsReference conditions; // additional loot condition GuidSet allowedGUIDs; ObjectGuid rollWinnerGUID; // Stores the guid of person who won loot, if his bags are full only he can see the item in loot list! - uint8 count : 8; - bool is_looted : 1; - bool is_blocked : 1; - bool freeforall : 1; // free for all - bool is_underthreshold : 1; - bool is_counted : 1; - bool needs_quest : 1; // quest drop - bool follow_loot_rules : 1; + uint32 count = 0; + LootItemType type = LootItemType::Item; + bool is_looted : 1 = false; + bool is_blocked : 1 = false; + bool freeforall : 1 = false; // free for all + bool is_underthreshold : 1 = false; + bool is_counted : 1 = false; + bool needs_quest : 1 = false; // quest drop + bool follow_loot_rules : 1 = false; // Constructor, copies most fields from LootStoreItem, generates random count and random suffixes/properties - // Should be called for non-reference LootStoreItem entries only (reference = 0) + // Should be called for non-reference LootStoreItem entries only explicit LootItem(LootStoreItem const& li); // Empty constructor for creating an empty LootItem to be filled in with DB data - LootItem() : itemid(0), LootListId(0), randomBonusListId(0), context(ItemContext::NONE), count(0), is_looted(false), is_blocked(false), - freeforall(false), is_underthreshold(false), is_counted(false), needs_quest(false), follow_loot_rules(false) { } + LootItem() = default; LootItem(LootItem const&); LootItem(LootItem&&) noexcept; @@ -207,8 +208,10 @@ struct TC_GAME_API LootItem // Basic checks for player/item compatibility - if false no chance to see the item in the loot - used only for loot generation bool AllowedForPlayer(Player const* player, Loot const* loot) const; - static bool AllowedForPlayer(Player const* player, Loot const* loot, uint32 itemid, bool needs_quest, bool follow_loot_rules, bool strictUsabilityCheck, + static bool AllowedForPlayer(Player const* player, LootStoreItem const& lootStoreItem, bool strictUsabilityCheck); + static bool ItemAllowedForPlayer(Player const* player, Loot const* loot, uint32 itemid, bool needs_quest, bool follow_loot_rules, bool strictUsabilityCheck, ConditionsReference const& conditions); + static bool CurrencyAllowedForPlayer(Player const* player, uint32 currencyId, bool needs_quest, ConditionsReference const& conditions); void AddAllowedLooter(Player const* player); GuidSet const& GetAllowedLooters() const { return allowedGUIDs; } bool HasAllowedLooter(ObjectGuid const& looter) const; diff --git a/src/server/game/Loot/LootItemStorage.cpp b/src/server/game/Loot/LootItemStorage.cpp index 9dc8cae1de6..c0362ea093b 100644 --- a/src/server/game/Loot/LootItemStorage.cpp +++ b/src/server/game/Loot/LootItemStorage.cpp @@ -25,6 +25,7 @@ #include "ObjectMgr.h" #include "Player.h" #include "StringConvert.h" +#include "Util.h" #include <sstream> #include <unordered_map> @@ -70,18 +71,19 @@ void LootItemStorage::LoadStorageFromDB() StoredLootContainer& storedContainer = _lootItemStore.try_emplace(key, key).first->second; LootItem lootItem; - lootItem.itemid = fields[1].GetUInt32(); - lootItem.count = fields[2].GetUInt32(); - lootItem.LootListId = fields[3].GetUInt32(); - lootItem.follow_loot_rules = fields[4].GetBool(); - lootItem.freeforall = fields[5].GetBool(); - lootItem.is_blocked = fields[6].GetBool(); - lootItem.is_counted = fields[7].GetBool(); - lootItem.is_underthreshold = fields[8].GetBool(); - lootItem.needs_quest = fields[9].GetBool(); - lootItem.randomBonusListId = fields[10].GetUInt32(); - lootItem.context = ItemContext(fields[11].GetUInt8()); - for (std::string_view bonusList : Trinity::Tokenize(fields[12].GetStringView(), ' ', false)) + lootItem.type = static_cast<LootItemType>(fields[1].GetInt8()); + lootItem.itemid = fields[2].GetUInt32(); + lootItem.count = fields[3].GetUInt32(); + lootItem.LootListId = fields[4].GetUInt32(); + lootItem.follow_loot_rules = fields[5].GetBool(); + lootItem.freeforall = fields[6].GetBool(); + lootItem.is_blocked = fields[7].GetBool(); + lootItem.is_counted = fields[8].GetBool(); + lootItem.is_underthreshold = fields[9].GetBool(); + lootItem.needs_quest = fields[10].GetBool(); + lootItem.randomBonusListId = fields[11].GetUInt32(); + lootItem.context = ItemContext(fields[12].GetUInt8()); + for (std::string_view bonusList : Trinity::Tokenize(fields[13].GetStringView(), ' ', false)) if (Optional<int32> bonusListID = Trinity::StringTo<int32>(bonusList)) lootItem.BonusListIDs.push_back(*bonusListID); @@ -226,7 +228,7 @@ void LootItemStorage::RemoveStoredLootForContainer(uint64 containerId) CharacterDatabase.CommitTransaction(trans); } -void LootItemStorage::RemoveStoredLootItemForContainer(uint64 containerId, uint32 itemId, uint32 count, uint32 itemIndex) +void LootItemStorage::RemoveStoredLootItemForContainer(uint64 containerId, LootItemType type, uint32 itemId, uint32 count, uint32 itemIndex) { // write std::unique_lock<std::shared_mutex> lock(*GetLock()); @@ -235,7 +237,7 @@ void LootItemStorage::RemoveStoredLootItemForContainer(uint64 containerId, uint3 if (itr == _lootItemStore.end()) return; - itr->second.RemoveItem(itemId, count, itemIndex); + itr->second.RemoveItem(type, itemId, count, itemIndex); } void LootItemStorage::AddNewStoredLoot(uint64 containerId, Loot* loot, Player* player) @@ -301,23 +303,24 @@ void StoredLootContainer::AddLootItem(LootItem const& lootItem, CharacterDatabas CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_ITEMCONTAINER_ITEMS); - // container_id, item_id, item_count, item_index, follow_rules, ffa, blocked, counted, under_threshold, needs_quest, rnd_prop, rnd_suffix + // container_id, item_type, item_id, item_count, item_index, follow_rules, ffa, blocked, counted, under_threshold, needs_quest, rnd_prop, rnd_suffix stmt->setUInt64(0, _containerId); - stmt->setUInt32(1, lootItem.itemid); - stmt->setUInt32(2, lootItem.count); - stmt->setUInt32(3, lootItem.LootListId); - stmt->setBool(4, lootItem.follow_loot_rules); - stmt->setBool(5, lootItem.freeforall); - stmt->setBool(6, lootItem.is_blocked); - stmt->setBool(7, lootItem.is_counted); - stmt->setBool(8, lootItem.is_underthreshold); - stmt->setBool(9, lootItem.needs_quest); - stmt->setInt32(10, lootItem.randomBonusListId); - stmt->setUInt8(11, AsUnderlyingType(lootItem.context)); + stmt->setInt8(1, AsUnderlyingType(lootItem.type)); + stmt->setUInt32(2, lootItem.itemid); + stmt->setUInt32(3, lootItem.count); + stmt->setUInt32(4, lootItem.LootListId); + stmt->setBool(5, lootItem.follow_loot_rules); + stmt->setBool(6, lootItem.freeforall); + stmt->setBool(7, lootItem.is_blocked); + stmt->setBool(8, lootItem.is_counted); + stmt->setBool(9, lootItem.is_underthreshold); + stmt->setBool(10, lootItem.needs_quest); + stmt->setInt32(11, lootItem.randomBonusListId); + stmt->setUInt8(13, AsUnderlyingType(lootItem.context)); std::ostringstream bonusListIDs; for (int32 bonusListID : lootItem.BonusListIDs) bonusListIDs << bonusListID << ' '; - stmt->setString(12, bonusListIDs.str()); + stmt->setString(13, bonusListIDs.str()); trans->Append(stmt); } @@ -346,7 +349,7 @@ void StoredLootContainer::RemoveMoney() CharacterDatabase.Execute(stmt); } -void StoredLootContainer::RemoveItem(uint32 itemId, uint32 count, uint32 itemIndex) +void StoredLootContainer::RemoveItem(LootItemType type, uint32 itemId, uint32 count, uint32 itemIndex) { auto bounds = _lootItems.equal_range(itemId); for (auto itr = bounds.first; itr != bounds.second; ++itr) @@ -361,8 +364,9 @@ void StoredLootContainer::RemoveItem(uint32 itemId, uint32 count, uint32 itemInd // Deletes a single item associated with an openable item from the DB CharacterDatabasePreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_DEL_ITEMCONTAINER_ITEM); stmt->setUInt64(0, _containerId); - stmt->setUInt32(1, itemId); - stmt->setUInt32(2, count); - stmt->setUInt32(3, itemIndex); + stmt->setInt8(1, AsUnderlyingType(type)); + stmt->setUInt32(2, itemId); + stmt->setUInt32(3, count); + stmt->setUInt32(4, itemIndex); CharacterDatabase.Execute(stmt); } diff --git a/src/server/game/Loot/LootItemStorage.h b/src/server/game/Loot/LootItemStorage.h index 3a0c635e96b..8df0a0c6a34 100644 --- a/src/server/game/Loot/LootItemStorage.h +++ b/src/server/game/Loot/LootItemStorage.h @@ -22,6 +22,7 @@ #include "DatabaseEnvFwd.h" #include "DBCEnums.h" #include "ItemEnchantmentMgr.h" +#include "LootItemType.h" #include <shared_mutex> #include <unordered_map> @@ -61,7 +62,7 @@ class StoredLootContainer void AddMoney(uint32 money, CharacterDatabaseTransaction trans); void RemoveMoney(); - void RemoveItem(uint32 itemId, uint32 count, uint32 itemIndex); + void RemoveItem(LootItemType type, uint32 itemId, uint32 count, uint32 itemIndex); uint64 GetContainer() const { return _containerId; } uint32 GetMoney() const { return _money; } @@ -83,7 +84,7 @@ class LootItemStorage bool LoadStoredLoot(Item* item, Player* player); void RemoveStoredMoneyForContainer(uint64 containerId); void RemoveStoredLootForContainer(uint64 containerId); - void RemoveStoredLootItemForContainer(uint64 containerId, uint32 itemId, uint32 count, uint32 itemIndex); + void RemoveStoredLootItemForContainer(uint64 containerId, LootItemType type, uint32 itemId, uint32 count, uint32 itemIndex); void AddNewStoredLoot(uint64 containerId, Loot* loot, Player* player); private: diff --git a/src/server/game/Loot/LootMgr.cpp b/src/server/game/Loot/LootMgr.cpp index de99abab57e..2104cc0c5ab 100644 --- a/src/server/game/Loot/LootMgr.cpp +++ b/src/server/game/Loot/LootMgr.cpp @@ -66,9 +66,7 @@ struct LootGroupInvalidSelector if (!(item->lootmode & _lootMode)) return true; - if (_personalLooter && !LootItem::AllowedForPlayer(_personalLooter, nullptr, item->itemid, item->needs_quest, - !item->needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(item->itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), - true, item->conditions)) + if (_personalLooter && !LootItem::AllowedForPlayer(_personalLooter, *item, true)) return true; return false; @@ -265,6 +263,14 @@ bool LootStoreItem::Roll(bool rate) const } case Type::Reference: return roll_chance_f(chance * (rate ? sWorld->getRate(RATE_DROP_ITEM_REFERENCED) : 1.0f)); + case Type::Currency: + { + CurrencyTypesEntry const* currency = sCurrencyTypesStore.AssertEntry(itemid); + + float qualityModifier = currency && rate && QualityToRate[currency->Quality] != MAX_RATES ? sWorld->getRate(QualityToRate[currency->Quality]) : 1.0f; + + return roll_chance_f(chance * qualityModifier); + } default: break; } @@ -327,6 +333,38 @@ bool LootStoreItem::IsValid(LootStore const& store, uint32 entry) const return false; } break; + case Type::Currency: + { + if (!sCurrencyTypesStore.HasRecord(itemid)) + { + TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: currency does not exist - skipped", + store.GetName(), entry, type, itemid); + return false; + } + + if (chance == 0 && groupid == 0) // Zero chance is allowed for grouped entries only + { + TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: equal-chanced grouped entry, but group not defined - skipped", + store.GetName(), entry, type, itemid); + return false; + } + + if (chance != 0 && chance < 0.0001f) // loot with low chance + { + TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: low chance ({}) - skipped", + store.GetName(), entry, type, itemid, chance); + return false; + } + + if (maxcount < mincount) // wrong max count + { + TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: MaxCount ({}) less that MinCount ({}) - skipped", + store.GetName(), entry, type, itemid, int32(maxcount), mincount); + return false; + } + break; + } + default: TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} Item {}: invalid ItemType {}, skipped", store.GetName(), entry, itemid, type); @@ -387,15 +425,11 @@ LootStoreItem const* LootTemplate::LootGroup::Roll(uint16 lootMode, Player const bool LootTemplate::LootGroup::HasDropForPlayer(Player const* player, bool strictUsabilityCheck) const { for (std::unique_ptr<LootStoreItem> const& lootStoreItem : ExplicitlyChanced) - if (LootItem::AllowedForPlayer(player, nullptr, lootStoreItem->itemid, lootStoreItem->needs_quest, - !lootStoreItem->needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(lootStoreItem->itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), - strictUsabilityCheck, lootStoreItem->conditions)) + if (LootItem::AllowedForPlayer(player, *lootStoreItem, strictUsabilityCheck)) return true; for (std::unique_ptr<LootStoreItem> const& lootStoreItem : EqualChanced) - if (LootItem::AllowedForPlayer(player, nullptr, lootStoreItem->itemid, lootStoreItem->needs_quest, - !lootStoreItem->needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(lootStoreItem->itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), - strictUsabilityCheck, lootStoreItem->conditions)) + if (LootItem::AllowedForPlayer(player, *lootStoreItem, strictUsabilityCheck)) return true; return false; @@ -416,10 +450,24 @@ bool LootTemplate::LootGroup::HasQuestDrop() const // True if group includes at least 1 quest drop entry for active quests of the player bool LootTemplate::LootGroup::HasQuestDropForPlayer(Player const* player) const { - if (std::ranges::any_of(ExplicitlyChanced, [player](uint32 itemId) { return player->HasQuestForItem(itemId); }, &LootStoreItem::itemid)) + auto hasQuestForLootItem = [player](std::unique_ptr<LootStoreItem> const& item) + { + switch (item->type) + { + case LootStoreItem::Type::Item: + return player->HasQuestForItem(item->itemid); + case LootStoreItem::Type::Currency: + return player->HasQuestForCurrency(item->itemid); + default: + break; + } + return false; + }; + + if (std::ranges::any_of(ExplicitlyChanced, hasQuestForLootItem)) return true; - if (std::ranges::any_of(EqualChanced, [player](uint32 itemId) { return player->HasQuestForItem(itemId); }, &LootStoreItem::itemid)) + if (std::ranges::any_of(EqualChanced, hasQuestForLootItem)) return true; return false; @@ -519,6 +567,22 @@ void LootTemplate::CopyConditions(LootItem* li) const // Copies the conditions list from a template item to a LootItemData for (std::unique_ptr<LootStoreItem> const& item : Entries) { + switch (item->type) + { + case LootStoreItem::Type::Item: + if (li->type != LootItemType::Item) + continue; + break; + case LootStoreItem::Type::Reference: + continue; + case LootStoreItem::Type::Currency: + if (li->type != LootItemType::Currency) + continue; + break; + default: + break; + } + if (item->itemid != li->itemid) continue; @@ -554,12 +618,10 @@ void LootTemplate::Process(Loot& loot, bool rate, uint16 lootMode, uint8 groupId switch (item->type) { case LootStoreItem::Type::Item: + case LootStoreItem::Type::Currency: // Plain entries (not a reference, not grouped) // Chance is already checked, just add - if (!personalLooter - || LootItem::AllowedForPlayer(personalLooter, nullptr, item->itemid, item->needs_quest, - !item->needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(item->itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), - true, item->conditions)) + if (!personalLooter || LootItem::AllowedForPlayer(personalLooter, *item, true)) loot.AddItem(*item); break; @@ -616,9 +678,7 @@ void LootTemplate::ProcessPersonalLoot(std::unordered_map<Player*, std::unique_p // Chance is already checked, just add std::vector<Player*> lootersForItem = getLootersForItem([&](Player const* looter) { - return LootItem::AllowedForPlayer(looter, nullptr, item->itemid, item->needs_quest, - !item->needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(item->itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), - true, item->conditions); + return LootItem::AllowedForPlayer(looter, *item, true); }); if (!lootersForItem.empty()) @@ -668,6 +728,19 @@ void LootTemplate::ProcessPersonalLoot(std::unordered_map<Player*, std::unique_p break; } + case LootStoreItem::Type::Currency: + { + // Plain entries (not a reference, not grouped) + // Chance is already checked, just add + std::vector<Player*> lootersForItem = getLootersForItem([&](Player const* looter) + { + return LootItem::AllowedForPlayer(looter, *item, true); + }); + + for (Player* looter : lootersForItem) + personalLoot[looter]->AddItem(*item); + break; + } default: break; } @@ -712,9 +785,8 @@ bool LootTemplate::HasDropForPlayer(Player const* player, uint8 groupId, bool st switch (lootStoreItem->type) { case LootStoreItem::Type::Item: - if (LootItem::AllowedForPlayer(player, nullptr, lootStoreItem->itemid, lootStoreItem->needs_quest, - !lootStoreItem->needs_quest || ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(lootStoreItem->itemid))->HasFlag(ITEM_FLAGS_CU_FOLLOW_LOOT_RULES), - strictUsabilityCheck, lootStoreItem->conditions)) + case LootStoreItem::Type::Currency: + if (LootItem::AllowedForPlayer(player, *lootStoreItem, strictUsabilityCheck)) return true; // active quest drop found break; case LootStoreItem::Type::Reference: @@ -758,6 +830,7 @@ bool LootTemplate::HasQuestDrop(LootTemplateMap const& store, uint8 groupId) con switch (item->type) { case LootStoreItem::Type::Item: + case LootStoreItem::Type::Currency: if (item->needs_quest) return true; // quest drop found break; @@ -815,6 +888,10 @@ bool LootTemplate::HasQuestDropForPlayer(LootTemplateMap const& store, Player co return true; break; } + case LootStoreItem::Type::Currency: + if (player->HasQuestForCurrency(item->itemid)) + return true; // active quest drop found + break; default: break; } diff --git a/src/server/game/Loot/LootMgr.h b/src/server/game/Loot/LootMgr.h index 1237347714f..92501e53ef5 100644 --- a/src/server/game/Loot/LootMgr.h +++ b/src/server/game/Loot/LootMgr.h @@ -41,6 +41,7 @@ struct TC_GAME_API LootStoreItem { Item = 0, Reference = 1, + Currency = 2, }; uint32 itemid; // id of the item diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index 02a354db4e0..0693d1e9200 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -344,7 +344,7 @@ enum TrinityStrings LANG_COMMAND_NPC_SHOWLOOT_LABEL_2 = 293, LANG_COMMAND_NPC_SHOWLOOT_SUBLABEL = 294, LANG_COMMAND_NPC_SHOWLOOT_ENTRY_2 = 295, - // 296 free + LANG_COMMAND_NPC_SHOWLOOT_CURRENCY = 296, // END LANG_COMMAND_WANDER_DISTANCE = 297, diff --git a/src/server/scripts/Commands/cs_modify.cpp b/src/server/scripts/Commands/cs_modify.cpp index e02e67a1d7c..7f570f938b6 100644 --- a/src/server/scripts/Commands/cs_modify.cpp +++ b/src/server/scripts/Commands/cs_modify.cpp @@ -39,6 +39,8 @@ EndScriptData */ #include "Util.h" #include "WorldSession.h" +using namespace Trinity::ChatCommands; + #if TRINITY_COMPILER == TRINITY_COMPILER_GNU #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif @@ -923,12 +925,9 @@ public: return true; } - static bool HandleModifyCurrencyCommand(ChatHandler* handler, const char* args) + static bool HandleModifyCurrencyCommand(ChatHandler* handler, CurrencyTypesEntry const* currency, int32 amount) { - if (!*args) - return false; - - Player* target = handler->getSelectedPlayer(); + Player* target = handler->getSelectedPlayerOrSelf(); if (!target) { handler->PSendSysMessage(LANG_PLAYER_NOT_FOUND); @@ -936,16 +935,7 @@ public: return false; } - uint32 currencyId = atoi(strtok((char*)args, " ")); - const CurrencyTypesEntry* currencyType = sCurrencyTypesStore.LookupEntry(currencyId); - if (!currencyType) - return false; - - uint32 amount = atoi(strtok(nullptr, " ")); - if (!amount) - return false; - - target->ModifyCurrency(currencyId, amount, CurrencyGainSource::Cheat, CurrencyDestroyReason::Cheat); + target->ModifyCurrency(currency->ID, amount, CurrencyGainSource::Cheat, CurrencyDestroyReason::Cheat); return true; } diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 81de26149c2..d14f88621c1 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -23,12 +23,13 @@ Category: commandscripts EndScriptData */ #include "ScriptMgr.h" +#include "CharacterCache.h" #include "Chat.h" #include "ChatCommand.h" #include "CreatureAI.h" #include "CreatureGroups.h" -#include "DatabaseEnv.h" #include "DB2Stores.h" +#include "DatabaseEnv.h" #include "FollowMovementGenerator.h" #include "GameTime.h" #include "Language.h" @@ -1166,7 +1167,7 @@ public: return true; } - static void _ShowLootEntry(ChatHandler* handler, uint32 itemId, uint8 itemCount, bool alternateString = false) + static void _ShowLootEntry(ChatHandler* handler, uint32 itemId, uint32 itemCount, bool alternateString = false) { ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(itemId); char const* name = nullptr; @@ -1178,6 +1179,18 @@ public: itemCount, ItemQualityColors[itemTemplate ? static_cast<ItemQualities>(itemTemplate->GetQuality()) : ITEM_QUALITY_POOR], itemId, name, itemId); } + static void _ShowLootCurrencyEntry(ChatHandler* handler, uint32 currencyId, uint32 count, bool alternateString = false) + { + CurrencyTypesEntry const* currency = sCurrencyTypesStore.LookupEntry(currencyId); + char const* name = nullptr; + if (currency) + name = currency->Name[handler->GetSessionDbcLocale()]; + if (!name) + name = "Unknown currency"; + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_CURRENCY, alternateString ? 6 : 3 /*number of bytes from following string*/, "\u2500\u2500", + count, ItemQualityColors[currency ? static_cast<ItemQualities>(currency->Quality) : ITEM_QUALITY_POOR], currencyId, count, name, currencyId); + } + static void _IterateNotNormalLootMap(ChatHandler* handler, NotNormalLootItemMap const& map, std::vector<LootItem> const& items) { for (NotNormalLootItemMap::value_type const& pair : map) @@ -1190,11 +1203,72 @@ public: for (auto it = pair.second->cbegin(); it != pair.second->cend(); ++it) { LootItem const& item = items[it->LootListId]; - if (!(it->is_looted) && !item.is_looted) - _ShowLootEntry(handler, item.itemid, item.count, true); + if (!it->is_looted && !item.is_looted) + { + switch (item.type) + { + case LootItemType::Item: + _ShowLootEntry(handler, item.itemid, item.count, true); + break; + case LootItemType::Currency: + _ShowLootCurrencyEntry(handler, item.itemid, item.count, true); + break; + } + } + } + } + } + + static void _ShowLootContents(ChatHandler* handler, bool all, Loot const* loot) + { + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_MONEY, loot->gold / GOLD, (loot->gold % GOLD) / SILVER, loot->gold % SILVER); + + if (!all) + { + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL, "Standard items", loot->items.size()); + for (LootItem const& item : loot->items) + { + if (!item.is_looted) + { + switch (item.type) + { + case LootItemType::Item: + _ShowLootEntry(handler, item.itemid, item.count); + break; + case LootItemType::Currency: + _ShowLootCurrencyEntry(handler, item.itemid, item.count); + break; + } + } + } + } + else + { + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL, "Standard items", loot->items.size()); + for (LootItem const& item : loot->items) + { + if (!item.is_looted && !item.freeforall && item.conditions.IsEmpty()) + { + switch (item.type) + { + case LootItemType::Item: + _ShowLootEntry(handler, item.itemid, item.count); + break; + case LootItemType::Currency: + _ShowLootCurrencyEntry(handler, item.itemid, item.count); + break; + } + } + } + + if (!loot->GetPlayerFFAItems().empty()) + { + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL_2, "FFA items per allowed player"); + _IterateNotNormalLootMap(handler, loot->GetPlayerFFAItems(), loot->items); } } } + static bool HandleNpcShowLootCommand(ChatHandler* handler, Optional<EXACT_SEQUENCE("all")> all) { Creature* creatureTarget = handler->getSelectedCreature(); @@ -1205,8 +1279,16 @@ public: return false; } + if (!creatureTarget->isDead()) + { + handler->PSendSysMessage(LANG_COMMAND_NOT_DEAD_OR_NO_LOOT, creatureTarget->GetName().c_str()); + handler->SetSentErrorMessage(true); + return false; + } + Loot const* loot = creatureTarget->m_loot.get(); - if (!creatureTarget->isDead() || !loot || loot->isLooted()) + if ((!loot || loot->isLooted()) + && !std::ranges::count_if(creatureTarget->m_personalLoot, std::not_fn(&Loot::isLooted), &std::unordered_map<ObjectGuid, std::unique_ptr<Loot>>::value_type::second)) { handler->PSendSysMessage(LANG_COMMAND_NOT_DEAD_OR_NO_LOOT, creatureTarget->GetName().c_str()); handler->SetSentErrorMessage(true); @@ -1214,26 +1296,18 @@ public: } handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_HEADER, creatureTarget->GetName().c_str(), creatureTarget->GetEntry()); - handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_MONEY, loot->gold / GOLD, (loot->gold % GOLD) / SILVER, loot->gold % SILVER); - if (!all) - { - handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL, "Standard items", loot->items.size()); - for (LootItem const& item : loot->items) - if (!item.is_looted) - _ShowLootEntry(handler, item.itemid, item.count); - } + if (loot) + _ShowLootContents(handler, all.has_value(), loot); else { - handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL, "Standard items", loot->items.size()); - for (LootItem const& item : loot->items) - if (!item.is_looted && !item.freeforall && item.conditions.IsEmpty()) - _ShowLootEntry(handler, item.itemid, item.count); + using namespace std::string_view_literals; - if (!loot->GetPlayerFFAItems().empty()) + for (auto const& [lootOwner, personalLoot] : creatureTarget->m_personalLoot) { - handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL_2, "FFA items per allowed player"); - _IterateNotNormalLootMap(handler, loot->GetPlayerFFAItems(), loot->items); + CharacterCacheEntry const* character = sCharacterCache->GetCharacterCacheByGuid(lootOwner); + handler->PSendSysMessage(LANG_COMMAND_NPC_SHOWLOOT_LABEL_2, Trinity::StringFormat("Personal loot for {}", character ? character->Name : ""sv)); + _ShowLootContents(handler, all.has_value(), personalLoot.get()); } } |