mirror of
https://github.com/mod-playerbots/mod-playerbots
synced 2025-11-29 15:58:20 +08:00
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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
93
src/PlayerbotOperation.h
Normal 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
500
src/PlayerbotOperations.h
Normal 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
|
||||||
217
src/PlayerbotWorldThreadProcessor.cpp
Normal file
217
src/PlayerbotWorldThreadProcessor.cpp
Normal 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
|
||||||
|
}
|
||||||
142
src/PlayerbotWorldThreadProcessor.h
Normal file
142
src/PlayerbotWorldThreadProcessor.h
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user