Add thread safety for group operations (#1816)

Fixes crashes and race conditions when bots perform group/guild/arena
operations by moving thread-unsafe code to world thread.

Potentially fixes #1124

## Changes

- Added operation queue system that runs in world thread
- Group operations (invite, remove, convert to raid, set leader) now
queued
- Arena formation refactored to use queue
- Guild operations changed to use packet queueing

## Testing

Set `MapUpdate.Threads` > 1 in worldserver.conf to enable multiple map
threads, then test:
- Group formation and disbanding
- Arena team formation
- Guild operations (invite, promote, demote, remove)

- Run with TSAN
cmake ../ \
  -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1" \
  -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" \
  -DCMAKE_INSTALL_PREFIX=/path/to/install \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo

build

export
TSAN_OPTIONS="log_path=tsan_report:halt_on_error=0:second_deadlock_stack=1"
./worldserver

The crashes/race conditions should no longer occur with concurrent map
threads.

## New Files

- `PlayerbotOperation.h` - Base class defining the operation interface
(Execute, IsValid, GetPriority)
- `PlayerbotOperations.h` - Concrete implementations:
GroupInviteOperation, GroupRemoveMemberOperation,
GroupConvertToRaidOperation, GroupSetLeaderOperation,
ArenaGroupFormationOperation
- `PlayerbotWorldThreadProcessor.h/cpp` - Singleton processor with
mutex-protected queue, processes operations in WorldScript::OnUpdate
hook, handles batch processing and validation

---------

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: SaW <swerkhoven@outlook.com>
Co-authored-by: bash <hermensb@gmail.com>
This commit is contained in:
blinkysc
2025-11-21 14:55:55 -06:00
committed by GitHub
parent d97870facd
commit 10213d8381
11 changed files with 1015 additions and 55 deletions

View File

@@ -725,8 +725,8 @@ std::string const PlayerbotAIConfig::GetTimestampStr()
// HH hour (2 digits 00-23) // HH hour (2 digits 00-23)
// MM minutes (2 digits 00-59) // MM minutes (2 digits 00-59)
// SS seconds (2 digits 00-59) // SS seconds (2 digits 00-59)
char buf[20]; char buf[32];
snprintf(buf, 20, "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour, snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour,
aTm->tm_min, aTm->tm_sec); aTm->tm_min, aTm->tm_sec);
return std::string(buf); return std::string(buf);
} }

View File

@@ -27,7 +27,9 @@
#include "PlayerbotAIConfig.h" #include "PlayerbotAIConfig.h"
#include "PlayerbotDbStore.h" #include "PlayerbotDbStore.h"
#include "PlayerbotFactory.h" #include "PlayerbotFactory.h"
#include "PlayerbotOperations.h"
#include "PlayerbotSecurity.h" #include "PlayerbotSecurity.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "SharedDefines.h" #include "SharedDefines.h"
@@ -85,7 +87,6 @@ public:
void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId) void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId)
{ {
// bot is loading
if (botLoading.find(playerGuid) != botLoading.end()) if (botLoading.find(playerGuid) != botLoading.end())
return; return;
@@ -195,7 +196,9 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con
} }
sRandomPlayerbotMgr->OnPlayerLogin(bot); sRandomPlayerbotMgr->OnPlayerLogin(bot);
OnBotLogin(bot);
auto op = std::make_unique<OnBotLoginOperation>(bot->GetGUID(), this);
sPlayerbotWorldProcessor->QueueOperation(std::move(op));
botLoading.erase(holder.GetGuid()); botLoading.erase(holder.GetGuid());
} }
@@ -316,11 +319,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid)
if (!botAI) if (!botAI)
return; return;
Group* group = bot->GetGroup(); // Queue group cleanup operation for world thread
if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster()) auto cleanupOp = std::make_unique<BotLogoutGroupCleanupOperation>(guid);
{ sPlayerbotWorldProcessor->QueueOperation(std::move(cleanupOp));
sPlayerbotDbStore->Save(botAI);
}
LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str()); LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str());
bot->SaveToDB(false, false); bot->SaveToDB(false, false);
@@ -549,6 +550,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot)
botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK); botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK);
// Queue group operations for world thread
if (master && master->GetGroup() && !group) if (master && master->GetGroup() && !group)
{ {
Group* mgroup = master->GetGroup(); Group* mgroup = master->GetGroup();
@@ -556,24 +558,29 @@ void PlayerbotHolder::OnBotLogin(Player* const bot)
{ {
if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup()) if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup())
{ {
mgroup->ConvertToRaid(); // Queue ConvertToRaid operation
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(master->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
} }
if (mgroup->isRaidGroup()) if (mgroup->isRaidGroup())
{ {
mgroup->AddMember(bot); // Queue AddMember operation
auto addOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(addOp));
} }
} }
else else
{ {
mgroup->AddMember(bot); // Queue AddMember operation
auto addOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(addOp));
} }
} }
else if (master && !group) else if (master && !group)
{ {
Group* newGroup = new Group(); // Queue group creation and AddMember operation
newGroup->Create(master); auto inviteOp = std::make_unique<GroupInviteOperation>(master->GetGUID(), bot->GetGUID());
sGroupMgr->AddGroup(newGroup); sPlayerbotWorldProcessor->QueueOperation(std::move(inviteOp));
newGroup->AddMember(bot);
} }
// if (master) // if (master)
// { // {

