[HOT FIX] MS build issues regarding folder / command lenght usage or rc.exe (#2038)

This commit is contained in:
bashermens
2026-01-19 22:45:28 +01:00
committed by GitHub
parent fd07e02a8a
commit 41c53365ae
1119 changed files with 27 additions and 27 deletions

View File

@@ -0,0 +1,212 @@
/*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "BattleGroundTactics.h"
#include "Chat.h"
#include "GuildTaskMgr.h"
#include "PerfMonitor.h"
#include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h"
using namespace Acore::ChatCommands;
class playerbots_commandscript : public CommandScript
{
public:
playerbots_commandscript() : CommandScript("playerbots_commandscript") {}
ChatCommandTable GetCommands() const override
{
static ChatCommandTable playerbotsDebugCommandTable = {
{"bg", HandleDebugBGCommand, SEC_GAMEMASTER, Console::Yes},
};
static ChatCommandTable playerbotsAccountCommandTable = {
{"setKey", HandleSetSecurityKeyCommand, SEC_PLAYER, Console::No},
{"link", HandleLinkAccountCommand, SEC_PLAYER, Console::No},
{"linkedAccounts", HandleViewLinkedAccountsCommand, SEC_PLAYER, Console::No},
{"unlink", HandleUnlinkAccountCommand, SEC_PLAYER, Console::No},
};
static ChatCommandTable playerbotsCommandTable = {
{"bot", HandlePlayerbotCommand, SEC_PLAYER, Console::No},
{"gtask", HandleGuildTaskCommand, SEC_GAMEMASTER, Console::Yes},
{"pmon", HandlePerfMonCommand, SEC_GAMEMASTER, Console::Yes},
{"rndbot", HandleRandomPlayerbotCommand, SEC_GAMEMASTER, Console::Yes},
{"debug", playerbotsDebugCommandTable},
{"account", playerbotsAccountCommandTable},
};
static ChatCommandTable commandTable = {
{"playerbots", playerbotsCommandTable},
};
return commandTable;
}
static bool HandlePlayerbotCommand(ChatHandler* handler, char const* args)
{
return PlayerbotMgr::HandlePlayerbotMgrCommand(handler, args);
}
static bool HandleRandomPlayerbotCommand(ChatHandler* handler, char const* args)
{
return RandomPlayerbotMgr::HandlePlayerbotConsoleCommand(handler, args);
}
static bool HandleGuildTaskCommand(ChatHandler* handler, char const* args)
{
return GuildTaskMgr::HandleConsoleCommand(handler, args);
}
static bool HandlePerfMonCommand(ChatHandler* handler, char const* args)
{
if (!strcmp(args, "reset"))
{
sPerfMonitor->Reset();
return true;
}
if (!strcmp(args, "tick"))
{
sPerfMonitor->PrintStats(true, false);
return true;
}
if (!strcmp(args, "stack"))
{
sPerfMonitor->PrintStats(false, true);
return true;
}
if (!strcmp(args, "toggle"))
{
sPlayerbotAIConfig->perfMonEnabled = !sPlayerbotAIConfig->perfMonEnabled;
if (sPlayerbotAIConfig->perfMonEnabled)
LOG_INFO("playerbots", "Performance monitor enabled");
else
LOG_INFO("playerbots", "Performance monitor disabled");
return true;
}
sPerfMonitor->PrintStats();
return true;
}
static bool HandleDebugBGCommand(ChatHandler* handler, char const* args)
{
return BGTactics::HandleConsoleCommand(handler, args);
}
static bool HandleSetSecurityKeyCommand(ChatHandler* handler, char const* args)
{
if (!args || !*args)
{
handler->PSendSysMessage("Usage: .playerbots account setKey <securityKey>");
return false;
}
Player* player = handler->GetSession()->GetPlayer();
std::string key = args;
PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player);
if (mgr)
{
mgr->HandleSetSecurityKeyCommand(player, key);
return true;
}
else
{
handler->PSendSysMessage("PlayerbotMgr instance not found.");
return false;
}
}
static bool HandleLinkAccountCommand(ChatHandler* handler, char const* args)
{
if (!args || !*args)
return false;
char* accountName = strtok((char*)args, " ");
char* key = strtok(nullptr, " ");
if (!accountName || !key)
{
handler->PSendSysMessage("Usage: .playerbots account link <accountName> <securityKey>");
return false;
}
Player* player = handler->GetSession()->GetPlayer();
PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player);
if (mgr)
{
mgr->HandleLinkAccountCommand(player, accountName, key);
return true;
}
else
{
handler->PSendSysMessage("PlayerbotMgr instance not found.");
return false;
}
}
static bool HandleViewLinkedAccountsCommand(ChatHandler* handler, char const* /*args*/)
{
Player* player = handler->GetSession()->GetPlayer();
PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player);
if (mgr)
{
mgr->HandleViewLinkedAccountsCommand(player);
return true;
}
else
{
handler->PSendSysMessage("PlayerbotMgr instance not found.");
return false;
}
}
static bool HandleUnlinkAccountCommand(ChatHandler* handler, char const* args)
{
if (!args || !*args)
return false;
char* accountName = strtok((char*)args, " ");
if (!accountName)
{
handler->PSendSysMessage("Usage: .playerbots account unlink <accountName>");
return false;
}
Player* player = handler->GetSession()->GetPlayer();
PlayerbotMgr* mgr = sPlayerbotsMgr->GetPlayerbotMgr(player);
if (mgr)
{
mgr->HandleUnlinkAccountCommand(player, accountName);
return true;
}
else
{
handler->PSendSysMessage("PlayerbotMgr instance not found.");
return false;
}
}
};
void AddPlayerbotsCommandscripts() { new playerbots_commandscript(); }

