Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1f8cc0c
refactor: add foundation for service-based architecture
TOoSmOotH Jan 24, 2026
8ff4c77
refactor: extract service implementations from PlayerbotAI
TOoSmOotH Jan 24, 2026
682dde0
refactor: extract handler implementations from PlayerbotAI
TOoSmOotH Jan 24, 2026
0a68a63
refactor: integrate BotServiceContainer into PlayerbotAI
TOoSmOotH Jan 24, 2026
10fdb6d
refactor: add manager interfaces and registry
TOoSmOotH Jan 24, 2026
3a70911
test: add unit tests for services and manager registry
TOoSmOotH Jan 24, 2026
8817414
test: add unit tests for AI pattern logic
TOoSmOotH Jan 24, 2026
c0e2ee7
test: add comprehensive unit tests for all AI subsystems
TOoSmOotH Jan 24, 2026
832b1ac
refactor: migrate role service from PlayerbotAI to BotRoleService
TOoSmOotH Jan 24, 2026
28def1a
refactor: update all spell method callers to use BotSpellService
TOoSmOotH Jan 24, 2026
51a9e80
refactor: update all chat method callers to use BotChatService
TOoSmOotH Jan 24, 2026
3d407ea
refactor: update all item method callers to use BotItemService
TOoSmOotH Jan 24, 2026
f7b5a69
test: fix unit test infrastructure for standalone building
TOoSmOotH Jan 24, 2026
b6593be
Delete SERVICE_MIGRATION_PROGRESS.md
TOoSmOotH Jan 24, 2026
884ff94
test: fix all failing unit test algorithm issues
TOoSmOotH Jan 24, 2026
69f75c1
fix: resolve build failures from service migration refactoring
TOoSmOotH Jan 25, 2026
66e1e7a
Add PlayerbotSecurity.h fixture for standalone tests
TOoSmOotH Jan 25, 2026
2f47399
feat: merge 6 upstream commits with service architecture adaptation
TOoSmOotH Jan 25, 2026
be61b81
Merge branch 'master' into refactor/total-architecture
TOoSmOotH Jan 25, 2026
2b2748f
Delete SERVICE_MIGRATION_PROGRESS.md
TOoSmOotH Jan 25, 2026
5365ba8
Added PR template (#2063)
hermensbas Jan 25, 2026
7abd836
Update PULL_REQUEST_TEMPLATE.md (#2064)
hermensbas Jan 25, 2026
c59a02e
Update PULL_REQUEST_TEMPLATE.md (#2065)
hermensbas Jan 25, 2026
43e8e31
Update PULL_REQUEST_TEMPLATE.md (#2066)
hermensbas Jan 25, 2026
f5711dc
FIX Onyxia Crash (#2062)
gutoukuaiken Jan 25, 2026
ba7a1d5
feat: wire up ManagerRegistry with production adapters
TOoSmOotH Jan 25, 2026
a1e0ae7
feat: expand service layer with item, spell, and chat functionality
TOoSmOotH Jan 25, 2026
4f5231f
chore: fix compiler warnings and update ARCHITECTURE.md
TOoSmOotH Jan 25, 2026
f82958a
fix: resolve undeclared identifiers in BattleGroundJoinAction
TOoSmOotH Jan 25, 2026
38af68c
style: remove extra blank line in PlayerbotMgr.cpp
TOoSmOotH Jan 25, 2026
a3b11b7
fix: initialize all ChatContext members in BuildChatContext
TOoSmOotH Jan 26, 2026
f67297e
Fix coding standard violations across mod-playerbots
TOoSmOotH Jan 26, 2026
58c81b2
Merge branch 'master' into refactor/total-architecture
TOoSmOotH Jan 26, 2026
c37c819
fix: prevent crash when character customization data is missing
TOoSmOotH Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
544 changes: 544 additions & 0 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Pull Request

Describe what this change does and why it is needed...

---

## Design Philosophy

We prioritize **stability, performance, and predictability** over behavioral realism.
Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and
long-term robustness.

Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all
participants. Because every action and
decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and
negatively affect both players and
world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a
project goal. Increased behavioral
realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead.

Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and
maintained continuously as the system evolves.
If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision
model**. More complex behavior should only be
available as an **explicit opt-in option**, clearly documented as having a measurable performance cost.

Principles:

- **Stability before intelligence**
A stable system is always preferred over a smarter one.

- **Performance is a shared resource**
Any increase in bot cost affects all players and all bots.

- **Simple logic scales better than smart logic**
Predictable behavior under load is more valuable than perfect decisions.

- **Complexity must justify itself**
If a feature cannot clearly explain its cost, it should not exist.

- **Defaults must be cheap**
Expensive behavior must always be optional and clearly communicated.

- **Bots should look reasonable, not perfect**
The goal is believable behavior, not human simulation.

Before submitting, confirm that this change aligns with those principles.

---

## Feature Evaluation

Please answer the following:

- Describe the **minimum logic** required to achieve the intended behavior?
- Describe the **cheapest implementation** that produces an acceptable result?
- Describe the **runtime cost** when this logic executes across many bots?

---

## How to Test the Changes

- Step-by-step instructions to test the change
- Any required setup (e.g. multiple players, bots, specific configuration)
- Expected behavior and how to verify it

## Complexity & Impact

- Does this change add new decision branches?
- [ ] No
- [ ] Yes (**explain below**)

- Does this change increase per-bot or per-tick processing?
- [ ] No
- [ ] Yes (**describe and justify impact**)

- Could this logic scale poorly under load?
- [ ] No
- [ ] Yes (**explain why**)

---

## Defaults & Configuration

- Does this change modify default bot behavior?
- [ ] No
- [ ] Yes (**explain why**)

If this introduces more advanced or AI-heavy logic:

- [ ] Lightweight mode remains the default
- [ ] More complex behavior is optional and thereby configurable

---

## AI Assistance

- Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change?
- [ ] No
- [ ] Yes (**explain below**)

If yes, please specify:

- AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.)
- Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation)
- Which parts of the change were influenced or generated
- Whether the result was manually reviewed and adapted

AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor.
Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest
about what they do and do not understand.

---

## Final Checklist

- [ ] Stability is not compromised
- [ ] Performance impact is understood, tested, and acceptable
- [ ] Added logic complexity is justified and explained
- [ ] Documentation updated if needed

---

## Notes for Reviewers

Anything that significantly improves realism at the cost of stability or performance should be carefully discussed
before merging.
2 changes: 1 addition & 1 deletion conf/playerbots.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ AiPlayerbot.SummonWhenGroup = 1
AiPlayerbot.SelfBotLevel = 1

# Non-GM player can only use init=auto to initialize bots based on their own level and gearscore
# Default: 0 (non-GM player can use any intialization commands)
# Default: 0 (non-GM player can use any initialization commands)
AiPlayerbot.AutoInitOnly = 0

# The upper limit ratio of bot equipment level for init=auto
Expand Down
2 changes: 1 addition & 1 deletion src/Ai/Base/Actions/AcceptBattlegroundInvitationAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#include "Event.h"
#include "Playerbots.h"

bool AcceptBgInvitationAction::Execute(Event event)
bool AcceptBgInvitationAction::Execute(Event /*event*/)
{
uint8 type = 0; // arenatype if arena
uint8 unk2 = 0; // unk, can be 0x0 (may be if was invited?) and 0x1
Expand Down
6 changes: 4 additions & 2 deletions src/Ai/Base/Actions/AcceptInvitationAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/

#include "BotChatService.h"
#include "AcceptInvitationAction.h"

#include "Bot/Core/ManagerRegistry.h"
#include "Event.h"
#include "ObjectAccessor.h"
#include "PlayerbotAIConfig.h"
Expand Down Expand Up @@ -46,7 +48,7 @@ bool AcceptInvitationAction::Execute(Event event)
if (!bot->GetGroup() || !bot->GetGroup()->IsMember(inviter->GetGUID()))
return false;

if (sRandomPlayerbotMgr->IsRandomBot(bot))
if (sManagerRegistry.GetRandomBotManager().IsRandomBot(bot))
botAI->SetMaster(inviter);
// else
// sPlayerbotRepository->Save(botAI);
Expand All @@ -55,7 +57,7 @@ bool AcceptInvitationAction::Execute(Event event)
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
botAI->Reset();

botAI->TellMaster("Hello");
botAI->GetServices().GetChatService().TellMaster("Hello");

if (sPlayerbotAIConfig->summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig->sightDistance)
{
Expand Down
13 changes: 7 additions & 6 deletions src/Ai/Base/Actions/AcceptQuestAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/

#include "BotChatService.h"
#include "AcceptQuestAction.h"

#include "Event.h"
Expand Down Expand Up @@ -88,7 +89,7 @@ bool AcceptQuestAction::Execute(Event event)
std::stringstream ss;
ss << "AcceptQuestAction [" << qInfo->GetTitle() << "] - [" << std::to_string(qInfo->GetQuestId()) << "]";
LOG_DEBUG("playerbots", "{}", ss.str().c_str());
// botAI->TellMaster(ss.str());
// botAI->GetServices().GetChatService().TellMaster(ss.str());
}

return hasAccept;
Expand All @@ -113,15 +114,15 @@ bool AcceptQuestShareAction::Execute(Event event)
if (bot->HasQuest(quest))
{
bot->SetDivider(ObjectGuid::Empty);
botAI->TellError("I have this quest");
botAI->GetServices().GetChatService().TellError("I have this quest");
return false;
}

if (!bot->CanTakeQuest(qInfo, false))
{
// can't take quest
bot->SetDivider(ObjectGuid::Empty);
botAI->TellError("I can't take this quest");
botAI->GetServices().GetChatService().TellError("I can't take this quest");

return false;
}
Expand Down Expand Up @@ -149,7 +150,7 @@ bool AcceptQuestShareAction::Execute(Event event)
bot->CastSpell(bot, qInfo->GetSrcSpell(), true);
}

botAI->TellMaster("Quest accepted");
botAI->GetServices().GetChatService().TellMaster("Quest accepted");
return true;
}

Expand All @@ -174,7 +175,7 @@ bool ConfirmQuestAction::Execute(Event event)
if (!bot->CanTakeQuest(qInfo, false))
{
// can't take quest
// botAI->TellError("quest_cant_take");
// botAI->GetServices().GetChatService().TellError("quest_cant_take");
return false;
}

Expand All @@ -190,7 +191,7 @@ bool ConfirmQuestAction::Execute(Event event)
bot->CastSpell(bot, qInfo->GetSrcSpell(), true);
}

// botAI->TellMaster("quest_accept");
// botAI->GetServices().GetChatService().TellMaster("quest_accept");
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Ai/Base/Actions/AddLootAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ bool AddLootAction::Execute(Event event)
return AI_VALUE(LootObjectStack*, "available loot")->Add(guid);
}