93
src/PlayerbotOperation.h Normal file
View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_OPERATION_H
#define _PLAYERBOT_OPERATION_H
#include "Common.h"
#include "ObjectGuid.h"
#include <memory>
/**
* @brief Base class for thread-unsafe operations that must be executed in the world thread
*
* PlayerbotOperation represents an operation that needs to be deferred from a map thread
* to the world thread for safe execution. Examples include group modifications, LFG operations,
* guild operations, etc.
*
* Thread Safety:
* - The constructor and data members must be thread-safe (use copies, not pointers)
* - Execute() is called in the world thread and can safely perform thread-unsafe operations
* - Subclasses must not store raw pointers to (core/world thread) game object (use ObjectGuid instead)
*/
class PlayerbotOperation
{
public:
virtual ~PlayerbotOperation() = default;
/**
* @brief Execute this operation in the world thread
*
* This method is called by PlayerbotWorldThreadProcessor::Update() which runs in the world thread.
* It's safe to perform any thread-unsafe operation here (Group, LFG, Guild, etc.)
*
* @return true if operation succeeded, false if it failed
*/
virtual bool Execute() = 0;
/**
* @brief Get the bot GUID this operation is for (optional)
*
* Used for logging and debugging purposes.
*
* @return ObjectGuid of the bot, or ObjectGuid::Empty if not applicable
*/
virtual ObjectGuid GetBotGuid() const { return ObjectGuid::Empty; }
/**
* @brief Get the operation priority (higher = more urgent)
*
* Priority levels:
* - 100: Critical (crash prevention, cleanup operations)
* - 50: High (player-facing operations like group invites)
* - 10: Normal (background operations)
* - 0: Low (statistics, logging)
*
* @return Priority value (0-100)
*/
virtual uint32 GetPriority() const { return 10; }
/**
* @brief Get a human-readable name for this operation
*
* Used for logging and debugging.
*
* @return Operation name
*/
virtual std::string GetName() const { return "Unknown Operation"; }
/**
* @brief Check if this operation is still valid
*
* Called before Execute() to check if the operation should still be executed.
* For example, if a bot logged out, group invite operations for that bot can be skipped.
*
* @return true if operation should be executed, false to skip
*/
virtual bool IsValid() const { return true; }
};
/**
* @brief Comparison operator for priority queue (higher priority first)
*/
struct PlayerbotOperationComparator
{
bool operator()(const std::unique_ptr<PlayerbotOperation>& a, const std::unique_ptr<PlayerbotOperation>& b) const
{
return a->GetPriority() < b->GetPriority(); // Lower priority goes to back of queue
}
};
#endif

500
src/PlayerbotOperations.h Normal file
View File

