mod-playerbots/src/Bot/PlayerbotMgr.h
Hokken ce1adebc78
fix(Core): scope AddPlayerBot loading count to master account (#2307)
## 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<ObjectGuid>` 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 <name>` 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 <character_name>` 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<ObjectGuid>` to
`unordered_map<ObjectGuid, uint32>` 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<ObjectGuid>` → `unordered_map<ObjectGuid, uint32>` |
| `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 <hermensb@gmail.com>
Co-authored-by: Revision <tkn963@gmail.com>
Co-authored-by: kadeshar <kadeshar@gmail.com>
Co-authored-by: Hokken <Hokken@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 11:26:42 -07:00

130 lines
4.3 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.
*/
#ifndef _PLAYERBOT_PLAYERBOTMGR_H
#define _PLAYERBOT_PLAYERBOTMGR_H
#include "ObjectGuid.h"
#include "Player.h"
#include "PlayerbotAIBase.h"
class ChatHandler;
class PlayerbotAI;
class PlayerbotLoginQueryHolder;
class WorldPacket;
typedef std::map<ObjectGuid, Player*> PlayerBotMap;
typedef std::map<std::string, std::set<std::string> > PlayerBotErrorMap;
class PlayerbotHolder : public PlayerbotAIBase
{
public:
PlayerbotHolder();
virtual ~PlayerbotHolder(){};
void AddPlayerBot(ObjectGuid guid, uint32 masterAccountId);
bool IsAccountLinked(uint32 accountId, uint32 masterAccountId);
void HandlePlayerBotLoginCallback(PlayerbotLoginQueryHolder const& holder);
void LogoutPlayerBot(ObjectGuid guid);
void DisablePlayerBot(ObjectGuid guid);
void RemoveFromPlayerbotsMap(ObjectGuid guid);
Player* GetPlayerBot(ObjectGuid guid) const;
Player* GetPlayerBot(ObjectGuid::LowType lowGuid) const;
PlayerBotMap::const_iterator GetPlayerBotsBegin() const { return playerBots.begin(); }
PlayerBotMap::const_iterator GetPlayerBotsEnd() const { return playerBots.end(); }
void UpdateAIInternal([[maybe_unused]] uint32 elapsed, [[maybe_unused]] bool minimal = false) override{};
void UpdateSessions();
void HandleBotPackets(WorldSession* session);
void LogoutAllBots();
void OnBotLogin(Player* const bot);
std::vector<std::string> HandlePlayerbotCommand(char const* args, Player* master = nullptr);
std::string const ProcessBotCommand(std::string const cmd, ObjectGuid guid, ObjectGuid masterguid, bool admin,
uint32 masterAccountId, uint32 masterGuildId);
uint32 GetAccountId(std::string const name);
uint32 GetAccountId(ObjectGuid guid);
std::string const ListBots(Player* master);
std::string const LookupBots(Player* master);
uint32 GetPlayerbotsCount() { return playerBots.size(); }
uint32 GetPlayerbotsCountByClass(uint32 cls);
protected:
virtual void OnBotLoginInternal(Player* const bot) = 0;
PlayerBotMap playerBots;
static std::unordered_map<ObjectGuid, uint32> botLoading;
};
class PlayerbotMgr : public PlayerbotHolder
{
public:
PlayerbotMgr(Player* const master);
virtual ~PlayerbotMgr();
static bool HandlePlayerbotMgrCommand(ChatHandler* handler, char const* args);
void HandleMasterIncomingPacket(WorldPacket const& packet);
void HandleMasterOutgoingPacket(WorldPacket const& packet);
void HandleCommand(uint32 type, std::string const text);
void OnPlayerLogin(Player* player);
void CancelLogout();
void UpdateAIInternal(uint32 elapsed, bool minimal = false) override;
void TellError(std::string const botName, std::string const text);
Player* GetMaster() const { return master; };
void SaveToDB();
void HandleSetSecurityKeyCommand(Player* player, const std::string& key);
void HandleLinkAccountCommand(Player* player, const std::string& accountName, const std::string& key);
void HandleViewLinkedAccountsCommand(Player* player);
void HandleUnlinkAccountCommand(Player* player, const std::string& accountName);
protected:
void OnBotLoginInternal(Player* const bot) override;
void CheckTellErrors(uint32 elapsed);
private:
Player* const master;
PlayerBotErrorMap errors;
time_t lastErrorTell;
};
class PlayerbotsMgr
{
public:
static PlayerbotsMgr& instance()
{
static PlayerbotsMgr instance;
return instance;
}
void AddPlayerbotData(Player* player, bool isBotAI);
void RemovePlayerBotData(ObjectGuid const& guid, bool is_AI);
PlayerbotAI* GetPlayerbotAI(Player* player);
PlayerbotMgr* GetPlayerbotMgr(Player* player);
private:
PlayerbotsMgr() = default;
~PlayerbotsMgr() = default;
PlayerbotsMgr(const PlayerbotsMgr&) = delete;
PlayerbotsMgr& operator=(const PlayerbotsMgr&) = delete;
PlayerbotsMgr(PlayerbotsMgr&&) = delete;
PlayerbotsMgr& operator=(PlayerbotsMgr&&) = delete;
std::unordered_map<ObjectGuid, PlayerbotAIBase*> _playerbotsAIMap;
std::unordered_map<ObjectGuid, PlayerbotAIBase*> _playerbotsMgrMap;
};
#define sPlayerbotsMgr PlayerbotsMgr::instance()
#endif