feat: implement WoW macro conditional evaluator for /cast

Adds evaluateMacroConditionals() which parses the [cond1,cond2] Spell;
[cond3] Spell2; Default syntax and returns the first matching
alternative. Supported conditions:

- mod:shift/ctrl/alt, nomod  — keyboard modifier state
- target=player/focus/target, @player/@focus/@target — target override
- help / harm (noharm / nohelp)  — target faction check
- dead / nodead                  — target health check
- exists / noexists              — target presence check
- combat / nocombat              — player combat state
- noform / nostance / form:0     — shapeshift/stance state
- Unknown conditions are permissive (true) to avoid false negatives.

/cast now resolves conditionals before spell lookup and routes
castSpell() to the [target=X] override GUID when specified.
isHostileFaction() exposed as isHostileFactionPublic() for UI use.
This commit is contained in:
Kelsi 2026-03-18 03:04:45 -07:00
parent ed3bca3d17
commit 30513d0f06
2 changed files with 186 additions and 2 deletions

View file

@ -1888,6 +1888,7 @@ public:
bool isMounted() const { return currentMountDisplayId_ != 0; } bool isMounted() const { return currentMountDisplayId_ != 0; }
bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; } bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
bool isHostileFactionPublic(uint32_t factionTemplateId) const { return isHostileFaction(factionTemplateId); }
float getServerRunSpeed() const { return serverRunSpeed_; } float getServerRunSpeed() const { return serverRunSpeed_; }
float getServerWalkSpeed() const { return serverWalkSpeed_; } float getServerWalkSpeed() const { return serverWalkSpeed_; }
float getServerSwimSpeed() const { return serverSwimSpeed_; } float getServerSwimSpeed() const { return serverSwimSpeed_; }

View file

