#include "aoe_loot.h" #include "ScriptMgr.h" #include "LootMgr.h" #include "ServerScript.h" #include "WorldSession.h" #include "WorldPacket.h" #include "Player.h" #include "Chat.h" #include "ChatCommand.h" #include "ChatCommandArgs.h" #include "Creature.h" #include "Config.h" #include "Map.h" #include "Corpse.h" #include "Group.h" #include "ObjectMgr.h" #include #include using namespace Acore::ChatCommands; using namespace WorldPackets; // Thread-safe player AoE loot preference storage (renamed) std::map playerAoeLootPreferences; std::mutex AoeLootPreferencesMutex; // Helper function to check if loot system module is enabled bool IsAoeLootModuleEnabled() { return sConfigMgr->GetOption("AoeLoot.EnableMod", true); } bool IsPlayerEligibleForQuestItem(Player* player, LootItem const& item) { ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item.itemid); if (!itemTemplate || itemTemplate->Class != ITEM_CLASS_QUEST) { return false; } // For quest starter items if (itemTemplate->StartQuest) { uint32 prevQuestId = 0; if (Quest const* startQuest = sObjectMgr->GetQuestTemplate(itemTemplate->StartQuest)) prevQuestId = startQuest->GetPrevQuestId(); // Already completed the quest if (player->GetQuestStatus(itemTemplate->StartQuest) != QUEST_STATUS_NONE) return false; // Already has the item and it's unique if (itemTemplate->MaxCount && player->HasItemCount(item.itemid, itemTemplate->MaxCount)) return false; // Has not completed prerequisite quest if (prevQuestId && !player->GetQuestRewardStatus(prevQuestId)) return false; return true; } // For normal quest items (objectives) std::vector questIds; for (auto const& questPair : sObjectMgr->GetQuestTemplates()) { Quest const* quest = questPair.second; if (!quest) continue; for (uint32 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) { if (quest->RequiredItemId[i] == item.itemid) { questIds.push_back(quest->GetQuestId()); break; } } } for (uint32 questId : questIds) { if (player->GetQuestStatus(questId) == QUEST_STATUS_INCOMPLETE) { uint32 requiredCount = 1; Quest const* quest = sObjectMgr->GetQuestTemplate(questId); if (quest) { for (uint32 i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; ++i) { if (quest->RequiredItemId[i] == item.itemid) { requiredCount = quest->RequiredItemCount[i]; break; } } } if (player->GetItemCount(item.itemid, true) < requiredCount) return true; } } return false; } // Helper function to get configured loot method from config value LootMethod GetLootMethodFromConfig(uint32 configValue) { switch (configValue) { case 0: return FREE_FOR_ALL; case 1: return ROUND_ROBIN; case 2: return MASTER_LOOT; case 3: return GROUP_LOOT; case 4: return NEED_BEFORE_GREED; default: return GROUP_LOOT; } } // Helper function to apply default loot settings to a group void ApplyDefaultLootSettings(Group* group, Player* leaderOrPlayer) { if (!group || !leaderOrPlayer) return; uint32 defaultLootMethodConfig = sConfigMgr->GetOption("AoeLoot.DefaultLootMethod", 3); LootMethod lootMethod = GetLootMethodFromConfig(defaultLootMethodConfig); group->SetLootMethod(lootMethod); if (lootMethod == NEED_BEFORE_GREED || lootMethod == GROUP_LOOT) { uint32 defaultLootThreshold = sConfigMgr->GetOption("AoeLoot.DefaultLootThreshold", 2); group->SetLootThreshold(ItemQualities(defaultLootThreshold)); } if (lootMethod == MASTER_LOOT) { group->SetMasterLooterGuid(leaderOrPlayer->GetGUID()); } } // Define the roll vote enum value - use the correct RollVote enum #ifndef NOT_EMITED_YET #define NOT_EMITED_YET RollVote(0) #endif // Helper function to check if AOE loot is enabled for current context bool AoeLootCommandScript::IsAoeLootEnabledForPlayer(Player* player) { uint32 AoeLootMode = sConfigMgr->GetOption("AoeLoot.EnableAOELoot", 2); switch (AoeLootMode) { case 0: // Disabled return false; case 1: // Enabled for solo play only return !player->GetGroup(); case 2: // Enabled for both solo and group play return true; default: // Enabled for both solo and group play return true; } } bool AoeLootServer::CanPacketReceive(WorldSession* session, WorldPacket& packet) { if (!IsAoeLootModuleEnabled()) return true; if (packet.GetOpcode() == CMSG_LOOT) { Player* player = session->GetPlayer(); if (!player) return true; // Check if AOE loot is enabled for this player's context if (!AoeLootCommandScript::IsAoeLootEnabledForPlayer(player)) return true; // Additional safety checks if (!player->IsInWorld() || player->isDead()) return true; // Extract target GUID from loot packet to determine loot type ObjectGuid targetGuid; packet >> targetGuid; // Only trigger AOE loot for CREATURE corpses (not other loot types) if (Creature* creature = player->GetMap()->GetCreature(targetGuid)) { // Skip if creature is alive (pickpocketing) if (creature->IsAlive()) { if (player->IsClass(CLASS_ROGUE, CLASS_CONTEXT_ABILITY) && creature->loot.loot_type == LOOT_PICKPOCKETING) { // This is pickpocketing - let normal loot proceed return true; } // For other live creature interactions, don't trigger AOE return true; } // Only trigger AOE for dead creature corpses if (!creature->isDead()) return true; // Proceed with AOE loot for dead creatures } else { // Target is NOT a creature - don't trigger AOE loot // This covers: GameObjects, Items, Corpses (player), etc. return true; } uint64 guid = player->GetGUID().GetRawValue(); // Check if player has explicitly disabled AOE loot (thread-safe) { std::lock_guard lock(AoeLootPreferencesMutex); auto it = playerAoeLootPreferences.find(guid); if (it != playerAoeLootPreferences.end() && !it->second) { // Let normal looting proceed return true; } } // Only trigger AOE loot if player is not already looting if (player->GetLootGUID().IsEmpty()) { ChatHandler handler(player->GetSession()); handler.ParseCommands(".AoeLoot lootall"); } } return true; } ChatCommandTable AoeLootCommandScript::GetCommands() const { static ChatCommandTable AoeLootSubCommandTable = { { "lootall", TriggerAoeLootCommand, SEC_PLAYER, Console::No }, { "on", EnableAoeLootCommand, SEC_PLAYER, Console::No }, { "off", DisableAoeLootCommand, SEC_PLAYER, Console::No } }; static ChatCommandTable AoeLootCommandTable = { { "AoeLoot", AoeLootSubCommandTable } }; return AoeLootCommandTable; } bool AoeLootCommandScript::EnableAoeLootCommand(ChatHandler* handler, Optional) { if (!IsAoeLootModuleEnabled()) return true; Player* player = handler->GetSession()->GetPlayer(); if (!player || !IsAoeLootEnabledForPlayer(player)) return true; uint64 guid = player->GetGUID().GetRawValue(); { std::lock_guard lock(AoeLootPreferencesMutex); playerAoeLootPreferences[guid] = true; } return true; } bool AoeLootCommandScript::DisableAoeLootCommand(ChatHandler* handler, Optional) { if (!IsAoeLootModuleEnabled()) return true; Player* player = handler->GetSession()->GetPlayer(); if (!player) return true; uint64 guid = player->GetGUID().GetRawValue(); // Thread-safe update { std::lock_guard lock(AoeLootPreferencesMutex); playerAoeLootPreferences[guid] = false; } return true; } bool AoeLootCommandScript::ValidateLootingDistance(Player* player, ObjectGuid lguid, float maxDistance) { if (!player) return false; // Use configured AOE distance if no specific distance provided if (maxDistance <= 0.0f) maxDistance = sConfigMgr->GetOption("AoeLoot.Range", 55.0f); if (lguid.IsGameObject()) { GameObject* go = player->GetMap()->GetGameObject(lguid); if (!go) return false; // Special cases for owned objects or fishing holes if (go->GetOwnerGUID() == player->GetGUID() || go->GetGoType() == GAMEOBJECT_TYPE_FISHINGHOLE) return true; return go->IsWithinDistInMap(player, maxDistance); } else if (lguid.IsItem()) { ::Item* pItem = player->GetItemByGuid(lguid); return (pItem != nullptr); // Items in inventory don't need distance check } else if (lguid.IsCorpse()) { Corpse* corpse = ObjectAccessor::GetCorpse(*player, lguid); if (!corpse) return false; return corpse->IsWithinDistInMap(player, maxDistance); } else // Creature { Creature* creature = player->GetMap()->GetCreature(lguid); if (!creature) return false; // For pickpocketing, use strict interaction distance if (creature->IsAlive() && player->IsClass(CLASS_ROGUE, CLASS_CONTEXT_ABILITY) && creature->loot.loot_type == LOOT_PICKPOCKETING) { return creature->IsWithinDistInMap(player, INTERACTION_DISTANCE); } // For corpses, use AOE distance but still validate return creature->IsWithinDistInMap(player, maxDistance); } } bool AoeLootCommandScript::ProcessCreatureGold(Player* player, Creature* creature) { if (!player || !creature) return false; // Validate distance before processing money if (!ValidateLootingDistance(player, creature->GetGUID())) { player->SendLootError(creature->GetGUID(), LOOT_ERROR_TOO_FAR); return false; } Loot* loot = &creature->loot; if (!loot || loot->gold == 0) return false; uint32 goldAmount = loot->gold; bool shareMoney = true; // Share by default for creature corpses if (shareMoney && player->GetGroup()) { Group* group = player->GetGroup(); std::vector playersNear; for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); if (member) playersNear.push_back(member); } uint32 goldPerPlayer = uint32((loot->gold) / (playersNear.size())); for (Player* groupMember : playersNear) { groupMember->ModifyMoney(goldPerPlayer); groupMember->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_MONEY, goldPerPlayer); WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1); data << uint32(goldPerPlayer); data << uint8(playersNear.size() > 1 ? 0 : 1); groupMember->GetSession()->SendPacket(&data); } } else { // No group - give all gold to the player player->ModifyMoney(loot->gold); player->UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_MONEY, loot->gold); WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1); data << uint32(loot->gold); data << uint8(1); player->GetSession()->SendPacket(&data); } // Mark the money as looted loot->gold = 0; loot->NotifyMoneyRemoved(); return true; } void AoeLootCommandScript::ReleaseAndCleanupLoot(ObjectGuid lguid, Player* player, Loot*) { player->SetLootGUID(ObjectGuid::Empty); player->SendLootRelease(lguid); player->RemoveUnitFlag(UNIT_FLAG_LOOTING); if (!player->IsInWorld()) return; Loot* loot = nullptr; if (lguid.IsGameObject()) { GameObject* go = player->GetMap()->GetGameObject(lguid); if (!go) { player->SendLootRelease(lguid); return; } // Validate distance for GameObjects if (!ValidateLootingDistance(player, lguid)) { player->SendLootRelease(lguid); return; } loot = &go->loot; } else if (lguid.IsCorpse()) { Corpse* corpse = ObjectAccessor::GetCorpse(*player, lguid); if (!corpse) return; // Validate distance for corpses if (!ValidateLootingDistance(player, lguid)) return; loot = &corpse->loot; } else if (lguid.IsItem()) { ::Item* pItem = player->GetItemByGuid(lguid); if (!pItem) return; // For items, we don't need to process loot further return; } else // Must be a creature { Creature* creature = player->GetMap()->GetCreature(lguid); if (!creature) return; // Validate distance for creatures (includes pickpocketing checks) if (!ValidateLootingDistance(player, lguid)) { player->SendLootError(lguid, LOOT_ERROR_TOO_FAR); return; } loot = &creature->loot; if (!loot) return; if (loot->isLooted()) { // skip pickpocketing loot for speed, skinning timer reduction is no-op in fact if (!creature->IsAlive()) creature->AllLootRemovedFromCorpse(); creature->RemoveDynamicFlag(UNIT_DYNFLAG_LOOTABLE); loot->clear(); } else { // if the round robin player release, reset it. if (player->GetGUID() == loot->roundRobinPlayer) { loot->roundRobinPlayer.Clear(); if (Group* group = player->GetGroup()) group->SendLooter(creature, nullptr); } // force dynflag update to update looter and lootable info creature->ForceValuesUpdateAtIndex(UNIT_DYNAMIC_FLAGS); } } // Player is not looking at loot list, he doesn't need to see updates on the loot list if (!lguid.IsItem() && loot) { loot->RemoveLooter(player->GetGUID()); } } bool AoeLootCommandScript::ProcessSingleLootSlot(Player* player, ObjectGuid lguid, uint8 lootSlot) { if (!IsAoeLootModuleEnabled()) return true; if (!player) return false; // Validate distance first if (!ValidateLootingDistance(player, lguid)) { player->SendLootError(lguid, LOOT_ERROR_TOO_FAR); return false; } Loot* loot = nullptr; // Get the loot object based on the GUID type if (lguid.IsGameObject()) { GameObject* go = player->GetMap()->GetGameObject(lguid); if (!go) { player->SendLootRelease(lguid); return false; } loot = &go->loot; } else if (lguid.IsItem()) { ::Item* pItem = player->GetItemByGuid(lguid); if (!pItem) { player->SendLootRelease(lguid); return false; } loot = &pItem->loot; } else if (lguid.IsCorpse()) { Corpse* bones = ObjectAccessor::GetCorpse(*player, lguid); if (!bones) { player->SendLootRelease(lguid); return false; } loot = &bones->loot; } else { Creature* creature = player->GetMap()->GetCreature(lguid); if (!creature) { player->SendLootRelease(lguid); return false; } loot = &creature->loot; } // Handle quest item slots if (lootSlot >= loot->items.size()) { // Calculate quest and FFA quest item slot uint8 questItemOffset = loot->items.size(); const QuestItemMap& questItems = loot->GetPlayerQuestItems(); auto q_itr = questItems.find(player->GetGUID()); if (q_itr != questItems.end()) { const QuestItemList* qlist = q_itr->second; uint8 questCount = qlist->size(); if (lootSlot < questItemOffset + questCount) { uint8 questIndex = lootSlot - questItemOffset; const QuestItem& qitem = (*qlist)[questIndex]; if (!qitem.is_looted) { if (qitem.index < loot->quest_items.size()) { LootItem& li = loot->quest_items[qitem.index]; player->AddItem(li.itemid, li.count); const_cast(qitem).is_looted = true; return true; } } } } // FFA quest items const QuestItemMap& ffaItems = loot->GetPlayerFFAItems(); auto ffa_itr = ffaItems.find(player->GetGUID()); if (ffa_itr != ffaItems.end()) { const QuestItemList* flist = ffa_itr->second; uint8 ffaOffset = questItemOffset + (q_itr != questItems.end() ? q_itr->second->size() : 0); uint8 ffaCount = flist->size(); if (lootSlot < ffaOffset + ffaCount) { uint8 ffaIndex = lootSlot - ffaOffset; const QuestItem& fitem = (*flist)[ffaIndex]; if (!fitem.is_looted) { if (fitem.index < loot->quest_items.size()) { LootItem& li = loot->quest_items[fitem.index]; player->AddItem(li.itemid, li.count); const_cast(fitem).is_looted = true; return true; } } } } return false; } // --- Begin standard loot logic for solo/group --- Group* group = player->GetGroup(); LootItem* lootItem = nullptr; InventoryResult msg = EQUIP_ERR_OK; bool isGroupLoot = false; bool isFFA = false; bool isMasterLooter = false; bool isRoundRobin = false; bool isThreshold = false; LootMethod lootMethod = GROUP_LOOT; uint8 groupLootThreshold = 2; // Use group's configured threshold, fallback to Uncommon isFFA = loot->items[lootSlot].freeforall; if (group) { lootMethod = group->GetLootMethod(); groupLootThreshold = group->GetLootThreshold(); isMasterLooter = (lootMethod == MASTER_LOOT); isRoundRobin = (lootMethod == ROUND_ROBIN); isGroupLoot = (lootMethod == GROUP_LOOT || lootMethod == NEED_BEFORE_GREED); ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(loot->items[lootSlot].itemid); isThreshold = (itemTemplate && itemTemplate->Quality >= groupLootThreshold); } // If in group and item meets group loot threshold, trigger group roll if (group && isGroupLoot && isThreshold && !isFFA && !isMasterLooter) { if (!loot->items[lootSlot].is_blocked) { Roll roll(lguid, loot->items[lootSlot]); roll.itemSlot = lootSlot; roll.setLoot(loot); for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next()) { Player* member = itr->GetSource(); if (member && member->IsInWorld() && !member->isDead()) { roll.playerVote[member->GetGUID()] = RollVote(0); roll.totalPlayersRolling++; } } group->SendLootStartRoll(60, player->GetMapId(), roll); } return true; } else if (group && isMasterLooter && !isFFA) { if (group->GetMasterLooterGuid() != player->GetGUID()) { player->SendLootError(lguid, LOOT_ERROR_MASTER_OTHER); return false; } } else if (group && isRoundRobin && loot->roundRobinPlayer && loot->roundRobinPlayer != player->GetGUID()) { return false; } sScriptMgr->OnPlayerAfterCreatureLoot(player); if (!loot) return false; bool isLootedBefore = loot->items[lootSlot].is_looted; lootItem = player->StoreLootItem(lootSlot, loot, msg); bool isLootedAfter = loot->items[lootSlot].is_looted; if (msg != EQUIP_ERR_OK && lguid.IsItem() && loot->loot_type != LOOT_CORPSE) { lootItem->is_looted = true; loot->NotifyItemRemoved(lootItem->itemIndex); loot->unlootedCount--; player->SendItemRetrievalMail(lootItem->itemid, lootItem->count); } if (loot->isLooted() && lguid.IsItem()) { ReleaseAndCleanupLoot(lguid, player, loot); } return true; } bool AoeLootCommandScript::TriggerAoeLootCommand(ChatHandler* handler, Optional /*args*/) { Player* player = handler->GetSession()->GetPlayer(); if (!IsAoeLootModuleEnabled()) return true; if (!player) return true; // Check if AOE loot is enabled for this player's context if (!IsAoeLootEnabledForPlayer(player)) { return true; } float range = sConfigMgr->GetOption("AoeLoot.Range", 55.0); std::list nearbyCorpses; player->GetDeadCreatureListInGrid(nearbyCorpses, range); // Filter valid corpses std::list validCorpses; for (auto* creature : nearbyCorpses) { if (!player || !creature) continue; if (!player->isAllowedToLoot(creature)) continue; if (!creature->HasDynamicFlag(UNIT_DYNFLAG_LOOTABLE)) continue; if (!creature->hasLootRecipient()) continue; if (!creature->isTappedBy(player)) continue; // Get player's group and check loot permissions based on group loot method Group* group = player->GetGroup(); if (group) { Loot* loot = &creature->loot; LootMethod lootMethod = group->GetLootMethod(); // For Round Robin loot, check if this player is the designated looter if (lootMethod == ROUND_ROBIN) { if (loot->roundRobinPlayer && loot->roundRobinPlayer != player->GetGUID()) { continue; } } // For Master Loot, check if this player is the master looter else if (lootMethod == MASTER_LOOT) { if (group->GetMasterLooterGuid() != player->GetGUID()) { continue; } } } validCorpses.push_back(creature); } // Process all valid corpses for (auto* creature : validCorpses) { ObjectGuid lguid = creature->GetGUID(); Loot* loot = &creature->loot; if (!loot) continue; // Double-check distance validation for security if (!ValidateLootingDistance(player, lguid)) { continue; } player->SetLootGUID(lguid); // Process all normal loot items for (uint8 lootSlot = 0; lootSlot < loot->items.size(); ++lootSlot) { ProcessSingleLootSlot(player, lguid, lootSlot); } // Explicitly process per-player quest items const QuestItemMap& questItems = loot->GetPlayerQuestItems(); auto q_itr = questItems.find(player->GetGUID()); if (q_itr != questItems.end()) { const QuestItemList* qlist = q_itr->second; for (uint8 i = 0; i < qlist->size(); ++i) { // Quest item slots follow after normal loot slots uint8 questSlot = loot->items.size() + i; ProcessSingleLootSlot(player, lguid, questSlot); } } // Explicitly process per-player FFA quest items const QuestItemMap& ffaItems = loot->GetPlayerFFAItems(); auto ffa_itr = ffaItems.find(player->GetGUID()); if (ffa_itr != ffaItems.end()) { const QuestItemList* flist = ffa_itr->second; for (uint8 i = 0; i < flist->size(); ++i) { // FFA item slots follow after normal loot slots and quest item slots uint8 ffaSlot = loot->items.size() + (q_itr != questItems.end() ? q_itr->second->size() : 0) + i; ProcessSingleLootSlot(player, lguid, ffaSlot); } } // After processing all slots, log how many lootable items remain size_t lootableCount = 0; for (const auto& item : loot->items) { if (!item.is_looted) ++lootableCount; } // Also count quest items and FFA quest items if (q_itr != questItems.end()) { for (const auto& item : *q_itr->second) { if (!item.is_looted) ++lootableCount; } } if (ffa_itr != ffaItems.end()) { for (const auto& item : *ffa_itr->second) { if (!item.is_looted) ++lootableCount; } } // Handle money if (loot->gold > 0) { uint32 goldAmount = loot->gold; ProcessCreatureGold(player, creature); } if (loot->isLooted()) { ReleaseAndCleanupLoot(lguid, player, loot); } } return true; } void AoeLootPlayer::OnPlayerLogin(Player* player) { if (!IsAoeLootModuleEnabled()) return; // If player is in a group, apply default loot settings if (Group* group = player->GetGroup()) { ApplyDefaultLootSettings(group, player); } uint32 AoeLootMode = sConfigMgr->GetOption("AoeLoot.EnableAOELoot", 2); } void AoeLootGroupScript::OnCreate(Group* group, Player* leader) { if (!IsAoeLootModuleEnabled()) return; if (!group || !leader) return; // Apply default loot settings to the group ApplyDefaultLootSettings(group, leader); } class AoeLootQuestParty : public PlayerScript { public: AoeLootQuestParty() : PlayerScript("AoeLootQuestParty") { } void OnPlayerBeforeFillQuestLootItem(Player* /*player*/, LootItem& item) override { ItemTemplate const* itemTemplate = sObjectMgr->GetItemTemplate(item.itemid); if (itemTemplate && itemTemplate->Quality == ITEM_QUALITY_NORMAL && itemTemplate->Class == ITEM_CLASS_QUEST && itemTemplate->SubClass == ITEM_SUBCLASS_QUEST && itemTemplate->Bonding == BIND_QUEST_ITEM) { item.freeforall = true; } } }; void AddSC_AoeLoot() { new AoeLootPlayer(); new AoeLootServer(); new AoeLootCommandScript(); new AoeLootGroupScript(); new AoeLootQuestParty(); }