bool AddAllLootAction::Execute(Event event)
bool AddAllLootAction::Execute(Event /*event*/)
{
bool added = false;

Expand Down
9 changes: 5 additions & 4 deletions src/Ai/Base/Actions/AreaTriggerAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* and/or modify it under version 3 of the License, or (at your option), any later version.
*/

#include "BotChatService.h"
#include "AreaTriggerAction.h"

#include "Event.h"
Expand Down Expand Up @@ -36,7 +37,7 @@ bool ReachAreaTriggerAction::Execute(Event event)

if (bot->GetMapId() != at->map)
{
botAI->TellError("I won't follow: too far away");
botAI->GetServices().GetChatService().TellError("I won't follow: too far away");
return true;
}

Expand All @@ -51,14 +52,14 @@ bool ReachAreaTriggerAction::Execute(Event event)

float distance = bot->GetDistance(at->x, at->y, at->z);
float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig->reactDelay;
botAI->TellError("Wait for me");
botAI->GetServices().GetChatService().TellError("Wait for me");
botAI->SetNextCheckDelay(delay);
context->GetValue<LastMovement&>("last area trigger")->Get().lastAreaTrigger = triggerId;

return true;
}

bool AreaTriggerAction::Execute(Event event)
bool AreaTriggerAction::Execute(Event /*event*/)
{
LastMovement& movement = context->GetValue<LastMovement&>("last area trigger")->Get();

Expand All @@ -76,6 +77,6 @@ bool AreaTriggerAction::Execute(Event event)
p.rpos(0);
bot->GetSession()->HandleAreaTriggerOpcode(p);

botAI->TellMaster("Hello");
botAI->GetServices().GetChatService().TellMaster("Hello");
return true;
}
24 changes: 13 additions & 11 deletions src/Ai/Base/Actions/AttackAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/

