|
| 1 | +// This file is part of ClassicAPI. |
| 2 | +// |
| 3 | +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms |
| 4 | +// of the GNU General Public License as published by the Free Software Foundation, either |
| 5 | +// version 3 of the License, or (at your option) any later version. |
| 6 | +// |
| 7 | +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY |
| 8 | +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR |
| 9 | +// PURPOSE. See the GNU General Public License for more details. |
| 10 | +// |
| 11 | +// You should have received a copy of the GNU General Public License along with |
| 12 | +// ClassicAPI. If not, see <https://www.gnu.org/licenses/>. |
| 13 | + |
| 14 | +// Modern positional script arguments — make every frame-script handler |
| 15 | +// receive `(self, arg1..argN)` the way later clients do, on top of the |
| 16 | +// vanilla `this`/`arg1`/`event` globals. |
| 17 | +// |
| 18 | +// frame:SetScript("OnMouseWheel", function() print(this, arg1) end) -- vanilla |
| 19 | +// frame:SetScript("OnMouseWheel", function(self, delta) print(self, delta) end) -- modern |
| 20 | +// |
| 21 | +// Vanilla ALWAYS invokes a script handler with ZERO Lua arguments: the two |
| 22 | +// runners the engine funnels every dispatch through — FUN_FRAME_INVOKE_SCRIPT |
| 23 | +// (no-arg scripts) and FUN_FRAME_RUN_SCRIPT_ARGS (scripts with values) — set |
| 24 | +// the `this`/`arg1..argN` globals and then `lua_pcall(handler, /*nargs*/0)`. |
| 25 | +// So `function(self, delta)` gets nothing today. |
| 26 | +// |
| 27 | +// We co-hook both runners and REPLACE their pcall tail: keep setting the same |
| 28 | +// globals (so vanilla handlers reading `this`/`arg1` keep working — this is |
| 29 | +// purely additive; Lua silently drops the extra positional args a zero-param |
| 30 | +// handler doesn't declare), but push `self` + `arg1..argN` as real arguments |
| 31 | +// and `pcall` with `nargs = 1 + N`. The reimplementation mirrors the engine's |
| 32 | +// exact global save/set/restore and its message-handler errfunc; only the |
| 33 | +// final push+pcall differs. |
| 34 | +// |
| 35 | +// Convention: `(self, arg1..argN)` for every script, plus `event` inserted as |
| 36 | +// the first positional for OnEvent — `(self, event, arg1..argN)` — matching |
| 37 | +// retail (and Frame::Modern's HookScript). An OnEvent dispatch is detected |
| 38 | +// structurally at the runner: the handler ref being invoked equals the frame's |
| 39 | +// OnEvent slot handler (frame+OFF_FRAME_ONEVENT_SLOT). This is exact — each |
| 40 | +// SetScript creates a distinct ref, so a function bound to two scripts still |
| 41 | +// differs per slot — so no fragile "are we mid event-dispatch" flag is needed. |
| 42 | +// |
| 43 | +// Runtime switch (default ON): `SetModernScriptArgs(false)` / (true), |
| 44 | +// `GetModernScriptArgs()`. While disabled the hook is a straight passthrough |
| 45 | +// to the original runner (zero reimplementation risk), so it can be toggled |
| 46 | +// safely per session — worth knowing, since this lands on the hottest Lua path |
| 47 | +// in the engine (FUN_FRAME_RUN_SCRIPT_ARGS fires for every OnUpdate frame). |
| 48 | + |
| 49 | +#include "Game.h" |
| 50 | +#include "Offsets.h" |
| 51 | + |
| 52 | +#include <cstdint> |
| 53 | + |
| 54 | +namespace Frame::ScriptArgs { |
| 55 | + |
| 56 | +namespace { |
| 57 | + |
| 58 | +// Runtime switch. Default ON. When false, both detours tail-call the original |
| 59 | +// runner unchanged (exact vanilla behavior, no reimplementation risk). |
| 60 | +bool g_enabled = true; |
| 61 | + |
| 62 | +// Engine cap: the runner stops setting arg globals at index 19 (arg1..arg19). |
| 63 | +constexpr int kMaxArgs = 19; |
| 64 | + |
| 65 | +// --- raw engine primitives not exposed via Game::Lua ----------------------- |
| 66 | +// lua_rawgeti(L, idx, n) — push table_at_idx[n]. Used with idx = REGISTRY to |
| 67 | +// push a value stored under an integer ref (handler / frame object / message |
| 68 | +// handler / luaL_ref'd saved globals). |
| 69 | +using RawGetI_t = void(__fastcall *)(void *L, int idx, int ref); |
| 70 | +// luaL_ref(L, t) → int — pop the top, store it under a fresh int key in table |
| 71 | +// `t`, return the key. luaL_unref frees it. |
| 72 | +using LuaLRef_t = int(__fastcall *)(void *L, int t); |
| 73 | +using LuaLUnref_t = void(__fastcall *)(void *L, int t, int ref); |
| 74 | +// FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER — __thiscall(frame, nameOrNull); |
| 75 | +// lazily materializes the frame's Lua-registry ref at frame+OFF_COBJECT_LUA_REF. |
| 76 | +using ScriptRegister_t = void(__thiscall *)(void *frame, void *nameOrNull); |
| 77 | + |
| 78 | +const auto RawGetI = reinterpret_cast<RawGetI_t>(Offsets::LUA_RAWGETI); |
| 79 | +const auto LuaLRef = reinterpret_cast<LuaLRef_t>(Offsets::LUA_REF_REF); |
| 80 | +const auto LuaLUnref = reinterpret_cast<LuaLUnref_t>(Offsets::LUA_REF_UNREF); |
| 81 | + |
| 82 | +// Returns the frame's Lua object ref (frame+0x08), lazily creating it exactly |
| 83 | +// as the engine runner does (`if (refcount == 0) ScriptRegister(frame, 0)`). |
| 84 | +int EnsureLuaRef(void *frame) { |
| 85 | + auto *f = reinterpret_cast<uint8_t *>(frame); |
| 86 | + if (*reinterpret_cast<int *>(f + Offsets::OFF_COBJECT_LUA_REFCOUNT) == 0) |
| 87 | + reinterpret_cast<ScriptRegister_t>( |
| 88 | + Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)(frame, nullptr); |
| 89 | + return *reinterpret_cast<int *>(f + Offsets::OFF_COBJECT_LUA_REF); |
| 90 | +} |
| 91 | + |
| 92 | +// Writes "arg" + k (k in 1..19) into buf; buf must hold at least 6 bytes. |
| 93 | +void ArgName(char *buf, int k) { |
| 94 | + buf[0] = 'a'; |
| 95 | + buf[1] = 'r'; |
| 96 | + buf[2] = 'g'; |
| 97 | + if (k < 10) { |
| 98 | + buf[3] = static_cast<char>('0' + k); |
| 99 | + buf[4] = '\0'; |
| 100 | + } else { |
| 101 | + buf[3] = static_cast<char>('0' + k / 10); |
| 102 | + buf[4] = static_cast<char>('0' + k % 10); |
| 103 | + buf[5] = '\0'; |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +// Sets _G[name] = <value currently on the stack top>, capturing the previous |
| 108 | +// _G[name] into *outOldRef (a luaL_ref for later restore). Stack-neutral: |
| 109 | +// consumes the value, leaves the stack as it was minus that value. |
| 110 | +void SetGlobalSaving(void *L, const char *name, int *outOldRef) { |
| 111 | + using namespace Game::Lua; |
| 112 | + PushString(L, name); // [.., newVal, name] |
| 113 | + GetTable(L, GLOBALS_INDEX); // [.., newVal, oldVal] |
| 114 | + *outOldRef = LuaLRef(L, REGISTRY_INDEX); // [.., newVal] |
| 115 | + PushString(L, name); // [.., newVal, name] |
| 116 | + Insert(L, -2); // [.., name, newVal] |
| 117 | + SetTable(L, GLOBALS_INDEX); // [..] |
| 118 | +} |
| 119 | + |
| 120 | +// Restores _G[name] to the value at oldRef, then frees the ref. |
| 121 | +void RestoreGlobal(void *L, const char *name, int oldRef) { |
| 122 | + using namespace Game::Lua; |
| 123 | + RawGetI(L, REGISTRY_INDEX, oldRef); // [.., oldVal] |
| 124 | + PushString(L, name); // [.., oldVal, name] |
| 125 | + Insert(L, -2); // [.., name, oldVal] |
| 126 | + SetTable(L, GLOBALS_INDEX); // [..] |
| 127 | + LuaLUnref(L, REGISTRY_INDEX, oldRef); |
| 128 | +} |
| 129 | + |
| 130 | +// Reimplements the runner tail: set globals (with save/restore), then invoke |
| 131 | +// the handler with modern positional args `(self, arg1..argN)` under the |
| 132 | +// engine's own message-handler errfunc. `fmt`/`vaPtr` are null for the no-arg |
| 133 | +// runner (N == 0). |
| 134 | +void RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr) { |
| 135 | + using namespace Game::Lua; |
| 136 | + void *L = State(); |
| 137 | + if (L == nullptr) |
| 138 | + return; |
| 139 | + |
| 140 | + // --- _G.this = frame ----------------------------------------------------- |
| 141 | + const bool haveThis = frame != nullptr; |
| 142 | + // OnEvent dispatch? The event dispatchers invoke the frame's OnEvent slot |
| 143 | + // (frame+0x0C); matching the handler ref against that slot identifies it |
| 144 | + // exactly. When so, `event` (a global the dispatcher has set) is prepended |
| 145 | + // as the first positional to mirror retail's (self, event, arg1..argN). |
| 146 | + const bool isOnEvent = |
| 147 | + haveThis && |
| 148 | + *reinterpret_cast<const int *>(reinterpret_cast<uint8_t *>(frame) + |
| 149 | + Offsets::OFF_FRAME_ONEVENT_SLOT) == |
| 150 | + handlerRef; |
| 151 | + int selfRef = 0; |
| 152 | + int oldThisRef = 0; |
| 153 | + if (haveThis) { |
| 154 | + selfRef = EnsureLuaRef(frame); |
| 155 | + RawGetI(L, REGISTRY_INDEX, selfRef); // push frame object |
| 156 | + SetGlobalSaving(L, "this", &oldThisRef); |
| 157 | + } |
| 158 | + |
| 159 | + // --- _G.arg1..argN from the format -------------------------------------- |
| 160 | + int oldArgRefs[kMaxArgs]; |
| 161 | + char argNames[kMaxArgs][6]; |
| 162 | + int n = 0; |
| 163 | + if (fmt != nullptr) { |
| 164 | + const char *cur = static_cast<const char *>(vaPtr); |
| 165 | + for (const char *p = fmt; *p != '\0' && n < kMaxArgs; ++p) { |
| 166 | + if (*p != '%') |
| 167 | + continue; |
| 168 | + ++p; |
| 169 | + if (*p == '\0') |
| 170 | + break; |
| 171 | + bool consumed = true; |
| 172 | + switch (*p) { |
| 173 | + case 'd': |
| 174 | + PushNumber(L, static_cast<double>(*reinterpret_cast<const int *>(cur))); |
| 175 | + cur += 4; |
| 176 | + break; |
| 177 | + case 'u': |
| 178 | + PushNumber(L, static_cast<double>( |
| 179 | + *reinterpret_cast<const unsigned int *>(cur))); |
| 180 | + cur += 4; |
| 181 | + break; |
| 182 | + case 'f': |
| 183 | + PushNumber(L, *reinterpret_cast<const double *>(cur)); |
| 184 | + cur += 8; |
| 185 | + break; |
| 186 | + case 's': |
| 187 | + PushString(L, *reinterpret_cast<const char *const *>(cur)); |
| 188 | + cur += 4; |
| 189 | + break; |
| 190 | + default: |
| 191 | + consumed = false; // unknown spec — skip, consume nothing |
| 192 | + break; |
| 193 | + } |
| 194 | + if (!consumed) |
| 195 | + continue; |
| 196 | + ArgName(argNames[n], n + 1); |
| 197 | + SetGlobalSaving(L, argNames[n], &oldArgRefs[n]); |
| 198 | + ++n; |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + // --- call handler(self, arg1..argN) ------------------------------------- |
| 203 | + const int base = GetTop(L); |
| 204 | + const int errRef = *reinterpret_cast<const int *>( |
| 205 | + static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF)); |
| 206 | + int errIdx = 0; |
| 207 | + if (errRef > 0) { |
| 208 | + RawGetI(L, REGISTRY_INDEX, errRef); // message handler |
| 209 | + errIdx = base + 1; // absolute index of the errfunc |
| 210 | + } |
| 211 | + RawGetI(L, REGISTRY_INDEX, handlerRef); // handler |
| 212 | + if (haveThis) |
| 213 | + RawGetI(L, REGISTRY_INDEX, selfRef); // self |
| 214 | + else |
| 215 | + PushNil(L); |
| 216 | + if (isOnEvent) { // event goes before the args, retail-style |
| 217 | + PushString(L, "event"); |
| 218 | + GetTable(L, GLOBALS_INDEX); |
| 219 | + } |
| 220 | + for (int k = 0; k < n; ++k) { // positional args, re-read from the globals |
| 221 | + PushString(L, argNames[k]); |
| 222 | + GetTable(L, GLOBALS_INDEX); |
| 223 | + } |
| 224 | + PCall(L, 1 + (isOnEvent ? 1 : 0) + n, 0, errIdx); |
| 225 | + SetTop(L, base); // drop the message handler + any leftover error object |
| 226 | + |
| 227 | + // --- restore globals (reverse order) ------------------------------------ |
| 228 | + for (int k = n - 1; k >= 0; --k) |
| 229 | + RestoreGlobal(L, argNames[k], oldArgRefs[k]); |
| 230 | + if (haveThis) |
| 231 | + RestoreGlobal(L, "this", oldThisRef); |
| 232 | +} |
| 233 | + |
| 234 | +// --- FUN_FRAME_RUN_SCRIPT_ARGS co-hook (scripts with values) ---------------- |
| 235 | +using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt, |
| 236 | + const void *vaPtr); |
| 237 | +RunArgs_t g_origRunArgs = nullptr; |
| 238 | + |
| 239 | +void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt, |
| 240 | + const void *vaPtr) { |
| 241 | + if (!g_enabled || handlerRef == 0 || fmt == nullptr) { |
| 242 | + g_origRunArgs(handlerRef, frame, fmt, vaPtr); |
| 243 | + return; |
| 244 | + } |
| 245 | + RunModern(handlerRef, frame, fmt, vaPtr); |
| 246 | +} |
| 247 | + |
| 248 | +// --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) ----------------------- |
| 249 | +// Same address Tooltip::SetEvents calls directly to fire OnTooltipSet*; with |
| 250 | +// the switch enabled those fire with (self) too, which is the modern shape. |
| 251 | +using Invoke_t = void(__fastcall *)(int handlerRef, void *frame); |
| 252 | +Invoke_t g_origInvoke = nullptr; |
| 253 | + |
| 254 | +void __fastcall Invoke_h(int handlerRef, void *frame) { |
| 255 | + if (!g_enabled || handlerRef == 0 || frame == nullptr) { |
| 256 | + g_origInvoke(handlerRef, frame); |
| 257 | + return; |
| 258 | + } |
| 259 | + RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr); |
| 260 | +} |
| 261 | + |
| 262 | +const Game::HookAutoRegister _runArgsHook{ |
| 263 | + Offsets::FUN_FRAME_RUN_SCRIPT_ARGS, |
| 264 | + reinterpret_cast<void *>(&RunArgs_h), |
| 265 | + reinterpret_cast<void **>(&g_origRunArgs)}; |
| 266 | + |
| 267 | +const Game::HookAutoRegister _invokeHook{ |
| 268 | + Offsets::FUN_FRAME_INVOKE_SCRIPT, |
| 269 | + reinterpret_cast<void *>(&Invoke_h), |
| 270 | + reinterpret_cast<void **>(&g_origInvoke)}; |
| 271 | + |
| 272 | +// --- Lua toggle ------------------------------------------------------------- |
| 273 | +int __fastcall Script_SetModernScriptArgs(void *L) { |
| 274 | + g_enabled = Game::Lua::ToBoolean(L, 1) != 0; |
| 275 | + Game::Lua::PushBool(L, g_enabled); |
| 276 | + return 1; |
| 277 | +} |
| 278 | + |
| 279 | +int __fastcall Script_GetModernScriptArgs(void *L) { |
| 280 | + Game::Lua::PushBool(L, g_enabled); |
| 281 | + return 1; |
| 282 | +} |
| 283 | + |
| 284 | +void RegisterLuaFunctions() { |
| 285 | + Game::Lua::RegisterGlobalFunction("SetModernScriptArgs", |
| 286 | + &Script_SetModernScriptArgs); |
| 287 | + Game::Lua::RegisterGlobalFunction("GetModernScriptArgs", |
| 288 | + &Script_GetModernScriptArgs); |
| 289 | +} |
| 290 | + |
| 291 | +const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; |
| 292 | + |
| 293 | +} // namespace |
| 294 | + |
| 295 | +} // namespace Frame::ScriptArgs |
0 commit comments