initial commit

This commit is contained in:
crow
2025-08-03 15:11:57 -05:00
parent d42d763613
commit 698222fe46
8 changed files with 806 additions and 225 deletions

8
.editorconfig Normal file
View File

@@ -0,0 +1,8 @@
[*]
charset = utf-8
indent_style = space
indent_size = 4
tab_width = 4
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 80

62
.gitattributes vendored Normal file
View File

@@ -0,0 +1,62 @@
## AUTO-DETECT
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto eol=lf
# Text
*.conf
*.conf.dist
*.txt
*.md
*.cmake
# Bash
*.sh text
# Lua if lua module?
*.lua
# SQL
*.sql
# C++
*.c text
*.cc text
*.cxx text
*.cpp text
*.c++ text
*.hpp text
*.h text
*.h++ text
*.hh text
## For documentation
# Documents
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as an asset (binary) by default. If you want to treat it as text,
# comment-out the following line and uncomment the line after.
*.svg binary
#*.svg text
*.eps binary

49
.gitignore vendored Normal file
View File

@@ -0,0 +1,49 @@
!.gitignore
#
#Generic
#
.directory
.mailmap
*.orig
*.rej
*~
.hg/
*.kdev*
.DS_Store
CMakeLists.txt.user
*.bak
*.patch
*.diff
*.REMOTE.*
*.BACKUP.*
*.BASE.*
*.LOCAL.*
#
# IDE & other softwares
#
/.settings/
/.externalToolBuilders/*
# exclude in all levels
nbproject/
.sync.ffs_db
*.kate-swp
#
# Eclipse
#
*.pydevproject
.metadata
.gradle
tmp/
*.tmp
*.swp
*~.nib
local.properties
.settings/
.loadpath
.project
.cproject
.vscode/c_cpp_properties.json

38
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,38 @@
{
"files.associations": {
"list": "cpp",
"vector": "cpp",
"xstring": "cpp",
"xtree": "cpp",
"xutility": "cpp",
"atomic": "cpp",
"compare": "cpp",
"concepts": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"exception": "cpp",
"initializer_list": "cpp",
"iosfwd": "cpp",
"limits": "cpp",
"map": "cpp",
"memory": "cpp",
"mutex": "cpp",
"new": "cpp",
"ratio": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"system_error": "cpp",
"thread": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"utility": "cpp",
"xmemory": "cpp",
"xtr1common": "cpp"
}
}

View File

@@ -3,46 +3,58 @@
########################################
#
# AOELoot.Enable
# Description: Enables Module
# Default: 0 - (Disabled)
# 1 - (Enabled)
#
# Default: 2 - (Enabled for solo and group play)
# 1 - (Enabled for solo play only)
# 0 - (Disabled)
AOELoot.Enable = 1
AOELoot.Enable = 2
#
# AOELoot.Message
# Description: Enable message on login
# Default: 0 - (Disabled)
# 1 - (Enabled)
#
# Description: Enable module notification message on login
# Default: 0 - (Disabled)
# 1 - (Enabled)
AOELoot.Message = 0
#
# AOELoot.Range
# Description: Maximum reach range search loot.
# Default: 55.0
#
# Description: Radius for AoE looting area
# Default: 55.0
AOELoot.Range = 55.0
#
# AOELoot.Group
# Description: Enables area loot if the player is in a group
# Default: 1 (Enabled)
# Possible values: 0 - (Disabled)
# 1 - (Enabled)
#
# AOELoot.DefaultLootMethod
# Description: Sets the default loot method for newly created groups
# Values: 0 = Free For All (everyone can loot everything)
# 1 = Round Robin (items are distributed in turn)
# 2 = Master Loot (only master looter can distribute items)
# 3 = Group Loot (group rolls on items above threshold)
# 4 = Need Before Greed (players roll Need/Greed/Pass on items above threshold)
# Default: 3 (Group Loot)
AOELoot.Group = 1
AOELoot.DefaultLootMethod = 3
#
# AOELoot.DefaultLootThreshold
# Description: Item quality threshold for group loot rolls (applies only to Group Loot and Need Before Greed)
# Values: 0 = Poor (Gray)
# 1 = Common (White)
# 2 = Uncommon (Green)
# 3 = Rare (Blue)
# 4 = Epic (Purple)
# 5 = Legendary (Orange)
# Default: 2 (Uncommon)
AOELoot.DefaultLootThreshold = 2
# AOELoot.Debug
# Description: Enables debuging mode. This will print out the items Detected values of loot in the chat console. The values in the chat should match the looted values, give or take the main looted creature.
# Default: 0 (Disabled)
# Possible values: 0 - (Disabled)
# 1 - (Enabled)
#
# Description: Enables debugging mode.
# Default: 0 (Disabled)
# 1 - (Enabled)
AOELoot.Debug = 0
# Chat Commands
# .aoeloot off If module is enabled, disables AoE looting for player
# .aoeloot on If module is enabled, reenables AoE looting for player
# .aoeloot lootall Loot all enemies within AOELoot.Range
#

0
include.sh Normal file
View File

View File

@@ -18,49 +18,147 @@
#include "Corpse.h"
#include "Group.h"
#include "ObjectMgr.h"
#include <mutex>
#include <set>
using namespace Acore::ChatCommands;
using namespace WorldPackets;
std::map<uint64, bool> playerAoeLootEnabled;
// Thread-safe player AoE loot preference storage (renamed)
std::map<uint64, bool> playerAoeLootPreferences;
std::mutex aoeLootPreferencesMutex;
// Track groups that have had default loot settings applied by us
std::set<uint64> groupsWithAppliedDefaults;
std::mutex groupLootMethodMutex;
// 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:
LOG_WARN("module.aoe_loot", "Invalid AOELoot.DefaultLootMethod value: {}. Using Group Loot.", configValue);
return GROUP_LOOT;
}
}
// 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<uint32>("AOELoot.Enable", 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: // Invalid value, default to solo + group
LOG_WARN("module.aoe_loot", "Invalid AOELoot.Enable value: {}. Using default (2).", aoeLootMode);
return true;
}
}
bool AoeLootServer::CanPacketReceive(WorldSession* session, WorldPacket& packet)
{
if (packet.GetOpcode() == CMSG_LOOT)
{
Player* player = session->GetPlayer();
if (player)
{
uint64 guid = player->GetGUID().GetRawValue();
if (!player)
return true;
// Check if player has explicitly disabled AOE loot
if (playerAoeLootEnabled.find(guid) != playerAoeLootEnabled.end() &&
!playerAoeLootEnabled[guid])
// 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<std::mutex> lock(aoeLootPreferencesMutex);
auto it = playerAoeLootPreferences.find(guid);
if (it != playerAoeLootPreferences.end() && !it->second)
{
// Let normal looting proceed
return true;
}
// Trigger AOE loot when a player attempts to loot a corpse
}
// Only trigger AOE loot if player is not already looting
if (player->GetLootGUID().IsEmpty())
{
ChatHandler handler(player->GetSession());
handler.ParseCommands(".aoeloot startaoeloot");
handler.ParseCommands(".aoeloot lootall");
}
}
return true;
}
ChatCommandTable AoeLootCommandScript::GetCommands() const
{
static ChatCommandTable aoeLootSubCommandTable =
{
{ "startaoeloot", HandleStartAoeLootCommand, SEC_PLAYER, Console::No },
{ "on", HandleAoeLootOnCommand, SEC_PLAYER, Console::No },
{ "off", HandleAoeLootOffCommand, SEC_PLAYER, Console::No }
{ "lootall", TriggerAoeLootCommand, SEC_PLAYER, Console::No },
{ "on", EnableAoeLootCommand, SEC_PLAYER, Console::No },
{ "off", DisableAoeLootCommand, SEC_PLAYER, Console::No }
};
static ChatCommandTable aoeLootCommandTable =
@@ -71,126 +169,127 @@ ChatCommandTable AoeLootCommandScript::GetCommands() const
return aoeLootCommandTable;
}
bool AoeLootCommandScript::HandleAoeLootOnCommand(ChatHandler* handler, Optional<std::string> /*args*/)
bool AoeLootCommandScript::EnableAoeLootCommand(ChatHandler* handler, Optional<std::string> /*args*/)
{
Player* player = handler->GetSession()->GetPlayer();
if (!player)
return true;
uint64 guid = player->GetGUID().GetRawValue();
playerAoeLootEnabled[guid] = true;
// Check if AOE loot is enabled server-side
if (!IsAoeLootEnabledForPlayer(player))
{
uint32 aoeLootMode = sConfigMgr->GetOption<uint32>("AOELoot.Enable", 2);
switch (aoeLootMode)
{
case 0:
handler->PSendSysMessage("AOE looting is completely disabled on this server.");
break;
case 1:
handler->PSendSysMessage("AOE looting is only available for solo play (you are currently in a group).");
break;
default:
handler->PSendSysMessage("AOE looting is not available in your current context.");
break;
}
return true;
}
// Thread-safe update
{
std::lock_guard<std::mutex> lock(aoeLootPreferencesMutex);
playerAoeLootPreferences[guid] = true;
}
handler->PSendSysMessage("AOE looting has been enabled for your character. Type: '.aoeloot off' to turn AoE Looting Off.");
return true;
}
bool AoeLootCommandScript::HandleAoeLootOffCommand(ChatHandler* handler, Optional<std::string> /*args*/)
bool AoeLootCommandScript::DisableAoeLootCommand(ChatHandler* handler, Optional<std::string> /*args*/)
{
Player* player = handler->GetSession()->GetPlayer();
if (!player)
return true;
uint64 guid = player->GetGUID().GetRawValue();
playerAoeLootEnabled[guid] = false;
// Thread-safe update
{
std::lock_guard<std::mutex> lock(aoeLootPreferencesMutex);
playerAoeLootPreferences[guid] = false;
}
handler->PSendSysMessage("AOE looting has been disabled for your character. Type: '.aoeloot on' to turn AoE Looting on.");
return true;
}
void AoeLootCommandScript::ProcessLootRelease(ObjectGuid lguid, Player* player, Loot* loot)
// Rename ValidateLootDistance to ValidateLootingDistance
bool AoeLootCommandScript::ValidateLootingDistance(Player* player, ObjectGuid lguid, float maxDistance)
{
player->SetLootGUID(ObjectGuid::Empty);
player->SendLootRelease(lguid);
player->RemoveUnitFlag(UNIT_FLAG_LOOTING);
if (!player)
return false;
if (!player->IsInWorld())
return;
// Use configured AOE distance if no specific distance provided
if (maxDistance <= 0.0f)
maxDistance = sConfigMgr->GetOption<float>("AOELoot.Range", 55.0f);
if (lguid.IsGameObject())
{
GameObject* go = player->GetMap()->GetGameObject(lguid);
if (!go)
return false;
// not check distance for GO in case owned GO (fishing bobber case, for example) or Fishing hole GO
if (!go || ((go->GetOwnerGUID() != player->GetGUID() && go->GetGoType() != GAMEOBJECT_TYPE_FISHINGHOLE) && !go->IsWithinDistInMap(player)))
{
player->SendLootRelease(lguid);
return;
}
// Special cases for owned objects or fishing holes
if (go->GetOwnerGUID() == player->GetGUID() || go->GetGoType() == GAMEOBJECT_TYPE_FISHINGHOLE)
return true;
loot = &go->loot;
}
else if (lguid.IsCorpse()) // ONLY remove insignia at BG
{
Corpse* corpse = ObjectAccessor::GetCorpse(*player, lguid);
if (!corpse)
return;
loot = &corpse->loot;
return go->IsWithinDistInMap(player, maxDistance);
}
else if (lguid.IsItem())
{
Item* pItem = player->GetItemByGuid(lguid);
if (!pItem)
return;
return (pItem != nullptr); // Items in inventory don't need distance check
}
else // Must be a creature
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;
// Skip distance check for dead creatures (corpses)
// Keep distance check for pickpocketing (live creatures)
bool isPickpocketing = creature && creature->IsAlive() && player->IsClass(CLASS_ROGUE, CLASS_CONTEXT_ABILITY) && creature->loot.loot_type == LOOT_PICKPOCKETING;
// Only check distance for pickpocketing, not for corpse looting
if (isPickpocketing && !creature->IsWithinDistInMap(player, INTERACTION_DISTANCE))
// For pickpocketing, use strict interaction distance
if (creature->IsAlive() && player->IsClass(CLASS_ROGUE, CLASS_CONTEXT_ABILITY) &&
creature->loot.loot_type == LOOT_PICKPOCKETING)
{
player->SendLootError(lguid, LOOT_ERROR_TOO_FAR);
return;
return creature->IsWithinDistInMap(player, INTERACTION_DISTANCE);
}
loot = &creature->loot;
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->RemoveLooter(player->GetGUID());
// For corpses, use AOE distance but still validate
return creature->IsWithinDistInMap(player, maxDistance);
}
}
// Handle gold looting without distance checks
bool AoeLootCommandScript::ProcessLootMoney(Player* player, Creature* creature)
// Rename ProcessLootMoney to ProcessCreatureGold
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;
@@ -219,7 +318,7 @@ bool AoeLootCommandScript::ProcessLootMoney(Player* player, Creature* creature)
WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1);
data << uint32(goldPerPlayer);
data << uint8(playersNear.size() > 1 ? 0 : 1); // 0 is "Your share is..." and 1 is "You loot..."
data << uint8(playersNear.size() > 1 ? 0 : 1);
groupMember->GetSession()->SendPacket(&data);
}
}
@@ -231,7 +330,7 @@ bool AoeLootCommandScript::ProcessLootMoney(Player* player, Creature* creature)
WorldPacket data(SMSG_LOOT_MONEY_NOTIFY, 4 + 1);
data << uint32(loot->gold);
data << uint8(1); // "You loot..."
data << uint8(1);
player->GetSession()->SendPacket(&data);
}
@@ -242,19 +341,126 @@ bool AoeLootCommandScript::ProcessLootMoney(Player* player, Creature* creature)
return true;
}
bool AoeLootCommandScript::ProcessLootSlot(Player* player, ObjectGuid lguid, uint8 lootSlot)
// Rename ProcessLootRelease to ReleaseAndCleanupLoot
void AoeLootCommandScript::ReleaseAndCleanupLoot(ObjectGuid lguid, Player* player, Loot* /*originalLoot*/)
{
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());
}
}
// Rename ProcessLootSlot to ProcessSingleLootSlot
bool AoeLootCommandScript::ProcessSingleLootSlot(Player* player, ObjectGuid lguid, uint8 lootSlot)
{
if (!player)
return false;
// Validate distance first
if (!ValidateLootingDistance(player, lguid))
{
player->SendLootError(lguid, LOOT_ERROR_TOO_FAR);
return false;
}
Loot* loot = nullptr;
float aoeDistance = sConfigMgr->GetOption<float>("AOELoot.Range", 55.0f);
// Get the loot object based on the GUID type
if (lguid.IsGameObject())
{
GameObject* go = player->GetMap()->GetGameObject(lguid);
if (!go || ((go->GetOwnerGUID() != player->GetGUID() && go->GetGoType() != GAMEOBJECT_TYPE_FISHINGHOLE) && !go->IsWithinDistInMap(player, aoeDistance)))
if (!go)
{
player->SendLootRelease(lguid);
return false;
@@ -284,13 +490,9 @@ bool AoeLootCommandScript::ProcessLootSlot(Player* player, ObjectGuid lguid, uin
else
{
Creature* creature = player->GetMap()->GetCreature(lguid);
// Skip distance check for dead creatures (corpses)
// Keep distance check for pickpocketing (live creatures)
bool isPickpocketing = creature && creature->IsAlive() && player->IsClass(CLASS_ROGUE, CLASS_CONTEXT_ABILITY) && creature->loot.loot_type == LOOT_PICKPOCKETING;
// Only check distance for pickpocketing, not for corpse looting
if (isPickpocketing && !creature->IsWithinDistInMap(player, INTERACTION_DISTANCE))
if (!creature)
{
player->SendLootError(lguid, LOOT_ERROR_TOO_FAR);
player->SendLootRelease(lguid);
return false;
}
loot = &creature->loot;
@@ -305,12 +507,20 @@ bool AoeLootCommandScript::ProcessLootSlot(Player* player, ObjectGuid lguid, uin
LootItem* lootItem = nullptr;
InventoryResult msg = EQUIP_ERR_OK;
bool isGroupLoot = false;
bool isFFA = loot->items[lootSlot].freeforall;
bool isFFA = false;
bool isMasterLooter = false;
bool isRoundRobin = false;
bool isThreshold = false;
LootMethod lootMethod = GROUP_LOOT;
uint8 groupLootThreshold = 2; // Default: Uncommon
uint8 groupLootThreshold = 2; // Use group's configured threshold, fallback to Uncommon
// Bounds check for loot slot
if (lootSlot >= loot->items.size())
{
return false;
}
isFFA = loot->items[lootSlot].freeforall;
if (group)
{
@@ -319,8 +529,13 @@ bool AoeLootCommandScript::ProcessLootSlot(Player* player, ObjectGuid lguid, uin
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);
// Bounds check before accessing loot item
if (lootSlot < loot->items.size())
{
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
@@ -345,7 +560,7 @@ bool AoeLootCommandScript::ProcessLootSlot(Player* player, ObjectGuid lguid, uin
}
}
// Call SendLootStartRoll with 60 second countdown
// Call SendLootStartRoll using the group's configured roll timeout
group->SendLootStartRoll(60, player->GetMapId(), roll);
}
return true;
@@ -378,22 +593,24 @@ bool AoeLootCommandScript::ProcessLootSlot(Player* player, ObjectGuid lguid, uin
// If player is removing the last LootItem, delete the empty container
if (loot->isLooted() && lguid.IsItem())
{
ProcessLootRelease(lguid, player, loot);
ReleaseAndCleanupLoot(lguid, player, loot);
}
return true;
}
bool AoeLootCommandScript::HandleStartAoeLootCommand(ChatHandler* handler, Optional<std::string> /*args*/)
bool AoeLootCommandScript::TriggerAoeLootCommand(ChatHandler* handler, Optional<std::string> /*args*/)
{
if (!sConfigMgr->GetOption<bool>("AOELoot.Enable", true))
return true;
Player* player = handler->GetSession()->GetPlayer();
if (!player)
return true;
// Check if AOE loot is enabled for this player's context
if (!IsAoeLootEnabledForPlayer(player))
{
handler->PSendSysMessage("AOE looting is not available in your current context.");
return true;
}
float range = sConfigMgr->GetOption<float>("AOELoot.Range", 55.0);
bool debugMode = sConfigMgr->GetOption<bool>("AOELoot.Debug", false);
@@ -474,94 +691,46 @@ bool AoeLootCommandScript::HandleStartAoeLootCommand(ChatHandler* handler, Optio
{
ObjectGuid lguid = creature->GetGUID();
Loot* loot = &creature->loot;
if (validCorpses.size() <= 1)
{
break;
}
if (!loot)
continue;
// Double-check distance validation for security
if (!ValidateLootingDistance(player, lguid))
{
if (debugMode)
LOG_DEBUG("module.aoe_loot", "AOE Loot: Skipping creature {} - too far away", lguid.ToString());
continue;
}
player->SetLootGUID(lguid);
// Process quest items
QuestItemMap const& playerNonQuestNonFFAConditionalItems = loot->GetPlayerNonQuestNonFFAConditionalItems();
if (!playerNonQuestNonFFAConditionalItems.empty())
{
for (uint8 i = 0; i < playerNonQuestNonFFAConditionalItems.size(); ++i)
{
uint8 lootSlot = playerNonQuestNonFFAConditionalItems.size() + i;
ProcessLootSlot(player, lguid, lootSlot);
if (debugMode)
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted quest item in slot {}", lootSlot);
}
}
// Process quest items
QuestItemMap const& playerFFAItems = loot->GetPlayerFFAItems();
if (!playerFFAItems.empty())
{
for (uint8 i = 0; i < playerFFAItems.size(); ++i)
{
uint8 lootSlot = playerFFAItems.size() + i;
ProcessLootSlot(player, lguid, lootSlot);
if (debugMode)
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted quest item in slot {}", lootSlot);
}
}
// Process quest items
QuestItemMap const& playerQuestItems = loot->GetPlayerQuestItems();
if (!playerQuestItems.empty())
{
for (uint8 i = 0; i < playerQuestItems.size(); ++i)
{
uint8 lootSlot = playerQuestItems.size() + i;
ProcessLootSlot(player, lguid, lootSlot);
if (debugMode)
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted quest item in slot {}", lootSlot);
}
}
// Process quest items
if (!loot->quest_items.empty())
{
for (uint8 i = 0; i < loot->quest_items.size(); ++i)
{
uint8 lootSlot = loot->items.size() + i;
ProcessLootSlot(player, lguid, lootSlot);
if (debugMode)
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted quest item in slot {}", lootSlot);
}
}
// Process quest items
if (!loot->quest_items.empty())
{
for (uint8 i = 0; i < loot->quest_items.size(); ++i)
{
uint8 lootSlot = loot->items.size() + i;
ProcessLootSlot(player, lguid, lootSlot);
if (debugMode)
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted quest item in slot {}", lootSlot);
}
}
// Process regular items
// Process all regular items (includes quest items and FFA items automatically)
for (uint8 lootSlot = 0; lootSlot < loot->items.size(); ++lootSlot)
{
player->SetLootGUID(lguid);
ProcessLootSlot(player, lguid, lootSlot);
if (debugMode && lootSlot < loot->items.size())
ProcessSingleLootSlot(player, lguid, lootSlot);
if (debugMode)
{
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted item from slot {} (ID: {}) from {}", lootSlot, loot->items[lootSlot].itemid, lguid.ToString());
}
}
// Process quest items separately if they exist
for (uint8 i = 0; i < loot->quest_items.size(); ++i)
{
uint8 questLootSlot = loot->items.size() + i;
ProcessSingleLootSlot(player, lguid, questLootSlot);
if (debugMode)
{
LOG_DEBUG("module.aoe_loot", "AOE Loot: looted quest item in slot {}", questLootSlot);
}
}
// Handle money
if (loot->gold > 0)
{
uint32 goldAmount = loot->gold;
ProcessLootMoney(player, creature);
ProcessCreatureGold(player, creature);
if (debugMode)
{
LOG_DEBUG("module.aoe_loot", "AOE Loot: Looted {} copper from {}", goldAmount, lguid.ToString());
@@ -570,7 +739,7 @@ bool AoeLootCommandScript::HandleStartAoeLootCommand(ChatHandler* handler, Optio
if (loot->isLooted())
{
ProcessLootRelease(lguid, player, loot);
ReleaseAndCleanupLoot(lguid, player, loot);
}
}
return true;
@@ -579,10 +748,230 @@ bool AoeLootCommandScript::HandleStartAoeLootCommand(ChatHandler* handler, Optio
// Display login message to player
void AoeLootPlayer::OnPlayerLogin(Player* player)
{
if (sConfigMgr->GetOption<bool>("AOELoot.Enable", true) &&
sConfigMgr->GetOption<bool>("AOELoot.Message", true))
// Check if AOE loot is enabled at all (any mode > 0)
uint32 aoeLootMode = sConfigMgr->GetOption<uint32>("AOELoot.Enable", 2);
if (aoeLootMode > 0 && sConfigMgr->GetOption<bool>("AOELoot.Message", true))
{
ChatHandler(player->GetSession()).PSendSysMessage("AOE looting has been enabled for your character. Type: '.aoeloot off' to turn AoE Looting Off.");
std::string message = "AOE looting has been enabled for your character";
switch (aoeLootMode)
{
case 1:
message += " (solo play only)";
break;
case 2:
message += " (solo and group play)";
break;
}
message += ". Type: '.aoeloot off' to turn AoE Looting Off.";
ChatHandler(player->GetSession()).PSendSysMessage(message.c_str());
}
}
// Group script implementation
void AoeLootGroupScript::OnCreate(Group* group, Player* leader)
{
// Only apply default loot method to brand new groups, not reformed/rejoined groups
if (!group || !leader)
return;
// Only apply to truly new groups (single member = the leader)
// When a player rejoins an existing group, the group already has its loot method preserved
if (group->GetMembersCount() > 1)
return;
// Check if this is a playerbot scenario - if the leader is a bot, delay loot method setting
// to allow playerbot logic to handle leadership transfers when the player logs in
// Note: Since IsBot() may not be available, we'll use a more conservative approach
// and only apply defaults when we're certain it's a real player-led group
if (!leader->GetSession() || !leader->GetSession()->GetPlayer())
{
// No valid session - likely a bot or disconnected player
// Don't set loot method immediately
return;
}
// Apply default loot settings for real player leaders
ApplyDefaultLootSettings(group, leader);
}
// Handle leadership changes (important for playerbot scenarios)
void AoeLootGroupScript::OnChangeLeader(Group* group, ObjectGuid newLeaderGuid, ObjectGuid /*oldLeaderGuid*/)
{
if (!group)
return;
Player* newLeader = ObjectAccessor::FindPlayer(newLeaderGuid);
if (!newLeader)
return;
// Only apply default loot settings if:
// 1. The new leader is a real player (has valid session)
// 2. The loot method hasn't been manually changed from our defaults
if (newLeader->GetSession() && newLeader->GetSession()->GetPlayer())
{
// Safety check for valid group GUID
ObjectGuid groupGuid = group->GetGUID();
if (!groupGuid.IsEmpty())
{
uint64 groupId = groupGuid.GetRawValue();
if (groupId != 0) // Additional safety check
{
try
{
// Check if this group has had our defaults applied and if they've been changed
std::lock_guard<std::mutex> lock(groupLootMethodMutex);
if (groupsWithAppliedDefaults.find(groupId) == groupsWithAppliedDefaults.end())
{
// This group has never had our defaults applied
// It's safe to apply our defaults
ApplyDefaultLootSettings(group, newLeader);
}
else if (!IsLootMethodManuallySet(group))
{
// We've applied defaults before, but they haven't been manually changed
// This handles bot-to-player leadership transfers where we want to reapply defaults
ApplyDefaultLootSettings(group, newLeader);
}
// If the loot method has been manually changed, don't override it
}
catch (const std::exception& e)
{
LOG_ERROR("module.aoe_loot", "Failed to handle leadership change for group {}: {}", groupId, e.what());
}
}
else
{
LOG_WARN("module.aoe_loot", "Group has invalid GUID (0) in OnChangeLeader");
}
}
else
{
LOG_WARN("module.aoe_loot", "Group has empty GUID in OnChangeLeader");
}
}
}
// Handle group dissolution - clean up tracking
void AoeLootGroupScript::OnDisband(Group* group)
{
if (!group)
return;
// Remove this group from our tracking when it's disbanded
// Safety check for valid group GUID
ObjectGuid groupGuid = group->GetGUID();
if (!groupGuid.IsEmpty())
{
uint64 groupId = groupGuid.GetRawValue();
if (groupId != 0) // Additional safety check
{
try
{
std::lock_guard<std::mutex> lock(groupLootMethodMutex);
groupsWithAppliedDefaults.erase(groupId);
}
catch (const std::exception& e)
{
LOG_ERROR("module.aoe_loot", "Failed to clean up tracking for group {}: {}", groupId, e.what());
}
}
}
}
// Helper function to apply default loot settings
void AoeLootGroupScript::ApplyDefaultLootSettings(Group* group, Player* leader)
{
if (!group || !leader)
return;
// Get configured default loot method
uint32 defaultLootMethodConfig = sConfigMgr->GetOption<uint32>("AOELoot.DefaultLootMethod", 3); // 3 = Group Loot (default)
uint32 defaultLootThreshold = sConfigMgr->GetOption<uint32>("AOELoot.DefaultLootThreshold", 2); // Default: Uncommon
// Set the configured loot method for new groups
LootMethod lootMethod = GetLootMethodFromConfig(defaultLootMethodConfig);
group->SetLootMethod(lootMethod);
// If using Need Before Greed or Group Loot, also set the quality threshold
if (lootMethod == NEED_BEFORE_GREED || lootMethod == GROUP_LOOT)
{
group->SetLootThreshold(ItemQualities(defaultLootThreshold));
}
// If using Master Loot, set the leader as master looter
if (lootMethod == MASTER_LOOT && leader)
{
group->SetMasterLooterGuid(leader->GetGUID());
}
// Track that we've applied defaults to this group
// Add safety check for valid group GUID
ObjectGuid groupGuid = group->GetGUID();
if (!groupGuid.IsEmpty())
{
uint64 groupId = groupGuid.GetRawValue();
if (groupId != 0) // Additional safety check
{
try
{
std::lock_guard<std::mutex> lock(groupLootMethodMutex);
groupsWithAppliedDefaults.insert(groupId);
}
catch (const std::exception& e)
{
LOG_ERROR("module.aoe_loot", "Failed to track group {} in ApplyDefaultLootSettings: {}", groupId, e.what());
}
}
else
{
LOG_WARN("module.aoe_loot", "Group has invalid GUID (0) in ApplyDefaultLootSettings");
}
}
else
{
LOG_WARN("module.aoe_loot", "Group has empty GUID in ApplyDefaultLootSettings");
}
}
// Helper function to check if loot method appears to be manually set
bool AoeLootGroupScript::IsLootMethodManuallySet(Group* group)
{
if (!group)
return false;
// Get our configured defaults
uint32 defaultLootMethodConfig = sConfigMgr->GetOption<uint32>("AOELoot.DefaultLootMethod", 3);
uint32 defaultLootThreshold = sConfigMgr->GetOption<uint32>("AOELoot.DefaultLootThreshold", 2);
LootMethod expectedDefaultMethod = GetLootMethodFromConfig(defaultLootMethodConfig);
// If the current loot method or threshold differs from our defaults,
// it was likely changed manually by a player
if (group->GetLootMethod() != expectedDefaultMethod)
return true;
// For Group Loot and Need Before Greed, also check the threshold
if ((expectedDefaultMethod == GROUP_LOOT || expectedDefaultMethod == NEED_BEFORE_GREED) &&
group->GetLootThreshold() != defaultLootThreshold)
return true;
return false;
}
// Helper function implementation for AoeLootGroupScript
LootMethod AoeLootGroupScript::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; // Safe default
}
}
@@ -592,4 +981,5 @@ void AddSC_AoeLoot()
new AoeLootPlayer();
new AoeLootServer();
new AoeLootCommandScript();
new AoeLootGroupScript();
}

View File

@@ -15,6 +15,7 @@
#include <list>
#include <map>
#include <ObjectGuid.h>
#include <mutex>
using namespace Acore::ChatCommands;
@@ -22,7 +23,6 @@ class AoeLootServer : public ServerScript
{
public:
AoeLootServer() : ServerScript("AoeLootServer") {}
bool CanPacketReceive(WorldSession* session, WorldPacket& packet) override;
};
@@ -30,9 +30,7 @@ class AoeLootPlayer : public PlayerScript
{
public:
AoeLootPlayer() : PlayerScript("AoeLootPlayer") {}
void OnPlayerLogin(Player* player) override;
};
class AoeLootCommandScript : public CommandScript
@@ -41,16 +39,40 @@ public:
AoeLootCommandScript() : CommandScript("AoeLootCommandScript") {}
ChatCommandTable GetCommands() const override;
static bool HandleAoeLootOnCommand(ChatHandler* handler, Optional<std::string> args);
static bool HandleAoeLootOffCommand(ChatHandler* handler, Optional<std::string> args);
static bool HandleStartAoeLootCommand(ChatHandler* handler, Optional<std::string> args);
static bool ProcessLootSlot(Player* player, ObjectGuid lguid, uint8 lootSlot);
static bool ProcessLootMoney(Player* player, Creature* creature);
static void ProcessLootRelease(ObjectGuid lguid, Player* player, Loot* loot);
// Renamed command handlers (removed "Handle" prefix)
static bool EnableAoeLootCommand(ChatHandler* handler, Optional<std::string> args);
static bool DisableAoeLootCommand(ChatHandler* handler, Optional<std::string> args);
static bool TriggerAoeLootCommand(ChatHandler* handler, Optional<std::string> args);
// Renamed core processing functions
static bool ProcessSingleLootSlot(Player* player, ObjectGuid lguid, uint8 lootSlot);
static bool ProcessCreatureGold(Player* player, Creature* creature);
static void ReleaseAndCleanupLoot(ObjectGuid lguid, Player* player, Loot* loot);
// Renamed validation functions
static bool ValidateLootingDistance(Player* player, ObjectGuid lguid, float maxDistance = 0.0f);
static bool IsAoeLootEnabledForPlayer(Player* player);
};
class AoeLootGroupScript : public GroupScript
{
public:
AoeLootGroupScript() : GroupScript("AoeLootGroupScript") {}
void OnCreate(Group* group, Player* leader) override;
void OnChangeLeader(Group* group, ObjectGuid newLeaderGuid, ObjectGuid oldLeaderGuid) override;
void OnDisband(Group* group) override;
private:
// Helper function to convert config value to LootMethod enum
LootMethod GetLootMethodFromConfig(uint32 configValue);
// Helper function to apply default loot settings
void ApplyDefaultLootSettings(Group* group, Player* leader);
// Helper function to check if loot method appears to be manually set
bool IsLootMethodManuallySet(Group* group);
};
void AddSC_AoeLoot();
#endif //MODULE_AOELOOT_H