From ce1adebc789d8006a9a4535b15c96bf6f43a9802 Mon Sep 17 00:00:00 2001 From: Hokken Date: Fri, 17 Apr 2026 19:26:42 +0100 Subject: [PATCH] fix(Core): scope AddPlayerBot loading count to master account (#2307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem `AddPlayerBot()` falsely rejects player bot additions with *"You have added too many bots (more than 40)"* even when the player has zero personal bots. This happens because the `MaxAddedBots` check at `PlayerbotMgr.cpp:124` adds `botLoading.size()` to the player's personal bot count: ```cpp uint32 count = mgr->GetPlayerbotsCount() + botLoading.size(); ``` `botLoading` is a `static std::unordered_set` on `PlayerbotHolder` — shared by both `PlayerbotMgr` (per-player) and `RandomPlayerbotMgr` (singleton). When `RandomPlayerbotMgr` loads random bots at startup (up to 60 per interval via `RandomBotsPerInterval`), their GUIDs go into the same global set. During the startup loading window, `botLoading.size()` can easily reach 100–300, far exceeding the default `MaxAddedBots = 40` limit. The result: any player who logs in during the random bot loading window and tries `.playerbot add ` gets blocked, even though the limit is intended to be per-player. ### How to reproduce 1. Set `AiPlayerbot.RandomBotAutologin = 1` (default) with 500 random bots 2. Start the server 3. Log in immediately while random bots are still loading 4. Run `.playerbot add ` for an offline character on your account 5. Get *"You have added too many bots (more than 40)"* despite having 0 personal bots 6. Wait 1–2 minutes for random bot loading to finish, try again — works ### Root cause - `PlayerbotHolder::botLoading` is declared `static` at `PlayerbotMgr.h:60`, so both `PlayerbotMgr` and `RandomPlayerbotMgr` share the same set - `AddPlayerBot()` inserts into `botLoading` at line 147 for ALL callers — both player-initiated adds (`masterAccountId > 0`) and random bot spawns (`masterAccountId = 0`) - The count check at line 124 uses `botLoading.size()` (the entire global set) instead of filtering to bots being loaded for the requesting player - The config comment confirms the intended scope: *"The maximum number of bots that a player can control simultaneously"* ## Fix Change `botLoading` from `unordered_set` to `unordered_map` where the value is the `masterAccountId` passed to `AddPlayerBot()`. Random bots are loaded with `masterAccountId = 0`. The count check now iterates the map and only counts entries matching the current player's `masterAccountId`: ```cpp uint32 loadingForMaster = 0; for (auto const& [guid, acctId] : botLoading) { if (acctId == masterAccountId) ++loadingForMaster; } uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster; ``` ### Callsite compatibility All 10 existing `botLoading` callsites were audited: | Callsite | Operation | Compatible | |----------|-----------|-----------| | `PlayerbotMgr.cpp:85` | `find()` by key | Yes | | `PlayerbotMgr.cpp:153` | `emplace()` (was `insert()`) | Changed | | `PlayerbotMgr.cpp:174` | `erase()` by key | Yes | | `PlayerbotMgr.cpp:209` | `erase()` by key | Yes | | `PlayerbotMgr.cpp:229` | `erase()` by key | Yes | | `PlayerbotMgr.cpp:1163` | `find()` by key | Yes | | `RandomPlayerbotMgr.cpp:429` | `empty()` | Yes | The six unchanged callsites use `find()`, `erase()`, and `empty()` which operate on keys identically for both `unordered_set` and `unordered_map`. ## Files changed | File | Change | |------|--------| | `src/Bot/PlayerbotMgr.h` | `botLoading` type: `unordered_set` → `unordered_map` | | `src/Bot/PlayerbotMgr.cpp` | Definition type updated, `insert` → `emplace` with `masterAccountId`, count check filters by `masterAccountId` | ## What is NOT changed - `MaxAddedBots` config key and default value (40) — unchanged - Random bot loading behavior — unchanged - The `botLoading.empty()` throttle in `RandomPlayerbotMgr` — unchanged - In-game group invite flow — unaffected (does not go through `AddPlayerBot`) - No new config keys, no schema changes, no API changes --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar Co-authored-by: Hokken Co-authored-by: Claude Opus 4.6 (1M context) --- src/Bot/PlayerbotMgr.cpp | 12 +++++++++--- src/Bot/PlayerbotMgr.h | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Bot/PlayerbotMgr.cpp b/src/Bot/PlayerbotMgr.cpp index c3b614a98..8327e2f36 100644 --- a/src/Bot/PlayerbotMgr.cpp +++ b/src/Bot/PlayerbotMgr.cpp @@ -64,7 +64,7 @@ private: }; std::unordered_set BotInitGuard::botsBeingInitialized; -std::unordered_set PlayerbotHolder::botLoading; +std::unordered_map PlayerbotHolder::botLoading; PlayerbotHolder::PlayerbotHolder() : PlayerbotAIBase(false) {} class PlayerbotLoginQueryHolder : public LoginQueryHolder @@ -121,7 +121,13 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId LOG_DEBUG("playerbots", "PlayerbotMgr not found for master player with GUID: {}", masterPlayer->GetGUID().GetRawValue()); return; } - uint32 count = mgr->GetPlayerbotsCount() + botLoading.size(); + uint32 loadingForMaster = 0; + for (auto const& [guid, acctId] : botLoading) + { + if (acctId == masterAccountId) + ++loadingForMaster; + } + uint32 count = mgr->GetPlayerbotsCount() + loadingForMaster; if (count >= PlayerbotAIConfig::instance().maxAddedBots) { allowed = false; @@ -144,7 +150,7 @@ void PlayerbotHolder::AddPlayerBot(ObjectGuid playerGuid, uint32 masterAccountId return; } - botLoading.insert(playerGuid); + botLoading.emplace(playerGuid, masterAccountId); // Always login in with world session to avoid race condition sWorld->AddQueryHolderCallback(CharacterDatabase.DelayQueryHolder(holder)) diff --git a/src/Bot/PlayerbotMgr.h b/src/Bot/PlayerbotMgr.h index b80f6f236..316e34d47 100644 --- a/src/Bot/PlayerbotMgr.h +++ b/src/Bot/PlayerbotMgr.h @@ -57,7 +57,7 @@ protected: virtual void OnBotLoginInternal(Player* const bot) = 0; PlayerBotMap playerBots; - static std::unordered_set botLoading; + static std::unordered_map botLoading; }; class PlayerbotMgr : public PlayerbotHolder