From e7f6a9dd80c1768e48eb7b904b26978383e8e252 Mon Sep 17 00:00:00 2001 From: Petr Komogortsev <44289657+peterkmg@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:51:35 +0200 Subject: [PATCH] feat(bindings): backport direct-action and override binding APIs Add SetBindingSpell/Item/Macro/Click plus the owner-scoped SetOverrideBinding family. Store permanent commands through vanilla SetBinding, execute SPELL/ITEM/MACRO/CLICK in the resolved-command hook, and layer priority overrides ahead of the native binding table. Factor raw and saved macro dispatch into Macro::Execute so click attributes and binding commands share the same parser fallback. Preserve key-down/key-up pairing when overrides change during an action. Document return values, persistence, precedence, macro-text usage, and the deliberate 1.12 differences in README and docs/API.md. --- README.md | 15 +- docs/API.md | 102 +++++++- src/Offsets.h | 23 ++ src/bindings/Api.cpp | 541 +++++++++++++++++++++++++++++++++++++++ src/frame/Attributes.cpp | 79 +----- src/macro/Execute.cpp | 196 ++++++++++++++ src/macro/Execute.h | 27 ++ 7 files changed, 905 insertions(+), 78 deletions(-) create mode 100644 src/bindings/Api.cpp create mode 100644 src/macro/Execute.cpp create mode 100644 src/macro/Execute.h diff --git a/README.md b/README.md index 273dbe9..4a5feb9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**. | [Action](docs/API.md#action) | `GetActionInfo` | | [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` | | [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` | +| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` | | [Chat](docs/API.md#chat) | `GetCurrentChatGUID` | | [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` | | [Class](docs/API.md#class) | `FillLocalizedClassList` | @@ -201,6 +202,17 @@ when launching with `-console`), not as Lua functions. See the ### Bindings +ClassicAPI backports the later-client direct-action and temporary override +binding families. Permanent bindings use the standard `SPELL`, `ITEM`, +`MACRO`, and `CLICK` command strings and can be saved normally. Overrides are +session-only, frame-owned, and support the usual priority flag. + +See the [Bindings API reference](docs/API.md#bindings) for signatures, +precedence, execution behavior, macro-text usage, and 1.12 compatibility +notes. + +#### Predefined focus bindings + Injected into the engine's **Targeting Functions** group at FrameXML Bindings.xml load time, so they appear in the keybind UI alongside native targeting bindings instead of orphaned at the bottom. @@ -210,7 +222,8 @@ native targeting bindings instead of orphaned at the bottom. | `FOCUSTARGET` | `FocusUnit("target")` — pin current target as focus | | `TARGETFOCUS` | `TargetUnit("focus")` — switch target to the focus | -See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the implementation note. +See [Predefined focus bindings](docs/API.md#predefined-focus-bindings-focustarget--targetfocus) +for the implementation note. ### Behaviors diff --git a/docs/API.md b/docs/API.md index 4334a2e..42e374b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3763,7 +3763,107 @@ after `"target"` (matching retail order), so a focused unit's GUID reverse-resolves to `"focus"` only if it isn't already addressable as `"player"` / `"party*"` / `"raid*"` / `"nameplate*"` / `"target"`. -### Bindings (`FOCUSTARGET` / `TARGETFOCUS`) +## Bindings + +Backports the direct-action and temporary override binding APIs added in WoW +2.0. The permanent helpers write later-client action command strings into +vanilla's native binding table; the override helpers add a separate, +session-only layer in front of that table. + +### Permanent direct-action bindings + +| Function | Equivalent command | +|----------|--------------------| +| `ok = SetBindingSpell(key, spell)` | `SetBinding(key, "SPELL " .. spell)` | +| `ok = SetBindingItem(key, item)` | `SetBinding(key, "ITEM " .. item)` | +| `ok = SetBindingMacro(key, macroNameOrIndex)` | `SetBinding(key, "MACRO " .. macroNameOrIndex)` | +| `ok = SetBindingClick(key, buttonName [, mouseButton])` | `SetBinding(key, "CLICK " .. buttonName .. (mouseButton and ":" .. mouseButton or ""))` | + +`key` is any binding string accepted by the client, such as `"CTRL-F"`, +`"SHIFT-BUTTON4"`, or `"F8"`. `spell`, `item`, `macroNameOrIndex`, and +`buttonName` identify the action to perform. `macroNameOrIndex` accepts either +a saved macro's name or its decimal index. `mouseButton` is passed to the +button's click handler and defaults to `"LeftButton"`; when supplied, it is +stored as the `:MouseButton` suffix of the `CLICK` command. + +Each helper returns `1` if vanilla's `SetBinding` changed the binding and +`nil` otherwise. It changes whichever binding set is currently loaded, fires +vanilla's normal `UPDATE_BINDINGS` notification, and is not persisted until +the addon calls `SaveBindings`. Use vanilla `SetBinding(key)` to unbind a key; +the typed helpers always require an action argument. + +The command hook also understands manually constructed `SPELL`, `ITEM`, +`MACRO`, and `CLICK` strings passed directly to vanilla `SetBinding`. + +### Temporary override bindings + +```lua +SetOverrideBinding(owner, isPriority, key [, command]) +SetOverrideBindingSpell(owner, isPriority, key, spell) +SetOverrideBindingItem(owner, isPriority, key, item) +SetOverrideBindingMacro(owner, isPriority, key, macroNameOrIndex) +SetOverrideBindingClick(owner, isPriority, key, buttonName [, mouseButton]) +ClearOverrideBindings(owner) +``` + +`owner` must be a frame and `isPriority` must be a Boolean. The typed helpers +construct the same four action strings shown above. The generic +`SetOverrideBinding` also accepts ordinary Bindings.xml command names, which +are delegated to vanilla's command executor. Override setters and +`ClearOverrideBindings` return no values. + +Passing no `command` (or `nil`) to `SetOverrideBinding` removes that owner's +override for `key`. `ClearOverrideBindings(owner)` removes every override +belonging to that owner. Overrides never change or save the native binding +table and are cleared whenever the FrameScript Lua state is recreated, such +as on `/reload` or logout. + +Resolution order is: + +1. Priority override bindings. +2. Non-priority override bindings. +3. The normal native binding. + +When multiple owners set the same key at the same priority, the most recently +set override wins. Removing it immediately reveals the next matching override +or the unchanged native binding. + +### Action execution + +| Command | Execution path | +|---------|----------------| +| `SPELL name` | Calls vanilla `CastSpellByName(name)`. The usual `/cast` spell-name syntax is accepted. | +| `ITEM item` | Calls `C_Item.UseItemByName(item)`, accepting the names, item strings, and item IDs supported by that API. | +| `MACRO nameOrIndex` | Resolves a saved macro by name or decimal index. An addon-provided `RunMacro` is used when available; otherwise ClassicAPI resolves it with `GetMacroInfo` and dispatches its body through `ChatEdit_ParseText`. | +| `CLICK ButtonName[:MouseButton]` | Resolves the named global frame and calls its ordinary `:Click(mouseButton)` method. The default is `LeftButton`. | + +`SetBindingMacro` and `SetOverrideBindingMacro` take a saved macro identifier, +not literal macro text. To bind literal macro text, bind a named button whose +ClassicAPI attributes describe a macro action: + +```lua +local button = CreateFrame("Button", "MyMacroTextBindingButton") +button:SetAttribute("type", "macro") +button:SetAttribute("macrotext", "/say Bound macro text") + +SetBindingClick("CTRL-F7", button:GetName()) +``` + +### 1.12 compatibility notes + +- There is no combat lockdown or secure execution. These functions remain + callable in combat, and `CLICK` invokes the frame's ordinary click path. +- Direct actions run on key-down. The corresponding key-up is consumed; a + click binding does not emulate later clients' separate mouse-down and + mouse-up delivery. +- The override layer is not reflected by vanilla `GetBindingAction`. + In particular, the later-client `GetBindingAction(key, true)` override query + is not backported. +- Override ownership is explicit. Call `ClearOverrideBindings(owner)` when the + addon's owner is no longer needed; all remaining overrides are also cleared + when the Lua state is recreated. + +### Predefined focus bindings (`FOCUSTARGET` / `TARGETFOCUS`) Two key bindings appear in the keybind UI (`Esc` → Key Bindings) under the **Targeting Functions** group, between `PETATTACK` and the diff --git a/src/Offsets.h b/src/Offsets.h index c0b4ce5..0e5420c 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -40,6 +40,29 @@ enum Offsets { // state, so `FrameScript_RegisterFunction` writes land on it. FUN_LOAD_GLUE_SCRIPT_FUNCTIONS = 0x0046ABB0, + // Binding manager internals used by the direct-action/override binding backport. + // + // Stock Lua `SetBinding` wrapper (`int __fastcall(lua_State *)`). + // The SetBindingSpell/Item/Macro/Click wrappers delegate to it after + // constructing their action command, preserving punctuation-key + // normalization, current-set selection, the Boolean return value, and the + // native UPDATE_BINDINGS notification. + FUN_SCRIPT_SET_BINDING = 0x004B8000, + + // Hardware-key dispatch, before the normal binding table is resolved: + // int __thiscall(manager, key, isDown) + // Hooking here lets owner-scoped override bindings remain a separate + // layer rather than temporarily mutating the character/account binding + // table. The normal dispatcher is called unchanged when no override wins. + FUN_BINDING_KEY_DISPATCH = 0x004B7990, + + // Executes a resolved binding command: + // int __thiscall(manager, command, isDown) + // Vanilla has no SPELL/ITEM/MACRO/CLICK direct-action command handlers. The + // hook recognizes those four families and delegates every native command + // to the client. + FUN_BINDING_COMMAND_EXECUTE = 0x004B7B50, + // GameTooltip script-method prologue helpers (used to resolve self → CFrameScriptObject*). // Underlying Lua C API names per [[docs/LuaCAPI.md]] are `lua_rawgeti` // (0x6F3BC0) and `lua_touserdata` (0x6F3740). The Set* method prologue's diff --git a/src/bindings/Api.cpp b/src/bindings/Api.cpp new file mode 100644 index 0000000..b3f3729 --- /dev/null +++ b/src/bindings/Api.cpp @@ -0,0 +1,541 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// Backports the direct-action and override binding APIs introduced in 2.0. +// Normal bindings are stored by vanilla's SetBinding implementation using the +// later-client SPELL/ITEM/MACRO/CLICK command strings. The resolved-command +// hook executes those four command families; every native binding command is +// passed through unchanged. +// +// Override bindings live in a separate owner-scoped layer in front of the +// native key lookup. They are deliberately never written to the saved binding +// table, so clearing an owner immediately reveals the next override or the +// unchanged native binding underneath it. + +#include "Game.h" +#include "Offsets.h" +#include "macro/Execute.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Bindings::Api { + +namespace { + +constexpr const char kSpellPrefix[] = "SPELL "; +constexpr const char kItemPrefix[] = "ITEM "; +constexpr const char kMacroPrefix[] = "MACRO "; +constexpr const char kClickPrefix[] = "CLICK "; +constexpr const char kDefaultMouseButton[] = "LeftButton"; + +using ScriptSetBinding_t = int(__fastcall *)(void *L); +using KeyDispatch_t = int(__thiscall *)(void *mgr, const char *key, int isDown); +using CommandExecute_t = int(__thiscall *)(void *mgr, const char *cmd, int isDown); + +KeyDispatch_t KeyDispatch_o = nullptr; +CommandExecute_t CommandExecute_o = nullptr; + +enum class CommandType { + None, + Spell, + Item, + Macro, + Click, +}; + +struct ParsedCommand { + CommandType type = CommandType::None; + const char *argument = nullptr; +}; + +struct OverrideEntry { + const void *owner = nullptr; + std::string key; + std::string command; + bool priority = false; + uint64_t sequence = 0; +}; + +std::vector g_overrides; +std::unordered_map g_activeOverrides; +uint64_t g_nextSequence = 0; + +std::string CanonicalKey(const char *key) { + std::string result = key ? key : ""; + for (char &character : result) { + if (character >= 'a' && character <= 'z') + character = static_cast(character - 'a' + 'A'); + } + + return result; +} + +std::string MakeCommand(const char *prefix, const char *arg) { + std::string cmd = prefix; + cmd += arg; + return cmd; +} + +std::string MakeClickCommand(const char *btnName, const char *mouseBtn) { + std::string cmd = MakeCommand(kClickPrefix, btnName); + if (mouseBtn != nullptr) { + cmd += ':'; + cmd += mouseBtn; + } + + return cmd; +} + +ParsedCommand ParseCommand(const char *cmd) { + if (cmd == nullptr) + return {}; + + struct PrefixEntry { + const char *prefix; + size_t length; + CommandType type; + }; + + static constexpr PrefixEntry prefixes[] = { + {kSpellPrefix, sizeof(kSpellPrefix) - 1, CommandType::Spell}, + {kItemPrefix, sizeof(kItemPrefix) - 1, CommandType::Item}, + {kMacroPrefix, sizeof(kMacroPrefix) - 1, CommandType::Macro}, + {kClickPrefix, sizeof(kClickPrefix) - 1, CommandType::Click}, + }; + + for (const PrefixEntry &entry : prefixes) { + if (std::strncmp(cmd, entry.prefix, entry.length) == 0) + return {entry.type, cmd + entry.length}; + } + + return {}; +} + +bool CallGlobalString(void *L, const char *func, const char *arg) { + const int top = Game::Lua::GetTop(L); + if (!Game::Lua::PushGlobalFunction(L, func)) { + Game::Lua::SetTop(L, top); + return false; + } + + Game::Lua::PushString(L, arg); + const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0; + Game::Lua::SetTop(L, top); + + return ok; +} + +bool CallTableString(void *L, const char *table, const char *func, const char *arg) { + const int top = Game::Lua::GetTop(L); + + Game::Lua::PushString(L, table); + Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); + if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) { + Game::Lua::SetTop(L, top); + return false; + } + + const int tableIndex = Game::Lua::GetTop(L); + + Game::Lua::PushString(L, func); + Game::Lua::GetTable(L, tableIndex); + if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) { + Game::Lua::SetTop(L, top); + return false; + } + + Game::Lua::PushString(L, arg); + const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0; + Game::Lua::SetTop(L, top); + + return ok; +} + +void InvokeButtonClick(void *L, const char *btnName, size_t btnNameLength, const char *mouseBtn) { + const int top = Game::Lua::GetTop(L); + + Game::Lua::PushLString(L, btnName, static_cast(btnNameLength)); + Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); + if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) { + Game::Lua::SetTop(L, top); + return; + } + + const int frameIndex = Game::Lua::GetTop(L); + Game::Lua::PushString(L, "Click"); + Game::Lua::GetTable(L, frameIndex); + if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION) { + Game::Lua::PushValue(L, frameIndex); + Game::Lua::PushString(L, mouseBtn); + Game::Lua::PCall(L, 2, 0, 0); + } + + Game::Lua::SetTop(L, top); +} + +void ExecuteClick(void *L, const char *arg) { + const char *separator = std::strrchr(arg, ':'); + if (separator == arg || (separator != nullptr && separator[1] == '\0')) + return; + + const char *mouseBtn = kDefaultMouseButton; + size_t btnNameLength = std::strlen(arg); + if (separator != nullptr) { + btnNameLength = static_cast(separator - arg); + mouseBtn = separator + 1; + } + + if (btnNameLength == 0) + return; + + InvokeButtonClick(L, arg, btnNameLength, mouseBtn); +} + +// Returns true for every recognized direct-action command, including malformed +// or stale commands. Vanilla cannot execute these namespaces, so a bad command +// is consumed as a no-op instead of falling through to an unrelated handler. +bool ExecuteSpecialCommand(const char *cmd, int isDown) { + const ParsedCommand parsed = ParseCommand(cmd); + if (parsed.type == CommandType::None) + return false; + + if (!isDown || parsed.argument == nullptr || *parsed.argument == '\0') + return true; + + void *L = Game::Lua::State(); + if (L == nullptr) + return true; + + switch (parsed.type) { + case CommandType::Spell: + CallGlobalString(L, "CastSpellByName", parsed.argument); + break; + case CommandType::Item: + CallTableString(L, "C_Item", "UseItemByName", parsed.argument); + break; + case CommandType::Macro: + Macro::Execute::Saved(L, parsed.argument); + break; + case CommandType::Click: + ExecuteClick(L, parsed.argument); + break; + default: + break; + } + + return true; +} + +const OverrideEntry *FindEffectiveOverride(const std::string &key) { + const OverrideEntry *best = nullptr; + for (const OverrideEntry &entry : g_overrides) { + if (entry.key != key) + continue; + + if (best == nullptr || entry.priority > best->priority || + (entry.priority == best->priority && entry.sequence > best->sequence)) + best = &entry; + } + + return best; +} + +void SetOverride(const void *owner, bool priority, const std::string &key, const std::string &cmd) { + auto existing = + std::find_if(g_overrides.begin(), g_overrides.end(), + [&](const OverrideEntry &entry) { + return entry.owner == owner && entry.key == key; + }); + + if (existing == g_overrides.end()) { + g_overrides.push_back( + {owner, key, cmd, priority, ++g_nextSequence}); + return; + } + + existing->command = cmd; + existing->priority = priority; + existing->sequence = ++g_nextSequence; +} + +void RemoveOverride(const void *owner, const std::string &key) { + g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(), + [&](const OverrideEntry &entry) { + return entry.owner == owner && + entry.key == key; + }), + g_overrides.end()); +} + +int ExecuteCommand(void *mgr, const char *cmd, int isDown) { + if (ExecuteSpecialCommand(cmd, isDown)) + return 1; + + return CommandExecute_o(mgr, cmd, isDown); +} + +int __fastcall CommandExecute_h(void *mgr, void *, const char *cmd, int isDown) { + return ExecuteCommand(mgr, cmd, isDown); +} + +int __fastcall KeyDispatch_h(void *mgr, void *, const char *key, int isDown) { + const std::string canonicalKey = CanonicalKey(key); + + // Keep a key-down paired with its override command even if that override is + // cleared while the command executes. The newly revealed native binding + // must not receive only the key-up half of the same physical press. + if (!isDown) { + auto active = g_activeOverrides.find(canonicalKey); + if (active != g_activeOverrides.end()) { + ExecuteCommand(mgr, active->second.c_str(), 0); + g_activeOverrides.erase(active); + return 1; + } + } + + const OverrideEntry *entry = FindEffectiveOverride(canonicalKey); + if (entry == nullptr) + return KeyDispatch_o(mgr, key, isDown); + + if (isDown) + g_activeOverrides[canonicalKey] = entry->command; + + return ExecuteCommand(mgr, entry->command.c_str(), isDown); +} + +int SetBindingCommand(void *L, const char *prefix, const char *usage) { + if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) { + Game::Lua::Error(L, usage); + return 0; + } + + const std::string key = Game::Lua::ToString(L, 1); + const std::string command = MakeCommand(prefix, Game::Lua::ToString(L, 2)); + + // Preserve the stock SetBinding return value, binding-set selection, + // key normalization, UPDATE_BINDINGS event, and SaveBindings behavior. + Game::Lua::SetTop(L, 0); + Game::Lua::PushString(L, key.c_str()); + Game::Lua::PushString(L, command.c_str()); + auto setBinding = + reinterpret_cast(Offsets::FUN_SCRIPT_SET_BINDING); + + return setBinding(L); +} + +const char *OptionalMouseButton(void *L, int index, const char *usage) { + if (Game::Lua::GetTop(L) < index || Game::Lua::Type(L, index) == Game::Lua::TYPE_NIL) + return nullptr; + + if (!Game::Lua::IsString(L, index)) { + Game::Lua::Error(L, usage); + return nullptr; + } + + return Game::Lua::ToString(L, index); +} + +int __fastcall Script_SetBindingSpell(void *L) { + return SetBindingCommand(L, kSpellPrefix, "Usage: SetBindingSpell(key, spell)"); +} + +int __fastcall Script_SetBindingItem(void *L) { + return SetBindingCommand(L, kItemPrefix, "Usage: SetBindingItem(key, item)"); +} + +int __fastcall Script_SetBindingMacro(void *L) { + return SetBindingCommand(L, kMacroPrefix, "Usage: SetBindingMacro(key, macro)"); +} + +int __fastcall Script_SetBindingClick(void *L) { + constexpr const char *usage = "Usage: SetBindingClick(key, buttonName [, mouseButton])"; + if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) { + Game::Lua::Error(L, usage); + return 0; + } + + const std::string key = Game::Lua::ToString(L, 1); + const char *buttonName = Game::Lua::ToString(L, 2); + const char *mouseButton = OptionalMouseButton(L, 3, usage); + const std::string command = MakeClickCommand(buttonName, mouseButton); + + Game::Lua::SetTop(L, 0); + Game::Lua::PushString(L, key.c_str()); + Game::Lua::PushString(L, command.c_str()); + auto setBinding = + reinterpret_cast(Offsets::FUN_SCRIPT_SET_BINDING); + + return setBinding(L); +} + +bool ReadOverrideHeader(void *L, const char *usage, const void **owner, + bool *priority, std::string *key) { + *owner = Game::Lua::ResolveObject(L, 1); + if (*owner == nullptr || Game::Lua::Type(L, 2) != Game::Lua::TYPE_BOOLEAN || + !Game::Lua::IsString(L, 3)) { + Game::Lua::Error(L, usage); + return false; + } + + *priority = Game::Lua::ToBoolean(L, 2) != 0; + *key = CanonicalKey(Game::Lua::ToString(L, 3)); + return true; +} + +int SetOverrideBindingCommand(void *L, const char *prefix, const char *usage) { + const void *owner = nullptr; + bool priority = false; + + std::string key; + if (!ReadOverrideHeader(L, usage, &owner, &priority, &key)) + return 0; + + if (!Game::Lua::IsString(L, 4)) { + Game::Lua::Error(L, usage); + return 0; + } + + SetOverride(owner, priority, key, + MakeCommand(prefix, Game::Lua::ToString(L, 4))); + + return 0; +} + +int __fastcall Script_SetOverrideBinding(void *L) { + constexpr const char *usage = "Usage: SetOverrideBinding(owner, isPriority, key [, command])"; + const void *owner = nullptr; + bool priority = false; + + std::string key; + if (!ReadOverrideHeader(L, usage, &owner, &priority, &key)) + return 0; + + if (Game::Lua::GetTop(L) < 4 || + Game::Lua::Type(L, 4) == Game::Lua::TYPE_NIL) { + RemoveOverride(owner, key); + return 0; + } + + if (!Game::Lua::IsString(L, 4)) { + Game::Lua::Error(L, usage); + return 0; + } + + SetOverride(owner, priority, key, Game::Lua::ToString(L, 4)); + return 0; +} + +int __fastcall Script_SetOverrideBindingSpell(void *L) { + return SetOverrideBindingCommand( + L, kSpellPrefix, + "Usage: SetOverrideBindingSpell(owner, isPriority, key, spell)"); +} + +int __fastcall Script_SetOverrideBindingItem(void *L) { + return SetOverrideBindingCommand( + L, kItemPrefix, + "Usage: SetOverrideBindingItem(owner, isPriority, key, item)"); +} + +int __fastcall Script_SetOverrideBindingMacro(void *L) { + return SetOverrideBindingCommand( + L, kMacroPrefix, + "Usage: SetOverrideBindingMacro(owner, isPriority, key, macro)"); +} + +int __fastcall Script_SetOverrideBindingClick(void *L) { + constexpr const char *usage = + "Usage: SetOverrideBindingClick(owner, " + "isPriority, key, buttonName [, mouseButton])"; + + const void *owner = nullptr; + bool priority = false; + + std::string key; + if (!ReadOverrideHeader(L, usage, &owner, &priority, &key)) + return 0; + + if (!Game::Lua::IsString(L, 4)) { + Game::Lua::Error(L, usage); + return 0; + } + + const char *buttonName = Game::Lua::ToString(L, 4); + const char *mouseButton = OptionalMouseButton(L, 5, usage); + SetOverride(owner, priority, key, + MakeClickCommand(buttonName, mouseButton)); + + return 0; +} + +int __fastcall Script_ClearOverrideBindings(void *L) { + const void *owner = Game::Lua::ResolveObject(L, 1); + if (owner == nullptr) { + Game::Lua::Error(L, "Usage: ClearOverrideBindings(owner)"); + return 0; + } + + g_overrides.erase(std::remove_if(g_overrides.begin(), g_overrides.end(), + [&](const OverrideEntry &entry) { + return entry.owner == owner; + }), + g_overrides.end()); + return 0; +} + +void RegisterLuaFunctions() { + // A fresh FrameScript state makes every owner pointer from the previous + // state stale. Permanent bindings remain in vanilla's binding table. + g_overrides.clear(); + g_activeOverrides.clear(); + g_nextSequence = 0; + + Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell); + Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem); + Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro); + Game::Lua::RegisterGlobalFunction("SetBindingClick", &Script_SetBindingClick); + Game::Lua::RegisterGlobalFunction("SetOverrideBinding", + &Script_SetOverrideBinding); + Game::Lua::RegisterGlobalFunction("SetOverrideBindingSpell", + &Script_SetOverrideBindingSpell); + Game::Lua::RegisterGlobalFunction("SetOverrideBindingItem", + &Script_SetOverrideBindingItem); + Game::Lua::RegisterGlobalFunction("SetOverrideBindingMacro", + &Script_SetOverrideBindingMacro); + Game::Lua::RegisterGlobalFunction("SetOverrideBindingClick", + &Script_SetOverrideBindingClick); + Game::Lua::RegisterGlobalFunction("ClearOverrideBindings", + &Script_ClearOverrideBindings); +} + +const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +const Game::HookAutoRegister _hookKeyDispatch{ + Offsets::FUN_BINDING_KEY_DISPATCH, reinterpret_cast(&KeyDispatch_h), + reinterpret_cast(&KeyDispatch_o)}; + +const Game::HookAutoRegister _hookCommandExecute{ + Offsets::FUN_BINDING_COMMAND_EXECUTE, + reinterpret_cast(&CommandExecute_h), + reinterpret_cast(&CommandExecute_o)}; + +} // namespace + +} // namespace Bindings::Api diff --git a/src/frame/Attributes.cpp b/src/frame/Attributes.cpp index 4dc5dc5..fd72343 100644 --- a/src/frame/Attributes.cpp +++ b/src/frame/Attributes.cpp @@ -98,6 +98,7 @@ #include "Offsets.h" #include "baselib/Ascii.h" #include "cursor/Info.h" +#include "macro/Execute.h" #include "object/Resolve.h" #include "spell/AtCursor.h" #include "spell/AtUnit.h" @@ -299,78 +300,6 @@ bool CallTableFunc2Str(void *L, const char *table, const char *method, return called; } -// ---- macrotext execution (stock ChatEdit_ParseText, no addon dependency) ---- -// -// 1.12 has no `RunMacroText`/`RunMacro` global — those are addon shims (pfUI -// wraps `ChatEdit_ParseText`; SuperCleveRoidMacros ships its own `RunMacro`). -// To run macro text without depending on an addon, we replicate pfUI's shim in -// C: build a throwaway "edit box" whose `GetText` returns the line and whose -// every other method is a harmless no-op (via an `__index` metamethod), then -// hand it to the stock FrameXML `ChatEdit_ParseText(editBox, 1)` — the same -// path the real chat box uses to dispatch a slash command / send a line. - -// GetText: returns upvalue(1), the captured line. -int __fastcall MacroGetText_c(void *L) { - Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1)); - return 1; -} - -// A no-op standing in for any edit-box method the parser happens to call. -int __fastcall MacroNoop_c(void *) { return 0; } - -// __index(tab, key): hand back the no-op so `editBox:AnyMethod()` is safe. -int __fastcall MacroIndex_c(void *L) { - Game::Lua::PushCClosure(L, &MacroNoop_c, 0); - return 1; -} - -// Runs a single macro line through the stock chat parser. -void RunMacroLineC(void *L, const char *line, size_t len) { - if (len == 0) return; - const int top = Game::Lua::GetTop(L); - - Game::Lua::NewTable(L); // fake editBox - const int obj = Game::Lua::GetTop(L); - - Game::Lua::PushString(L, "GetText"); // obj.GetText = closure over line - Game::Lua::PushLString(L, line, static_cast(len)); - Game::Lua::PushCClosure(L, &MacroGetText_c, 1); - Game::Lua::SetTable(L, obj); - - Game::Lua::NewTable(L); // metatable { __index = noop } - const int mt = Game::Lua::GetTop(L); - Game::Lua::PushString(L, "__index"); - Game::Lua::PushCClosure(L, &MacroIndex_c, 0); - Game::Lua::SetTable(L, mt); - - // No lua_setmetatable binding — use the Lua global. - if (Game::Lua::PushGlobalFunction(L, "setmetatable")) { - Game::Lua::PushValue(L, obj); - Game::Lua::PushValue(L, mt); - Game::Lua::Call(L, 2, 0); - } - - if (Game::Lua::PushGlobalFunction(L, "ChatEdit_ParseText")) { - Game::Lua::PushValue(L, obj); - Game::Lua::PushNumber(L, 1); // send = 1 - Game::Lua::Call(L, 2, 0); - } - - Game::Lua::SetTop(L, top); -} - -// Runs macro text one line at a time — a real macro is line-delimited, and a -// single ChatEdit_ParseText call only dispatches one command. -void RunMacroTextC(void *L, const char *text) { - if (!text) return; - for (const char *p = text; *p;) { - const char *nl = p; - while (*nl && *nl != '\n') ++nl; - RunMacroLineC(L, p, static_cast(nl - p)); - p = (*nl == '\n') ? nl + 1 : nl; - } -} - // ---- mouse-focus poll (drives the native mouseover slot) ------------------- // frame CFrameScriptObject* → current `unit` token, for the mouse-focus poll. @@ -676,16 +605,14 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix, // (which is what SuperCleveRoidMacros ships) can't interpret text and // would silently resolve nothing. if (ReadModAttr(L, fi, prefix, "macrotext", suffix, macro, sizeof macro)) { - RunMacroTextC(L, macro); + Macro::Execute::Text(L, macro); handled = true; } // Deprecated `macro` form: a saved-macro name/index. Prefer an // addon-provided RunMacro (SuperCleveRoidMacros, pfUI, …) to run the // named macro's body; fall back to the stock parser when none exists. else if (ReadModAttr(L, fi, prefix, "macro", suffix, macro, sizeof macro)) { - if (!Game::Lua::CallGlobalString(L, "RunMacro", macro)) - RunMacroTextC(L, macro); - handled = true; + handled = Macro::Execute::Saved(L, macro); } // Restore the target the click swapped away from. `FUN_TARGET_BY_GUID` // validates the GUID resolves to a live unit and bails otherwise, so a diff --git a/src/macro/Execute.cpp b/src/macro/Execute.cpp new file mode 100644 index 0000000..7cd6be1 --- /dev/null +++ b/src/macro/Execute.cpp @@ -0,0 +1,196 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// Vanilla 1.12 has no RunMacro or RunMacroText global. Raw macro text is +// dispatched one line at a time through FrameXML's ChatEdit_ParseText using a +// small throwaway edit-box table. Saved macros are resolved with the stock +// GetMacroIndexByName/GetMacroInfo globals before entering that same path. + +#include "macro/Execute.h" + +#include "Game.h" + +#include +#include + +namespace Macro::Execute { + +namespace { + +int __fastcall Script_GetText(void *L) { + Game::Lua::PushValue(L, Game::Lua::UpvalueIndex(1)); + return 1; +} + +int __fastcall Script_Noop(void *) { return 0; } + +int __fastcall Script_Index(void *L) { + Game::Lua::PushCClosure(L, &Script_Noop, 0); + return 1; +} + +void Line(void *L, const char *line, size_t length) { + if (length == 0) + return; + if (line[length - 1] == '\r') + --length; + if (length == 0) + return; + + const int top = Game::Lua::GetTop(L); + + Game::Lua::NewTable(L); + const int objectIndex = Game::Lua::GetTop(L); + + Game::Lua::PushString(L, "GetText"); + Game::Lua::PushLString(L, line, static_cast(length)); + Game::Lua::PushCClosure(L, &Script_GetText, 1); + Game::Lua::SetTable(L, objectIndex); + + Game::Lua::NewTable(L); + const int metatableIndex = Game::Lua::GetTop(L); + Game::Lua::PushString(L, "__index"); + Game::Lua::PushCClosure(L, &Script_Index, 0); + Game::Lua::SetTable(L, metatableIndex); + + if (Game::Lua::PushGlobalFunction(L, "setmetatable")) { + Game::Lua::PushValue(L, objectIndex); + Game::Lua::PushValue(L, metatableIndex); + Game::Lua::PCall(L, 2, 0, 0); + } + + if (Game::Lua::PushGlobalFunction(L, "ChatEdit_ParseText")) { + Game::Lua::PushValue(L, objectIndex); + Game::Lua::PushNumber(L, 1); + Game::Lua::PCall(L, 2, 0, 0); + } + + Game::Lua::SetTop(L, top); +} + +bool ParseIndex(const char *value, int *index) { + if (value == nullptr || *value == '\0') + return false; + + int result = 0; + for (const char *p = value; *p != '\0'; ++p) { + if (*p < '0' || *p > '9') + return false; + + const int digit = *p - '0'; + if (result > (0x7FFFFFFF - digit) / 10) + return false; + + result = result * 10 + digit; + } + + if (result <= 0) + return false; + + *index = result; + return true; +} + +bool CallRunMacro(void *L, const char *macro) { + const int top = Game::Lua::GetTop(L); + if (!Game::Lua::PushGlobalFunction(L, "RunMacro")) { + Game::Lua::SetTop(L, top); + return false; + } + + int index = 0; + if (ParseIndex(macro, &index)) + Game::Lua::PushNumber(L, index); + else + Game::Lua::PushString(L, macro); + + const bool ok = Game::Lua::PCall(L, 1, 0, 0) == 0; + Game::Lua::SetTop(L, top); + + return ok; +} + +int ResolveIndex(void *L, const char *macro) { + int index = 0; + if (ParseIndex(macro, &index)) + return index; + + const int top = Game::Lua::GetTop(L); + if (!Game::Lua::PushGlobalFunction(L, "GetMacroIndexByName")) { + Game::Lua::SetTop(L, top); + return 0; + } + + Game::Lua::PushString(L, macro); + if (Game::Lua::PCall(L, 1, 1, 0) == 0 && Game::Lua::IsNumber(L, -1)) + index = static_cast(Game::Lua::ToNumber(L, -1)); + + Game::Lua::SetTop(L, top); + + return index; +} + +bool GetBody(void *L, int index, std::string *body) { + const int top = Game::Lua::GetTop(L); + if (!Game::Lua::PushGlobalFunction(L, "GetMacroInfo")) { + Game::Lua::SetTop(L, top); + return false; + } + + Game::Lua::PushNumber(L, index); + const bool called = Game::Lua::PCall(L, 1, 3, 0) == 0; + const bool found = called && Game::Lua::IsString(L, -1); + if (found) + *body = Game::Lua::ToString(L, -1); + + Game::Lua::SetTop(L, top); + + return found; +} + +} // namespace + +void Text(void *L, const char *text) { + if (L == nullptr || text == nullptr) + return; + + for (const char *line = text; *line != '\0';) { + const char *end = line; + while (*end != '\0' && *end != '\n') + ++end; + Line(L, line, static_cast(end - line)); + line = *end == '\n' ? end + 1 : end; + } +} + +bool Saved(void *L, const char *macro) { + if (L == nullptr || macro == nullptr || *macro == '\0') + return false; + + if (CallRunMacro(L, macro)) + return true; + + const int index = ResolveIndex(L, macro); + if (index <= 0) + return false; + + std::string body; + if (!GetBody(L, index, &body)) + return false; + + Text(L, body.c_str()); + return true; +} + +} // namespace Macro::Execute diff --git a/src/macro/Execute.h b/src/macro/Execute.h new file mode 100644 index 0000000..eb20050 --- /dev/null +++ b/src/macro/Execute.h @@ -0,0 +1,27 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +// A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +namespace Macro::Execute { + +// Runs raw macro text through vanilla FrameXML's ChatEdit_ParseText dispatcher. +void Text(void *L, const char *text); + +// Runs a saved macro by name or decimal index. Prefers an addon-provided +// RunMacro implementation when present, then falls back to GetMacroInfo plus +// the stock text dispatcher. +bool Saved(void *L, const char *macro); + +} // namespace Macro::Execute