#include "AttackAction.h"
#include "BotChatService.h"
#include "BotRoleService.h"

#include "CreatureAI.h"
#include "Event.h"
Expand Down Expand Up @@ -37,7 +39,7 @@ bool AttackMyTargetAction::Execute(Event /*event*/)
if (!guid)
{
if (verbose)
botAI->TellError("You have no target");
botAI->GetServices().GetChatService().TellError("You have no target");

return false;
}
Expand All @@ -53,7 +55,7 @@ bool AttackMyTargetAction::Execute(Event /*event*/)
bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
{
Unit* oldTarget = context->GetValue<Unit*>("current target")->Get();
bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot);
bool shouldMelee = bot->IsWithinMeleeRange(target) || BotRoleService::IsMeleeStatic(bot);

bool sameTarget = oldTarget == target && bot->GetVictim() == target;
bool inCombat = botAI->GetState() == BOT_STATE_COMBAT;
Expand All @@ -63,23 +65,23 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
bot->HasUnitState(UNIT_STATE_IN_FLIGHT))
{
if (verbose)
botAI->TellError("I cannot attack in flight");
botAI->GetServices().GetChatService().TellError("I cannot attack in flight");

return false;
}

if (!target)
{
if (verbose)
botAI->TellError("I have no target");
botAI->GetServices().GetChatService().TellError("I have no target");

return false;
}