@@ -0,0 +1,500 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_OPERATIONS_H
#define _PLAYERBOT_OPERATIONS_H
#include "Group.h"
#include "GroupMgr.h"
#include "GuildMgr.h"
#include "ObjectAccessor.h"
#include "PlayerbotOperation.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotMgr.h"
#include "PlayerbotDbStore.h"
#include "RandomPlayerbotMgr.h"
// Group invite operation
class GroupInviteOperation : public PlayerbotOperation
{
public:
GroupInviteOperation(ObjectGuid botGuid, ObjectGuid targetGuid)
: m_botGuid(botGuid), m_targetGuid(targetGuid)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* target = ObjectAccessor::FindPlayer(m_targetGuid);
if (!bot || !target)
{
LOG_DEBUG("playerbots", "GroupInviteOperation: Bot or target not found");
return false;
}
// Check if target is already in a group
if (target->GetGroup())
{
LOG_DEBUG("playerbots", "GroupInviteOperation: Target {} is already in a group", target->GetName());
return false;
}
Group* group = bot->GetGroup();
// Create group if bot doesn't have one
if (!group)
{
group = new Group;
if (!group->Create(bot))
{
delete group;
LOG_ERROR("playerbots", "GroupInviteOperation: Failed to create group for bot {}", bot->GetName());
return false;
}
sGroupMgr->AddGroup(group);
LOG_DEBUG("playerbots", "GroupInviteOperation: Created new group for bot {}", bot->GetName());
}
// Convert to raid if needed (more than 5 members)
if (!group->isRaidGroup() && group->GetMembersCount() >= 5)
{
group->ConvertToRaid();
LOG_DEBUG("playerbots", "GroupInviteOperation: Converted group to raid");
}
// Add member to group
if (group->AddMember(target))
{
LOG_DEBUG("playerbots", "GroupInviteOperation: Successfully added {} to group", target->GetName());
return true;
}
else
{
LOG_ERROR("playerbots", "GroupInviteOperation: Failed to add {} to group", target->GetName());
return false;
}
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; } // High priority (player-facing)
std::string GetName() const override { return "GroupInvite"; }
bool IsValid() const override
{
// Check if bot still exists and is online
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* target = ObjectAccessor::FindPlayer(m_targetGuid);
return bot && target;
}
private:
ObjectGuid m_botGuid;
ObjectGuid m_targetGuid;
};
// Remove member from group
class GroupRemoveMemberOperation : public PlayerbotOperation
{
public:
GroupRemoveMemberOperation(ObjectGuid botGuid, ObjectGuid targetGuid)
: m_botGuid(botGuid), m_targetGuid(targetGuid)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* target = ObjectAccessor::FindPlayer(m_targetGuid);
if (!bot || !target)
return false;
Group* group = bot->GetGroup();
if (!group)
{
LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Bot is not in a group");
return false;
}
if (!group->IsMember(target->GetGUID()))
{
LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Target is not in bot's group");
return false;
}
group->RemoveMember(target->GetGUID());
LOG_DEBUG("playerbots", "GroupRemoveMemberOperation: Removed {} from group", target->GetName());
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; }
std::string GetName() const override { return "GroupRemoveMember"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
return bot != nullptr;
}
private:
ObjectGuid m_botGuid;
ObjectGuid m_targetGuid;
};
// Convert group to raid
class GroupConvertToRaidOperation : public PlayerbotOperation
{
public:
GroupConvertToRaidOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
if (!bot)
return false;
Group* group = bot->GetGroup();
if (!group)
{
LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Bot is not in a group");
return false;
}
if (group->isRaidGroup())
{
LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Group is already a raid");
return true; // Success - already in desired state
}
group->ConvertToRaid();
LOG_DEBUG("playerbots", "GroupConvertToRaidOperation: Converted group to raid");
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; }
std::string GetName() const override { return "GroupConvertToRaid"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
return bot != nullptr;
}
private:
ObjectGuid m_botGuid;
};
// Set group leader
class GroupSetLeaderOperation : public PlayerbotOperation
{
public:
GroupSetLeaderOperation(ObjectGuid botGuid, ObjectGuid newLeaderGuid)
: m_botGuid(botGuid), m_newLeaderGuid(newLeaderGuid)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid);
if (!bot || !newLeader)
return false;
Group* group = bot->GetGroup();
if (!group)
{
LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Bot is not in a group");
return false;
}
if (!group->IsMember(newLeader->GetGUID()))
{
LOG_DEBUG("playerbots", "GroupSetLeaderOperation: New leader is not in the group");
return false;
}
group->ChangeLeader(newLeader->GetGUID());
LOG_DEBUG("playerbots", "GroupSetLeaderOperation: Changed leader to {}", newLeader->GetName());
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; }
std::string GetName() const override { return "GroupSetLeader"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
Player* newLeader = ObjectAccessor::FindPlayer(m_newLeaderGuid);
return bot && newLeader;
}
private:
ObjectGuid m_botGuid;
ObjectGuid m_newLeaderGuid;
};
// Form arena group
class ArenaGroupFormationOperation : public PlayerbotOperation
{
public:
ArenaGroupFormationOperation(ObjectGuid leaderGuid, std::vector<ObjectGuid> memberGuids,
uint32 requiredSize, uint32 arenaTeamId, std::string arenaTeamName)
: m_leaderGuid(leaderGuid), m_memberGuids(memberGuids),
m_requiredSize(requiredSize), m_arenaTeamId(arenaTeamId), m_arenaTeamName(arenaTeamName)
{
}
bool Execute() override
{
Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid);
if (!leader)
{
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Leader not found");
return false;
}
// Step 1: Remove all members from their existing groups
for (const ObjectGuid& memberGuid : m_memberGuids)
{
Player* member = ObjectAccessor::FindPlayer(memberGuid);
if (!member)
continue;
Group* memberGroup = member->GetGroup();
if (memberGroup)
{
memberGroup->RemoveMember(memberGuid);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Removed {} from their existing group",
member->GetName());
}
}
// Step 2: Disband leader's existing group
Group* leaderGroup = leader->GetGroup();
if (leaderGroup)
{
leaderGroup->Disband(true);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Disbanded leader's existing group");
}
// Step 3: Create new group with leader
Group* newGroup = new Group();
if (!newGroup->Create(leader))
{
delete newGroup;
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to create arena group for leader {}",
leader->GetName());
return false;
}
sGroupMgr->AddGroup(newGroup);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Created new arena group with leader {}",
leader->GetName());
// Step 4: Add members to the new group
uint32 addedMembers = 0;
for (const ObjectGuid& memberGuid : m_memberGuids)
{
Player* member = ObjectAccessor::FindPlayer(memberGuid);
if (!member)
{
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} not found, skipping",
memberGuid.ToString());
continue;
}
if (member->GetLevel() < 70)
{
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Member {} is below level 70, skipping",
member->GetName());
continue;
}
if (newGroup->AddMember(member))
{
addedMembers++;
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Added {} to arena group",
member->GetName());
}
else
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: Failed to add {} to arena group",
member->GetName());
}
if (addedMembers == 0)
{
LOG_ERROR("playerbots", "ArenaGroupFormationOperation: No members were added to the arena group");
newGroup->Disband();
return false;
}
// Step 5: Teleport members to leader and reset AI
for (const ObjectGuid& memberGuid : m_memberGuids)
{
Player* member = ObjectAccessor::FindPlayer(memberGuid);
if (!member || !newGroup->IsMember(memberGuid))
continue;
PlayerbotAI* memberBotAI = sPlayerbotsMgr->GetPlayerbotAI(member);
if (memberBotAI)
memberBotAI->Reset();
member->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
member->TeleportTo(leader->GetMapId(), leader->GetPositionX(), leader->GetPositionY(),
leader->GetPositionZ(), 0);
LOG_DEBUG("playerbots", "ArenaGroupFormationOperation: Teleported {} to leader", member->GetName());
}
// Check if we have enough members
if (newGroup->GetMembersCount() < m_requiredSize)
{
LOG_INFO("playerbots", "Team #{} <{}> Group is not ready for match (not enough members: {}/{})",
m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount(), m_requiredSize);
newGroup->Disband();
return false;
}
LOG_INFO("playerbots", "Team #{} <{}> Group is ready for match with {} members",
m_arenaTeamId, m_arenaTeamName, newGroup->GetMembersCount());
return true;
}
ObjectGuid GetBotGuid() const override { return m_leaderGuid; }
uint32 GetPriority() const override { return 60; } // Very high priority (arena/BG operations)
std::string GetName() const override { return "ArenaGroupFormation"; }
bool IsValid() const override
{
Player* leader = ObjectAccessor::FindPlayer(m_leaderGuid);
return leader != nullptr;
}
private:
ObjectGuid m_leaderGuid;
std::vector<ObjectGuid> m_memberGuids;
uint32 m_requiredSize;
uint32 m_arenaTeamId;
std::string m_arenaTeamName;
};
// Bot logout group cleanup operation
class BotLogoutGroupCleanupOperation : public PlayerbotOperation
{
public:
BotLogoutGroupCleanupOperation(ObjectGuid botGuid) : m_botGuid(botGuid) {}
bool Execute() override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
if (!bot)
return false;
PlayerbotAI* botAI = sPlayerbotsMgr->GetPlayerbotAI(bot);
if (!botAI)
return false;
Group* group = bot->GetGroup();
if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster())
sPlayerbotDbStore->Save(botAI);
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 70; }
std::string GetName() const override { return "BotLogoutGroupCleanup"; }
bool IsValid() const override
{
Player* bot = ObjectAccessor::FindPlayer(m_botGuid);
return bot != nullptr;
}
private:
ObjectGuid m_botGuid;
};
// Add player bot operation (for logging in bots from map threads)
class AddPlayerBotOperation : public PlayerbotOperation
{
public:
AddPlayerBotOperation(ObjectGuid botGuid, uint32 masterAccountId)
: m_botGuid(botGuid), m_masterAccountId(masterAccountId)
{
}
bool Execute() override
{
sRandomPlayerbotMgr->AddPlayerBot(m_botGuid, m_masterAccountId);
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 50; } // High priority
std::string GetName() const override { return "AddPlayerBot"; }
bool IsValid() const override
{
return !ObjectAccessor::FindConnectedPlayer(m_botGuid);
}
private:
ObjectGuid m_botGuid;
uint32 m_masterAccountId;
};
class OnBotLoginOperation : public PlayerbotOperation
{
public:
OnBotLoginOperation(ObjectGuid botGuid, PlayerbotHolder* holder)
: m_botGuid(botGuid), m_holder(holder)
{
}
bool Execute() override
{
Player* bot = ObjectAccessor::FindConnectedPlayer(m_botGuid);
if (!bot || !m_holder)
return false;
m_holder->OnBotLogin(bot);
return true;
}
ObjectGuid GetBotGuid() const override { return m_botGuid; }
uint32 GetPriority() const override { return 100; }
std::string GetName() const override { return "OnBotLogin"; }
bool IsValid() const override
{
return ObjectAccessor::FindConnectedPlayer(m_botGuid) != nullptr;
}
private:
ObjectGuid m_botGuid;
PlayerbotHolder* m_holder;
};
#endif

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#include "PlayerbotWorldThreadProcessor.h"
#include "Log.h"
#include "PlayerbotAIConfig.h"
#include <algorithm>
PlayerbotWorldThreadProcessor::PlayerbotWorldThreadProcessor()
: m_enabled(true), m_maxQueueSize(10000), m_batchSize(100), m_queueWarningThreshold(80),
m_timeSinceLastUpdate(0), m_updateInterval(50) // Process at least every 50ms
{
LOG_INFO("playerbots", "PlayerbotWorldThreadProcessor initialized");
}
PlayerbotWorldThreadProcessor::~PlayerbotWorldThreadProcessor() { ClearQueue(); }
PlayerbotWorldThreadProcessor* PlayerbotWorldThreadProcessor::instance()
{
static PlayerbotWorldThreadProcessor instance;
return &instance;
}
void PlayerbotWorldThreadProcessor::Update(uint32 diff)
{
if (!m_enabled)
return;
// Accumulate time
m_timeSinceLastUpdate += diff;
// Don't process too frequently to reduce overhead
if (m_timeSinceLastUpdate < m_updateInterval)
return;
m_timeSinceLastUpdate = 0;
// Check queue health (warn if getting full)
CheckQueueHealth();
// Process a batch of operations
ProcessBatch();
}
bool PlayerbotWorldThreadProcessor::QueueOperation(std::unique_ptr<PlayerbotOperation> operation)
{
if (!operation)
{
LOG_ERROR("playerbots", "Attempted to queue null operation");
return false;
}
std::lock_guard<std::mutex> lock(m_queueMutex);
// Check if queue is full
if (m_operationQueue.size() >= m_maxQueueSize)
{
LOG_ERROR("playerbots",
"PlayerbotWorldThreadProcessor queue is full ({} operations). Dropping operation: {}",
m_maxQueueSize, operation->GetName());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsSkipped++;
return false;
}
// Queue the operation
m_operationQueue.push(std::move(operation));
// Update statistics
{
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.currentQueueSize = static_cast<uint32>(m_operationQueue.size());
m_stats.maxQueueSize = std::max(m_stats.maxQueueSize, m_stats.currentQueueSize);
}
return true;
}
void PlayerbotWorldThreadProcessor::ProcessBatch()
{
// Extract a batch of operations from the queue
std::vector<std::unique_ptr<PlayerbotOperation>> batch;
batch.reserve(m_batchSize);
{
std::lock_guard<std::mutex> lock(m_queueMutex);
// Extract up to batchSize operations
while (!m_operationQueue.empty() && batch.size() < m_batchSize)
{
batch.push_back(std::move(m_operationQueue.front()));
m_operationQueue.pop();
}
// Update current queue size stat
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.currentQueueSize = static_cast<uint32>(m_operationQueue.size());
}
// Execute operations outside of lock to avoid blocking queue
uint32 totalExecutionTime = 0;
for (auto& operation : batch)
{
if (!operation)
continue;
try
{
// Check if operation is still valid
if (!operation->IsValid())
{
LOG_DEBUG("playerbots", "Skipping invalid operation: {}", operation->GetName());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsSkipped++;
continue;
}
// Time the execution
uint32 startTime = getMSTime();
// Execute the operation
bool success = operation->Execute();
uint32 executionTime = GetMSTimeDiffToNow(startTime);
totalExecutionTime += executionTime;
// Log slow operations
if (executionTime > 100)
LOG_WARN("playerbots", "Slow operation: {} took {}ms", operation->GetName(), executionTime);
// Update statistics
std::lock_guard<std::mutex> statsLock(m_statsMutex);
if (success)
m_stats.totalOperationsProcessed++;
else
{
m_stats.totalOperationsFailed++;
LOG_DEBUG("playerbots", "Operation failed: {}", operation->GetName());
}
}
catch (std::exception const& e)
{
LOG_ERROR("playerbots", "Exception in operation {}: {}", operation->GetName(), e.what());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsFailed++;
}
catch (...)
{
LOG_ERROR("playerbots", "Unknown exception in operation {}", operation->GetName());
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.totalOperationsFailed++;
}
}
// Update average execution time
if (!batch.empty())
{
std::lock_guard<std::mutex> statsLock(m_statsMutex);
uint32 avgTime = totalExecutionTime / static_cast<uint32>(batch.size());
// Exponential moving average
m_stats.averageExecutionTimeMs =
(m_stats.averageExecutionTimeMs * 9 + avgTime) / 10; // 90% old, 10% new
}
}
void PlayerbotWorldThreadProcessor::CheckQueueHealth()
{
uint32 queueSize = GetQueueSize();
uint32 threshold = (m_maxQueueSize * m_queueWarningThreshold) / 100;
if (queueSize >= threshold)
{
LOG_WARN("playerbots",
"PlayerbotWorldThreadProcessor queue is {}% full ({}/{}). "
"Consider increasing update frequency or batch size.",
(queueSize * 100) / m_maxQueueSize, queueSize, m_maxQueueSize);
}
}
uint32 PlayerbotWorldThreadProcessor::GetQueueSize() const
{
std::lock_guard<std::mutex> lock(m_queueMutex);
return static_cast<uint32>(m_operationQueue.size());
}
void PlayerbotWorldThreadProcessor::ClearQueue()
{
std::lock_guard<std::mutex> lock(m_queueMutex);
uint32 cleared = static_cast<uint32>(m_operationQueue.size());
if (cleared > 0)
LOG_INFO("playerbots", "Clearing {} queued operations", cleared);
// Clear the queue
while (!m_operationQueue.empty())
{
m_operationQueue.pop();
}
// Reset queue size stat
std::lock_guard<std::mutex> statsLock(m_statsMutex);
m_stats.currentQueueSize = 0;
}
PlayerbotWorldThreadProcessor::Statistics PlayerbotWorldThreadProcessor::GetStatistics() const
{
std::lock_guard<std::mutex> statsLock(m_statsMutex);
return m_stats; // Return a copy
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/
#ifndef _PLAYERBOT_WORLD_THREAD_PROCESSOR_H
#define _PLAYERBOT_WORLD_THREAD_PROCESSOR_H
#include "Common.h"
#include "PlayerbotOperation.h"
#include <memory>
#include <mutex>
#include <queue>
/**
* @brief Processes thread-unsafe bot operations in the world thread
*
* The PlayerbotWorldThreadProcessor manages a queue of operations that must be executed
* in the world thread rather than map threads. This ensures thread safety for operations
* like group modifications, LFG, guilds, battlegrounds, etc.
*
* Architecture:
* - Map threads queue operations via QueueOperation()
* - World thread processes operations via Update() (called from WorldScript::OnUpdate)
* - Operations are processed in priority order
* - Thread-safe queue protected by mutex
*
* Usage:
* auto op = std::make_unique<MyOperation>(botGuid, params);
* sPlayerbotWorldProcessor->QueueOperation(std::move(op));
*/
class PlayerbotWorldThreadProcessor
{
public:
PlayerbotWorldThreadProcessor();
~PlayerbotWorldThreadProcessor();
static PlayerbotWorldThreadProcessor* instance();
/**
* @brief Update and process queued operations (called from world thread)
*
* This method should be called from WorldScript::OnUpdate hook, which runs in the world thread.
* It processes a batch of queued operations.
*
* @param diff Time since last update in milliseconds
*/
void Update(uint32 diff);
/**
* @brief Queue an operation for execution in the world thread
*
* Thread-safe method that can be called from any thread (typically map threads).
* The operation will be executed later during Update().
*
* @param operation Unique pointer to the operation (ownership is transferred)
* @return true if operation was queued, false if queue is full
*/
bool QueueOperation(std::unique_ptr<PlayerbotOperation> operation);
/**
* @brief Get current queue size
*
* Thread-safe method for monitoring queue size.
*
* @return Number of operations waiting to be processed
*/
uint32 GetQueueSize() const;
/**
* @brief Clear all queued operations
*
* Used during shutdown or emergency situations.
*/
void ClearQueue();
/**
* @brief Get statistics about operation processing
*/
struct Statistics
{
uint64 totalOperationsProcessed = 0;
uint64 totalOperationsFailed = 0;
uint64 totalOperationsSkipped = 0;
uint32 currentQueueSize = 0;
uint32 maxQueueSize = 0;
uint32 averageExecutionTimeMs = 0;
};
Statistics GetStatistics() const;
/**
* @brief Enable/disable operation processing
*
* When disabled, operations are still queued but not processed.
* Useful for testing or temporary suspension.
*
* @param enabled true to enable processing, false to disable
*/
void SetEnabled(bool enabled) { m_enabled = enabled; }
bool IsEnabled() const { return m_enabled; }
private:
/**
* @brief Process a single batch of operations
*
* Extracts operations from queue and executes them.
* Called internally by Update().
*/
void ProcessBatch();
/**
* @brief Check if queue is approaching capacity
*
* Logs warning if queue is getting full.
*/
void CheckQueueHealth();
// Thread-safe queue
mutable std::mutex m_queueMutex;
std::queue<std::unique_ptr<PlayerbotOperation>> m_operationQueue;
// Configuration
bool m_enabled;
uint32 m_maxQueueSize; // Maximum operations in queue
uint32 m_batchSize; // Operations to process per Update()
uint32 m_queueWarningThreshold; // Warn when queue reaches this percentage
// Statistics
mutable std::mutex m_statsMutex;
Statistics m_stats;
// Timing
uint32 m_timeSinceLastUpdate;
uint32 m_updateInterval; // Minimum ms between updates
};
#define sPlayerbotWorldProcessor PlayerbotWorldThreadProcessor::instance()
#endif

View File

@@ -25,6 +25,7 @@
#include "Metric.h" #include "Metric.h"
#include "PlayerScript.h" #include "PlayerScript.h"
#include "PlayerbotAIConfig.h" #include "PlayerbotAIConfig.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "RandomPlayerbotMgr.h" #include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h" #include "ScriptMgr.h"
#include "cs_playerbots.h" #include "cs_playerbots.h"
@@ -300,7 +301,8 @@ class PlayerbotsWorldScript : public WorldScript
{ {
public: public:
PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", { PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", {
WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED,
WORLDHOOK_ON_UPDATE
}) {} }) {}
void OnBeforeWorldInitialized() override void OnBeforeWorldInitialized() override
@@ -329,6 +331,13 @@ public:
LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime)); LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " "); LOG_INFO("server.loading", " ");
LOG_INFO("server.loading", "Playerbots World Thread Processor initialized");
}
void OnUpdate(uint32 diff) override
{
sPlayerbotWorldProcessor->Update(diff);
sRandomPlayerbotMgr->UpdateAI(diff); // World thread only
} }
}; };
@@ -390,8 +399,7 @@ public:
void OnPlayerbotUpdate(uint32 diff) override void OnPlayerbotUpdate(uint32 diff) override
{ {
sRandomPlayerbotMgr->UpdateAI(diff); sRandomPlayerbotMgr->UpdateSessions(); // Per-bot updates only
sRandomPlayerbotMgr->UpdateSessions();
} }
void OnPlayerbotUpdateSessions(Player* player) override void OnPlayerbotUpdateSessions(Player* player) override

