/* * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "LootMgr.h" #include "Containers.h" #include "DB2Stores.h" #include "DatabaseEnv.h" #include "ItemBonusMgr.h" #include "ItemTemplate.h" #include "Log.h" #include "Loot.h" #include "MapUtils.h" #include "ObjectMgr.h" #include "Player.h" #include "Random.h" #include "SpellInfo.h" #include "SpellMgr.h" #include "World.h" static constexpr Rates QualityToRate[MAX_ITEM_QUALITY] = { RATE_DROP_ITEM_POOR, // ITEM_QUALITY_POOR RATE_DROP_ITEM_NORMAL, // ITEM_QUALITY_NORMAL RATE_DROP_ITEM_UNCOMMON, // ITEM_QUALITY_UNCOMMON RATE_DROP_ITEM_RARE, // ITEM_QUALITY_RARE RATE_DROP_ITEM_EPIC, // ITEM_QUALITY_EPIC RATE_DROP_ITEM_LEGENDARY, // ITEM_QUALITY_LEGENDARY RATE_DROP_ITEM_ARTIFACT, // ITEM_QUALITY_ARTIFACT, MAX_RATES, // ITEM_QUALITY_HEIRLOOM MAX_RATES, // ITEM_QUALITY_WOW_TOKEN }; LootStore LootTemplates_Creature("creature_loot_template", "creature entry", true); LootStore LootTemplates_Disenchant("disenchant_loot_template", "item disenchant id", true); LootStore LootTemplates_Fishing("fishing_loot_template", "area id", true); LootStore LootTemplates_Gameobject("gameobject_loot_template", "gameobject entry", true); LootStore LootTemplates_Item("item_loot_template", "item entry", true); LootStore LootTemplates_Mail("mail_loot_template", "mail template id", false); LootStore LootTemplates_Milling("milling_loot_template", "item entry (herb)", true); LootStore LootTemplates_Pickpocketing("pickpocketing_loot_template", "creature pickpocket lootid", true); LootStore LootTemplates_Prospecting("prospecting_loot_template", "item entry (ore)", true); LootStore LootTemplates_Reference("reference_loot_template", "reference id", false); LootStore LootTemplates_Skinning("skinning_loot_template", "creature skinning id", true); LootStore LootTemplates_Spell("spell_loot_template", "spell id (random item creating)", false); // Selects invalid loot items to be removed from group possible entries (before rolling) struct LootGroupInvalidSelector { explicit LootGroupInvalidSelector(uint16 lootMode, Player const* personalLooter) : _lootMode(lootMode), _personalLooter(personalLooter) { } bool operator()(LootStoreItem const* item) const { if (!(item->lootmode & _lootMode)) return true; if (_personalLooter && !LootItem::AllowedForPlayer(_personalLooter, *item, true)) return true; return false; } private: uint16 _lootMode; Player const* _personalLooter; }; class LootTemplate::LootGroup // A set of loot definitions for items (refs are not allowed) { public: LootGroup() = default; LootGroup(LootGroup const&) = delete; LootGroup(LootGroup&&) = delete; LootGroup& operator=(LootGroup const&) = delete; LootGroup& operator=(LootGroup&&) = delete; ~LootGroup() = default; void AddEntry(LootStoreItem* item); // Adds an entry to the group (at loading stage) bool HasDropForPlayer(Player const* player, bool strictUsabilityCheck) const; bool HasQuestDrop() const; // True if group includes at least 1 quest drop entry bool HasQuestDropForPlayer(Player const* player) const; // The same for active quests of the player void Process(Loot& loot, uint16 lootMode, Player const* personalLooter = nullptr) const; // Rolls an item from the group (if any) and adds the item to the loot float RawTotalChance() const; // Overall chance for the group (without equal chanced items) float TotalChance() const; // Overall chance for the group void Verify(LootStore const& lootstore, uint32 id, uint8 group_id) const; void CheckLootRefs(LootTemplateMap const& store, LootIdSet* ref_set) const; LootStoreItemList* GetExplicitlyChancedItemList() { return &ExplicitlyChanced; } LootStoreItemList* GetEqualChancedItemList() { return &EqualChanced; } private: LootStoreItemList ExplicitlyChanced; // Entries with chances defined in DB LootStoreItemList EqualChanced; // Zero chances - every entry takes the same chance // Rolls an item from the group, returns NULL if all miss their chances LootStoreItem const* Roll(uint16 lootMode, Player const* personalLooter = nullptr) const; }; LootStore::LootStore(char const* name, char const* entryName, bool ratesAllowed) : m_name(name), m_entryName(entryName), m_ratesAllowed(ratesAllowed) { } LootStore::LootStore(LootStore&&) noexcept = default; LootStore& LootStore::operator=(LootStore&&) noexcept = default; LootStore::~LootStore() = default; //Remove all data and free all memory void LootStore::Clear() { m_LootTemplates.clear(); } // Checks validity of the loot store // Actual checks are done within LootTemplate::Verify() which is called for every template void LootStore::Verify() const { for (auto const& [lootId, lootTemplate] : m_LootTemplates) lootTemplate->Verify(*this, lootId); } // Loads a *_loot_template DB table into loot store // All checks of the loaded template are called from here, no error reports at loot generation required uint32 LootStore::LoadLootTable() { // Clearing store (for reloading case) Clear(); // 0 1 2 3 4 5 6 7 8 QueryResult result = WorldDatabase.PQuery("SELECT Entry, ItemType, Item, Chance, QuestRequired, LootMode, GroupId, MinCount, MaxCount FROM {}", GetName()); if (!result) return 0; uint32 count = 0; do { Field* fields = result->Fetch(); uint32 entry = fields[0].GetUInt32(); LootStoreItem::Type type = static_cast(fields[1].GetInt8()); uint32 item = fields[2].GetUInt32(); float chance = fields[3].GetFloat(); bool needsquest = fields[4].GetBool(); uint16 lootmode = fields[5].GetUInt16(); uint8 groupid = fields[6].GetUInt8(); uint8 mincount = fields[7].GetUInt8(); uint8 maxcount = fields[8].GetUInt8(); LootStoreItem* storeitem = new LootStoreItem(item, type, chance, needsquest, lootmode, groupid, mincount, maxcount); if (!storeitem->IsValid(*this, entry)) // Validity checks { delete storeitem; continue; } // Looking for the template of the entry auto [tab, isNew] = m_LootTemplates.try_emplace(entry); if (isNew) tab->second.reset(new LootTemplate()); // Adds current row to the template tab->second->AddEntry(storeitem); ++count; } while (result->NextRow()); Verify(); // Checks validity of the loot store return count; } bool LootStore::HaveQuestLootFor(uint32 loot_id) const { // scan loot for quest items if (LootTemplate const* lootTemplate = Trinity::Containers::MapGetValuePtr(m_LootTemplates, loot_id)) return lootTemplate->HasQuestDrop(m_LootTemplates); return false; } bool LootStore::HaveQuestLootForPlayer(uint32 loot_id, Player const* player) const { if (LootTemplate const* lootTemplate = Trinity::Containers::MapGetValuePtr(m_LootTemplates, loot_id)) if (lootTemplate->HasQuestDropForPlayer(m_LootTemplates, player)) return true; return false; } LootTemplate const* LootStore::GetLootFor(uint32 loot_id) const { return Trinity::Containers::MapGetValuePtr(m_LootTemplates, loot_id); } LootTemplate* LootStore::GetLootForConditionFill(uint32 loot_id) { return Trinity::Containers::MapGetValuePtr(m_LootTemplates, loot_id); } uint32 LootStore::LoadAndCollectLootIds(LootIdSet& lootIdSet) { uint32 count = LoadLootTable(); std::ranges::transform(m_LootTemplates, std::inserter(lootIdSet, lootIdSet.end()), Trinity::Containers::MapKey); return count; } void LootStore::CheckLootRefs(LootIdSet* ref_set) const { for (auto const& [_, lootTemplate] : m_LootTemplates) lootTemplate->CheckLootRefs(m_LootTemplates, ref_set); } void LootStore::ReportUnusedIds(LootIdSet const& lootIdSet) const { // all still listed ids isn't referenced for (uint32 lootId : lootIdSet) TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} isn't {} and not referenced from loot, and thus useless.", GetName(), lootId, GetEntryName()); } void LootStore::ReportNonExistingId(uint32 lootId) const { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} does not exist", GetName(), lootId); } void LootStore::ReportNonExistingId(uint32 lootId, char const* ownerType, uint32 ownerId) const { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} does not exist but it is used by {} {}", GetName(), lootId, ownerType, ownerId); } // // --------- LootStoreItem --------- // // Checks if the entry (quest, non-quest, reference) takes it's chance (at loot generation) // RATE_DROP_ITEMS is no longer used for all types of entries bool LootStoreItem::Roll(bool rate) const { if (chance >= 100.0f) return true; switch (type) { case Type::Item: { ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(itemid); float qualityModifier = pProto && rate && QualityToRate[pProto->GetQuality()] != MAX_RATES ? sWorld->getRate(QualityToRate[pProto->GetQuality()]) : 1.0f; return roll_chance_f(chance * qualityModifier); } 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); } case Type::TrackingQuest: return roll_chance_f(chance); default: break; } return false; } // Checks correctness of values bool LootStoreItem::IsValid(LootStore const& store, uint32 entry) const { if (mincount == 0) { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: wrong MinCount ({}) - skipped", store.GetName(), entry, type, itemid, mincount); return false; } switch (type) { case Type::Item: { ItemTemplate const* proto = sObjectMgr->GetItemTemplate(itemid); if (!proto) { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: item 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; } case Type::Reference: if (needs_quest) TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: quest required will be ignored", store.GetName(), entry, type, itemid); else if (chance == 0) // no chance for the reference { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: zero chance is specified for a reference, skipped", store.GetName(), entry, type, itemid); 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; } case Type::TrackingQuest: { Quest const* quest = sObjectMgr->GetQuestTemplate(itemid); if (!quest) { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: quest does not exist - skipped", store.GetName(), entry, type, itemid); return false; } if (!quest->HasFlag(QUEST_FLAGS_TRACKING_EVENT)) { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: quest is not a tracking flag - 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 (mincount != 1 || maxcount) // wrong count { TC_LOG_ERROR("sql.sql", "Table '{}' Entry {} ItemType {} Item {}: tracking quest has count other than 1 (MinCount {} MaxCount {}) - 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); return false; } return true; // Referenced template existence is checked at whole store level } // // --------- LootTemplate::LootGroup --------- // // Adds an entry to the group (at loading stage) void LootTemplate::LootGroup::AddEntry(LootStoreItem* item) { if (item->chance != 0) ExplicitlyChanced.emplace_back(item); else EqualChanced.emplace_back(item); } // Rolls an item from the group, returns NULL if all miss their chances LootStoreItem const* LootTemplate::LootGroup::Roll(uint16 lootMode, Player const* personalLooter /*= nullptr*/) const { auto getValidLoot = [](LootStoreItemList const& items, uint16 lootMode, Player const* personalLooter) { std::vector possibleLoot; possibleLoot.resize(items.size()); std::ranges::transform(items, possibleLoot.begin(), [](std::unique_ptr const& i) { return i.get(); }); Trinity::Containers::EraseIf(possibleLoot, LootGroupInvalidSelector(lootMode, personalLooter)); return possibleLoot; }; std::vector possibleLoot = getValidLoot(ExplicitlyChanced, lootMode, personalLooter); if (!possibleLoot.empty()) // First explicitly chanced entries are checked { float roll = rand_chance(); for (LootStoreItem const* item : possibleLoot) // check each explicitly chanced entry in the template and modify its chance based on quality. { if (item->chance >= 100.0f) return item; roll -= item->chance; if (roll < 0) return item; } } possibleLoot = getValidLoot(EqualChanced, lootMode, personalLooter); if (!possibleLoot.empty()) // If nothing selected yet - an item is taken from equal-chanced part return Trinity::Containers::SelectRandomContainerElement(possibleLoot); return nullptr; // Empty drop from the group } bool LootTemplate::LootGroup::HasDropForPlayer(Player const* player, bool strictUsabilityCheck) const { for (std::unique_ptr const& lootStoreItem : ExplicitlyChanced) if (LootItem::AllowedForPlayer(player, *lootStoreItem, strictUsabilityCheck)) return true; for (std::unique_ptr const& lootStoreItem : EqualChanced) if (LootItem::AllowedForPlayer(player, *lootStoreItem, strictUsabilityCheck)) return true; return false; } // True if group includes at least 1 quest drop entry bool LootTemplate::LootGroup::HasQuestDrop() const { if (std::ranges::any_of(ExplicitlyChanced, &LootStoreItem::needs_quest)) return true; if (std::ranges::any_of(EqualChanced, &LootStoreItem::needs_quest)) return true; return false; } // True if group includes at least 1 quest drop entry for active quests of the player bool LootTemplate::LootGroup::HasQuestDropForPlayer(Player const* player) const { auto hasQuestForLootItem = [player](std::unique_ptr 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, hasQuestForLootItem)) return true; return false; } // Rolls an item from the group (if any takes its chance) and adds the item to the loot void LootTemplate::LootGroup::Process(Loot& loot, uint16 lootMode, Player const* personalLooter /*= nullptr*/) const { if (LootStoreItem const* item = Roll(lootMode, personalLooter)) loot.AddItem(*item); } // Overall chance for the group without equal chanced items float LootTemplate::LootGroup::RawTotalChance() const { float result = 0; for (std::unique_ptr const& item : ExplicitlyChanced) if (!item->needs_quest) result += item->chance; return result; } // Overall chance for the group float LootTemplate::LootGroup::TotalChance() const { float result = RawTotalChance(); if (!EqualChanced.empty() && result < 100.0f) return 100.0f; return result; } void LootTemplate::LootGroup::Verify(LootStore const& lootstore, uint32 id, uint8 group_id) const { float chance = RawTotalChance(); if (chance > 101.0f) /// @todo replace with 100% when DBs will be ready TC_LOG_ERROR("sql.sql", "Table '{}' entry {} group {} has total chance > 100% ({})", lootstore.GetName(), id, group_id, chance); if (chance >= 100.0f && !EqualChanced.empty()) TC_LOG_ERROR("sql.sql", "Table '{}' entry {} group {} has items with chance=0% but group total chance >= 100% ({})", lootstore.GetName(), id, group_id, chance); } void LootTemplate::LootGroup::CheckLootRefs(LootTemplateMap const& /*store*/, LootIdSet* ref_set) const { for (std::unique_ptr const& item : ExplicitlyChanced) { if (item->type == LootStoreItem::Type::Reference) { if (!LootTemplates_Reference.GetLootFor(item->itemid)) LootTemplates_Reference.ReportNonExistingId(item->itemid, "Reference", item->itemid); else if (ref_set) ref_set->erase(item->itemid); } } for (std::unique_ptr const& item : EqualChanced) { if (item->type == LootStoreItem::Type::Reference) { if (!LootTemplates_Reference.GetLootFor(item->itemid)) LootTemplates_Reference.ReportNonExistingId(item->itemid, "Reference", item->itemid); else if (ref_set) ref_set->erase(item->itemid); } } } // // --------- LootTemplate --------- // LootTemplate::LootTemplate() = default; LootTemplate::LootTemplate(LootTemplate&&) noexcept = default; LootTemplate& LootTemplate::operator=(LootTemplate&&) noexcept = default; LootTemplate::~LootTemplate() = default; // Adds an entry to the group (at loading stage) void LootTemplate::AddEntry(LootStoreItem* item) { if (item->groupid > 0 && item->type != LootStoreItem::Type::Reference) // Group { std::unique_ptr& group = Trinity::Containers::EnsureWritableVectorIndex(Groups, item->groupid - 1); if (!group) group.reset(new LootGroup()); // Adds new group the the loot template if needed group->AddEntry(item); // Adds new entry to the group } else // Non-grouped entries and references are stored together Entries.emplace_back(item); } void LootTemplate::CopyConditions(LootItem* li) const { // Copies the conditions list from a template item to a LootItemData for (std::unique_ptr 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; case LootStoreItem::Type::TrackingQuest: if (li->type != LootItemType::TrackingQuest) continue; break; default: break; } if (item->itemid != li->itemid) continue; li->conditions = item->conditions; break; } } // Rolls for every item in the template and adds the rolled items the the loot void LootTemplate::Process(Loot& loot, bool rate, uint16 lootMode, uint8 groupId, Player const* personalLooter /*= nullptr*/) const { if (groupId) // Group reference uses own processing of the group { if (groupId > Groups.size()) return; // Error message already printed at loading stage if (!Groups[groupId - 1]) return; Groups[groupId - 1]->Process(loot, lootMode, personalLooter); return; } // Rolling non-grouped items for (std::unique_ptr const& item : Entries) { if (!(item->lootmode & lootMode)) // Do not add if mode mismatch continue; if (!item->Roll(rate)) continue; // Bad luck for the entry switch (item->type) { case LootStoreItem::Type::Item: case LootStoreItem::Type::Currency: case LootStoreItem::Type::TrackingQuest: // Plain entries (not a reference, not grouped) // Chance is already checked, just add if (!personalLooter || LootItem::AllowedForPlayer(personalLooter, *item, true)) loot.AddItem(*item); break; case LootStoreItem::Type::Reference: { LootTemplate const* Referenced = LootTemplates_Reference.GetLootFor(item->itemid); if (!Referenced) continue; // Error message already printed at loading stage uint32 maxcount = uint32(float(item->maxcount) * sWorld->getRate(RATE_DROP_ITEM_REFERENCED_AMOUNT)); for (uint32 loop = 0; loop < maxcount; ++loop) // Ref multiplicator Referenced->Process(loot, rate, lootMode, item->groupid, personalLooter); break; } default: break; } } // Now processing groups for (std::unique_ptr const& group : Groups) if (group) group->Process(loot, lootMode, personalLooter); } void LootTemplate::ProcessPersonalLoot(std::unordered_map>& personalLoot, bool rate, uint16 lootMode) const { auto getLootersForItem = [&personalLoot](auto&& predicate) { std::vector lootersForItem; for (auto&& [looter, loot] : personalLoot) { if (predicate(looter)) lootersForItem.push_back(looter); } return lootersForItem; }; // Rolling non-grouped items for (std::unique_ptr const& item : Entries) { if (!(item->lootmode & lootMode)) // Do not add if mode mismatch continue; if (!item->Roll(rate)) continue; // Bad luck for the entry switch (item->type) { case LootStoreItem::Type::Item: { // Plain entries (not a reference, not grouped) // Chance is already checked, just add std::vector lootersForItem = getLootersForItem([&](Player const* looter) { return LootItem::AllowedForPlayer(looter, *item, true); }); if (!lootersForItem.empty()) { Player* chosenLooter = Trinity::Containers::SelectRandomContainerElement(lootersForItem); personalLoot[chosenLooter]->AddItem(*item); } break; } case LootStoreItem::Type::Reference: { LootTemplate const* referenced = LootTemplates_Reference.GetLootFor(item->itemid); if (!referenced) continue; // Error message already printed at loading stage uint32 maxcount = uint32(float(item->maxcount) * sWorld->getRate(RATE_DROP_ITEM_REFERENCED_AMOUNT)); std::vector gotLoot; for (uint32 loop = 0; loop < maxcount; ++loop) // Ref multiplicator { std::vector lootersForItem = getLootersForItem([&](Player const* looter) { return referenced->HasDropForPlayer(looter, item->groupid, true); }); // nobody can loot this, skip it if (lootersForItem.empty()) break; auto newEnd = std::remove_if(lootersForItem.begin(), lootersForItem.end(), [&](Player const* looter) { return advstd::ranges::contains(gotLoot, looter); }); if (lootersForItem.begin() == newEnd) { // if we run out of looters this means that there are more items dropped than players // start a new cycle adding one item to everyone gotLoot.clear(); } else lootersForItem.erase(newEnd, lootersForItem.end()); Player* chosenLooter = Trinity::Containers::SelectRandomContainerElement(lootersForItem); referenced->Process(*personalLoot[chosenLooter], rate, lootMode, item->groupid, chosenLooter); gotLoot.push_back(chosenLooter); } break; } case LootStoreItem::Type::Currency: case LootStoreItem::Type::TrackingQuest: { // Plain entries (not a reference, not grouped) // Chance is already checked, just add std::vector lootersForItem = getLootersForItem([&](Player const* looter) { return LootItem::AllowedForPlayer(looter, *item, true); }); for (Player* looter : lootersForItem) personalLoot[looter]->AddItem(*item); break; } default: break; } } // Now processing groups for (std::unique_ptr const& group : Groups) { if (group) { std::vector lootersForGroup = getLootersForItem([&](Player const* looter) { return group->HasDropForPlayer(looter, true); }); if (!lootersForGroup.empty()) { Player* chosenLooter = Trinity::Containers::SelectRandomContainerElement(lootersForGroup); group->Process(*personalLoot[chosenLooter], lootMode); } } } } // True if template includes at least 1 drop for the player bool LootTemplate::HasDropForPlayer(Player const* player, uint8 groupId, bool strictUsabilityCheck) const { if (groupId) // Group reference { if (groupId > Groups.size()) return false; // Error message already printed at loading stage if (!Groups[groupId - 1]) return false; return Groups[groupId - 1]->HasDropForPlayer(player, strictUsabilityCheck); } // Checking non-grouped entries for (std::unique_ptr const& lootStoreItem : Entries) { switch (lootStoreItem->type) { case LootStoreItem::Type::Item: case LootStoreItem::Type::Currency: case LootStoreItem::Type::TrackingQuest: if (LootItem::AllowedForPlayer(player, *lootStoreItem, strictUsabilityCheck)) return true; // active quest drop found break; case LootStoreItem::Type::Reference: { LootTemplate const* referenced = LootTemplates_Reference.GetLootFor(lootStoreItem->itemid); if (!referenced) continue; // Error message already printed at loading stage if (referenced->HasDropForPlayer(player, lootStoreItem->groupid, strictUsabilityCheck)) return true; break; } default: break; } } // Now checking groups for (std::unique_ptr const& group : Groups) if (group && group->HasDropForPlayer(player, strictUsabilityCheck)) return true; return false; } // True if template includes at least 1 quest drop entry bool LootTemplate::HasQuestDrop(LootTemplateMap const& store, uint8 groupId) const { if (groupId) // Group reference { if (groupId > Groups.size()) return false; // Error message [should be] already printed at loading stage if (!Groups[groupId - 1]) return false; return Groups[groupId-1]->HasQuestDrop(); } for (std::unique_ptr const& item : Entries) { switch (item->type) { case LootStoreItem::Type::Item: case LootStoreItem::Type::Currency: if (item->needs_quest) return true; // quest drop found break; case LootStoreItem::Type::Reference: { LootTemplateMap::const_iterator Referenced = store.find(item->itemid); if (Referenced == store.end()) continue; // Error message [should be] already printed at loading stage if (Referenced->second->HasQuestDrop(store, item->groupid)) return true; break; } default: break; } } // Now processing groups for (std::unique_ptr const& group : Groups) if (group && group->HasQuestDrop()) return true; return false; } // True if template includes at least 1 quest drop for an active quest of the player bool LootTemplate::HasQuestDropForPlayer(LootTemplateMap const& store, Player const* player, uint8 groupId) const { if (groupId) // Group reference { if (groupId > Groups.size()) return false; // Error message already printed at loading stage if (!Groups[groupId - 1]) return false; return Groups[groupId - 1]->HasQuestDropForPlayer(player); } // Checking non-grouped entries for (std::unique_ptr const& item : Entries) { switch (item->type) { case LootStoreItem::Type::Item: if (player->HasQuestForItem(item->itemid)) return true; // active quest drop found break; case LootStoreItem::Type::Reference: { LootTemplateMap::const_iterator Referenced = store.find(item->itemid); if (Referenced == store.end()) continue; // Error message already printed at loading stage if (Referenced->second->HasQuestDropForPlayer(store, player, item->groupid)) return true; break; } case LootStoreItem::Type::Currency: if (player->HasQuestForCurrency(item->itemid)) return true; // active quest drop found break; default: break; } } // Now checking groups for (std::unique_ptr const& group : Groups) if (group && group->HasQuestDropForPlayer(player)) return true; return false; } // Checks integrity of the template void LootTemplate::Verify(LootStore const& lootstore, uint32 id) const { // Checking group chances for (uint32 i = 0; i < Groups.size(); ++i) if (Groups[i]) Groups[i]->Verify(lootstore, id, i + 1); /// @todo References validity checks } void LootTemplate::CheckLootRefs(LootTemplateMap const& store, LootIdSet* ref_set) const { for (std::unique_ptr const& item : Entries) { if (item->type == LootStoreItem::Type::Reference) { if (!LootTemplates_Reference.GetLootFor(item->itemid)) LootTemplates_Reference.ReportNonExistingId(item->itemid, "Reference", item->itemid); else if (ref_set) ref_set->erase(item->itemid); } } for (std::unique_ptr const& group : Groups) if (group) group->CheckLootRefs(store, ref_set); } bool LootTemplate::LinkConditions(ConditionId const& id, ConditionsReference reference) { if (!Entries.empty()) { for (std::unique_ptr& item : Entries) { if (item->itemid == uint32(id.SourceEntry)) { item->conditions = std::move(reference); return true; } } } if (!Groups.empty()) { for (std::unique_ptr& group : Groups) { if (!group) continue; LootStoreItemList* itemList = group->GetExplicitlyChancedItemList(); if (!itemList->empty()) { for (std::unique_ptr const& item : *itemList) { if (item->itemid == uint32(id.SourceEntry)) { item->conditions = std::move(reference); return true; } } } itemList = group->GetEqualChancedItemList(); if (!itemList->empty()) { for (std::unique_ptr const& item : *itemList) { if (item->itemid == uint32(id.SourceEntry)) { item->conditions = std::move(reference); return true; } } } } } return false; } std::unordered_map> GenerateDungeonEncounterPersonalLoot(uint32 dungeonEncounterId, uint32 lootId, LootStore const& store, LootType type, WorldObject const* lootOwner, uint32 minMoney, uint32 maxMoney, uint16 lootMode, MapDifficultyEntry const* mapDifficulty, std::vector const& tappers) { std::unordered_map> tempLoot; for (Player* tapper : tappers) { if (tapper->IsLockedToDungeonEncounter(dungeonEncounterId)) continue; std::unique_ptr& loot = tempLoot[tapper]; loot.reset(new Loot(lootOwner->GetMap(), lootOwner->GetGUID(), type, nullptr)); loot->SetItemContext(ItemBonusMgr::GetContextForPlayer(mapDifficulty, tapper)); loot->SetDungeonEncounterId(dungeonEncounterId); loot->generateMoneyLoot(minMoney, maxMoney); } if (LootTemplate const* tab = store.GetLootFor(lootId)) tab->ProcessPersonalLoot(tempLoot, store.IsRatesAllowed(), lootMode); std::unordered_map> personalLoot; for (auto&& [looter, loot] : tempLoot) { loot->FillNotNormalLootFor(looter); if (loot->isLooted()) continue; personalLoot[looter->GetGUID()] = std::move(loot); } return personalLoot; } void LoadLootTemplates_Creature() { TC_LOG_INFO("server.loading", "Loading creature loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet, lootIdSetUsed; uint32 count = LootTemplates_Creature.LoadAndCollectLootIds(lootIdSet); // Remove real entries and check loot existence CreatureTemplateContainer const& ctc = sObjectMgr->GetCreatureTemplates(); for (auto const& [creatureId, creatureTemplate] : ctc) { for (auto const& [difficulty, creatureDifficulty] : creatureTemplate.difficultyStore) { if (uint32 lootid = creatureDifficulty.LootID) { if (!lootIdSet.contains(lootid)) LootTemplates_Creature.ReportNonExistingId(lootid, "Creature", creatureId); else lootIdSetUsed.insert(lootid); } } } for (uint32 lootId : lootIdSetUsed) lootIdSet.erase(lootId); // 1 means loot for player corpse lootIdSet.erase(PLAYER_CORPSE_LOOT_ENTRY); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Creature.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} creature loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 creature loot templates. DB table `creature_loot_template` is empty"); } void LoadLootTemplates_Disenchant() { TC_LOG_INFO("server.loading", "Loading disenchanting loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet, lootIdSetUsed; uint32 count = LootTemplates_Disenchant.LoadAndCollectLootIds(lootIdSet); for (ItemDisenchantLootEntry const* disenchant : sItemDisenchantLootStore) { uint32 lootid = disenchant->ID; if (!lootIdSet.contains(lootid)) LootTemplates_Disenchant.ReportNonExistingId(lootid); else lootIdSetUsed.insert(lootid); } for (ItemBonusEntry const* itemBonus : sItemBonusStore) { if (itemBonus->Type != ITEM_BONUS_DISENCHANT_LOOT_ID) continue; uint32 lootid = itemBonus->Value[0]; if (!lootIdSet.contains(lootid)) LootTemplates_Disenchant.ReportNonExistingId(lootid); else lootIdSetUsed.insert(lootid); } for (uint32 lootId : lootIdSetUsed) lootIdSet.erase(lootId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Disenchant.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} disenchanting loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 disenchanting loot templates. DB table `disenchant_loot_template` is empty"); } void LoadLootTemplates_Fishing() { TC_LOG_INFO("server.loading", "Loading fishing loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet; uint32 count = LootTemplates_Fishing.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot for (AreaTableEntry const* areaTable : sAreaTableStore) lootIdSet.erase(areaTable->ID); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Fishing.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} fishing loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 fishing loot templates. DB table `fishing_loot_template` is empty"); } void LoadLootTemplates_Gameobject() { TC_LOG_INFO("server.loading", "Loading gameobject loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet, lootIdSetUsed; uint32 count = LootTemplates_Gameobject.LoadAndCollectLootIds(lootIdSet); auto checkLootId = [&](uint32 lootId, uint32 gameObjectId) { if (!lootIdSet.contains(lootId)) LootTemplates_Gameobject.ReportNonExistingId(lootId, "Gameobject", gameObjectId); else lootIdSetUsed.insert(lootId); }; // remove real entries and check existence loot GameObjectTemplateContainer const& gotc = sObjectMgr->GetGameObjectTemplates(); for (auto const& [gameObjectId, gameObjectTemplate] : gotc) { if (uint32 lootid = gameObjectTemplate.GetLootId()) checkLootId(lootid, gameObjectId); if (gameObjectTemplate.type == GAMEOBJECT_TYPE_CHEST) { if (gameObjectTemplate.chest.chestPersonalLoot) checkLootId(gameObjectTemplate.chest.chestPersonalLoot, gameObjectId); if (gameObjectTemplate.chest.chestPushLoot) checkLootId(gameObjectTemplate.chest.chestPushLoot, gameObjectId); } } for (uint32 lootId : lootIdSetUsed) lootIdSet.erase(lootId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Gameobject.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} gameobject loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 gameobject loot templates. DB table `gameobject_loot_template` is empty"); } void LoadLootTemplates_Item() { TC_LOG_INFO("server.loading", "Loading item loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet; uint32 count = LootTemplates_Item.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot ItemTemplateContainer const& its = sObjectMgr->GetItemTemplateStore(); for (auto const& [itemId, itemTemplate] : its) if (itemTemplate.HasFlag(ITEM_FLAG_HAS_LOOT)) if (!lootIdSet.erase(itemId)) LootTemplates_Item.ReportNonExistingId(itemId, "Item", itemId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Item.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} item loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 item loot templates. DB table `item_loot_template` is empty"); } void LoadLootTemplates_Milling() { TC_LOG_INFO("server.loading", "Loading milling loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet; uint32 count = LootTemplates_Milling.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot ItemTemplateContainer const& its = sObjectMgr->GetItemTemplateStore(); for (auto const& [itemId, itemTemplate] : its) if (itemTemplate.HasFlag(ITEM_FLAG_IS_MILLABLE)) if (!lootIdSet.erase(itemId)) LootTemplates_Milling.ReportNonExistingId(itemId, "Item", itemId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Milling.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} milling loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 milling loot templates. DB table `milling_loot_template` is empty"); } void LoadLootTemplates_Pickpocketing() { TC_LOG_INFO("server.loading", "Loading pickpocketing loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet, lootIdSetUsed; uint32 count = LootTemplates_Pickpocketing.LoadAndCollectLootIds(lootIdSet); // Remove real entries and check loot existence CreatureTemplateContainer const& ctc = sObjectMgr->GetCreatureTemplates(); for (auto const& [creatureId, creatureTemplate] : ctc) { for (auto const& [difficulty, creatureDifficulty] : creatureTemplate.difficultyStore) { if (uint32 lootid = creatureDifficulty.PickPocketLootID) { if (!lootIdSet.contains(lootid)) LootTemplates_Pickpocketing.ReportNonExistingId(lootid, "Creature", creatureId); else lootIdSetUsed.insert(lootid); } } } for (uint32 lootId : lootIdSetUsed) lootIdSet.erase(lootId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Pickpocketing.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} pickpocketing loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 pickpocketing loot templates. DB table `pickpocketing_loot_template` is empty"); } void LoadLootTemplates_Prospecting() { TC_LOG_INFO("server.loading", "Loading prospecting loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet; uint32 count = LootTemplates_Prospecting.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot ItemTemplateContainer const& its = sObjectMgr->GetItemTemplateStore(); for (auto const& [itemId, itemTemplate] : its) if (itemTemplate.HasFlag(ITEM_FLAG_IS_PROSPECTABLE)) if (!lootIdSet.erase(itemId)) LootTemplates_Prospecting.ReportNonExistingId(itemId, "Item", itemId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Prospecting.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} prospecting loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 prospecting loot templates. DB table `prospecting_loot_template` is empty"); } void LoadLootTemplates_Mail() { TC_LOG_INFO("server.loading", "Loading mail loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet; uint32 count = LootTemplates_Mail.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot for (MailTemplateEntry const* mailTemplate : sMailTemplateStore) lootIdSet.erase(mailTemplate->ID); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Mail.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} mail loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 mail loot templates. DB table `mail_loot_template` is empty"); } void LoadLootTemplates_Skinning() { TC_LOG_INFO("server.loading", "Loading skinning loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet, lootIdSetUsed; uint32 count = LootTemplates_Skinning.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot CreatureTemplateContainer const& ctc = sObjectMgr->GetCreatureTemplates(); for (auto const& [creatureId, creatureTemplate] : ctc) { for (auto const& [difficulty, creatureDifficulty] : creatureTemplate.difficultyStore) { if (uint32 lootid = creatureDifficulty.SkinLootID) { if (!lootIdSet.contains(lootid)) LootTemplates_Skinning.ReportNonExistingId(lootid, "Creature", creatureId); else lootIdSetUsed.insert(lootid); } } } for (uint32 lootId : lootIdSetUsed) lootIdSet.erase(lootId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Skinning.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} skinning loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 skinning loot templates. DB table `skinning_loot_template` is empty"); } void LoadLootTemplates_Spell() { // TODO: change this to use MiscValue from spell effect as id instead of spell id TC_LOG_INFO("server.loading", "Loading spell loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet, lootIdSetUsed; uint32 count = LootTemplates_Spell.LoadAndCollectLootIds(lootIdSet); // remove real entries and check existence loot sSpellMgr->ForEachSpellInfo([&](SpellInfo const* spellInfo) { // possible cases if (!spellInfo->IsLootCrafting()) return; if (!lootIdSet.contains(spellInfo->Id)) { // not report about not trainable spells (optionally supported by DB) // ignore 61756 (Northrend Inscription Research (FAST QA VERSION) for example if (!spellInfo->HasAttribute(SPELL_ATTR0_NOT_SHAPESHIFTED) || spellInfo->HasAttribute(SPELL_ATTR0_IS_TRADESKILL)) LootTemplates_Spell.ReportNonExistingId(spellInfo->Id, "Spell", spellInfo->Id); } else lootIdSetUsed.insert(spellInfo->Id); }); for (uint32 lootId : lootIdSetUsed) lootIdSet.erase(lootId); // output error for any still listed (not referenced from appropriate table) ids LootTemplates_Spell.ReportUnusedIds(lootIdSet); if (count) TC_LOG_INFO("server.loading", ">> Loaded {} spell loot templates in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); else TC_LOG_INFO("server.loading", ">> Loaded 0 spell loot templates. DB table `spell_loot_template` is empty"); } void LoadLootTemplates_Reference() { TC_LOG_INFO("server.loading", "Loading reference loot templates..."); uint32 oldMSTime = getMSTime(); LootIdSet lootIdSet; LootTemplates_Reference.LoadAndCollectLootIds(lootIdSet); // check references and remove used LootTemplates_Creature.CheckLootRefs(&lootIdSet); LootTemplates_Fishing.CheckLootRefs(&lootIdSet); LootTemplates_Gameobject.CheckLootRefs(&lootIdSet); LootTemplates_Item.CheckLootRefs(&lootIdSet); LootTemplates_Milling.CheckLootRefs(&lootIdSet); LootTemplates_Pickpocketing.CheckLootRefs(&lootIdSet); LootTemplates_Skinning.CheckLootRefs(&lootIdSet); LootTemplates_Disenchant.CheckLootRefs(&lootIdSet); LootTemplates_Prospecting.CheckLootRefs(&lootIdSet); LootTemplates_Mail.CheckLootRefs(&lootIdSet); LootTemplates_Reference.CheckLootRefs(&lootIdSet); // output error for any still listed ids (not referenced from any loot table) LootTemplates_Reference.ReportUnusedIds(lootIdSet); TC_LOG_INFO("server.loading", ">> Loaded reference loot templates in {} ms", GetMSTimeDiffToNow(oldMSTime)); } void LoadLootTables() { LoadLootTemplates_Creature(); LoadLootTemplates_Fishing(); LoadLootTemplates_Gameobject(); LoadLootTemplates_Item(); LoadLootTemplates_Mail(); LoadLootTemplates_Milling(); LoadLootTemplates_Pickpocketing(); LoadLootTemplates_Skinning(); LoadLootTemplates_Disenchant(); LoadLootTemplates_Prospecting(); LoadLootTemplates_Spell(); LoadLootTemplates_Reference(); }