if (!target->IsInWorld())
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is no longer in the world.");
botAI->GetServices().GetChatService().TellError(std::string(target->GetName()) + " is no longer in the world.");

return false;
}
Expand All @@ -91,47 +93,47 @@ bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
sPlayerbotAIConfig->IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
{
if (verbose)
botAI->TellError("I cannot attack other players in PvP prohibited areas.");
botAI->GetServices().GetChatService().TellError("I cannot attack other players in PvP prohibited areas.");

return false;
}

if (bot->IsFriendlyTo(target))
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is friendly to me.");
botAI->GetServices().GetChatService().TellError(std::string(target->GetName()) + " is friendly to me.");

return false;
}

if (target->isDead())
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is dead.");
botAI->GetServices().GetChatService().TellError(std::string(target->GetName()) + " is dead.");

return false;
}

if (!bot->IsWithinLOSInMap(target))
{
if (verbose)
botAI->TellError(std::string(target->GetName()) + " is not in my sight.");
botAI->GetServices().GetChatService().TellError(std::string(target->GetName()) + " is not in my sight.");

return false;
}

if (sameTarget && inCombat && sameAttackMode)
{
if (verbose)
botAI->TellError("I am already attacking " + std::string(target->GetName()) + ".");
botAI->GetServices().GetChatService().TellError("I am already attacking " + std::string(target->GetName()) + ".");

return false;
}

if (!bot->IsValidAttackTarget(target))
{
if (verbose)
botAI->TellError("I cannot attack an invalid target.");
botAI->GetServices().GetChatService().TellError("I cannot attack an invalid target.");

return false;
}
Expand Down
Loading