Skip to content

Commit 50867f6

Browse files
authored
Harden Playerbot authorization and random bot provisioning (#478)
* Harden player-owned bot command authorization * Constrain random bot mounts to Classic eligibility * Create only missing random bot classes * Guard unsupported random bot classes * Authorize raid warnings by dispatched command * Guard random bot race policy assertions * Add project headers to Playerbot policy files * Use command-specific Playerbot authorization * Keep master-relative bot tactics owner-only
1 parent 1817ae1 commit 50867f6

13 files changed

Lines changed: 771 additions & 157 deletions

src/modules/Bots/playerbot/PlayerbotAI.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -650,11 +650,6 @@ void PlayerbotAI::Reset()
650650
*/
651651
void PlayerbotAI::HandleCommand(uint32 type, const string& text, Player& fromPlayer)
652652
{
653-
if (!GetSecurity()->CheckLevelFor(PLAYERBOT_SECURITY_INVITE, type != CHAT_MSG_WHISPER, &fromPlayer))
654-
{
655-
return;
656-
}
657-
658653
if (type == CHAT_MSG_ADDON)
659654
{
660655
return;
@@ -682,18 +677,24 @@ void PlayerbotAI::HandleCommand(uint32 type, const string& text, Player& fromPla
682677
// and because a channel line is fanned out to every random bot in the world, one "~who"
683678
// in trade chat returned a whisper from each of them at once -- disclosure and a
684679
// whisper flood from a single message. It now needs TALK, the level the module already
685-
// uses to mean "may hold a conversation with this bot"; everything else still needs
686-
// full control.
687-
PlayerbotSecurityLevel required = (filtered.find("who") == 0)
688-
? PLAYERBOT_SECURITY_TALK
689-
: PLAYERBOT_SECURITY_ALLOW_ALL;
680+
// uses to mean "may hold a conversation with this bot". Follow, stay and attack use the
681+
// same-subgroup tactical tier; everything else still needs full control.
682+
bool const dispatchesRaidWarning =
683+
type == CHAT_MSG_RAID_WARNING &&
684+
filtered.find(bot->GetName()) != string::npos &&
685+
filtered.find("award") == string::npos;
686+
687+
// Keep this check after all prefix/filter normalization. The same routing decision is
688+
// used below so authorization always describes the command that will actually run.
689+
PlayerbotSecurityLevel required =
690+
GetPlayerbotDispatchedCommandSecurityLevel(filtered, dispatchesRaidWarning);
690691

691692
if (!GetSecurity()->CheckLevelFor(required, type != CHAT_MSG_WHISPER, &fromPlayer))
692693
{
693694
return;
694695
}
695696

696-
if (type == CHAT_MSG_RAID_WARNING && filtered.find(bot->GetName()) != string::npos && filtered.find("award") == string::npos)
697+
if (dispatchesRaidWarning)
697698
{
698699
ChatCommandHolder cmd("warning", &fromPlayer, type);
699700
chatCommands.push(cmd);

src/modules/Bots/playerbot/PlayerbotAIConfig.cpp

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "PlayerbotAIConfig.h"
66
#include "playerbot.h"
77
#include "RandomPlayerbotFactory.h"
8+
#include "RandomBotClassPolicy.h"
89
#include "AccountMgr.h"
910
#include "SystemConfig.h"
1011

@@ -582,35 +583,36 @@ void PlayerbotAIConfig::CreateRandomBots()
582583

583584
randomBotAccounts.push_back(accountId);
584585

585-
// Count the classes this loop will actually create rather than assuming ten. It
586-
// creates one per playable class, and vanilla has nine -- 6 and 10 do not exist
587-
// here -- so an account that was already full at nine looked incomplete against a
588-
// threshold of ten and got a second full set. Every restart without a wipe
589-
// therefore doubled the roster, 450 characters to 900 at eighteen per account,
590-
// until the count finally cleared ten. CreateRandomBot bypasses the normal
591-
// per-account character limit, so nothing else stopped it.
592-
uint32 expectedChars = 0;
593-
for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
586+
vector<uint8> existingClasses;
587+
QueryResult* characters = CharacterDatabase.PQuery(
588+
"SELECT `class` FROM `characters` WHERE `account` = '%u' UNION ALL SELECT 0",
589+
accountId);
590+
if (!characters)
594591
{
595-
if (cls != 10 && cls != 6)
596-
{
597-
++expectedChars;
598-
}
592+
sLog.outError("Unable to inspect random bot classes for account %u", accountId);
593+
totalRandomBotChars += sAccountMgr.GetCharactersCount(accountId);
594+
continue;
599595
}
600596

601-
int count = sAccountMgr.GetCharactersCount(accountId);
602-
if (count >= (int)expectedChars)
597+
do
603598
{
604-
totalRandomBotChars += count;
605-
continue;
606-
}
599+
existingClasses.push_back(characters->Fetch()[0].GetUInt8());
600+
} while (characters->NextRow());
601+
602+
delete characters;
607603

608-
RandomPlayerbotFactory factory(accountId);
609-
for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
604+
vector<uint8> missingClasses = ai::GetMissingRandomBotClasses(existingClasses);
605+
if (!missingClasses.empty())
610606
{
611-
if (cls != 10 && cls != 6)
607+
RandomPlayerbotFactory factory(accountId);
608+
for (vector<uint8>::const_iterator itr = missingClasses.begin();
609+
itr != missingClasses.end(); ++itr)
612610
{
613-
factory.CreateRandomBot(cls);
611+
if (!factory.CreateRandomBot(*itr))
612+
{
613+
sLog.outError("Unable to create missing random bot class %u for account %u",
614+
*itr, accountId);
615+
}
614616
}
615617
}
616618

src/modules/Bots/playerbot/PlayerbotFactory.cpp

Lines changed: 13 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "SQLStorages.h"
88
#include "ItemPrototype.h"
99
#include "PlayerbotAIConfig.h"
10+
#include "PlayerbotMountPolicy.h"
1011
#include "AccountMgr.h"
1112
#include "DBCStore.h"
1213
#include "SharedDefines.h"
@@ -2111,96 +2112,29 @@ void PlayerbotFactory::InitAmmo()
21112112
*/
21122113
void PlayerbotFactory::InitMounts()
21132114
{
2114-
// This used to group every SPELL_AURA_MOUNTED spell by speed and then learn one from
2115-
// EVERY group, twice over, with no check of level, class, race or riding skill. Two
2116-
// consequences, both confirmed against the live character_spell table: all 200 bots
2117-
// knew 3363 Summon Riding Gryphon -- a 499% flyer, the sole member of its speed group,
2118-
// on characters as low as level 1 -- and class mounts leaked across classes, with the
2119-
// Paladin Warhorse and Warlock Felsteed turning up on priests, rogues and druids.
2120-
//
2121-
// Riding skill is the real gate and InitSkills has already set it from the bot's level:
2122-
// 75 buys the 60% mount at 40, 150 buys the 100% mount at 60.
21232115
uint32 riding = bot->GetSkillValue(SKILL_RIDING);
2124-
if (riding < 75)
2125-
{
2126-
return;
2127-
}
2128-
2129-
// Effect base points carry speed-minus-one, so a 60% mount reads 59 and a 100% reads 99.
2130-
// Anything above that is a flying mount, which 1.12 does not have.
2131-
const int32 maxIncrease = (riding >= 150) ? 99 : 59;
2132-
2133-
const uint32 raceMask = bot->getRaceMask();
2134-
const uint32 classMask = bot->getClassMask();
2135-
2136-
map<int32, vector<uint32> > spells;
2137-
2138-
for (uint32 spellId = 0; spellId < sSpellStore.GetNumRows(); ++spellId)
2116+
vector<uint32> candidates = GetPlayerbotMountSpells(bot->getRace(), bot->getClass(), riding);
2117+
vector<uint32> available;
2118+
for (vector<uint32>::const_iterator itr = candidates.begin(); itr != candidates.end(); ++itr)
21392119
{
2140-
SpellEntry const *spellInfo = sSpellStore.LookupEntry(spellId);
2141-
if (!spellInfo || spellInfo->EffectAura[0] != SPELL_AURA_MOUNTED)
2120+
SpellEntry const* spellInfo = sSpellStore.LookupEntry(*itr);
2121+
if (!spellInfo)
21422122
{
21432123
continue;
21442124
}
21452125

2146-
if (GetSpellCastTime(spellInfo) < 500 || GetSpellDuration(spellInfo) != -1)
2126+
int32 speedIncrease = max(spellInfo->EffectBasePoints[1], spellInfo->EffectBasePoints[2]);
2127+
if (IsPlayerbotMountSpellCompatible(
2128+
riding, spellInfo->EffectAura[0] == SPELL_AURA_MOUNTED,
2129+
GetSpellCastTime(spellInfo), GetSpellDuration(spellInfo), speedIncrease))
21472130
{
2148-
continue;
2131+
available.push_back(*itr);
21492132
}
2150-
2151-
int32 effect = max(spellInfo->EffectBasePoints[1], spellInfo->EffectBasePoints[2]);
2152-
if (effect < 50 || effect > maxIncrease)
2153-
{
2154-
continue;
2155-
}
2156-
2157-
// Class mounts carry a SkillLineAbility row that names the class -- Warhorse is
2158-
// classMask 2, Felsteed is 256 -- so where one exists it must be honoured. Racial
2159-
// and vendor mounts have no row at all in 1.12 (Brown Horse 458 and the rest), so
2160-
// an absent row cannot mean "forbidden" or every bot would end up on foot.
2161-
SkillLineAbilityMapBounds bounds = sSpellMgr.GetSkillLineAbilityMapBounds(spellId);
2162-
bool restricted = false;
2163-
bool permitted = false;
2164-
for (SkillLineAbilityMap::const_iterator i = bounds.first; i != bounds.second; ++i)
2165-
{
2166-
SkillLineAbilityEntry const* ability = i->second;
2167-
if (!ability->RaceMask && !ability->ClassMask)
2168-
{
2169-
continue;
2170-
}
2171-
2172-
restricted = true;
2173-
if (ability->RaceMask && !(ability->RaceMask & raceMask))
2174-
{
2175-
continue;
2176-
}
2177-
if (ability->ClassMask && !(ability->ClassMask & classMask))
2178-
{
2179-
continue;
2180-
}
2181-
2182-
permitted = true;
2183-
break;
2184-
}
2185-
2186-
if (restricted && !permitted)
2187-
{
2188-
continue;
2189-
}
2190-
2191-
spells[effect].push_back(spellId);
2192-
}
2193-
2194-
// One mount, at the best speed this bot has the skill for -- not one from every tier.
2195-
if (spells.empty())
2196-
{
2197-
return;
21982133
}
21992134

2200-
vector<uint32>& ids = spells.rbegin()->second;
2201-
if (!ids.empty())
2135+
if (!available.empty())
22022136
{
2203-
bot->learnSpell(ids[urand(0, ids.size() - 1)], false);
2137+
bot->learnSpell(available[urand(0, available.size() - 1)], false);
22042138
}
22052139
}
22062140

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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 "SharedDefines.h"
29+
30+
#include <vector>
31+
32+
namespace ai
33+
{
34+
inline bool IsPlayerbotMountSpellCompatible(uint32 riding, bool isMountAura,
35+
int32 castTime, int32 duration,
36+
int32 speedIncrease)
37+
{
38+
// Classic mount rows cast for at least 500 ms, last indefinitely, and store their
39+
// speed increase minus one in EffectBasePoints: 59 means 60%, 99 means 100%.
40+
if (riding < 75 || !isMountAura || castTime < 500 || duration != -1)
41+
{
42+
return false;
43+
}
44+
return speedIncrease == (riding >= 150 ? 99 : 59);
45+
}
46+
47+
inline void AddPlayerbotRacialMounts(std::vector<uint32>& spells, uint8 race, bool fast)
48+
{
49+
switch (race)
50+
{
51+
case RACE_HUMAN:
52+
if (fast)
53+
{
54+
spells.insert(spells.end(), {23227, 23228, 23229});
55+
}
56+
else
57+
{
58+
spells.insert(spells.end(), {458, 470, 472, 6648});
59+
}
60+
break;
61+
case RACE_ORC:
62+
if (fast)
63+
{
64+
spells.insert(spells.end(), {23250, 23251, 23252});
65+
}
66+
else
67+
{
68+
spells.insert(spells.end(), {580, 6653, 6654});
69+
}
70+
break;
71+
case RACE_DWARF:
72+
if (fast)
73+
{
74+
spells.insert(spells.end(), {23238, 23239, 23240});
75+
}
76+
else
77+
{
78+
spells.insert(spells.end(), {6777, 6898, 6899});
79+
}
80+
break;
81+
case RACE_NIGHTELF:
82+
if (fast)
83+
{
84+
spells.insert(spells.end(), {23219, 23221, 23338});
85+
}
86+
else
87+
{
88+
spells.insert(spells.end(), {8394, 10789, 10793});
89+
}
90+
break;
91+
case RACE_UNDEAD:
92+
if (fast)
93+
{
94+
spells.insert(spells.end(), {17465, 23246});
95+
}
96+
else
97+
{
98+
spells.insert(spells.end(), {17462, 17463, 17464});
99+
}
100+
break;
101+
case RACE_TAUREN:
102+
if (fast)
103+
{
104+
spells.insert(spells.end(), {23247, 23248, 23249});
105+
}
106+
else
107+
{
108+
spells.insert(spells.end(), {18989, 18990});
109+
}
110+
break;
111+
case RACE_TROLL:
112+
if (fast)
113+
{
114+
spells.insert(spells.end(), {23241, 23242, 23243});
115+
}
116+
else
117+
{
118+
spells.insert(spells.end(), {8395, 10796, 10799});
119+
}
120+
break;
121+
case RACE_GNOME:
122+
if (fast)
123+
{
124+
spells.insert(spells.end(), {23222, 23223, 23225});
125+
}
126+
else
127+
{
128+
spells.insert(spells.end(), {10873, 10969, 17453, 17454});
129+
}
130+
break;
131+
}
132+
}
133+
134+
inline void AddPlayerbotClassMount(std::vector<uint32>& spells, uint8 race,
135+
uint8 cls, bool fast)
136+
{
137+
if (cls == CLASS_PALADIN && (race == RACE_HUMAN || race == RACE_DWARF))
138+
{
139+
spells.push_back(fast ? 23214 : 13819);
140+
}
141+
else if (cls == CLASS_WARLOCK &&
142+
(race == RACE_HUMAN || race == RACE_ORC ||
143+
race == RACE_UNDEAD || race == RACE_GNOME))
144+
{
145+
spells.push_back(fast ? 23161 : 5784);
146+
}
147+
}
148+
149+
inline std::vector<uint32> GetPlayerbotMountSpells(uint8 race, uint8 cls, uint32 riding)
150+
{
151+
std::vector<uint32> spells;
152+
if (riding < 75)
153+
{
154+
return spells;
155+
}
156+
157+
bool const fast = riding >= 150;
158+
AddPlayerbotRacialMounts(spells, race, fast);
159+
AddPlayerbotClassMount(spells, race, cls, fast);
160+
return spells;
161+
}
162+
}

0 commit comments

Comments
 (0)