View File

@@ -0,0 +1 @@
void AddPlayerbotsCommandscripts();

520
src/Script/Playerbots.cpp Normal file
View File

@@ -0,0 +1,520 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by the
* Free Software Foundation; either version 3 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Playerbots.h"
#include "Channel.h"
#include "Config.h"
#include "DatabaseEnv.h"
#include "DatabaseLoader.h"
#include "GuildTaskMgr.h"
#include "Metric.h"
#include "PlayerScript.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotGuildMgr.h"
#include "PlayerbotSpellRepository.h"
#include "PlayerbotWorldThreadProcessor.h"
#include "RandomPlayerbotMgr.h"
#include "ScriptMgr.h"
#include "PlayerbotCommandScript.h"
#include "cmath"
#include "BattleGroundTactics.h"
class PlayerbotsDatabaseScript : public DatabaseScript
{
public:
PlayerbotsDatabaseScript() : DatabaseScript("PlayerbotsDatabaseScript") {}
bool OnDatabasesLoading() override
{
DatabaseLoader playerbotLoader("server.playerbots");
playerbotLoader.SetUpdateFlags(sConfigMgr->GetOption<bool>("Playerbots.Updates.EnableDatabases", true)
? DatabaseLoader::DATABASE_PLAYERBOTS
: 0);
playerbotLoader.AddDatabase(PlayerbotsDatabase, "Playerbots");
return playerbotLoader.Load();
}
void OnDatabasesKeepAlive() override { PlayerbotsDatabase.KeepAlive(); }
void OnDatabasesClosing() override { PlayerbotsDatabase.Close(); }
void OnDatabaseWarnAboutSyncQueries(bool apply) override { PlayerbotsDatabase.WarnAboutSyncQueries(apply); }
void OnDatabaseSelectIndexLogout(Player* player, uint32& statementIndex, uint32& statementParam) override
{
statementIndex = CHAR_UPD_CHAR_OFFLINE;
statementParam = player->GetGUID().GetCounter();
}
void OnDatabaseGetDBRevision(std::string& revision) override
{
if (QueryResult resultPlayerbot =
PlayerbotsDatabase.Query("SELECT date FROM version_db_playerbots ORDER BY date DESC LIMIT 1"))
{
Field* fields = resultPlayerbot->Fetch();
revision = fields[0].Get<std::string>();
}
if (revision.empty())
{
revision = "Unknown Playerbots Database Revision";
}
}
};
class PlayerbotsPlayerScript : public PlayerScript
{
public:
PlayerbotsPlayerScript() : PlayerScript("PlayerbotsPlayerScript", {
PLAYERHOOK_ON_LOGIN,
PLAYERHOOK_ON_AFTER_UPDATE,
PLAYERHOOK_ON_BEFORE_CRITERIA_PROGRESS,
PLAYERHOOK_ON_BEFORE_ACHI_COMPLETE,
PLAYERHOOK_CAN_PLAYER_USE_PRIVATE_CHAT,
PLAYERHOOK_CAN_PLAYER_USE_GROUP_CHAT,
PLAYERHOOK_CAN_PLAYER_USE_GUILD_CHAT,
PLAYERHOOK_CAN_PLAYER_USE_CHANNEL_CHAT,
PLAYERHOOK_ON_GIVE_EXP,
PLAYERHOOK_ON_BEFORE_TELEPORT
}) {}
void OnPlayerLogin(Player* player) override
{
if (!player->GetSession()->IsBot())
{
sPlayerbotsMgr->AddPlayerbotData(player, false);
sRandomPlayerbotMgr->OnPlayerLogin(player);
// Before modifying the following messages, please make sure it does not violate the AGPLv3.0 license
// especially if you are distributing a repack or hosting a public server
// e.g. you can replace the URL with your own repository,
// but it should be publicly accessible and include all modifications you've made
if (sPlayerbotAIConfig->enabled)
{
ChatHandler(player->GetSession()).SendSysMessage(
"|cff00ff00This server runs with |cff00ccffmod-playerbots|r "
"|cffcccccchttps://github.com/mod-playerbots/mod-playerbots|r");
}
if (sPlayerbotAIConfig->enabled || sPlayerbotAIConfig->randomBotAutologin)
{
std::string roundedTime =
std::to_string(std::ceil((sPlayerbotAIConfig->maxRandomBots * 0.11 / 60) * 10) / 10.0);
roundedTime = roundedTime.substr(0, roundedTime.find('.') + 2);
ChatHandler(player->GetSession()).SendSysMessage(
"|cff00ff00Playerbots:|r bot initialization at server startup takes about '"
+ roundedTime + "' minutes.");
}
}
}
bool OnPlayerBeforeTeleport(Player* /*player*/, uint32 /*mapid*/, float /*x*/, float /*y*/, float /*z*/, float /*orientation*/, uint32 /*options*/, Unit* /*target*/) override
{
/* for now commmented out until proven its actually required
* havent seen any proof CleanVisibilityReferences() is needed
// If the player is not safe to touch, do nothing
if (!player)
return true;
// If same map or not in world do nothing
if (!player->IsInWorld() || player->GetMapId() == mapid)
return true;
// If real player do nothing
PlayerbotAI* ai = GET_PLAYERBOT_AI(player);
if (!ai || ai->IsRealPlayer())
return true;
// Cross-map bot teleport: defer visibility reference cleanup.
// CleanVisibilityReferences() erases this bot's GUID from other objects' visibility containers.
// This is intentionally done via the event queue (instead of directly here) because erasing
// from other players' visibility maps inside the teleport call stack can hit unsafe re-entrancy
// or iterator invalidation while visibility updates are in progress
ObjectGuid guid = player->GetGUID();
player->m_Events.AddEventAtOffset(
[guid, mapid]()
{
// do nothing, if the player is not safe to touch
Player* p = ObjectAccessor::FindPlayer(guid);
if (!p || !p->IsInWorld() || p->IsDuringRemoveFromWorld())
return;
// do nothing if we are already on the target map
if (p->GetMapId() == mapid)
return;
p->GetObjectVisibilityContainer().CleanVisibilityReferences();
},
Milliseconds(0));
*/
return true;
}
void OnPlayerAfterUpdate(Player* player, uint32 diff) override
{
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player))
{
botAI->UpdateAI(diff);
}
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
{
playerbotMgr->UpdateAI(diff);
}
}
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Player* receiver) override
{
if (type == CHAT_MSG_WHISPER)
{
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(receiver))
{
botAI->HandleCommand(type, msg, player);
// hotfix; otherwise the server will crash when whispering logout
// https://github.com/mod-playerbots/mod-playerbots/pull/1838
// TODO: find the root cause and solve it. (does not happen in party chat)
if (msg == "logout")
return false;
}
}
return true;
}
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Group* group) override
{
for (GroupReference* itr = group->GetFirstMember(); itr != nullptr; itr = itr->next())
{
if (Player* member = itr->GetSource())
{
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(member))
{
botAI->HandleCommand(type, msg, player);
}
}
}
return true;
}
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Guild* guild) override
{
if (type == CHAT_MSG_GUILD)
{
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
{
for (PlayerBotMap::const_iterator it = playerbotMgr->GetPlayerBotsBegin();
it != playerbotMgr->GetPlayerBotsEnd(); ++it)
{
if (Player* const bot = it->second)
{
if (bot->GetGuildId() == player->GetGuildId())
{
GET_PLAYERBOT_AI(bot)->HandleCommand(type, msg, player);
}
}
}
}
}
return true;
}
bool OnPlayerCanUseChat(Player* player, uint32 type, uint32 /*lang*/, std::string& msg, Channel* channel) override
{
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
{
if (channel->GetFlags() & 0x18)
{
playerbotMgr->HandleCommand(type, msg);
}
}
sRandomPlayerbotMgr->HandleCommand(type, msg, player);
return true;
}
bool OnPlayerBeforeAchievementComplete(Player* player, AchievementEntry const* achievement) override
{
if ((sRandomPlayerbotMgr->IsRandomBot(player) || sRandomPlayerbotMgr->IsAddclassBot(player)) &&
(achievement->flags & (ACHIEVEMENT_FLAG_REALM_FIRST_REACH | ACHIEVEMENT_FLAG_REALM_FIRST_KILL)))
{
return false;
}
return true;
}
void OnPlayerGiveXP(Player* player, uint32& amount, Unit* /*victim*/, uint8 /*xpSource*/) override
{
// early return
if (sPlayerbotAIConfig->randomBotXPRate == 1.0 || !player)
return;
// no XP multiplier, when player is no bot.
if (!player->GetSession()->IsBot() || !sRandomPlayerbotMgr->IsRandomBot(player))
return;
// no XP multiplier, when bot is in a group with a real player.
if (Group* group = player->GetGroup())
{
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
{
Player* member = gref->GetSource();
if (!member)
{
continue;
}
if (!member->GetSession()->IsBot())
{
return;
}
}
}
// otherwise apply bot XP multiplier.
amount = static_cast<uint32>(std::round(static_cast<float>(amount) * sPlayerbotAIConfig->randomBotXPRate));
}
};
class PlayerbotsMiscScript : public MiscScript
{
public:
PlayerbotsMiscScript() : MiscScript("PlayerbotsMiscScript", {MISCHOOK_ON_DESTRUCT_PLAYER}) {}
void OnDestructPlayer(Player* player) override
{
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player))
{
delete botAI;
}
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
{
delete playerbotMgr;
}
}
};
class PlayerbotsServerScript : public ServerScript
{
public:
PlayerbotsServerScript() : ServerScript("PlayerbotsServerScript", {
SERVERHOOK_CAN_PACKET_RECEIVE
}) {}
void OnPacketReceived(WorldSession* session, WorldPacket const& packet) override
{
if (Player* player = session->GetPlayer())
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
playerbotMgr->HandleMasterIncomingPacket(packet);
}
};
class PlayerbotsWorldScript : public WorldScript
{
public:
PlayerbotsWorldScript() : WorldScript("PlayerbotsWorldScript", {
WORLDHOOK_ON_BEFORE_WORLD_INITIALIZED,
WORLDHOOK_ON_UPDATE
}) {}
void OnBeforeWorldInitialized() override
{
// Before modifying the following messages, please make sure it does not violate the AGPLv3.0 license
// especially if you are distributing a repack or hosting a public server
// e.g. you can replace the URL with your own repository,
// but it should be publicly accessible and include all modifications you've made
LOG_INFO("server.loading", "╔══════════════════════════════════════════════════════════╗");
LOG_INFO("server.loading", "║ ║");
LOG_INFO("server.loading", "║ AzerothCore Playerbots Module ║");
LOG_INFO("server.loading", "║ ║");
LOG_INFO("server.loading", "╟──────────────────────────────────────────────────────────╢");
LOG_INFO("server.loading", "║ mod-playerbots is a community-driven open-source ║");
LOG_INFO("server.loading", "║ project based on AzerothCore, licensed under AGPLv3.0 ║");
LOG_INFO("server.loading", "╟──────────────────────────────────────────────────────────╢");
LOG_INFO("server.loading", "║ https://github.com/mod-playerbots/mod-playerbots ║");
LOG_INFO("server.loading", "╚══════════════════════════════════════════════════════════╝");
uint32 oldMSTime = getMSTime();
LOG_INFO("server.loading", " ");
LOG_INFO("server.loading", "Load Playerbots Config...");
sPlayerbotAIConfig->Initialize();
LOG_INFO("server.loading", ">> Loaded playerbots config in {} ms", GetMSTimeDiffToNow(oldMSTime));
LOG_INFO("server.loading", " ");
sPlayerbotSpellRepository->Initialize();
LOG_INFO("server.loading", "Playerbots World Thread Processor initialized");
}
void OnUpdate(uint32 diff) override
{
sPlayerbotWorldProcessor->Update(diff);
sRandomPlayerbotMgr->UpdateAI(diff); // World thread only
}
};
class PlayerbotsScript : public PlayerbotScript
{
public:
PlayerbotsScript() : PlayerbotScript("PlayerbotsScript") {}
bool OnPlayerbotCheckLFGQueue(lfg::Lfg5Guids const& guidsList) override
{
bool nonBotFound = false;
for (ObjectGuid const& guid : guidsList.guids)
{
Player* player = ObjectAccessor::FindPlayer(guid);
if (guid.IsGroup() || (player && !GET_PLAYERBOT_AI(player)))
{
nonBotFound = true;
break;
}
}
return nonBotFound;
}
void OnPlayerbotCheckKillTask(Player* player, Unit* victim) override
{
if (player)
sGuildTaskMgr->CheckKillTask(player, victim);
}
void OnPlayerbotCheckPetitionAccount(Player* player, bool& found) override
{
if (found && GET_PLAYERBOT_AI(player))
found = false;
}
bool OnPlayerbotCheckUpdatesToSend(Player* player) override
{
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player))
return botAI->IsRealPlayer();
return true;
}
void OnPlayerbotPacketSent(Player* player, WorldPacket const* packet) override
{
if (!player)
return;
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(player))
{
botAI->HandleBotOutgoingPacket(*packet);
}
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
{
playerbotMgr->HandleMasterOutgoingPacket(*packet);
}
}
void OnPlayerbotUpdate(uint32 diff) override
{
sRandomPlayerbotMgr->UpdateSessions(); // Per-bot updates only
}
void OnPlayerbotUpdateSessions(Player* player) override
{
if (player)
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
playerbotMgr->UpdateSessions();
}
void OnPlayerbotLogout(Player* player) override
{
if (PlayerbotMgr* playerbotMgr = GET_PLAYERBOT_MGR(player))
{
PlayerbotAI* botAI = GET_PLAYERBOT_AI(player);
if (!botAI || botAI->IsRealPlayer())
{
playerbotMgr->LogoutAllBots();
}
}
sRandomPlayerbotMgr->OnPlayerLogout(player);
}
void OnPlayerbotLogoutBots() override
{
LOG_INFO("playerbots", "Logging out all bots...");
sRandomPlayerbotMgr->LogoutAllBots();
}
};
class PlayerBotsBGScript : public BGScript
{
public:
PlayerBotsBGScript() : BGScript("PlayerBotsBGScript") {}
void OnBattlegroundStart(Battleground* bg) override
{
BGStrategyData data;
switch (bg->GetBgTypeID())
{
case BATTLEGROUND_WS:
data.allianceStrategy = urand(0, WS_STRATEGY_MAX - 1);
data.hordeStrategy = urand(0, WS_STRATEGY_MAX - 1);
break;
case BATTLEGROUND_AB:
data.allianceStrategy = urand(0, AB_STRATEGY_MAX - 1);
data.hordeStrategy = urand(0, AB_STRATEGY_MAX - 1);
break;
case BATTLEGROUND_AV:
data.allianceStrategy = urand(0, AV_STRATEGY_MAX - 1);
data.hordeStrategy = urand(0, AV_STRATEGY_MAX - 1);
break;
case BATTLEGROUND_EY:
data.allianceStrategy = urand(0, EY_STRATEGY_MAX - 1);
data.hordeStrategy = urand(0, EY_STRATEGY_MAX - 1);
break;
default:
break;
}
bgStrategies[bg->GetInstanceID()] = data;
}
void OnBattlegroundEnd(Battleground* bg, TeamId /*winnerTeam*/) override { bgStrategies.erase(bg->GetInstanceID()); }
};
void AddPlayerbotsSecureLoginScripts();
void AddPlayerbotsScripts()
{
new PlayerbotsDatabaseScript();
new PlayerbotsPlayerScript();
new PlayerbotsMiscScript();
new PlayerbotsServerScript();
new PlayerbotsWorldScript();
new PlayerbotsScript();
new PlayerBotsBGScript();
AddPlayerbotsSecureLoginScripts();
AddPlayerbotsCommandscripts();
PlayerBotsGuildValidationScript();
}

