From fbf8ed925699c2eff990ce24cfb2de701e291fb9 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 26 Aug 2025 21:21:22 -0500 Subject: [PATCH 1/3] Add chat filters for aura, aggro, and spec and make all filters case insensitive --- src/ChatFilter.cpp | 353 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 325 insertions(+), 28 deletions(-) diff --git a/src/ChatFilter.cpp b/src/ChatFilter.cpp index b07b0848..0b3e7f7a 100644 --- a/src/ChatFilter.cpp +++ b/src/ChatFilter.cpp @@ -8,6 +8,17 @@ #include "Group.h" #include "Playerbots.h" #include "RtiTargetValue.h" +#include "AiFactory.h" + +#include +#include + +static std::string ToLower(const std::string& str) +{ + std::string out = str; + std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c){ return std::tolower(c); }); + return out; +} std::string const ChatFilter::Filter(std::string& message) { @@ -25,32 +36,33 @@ public: std::string const Filter(std::string& message) override { Player* bot = botAI->GetBot(); + std::string msgLower = ToLower(message); - bool tank = message.find("@tank") == 0; + bool tank = msgLower.find("@tank") == 0; if (tank && !botAI->IsTank(bot)) return ""; - bool dps = message.find("@dps") == 0; + bool dps = msgLower.find("@dps") == 0; if (dps && (botAI->IsTank(bot) || botAI->IsHeal(bot))) return ""; - bool heal = message.find("@heal") == 0; + bool heal = msgLower.find("@heal") == 0; if (heal && !botAI->IsHeal(bot)) return ""; - bool ranged = message.find("@ranged") == 0; + bool ranged = msgLower.find("@ranged") == 0; if (ranged && !botAI->IsRanged(bot)) return ""; - bool melee = message.find("@melee") == 0; + bool melee = msgLower.find("@melee") == 0; if (melee && botAI->IsRanged(bot)) return ""; - bool rangeddps = message.find("@rangeddps") == 0; + bool rangeddps = msgLower.find("@rangeddps") == 0; if (rangeddps && (!botAI->IsRanged(bot) || botAI->IsTank(bot) || botAI->IsHeal(bot))) return ""; - bool meleedps = message.find("@meleedps") == 0; + bool meleedps = msgLower.find("@meleedps") == 0; if (meleedps && (!botAI->IsMelee(bot) || botAI->IsTank(bot) || botAI->IsHeal(bot))) return ""; @@ -69,11 +81,11 @@ public: std::string const Filter(std::string& message) override { Player* bot = botAI->GetBot(); - - if (message[0] != '@') + std::string msgLower = ToLower(message); + if (msgLower[0] != '@') return message; - if (message.find("-") != std::string::npos) + if (msgLower.find("-") != std::string::npos) { uint32 fromLevel = atoi(message.substr(message.find("@") + 1, message.find("-")).c_str()); uint32 toLevel = atoi(message.substr(message.find("-") + 1, message.find(" ")).c_str()); @@ -100,9 +112,10 @@ public: std::string const Filter(std::string& message) override { Player* bot = botAI->GetBot(); + std::string msgLower = ToLower(message); - bool melee = message.find("@melee") == 0; - bool ranged = message.find("@ranged") == 0; + bool melee = msgLower.find("@melee") == 0; + bool ranged = msgLower.find("@ranged") == 0; if (!melee && !ranged) return message; @@ -159,17 +172,17 @@ public: std::string const Filter(std::string& message) override { Player* bot = botAI->GetBot(); + std::string msgLower = ToLower(message); Group* group = bot->GetGroup(); if (!group) return message; bool found = false; - //bool isRti = false; //not used, shadowed by the next declaration, line marked for removal. for (std::vector::iterator i = rtis.begin(); i != rtis.end(); i++) { std::string const rti = *i; - - bool isRti = message.find(rti) == 0; + std::string rtiLower = ToLower(rti); + bool isRti = msgLower.find(rtiLower) == 0; if (!isRti) continue; @@ -204,7 +217,7 @@ class ClassChatFilter : public ChatFilter public: ClassChatFilter(PlayerbotAI* botAI) : ChatFilter(botAI) { - classNames["@death_knight"] = CLASS_DEATH_KNIGHT; + classNames["@dk"] = CLASS_DEATH_KNIGHT; classNames["@druid"] = CLASS_DRUID; classNames["@hunter"] = CLASS_HUNTER; classNames["@mage"] = CLASS_MAGE; @@ -219,12 +232,12 @@ public: std::string const Filter(std::string& message) override { Player* bot = botAI->GetBot(); + std::string msgLower = ToLower(message); bool found = false; - //bool isClass = false; //not used, shadowed by the next declaration, line marked for removal. for (std::map::iterator i = classNames.begin(); i != classNames.end(); i++) { - bool isClass = message.find(i->first) == 0; + bool isClass = msgLower.find(ToLower(i->first)) == 0; if (isClass && bot->getClass() != i->second) return ""; @@ -251,8 +264,8 @@ public: std::string const Filter(std::string& message) override { Player* bot = botAI->GetBot(); - - if (message.find("@group") == 0) + std::string msgLower = ToLower(message); + if (msgLower.find("@group") == 0) { size_t spacePos = message.find(" "); if (spacePos == std::string::npos) @@ -295,6 +308,290 @@ public: } }; +class SpecChatFilter : public ChatFilter +{ +public: + SpecChatFilter(PlayerbotAI* botAI) : ChatFilter(botAI) + { + // Map (class, specTab) to spec+class string + specTabNames[{CLASS_PALADIN, 0}] = "hpal"; + specTabNames[{CLASS_PALADIN, 1}] = "ppal"; + specTabNames[{CLASS_PALADIN, 2}] = "rpal"; + specTabNames[{CLASS_PRIEST, 0}] = "disc"; + specTabNames[{CLASS_PRIEST, 1}] = "hpr"; + specTabNames[{CLASS_PRIEST, 2}] = "spr"; + specTabNames[{CLASS_MAGE, 0}] = "arc"; + specTabNames[{CLASS_MAGE, 1}] = "frost"; + specTabNames[{CLASS_MAGE, 2}] = "fire"; + specTabNames[{CLASS_WARRIOR, 0}] = "arms"; + specTabNames[{CLASS_WARRIOR, 1}] = "fury"; + specTabNames[{CLASS_WARRIOR, 2}] = "pwar"; + specTabNames[{CLASS_WARLOCK, 0}] = "affl"; + specTabNames[{CLASS_WARLOCK, 1}] = "demo"; + specTabNames[{CLASS_WARLOCK, 2}] = "dest"; + specTabNames[{CLASS_SHAMAN, 0}] = "ele"; + specTabNames[{CLASS_SHAMAN, 1}] = "enh"; + specTabNames[{CLASS_SHAMAN, 2}] = "rsha"; + specTabNames[{CLASS_DRUID, 0}] = "bal"; + // See below for feral druid + specTabNames[{CLASS_DRUID, 2}] = "rdru"; + specTabNames[{CLASS_HUNTER, 0}] = "bmh"; + specTabNames[{CLASS_HUNTER, 1}] = "mmh"; + specTabNames[{CLASS_HUNTER, 2}] = "svh"; + specTabNames[{CLASS_ROGUE, 0}] = "mut"; + specTabNames[{CLASS_ROGUE, 1}] = "comb"; + specTabNames[{CLASS_ROGUE, 2}] = "sub"; + // See below for blood death knight + specTabNames[{CLASS_DEATH_KNIGHT, 1}] = "fdk"; + specTabNames[{CLASS_DEATH_KNIGHT, 2}] = "udk"; + } + + std::string const Filter(std::string& message) override + { + std::string msgLower = ToLower(message); + std::string specPrefix; + std::string rest; + if (!ParseSpecPrefix(message, specPrefix, rest)) + { + return message; + } + + Player* bot = botAI->GetBot(); + if (!MatchesSpec(bot, specPrefix)) + { + return ""; + } + + std::string result = ChatFilter::Filter(rest); + return result; + } + +private: + std::map, std::string> specTabNames; + + bool ParseSpecPrefix(const std::string& message, std::string& specPrefix, std::string& rest) + { + std::string msgLower = ToLower(message); + for (const auto& entry : specTabNames) + { + std::string prefix = "@" + entry.second; + if (msgLower.find(ToLower(prefix)) == 0) + { + specPrefix = entry.second; + size_t spacePos = message.find(' '); + rest = (spacePos != std::string::npos) ? message.substr(spacePos + 1) : ""; + return true; + } + } + return false; + } + + bool MatchesSpec(Player* bot, const std::string& specPrefix) + { + uint8 cls = bot->getClass(); + int specTab = AiFactory::GetPlayerSpecTab(bot); + std::string botSpecClass; + // For druids, specTab==1 is always feral; distinguish bear/cat at runtime by role + if (cls == CLASS_DRUID && specTab == 1) + { + botSpecClass = botAI->IsTank(bot) ? "bear" : "cat"; + } + // For death knights, specTab==0 is always blood; distinguish tank/dps at runtime by role + else if (cls == CLASS_DEATH_KNIGHT && specTab == 0) + { + botSpecClass = botAI->IsTank(bot) ? "bdkt" : "bdkd"; + } + else + { + auto it = specTabNames.find({cls, specTab}); + if (it != specTabNames.end()) + botSpecClass = it->second; + } + return ToLower(botSpecClass) == ToLower(specPrefix); + } +}; + +class AuraChatFilter : public ChatFilter +{ +public: + AuraChatFilter(PlayerbotAI* botAI) : ChatFilter(botAI) {} + + std::string const Filter(std::string& message) override + { + Player* bot = botAI->GetBot(); + std::string msgLower = ToLower(message); + const std::string auraPrefix = "@aura"; + const std::string noAuraPrefix = "@noaura"; + size_t prefixLen = 0; + bool isNoAura = false; + if (msgLower.find(auraPrefix) == 0) + { + prefixLen = auraPrefix.length(); + isNoAura = false; + } + else if (msgLower.find(noAuraPrefix) == 0) + { + prefixLen = noAuraPrefix.length(); + isNoAura = true; + } + else + { + return message; + } + + std::string auraIdOrName = message.substr(prefixLen); + if (auraIdOrName.empty()) + { + return message; + } + + std::string rest = ""; + uint32 auraId = 0; + + size_t spacePos = auraIdOrName.find(' '); + std::string idStr = (spacePos != std::string::npos) ? auraIdOrName.substr(0, spacePos) : auraIdOrName; + if (spacePos != std::string::npos) + { + rest = auraIdOrName.substr(spacePos + 1); + } + else + { + rest = ""; + } + if (!idStr.empty()) + { + bool isNumeric = std::all_of(idStr.begin(), idStr.end(), ::isdigit); + if (isNumeric) + { + auraId = atoi(idStr.c_str()); + } + } + + if (auraId == 0) + { + // Only allow numeric spell IDs + return message; + } + + bool hasAura = bot->HasAura(auraId); + bool match = isNoAura ? !hasAura : hasAura; + std::string result = match ? ChatFilter::Filter(rest) : ""; + return result; + } +}; + +class AggroByChatFilter : public ChatFilter +{ +public: + AggroByChatFilter(PlayerbotAI* botAI) : ChatFilter(botAI) {} + + std::string const Filter(std::string& message) override + { + Player* bot = botAI->GetBot(); + std::string msgLower = ToLower(message); + const std::string prefix = "@aggroby"; + size_t prefixLen = prefix.length(); + if (msgLower.find(prefix) != 0) + { + return message; + } + + std::string enemyStr = message.substr(prefixLen); + if (enemyStr.empty()) + { + return message; + } + + std::string rest = ""; + std::string enemyName = ""; + std::string entryIdStr = ""; + bool isName = false; + uint32 entryId = 0; + + if (enemyStr[0] == '"') + { + size_t endQuote = enemyStr.find('"', 1); + if (endQuote != std::string::npos) + { + enemyName = enemyStr.substr(1, endQuote - 1); + isName = true; + size_t spacePos = enemyStr.find(' ', endQuote + 1); + if (spacePos != std::string::npos) + { + rest = enemyStr.substr(spacePos + 1); + } + else + { + rest = ""; + } + } + else + { + enemyName = enemyStr.substr(1); + isName = true; + rest = ""; + } + } + else + { + size_t spacePos = enemyStr.find(' '); + std::string idOrName = (spacePos != std::string::npos) ? enemyStr.substr(0, spacePos) : enemyStr; + if (spacePos != std::string::npos) + { + rest = enemyStr.substr(spacePos + 1); + } + else + { + rest = ""; + } + if (!idOrName.empty()) + { + bool isNumeric = std::all_of(idOrName.begin(), idOrName.end(), ::isdigit); + if (isNumeric) + { + entryId = atoi(idOrName.c_str()); + } + else + { + enemyName = idOrName; + isName = true; + } + } + } + + // Use nearest hostile npcs value and filter by entryID or name within 100 yards + const float radius = 100.0f; + GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + bool match = false; + for (const auto& guid : npcs) + { + Unit* unit = botAI->GetUnit(guid); + if (!unit) + { + continue; + } + if (!unit->IsCreature()) + { + continue; + } + Creature* c = static_cast(unit); + bool nameMatch = isName && ToLower(c->GetName()) == ToLower(enemyName); + bool idMatch = (entryId != 0) && c->GetEntry() == entryId; + if ((nameMatch || idMatch) && c->GetDistance2d(bot) <= radius) + { + Unit* victim = c->GetVictim(); + if (victim && victim->GetGUID() == bot->GetGUID()) + { + match = true; + break; + } + } + } + std::string result = match ? ChatFilter::Filter(rest) : ""; + return result; + } +}; + CompositeChatFilter::CompositeChatFilter(PlayerbotAI* botAI) : ChatFilter(botAI) { filters.push_back(new StrategyChatFilter(botAI)); @@ -303,6 +600,9 @@ CompositeChatFilter::CompositeChatFilter(PlayerbotAI* botAI) : ChatFilter(botAI) filters.push_back(new CombatTypeChatFilter(botAI)); filters.push_back(new LevelChatFilter(botAI)); filters.push_back(new SubGroupChatFilter(botAI)); + filters.push_back(new SpecChatFilter(botAI)); + filters.push_back(new AuraChatFilter(botAI)); + filters.push_back(new AggroByChatFilter(botAI)); } CompositeChatFilter::~CompositeChatFilter() @@ -313,15 +613,12 @@ CompositeChatFilter::~CompositeChatFilter() std::string const CompositeChatFilter::Filter(std::string& message) { - for (uint32 j = 0; j < filters.size(); ++j) + for (auto* filter : filters) { - for (std::vector::iterator i = filters.begin(); i != filters.end(); i++) - { - message = (*i)->Filter(message); - if (message.empty()) - break; - } + message = filter->Filter(message); + if (message.empty()) + break; } - + return message; } From 3717c6133e21c188bdb3343fad4631710e17e790 Mon Sep 17 00:00:00 2001 From: crow Date: Tue, 2 Sep 2025 18:00:33 -0500 Subject: [PATCH 2/3] clean-ups and fixes simplified code fixed bug where neutral creatures were not captured by the aggroby filter trim white spaces so space between filter and message is permitted but not required --- src/ChatFilter.cpp | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/ChatFilter.cpp b/src/ChatFilter.cpp index 0b3e7f7a..1cdab40b 100644 --- a/src/ChatFilter.cpp +++ b/src/ChatFilter.cpp @@ -9,9 +9,11 @@ #include "Playerbots.h" #include "RtiTargetValue.h" #include "AiFactory.h" +#include "Log.h" #include #include +#include static std::string ToLower(const std::string& str) { @@ -439,25 +441,18 @@ public: return message; } + // Trim any leading spaces after @aura or @noaura (can use space between prefix and spell ID if desired, but not required) std::string auraIdOrName = message.substr(prefixLen); + auraIdOrName.erase(0, auraIdOrName.find_first_not_of(' ')); if (auraIdOrName.empty()) { return message; } - std::string rest = ""; uint32 auraId = 0; - size_t spacePos = auraIdOrName.find(' '); std::string idStr = (spacePos != std::string::npos) ? auraIdOrName.substr(0, spacePos) : auraIdOrName; - if (spacePos != std::string::npos) - { - rest = auraIdOrName.substr(spacePos + 1); - } - else - { - rest = ""; - } + std::string rest = (spacePos != std::string::npos) ? auraIdOrName.substr(spacePos + 1) : ""; if (!idStr.empty()) { bool isNumeric = std::all_of(idStr.begin(), idStr.end(), ::isdigit); @@ -468,10 +463,7 @@ public: } if (auraId == 0) - { - // Only allow numeric spell IDs return message; - } bool hasAura = bot->HasAura(auraId); bool match = isNoAura ? !hasAura : hasAura; @@ -496,15 +488,17 @@ public: return message; } + // Trim any leading spaces after @aggroby (can use space between prefix and entry ID/creature name if desired, but not required) std::string enemyStr = message.substr(prefixLen); + enemyStr.erase(0, enemyStr.find_first_not_of(' ')); if (enemyStr.empty()) { return message; } + // If creature name is more than one word, it must be enclosed in quotes, e.g. @aggroby "Scarlet Commander Mograine" flee std::string rest = ""; std::string enemyName = ""; - std::string entryIdStr = ""; bool isName = false; uint32 entryId = 0; @@ -534,11 +528,11 @@ public: } else { - size_t spacePos = enemyStr.find(' '); - std::string idOrName = (spacePos != std::string::npos) ? enemyStr.substr(0, spacePos) : enemyStr; - if (spacePos != std::string::npos) + size_t splitPos = enemyStr.find_first_of(" "); + std::string idOrName = (splitPos != std::string::npos) ? enemyStr.substr(0, splitPos) : enemyStr; + if (splitPos != std::string::npos) { - rest = enemyStr.substr(spacePos + 1); + rest = enemyStr.substr(splitPos + 1); } else { @@ -559,22 +553,17 @@ public: } } - // Use nearest hostile npcs value and filter by entryID or name within 100 yards const float radius = 100.0f; - GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest hostile npcs")->Get(); + GuidVector npcs = botAI->GetAiObjectContext()->GetValue("nearest npcs")->Get(); bool match = false; for (const auto& guid : npcs) { - Unit* unit = botAI->GetUnit(guid); - if (!unit) + Creature* c = botAI->GetCreature(guid); + if (!c) { continue; } - if (!unit->IsCreature()) - { - continue; - } - Creature* c = static_cast(unit); + bool nameMatch = isName && ToLower(c->GetName()) == ToLower(enemyName); bool idMatch = (entryId != 0) && c->GetEntry() == entryId; if ((nameMatch || idMatch) && c->GetDistance2d(bot) <= radius) From 0362db94adeefbd6bcd5a495ba600c9a4989434b Mon Sep 17 00:00:00 2001 From: Crow Date: Wed, 17 Sep 2025 11:09:59 -0500 Subject: [PATCH 3/3] Exclude Log.h Not needed --- src/ChatFilter.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ChatFilter.cpp b/src/ChatFilter.cpp index 1cdab40b..9d091578 100644 --- a/src/ChatFilter.cpp +++ b/src/ChatFilter.cpp @@ -9,7 +9,6 @@ #include "Playerbots.h" #include "RtiTargetValue.h" #include "AiFactory.h" -#include "Log.h" #include #include