@ -259,6 +259,9 @@ bool GameScreen::shouldShowMessage(const game::MessageChatData& msg, int tabInde
// Forward declaration — defined near sendChatMessage below // Forward declaration — defined near sendChatMessage below
static std::string firstMacroCommand(const std::string& macroText); static std::string firstMacroCommand(const std::string& macroText);
static std::vector<std::string> allMacroCommands(const std::string& macroText); static std::vector<std::string> allMacroCommands(const std::string& macroText);
static std::string evaluateMacroConditionals(const std::string& rawArg,
game::GameHandler& gameHandler,
uint64_t& targetOverride);
void GameScreen::render(game::GameHandler& gameHandler) { void GameScreen::render(game::GameHandler& gameHandler) {
// Set up chat bubble callback (once) // Set up chat bubble callback (once)
@ -5277,6 +5280,170 @@ static std::vector<std::string> allMacroCommands(const std::string& macroText) {
return cmds; return cmds;
} }
// ---------------------------------------------------------------------------
// WoW macro conditional evaluator
// Parses: [cond1,cond2] Spell1; [cond3] Spell2; DefaultSpell
// Returns the first matching alternative's argument, or "" if none matches.
// targetOverride is set to a specific GUID if [target=X] was in the conditions,
// or left as UINT64_MAX to mean "use the normal target".
// ---------------------------------------------------------------------------
static std::string evaluateMacroConditionals(const std::string& rawArg,
game::GameHandler& gameHandler,
uint64_t& targetOverride) {
targetOverride = static_cast<uint64_t>(-1);
auto& input = core::Input::getInstance();
const bool shiftHeld = input.isKeyPressed(SDL_SCANCODE_LSHIFT) ||
input.isKeyPressed(SDL_SCANCODE_RSHIFT);
const bool ctrlHeld = input.isKeyPressed(SDL_SCANCODE_LCTRL) ||
input.isKeyPressed(SDL_SCANCODE_RCTRL);
const bool altHeld = input.isKeyPressed(SDL_SCANCODE_LALT) ||
input.isKeyPressed(SDL_SCANCODE_RALT);
const bool anyMod = shiftHeld || ctrlHeld || altHeld;
// Split rawArg on ';' → alternatives
std::vector<std::string> alts;
{
std::string cur;
for (char c : rawArg) {
if (c == ';') { alts.push_back(cur); cur.clear(); }
else cur += c;
}
alts.push_back(cur);
}
// Evaluate a single comma-separated condition token.
// tgt is updated if a target= or @ specifier is found.
auto evalCond = [&](const std::string& raw, uint64_t& tgt) -> bool {
std::string c = raw;
// trim
size_t s = c.find_first_not_of(" \t"); if (s) c = (s != std::string::npos) ? c.substr(s) : "";
size_t e = c.find_last_not_of(" \t"); if (e != std::string::npos) c.resize(e + 1);
if (c.empty()) return true;
// @target specifiers: @player, @focus, @mouseover (mouseover → skip, no tracking)
if (!c.empty() && c[0] == '@') {
std::string spec = c.substr(1);
if (spec == "player") tgt = gameHandler.getPlayerGuid();
else if (spec == "focus") tgt = gameHandler.getFocusGuid();
else if (spec == "target") tgt = gameHandler.getTargetGuid();
// mouseover: no tracking yet — treat as "use current target"
return true;
}
// target=X specifiers
if (c.rfind("target=", 0) == 0) {
std::string spec = c.substr(7);
if (spec == "player") tgt = gameHandler.getPlayerGuid();
else if (spec == "focus") tgt = gameHandler.getFocusGuid();
else if (spec == "target") tgt = gameHandler.getTargetGuid();
return true;
}
// mod / nomod
if (c == "nomod" || c == "mod:none") return !anyMod;
if (c.rfind("mod:", 0) == 0) {
std::string mods = c.substr(4);
bool ok = true;
if (mods.find("shift") != std::string::npos && !shiftHeld) ok = false;
if (mods.find("ctrl") != std::string::npos && !ctrlHeld) ok = false;
if (mods.find("alt") != std::string::npos && !altHeld) ok = false;
return ok;
}
// combat / nocombat
if (c == "combat") return gameHandler.isInCombat();
if (c == "nocombat") return !gameHandler.isInCombat();
// Helper to get the effective target entity
auto effTarget = [&]() -> std::shared_ptr<game::Entity> {
if (tgt != static_cast<uint64_t>(-1) && tgt != 0)
return gameHandler.getEntityManager().getEntity(tgt);
return gameHandler.getTarget();
};
// exists / noexists
if (c == "exists") return effTarget() != nullptr;
if (c == "noexists") return effTarget() == nullptr;
// dead / nodead
if (c == "dead") {
auto t = effTarget();
auto u = t ? std::dynamic_pointer_cast<game::Unit>(t) : nullptr;
return u && u->getHealth() == 0;
}
if (c == "nodead") {
auto t = effTarget();
auto u = t ? std::dynamic_pointer_cast<game::Unit>(t) : nullptr;
return u && u->getHealth() > 0;
}
// help (friendly) / harm (hostile) and their no- variants
auto unitHostile = [&](const std::shared_ptr<game::Entity>& t) -> bool {
if (!t) return false;
auto u = std::dynamic_pointer_cast<game::Unit>(t);
return u && gameHandler.isHostileFactionPublic(u->getFactionTemplate());
};
if (c == "harm" || c == "nohelp") { return unitHostile(effTarget()); }
if (c == "help" || c == "noharm") { return !unitHostile(effTarget()); }
// noform / nostance — player is NOT in a shapeshift/stance
if (c == "noform" || c == "nostance") {
for (const auto& a : gameHandler.getPlayerAuras())
if (!a.isEmpty() && a.maxDurationMs == -1) return false;
return true;
}
// form:0 same as noform
if (c == "form:0" || c == "stance:0") {
for (const auto& a : gameHandler.getPlayerAuras())
if (!a.isEmpty() && a.maxDurationMs == -1) return false;
return true;
}
// Unknown → permissive (don't block)
return true;
};
for (auto& alt : alts) {
// trim
size_t fs = alt.find_first_not_of(" \t");
if (fs == std::string::npos) continue;
alt = alt.substr(fs);
size_t ls = alt.find_last_not_of(" \t");
if (ls != std::string::npos) alt.resize(ls + 1);
if (!alt.empty() && alt[0] == '[') {
size_t close = alt.find(']');
if (close == std::string::npos) continue;
std::string condStr = alt.substr(1, close - 1);
std::string argPart = alt.substr(close + 1);
// Trim argPart
size_t as = argPart.find_first_not_of(" \t");
argPart = (as != std::string::npos) ? argPart.substr(as) : "";
// Evaluate comma-separated conditions
uint64_t tgt = static_cast<uint64_t>(-1);
bool pass = true;
size_t cp = 0;
while (pass) {
size_t comma = condStr.find(',', cp);
std::string tok = condStr.substr(cp, comma == std::string::npos ? std::string::npos : comma - cp);
if (!evalCond(tok, tgt)) { pass = false; break; }
if (comma == std::string::npos) break;
cp = comma + 1;
}
if (pass) {
if (tgt != static_cast<uint64_t>(-1)) targetOverride = tgt;
return argPart;
}
} else {
// No condition block — default fallback always matches
return alt;
}
}
return {};
}
// Execute all non-comment lines of a macro body in sequence. // Execute all non-comment lines of a macro body in sequence.
// In WoW, every line executes per click; the server enforces spell-cast limits. // In WoW, every line executes per click; the server enforces spell-cast limits.
void GameScreen::executeMacroText(game::GameHandler& gameHandler, const std::string& macroText) { void GameScreen::executeMacroText(game::GameHandler& gameHandler, const std::string& macroText) {
@ -6175,6 +6342,18 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin()); while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin());
while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back(); while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back();
// Evaluate WoW macro conditionals: /cast [mod:shift] Greater Heal; Flash Heal
uint64_t castTargetOverride = static_cast<uint64_t>(-1);
if (!spellArg.empty() && spellArg.front() == '[') {
spellArg = evaluateMacroConditionals(spellArg, gameHandler, castTargetOverride);
if (spellArg.empty()) {
chatInputBuffer[0] = '\0';
return; // No conditional matched — skip cast
}
while (!spellArg.empty() && spellArg.front() == ' ') spellArg.erase(spellArg.begin());
while (!spellArg.empty() && spellArg.back() == ' ') spellArg.pop_back();
}
// Support numeric spell ID: /cast 133 or /cast #133 // Support numeric spell ID: /cast 133 or /cast #133
{ {
std::string numStr = spellArg; std::string numStr = spellArg;
@ -6186,7 +6365,9 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
uint32_t spellId = 0; uint32_t spellId = 0;
try { spellId = static_cast<uint32_t>(std::stoul(numStr)); } catch (...) {} try { spellId = static_cast<uint32_t>(std::stoul(numStr)); } catch (...) {}
if (spellId != 0) { if (spellId != 0) {
uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; uint64_t targetGuid = (castTargetOverride != static_cast<uint64_t>(-1))
? castTargetOverride
: (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0);
gameHandler.castSpell(spellId, targetGuid); gameHandler.castSpell(spellId, targetGuid);
} }
chatInputBuffer[0] = '\0'; chatInputBuffer[0] = '\0';
@ -6246,7 +6427,9 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
} }
if (bestSpellId) { if (bestSpellId) {
uint64_t targetGuid = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0; uint64_t targetGuid = (castTargetOverride != static_cast<uint64_t>(-1))
? castTargetOverride
: (gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0);
gameHandler.castSpell(bestSpellId, targetGuid); gameHandler.castSpell(bestSpellId, targetGuid);
} else { } else {
game::MessageChatData sysMsg; game::MessageChatData sysMsg;