mirror of
https://github.com/mod-playerbots/mod-playerbots
synced 2025-11-29 15:58:20 +08:00
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>
463 lines
14 KiB
C++
463 lines
14 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.
|
|
*/
|
|
|
|
#include "InviteToGroupAction.h"
|
|
|
|
#include "BroadcastHelper.h"
|
|
#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)
|
|
{
|
|
if (!player)
|
|
return false;
|
|
|
|
if (inviter == player)
|
|
return false;
|
|
|
|
if (!GET_PLAYERBOT_AI(player) && !botAI->GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, true, player))
|
|
return false;
|
|
|
|
if (Group* group = inviter->GetGroup())
|
|
{
|
|
if (GET_PLAYERBOT_AI(player) && !GET_PLAYERBOT_AI(player)->IsRealPlayer())
|
|
if (!group->isRaidGroup() && group->GetMembersCount() > 4)
|
|
{
|
|
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(inviter->GetGUID());
|
|
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
|
|
}
|
|
}
|
|
|
|
WorldPacket p;
|
|
uint32 roles_mask = 0;
|
|
p << player->GetName();
|
|
p << roles_mask;
|
|
inviter->GetSession()->HandleGroupInviteOpcode(p);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool InviteNearbyToGroupAction::Execute(Event event)
|
|
{
|
|
GuidVector nearGuids = botAI->GetAiObjectContext()->GetValue<GuidVector>("nearest friendly players")->Get();
|
|
for (auto& i : nearGuids)
|
|
{
|
|
Player* player = ObjectAccessor::FindPlayer(i);
|
|
if (!player)
|
|
continue;
|
|
|
|
if (player == bot)
|
|
continue;
|
|
|
|
if (player->GetMapId() != bot->GetMapId())
|
|
continue;
|
|
|
|
if (player->GetGroup())
|
|
continue;
|
|
|
|
if (!sPlayerbotAIConfig->randomBotInvitePlayer && GET_PLAYERBOT_AI(player)->IsRealPlayer())
|
|
continue;
|
|
|
|
Group* group = bot->GetGroup();
|
|
|
|
if (player->isDND())
|
|
continue;
|
|
|
|
if (player->IsBeingTeleported())
|
|
continue;
|
|
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
|
|
if (botAI)
|
|
{
|
|
if (botAI->GetGrouperType() == GrouperType::SOLO &&
|
|
!botAI->HasRealPlayerMaster()) // Do not invite solo players.
|
|
continue;
|
|
|
|
if (botAI->HasActivePlayerMaster()) // Do not invite alts of active players.
|
|
continue;
|
|
}
|
|
|
|
if (abs(int32(player->GetLevel() - bot->GetLevel())) > 2)
|
|
continue;
|
|
|
|
if (sServerFacade->GetDistance2d(bot, player) > sPlayerbotAIConfig->sightDistance)
|
|
continue;
|
|
|
|
// 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)
|
|
{
|
|
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(bot->GetGUID());
|
|
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
|
|
}
|
|
|
|
if (sPlayerbotAIConfig->inviteChat && sRandomPlayerbotMgr->IsRandomBot(bot))
|
|
{
|
|
std::map<std::string, std::string> placeholders;
|
|
placeholders["%player"] = player->GetName();
|
|
|
|
if (group && group->isRaidGroup())
|
|
bot->Say(BOT_TEXT2("join_raid", placeholders),
|
|
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
|
|
else
|
|
bot->Say(BOT_TEXT2("join_group", placeholders),
|
|
(bot->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH));
|
|
}
|
|
|
|
return Invite(bot, player);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool InviteNearbyToGroupAction::isUseful()
|
|
{
|
|
if (!sPlayerbotAIConfig->randomBotGroupNearby)
|
|
return false;
|
|
|
|
if (bot->InBattleground())
|
|
return false;
|
|
|
|
if (bot->InBattlegroundQueue())
|
|
return false;
|
|
|
|
GrouperType grouperType = botAI->GetGrouperType();
|
|
|
|
if (grouperType == GrouperType::SOLO || grouperType == GrouperType::MEMBER)
|
|
return false;
|
|
|
|
Group* group = bot->GetGroup();
|
|
|
|
if (group)
|
|
{
|
|
if (group->isRaidGroup() && group->IsFull())
|
|
return false;
|
|
|
|
if (botAI->GetGroupMaster() != bot)
|
|
return false;
|
|
|
|
uint32 memberCount = group->GetMembersCount();
|
|
|
|
if (memberCount >= uint8(grouperType))
|
|
return false;
|
|
}
|
|
|
|
if (botAI->HasActivePlayerMaster()) // Alts do not invite randomly
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
std::vector<Player*> InviteGuildToGroupAction::getGuildMembers()
|
|
{
|
|
Guild* guild = sGuildMgr->GetGuildById(bot->GetGuildId());
|
|
|
|
FindGuildMembers worker;
|
|
guild->BroadcastWorker(worker);
|
|
|
|
return worker.GetResult();
|
|
}
|
|
|
|
bool InviteGuildToGroupAction::Execute(Event event)
|
|
{
|
|
Guild* guild = sGuildMgr->GetGuildById(bot->GetGuildId());
|
|
|
|
for (auto& member : getGuildMembers())
|
|
{
|
|
Player* player = member;
|
|
|
|
if (!player)
|
|
continue;
|
|
|
|
if (player == bot)
|
|
continue;
|
|
|
|
if (player->GetGroup())
|
|
continue;
|
|
|
|
if (player->isDND())
|
|
continue;
|
|
|
|
if (!sPlayerbotAIConfig->randomBotInvitePlayer && GET_PLAYERBOT_AI(player)->IsRealPlayer())
|
|
continue;
|
|
|
|
if (player->IsBeingTeleported())
|
|
continue;
|
|
|
|
if (player->GetMapId() != bot->GetMapId() && player->GetLevel() < 30)
|
|
continue;
|
|
|
|
if (WorldPosition(player).distance(bot) > 1000 && player->GetLevel() < 15)
|
|
continue;
|
|
|
|
PlayerbotAI* playerAi = GET_PLAYERBOT_AI(player);
|
|
|
|
if (playerAi)
|
|
{
|
|
if (playerAi->GetGrouperType() == GrouperType::SOLO &&
|
|
!playerAi->HasRealPlayerMaster()) // Do not invite solo players.
|
|
continue;
|
|
|
|
if (playerAi->HasActivePlayerMaster()) // Do not invite alts of active players.
|
|
continue;
|
|
|
|
if (player->GetLevel() >
|
|
bot->GetLevel() + 5) // Invite higher levels that need money so they can grind money and help out.
|
|
{
|
|
if (!PAI_VALUE(bool, "should get money"))
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (bot->GetLevel() >
|
|
player->GetLevel() + 5) // Do not invite members that too low level or risk dragging them to deadly places.
|
|
continue;
|
|
|
|
if (!playerAi && sServerFacade->GetDistance2d(bot, player) > sPlayerbotAIConfig->sightDistance)
|
|
continue;
|
|
|
|
Group* group = bot->GetGroup();
|
|
// 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)
|
|
{
|
|
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(bot->GetGUID());
|
|
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
|
|
}
|
|
|
|
if (sPlayerbotAIConfig->inviteChat &&
|
|
(sRandomPlayerbotMgr->IsRandomBot(bot) || !botAI->HasActivePlayerMaster()))
|
|
{
|
|
BroadcastHelper::BroadcastGuildGroupOrRaidInvite(botAI, bot, player, group);
|
|
}
|
|
|
|
return Invite(bot, player);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool JoinGroupAction::Execute(Event event)
|
|
{
|
|
if (bot->InBattleground())
|
|
return false;
|
|
|
|
if (bot->InBattlegroundQueue())
|
|
return false;
|
|
|
|
Player* master = event.getOwner();
|
|
|
|
Group* group = master->GetGroup();
|
|
|
|
if (group)
|
|
{
|
|
if (group->IsFull())
|
|
return false;
|
|
|
|
if (bot->GetGroup() == group)
|
|
return false;
|
|
}
|
|
|
|
if (bot->GetGroup())
|
|
{
|
|
if (botAI->HasRealPlayerMaster())
|
|
return false;
|
|
|
|
if (!botAI->DoSpecificAction("leave", event, true))
|
|
return false;
|
|
}
|
|
|
|
return Invite(master, bot);
|
|
}
|
|
|
|
bool LfgAction::Execute(Event event)
|
|
{
|
|
Player* requester = event.getOwner() ? event.getOwner() : GetMaster();
|
|
|
|
if (bot->InBattleground())
|
|
return false;
|
|
|
|
if (bot->InBattlegroundQueue())
|
|
return false;
|
|
|
|
if (!botAI->IsSafe(requester))
|
|
return false;
|
|
|
|
if (requester->GetLevel() == DEFAULT_MAX_LEVEL && bot->GetLevel() != DEFAULT_MAX_LEVEL)
|
|
return false;
|
|
|
|
if (requester->GetLevel() > bot->GetLevel() + 4 || bot->GetLevel() > requester->GetLevel() + 4)
|
|
return false;
|
|
|
|
std::string param = event.getParam();
|
|
|
|
if (!param.empty() && param != "40" && param != "25" && param != "20" && param != "10" && param != "5")
|
|
return false;
|
|
|
|
Group* group = requester->GetGroup();
|
|
|
|
std::unordered_map<Classes, std::unordered_map<BotRoles, uint32>> allowedClassNr;
|
|
std::unordered_map<BotRoles, uint32> allowedRoles;
|
|
|
|
allowedRoles[BOT_ROLE_TANK] = 1;
|
|
allowedRoles[BOT_ROLE_HEALER] = 1;
|
|
allowedRoles[BOT_ROLE_DPS] = 3;
|
|
|
|
BotRoles role = botAI->IsTank(requester, false)
|
|
? BOT_ROLE_TANK
|
|
: (botAI->IsHeal(requester, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
|
|
Classes cls = (Classes)requester->getClass();
|
|
|
|
if (group)
|
|
{
|
|
if (param.empty() && group->isRaidGroup())
|
|
// Default to WotLK Raiding. Max size 25
|
|
param = "25";
|
|
// Select optimal group layout.
|
|
if (param == "40")
|
|
{
|
|
allowedRoles[BOT_ROLE_TANK] = 4;
|
|
allowedRoles[BOT_ROLE_HEALER] = 16;
|
|
allowedRoles[BOT_ROLE_DPS] = 20;
|
|
/*
|
|
allowedClassNr[CLASS_PALADIN][BOT_ROLE_TANK] = 0;
|
|
allowedClassNr[CLASS_DRUID][BOT_ROLE_TANK] = 1;
|
|
|
|
allowedClassNr[CLASS_DRUID][BOT_ROLE_HEALER] = 3;
|
|
allowedClassNr[CLASS_PALADIN][BOT_ROLE_HEALER] = 4;
|
|
allowedClassNr[CLASS_SHAMAN][BOT_ROLE_HEALER] = 4;
|
|
allowedClassNr[CLASS_PRIEST][BOT_ROLE_HEALER] = 11;
|
|
|
|
allowedClassNr[CLASS_WARRIOR][BOT_ROLE_DPS] = 8;
|
|
allowedClassNr[CLASS_PALADIN][BOT_ROLE_DPS] = 4;
|
|
allowedClassNr[CLASS_HUNTER][BOT_ROLE_DPS] = 4;
|
|
allowedClassNr[CLASS_ROGUE][BOT_ROLE_DPS] = 6;
|
|
allowedClassNr[CLASS_PRIEST][BOT_ROLE_DPS] = 1;
|
|
allowedClassNr[CLASS_SHAMAN][BOT_ROLE_DPS] = 4;
|
|
allowedClassNr[CLASS_MAGE][BOT_ROLE_DPS] = 15;
|
|
allowedClassNr[CLASS_WARLOCK][BOT_ROLE_DPS] = 4;
|
|
allowedClassNr[CLASS_DRUID][BOT_ROLE_DPS] = 1;
|
|
*/
|
|
}
|
|
else if (param == "25")
|
|
{
|
|
allowedRoles[BOT_ROLE_TANK] = 3;
|
|
allowedRoles[BOT_ROLE_HEALER] = 7;
|
|
allowedRoles[BOT_ROLE_DPS] = 15;
|
|
}
|
|
else if (param == "20")
|
|
{
|
|
allowedRoles[BOT_ROLE_TANK] = 2;
|
|
allowedRoles[BOT_ROLE_HEALER] = 5;
|
|
allowedRoles[BOT_ROLE_DPS] = 13;
|
|
}
|
|
else if (param == "10")
|
|
{
|
|
allowedRoles[BOT_ROLE_TANK] = 2;
|
|
allowedRoles[BOT_ROLE_HEALER] = 3;
|
|
allowedRoles[BOT_ROLE_DPS] = 5;
|
|
}
|
|
|
|
if (group->IsFull())
|
|
{
|
|
if (param.empty() || param == "5" || group->isRaidGroup())
|
|
return false; // Group or raid is full so stop trying.
|
|
else
|
|
{
|
|
auto convertOp = std::make_unique<GroupConvertToRaidOperation>(requester->GetGUID());
|
|
sPlayerbotWorldProcessor->QueueOperation(std::move(convertOp));
|
|
}
|
|
}
|
|
|
|
Group::MemberSlotList const& groupSlot = group->GetMemberSlots();
|
|
for (Group::member_citerator itr = groupSlot.begin(); itr != groupSlot.end(); itr++)
|
|
{
|
|
// Only add group member targets that are alive and near the player
|
|
Player* player = ObjectAccessor::FindPlayer(itr->guid);
|
|
|
|
if (!botAI->IsSafe(player))
|
|
return false;
|
|
|
|
role = botAI->IsTank(player, false) ? BOT_ROLE_TANK
|
|
: (botAI->IsHeal(player, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
|
|
cls = (Classes)player->getClass();
|
|
|
|
if (allowedRoles[role] > 0)
|
|
allowedRoles[role]--;
|
|
|
|
if (allowedClassNr[cls].find(role) != allowedClassNr[cls].end() && allowedClassNr[cls][role] > 0)
|
|
allowedClassNr[cls][role]--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (allowedRoles[role] > 0)
|
|
allowedRoles[role]--;
|
|
|
|
if (allowedClassNr[cls].find(role) != allowedClassNr[cls].end() && allowedClassNr[cls][role] > 0)
|
|
allowedClassNr[cls][role]--;
|
|
}
|
|
|
|
role = botAI->IsTank(bot, false) ? BOT_ROLE_TANK : (botAI->IsHeal(bot, false) ? BOT_ROLE_HEALER : BOT_ROLE_DPS);
|
|
cls = (Classes)bot->getClass();
|
|
|
|
if (allowedRoles[role] == 0)
|
|
return false;
|
|
|
|
if (allowedClassNr[cls].find(role) != allowedClassNr[cls].end() && allowedClassNr[cls][role] == 0)
|
|
return false;
|
|
|
|
if (bot->GetGroup())
|
|
{
|
|
if (botAI->HasRealPlayerMaster())
|
|
return false;
|
|
|
|
if (!botAI->DoSpecificAction("leave", event, true))
|
|
return false;
|
|
}
|
|
|
|
bool invite = Invite(requester, bot);
|
|
|
|
if (invite)
|
|
{
|
|
Event acceptEvent("accept invitation", requester ? requester->GetGUID() : ObjectGuid::Empty);
|
|
if (!botAI->DoSpecificAction("accept invitation", acceptEvent, true))
|
|
return false;
|
|
|
|
std::map<std::string, std::string> placeholders;
|
|
placeholders["%role"] = (role & BOT_ROLE_TANK ? "tank" : (role & BOT_ROLE_HEALER ? "healer" : "dps"));
|
|
placeholders["%spotsleft"] = std::to_string(allowedRoles[role] - 1);
|
|
|
|
std::ostringstream out;
|
|
if (allowedRoles[role] > 1)
|
|
{
|
|
out << "Joining as " << placeholders["%role"] << ", " << placeholders["%spotsleft"] << " "
|
|
<< placeholders["%role"] << " spots left.";
|
|
botAI->TellMasterNoFacing(out.str());
|
|
|
|
//botAI->DoSpecificAction("autogear");
|
|
//botAI->DoSpecificAction("maintenance");
|
|
}
|
|
else
|
|
{
|
|
out << "Joining as " << placeholders["%role"] << ".";
|
|
botAI->TellMasterNoFacing(out.str());
|
|
|
|
//botAI->DoSpecificAction("autogear");
|
|
//botAI->DoSpecificAction("maintenance");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|