/* * 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 "Loot.h" #include "DB2Stores.h" #include "DatabaseEnv.h" #include "GameTime.h" #include "Group.h" #include "Item.h" #include "ItemBonusMgr.h" #include "ItemTemplate.h" #include "Log.h" #include "LootMgr.h" #include "LootPackets.h" #include "Map.h" #include "MapUtils.h" #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Player.h" #include "Random.h" #include "World.h" #include "WorldSession.h" // // --------- LootItem --------- // // Constructor, copies most fields from LootStoreItem and generates random count LootItem::LootItem(LootStoreItem const& li) : itemid(li.itemid), conditions(li.conditions), needs_quest(li.needs_quest) { 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; case LootStoreItem::Type::TrackingQuest: type = LootItemType::TrackingQuest; freeforall = true; break; default: break; } } LootItem::LootItem(LootItem const&) = default; LootItem::LootItem(LootItem&&) noexcept = default; LootItem& LootItem::operator=(LootItem const&) = default; LootItem& LootItem::operator=(LootItem&&) noexcept = default; 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 { 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); case LootItemType::TrackingQuest: return TrackingQuestAllowedForPlayer(player, itemid, 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); case LootStoreItem::Type::TrackingQuest: return TrackingQuestAllowedForPlayer(player, lootStoreItem.itemid, lootStoreItem.conditions); default: break; } return false; } 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 if (!conditions.Meets(player)) return false; ItemTemplate const* pProto = sObjectMgr->GetItemTemplate(itemid); if (!pProto) return false; // not show loot for not own team if (pProto->HasFlag(ITEM_FLAG2_FACTION_HORDE) && player->GetTeam() != HORDE) return false; if (pProto->HasFlag(ITEM_FLAG2_FACTION_ALLIANCE) && player->GetTeam() != ALLIANCE) return false; // Master looter can see all items even if the character can't loot them if (loot && loot->GetLootMethod() == MASTER_LOOT && follow_loot_rules && loot->GetLootMasterGUID() == player->GetGUID()) return true; // Don't allow loot for players without profession or those who already know the recipe if (pProto->HasFlag(ITEM_FLAG_HIDE_UNUSABLE_RECIPE)) { if (!player->HasSkill(pProto->GetRequiredSkill())) return false; for (ItemEffectEntry const* itemEffect : pProto->Effects) { if (itemEffect->TriggerType != ITEM_SPELLTRIGGER_ON_LEARN) continue; if (player->HasSpell(itemEffect->SpellID)) return false; } } // check quest requirements if (!pProto->HasFlag(ITEM_FLAGS_CU_IGNORE_QUEST_STATUS) && ((needs_quest || (pProto->GetStartQuest() && player->GetQuestStatus(pProto->GetStartQuest()) != QUEST_STATUS_NONE)) && !player->HasQuestForItem(itemid))) return false; if (strictUsabilityCheck) { if ((pProto->IsWeapon() || pProto->IsArmor()) && !pProto->IsUsableByLootSpecialization(player, true)) return false; if (player->CanRollNeedForItem(pProto, nullptr, false) != EQUIP_ERR_OK) return false; } return true; } 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; } bool LootItem::TrackingQuestAllowedForPlayer(Player const* player, uint32 questId, ConditionsReference const& conditions) { // DB conditions check if (!conditions.Meets(player)) return false; if (player->IsQuestCompletedBitSet(questId)) return false; return true; } void LootItem::AddAllowedLooter(Player const* player) { allowedGUIDs.insert(player->GetGUID()); } bool LootItem::HasAllowedLooter(ObjectGuid const& looter) const { return allowedGUIDs.contains(looter); } Optional LootItem::GetUiTypeForPlayer(Player const* player, Loot const& loot) const { if (is_looted) return {}; if (!allowedGUIDs.contains(player->GetGUID())) return {}; if (freeforall) { if (NotNormalLootItemList const* ffaItems = Trinity::Containers::MapGetValuePtr(loot.GetPlayerFFAItems(), player->GetGUID())) { auto ffaItemItr = std::ranges::find(*ffaItems, LootListId, &NotNormalLootItem::LootListId); if (ffaItemItr != ffaItems->end() && !ffaItemItr->is_looted) return loot.GetLootMethod() == FREE_FOR_ALL ? LOOT_SLOT_TYPE_OWNER : LOOT_SLOT_TYPE_ALLOW_LOOT; } return {}; } if (needs_quest && !follow_loot_rules) return loot.GetLootMethod() == FREE_FOR_ALL ? LOOT_SLOT_TYPE_OWNER : LOOT_SLOT_TYPE_ALLOW_LOOT; switch (loot.GetLootMethod()) { case FREE_FOR_ALL: return LOOT_SLOT_TYPE_OWNER; case ROUND_ROBIN: if (!loot.roundRobinPlayer.IsEmpty() && loot.roundRobinPlayer != player->GetGUID()) return {}; return LOOT_SLOT_TYPE_ALLOW_LOOT; case MASTER_LOOT: if (is_underthreshold) { if (!loot.roundRobinPlayer.IsEmpty() && loot.roundRobinPlayer != player->GetGUID()) return {}; return LOOT_SLOT_TYPE_ALLOW_LOOT; } return loot.GetLootMasterGUID() == player->GetGUID() ? LOOT_SLOT_TYPE_MASTER : LOOT_SLOT_TYPE_LOCKED; case GROUP_LOOT: case NEED_BEFORE_GREED: if (is_underthreshold) if (!loot.roundRobinPlayer.IsEmpty() && loot.roundRobinPlayer != player->GetGUID()) return {}; if (is_blocked) return LOOT_SLOT_TYPE_ROLL_ONGOING; if (rollWinnerGUID.IsEmpty()) // all passed return LOOT_SLOT_TYPE_ALLOW_LOOT; if (rollWinnerGUID == player->GetGUID()) return LOOT_SLOT_TYPE_OWNER; return {}; case PERSONAL_LOOT: return LOOT_SLOT_TYPE_OWNER; default: break; } return {}; } // // ------- Loot Roll ------- // // Send the roll for the whole group void LootRoll::SendStartRoll() { ItemTemplate const* itemTemplate = ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(m_lootItem->itemid)); for (auto const& [playerGuid, roll] : m_rollVoteMap) { if (roll.Vote != RollVote::NotEmitedYet) continue; Player* player = ObjectAccessor::GetPlayer(m_map, playerGuid); if (!player) continue; WorldPackets::Loot::StartLootRoll startLootRoll; startLootRoll.LootObj = m_loot->GetGUID(); startLootRoll.MapID = m_map->GetId(); startLootRoll.RollTime = LOOT_ROLL_TIMEOUT; startLootRoll.Method = m_loot->GetLootMethod(); startLootRoll.ValidRolls = m_voteMask; // In NEED_BEFORE_GREED need disabled for non-usable item for player if (m_loot->GetLootMethod() == NEED_BEFORE_GREED && player->CanRollNeedForItem(itemTemplate, m_map, true) != EQUIP_ERR_OK) startLootRoll.ValidRolls &= ~ROLL_FLAG_TYPE_NEED; FillPacket(startLootRoll.Item); startLootRoll.Item.UIType = LOOT_SLOT_TYPE_ROLL_ONGOING; startLootRoll.DungeonEncounterID = m_loot->GetDungeonEncounterId(); player->SendDirectMessage(startLootRoll.Write()); } // Handle auto pass option for (auto const& [playerGuid, roll] : m_rollVoteMap) { if (roll.Vote != RollVote::Pass) continue; SendRoll(playerGuid, -1, RollVote::Pass, {}); } } // Send all passed message void LootRoll::SendAllPassed() { WorldPackets::Loot::LootAllPassed lootAllPassed; lootAllPassed.LootObj = m_loot->GetGUID(); FillPacket(lootAllPassed.Item); lootAllPassed.Item.UIType = LOOT_SLOT_TYPE_ALLOW_LOOT; lootAllPassed.DungeonEncounterID = m_loot->GetDungeonEncounterId(); lootAllPassed.Write(); for (auto const& [playerGuid, roll] : m_rollVoteMap) { if (roll.Vote != RollVote::NotValid) continue; Player* player = ObjectAccessor::GetPlayer(m_map, playerGuid); if (!player) continue; player->SendDirectMessage(lootAllPassed.GetRawPacket()); } } // Send roll of targetGuid to the whole group (included targuetGuid) void LootRoll::SendRoll(ObjectGuid const& targetGuid, int32 rollNumber, RollVote rollType, Optional const& rollWinner) { WorldPackets::Loot::LootRollBroadcast lootRoll; lootRoll.LootObj = m_loot->GetGUID(); lootRoll.Player = targetGuid; lootRoll.Roll = rollNumber; lootRoll.RollType = AsUnderlyingType(rollType); lootRoll.Autopassed = false; FillPacket(lootRoll.Item); lootRoll.Item.UIType = LOOT_SLOT_TYPE_ROLL_ONGOING; lootRoll.DungeonEncounterID = m_loot->GetDungeonEncounterId(); lootRoll.Write(); for (auto const& [playerGuid, roll] : m_rollVoteMap) { if (roll.Vote == RollVote::NotValid) continue; if (playerGuid == rollWinner) continue; Player* player = ObjectAccessor::GetPlayer(m_map, playerGuid); if (!player) continue; player->SendDirectMessage(lootRoll.GetRawPacket()); } if (rollWinner) { if (Player* player = ObjectAccessor::GetPlayer(m_map, *rollWinner)) { lootRoll.Item.UIType = LOOT_SLOT_TYPE_ALLOW_LOOT; lootRoll.Clear(); player->SendDirectMessage(lootRoll.Write()); } } } // Send roll 'value' of the whole group and the winner to the whole group void LootRoll::SendLootRollWon(ObjectGuid const& targetGuid, int32 rollNumber, RollVote rollType) { // Send roll values for (auto const& [playerGuid, roll] : m_rollVoteMap) { switch (roll.Vote) { case RollVote::Pass: break; case RollVote::NotEmitedYet: case RollVote::NotValid: SendRoll(playerGuid, 0, RollVote::Pass, targetGuid); break; default: SendRoll(playerGuid, roll.RollNumber, roll.Vote, targetGuid); break; } } WorldPackets::Loot::LootRollWon lootRollWon; lootRollWon.LootObj = m_loot->GetGUID(); lootRollWon.Winner = targetGuid; lootRollWon.Roll = rollNumber; lootRollWon.RollType = AsUnderlyingType(rollType); FillPacket(lootRollWon.Item); lootRollWon.Item.UIType = LOOT_SLOT_TYPE_LOCKED; lootRollWon.DungeonEncounterID = m_loot->GetDungeonEncounterId(); lootRollWon.MainSpec = true; // offspec rolls not implemented lootRollWon.Write(); for (auto const& [playerGuid, roll] : m_rollVoteMap) { if (roll.Vote == RollVote::NotValid) continue; if (playerGuid == targetGuid) continue; Player* player = ObjectAccessor::GetPlayer(m_map, playerGuid); if (!player) continue; player->SendDirectMessage(lootRollWon.GetRawPacket()); } if (Player* player = ObjectAccessor::GetPlayer(m_map, targetGuid)) { lootRollWon.Item.UIType = LOOT_SLOT_TYPE_ALLOW_LOOT; lootRollWon.Clear(); player->SendDirectMessage(lootRollWon.Write()); } } void LootRoll::FillPacket(WorldPackets::Loot::LootItemData& lootItem) const { lootItem.Quantity = m_lootItem->count; lootItem.LootListID = m_lootItem->LootListId; lootItem.CanTradeToTapList = m_lootItem->allowedGUIDs.size() > 1; lootItem.Loot.Initialize(*m_lootItem); } LootRoll::~LootRoll() { if (m_isStarted) SendAllPassed(); for (auto const& [playerGuid, roll] : m_rollVoteMap) { if (roll.Vote != RollVote::NotEmitedYet) continue; Player* player = ObjectAccessor::GetPlayer(m_map, playerGuid); if (!player) continue; player->RemoveLootRoll(this); } } // Try to start the group roll for the specified item (it may fail for quest item or any condition // If this method return false the roll have to be removed from the container to avoid any problem bool LootRoll::TryToStart(Map* map, Loot& loot, uint32 lootListId, uint16 enchantingSkill) { if (!m_isStarted) { if (lootListId >= loot.items.size()) return false; m_map = map; // initialize the data needed for the roll m_lootItem = &loot.items[lootListId]; m_loot = &loot; m_lootItem->is_blocked = true; // block the item while rolling uint32 playerCount = 0; for (ObjectGuid allowedLooter : m_lootItem->GetAllowedLooters()) { Player* plr = ObjectAccessor::GetPlayer(m_map, allowedLooter); if (!plr || !m_lootItem->HasAllowedLooter(plr->GetGUID())) // check if player meet the condition to be able to roll this item { m_rollVoteMap[allowedLooter].Vote = RollVote::NotValid; continue; } // initialize player vote map m_rollVoteMap[allowedLooter].Vote = plr->GetPassOnGroupLoot() ? RollVote::Pass : RollVote::NotEmitedYet; if (!plr->GetPassOnGroupLoot()) plr->AddLootRoll(this); ++playerCount; } // initialize item prototype and check enchant possibilities for this group ItemTemplate const* itemTemplate = ASSERT_NOTNULL(sObjectMgr->GetItemTemplate(m_lootItem->itemid)); m_voteMask = ROLL_ALL_TYPE_MASK; if (itemTemplate->HasFlag(ITEM_FLAG2_CAN_ONLY_ROLL_GREED)) m_voteMask = RollMask(m_voteMask & ~ROLL_FLAG_TYPE_NEED); if (Optional disenchantSkillRequired = GetItemDisenchantSkillRequired(); !disenchantSkillRequired || disenchantSkillRequired > enchantingSkill) m_voteMask = RollMask(m_voteMask & ~ROLL_FLAG_TYPE_DISENCHANT); if (playerCount > 1) // check if more than one player can loot this item { // start the roll SendStartRoll(); m_endTime = GameTime::Now() + LOOT_ROLL_TIMEOUT; m_isStarted = true; return true; } // no need to start roll if one or less player can loot this item so place it under threshold m_lootItem->is_underthreshold = true; m_lootItem->is_blocked = false; } return false; } // Add vote from playerGuid bool LootRoll::PlayerVote(Player* player, RollVote vote) { ObjectGuid const& playerGuid = player->GetGUID(); RollVoteMap::iterator voterItr = m_rollVoteMap.find(playerGuid); if (voterItr == m_rollVoteMap.end()) return false; voterItr->second.Vote = vote; if (vote != RollVote::Pass && vote != RollVote::NotValid) voterItr->second.RollNumber = urand(1, 100); switch (vote) { case RollVote::Pass: // Player choose pass { SendRoll(playerGuid, -1, RollVote::Pass, {}); break; } case RollVote::Need: // player choose Need { SendRoll(playerGuid, 0, RollVote::Need, {}); player->UpdateCriteria(CriteriaType::RollAnyNeed, 1); break; } case RollVote::Greed: // player choose Greed { SendRoll(playerGuid, -1, RollVote::Greed, {}); player->UpdateCriteria(CriteriaType::RollAnyGreed, 1); break; } case RollVote::Disenchant: // player choose Disenchant { SendRoll(playerGuid, -1, RollVote::Disenchant, {}); player->UpdateCriteria(CriteriaType::RollAnyGreed, 1); break; } default: // Roll removed case return false; } return true; } // check if we can found a winner for this roll or if timer is expired bool LootRoll::UpdateRoll() { RollVoteMap::const_iterator winnerItr = m_rollVoteMap.end(); if (AllPlayerVoted(winnerItr) || m_endTime <= GameTime::Now()) { Finish(winnerItr); return true; } return false; } bool LootRoll::IsLootItem(ObjectGuid const& lootObject, uint32 lootListId) const { return m_loot->GetGUID() == lootObject && m_lootItem->LootListId == lootListId; } /** * \brief Check if all player have voted and return true in that case. Also return current winner. * \param winnerItr > will be different than m_rollCoteMap.end() if winner exist. (Someone voted greed or need) * \returns true if all players voted **/ bool LootRoll::AllPlayerVoted(RollVoteMap::const_iterator& winnerItr) { uint32 notVoted = 0; bool isSomeoneNeed = false; winnerItr = m_rollVoteMap.end(); for (RollVoteMap::const_iterator itr = m_rollVoteMap.begin(); itr != m_rollVoteMap.end(); ++itr) { switch (itr->second.Vote) { case RollVote::Need: if (!isSomeoneNeed || winnerItr == m_rollVoteMap.end() || itr->second.RollNumber > winnerItr->second.RollNumber) { isSomeoneNeed = true; // first passage will force to set winner because need is prioritized winnerItr = itr; } break; case RollVote::Greed: case RollVote::Disenchant: if (!isSomeoneNeed) // if at least one need is detected then winner can't be a greed { if (winnerItr == m_rollVoteMap.end() || itr->second.RollNumber > winnerItr->second.RollNumber) winnerItr = itr; } break; // Explicitly passing excludes a player from winning loot, so no action required. case RollVote::Pass: break; case RollVote::NotEmitedYet: ++notVoted; break; default: break; } } return notVoted == 0; } Optional LootRoll::GetItemDisenchantLootId() const { WorldPackets::Item::ItemInstance itemInstance; itemInstance.Initialize(*m_lootItem); BonusData bonusData; bonusData.Initialize(itemInstance); if (!bonusData.CanDisenchant) return {}; if (bonusData.DisenchantLootId) return bonusData.DisenchantLootId; ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(m_lootItem->itemid); // ignore temporary item level scaling (pvp or timewalking) uint32 itemLevel = Item::GetItemLevel(itemTemplate, bonusData, bonusData.RequiredLevel, 0, 0, 0, 0, false, 0); ItemDisenchantLootEntry const* disenchantLoot = Item::GetBaseDisenchantLoot(itemTemplate, bonusData.Quality, itemLevel); if (!disenchantLoot) return {}; return disenchantLoot->ID; } Optional LootRoll::GetItemDisenchantSkillRequired() const { WorldPackets::Item::ItemInstance itemInstance; itemInstance.Initialize(*m_lootItem); BonusData bonusData; bonusData.Initialize(itemInstance); if (!bonusData.CanDisenchant) return {}; ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(m_lootItem->itemid); // ignore temporary item level scaling (pvp or timewalking) uint32 itemLevel = Item::GetItemLevel(itemTemplate, bonusData, bonusData.RequiredLevel, 0, 0, 0, 0, false, 0); ItemDisenchantLootEntry const* disenchantLoot = Item::GetBaseDisenchantLoot(itemTemplate, bonusData.Quality, itemLevel); if (!disenchantLoot) return {}; return disenchantLoot->SkillRequired; } // terminate the roll void LootRoll::Finish(RollVoteMap::const_iterator winnerItr) { m_lootItem->is_blocked = false; if (winnerItr == m_rollVoteMap.end()) { SendAllPassed(); } else { m_lootItem->rollWinnerGUID = winnerItr->first; SendLootRollWon(winnerItr->first, winnerItr->second.RollNumber, winnerItr->second.Vote); if (Player* player = ObjectAccessor::FindConnectedPlayer(winnerItr->first)) { if (winnerItr->second.Vote == RollVote::Need) player->UpdateCriteria(CriteriaType::RollNeed, m_lootItem->itemid, winnerItr->second.RollNumber); else if (winnerItr->second.Vote == RollVote::Disenchant) player->UpdateCriteria(CriteriaType::CastSpell, 13262); else player->UpdateCriteria(CriteriaType::RollGreed, m_lootItem->itemid, winnerItr->second.RollNumber); if (winnerItr->second.Vote == RollVote::Disenchant) { Loot loot(m_map, m_loot->GetOwnerGUID(), LOOT_DISENCHANTING, nullptr); loot.FillLoot(*GetItemDisenchantLootId(), LootTemplates_Disenchant, player, true, false, LOOT_MODE_DEFAULT, ItemContext::NONE); if (!loot.AutoStore(player, NULL_BAG, NULL_SLOT, true)) { for (uint32 i = 0; i < loot.items.size(); ++i) if (LootItem* disenchantLoot = loot.LootItemInSlot(i, player)) if (disenchantLoot->type == LootItemType::Item) player->SendItemRetrievalMail(disenchantLoot->itemid, disenchantLoot->count, disenchantLoot->context); } else m_loot->NotifyItemRemoved(m_lootItem->LootListId, m_map); } else player->StoreLootItem(m_loot->GetOwnerGUID(), m_lootItem->LootListId, m_loot); } } m_isStarted = false; } // // --------- Loot --------- // Loot::Loot(Map* map, ObjectGuid owner, LootType type, Group const* group) : gold(0), unlootedCount(0), loot_type(type), _guid(map ? ObjectGuid::Create(map->GetId(), 0, map->GenerateLowGuid()) : ObjectGuid::Empty), _owner(owner), _itemContext(ItemContext::NONE), _lootMethod(group ? group->GetLootMethod() : FREE_FOR_ALL), _lootMaster(group ? group->GetMasterLooterGuid() : ObjectGuid::Empty), _wasOpened(false), _changed(false), _dungeonEncounterId(0) { } Loot::~Loot() { GuidSet activeLooters = std::move(PlayersLooting); for (ObjectGuid playerGuid : activeLooters) if (Player* player = ObjectAccessor::FindConnectedPlayer(playerGuid)) player->GetSession()->DoLootRelease(this); } void Loot::NotifyLootList(Map const* map) const { WorldPackets::Loot::LootList lootList; lootList.Owner = GetOwnerGUID(); lootList.LootObj = GetGUID(); if (GetLootMethod() == MASTER_LOOT && hasOverThresholdItem()) lootList.Master = GetLootMasterGUID(); if (!roundRobinPlayer.IsEmpty()) lootList.RoundRobinWinner = roundRobinPlayer; lootList.Write(); for (ObjectGuid allowedLooterGuid : _allowedLooters) if (Player* allowedLooter = ObjectAccessor::GetPlayer(map, allowedLooterGuid)) allowedLooter->SendDirectMessage(lootList.GetRawPacket()); } void Loot::NotifyItemRemoved(uint8 lootListId, Map const* map) { // notify all players that are looting this that the item was removed // convert the index to the slot the player sees for (auto itr = PlayersLooting.begin(); itr != PlayersLooting.end();) { LootItem const& item = items[lootListId]; if (item.GetAllowedLooters().find(*itr) == item.GetAllowedLooters().end()) { ++itr; continue; } if (Player* player = ObjectAccessor::GetPlayer(map, *itr)) { player->SendNotifyLootItemRemoved(GetGUID(), GetOwnerGUID(), lootListId); ++itr; } else itr = PlayersLooting.erase(itr); } } void Loot::NotifyMoneyRemoved(Map const* map) { // notify all players that are looting this that the money was removed for (auto itr = PlayersLooting.begin(); itr != PlayersLooting.end();) { if (Player* player = ObjectAccessor::GetPlayer(map, *itr)) { player->SendNotifyLootMoneyRemoved(GetGUID()); ++itr; } else itr = PlayersLooting.erase(itr); } } void Loot::OnLootOpened(Map* map, Player* looter) { AddLooter(looter->GetGUID()); if (!_wasOpened) { _wasOpened = true; if (_lootMethod == GROUP_LOOT || _lootMethod == NEED_BEFORE_GREED) { uint16 maxEnchantingSkill = 0; for (ObjectGuid allowedLooterGuid : _allowedLooters) if (Player* allowedLooter = ObjectAccessor::GetPlayer(map, allowedLooterGuid)) maxEnchantingSkill = std::max(maxEnchantingSkill, allowedLooter->GetSkillValue(SKILL_ENCHANTING)); for (uint32 lootListId = 0; lootListId < items.size(); ++lootListId) { LootItem& item = items[lootListId]; if (!item.is_blocked) continue; auto&& [itr, inserted] = _rolls.try_emplace(lootListId); if (!itr->second.TryToStart(map, *this, lootListId, maxEnchantingSkill)) _rolls.erase(itr); } if (!_rolls.empty()) _changed = true; } else if (_lootMethod == MASTER_LOOT) { if (looter->GetGUID() == _lootMaster) { WorldPackets::Loot::MasterLootCandidateList masterLootCandidateList; masterLootCandidateList.LootObj = GetGUID(); masterLootCandidateList.Players = _allowedLooters; looter->SendDirectMessage(masterLootCandidateList.Write()); } } } // Flag tracking quests as completed after all items were scanned for this player (some might depend on this quest not being completed) //if (!_mailUnlootedItems) if (std::vector* ffaItems = Trinity::Containers::MapGetValuePtr(PlayerFFAItems, looter->GetGUID())) AutoStoreTrackingQuests(looter, *ffaItems); } bool Loot::HasAllowedLooter(ObjectGuid const& looter) const { return _allowedLooters.find(looter) != _allowedLooters.end(); } void Loot::generateMoneyLoot(uint32 minAmount, uint32 maxAmount) { if (maxAmount > 0) { if (maxAmount <= minAmount) gold = uint32(maxAmount * sWorld->getRate(RATE_DROP_MONEY)); else if ((maxAmount - minAmount) < 32700) gold = uint32(urand(minAmount, maxAmount) * sWorld->getRate(RATE_DROP_MONEY)); else gold = uint32(urand(minAmount >> 8, maxAmount >> 8) * sWorld->getRate(RATE_DROP_MONEY)) << 8; } } // Calls processor of corresponding LootTemplate (which handles everything including references) bool Loot::FillLoot(uint32 lootId, LootStore const& store, Player* lootOwner, bool personal, bool noEmptyError, uint16 lootMode /*= LOOT_MODE_DEFAULT*/, ItemContext context /*= ItemContext::NONE*/) { // Must be provided if (!lootOwner) return false; LootTemplate const* tab = store.GetLootFor(lootId); if (!tab) { if (!noEmptyError) TC_LOG_ERROR("sql.sql", "Table '{}' loot id #{} used but it doesn't have records.", store.GetName(), lootId); return false; } _itemContext = context; items.reserve(MAX_NR_LOOT_ITEMS); tab->Process(*this, store.IsRatesAllowed(), lootMode, 0); // Processing is done there, callback via Loot::AddItem() // Setting access rights for group loot case Group const* group = lootOwner->GetGroup(); if (!personal && group) { if (loot_type == LOOT_CORPSE) roundRobinPlayer = lootOwner->GetGUID(); for (GroupReference const& itr : group->GetMembers()) { Player* member = itr.GetSource(); // should actually be looted object instead of lootOwner but looter has to be really close so doesnt really matter if (member->IsAtGroupRewardDistance(lootOwner)) FillNotNormalLootFor(member); } for (LootItem& item : items) { if (!item.follow_loot_rules || item.freeforall || item.type != LootItemType::Item) continue; if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(item.itemid)) { if (proto->GetQuality() < uint32(group->GetLootThreshold())) item.is_underthreshold = true; else { switch (_lootMethod) { case MASTER_LOOT: case GROUP_LOOT: case NEED_BEFORE_GREED: { item.is_blocked = true; break; } default: break; } } } } } // ... for personal loot else FillNotNormalLootFor(lootOwner); return true; } // Inserts the item into the loot (called by LootTemplate processors) void Loot::AddItem(LootStoreItem const& item) { 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); 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; } case LootStoreItem::Type::TrackingQuest: { LootItem generatedLoot(item); generatedLoot.count = 1; generatedLoot.LootListId = items.size(); items.push_back(generatedLoot); break; } default: break; } } bool Loot::AutoStore(Player* player, uint8 bag, uint8 slot, bool broadcast, bool createdByPlayer) { bool allLooted = true; for (uint32 i = 0; i < items.size(); ++i) { NotNormalLootItem* ffaitem = nullptr; LootItem* lootItem = LootItemInSlot(i, player, &ffaitem); if (!lootItem || lootItem->is_looted) continue; if (!lootItem->HasAllowedLooter(player->GetGUID())) continue; if (lootItem->is_blocked) continue; // dont allow protected item to be looted by someone else if (!lootItem->rollWinnerGUID.IsEmpty() && lootItem->rollWinnerGUID != GetGUID()) continue; switch (lootItem->type) { 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); } else player->ApplyItemLootedSpell(sObjectMgr->GetItemTemplate(lootItem->itemid)); break; } case LootItemType::Currency: player->ModifyCurrency(lootItem->itemid, lootItem->count, CurrencyGainSource::Loot); break; case LootItemType::TrackingQuest: if (Quest const* quest = sObjectMgr->GetQuestTemplate(lootItem->itemid)) player->RewardQuest(quest, LootItemType::Item, 0, player, false); break; } if (ffaitem) ffaitem->is_looted = true; if (!lootItem->freeforall) lootItem->is_looted = true; --unlootedCount; } return allLooted; } void Loot::AutoStoreTrackingQuests(Player* player, NotNormalLootItemList& ffaItems) { for (NotNormalLootItem& ffaItem : ffaItems) { if (items[ffaItem.LootListId].type != LootItemType::TrackingQuest) continue; --unlootedCount; ffaItem.is_looted = true; if (Quest const* quest = sObjectMgr->GetQuestTemplate(items[ffaItem.LootListId].itemid)) player->RewardQuest(quest, LootItemType::Item, 0, player, false); } } void Loot::LootMoney() { gold = 0; _changed = true; } LootItem const* Loot::GetItemInSlot(uint32 lootListId) const { if (lootListId < items.size()) return &items[lootListId]; return nullptr; } LootItem* Loot::LootItemInSlot(uint32 lootListId, Player const* player, NotNormalLootItem** ffaItem) { if (lootListId >= items.size()) return nullptr; LootItem* item = &items[lootListId]; bool is_looted = item->is_looted; if (item->freeforall) { auto itr = PlayerFFAItems.find(player->GetGUID()); if (itr != PlayerFFAItems.end()) { for (NotNormalLootItem& notNormalLootItem : *itr->second) { if (notNormalLootItem.LootListId == lootListId) { is_looted = notNormalLootItem.is_looted; if (ffaItem) *ffaItem = ¬NormalLootItem; break; } } } } if (is_looted) return nullptr; _changed = true; return item; } // return true if there is any item that is lootable for any player (not quest item, FFA or conditional) bool Loot::hasItemForAll() const { // Gold is always lootable if (gold) return true; for (LootItem const& item : items) if (!item.is_looted && item.follow_loot_rules && !item.freeforall && item.conditions.IsEmpty()) return true; return false; } // return true if there is any FFA, quest or conditional item for the player. bool Loot::hasItemFor(Player const* player) const { // quest items for (LootItem const& lootItem : items) 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())) if (std::ranges::any_of(*ffaItems, &NotNormalLootItem::is_looted)) return true; return false; } // return true if there is any item over the group threshold (i.e. not underthreshold). bool Loot::hasOverThresholdItem() const { for (uint8 i = 0; i < items.size(); ++i) { if (!items[i].is_looted && !items[i].is_underthreshold && !items[i].freeforall) return true; } return false; } void Loot::BuildLootResponse(WorldPackets::Loot::LootResponse& packet, Player const* viewer) const { packet.Coins = gold; for (LootItem const& item : items) { Optional uiType = item.GetUiTypeForPlayer(viewer, *this); if (!uiType) continue; switch (item.type) { case LootItemType::Item: { WorldPackets::Loot::LootItemData& lootItem = packet.Items.emplace_back(); lootItem.LootListID = item.LootListId; lootItem.Type = item.type; 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; } } } void Loot::Update() { for (auto itr = _rolls.begin(); itr != _rolls.end(); ) { if (itr->second.UpdateRoll()) itr = _rolls.erase(itr); else ++itr; } } void Loot::FillNotNormalLootFor(Player* player) { ObjectGuid plguid = player->GetGUID(); _allowedLooters.insert(plguid); std::unique_ptr ffaItems = std::make_unique(); for (LootItem& item : items) { if (!item.AllowedForPlayer(player, this)) continue; item.AddAllowedLooter(player); if (item.freeforall) { ffaItems->emplace_back(item.LootListId); ++unlootedCount; } else if (!item.is_counted) { item.is_counted = true; ++unlootedCount; } } if (!ffaItems->empty()) { // TODO: flag immediately for loot that is supposed to be mailed if unlooted, otherwise flag when sending SMSG_LOOT_RESPONSE //if (_mailUnlootedItems) // AutoStoreTrackingQuests(player, *ffaItems); PlayerFFAItems[player->GetGUID()] = std::move(ffaItems); } } // // --------- AELootResult --------- // void AELootResult::Add(Item* item, uint8 count, LootType lootType, uint32 dungeonEncounterId) { auto itr = _byItem.find(item); if (itr != _byItem.end()) _byOrder[itr->second].count += count; else { _byItem[item] = _byOrder.size(); ResultValue value; value.item = item; value.count = count; value.lootType = lootType; value.dungeonEncounterId = dungeonEncounterId; _byOrder.push_back(value); } } AELootResult::OrderedStorage::const_iterator AELootResult::begin() const { return _byOrder.begin(); } AELootResult::OrderedStorage::const_iterator AELootResult::end() const { return _byOrder.end(); }