View File

@@ -58,6 +58,14 @@ Player* GuidManageAction::GetPlayer(Event event)
return nullptr; return nullptr;
} }
void GuidManageAction::SendPacket(WorldPacket const& packet)
{
// make a heap copy because QueuePacket takes ownership
WorldPacket* data = new WorldPacket(packet);
bot->GetSession()->QueuePacket(data);
}
bool GuidManageAction::Execute(Event event) bool GuidManageAction::Execute(Event event)
{ {
Player* player = GetPlayer(event); Player* player = GetPlayer(event);
@@ -84,12 +92,6 @@ bool GuildInviteAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_INVITE); return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_INVITE);
} }
void GuildInviteAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildInviteByName data = WorldPacket(packet);
bot->GetSession()->HandleGuildInviteOpcode(data);
}
bool GuildInviteAction::PlayerIsValid(Player* member) bool GuildInviteAction::PlayerIsValid(Player* member)
{ {
return !member->GetGuildId() && (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) || return !member->GetGuildId() && (sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_INTERACTION_GUILD) ||
@@ -101,12 +103,6 @@ bool GuildPromoteAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_PROMOTE); return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_PROMOTE);
} }
void GuildPromoteAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildPromoteMember data = WorldPacket(packet);
bot->GetSession()->HandleGuildPromoteOpcode(data);
}
bool GuildPromoteAction::PlayerIsValid(Player* member) bool GuildPromoteAction::PlayerIsValid(Player* member)
{ {
return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member) - 1; return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member) - 1;
@@ -117,12 +113,6 @@ bool GuildDemoteAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_DEMOTE); return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_DEMOTE);
} }
void GuildDemoteAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildDemoteMember data = WorldPacket(packet);
bot->GetSession()->HandleGuildDemoteOpcode(data);
}
bool GuildDemoteAction::PlayerIsValid(Player* member) bool GuildDemoteAction::PlayerIsValid(Player* member)
{ {
return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member);
@@ -133,12 +123,6 @@ bool GuildRemoveAction::isUseful()
return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_REMOVE); return bot->GetGuildId() && sGuildMgr->GetGuildById(bot->GetGuildId())->HasRankRight(bot, GR_RIGHT_REMOVE);
} }
void GuildRemoveAction::SendPacket(WorldPacket packet)
{
WorldPackets::Guild::GuildOfficerRemoveMember data = WorldPacket(packet);
bot->GetSession()->HandleGuildRemoveOpcode(data);
}
bool GuildRemoveAction::PlayerIsValid(Player* member) bool GuildRemoveAction::PlayerIsValid(Player* member)
{ {
return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member);

View File

@@ -25,7 +25,7 @@ public:
bool isUseful() override { return false; } bool isUseful() override { return false; }
protected: protected:
virtual void SendPacket(WorldPacket data){}; virtual void SendPacket(WorldPacket const& packet);
virtual Player* GetPlayer(Event event); virtual Player* GetPlayer(Event event);
virtual bool PlayerIsValid(Player* member); virtual bool PlayerIsValid(Player* member);
virtual uint8 GetRankId(Player* member); virtual uint8 GetRankId(Player* member);
@@ -44,7 +44,6 @@ public:
bool isUseful() override; bool isUseful() override;
protected: protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override; bool PlayerIsValid(Player* member) override;
}; };
@@ -59,7 +58,6 @@ public:
bool isUseful() override; bool isUseful() override;
protected: protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override; bool PlayerIsValid(Player* member) override;
}; };
@@ -74,7 +72,6 @@ public:
bool isUseful() override; bool isUseful() override;
protected: protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override; bool PlayerIsValid(Player* member) override;
}; };
@@ -89,7 +86,6 @@ public:
bool isUseful() override; bool isUseful() override;
protected: protected:
void SendPacket(WorldPacket data) override;
bool PlayerIsValid(Player* member) override; bool PlayerIsValid(Player* member) override;
}; };

