diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7da3b9ab..64709cf9 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -885,6 +885,10 @@ public: const std::array& getActionBar() const { return actionBar; } void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id); + // Client-side macro text storage (server sends only macro index; text is stored locally) + const std::string& getMacroText(uint32_t macroId) const; + void setMacroText(uint32_t macroId, const std::string& text); + void saveCharacterConfig(); void loadCharacterConfig(); static std::string getCharacterConfigDir(); @@ -2759,6 +2763,7 @@ private: float castTimeTotal = 0.0f; std::array actionBar{}; + std::unordered_map macros_; // client-side macro text (persisted in char config) std::vector playerAuras; std::vector targetAuras; std::unordered_map> unitAurasCache_; // per-unit aura cache diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 0e73c552..26db5c83 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -201,6 +201,10 @@ private: // Keybinding customization int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index bool awaitingKeyPress = false; + // Macro editor popup state + uint32_t macroEditorId_ = 0; // macro index being edited + bool macroEditorOpen_ = false; // deferred OpenPopup flag + char macroEditorBuf_[256] = {}; // edit buffer bool pendingUseOriginalSoundtrack = true; bool pendingShowActionBar2 = true; // Show second action bar above main bar float pendingActionBarScale = 1.0f; // Multiplier for action bar slot size (0.5–1.5) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6bce4ade..9089d438 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -23468,6 +23468,21 @@ std::string GameHandler::getCharacterConfigDir() { return dir; } +static const std::string EMPTY_MACRO_TEXT; + +const std::string& GameHandler::getMacroText(uint32_t macroId) const { + auto it = macros_.find(macroId); + return (it != macros_.end()) ? it->second : EMPTY_MACRO_TEXT; +} + +void GameHandler::setMacroText(uint32_t macroId, const std::string& text) { + if (text.empty()) + macros_.erase(macroId); + else + macros_[macroId] = text; + saveCharacterConfig(); +} + void GameHandler::saveCharacterConfig() { const Character* ch = getActiveCharacter(); if (!ch || ch->name.empty()) return; @@ -23494,6 +23509,12 @@ void GameHandler::saveCharacterConfig() { out << "action_bar_" << i << "_id=" << actionBar[i].id << "\n"; } + // Save client-side macro text + for (const auto& [id, text] : macros_) { + if (!text.empty()) + out << "macro_" << id << "_text=" << text << "\n"; + } + // Save quest log out << "quest_log_count=" << questLog_.size() << "\n"; for (size_t i = 0; i < questLog_.size(); i++) { @@ -23534,6 +23555,15 @@ void GameHandler::loadCharacterConfig() { try { savedGender = std::stoi(val); } catch (...) {} } else if (key == "use_female_model") { try { savedUseFemaleModel = std::stoi(val); } catch (...) {} + } else if (key.rfind("macro_", 0) == 0) { + // Parse macro_N_text + size_t firstUnder = 6; // length of "macro_" + size_t secondUnder = key.find('_', firstUnder); + if (secondUnder == std::string::npos) continue; + uint32_t macroId = 0; + try { macroId = static_cast(std::stoul(key.substr(firstUnder, secondUnder - firstUnder))); } catch (...) { continue; } + if (key.substr(secondUnder + 1) == "text" && !val.empty()) + macros_[macroId] = val; } else if (key.rfind("action_bar_", 0) == 0) { // Parse action_bar_N_type or action_bar_N_id size_t firstUnderscore = 11; // length of "action_bar_" diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f5baf974..658ff7f4 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -7483,6 +7483,16 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { gameHandler.castSpell(slot.id, target); } else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) { gameHandler.useItemById(slot.id); + } else if (slot.type == game::ActionBarSlot::MACRO) { + const std::string& text = gameHandler.getMacroText(slot.id); + if (!text.empty()) { + // Execute first line of macro as a chat command + size_t nl = text.find('\n'); + std::string firstLine = (nl != std::string::npos) ? text.substr(0, nl) : text; + strncpy(chatInputBuffer, firstLine.c_str(), sizeof(chatInputBuffer) - 1); + chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0'; + sendChatMessage(gameHandler); + } } } @@ -7513,6 +7523,24 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } } else if (slot.type == game::ActionBarSlot::MACRO) { ImGui::TextDisabled("Macro #%u", slot.id); + ImGui::Separator(); + if (ImGui::MenuItem("Execute")) { + const std::string& text = gameHandler.getMacroText(slot.id); + if (!text.empty()) { + size_t nl = text.find('\n'); + std::string firstLine = (nl != std::string::npos) ? text.substr(0, nl) : text; + strncpy(chatInputBuffer, firstLine.c_str(), sizeof(chatInputBuffer) - 1); + chatInputBuffer[sizeof(chatInputBuffer) - 1] = '\0'; + sendChatMessage(gameHandler); + } + } + if (ImGui::MenuItem("Edit")) { + const std::string& txt = gameHandler.getMacroText(slot.id); + strncpy(macroEditorBuf_, txt.c_str(), sizeof(macroEditorBuf_) - 1); + macroEditorBuf_[sizeof(macroEditorBuf_) - 1] = '\0'; + macroEditorId_ = slot.id; + macroEditorOpen_ = true; + } } ImGui::Separator(); if (ImGui::MenuItem("Clear Slot")) { @@ -7574,6 +7602,13 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { } else if (slot.type == game::ActionBarSlot::MACRO) { ImGui::BeginTooltip(); ImGui::Text("Macro #%u", slot.id); + const std::string& macroText = gameHandler.getMacroText(slot.id); + if (!macroText.empty()) { + ImGui::Separator(); + ImGui::TextUnformatted(macroText.c_str()); + } else { + ImGui::TextDisabled("(no text — right-click to Edit)"); + } ImGui::EndTooltip(); } else if (slot.type == game::ActionBarSlot::ITEM) { ImGui::BeginTooltip(); @@ -7786,6 +7821,28 @@ void GameScreen::renderActionBar(game::GameHandler& gameHandler) { if (i > 0) ImGui::SameLine(0, spacing); renderBarSlot(i, keyLabels1[i]); } + + // Macro editor modal — opened by "Edit" in action bar context menus + if (macroEditorOpen_) { + ImGui::OpenPopup("Edit Macro###MacroEdit"); + macroEditorOpen_ = false; + } + if (ImGui::BeginPopupModal("Edit Macro###MacroEdit", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) { + ImGui::Text("Macro #%u (first line executes on click)", macroEditorId_); + ImGui::SetNextItemWidth(320.0f); + ImGui::InputTextMultiline("##MacroText", macroEditorBuf_, sizeof(macroEditorBuf_), + ImVec2(320.0f, 80.0f)); + if (ImGui::Button("Save")) { + gameHandler.setMacroText(macroEditorId_, std::string(macroEditorBuf_)); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } } ImGui::End();