Skip to content

Commit 3f45817

Browse files
authored
Harden Playerbot event processing and diagnostics (#479)
* Harden Playerbot action logging * Preserve Playerbot event arrival order * Guard Playerbot packet dispatch * Instrument Playerbot performance hot paths
1 parent 071bc44 commit 3f45817

22 files changed

Lines changed: 747 additions & 21 deletions

src/game/Server/WorldSession.cpp

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
#endif /* ENABLE_ELUNA */
8383
#ifdef ENABLE_PLAYERBOTS
8484
#include "playerbot.h"
85+
#include "PlayerbotAIConfig.h"
86+
#include "PlayerbotPacketPolicy.h"
87+
#include "PlayerbotPerformanceMonitor.h"
8588
#endif
8689

8790
#include <cstdarg>
@@ -285,6 +288,10 @@ void WorldSession::SendPacket(WorldPacket const* packet)
285288
{
286289
if (GetPlayer()->GetPlayerbotAI())
287290
{
291+
if (sPlayerbotAIConfig.performanceMetricsInterval)
292+
{
293+
ai::sPlayerbotPerformanceMonitor.RecordBuiltPacket(packet->size());
294+
}
288295
GetPlayer()->GetPlayerbotAI()->HandleBotOutgoingPacket(*packet);
289296
}
290297
else if (GetPlayer()->GetPlayerbotMgr())
@@ -1121,12 +1128,35 @@ bool WorldSession::Update(PacketFilter& updater)
11211128
*/
11221129
void WorldSession::HandleBotPackets()
11231130
{
1124-
WorldPacket* packet;
1131+
WorldPacket* packet = NULL;
11251132
while (m_mailbox->Next(packet))
11261133
{
1127-
OpcodeHandler const& opHandle = opcodeTable[packet->GetOpcode()];
1128-
(this->*opHandle.handler)(*packet);
1129-
delete packet;
1134+
std::unique_ptr<WorldPacket> packetHolder(packet);
1135+
if (!packet || !_player || !_player->IsInWorld())
1136+
{
1137+
continue;
1138+
}
1139+
1140+
OpcodeHandler const* opHandle = ai::FindDispatchablePlayerbotOpcodeHandler(
1141+
opcodeTable, packet->GetOpcode(), STATUS_LOGGEDIN);
1142+
if (!opHandle)
1143+
{
1144+
sLog.outError(
1145+
"PLAYERBOT: rejected queued opcode %s (0x%.4X): not a dispatchable logged-in opcode",
1146+
LookupOpcodeName(packet->GetOpcode()), packet->GetOpcode());
1147+
continue;
1148+
}
1149+
1150+
try
1151+
{
1152+
ExecuteOpcode(*opHandle, packet);
1153+
}
1154+
catch (ByteBufferException&)
1155+
{
1156+
sLog.outError(
1157+
"WorldSession::HandleBotPackets ByteBufferException while parsing opcode %s (0x%.4X)",
1158+
LookupOpcodeName(packet->GetOpcode()), packet->GetOpcode());
1159+
}
11301160
}
11311161
}
11321162
#endif

src/game/WorldHandlers/World.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113

114114
#include "PlayerbotAIConfig.h"
115115
#include "PlayerbotMgr.h"
116+
#include "PlayerbotPerformanceMonitor.h"
116117
#include "RandomPlayerbotMgr.h"
117118
#endif
118119

@@ -1434,6 +1435,10 @@ void World::Update(uint32 diff)
14341435
#ifdef ENABLE_PLAYERBOTS
14351436
sRandomPlayerbotMgr.UpdateAI(diff);
14361437
sRandomPlayerbotMgr.UpdateSessions(diff);
1438+
if (sPlayerbotAIConfig.performanceMetricsInterval)
1439+
{
1440+
ai::ReportPlayerbotPerformanceIfDue(sPlayerbotAIConfig.performanceMetricsInterval);
1441+
}
14371442
#endif
14381443

14391444
/// <li> Handle session updates

src/modules/Bots/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ set(BOT_SRCS
3838
playerbot/PlayerbotAIBase.h
3939
playerbot/PlayerbotAIConfig.cpp
4040
playerbot/PlayerbotAIConfig.h
41+
playerbot/PlayerbotPerformanceMonitor.cpp
42+
playerbot/PlayerbotPerformanceMonitor.h
4143
playerbot/playerbotDefs.h
4244
playerbot/PlayerbotFactory.cpp
4345
playerbot/PlayerbotFactory.h

src/modules/Bots/playerbot/PlayerbotAI.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ void PacketHandlingHelper::Handle(ExternalEventHelper &helper)
6262
{
6363
while (!queue.empty())
6464
{
65-
helper.HandlePacket(handlers, queue.top());
65+
helper.HandlePacket(handlers, queue.front());
6666
queue.pop();
6767
}
6868
}
@@ -437,7 +437,7 @@ void PlayerbotAI::UpdateAIInternal(uint32 elapsed)
437437
ExternalEventHelper helper(aiObjectContext);
438438
while (!chatCommands.empty())
439439
{
440-
ChatCommandHolder holder = chatCommands.top();
440+
ChatCommandHolder holder = chatCommands.front();
441441
string command = holder.GetCommand();
442442
Player* owner = holder.GetOwner();
443443
if (!helper.ParseChatCommand(command, owner) && holder.GetType() == CHAT_MSG_WHISPER)

src/modules/Bots/playerbot/PlayerbotAI.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "strategy/Engine.h"
77
#include "strategy/ExternalEventHelper.h"
88
#include "ChatFilter.h"
9+
#include "PlayerbotEventQueue.h"
910
#include "PlayerbotSecurity.h"
1011

1112
class Player;
@@ -92,7 +93,7 @@ class PacketHandlingHelper
9293

9394
private:
9495
map<uint16, string> handlers;
95-
stack<WorldPacket> queue;
96+
ai::PlayerbotEventQueue<WorldPacket> queue;
9697
};
9798

9899
/**
@@ -280,7 +281,7 @@ class PlayerbotAI : public PlayerbotAIBase
280281
BotState currentState;
281282
uint32 m_targetContextRevision; ///< Invalidates target observations across AI context changes.
282283
ChatHelper chatHelper;
283-
stack<ChatCommandHolder> chatCommands;
284+
ai::PlayerbotEventQueue<ChatCommandHolder> chatCommands;
284285
PacketHandlingHelper botOutgoingPacketHandlers;
285286
PacketHandlingHelper masterIncomingPacketHandlers;
286287
PacketHandlingHelper masterOutgoingPacketHandlers;

src/modules/Bots/playerbot/PlayerbotAIBase.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "../botpch.h"
22
#include "playerbot.h"
33
#include "PlayerbotAIConfig.h"
4+
#include "PlayerbotPerformanceMonitor.h"
45

56
using namespace ai;
67
using namespace std;
@@ -32,11 +33,22 @@ void PlayerbotAIBase::UpdateAI(uint32 elapsed)
3233
// Check if the AI can be updated
3334
if (!CanUpdateAI())
3435
{
36+
if (sPlayerbotAIConfig.performanceMetricsInterval)
37+
{
38+
ai::sPlayerbotPerformanceMonitor.RecordAiDeferred();
39+
}
3540
return;
3641
}
3742

3843
// Update the AI internal state
44+
std::uint64_t const evaluationStartedAtMicros = sPlayerbotAIConfig.performanceMetricsInterval ?
45+
ai::PlayerbotPerformanceNowMicros() : 0;
3946
UpdateAIInternal(elapsed);
47+
if (sPlayerbotAIConfig.performanceMetricsInterval)
48+
{
49+
ai::sPlayerbotPerformanceMonitor.RecordAiEvaluation(
50+
ai::PlayerbotPerformanceNowMicros() - evaluationStartedAtMicros);
51+
}
4052
// Yield the current thread
4153
YieldThread();
4254
}

src/modules/Bots/playerbot/PlayerbotAIConfig.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ PlayerbotAIConfig::PlayerbotAIConfig()
7777
randomBotStarterZoneQuota(0),
7878
logInGroupOnly(false),
7979
logValuesPerTick(false),
80+
performanceMetricsInterval(0),
8081
fleeingEnabled(false),
8182
randomBotMinLevel(0),
8283
randomBotMaxLevel(0),
@@ -242,6 +243,14 @@ bool PlayerbotAIConfig::Initialize()
242243
randomBotJoinLfg = config.GetBoolDefault("AiPlayerbot.RandomBotJoinLfg", true);
243244
logInGroupOnly = config.GetBoolDefault("AiPlayerbot.LogInGroupOnly", true);
244245
logValuesPerTick = config.GetBoolDefault("AiPlayerbot.LogValuesPerTick", false);
246+
int32 configuredPerformanceMetricsInterval = config.GetIntDefault("AiPlayerbot.PerformanceMetricsInterval", 0);
247+
if (configuredPerformanceMetricsInterval < 0)
248+
{
249+
sLog.outError("AiPlayerbot.PerformanceMetricsInterval cannot be negative; %d given, disabling instrumentation",
250+
configuredPerformanceMetricsInterval);
251+
configuredPerformanceMetricsInterval = 0;
252+
}
253+
performanceMetricsInterval = static_cast<uint32>(configuredPerformanceMetricsInterval);
245254
fleeingEnabled = config.GetBoolDefault("AiPlayerbot.FleeingEnabled", true);
246255
randomBotMinLevel = config.GetIntDefault("AiPlayerbot.RandomBotMinLevel", 1);
247256
randomBotMaxLevel = config.GetIntDefault("AiPlayerbot.RandomBotMaxLevel", 255);

src/modules/Bots/playerbot/PlayerbotAIConfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class PlayerbotAIConfig
8181
uint32 randomBotStarterZoneQuota; ///< Active residents each starting zone should hold before the roster fills at random. 0 disables.
8282
uint32 randomBotHomeAreaMaxLevel; ///< Bots at or below this level stay in the starting SUB-AREA -- Shadowglen, Northshire. 0 disables.
8383
bool logInGroupOnly, logValuesPerTick;
84+
uint32 performanceMetricsInterval; ///< Seconds between aggregate performance reports; 0 disables instrumentation.
8485
bool fleeingEnabled; ///< Indicates if fleeing is enabled for bots.
8586
std::string randomBotCombatStrategies, randomBotNonCombatStrategies;
8687
std::string botTankStrategies, botDpsStrategies, botHealStrategies, botGroupNonCombatStrategies;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* SPDX-License-Identifier: GPL-3.0-or-later
3+
*
4+
* MaNGOS is a full featured server for World of Warcraft, supporting
5+
* the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8
6+
*
7+
* Copyright (C) 2005-2026 MaNGOS <https://www.getmangos.eu>
8+
*
9+
* This program is free software; you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as published by
11+
* the Free Software Foundation; either version 2 of the License, or
12+
* (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*
22+
* World of Warcraft, and all World of Warcraft or Warcraft art, images,
23+
* and lore are copyrighted by Blizzard Entertainment, Inc.
24+
*/
25+
26+
#pragma once
27+
28+
#include <queue>
29+
30+
namespace ai
31+
{
32+
template <typename T>
33+
using PlayerbotEventQueue = std::queue<T>;
34+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* SPDX-License-Identifier: GPL-3.0-or-later
3+
*
4+
* MaNGOS is a full featured server for World of Warcraft, supporting
5+
* the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8
6+
*
7+
* Copyright (C) 2005-2026 MaNGOS <https://www.getmangos.eu>
8+
*
9+
* This program is free software; you can redistribute it and/or modify
10+
* it under the terms of the GNU General Public License as published by
11+
* the Free Software Foundation; either version 2 of the License, or
12+
* (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU General Public License
20+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*
22+
* World of Warcraft, and all World of Warcraft or Warcraft art, images,
23+
* and lore are copyrighted by Blizzard Entertainment, Inc.
24+
*/
25+
26+
#pragma once
27+
28+
#include <cstdarg>
29+
#include <cstddef>
30+
#include <cstdio>
31+
#include <string>
32+
33+
namespace ai
34+
{
35+
constexpr std::size_t PLAYERBOT_LOG_MESSAGE_CAPACITY = 1024;
36+
37+
inline std::string FormatPlayerbotLogMessage(char const* format, va_list args)
38+
{
39+
if (!format)
40+
{
41+
return std::string();
42+
}
43+
44+
char buffer[PLAYERBOT_LOG_MESSAGE_CAPACITY] = {};
45+
int const result = std::vsnprintf(buffer, sizeof(buffer), format, args);
46+
buffer[sizeof(buffer) - 1] = '\0';
47+
return result < 0 ? std::string() : std::string(buffer);
48+
}
49+
50+
inline bool WritePlayerbotLogLine(FILE* file, std::string const& message)
51+
{
52+
if (!file || std::fputs(message.c_str(), file) == EOF)
53+
{
54+
return false;
55+
}
56+
57+
return std::fputc('\n', file) != EOF;
58+
}
59+
}

0 commit comments

Comments
 (0)