Skip to content

Commit 880fbfb

Browse files
committed
frame: pass modern positional args to script handlers
Vanilla always invokes frame-script handlers with zero Lua args; the handler reads this/arg1/event as globals. Modern addon ports write SetScript("OnMouseWheel", function(self, delta) ...) and get nothing. Co-hook both script runners -- FUN_FRAME_INVOKE_SCRIPT (no-arg scripts) and FUN_FRAME_RUN_SCRIPT_ARGS (scripts with values) -- and reimplement their pcall tail: keep the engine's exact this/argN global save-set- restore and message-handler errfunc, but additionally push self + arg1..argN as real positional arguments and pcall with nargs = 1+N. Purely additive: a zero-param vanilla handler ignores the extras and reads globals; a modern (self, delta) handler binds them. OnEvent gets (self, event, arg1..argN), detected structurally by matching the invoked handler ref against the frame OnEvent slot (frame+0x0C) -- exact, since each SetScript makes a distinct ref. Runtime switch SetModernScriptArgs()/GetModernScriptArgs(), default on; when off both detours are a straight passthrough to the original runner, which matters because FUN_FRAME_RUN_SCRIPT_ARGS is the hottest Lua path in the engine (every OnUpdate frame). Verified in-game across OnMouseWheel/OnClick/OnValueChanged/OnUpdate/OnEnter/OnLeave/OnEvent.
1 parent 6405f4b commit 880fbfb

2 files changed

Lines changed: 343 additions & 0 deletions

File tree

src/Offsets.h

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,39 @@ enum Offsets {
140140
// and call the inner invoker, which is self-contained.
141141
FUN_GAMETOOLTIP_SCRIPT_RESOLVER = 0x005295D0,
142142
FUN_FRAME_INVOKE_SCRIPT = 0x00704D50,
143+
// The arg-passing sibling of FUN_FRAME_INVOKE_SCRIPT — every frame
144+
// script that carries values (OnClick(button), OnMouseWheel(delta),
145+
// OnUpdate(elapsed), OnValueChanged(value), OnKeyDown(key), OnEvent's
146+
// args, …) funnels through here. __cdecl(int handlerRef, void *frame,
147+
// const char *fmt, void *vaPtr): fmt is a printf-style spec string
148+
// (`%d`/`%u`/`%f`/`%s`), vaPtr points at the packed vararg buffer
149+
// (4-byte stride for d/u/s, 8 for f). It sets the `arg1..argN` globals
150+
// from the format (saving/restoring the previous values), sets the
151+
// `this` global to `frame`, then runs the handler under a protected
152+
// lua_pcall with **zero Lua args** and the engine error handler
153+
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF) as errfunc. FUN_FRAME_INVOKE_SCRIPT
154+
// is the same shape for the no-arg scripts (OnShow/OnHide/OnEnter/…).
155+
// `Frame::ScriptArgs` co-hooks both to additionally pass the handler
156+
// modern positional args (self, arg1..argN). The frame's own Lua object
157+
// ref lives at frame+OFF_COBJECT_LUA_REF (refcount at
158+
// OFF_COBJECT_LUA_REFCOUNT); FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER
159+
// lazily creates it when the refcount is 0.
160+
FUN_FRAME_RUN_SCRIPT_ARGS = 0x00704F10,
161+
// The base ScriptObject's OnEvent handler slot — an 8-byte
162+
// {handler, context} pair at frame+0x0C (from FUN_00702590, the base
163+
// resolver every frame-type resolver chains through, and confirmed by
164+
// the event dispatchers FUN_00703E50 / FUN_00703F50 passing `frame+0xC`
165+
// as the slot). `*(int*)(frame + OFF_FRAME_ONEVENT_SLOT)` is the OnEvent
166+
// handler ref; `Frame::ScriptArgs` compares it against the handler ref
167+
// the runner is invoking to detect an OnEvent dispatch (exact — each
168+
// SetScript makes a distinct ref, so a function bound to two scripts
169+
// still differs per slot) and prepend the `event` positional.
170+
OFF_FRAME_ONEVENT_SLOT = 0x0C,
171+
// Registry ref (an int, not a pointer) to the frame-script message
172+
// handler the engine passes as the `errfunc` to every script pcall —
173+
// read as `*(int*)VAR_FRAMESCRIPT_ERROR_HANDLER_REF`, pushed via
174+
// lua_rawgeti(REGISTRY, ref). Set once at engine init.
175+
VAR_FRAMESCRIPT_ERROR_HANDLER_REF = 0x008722C8,
143176
// The base Frame script-name resolver — __thiscall(frame, const char *name)
144177
// -> int* slot, 0 for an unknown name. Maps the standard base-frame scripts
145178
// to their 8-byte {handler, context} slots on the frame (OnLoad@+0x118,
@@ -423,6 +456,12 @@ enum Offsets {
423456
// probe — equivalent to checking `this+0x08 > 0` but more direct.
424457
OFF_COBJECT_LUA_REFCOUNT = 0x04,
425458

459+
// `this+0x08` — the CObject's Lua-registry ref (an int key into the
460+
// registry table). `lua_rawgeti(REGISTRY, this[+0x08])` pushes the
461+
// frame's Lua-side object. Lazily populated by ScriptRegister when the
462+
// refcount above is 0.
463+
OFF_COBJECT_LUA_REF = 0x08,
464+
426465
// Direct cvar lookup — `__fastcall(const char *name) → CVar* | NULL`.
427466
// Hash-table by-name lookup over the CVar registry; same call
428467
// `Script_GetCVar` makes internally before the engine wraps the
@@ -4092,6 +4131,15 @@ enum Offsets {
40924131
LUA_NEW_TABLE = 0x6F3C90,
40934132
LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset)
40944133
LUA_RAW_GET = 0x6F3B00,
4134+
// `lua_rawgeti(L, idx, n)` — pushes `table_at_idx[n]` without invoking
4135+
// metamethods. __fastcall(L /*ecx*/, idx /*edx*/, n /*stack*/). The
4136+
// frame-script runner uses it with `idx = REGISTRY` to push a value
4137+
// stored under an integer ref (handler, frame object, message handler,
4138+
// and luaL_ref'd saved globals). Note it also carries the WoW
4139+
// frame-script exec-context stamp (the `DAT_00ceeac0` dance) for pushed
4140+
// CObjects — which is exactly why the engine pushes handler/frame
4141+
// through it rather than a plain rawget.
4142+
LUA_RAWGETI = 0x6F3BC0,
40954143
LUA_SET_TABLE = 0x6F3E20,
40964144
LUA_RAW_SET = 0x6F3EA0,
40974145
LUA_INSERT = 0x6F31A0,

src/frame/ScriptArgs.cpp

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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

Comments
 (0)