View File

@@ -9,7 +9,9 @@
#include "Event.h" #include "Event.h"
#include "GuildMgr.h" #include "GuildMgr.h"
#include "Log.h" #include "Log.h"
#include "PlayerbotOperations.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "ServerFacade.h" #include "ServerFacade.h"
bool InviteToGroupAction::Invite(Player* inviter, Player* player) bool InviteToGroupAction::Invite(Player* inviter, Player* player)
@@ -27,7 +29,10 @@ bool InviteToGroupAction::Invite(Player* inviter, Player* player)
{ {
if (GET_PLAYERBOT_AI(player) && !GET_PLAYERBOT_AI(player)->IsRealPlayer()) if (GET_PLAYERBOT_AI(player) && !GET_PLAYERBOT_AI(player)->IsRealPlayer())
if (!group->isRaidGroup() && group->GetMembersCount() > 4) if (!group->isRaidGroup() && group->GetMembersCount() > 4)
group->ConvertToRaid(); {
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(inviter->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
} }
WorldPacket p; WorldPacket p;
@@ -89,7 +94,10 @@ bool InviteNearbyToGroupAction::Execute(Event event)
// When inviting the 5th member of the group convert to raid for future invites. // When inviting the 5th member of the group convert to raid for future invites.
if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() &&
bot->GetGroup()->GetMembersCount() > 3) bot->GetGroup()->GetMembersCount() > 3)
group->ConvertToRaid(); {
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot)) if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot))
{ {
@@ -221,7 +229,8 @@ bool InviteGuildToGroupAction::Execute(Event event)
if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() &&
bot->GetGroup()->GetMembersCount() > 3) bot->GetGroup()->GetMembersCount() > 3)
{ {
group->ConvertToRaid(); auto convertOp = std::make_unique<GroupConvertToRaidOperation>(bot->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
} }
if (sPlayerbotAIConfig->inviteChat && if (sPlayerbotAIConfig->inviteChat &&
@@ -362,7 +371,10 @@ bool LfgAction::Execute(Event event)
if (param.empty() || param == "5" || group->isRaidGroup()) if (param.empty() || param == "5" || group->isRaidGroup())
return false; // Group or raid is full so stop trying. return false; // Group or raid is full so stop trying.
else else
group->ConvertToRaid(); // We want a raid but are in a group so convert and continue. {
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(requester->GetGUID());
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
}
} }
Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); Group::MemberSlotList const& groupSlot = group->GetMemberSlots();

View File

@@ -6,16 +6,17 @@
#include "PassLeadershipToMasterAction.h" #include "PassLeadershipToMasterAction.h"
#include "Event.h" #include "Event.h"
#include "PlayerbotOperations.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "PlayerbotWorldThreadProcessor.h"
bool PassLeadershipToMasterAction::Execute(Event event) bool PassLeadershipToMasterAction::Execute(Event event)
{ {
if (Player* master = GetMaster()) if (Player* master = GetMaster())
if (master && master != bot && bot->GetGroup() && bot->GetGroup()->IsMember(master->GetGUID())) if (master && master != bot && bot->GetGroup() && bot->GetGroup()->IsMember(master->GetGUID()))
{ {
WorldPacket p(SMSG_GROUP_SET_LEADER, 8); auto setLeaderOp = std::make_unique<GroupSetLeaderOperation>(bot->GetGUID(), master->GetGUID());
p << master->GetGUID(); sPlayerbotWorldProcessor->QueueOperation(std::move(setLeaderOp));
bot->GetSession()->HandleGroupSetLeaderOpcode(p);
if (!message.empty()) if (!message.empty())
botAI->TellMasterNoFacing(message); botAI->TellMasterNoFacing(message);