Files
mod-playerbots/src/PlayerbotOperations.h
blinkysc 10213d8381 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>
2025-11-21 21:55:55 +01:00

501 lines
15 KiB
C++

/*
* 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