52
src/Script/Playerbots.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* 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_H
#define _PLAYERBOT_H
#include "AiObjectContext.h"
#include "Group.h"
#include "Pet.h"
#include "PlayerbotAI.h"
#include "PlayerbotAIConfig.h"
#include "PlayerbotMgr.h"
#include "RandomPlayerbotMgr.h"
#include "SharedValueContext.h"
#include "Spell.h"
#include "SpellMgr.h"
#include "TravelNode.h"
std::vector<std::string> split(std::string const s, char delim);
void split(std::vector<std::string>& dest, std::string const str, char const* delim);
#ifndef WIN32
int strcmpi(char const* s1, char const* s2);
#endif
#define CAST_ANGLE_IN_FRONT (2.f * static_cast<float>(M_PI) / 3.f)
#define EMOTE_ANGLE_IN_FRONT (2.f * static_cast<float>(M_PI) / 6.f)
#define GET_PLAYERBOT_AI(object) sPlayerbotsMgr->GetPlayerbotAI(object)
#define GET_PLAYERBOT_MGR(object) sPlayerbotsMgr->GetPlayerbotMgr(object)
#define AI_VALUE(type, name) context->GetValue<type>(name)->Get()
#define AI_VALUE2(type, name, param) context->GetValue<type>(name, param)->Get()
#define AI_VALUE_LAZY(type, name) context->GetValue<type>(name)->LazyGet()
#define AI_VALUE2_LAZY(type, name, param) context->GetValue<type>(name, param)->LazyGet()
#define AI_VALUE_REF(type, name) context->GetValue<type>(name)->RefGet()
#define SET_AI_VALUE(type, name, value) context->GetValue<type>(name)->Set(value)
#define SET_AI_VALUE2(type, name, param, value) context->GetValue<type>(name, param)->Set(value)
#define RESET_AI_VALUE(type, name) context->GetValue<type>(name)->Reset()
#define RESET_AI_VALUE2(type, name, param) context->GetValue<type>(name, param)->Reset()
#define PAI_VALUE(type, name) sPlayerbotsMgr->GetPlayerbotAI(player)->GetAiObjectContext()->GetValue<type>(name)->Get()
#define PAI_VALUE2(type, name, param) \
sPlayerbotsMgr->GetPlayerbotAI(player)->GetAiObjectContext()->GetValue<type>(name, param)->Get()
#define GAI_VALUE(type, name) sSharedValueContext->getGlobalValue<type>(name)->Get()
#define GAI_VALUE2(type, name, param) sSharedValueContext->getGlobalValue<type>(name, param)->Get()
#endif

