[Bots] Remove hardcoded race-class combinations from bots. (#1375)

* [Bots] Remove hardcoded race-class combinations from bots.
- Allows server operators to directly influence via a database table the classes a specific bot race can be.
- Previously this was hardcoded and required a source modification to do.
- Allowed races, classes, and genders have been removed due to redundancy at this point.

* Remove const cast and modify saylink definition.
This commit is contained in:
Alex 2021-06-11 14:30:56 -04:00 committed by GitHub
parent 0461ac7912
commit c3456ebea0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 147 additions and 365 deletions

View File

@ -563,9 +563,6 @@ RULE_BOOL(Bots, BotGroupXP, false, "Determines whether client gets experience fo
RULE_BOOL(Bots, BotLevelsWithOwner, false, "Auto-updates spawned bots as owner levels/de-levels (false is original behavior)")
RULE_INT(Bots, BotCharacterLevel, 0, "If level is greater that value player can spawn bots if BotCharacterLevelEnabled is true")
RULE_INT(Bots, CasterStopMeleeLevel, 13, "Level at which caster bots stop melee attacks")
RULE_INT(Bots, AllowedClasses, 0xFFFFFFFF, "Bitmask of allowed bot classes")
RULE_INT(Bots, AllowedRaces, 0xFFFFFFFF, "Bitmask of allowed bot races")
RULE_INT(Bots, AllowedGenders, 0x3, "Bitmask of allowed bot genders")
RULE_BOOL(Bots, AllowOwnerOptionAltCombat, true, "When option is enabled, bots will use an auto-/shared-aggro combat model")
RULE_BOOL(Bots, AllowOwnerOptionAutoDefend, true, "When option is enabled, bots will defend their owner on enemy aggro")
RULE_REAL(Bots, LeashDistance, 562500.0f, "Distance a bot is allowed to travel from leash owner before being pulled back (squared value)")

View File

@ -37,7 +37,7 @@
#define CURRENT_BINARY_DATABASE_VERSION 9166
#ifdef BOTS
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9027
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 9028
#else
#define CURRENT_BINARY_BOTS_DATABASE_VERSION 0 // must be 0
#endif

View File

@ -26,6 +26,7 @@
9025|2019_08_26_bots_owner_option_spawn_message.sql|SELECT * FROM db_version WHERE bots_version >= 9025|empty|
9026|2019_09_09_bots_owner_options_rework.sql|SHOW COLUMNS FROM `bot_owner_options` LIKE 'option_type'|empty|
9027|2020_03_30_bots_view_update.sql|SELECT * FROM db_version WHERE bots_version >= 9027|empty|
9028|2021_06_04_bot_create_combinations.sql|SHOW TABLES LIKE 'bot_create_combinations'|empty|
# Upgrade conditions:
# This won't be needed after this system is implemented, but it is used database that are not

View File

@ -0,0 +1,34 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for bot_create_combinations
-- ----------------------------
DROP TABLE IF EXISTS `bot_create_combinations`;
CREATE TABLE `bot_create_combinations` (
`race` int UNSIGNED NOT NULL DEFAULT 0,
`classes` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`race`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of bot_create_combinations
-- ----------------------------
INSERT INTO `bot_create_combinations` VALUES (1, 15871); -- Human
INSERT INTO `bot_create_combinations` VALUES (2, 49921); -- Barbarian
INSERT INTO `bot_create_combinations` VALUES (3, 15382); -- Erudite
INSERT INTO `bot_create_combinations` VALUES (4, 425); -- Wood Elf
INSERT INTO `bot_create_combinations` VALUES (5, 14342); -- High Elf
INSERT INTO `bot_create_combinations` VALUES (6, 15635); -- Dark Elf
INSERT INTO `bot_create_combinations` VALUES (7, 429); -- Half Elf
INSERT INTO `bot_create_combinations` VALUES (8, 33031); -- Dwarf
INSERT INTO `bot_create_combinations` VALUES (9, 49681); -- Troll
INSERT INTO `bot_create_combinations` VALUES (10, 49681); -- Ogre
INSERT INTO `bot_create_combinations` VALUES (11, 303); -- Halfling
INSERT INTO `bot_create_combinations` VALUES (12, 15639); -- Gnome
INSERT INTO `bot_create_combinations` VALUES (128, 18001); -- Iksar
INSERT INTO `bot_create_combinations` VALUES (130, 50049); -- Vah Shir
INSERT INTO `bot_create_combinations` VALUES (330, 3863); -- Froglok
INSERT INTO `bot_create_combinations` VALUES (522, 15871); -- Drakkin
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -1701,206 +1701,15 @@ bool Bot::IsValidRaceClassCombo()
return Bot::IsValidRaceClassCombo(GetRace(), GetClass());
}
bool Bot::IsValidRaceClassCombo(uint16 r, uint8 c)
bool Bot::IsValidRaceClassCombo(uint16 bot_race, uint8 bot_class)
{
switch (r) {
case HUMAN:
switch (c) {
case WARRIOR:
case CLERIC:
case PALADIN:
case RANGER:
case SHADOWKNIGHT:
case DRUID:
case MONK:
case BARD:
case ROGUE:
case NECROMANCER:
case WIZARD:
case MAGICIAN:
case ENCHANTER:
return true;
bool is_valid = false;
auto classes = database.botdb.GetRaceClassBitmask(bot_race);
auto bot_class_bitmask = GetPlayerClassBit(bot_class);
if (classes & bot_class_bitmask) {
is_valid = true;
}
break;
case BARBARIAN:
switch (c) {
case WARRIOR:
case ROGUE:
case SHAMAN:
case BEASTLORD:
case BERSERKER:
return true;
}
break;
case ERUDITE:
switch (c) {
case CLERIC:
case PALADIN:
case SHADOWKNIGHT:
case NECROMANCER:
case WIZARD:
case MAGICIAN:
case ENCHANTER:
return true;
}
break;
case WOOD_ELF:
switch (c) {
case WARRIOR:
case RANGER:
case DRUID:
case BARD:
case ROGUE:
return true;
}
break;
case HIGH_ELF:
switch (c) {
case CLERIC:
case PALADIN:
case WIZARD:
case MAGICIAN:
case ENCHANTER:
return true;
}
break;
case DARK_ELF:
switch (c) {
case WARRIOR:
case CLERIC:
case SHADOWKNIGHT:
case ROGUE:
case NECROMANCER:
case WIZARD:
case MAGICIAN:
case ENCHANTER:
return true;
}
break;
case HALF_ELF:
switch (c) {
case WARRIOR:
case PALADIN:
case RANGER:
case DRUID:
case BARD:
case ROGUE:
return true;
}
break;
case DWARF:
switch (c) {
case WARRIOR:
case CLERIC:
case PALADIN:
case ROGUE:
case BERSERKER:
return true;
}
break;
case TROLL:
switch (c) {
case WARRIOR:
case SHADOWKNIGHT:
case SHAMAN:
case BEASTLORD:
case BERSERKER:
return true;
}
break;
case OGRE:
switch (c) {
case WARRIOR:
case SHADOWKNIGHT:
case SHAMAN:
case BEASTLORD:
case BERSERKER:
return true;
}
break;
case HALFLING:
switch (c) {
case WARRIOR:
case CLERIC:
case PALADIN:
case RANGER:
case DRUID:
case ROGUE:
return true;
}
break;
case GNOME:
switch (c) {
case WARRIOR:
case CLERIC:
case PALADIN:
case SHADOWKNIGHT:
case ROGUE:
case NECROMANCER:
case WIZARD:
case MAGICIAN:
case ENCHANTER:
return true;
}
break;
case IKSAR:
switch (c) {
case WARRIOR:
case SHADOWKNIGHT:
case MONK:
case SHAMAN:
case NECROMANCER:
case BEASTLORD:
return true;
}
break;
case VAHSHIR:
switch (c) {
case WARRIOR:
case BARD:
case ROGUE:
case SHAMAN:
case BEASTLORD:
case BERSERKER:
return true;
}
break;
case FROGLOK:
switch (c) {
case WARRIOR:
case CLERIC:
case PALADIN:
case SHADOWKNIGHT:
case ROGUE:
case SHAMAN:
case NECROMANCER:
case WIZARD:
return true;
}
break;
case DRAKKIN:
switch (c) {
case WARRIOR:
case CLERIC:
case PALADIN:
case RANGER:
case SHADOWKNIGHT:
case DRUID:
case MONK:
case BARD:
case ROGUE:
case NECROMANCER:
case WIZARD:
case MAGICIAN:
case ENCHANTER:
return true;
}
break;
default:
break;
}
return false;
return is_valid;
}
bool Bot::IsValidName()
@ -4264,124 +4073,6 @@ void Bot::LevelBotWithClient(Client* client, uint8 level, bool sendlvlapp) {
}
}
std::string Bot::ClassIdToString(uint16 classId) {
std::string Result;
if(classId > 0 && classId < 17) {
switch(classId) {
case 1:
Result = std::string("Warrior");
break;
case 2:
Result = std::string("Cleric");
break;
case 3:
Result = std::string("Paladin");
break;
case 4:
Result = std::string("Ranger");
break;
case 5:
Result = std::string("Shadowknight");
break;
case 6:
Result = std::string("Druid");
break;
case 7:
Result = std::string("Monk");
break;
case 8:
Result = std::string("Bard");
break;
case 9:
Result = std::string("Rogue");
break;
case 10:
Result = std::string("Shaman");
break;
case 11:
Result = std::string("Necromancer");
break;
case 12:
Result = std::string("Wizard");
break;
case 13:
Result = std::string("Magician");
break;
case 14:
Result = std::string("Enchanter");
break;
case 15:
Result = std::string("Beastlord");
break;
case 16:
Result = std::string("Berserker");
break;
}
}
return Result;
}
std::string Bot::RaceIdToString(uint16 raceId) {
std::string Result;
if(raceId > 0) {
switch(raceId) {
case 1:
Result = std::string("Human");
break;
case 2:
Result = std::string("Barbarian");
break;
case 3:
Result = std::string("Erudite");
break;
case 4:
Result = std::string("Wood Elf");
break;
case 5:
Result = std::string("High Elf");
break;
case 6:
Result = std::string("Dark Elf");
break;
case 7:
Result = std::string("Half Elf");
break;
case 8:
Result = std::string("Dwarf");
break;
case 9:
Result = std::string("Troll");
break;
case 10:
Result = std::string("Ogre");
break;
case 11:
Result = std::string("Halfling");
break;
case 12:
Result = std::string("Gnome");
break;
case 128:
Result = std::string("Iksar");
break;
case 130:
Result = std::string("Vah Shir");
break;
case 330:
Result = std::string("Froglok");
break;
case 522:
Result = std::string("Drakkin");
break;
}
}
return Result;
}
void Bot::SendBotArcheryWearChange(uint8 material_slot, uint32 material, uint32 color) {
EQApplicationPacket* outapp = new EQApplicationPacket(OP_WearChange, sizeof(WearChange_Struct));
WearChange_Struct* wc = (WearChange_Struct*)outapp->pBuffer;

View File

@ -358,8 +358,6 @@ public:
static uint32 SpawnedBotCount(uint32 botOwnerCharacterID);
static void LevelBotWithClient(Client* client, uint8 level, bool sendlvlapp);
//static bool SetBotOwnerCharacterID(uint32 botID, uint32 botOwnerCharacterID, std::string* errorMessage);
static std::string ClassIdToString(uint16 classId);
static std::string RaceIdToString(uint16 raceId);
static bool IsBotAttackAllowed(Mob* attacker, Mob* target, bool& hasRuleDefined);
static Bot* GetBotByBotClientOwnerAndBotName(Client* c, std::string botName);
static void ProcessBotGroupInvite(Client* c, std::string botName);

View File

@ -1119,7 +1119,7 @@ private:
for (bcst_levels::iterator levels_iter = bot_levels.begin(); levels_iter != bot_levels.end(); ++levels_iter) {
if (levels_iter->second < test_iter->second)
test_iter = levels_iter;
if (strcasecmp(Bot::ClassIdToString(levels_iter->first).c_str(), Bot::ClassIdToString(test_iter->first).c_str()) < 0 && levels_iter->second <= test_iter->second)
if (strcasecmp(GetClassIDName(levels_iter->first), GetClassIDName(test_iter->first)) < 0 && levels_iter->second <= test_iter->second)
test_iter = levels_iter;
}
@ -1131,8 +1131,8 @@ private:
else
bot_segment = " or %s(%u)";
required_bots_map[type_index].append(StringFormat(bot_segment.c_str(), Bot::ClassIdToString(test_iter->first).c_str(), test_iter->second));
required_bots_map_by_class[type_index][test_iter->first] = StringFormat("%s(%u)", Bot::ClassIdToString(test_iter->first).c_str(), test_iter->second);
required_bots_map[type_index].append(StringFormat(bot_segment.c_str(), GetClassIDName(test_iter->first), test_iter->second));
required_bots_map_by_class[type_index][test_iter->first] = StringFormat("%s(%u)", GetClassIDName(test_iter->first), test_iter->second);
bot_levels.erase(test_iter);
}
}
@ -1428,6 +1428,7 @@ int bot_command_init(void)
bot_command_add("suspend", "Suspends a bot's AI processing until released", 0, bot_command_suspend) ||
bot_command_add("taunt", "Toggles taunt use by a bot", 0, bot_command_taunt) ||
bot_command_add("track", "Orders a capable bot to track enemies", 0, bot_command_track) ||
bot_command_add("viewcombos", "Views bot race class combinations", 0, bot_command_view_combos) ||
bot_command_add("waterbreathing", "Orders a bot to cast a water breathing spell", 0, bot_command_water_breathing)
) {
bot_command_deinit();
@ -5107,6 +5108,68 @@ void bot_subcommand_bot_clone(Client *c, const Seperator *sep)
c->Message(m_action, "Bot '%s' was successfully cloned to bot '%s'", my_bot->GetCleanName(), bot_name.c_str());
}
void bot_command_view_combos(Client *c, const Seperator *sep)
{
const std::string class_substrs[17] = { "",
"%u (WAR)", "%u (CLR)", "%u (PAL)", "%u (RNG)",
"%u (SHD)", "%u (DRU)", "%u (MNK)", "%u (BRD)",
"%u (ROG)", "%u (SHM)", "%u (NEC)", "%u (WIZ)",
"%u (MAG)", "%u (ENC)", "%u (BST)", "%u (BER)"
};
const std::string race_substrs[17] = { "",
"%u (HUM)", "%u (BAR)", "%u (ERU)", "%u (ELF)",
"%u (HIE)", "%u (DEF)", "%u (HEF)", "%u (DWF)",
"%u (TRL)", "%u (OGR)", "%u (HFL)", "%u (GNM)",
"%u (IKS)", "%u (VAH)", "%u (FRG)", "%u (DRK)"
};
const uint16 race_values[17] = { 0,
HUMAN, BARBARIAN, ERUDITE, WOOD_ELF,
HIGH_ELF, DARK_ELF, HALF_ELF, DWARF,
TROLL, OGRE, HALFLING, GNOME,
IKSAR, VAHSHIR, FROGLOK, DRAKKIN
};
if (helper_command_alias_fail(c, "bot_command_view_combos", sep->arg[0], "viewcombos"))
return;
if (helper_is_help_or_usage(sep->arg[1])) {
std::string window_title = "Bot Races";
std::string window_text;
std::string message_separator = " ";
c->Message(m_usage, "Usage: %s [bot_race]", sep->arg[0]);
window_text.append("<c \"#FFFFFF\">Races:<c \"#FFFF\">");
for (int race_id = 0; race_id <= 15; ++race_id) {
window_text.append(message_separator);
window_text.append(StringFormat(race_substrs[race_id + 1].c_str(), race_values[race_id + 1]));
message_separator = ", ";
}
c->SendPopupToClient(window_title.c_str(), window_text.c_str());
return;
}
if (sep->arg[1][0] == '\0' || !sep->IsNumber(1)) {
c->Message(m_fail, "Invalid Race!");
return;
}
uint16 bot_race = atoi(sep->arg[1]);
auto classes_bitmask = database.botdb.GetRaceClassBitmask(bot_race);
auto race_name = GetRaceIDName(bot_race);
std::string window_title = "Bot Classes";
std::string window_text;
std::string message_separator = " ";
c->Message(m_usage, "%s can be these classes.", race_name);
window_text.append("<c \"#FFFFFF\">Classes:<c \"#FFFF\">");
for (int class_id = 0; class_id <= 15; ++class_id) {
if (classes_bitmask & GetPlayerClassBit(class_id)) {
window_text.append(message_separator);
window_text.append(StringFormat(class_substrs[class_id].c_str(), class_id));
message_separator = ", ";
}
}
c->SendPopupToClient(window_title.c_str(), window_text.c_str());
return;
}
void bot_subcommand_bot_create(Client *c, const Seperator *sep)
{
const std::string class_substrs[17] = { "",
@ -5148,10 +5211,7 @@ void bot_subcommand_bot_create(Client *c, const Seperator *sep)
message_separator = " ";
object_count = 1;
for (int i = 0; i <= 15; ++i) {
if (((1 << i) & RuleI(Bots, AllowedClasses)) == 0)
continue;
window_text.append(const_cast<const std::string&>(message_separator));
window_text.append(message_separator);
if (object_count >= object_max) {
window_text.append("<br>");
object_count = 0;
@ -5166,10 +5226,7 @@ void bot_subcommand_bot_create(Client *c, const Seperator *sep)
message_separator = " ";
object_count = 1;
for (int i = 0; i <= 15; ++i) {
if (((1 << i) & RuleI(Bots, AllowedRaces)) == 0)
continue;
window_text.append(const_cast<const std::string&>(message_separator));
window_text.append(message_separator);
if (object_count >= object_max) {
window_text.append("<br>");
object_count = 0;
@ -5183,12 +5240,8 @@ void bot_subcommand_bot_create(Client *c, const Seperator *sep)
window_text.append("<c \"#FFFFFF\">Genders:<c \"#FFFF\">");
message_separator = " ";
for (int i = 0; i <= 1; ++i) {
if (((1 << i) & RuleI(Bots, AllowedGenders)) == 0)
continue;
window_text.append(const_cast<const std::string&>(message_separator));
window_text.append(message_separator);
window_text.append(StringFormat(gender_substrs[i].c_str(), i));
message_separator = ", ";
}
@ -5802,9 +5855,9 @@ void bot_subcommand_bot_list(Client *c, const Seperator *sep)
c->Message(Chat::White, "[%s] is a level %u %s %s %s who is owned by %s",
((c->CharacterID() == bots_iter.Owner_ID) && (!botCheckNotOnline) ? (EQ::SayLinkEngine::GenerateQuestSaylink(botspawn_saylink, false, bots_iter.Name).c_str()) : (bots_iter.Name)),
bots_iter.Level,
Bot::RaceIdToString(bots_iter.Race).c_str(),
GetRaceIDName(bots_iter.Race),
((bots_iter.Gender == FEMALE) ? ("Female") : ((bots_iter.Gender == MALE) ? ("Male") : ("Neuter"))),
Bot::ClassIdToString(bots_iter.Class).c_str(),
GetClassIDName(bots_iter.Class),
bots_iter.Owner
);
if (c->CharacterID() == bots_iter.Owner_ID) { ++bots_owned; }
@ -5977,7 +6030,7 @@ void bot_subcommand_bot_report(Client *c, const Seperator *sep)
if (!bot_iter)
continue;
std::string report_msg = StringFormat("%s %s reports", Bot::ClassIdToString(bot_iter->GetClass()).c_str(), bot_iter->GetCleanName());
std::string report_msg = StringFormat("%s %s reports", GetClassIDName(bot_iter->GetClass()), bot_iter->GetCleanName());
report_msg.append(StringFormat(": %3.1f%% health", bot_iter->GetHPRatio()));
if (!IsNonSpellFighterClass(bot_iter->GetClass()))
report_msg.append(StringFormat(": %3.1f%% mana", bot_iter->GetManaRatio()));
@ -8672,33 +8725,25 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas
return bot_id;
}
auto class_bit = GetPlayerClassBit(bot_class);
if ((class_bit & RuleI(Bots, AllowedClasses)) == PLAYER_CLASS_UNKNOWN_BIT) {
bot_owner->Message(m_fail, "Class '%s' bots are not allowed on this server", GetPlayerClassName(bot_class));
return bot_id;
}
auto race_bit = GetPlayerRaceBit(bot_race);
if ((race_bit & RuleI(Bots, AllowedRaces)) == PLAYER_RACE_UNKNOWN_BIT) {
bot_owner->Message(m_fail, "Race '%s' bots are not allowed on this server", GetPlayerRaceName(bot_class));
return bot_id;
}
if (!Bot::IsValidRaceClassCombo(bot_race, bot_class)) {
bot_owner->Message(m_fail, "'%s'(%u):'%s'(%u) is an invalid race-class combination",
Bot::RaceIdToString(bot_race).c_str(), bot_race, Bot::ClassIdToString(bot_class).c_str(), bot_class);
const char* bot_race_name = GetRaceIDName(bot_race);
const char* bot_class_name = GetClassIDName(bot_class);
std::string view_saylink = EQ::SayLinkEngine::GenerateQuestSaylink(fmt::format("^viewcombos {}", bot_race), false, "view");
bot_owner->Message(
m_fail,
fmt::format(
"{} {} is an invalid race-class combination, would you like to {} proper combinations for {}?",
bot_race_name,
bot_class_name,
view_saylink,
bot_race_name
).c_str()
);
return bot_id;
}
if (bot_gender > FEMALE || (((1 << bot_gender) & RuleI(Bots, AllowedGenders)) == 0)) {
if (RuleI(Bots, AllowedGenders) == 3)
bot_owner->Message(m_fail, "gender: %u(M), %u(F)", MALE, FEMALE);
else if (RuleI(Bots, AllowedGenders) == 2)
bot_owner->Message(m_fail, "gender: %u(F)", FEMALE);
else if (RuleI(Bots, AllowedGenders) == 1)
bot_owner->Message(m_fail, "gender: %u(M)", MALE);
else
bot_owner->Message(m_fail, "gender: ERROR - No valid genders exist");
if (bot_gender > FEMALE) {
bot_owner->Message(m_fail, "gender: %u (M), %u (F)", MALE, FEMALE);
return bot_id;
}
@ -8710,7 +8755,7 @@ uint32 helper_bot_create(Client *bot_owner, std::string bot_name, uint8 bot_clas
return bot_id;
}
if (bot_count >= max_bot_count) {
bot_owner->Message(m_fail, "You have reached the maximum limit of %i bots", max_bot_count);
bot_owner->Message(m_fail, "You have reached the maximum limit of %i bots.", max_bot_count);
return bot_id;
}

View File

@ -593,6 +593,7 @@ void bot_command_summon_corpse(Client *c, const Seperator *sep);
void bot_command_suspend(Client *c, const Seperator *sep);
void bot_command_taunt(Client *c, const Seperator *sep);
void bot_command_track(Client *c, const Seperator *sep);
void bot_command_view_combos(Client *c, const Seperator *sep);
void bot_command_water_breathing(Client *c, const Seperator *sep);

View File

@ -2952,6 +2952,20 @@ uint8 BotDatabase::GetSpellCastingChance(uint8 spell_type_index, uint8 class_ind
return Bot::spell_casting_chances[spell_type_index][class_index][stance_index][conditional_index];
}
uint16 BotDatabase::GetRaceClassBitmask(uint16 bot_race)
{
std::string query = fmt::format(
"SELECT `classes` FROM `bot_create_combinations` WHERE `race` = {}",
bot_race
);
auto results = database.QueryDatabase(query);
uint16 classes = 0;
if (results.RowCount() == 1) {
auto row = results.begin();
classes = atoi(row[0]);
}
return classes;
}
/* fail::Bot functions */
const char* BotDatabase::fail::QueryNameAvailablity() { return "Failed to query name availability"; }

View File

@ -186,6 +186,7 @@ public:
/* Bot miscellaneous functions */
uint8 GetSpellCastingChance(uint8 spell_type_index, uint8 class_index, uint8 stance_index, uint8 conditional_index);
uint16 GetRaceClassBitmask(uint16 bot_race);
class fail {
public: