diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index bdafc10c..db69387e 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -725,8 +725,8 @@ std::string const PlayerbotAIConfig::GetTimestampStr() // HH hour (2 digits 00-23) // MM minutes (2 digits 00-59) // SS seconds (2 digits 00-59) - char buf[20]; - snprintf(buf, 20, "%04d-%02d-%02d %02d-%02d-%02d", aTm->tm_year + 1900, aTm->tm_mon + 1, aTm->tm_mday, aTm->tm_hour, + char buf[32]; + 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); return std::string(buf); } diff --git a/src/PlayerbotMgr.cpp b/src/PlayerbotMgr.cpp index fe4a714f..d96851da 100644 --- a/src/PlayerbotMgr.cpp +++ b/src/PlayerbotMgr.cpp @@ -27,7 +27,9 @@ #include "PlayerbotAIConfig.h" #include "PlayerbotDbStore.h" #include "PlayerbotFactory.h" +#include "PlayerbotOperations.h" #include "PlayerbotSecurity.h" +#include "PlayerbotWorldThreadProcessor.h" #include "Playerbots.h" #include "RandomPlayerbotMgr.h" #include "SharedDefines.h" @@ -85,7 +87,6 @@ public: void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId) { - // bot is loading if (botLoading.find(playerGuid) != botLoading.end()) return; @@ -195,7 +196,9 @@ void PlayerbotHolder::HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder con } sRandomPlayerbotMgr->OnPlayerLogin(bot); - OnBotLogin(bot); + + auto op = std::make_unique(bot->GetGUID(), this); + sPlayerbotWorldProcessor->QueueOperation(std::move(op)); botLoading.erase(holder.GetGuid()); } @@ -316,11 +319,9 @@ void PlayerbotHolder::LogoutPlayerBot(ObjectGuid guid) if (!botAI) return; - Group* group = bot->GetGroup(); - if (group && !bot->InBattleground() && !bot->InBattlegroundQueue() && botAI->HasActivePlayerMaster()) - { - sPlayerbotDbStore->Save(botAI); - } + // Queue group cleanup operation for world thread + auto cleanupOp = std::make_unique(guid); + sPlayerbotWorldProcessor->QueueOperation(std::move(cleanupOp)); LOG_DEBUG("playerbots", "Bot {} logging out", bot->GetName().c_str()); bot->SaveToDB(false, false); @@ -549,6 +550,7 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) botAI->TellMaster("Hello!", PLAYERBOT_SECURITY_TALK); + // Queue group operations for world thread if (master && master->GetGroup() && !group) { Group* mgroup = master->GetGroup(); @@ -556,24 +558,29 @@ void PlayerbotHolder::OnBotLogin(Player* const bot) { if (!mgroup->isRaidGroup() && !mgroup->isLFGGroup() && !mgroup->isBGGroup() && !mgroup->isBFGroup()) { - mgroup->ConvertToRaid(); + // Queue ConvertToRaid operation + auto convertOp = std::make_unique(master->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); } if (mgroup->isRaidGroup()) { - mgroup->AddMember(bot); + // Queue AddMember operation + auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(addOp)); } } else { - mgroup->AddMember(bot); + // Queue AddMember operation + auto addOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(addOp)); } } else if (master && !group) { - Group* newGroup = new Group(); - newGroup->Create(master); - sGroupMgr->AddGroup(newGroup); - newGroup->AddMember(bot); + // Queue group creation and AddMember operation + auto inviteOp = std::make_unique(master->GetGUID(), bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(inviteOp)); } // if (master) // { diff --git a/src/PlayerbotOperation.h b/src/PlayerbotOperation.h new file mode 100644 index 00000000..6ac303d3 --- /dev/null +++ b/src/PlayerbotOperation.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016+ AzerothCore , 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 + +/** + * @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& a, const std::unique_ptr& b) const + { + return a->GetPriority() < b->GetPriority(); // Lower priority goes to back of queue + } +}; + +#endif diff --git a/src/PlayerbotOperations.h b/src/PlayerbotOperations.h new file mode 100644 index 00000000..d7c2b47b --- /dev/null +++ b/src/PlayerbotOperations.h @@ -0,0 +1,500 @@ +/* + * Copyright (C) 2016+ AzerothCore , 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 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 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 diff --git a/src/PlayerbotWorldThreadProcessor.cpp b/src/PlayerbotWorldThreadProcessor.cpp new file mode 100644 index 00000000..c776eb12 --- /dev/null +++ b/src/PlayerbotWorldThreadProcessor.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016+ AzerothCore , 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 + +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 operation) +{ + if (!operation) + { + LOG_ERROR("playerbots", "Attempted to queue null operation"); + return false; + } + + std::lock_guard 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 statsLock(m_statsMutex); + m_stats.totalOperationsSkipped++; + return false; + } + + // Queue the operation + m_operationQueue.push(std::move(operation)); + + // Update statistics + { + std::lock_guard statsLock(m_statsMutex); + m_stats.currentQueueSize = static_cast(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> batch; + batch.reserve(m_batchSize); + + { + std::lock_guard 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 statsLock(m_statsMutex); + m_stats.currentQueueSize = static_cast(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 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 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 statsLock(m_statsMutex); + m_stats.totalOperationsFailed++; + } + catch (...) + { + LOG_ERROR("playerbots", "Unknown exception in operation {}", operation->GetName()); + + std::lock_guard statsLock(m_statsMutex); + m_stats.totalOperationsFailed++; + } + } + + // Update average execution time + if (!batch.empty()) + { + std::lock_guard statsLock(m_statsMutex); + uint32 avgTime = totalExecutionTime / static_cast(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 lock(m_queueMutex); + return static_cast(m_operationQueue.size()); +} + +void PlayerbotWorldThreadProcessor::ClearQueue() +{ + std::lock_guard lock(m_queueMutex); + + uint32 cleared = static_cast(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 statsLock(m_statsMutex); + m_stats.currentQueueSize = 0; +} + +PlayerbotWorldThreadProcessor::Statistics PlayerbotWorldThreadProcessor::GetStatistics() const +{ + std::lock_guard statsLock(m_statsMutex); + return m_stats; // Return a copy +} diff --git a/src/PlayerbotWorldThreadProcessor.h b/src/PlayerbotWorldThreadProcessor.h new file mode 100644 index 00000000..e37d2b5b --- /dev/null +++ b/src/PlayerbotWorldThreadProcessor.h @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016+ AzerothCore , 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 +#include +#include + +/** + * @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(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 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> 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 diff --git a/src/Playerbots.cpp b/src/Playerbots.cpp index a7217d7b..136d4e61 100644 --- a/src/Playerbots.cpp +++ b/src/Playerbots.cpp @@ -25,6 +25,7 @@ #include "Metric.h" #include "PlayerScript.h" #include "PlayerbotAIConfig.h" +#include "PlayerbotWorldThreadProcessor.h" #include "RandomPlayerbotMgr.h" #include "ScriptMgr.h" #include "cs_playerbots.h" @@ -300,7 +301,8 @@ class PlayerbotsWorldScript : public WorldScript { public: PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", { - WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED + WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED, + WORLDHOOK_ON_UPDATE }) {} void OnBeforeWorldInitialized() override @@ -329,6 +331,13 @@ public: LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime)); 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 { - sRandomPlayerbotMgr->UpdateAI(diff); - sRandomPlayerbotMgr->UpdateSessions(); + sRandomPlayerbotMgr->UpdateSessions(); // Per-bot updates only } void OnPlayerbotUpdateSessions(Player* player) override diff --git a/src/strategy/actions/GuildManagementActions.cpp b/src/strategy/actions/GuildManagementActions.cpp index 4ab6d72c..f00a955e 100644 --- a/src/strategy/actions/GuildManagementActions.cpp +++ b/src/strategy/actions/GuildManagementActions.cpp @@ -58,6 +58,14 @@ Player* GuidManageAction::GetPlayer(Event event) 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) { Player* player = GetPlayer(event); @@ -84,12 +92,6 @@ bool GuildInviteAction::isUseful() 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) { 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); } -void GuildPromoteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildPromoteMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildPromoteOpcode(data); -} - bool GuildPromoteAction::PlayerIsValid(Player* member) { 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); } -void GuildDemoteAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildDemoteMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildDemoteOpcode(data); -} - bool GuildDemoteAction::PlayerIsValid(Player* 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); } -void GuildRemoveAction::SendPacket(WorldPacket packet) -{ - WorldPackets::Guild::GuildOfficerRemoveMember data = WorldPacket(packet); - bot->GetSession()->HandleGuildRemoveOpcode(data); -} - bool GuildRemoveAction::PlayerIsValid(Player* member) { return member->GetGuildId() == bot->GetGuildId() && GetRankId(bot) < GetRankId(member); diff --git a/src/strategy/actions/GuildManagementActions.h b/src/strategy/actions/GuildManagementActions.h index bf46d1b7..b1d363e8 100644 --- a/src/strategy/actions/GuildManagementActions.h +++ b/src/strategy/actions/GuildManagementActions.h @@ -25,7 +25,7 @@ public: bool isUseful() override { return false; } protected: - virtual void SendPacket(WorldPacket data){}; + virtual void SendPacket(WorldPacket const& packet); virtual Player* GetPlayer(Event event); virtual bool PlayerIsValid(Player* member); virtual uint8 GetRankId(Player* member); @@ -44,7 +44,6 @@ public: bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -59,7 +58,6 @@ public: bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -74,7 +72,6 @@ public: bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; @@ -89,7 +86,6 @@ public: bool isUseful() override; protected: - void SendPacket(WorldPacket data) override; bool PlayerIsValid(Player* member) override; }; diff --git a/src/strategy/actions/InviteToGroupAction.cpp b/src/strategy/actions/InviteToGroupAction.cpp index 7af26210..4d0b4df7 100644 --- a/src/strategy/actions/InviteToGroupAction.cpp +++ b/src/strategy/actions/InviteToGroupAction.cpp @@ -9,7 +9,9 @@ #include "Event.h" #include "GuildMgr.h" #include "Log.h" +#include "PlayerbotOperations.h" #include "Playerbots.h" +#include "PlayerbotWorldThreadProcessor.h" #include "ServerFacade.h" 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 (!group->isRaidGroup() && group->GetMembersCount() > 4) - group->ConvertToRaid(); + { + auto convertOp = std::make_unique(inviter->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } } 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. if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && bot->GetGroup()->GetMembersCount() > 3) - group->ConvertToRaid(); + { + auto convertOp = std::make_unique(bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot)) { @@ -221,7 +229,8 @@ bool InviteGuildToGroupAction::Execute(Event event) if (group && botAI->GetGrouperType() > GrouperType::LEADER_5 && !group->isRaidGroup() && bot->GetGroup()->GetMembersCount() > 3) { - group->ConvertToRaid(); + auto convertOp = std::make_unique(bot->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); } if (sPlayerbotAIConfig->inviteChat && @@ -362,7 +371,10 @@ bool LfgAction::Execute(Event event) if (param.empty() || param == "5" || group->isRaidGroup()) return false; // Group or raid is full so stop trying. else - group->ConvertToRaid(); // We want a raid but are in a group so convert and continue. + { + auto convertOp = std::make_unique(requester->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp)); + } } Group::MemberSlotList const& groupSlot = group->GetMemberSlots(); diff --git a/src/strategy/actions/PassLeadershipToMasterAction.cpp b/src/strategy/actions/PassLeadershipToMasterAction.cpp index ceb1fbbc..87890c1c 100644 --- a/src/strategy/actions/PassLeadershipToMasterAction.cpp +++ b/src/strategy/actions/PassLeadershipToMasterAction.cpp @@ -6,16 +6,17 @@ #include "PassLeadershipToMasterAction.h" #include "Event.h" +#include "PlayerbotOperations.h" #include "Playerbots.h" +#include "PlayerbotWorldThreadProcessor.h" bool PassLeadershipToMasterAction::Execute(Event event) { if (Player* master = GetMaster()) if (master && master != bot && bot->GetGroup() && bot->GetGroup()->IsMember(master->GetGUID())) { - WorldPacket p(SMSG_GROUP_SET_LEADER, 8); - p << master->GetGUID(); - bot->GetSession()->HandleGroupSetLeaderOpcode(p); + auto setLeaderOp = std::make_unique(bot->GetGUID(), master->GetGUID()); + sPlayerbotWorldProcessor->QueueOperation(std::move(setLeaderOp)); if (!message.empty()) botAI->TellMasterNoFacing(message);