View File

@@ -0,0 +1,82 @@
#include "ScriptMgr.h"
#include "Opcodes.h"
#include "Player.h"
#include "ObjectAccessor.h"
#include "Playerbots.h"
namespace
{
static Player* FindOnlineAltbotByGuid(ObjectGuid guid)
{
if (!guid)
return nullptr;
Player* p = ObjectAccessor::FindPlayer(guid);
if (!p)
return nullptr;
PlayerbotAI* ai = GET_PLAYERBOT_AI(p);
if (!ai || ai->IsRealPlayer())
return nullptr;
return p;
}
static void ForceLogoutViaPlayerbotHolder(Player* target)
{
if (!target)
return;
PlayerbotAI* ai = GET_PLAYERBOT_AI(target);
if (!ai)
return;
if (Player* master = ai->GetMaster())
{
if (PlayerbotMgr* mgr = GET_PLAYERBOT_MGR(master))
{
mgr->LogoutPlayerBot(target->GetGUID());
return;
}
}
if (sRandomPlayerbotMgr)
{
sRandomPlayerbotMgr->LogoutPlayerBot(target->GetGUID());
return;
}
}
}
class PlayerbotsSecureLoginServerScript : public ServerScript
{
public:
PlayerbotsSecureLoginServerScript()
: ServerScript("PlayerbotsSecureLoginServerScript", { SERVERHOOK_CAN_PACKET_RECEIVE }) {}
bool CanPacketReceive(WorldSession* /*session*/, WorldPacket& packet) override
{
if (packet.GetOpcode() != CMSG_PLAYER_LOGIN)
return true;
auto const oldPos = packet.rpos();
ObjectGuid loginGuid;
packet >> loginGuid;
packet.rpos(oldPos);
if (!loginGuid)
return true;
Player* existingAltbot = FindOnlineAltbotByGuid(loginGuid);
if (existingAltbot)
ForceLogoutViaPlayerbotHolder(existingAltbot);
return true;
}
};
void AddPlayerbotsSecureLoginScripts()
{
new PlayerbotsSecureLoginServerScript();
}

