From 09b0bea981c0adc6f66550c533a06f646455c352 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 04:14:44 -0700 Subject: [PATCH] feat: add /stopmacro support and low durability warning for equipped items - /stopmacro [conditions] halts remaining macro commands; supports all existing macro conditionals ([combat], [nocombat], [mod:shift], etc.) via the sentinel action trick on evaluateMacroConditionals - macroStopped_ flag in GameScreen; executeMacroText resets and checks it after each command so /stopmacro mid-macro skips all subsequent lines - Emit a "X is about to break!" UI error + system chat when an equipped item's durability drops below 20% via SMSG_UPDATE_OBJECT field delta; warning fires once per threshold crossing (prevDur >= maxDur/5, newDur < maxDur/5) --- include/ui/game_screen.hpp | 3 +++ src/game/game_handler.cpp | 22 ++++++++++++++++++++++ src/ui/game_screen.cpp | 27 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 0b958cb2..e7bc42dc 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -55,6 +55,9 @@ private: std::vector chatSentHistory_; int chatHistoryIdx_ = -1; // -1 = not browsing history + // Set to true by /stopmacro; checked in executeMacroText to halt remaining commands. + bool macroStopped_ = false; + // Tab-completion state for slash commands std::string chatTabPrefix_; // prefix captured on first Tab press std::vector chatTabMatches_; // matching command list diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 664be8fe..16393cb0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -12302,8 +12302,30 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem } } else if (key == itemDurField && isItemInInventory) { if (it->second.curDurability != val) { + const uint32_t prevDur = it->second.curDurability; it->second.curDurability = val; inventoryChanged = true; + // Warn once when durability drops below 20% for an equipped item. + const uint32_t maxDur = it->second.maxDurability; + if (maxDur > 0 && val < maxDur / 5u && prevDur >= maxDur / 5u) { + // Check if this item is in an equip slot (not bag inventory). + bool isEquipped = false; + for (uint64_t slotGuid : equipSlotGuids_) { + if (slotGuid == block.guid) { isEquipped = true; break; } + } + if (isEquipped) { + std::string itemName; + const auto* info = getItemInfo(it->second.entry); + if (info) itemName = info->name; + char buf[128]; + if (!itemName.empty()) + std::snprintf(buf, sizeof(buf), "%s is about to break!", itemName.c_str()); + else + std::snprintf(buf, sizeof(buf), "An equipped item is about to break!"); + addUIError(buf); + addSystemChatMessage(buf); + } + } } } else if (key == itemMaxDurField && isItemInInventory) { if (it->second.maxDurability != val) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 25b5f594..7338da94 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -5564,12 +5564,16 @@ static std::string evaluateMacroConditionals(const std::string& rawArg, // Execute all non-comment lines of a macro body in sequence. // In WoW, every line executes per click; the server enforces spell-cast limits. +// /stopmacro (with optional conditionals) halts the remaining commands early. void GameScreen::executeMacroText(game::GameHandler& gameHandler, const std::string& macroText) { + macroStopped_ = false; for (const auto& cmd : allMacroCommands(macroText)) { strncpy(chatInputBuffer, cmd.c_str(), sizeof(chatInputBuffer) - 1); chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0'; sendChatMessage(gameHandler); + if (macroStopped_) break; } + macroStopped_ = false; } // /castsequence persistent state — shared across all macros using the same spell list. @@ -5633,6 +5637,29 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) { return; } + // /stopmacro [conditions] + // Halts execution of the current macro (remaining lines are skipped). + // With a condition block, only stops if the conditions evaluate to true. + // /stopmacro → always stops + // /stopmacro [combat] → stops only while in combat + // /stopmacro [nocombat] → stops only when not in combat + if (cmdLower == "stopmacro") { + bool shouldStop = true; + if (spacePos != std::string::npos) { + std::string condArg = command.substr(spacePos + 1); + while (!condArg.empty() && condArg.front() == ' ') condArg.erase(condArg.begin()); + if (!condArg.empty() && condArg.front() == '[') { + // Append a sentinel action so evaluateMacroConditionals can signal a match. + uint64_t tgtOver = static_cast(-1); + std::string hit = evaluateMacroConditionals(condArg + " __stop__", gameHandler, tgtOver); + shouldStop = !hit.empty(); + } + } + if (shouldStop) macroStopped_ = true; + chatInputBuffer[0] = '\0'; + return; + } + // /invite command if (cmdLower == "invite" && spacePos != std::string::npos) { std::string targetName = command.substr(spacePos + 1);