View File

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

View File

@@ -0,0 +1,513 @@
/*
* 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 "Playerbots.h"
#include "ObjectAccessor.h"
#include "PlayerbotOperation.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "PlayerbotMgr.h"
#include "PlayerbotRepository.h"
#include "RandomPlayerbotMgr.h"
#include "WorldSession.h"
#include "WorldSessionMgr.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())
sPlayerbotRepository->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, uint32 masterAccountId)
: m_botGuid(botGuid), m_masterAccountId(masterAccountId)
{
}
bool Execute() override
{
// find and verify bot still exists
Player* bot = ObjectAccessor::FindConnectedPlayer(m_botGuid);
if (!bot)
return false;
PlayerbotHolder* holder = sRandomPlayerbotMgr;
if (m_masterAccountId)
{
WorldSession* masterSession = sWorldSessionMgr->FindSession(m_masterAccountId);
Player* masterPlayer = masterSession ? masterSession->GetPlayer() : nullptr;
if (masterPlayer)
holder = GET_PLAYERBOT_MGR(masterPlayer);
}
if (!holder)
return false;
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;
uint32 m_masterAccountId = 0;
};
#endif

View File

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

View File

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

View File

@@ -0,0 +1,139 @@
/*
* 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 "Queue.h"
#include "AiObjectContext.h"
#include "Log.h"
#include "PlayerbotAIConfig.h"
void Queue::Push(ActionBasket* action)
{
if (!action)
{
return;
}
for (ActionBasket* basket : actions)
{
if (action->getAction()->getName() == basket->getAction()->getName())
{
updateExistingBasket(basket, action);
return;
}
}
actions.push_back(action);
}
ActionNode* Queue::Pop()
{
ActionBasket* highestRelevanceBasket = findHighestRelevanceBasket();
if (!highestRelevanceBasket)
{
return nullptr;
}
return extractAndDeleteBasket(highestRelevanceBasket);
}
ActionBasket* Queue::Peek()
{
return findHighestRelevanceBasket();
}
uint32 Queue::Size()
{
return actions.size();
}
void Queue::RemoveExpired()
{
if (!sPlayerbotAIConfig->expireActionTime)
{
return;
}
std::list<ActionBasket*> expiredBaskets;
collectExpiredBaskets(expiredBaskets);
removeAndDeleteBaskets(expiredBaskets);
}
// Private helper methods
void Queue::updateExistingBasket(ActionBasket* existing, ActionBasket* newBasket)
{
if (existing->getRelevance() < newBasket->getRelevance())
{
existing->setRelevance(newBasket->getRelevance());
}
if (ActionNode* actionNode = newBasket->getAction())
{
delete actionNode;
}
delete newBasket;
}
ActionBasket* Queue::findHighestRelevanceBasket() const
{
if (actions.empty())
{
return nullptr;
}
float maxRelevance = -1.0f;
ActionBasket* selection = nullptr;
for (ActionBasket* basket : actions)
{
if (!basket)
{
continue;
}
if (basket->getRelevance() > maxRelevance)
{
maxRelevance = basket->getRelevance();
selection = basket;
}
}
return selection;
}
ActionNode* Queue::extractAndDeleteBasket(ActionBasket* basket)
{
ActionNode* action = basket->getAction();
actions.remove(basket);
delete basket;
return action;
}
void Queue::collectExpiredBaskets(std::list<ActionBasket*>& expiredBaskets)
{
uint32 expiryTime = sPlayerbotAIConfig->expireActionTime;
for (ActionBasket* basket : actions)
{
if (basket->isExpired(expiryTime))
{
expiredBaskets.push_back(basket);
}
}
}
void Queue::removeAndDeleteBaskets(std::list<ActionBasket*>& basketsToRemove)
{
for (ActionBasket* basket : basketsToRemove)
{
actions.remove(basket);
if (ActionNode* action = basket->getAction())
{
delete action;
}
delete basket;
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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_QUEUE_H
#define PLAYERBOT_QUEUE_H
#include "Action.h"
#include "Common.h"
/**
* @class Queue
* @brief Manages a priority queue of actions for the playerbot system
*
* This queue maintains a list of ActionBasket objects, each containing an action
* and its relevance score. Actions with higher relevance scores are prioritized.
*/
class Queue
{
public:
Queue() = default;
~Queue() = default;
/**
* @brief Adds an action to the queue or updates existing action's relevance
* @param action Pointer to the ActionBasket to be added
*
* If an action with the same name exists, updates its relevance if the new
* relevance is higher, then deletes the new action. Otherwise, adds the new
* action to the queue.
*/
void Push(ActionBasket* action);
/**
* @brief Removes and returns the action with highest relevance
* @return Pointer to the highest relevance ActionNode, or nullptr if queue is empty
*
* Ownership of the returned ActionNode is transferred to the caller.
* The associated ActionBasket is deleted.
*/
ActionNode* Pop();
/**
* @brief Returns the action with highest relevance without removing it
* @return Pointer to the ActionBasket with highest relevance, or nullptr if queue is empty
*/
ActionBasket* Peek();
/**
* @brief Returns the current size of the queue
* @return Number of actions in the queue
*/
uint32 Size();
/**
* @brief Removes and deletes expired actions from the queue
*
* Uses sPlayerbotAIConfig->expireActionTime to determine if actions have expired.
* Both the ActionNode and ActionBasket are deleted for expired actions.
*/
void RemoveExpired();
private:
/**
* @brief Updates existing basket with new relevance and cleans up new basket
*/
void updateExistingBasket(ActionBasket* existing, ActionBasket* newBasket);
/**
* @brief Finds the basket with the highest relevance score
* @return Pointer to the highest relevance basket, or nullptr if queue is empty
*/
ActionBasket* findHighestRelevanceBasket() const;
/**
* @brief Extracts action from basket and handles basket cleanup
*/
ActionNode* extractAndDeleteBasket(ActionBasket* basket);
/**
* @brief Collects all expired baskets into the provided list
*/
void collectExpiredBaskets(std::list<ActionBasket*>& expiredBaskets);
/**
* @brief Removes and deletes all baskets in the provided list
*/
void removeAndDeleteBaskets(std::list<ActionBasket*>& basketsToRemove);
std::list<ActionBasket*> actions; /**< Container for action baskets */
};
#endif

View File

@@ -0,0 +1,22 @@
/*
* This file is part of the AzerothCore Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// From SC
void AddPlayerbotsScripts();
// Add all
void Addmod_playerbotsScripts() { AddPlayerbotsScripts(); }