Skip to content

Commit e4ae0c9

Browse files
committed
fix(frame): only pass positional script args to handlers that declare them
Default-ON modern script args changed behavior for every handler on the engine's hottest Lua path, and the docs' "purely additive" claim was wrong: a handler that DECLARES a parameter received a real value where vanilla always passed nil. Gate the reimplemented positional path on the handler's arity: a Lua closure that declares a parameter or is vararg (function(self, delta) / function(self, ...)) takes it; a param-less handler (the vast majority of vanilla handlers, which read this/arg1 globals) and a C-closure handler tail-call the original engine runner unchanged. So param-less handlers are provably identical to vanilla AND off the hot path (also the efficiency win: OnUpdate handlers that gain nothing no longer pay the modern tail every frame). Only param-declaring handlers observe the (self, [event,] arg1..N) shape. Arity is read from the handler's Proto; offsets verified against the engine's own luaD_precall (numparams @ +0x45, is_vararg @ +0x46), lua_dump, and lua_iscfunction/lua_tocfunction (closure @ TValue+8, isC @ +6, Proto @ +0xC). The param-declaring-vanilla-handler case (a function reused as both a direct call and a handler) is inherently ambiguous and still gets the value; documented honestly in ScriptArgs.cpp, docs/API.md, and README with SetModernScriptArgs (false) as the opt-out. Verified in-game across both runners, both paths, the toggle, and the caveat.
1 parent 035a672 commit e4ae0c9

3 files changed

Lines changed: 96 additions & 17 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ Transparent engine tweaks — no API to call, they just fix a vanilla limitation
238238
| Lua 5.1 environment protection | `getfenv` / `setfenv` honor a `__environment` metatable field — the Lua 5.1 sandbox form — in addition to vanilla's raw `__fenv`. See [getfenv / setfenv environment protection](docs/API.md#getfenv--setfenv-environment-protection). |
239239
| Multi-flavor & conditional TOC loading | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. See [Conditional and multi-flavor TOC loading](docs/API.md#conditional-and-multi-flavor-toc-loading). |
240240
| SavedVariables loaded first | Honors the modern `## LoadSavedVariablesFirst` TOC directive: a flagged addon's SavedVariables load before its Lua runs, so file-scope code sees restored config (instead of vanilla's `nil`). See [SavedVariables loaded first](docs/API.md#savedvariables-loaded-first). |
241-
| Modern script-handler arguments | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — the way 5.1+ clients do, so modern addon ports work unmodified. Vanilla passes only the `this` / `arg1` globals (still set, so old handlers keep working). On by default; `SetModernScriptArgs(false)` reverts to exact vanilla dispatch. See [SetModernScriptArgs](docs/API.md#setmodernscriptargsenable--getmodernscriptargs). |
241+
| Modern script-handler arguments | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — the way 5.1+ clients do, so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. See [SetModernScriptArgs](docs/API.md#setmodernscriptargsenable--getmodernscriptargs). |
242242

243243
## Installation
244244

docs/API.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4614,11 +4614,20 @@ addon ports written as `function(self, delta) … end` work unmodified:
46144614
`OnClick(self, button)`, `OnValueChanged(self, value)`, `OnUpdate(self, elapsed)`.
46154615
- `OnEvent`: `(self, event, arg1..argN)`.
46164616

4617-
It's purely **additive** — the `this` / `arg1` / `event` globals are still set, so
4618-
vanilla-style handlers keep working; a handler declaring no parameters just ignores
4619-
the extras. `SetModernScriptArgs(enable)` sets the state and returns it;
4617+
The `this` / `arg1` / `event` globals stay set, so a handler that reads them still
4618+
works. A handler that declares no parameters is unaffected — it cannot see the
4619+
positional arguments. `SetModernScriptArgs(enable)` sets the state and returns it.
46204620
`GetModernScriptArgs()` returns the current state.
46214621

4622+
**Caveat — a parameter that was always nil now gets a value.** Vanilla passed
4623+
every handler zero arguments, so any parameter a handler declared was always nil.
4624+
When this feature is on, a declared parameter gets its real value. A modern
4625+
`function(self, delta)` handler needs this behavior. But it also changes a vanilla
4626+
handler that declared a parameter and expected it to be nil. One example is a
4627+
function used both as a direct call (with a real argument) and as a script
4628+
handler. If such a handler misbehaves, disable the feature with
4629+
`SetModernScriptArgs(false)`.
4630+
46224631
**Default ON.** Modern handler signatures are a core Lua 5.1 feature, so ports
46234632
that use them work with no setup. It reimplements the tail of the engine's
46244633
hottest Lua path (the runner that fires for every `OnUpdate`, every frame); if

src/frame/ScriptArgs.cpp

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,28 @@
2525
// So `function(self, delta)` gets nothing today.
2626
//
2727
// 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-
// message-handler errfunc, but saves the clobbered globals on the Lua stack
33-
// (not registry refs like the engine) so an out-of-memory longjmp mid-push
34-
// unwinds them for free — no leaked refs, no per-call ref churn. See RunModern.
28+
// globals (so handlers reading `this`/`arg1` keep working), but push `self` +
29+
// `arg1..argN` as real arguments and `pcall` with `nargs = 1 + N`. The
30+
// reimplementation mirrors the engine's message-handler errfunc, but saves the
31+
// clobbered globals on the Lua stack (not registry refs like the engine) so an
32+
// out-of-memory longjmp mid-push unwinds them for free — no leaked refs, no
33+
// per-call ref churn. See RunModern.
34+
//
35+
// The reimplemented path is taken ONLY for a handler that actually declares a
36+
// parameter or is vararg (checked via its Proto — see HandlerWantsArgs). A
37+
// handler that declares NO parameters can't observe positional args, so pushing
38+
// them is a no-op — those route through the original engine runner unchanged:
39+
// provably identical to vanilla, and OFF the engine's hottest Lua path (the vast
40+
// majority of vanilla handlers, which read `this`/`arg1`, are param-less). So a
41+
// param-less vanilla handler is untouched; only a param-declaring handler sees
42+
// the `(self, [event,] arg1..argN)` shape.
43+
//
44+
// Irreducible caveat: a param-declaring handler and a modern handler are
45+
// indistinguishable to the dispatcher — both are one-param Lua functions. So a
46+
// vanilla handler that declares a parameter expecting it to be nil (e.g. a
47+
// function reused as both a direct call and a handler) WILL now receive a real
48+
// value. There is no signal to tell the two apart; such a handler must opt out
49+
// per-session via `SetModernScriptArgs(false)`.
3550
//
3651
// Convention: `(self, arg1..argN)` for every script, plus `event` inserted as
3752
// the first positional for OnEvent — `(self, event, arg1..argN)` — matching
@@ -266,16 +281,68 @@ bool RunModern(int handlerRef, void *frame, const char *fmt, const void *vaPtr)
266281
return true;
267282
}
268283

284+
// Lua 5.0 internal offsets for reading a handler's arity — verified against the
285+
// engine's own luaD_precall (0x006F6050) / lua_dump (0x006F4370) /
286+
// lua_iscfunction (0x006F34A0):
287+
// lua_State: top @ +0x08 (TValue *, next free slot)
288+
// TValue: tag @ +0x00 (int); GC/closure pointer @ +0x08 (16-byte TValue)
289+
// Closure: isC byte @ +0x06; union member (C func / Proto) @ +0x0C
290+
// Proto: numparams (u8) @ +0x45, is_vararg (u8) @ +0x46
291+
// Single-use here, so kept local (like the lua_State field offsets elsewhere).
292+
constexpr uintptr_t OFF_LUASTATE_TOP = 0x08;
293+
constexpr uintptr_t OFF_TVALUE_GC = 0x08;
294+
constexpr uintptr_t OFF_CLOSURE_ISC = 0x06;
295+
constexpr uintptr_t OFF_LCLOSURE_PROTO = 0x0C;
296+
constexpr uintptr_t OFF_PROTO_NUMPARAMS = 0x45;
297+
constexpr uintptr_t OFF_PROTO_IS_VARARG = 0x46;
298+
constexpr int kLuaTFunction = 6;
299+
constexpr uintptr_t kTValueSize = 0x10;
300+
301+
// Does this handler actually observe positional args? A vanilla handler reads
302+
// this/arg1 globals and declares NO parameters, so pushing positional args to
303+
// it is observably identical to vanilla — those route through the original
304+
// engine runner instead (exact vanilla, and off the hottest Lua path). Only a
305+
// Lua closure that declares a parameter or is vararg — a modern
306+
// `function(self, delta)` / `function(self, ...)` — takes the reimplemented
307+
// positional path. A C-closure handler (HookScript's thunk, say) reads
308+
// globals/upvalues, never positional args, so it counts as "no" too.
309+
//
310+
// Reads the handler's Proto directly (the engine exposes no arity via the C
311+
// API); every read is bounded — a non-function ref, a C closure, or a null
312+
// Proto all fall through to "no".
313+
bool HandlerWantsArgs(void *L, int handlerRef) {
314+
RawGetI(L, Game::Lua::REGISTRY_INDEX, handlerRef); // push registry[handlerRef]
315+
bool wants = false;
316+
const void *tv = reinterpret_cast<const void *>(
317+
Game::Read<uintptr_t>(L, OFF_LUASTATE_TOP) - kTValueSize);
318+
if (Game::Read<int>(tv, 0) == kLuaTFunction) {
319+
const void *cl =
320+
reinterpret_cast<const void *>(Game::Read<uintptr_t>(tv, OFF_TVALUE_GC));
321+
if (cl != nullptr && Game::Read<uint8_t>(cl, OFF_CLOSURE_ISC) == 0) { // Lua closure
322+
const void *proto = reinterpret_cast<const void *>(
323+
Game::Read<uintptr_t>(cl, OFF_LCLOSURE_PROTO));
324+
if (proto != nullptr)
325+
wants = Game::Read<uint8_t>(proto, OFF_PROTO_NUMPARAMS) > 0 ||
326+
Game::Read<uint8_t>(proto, OFF_PROTO_IS_VARARG) != 0;
327+
}
328+
}
329+
Game::Lua::SetTop(L, -2); // pop the handler
330+
return wants;
331+
}
332+
269333
// --- FUN_FRAME_RUN_SCRIPT_ARGS co-hook (scripts with values) ----------------
270334
using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt,
271335
const void *vaPtr);
272336
RunArgs_t g_origRunArgs = nullptr;
273337

274338
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
275339
const void *vaPtr) {
276-
if (g_enabled && handlerRef != 0 && fmt != nullptr &&
277-
RunModern(handlerRef, frame, fmt, vaPtr))
278-
return;
340+
if (g_enabled && handlerRef != 0 && fmt != nullptr) {
341+
void *L = Game::Lua::State();
342+
if (L != nullptr && HandlerWantsArgs(L, handlerRef) &&
343+
RunModern(handlerRef, frame, fmt, vaPtr))
344+
return;
345+
}
279346
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
280347
}
281348

@@ -286,9 +353,12 @@ using Invoke_t = void(__fastcall *)(int handlerRef, void *frame);
286353
Invoke_t g_origInvoke = nullptr;
287354

288355
void __fastcall Invoke_h(int handlerRef, void *frame) {
289-
if (g_enabled && handlerRef != 0 && frame != nullptr &&
290-
RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr))
291-
return;
356+
if (g_enabled && handlerRef != 0 && frame != nullptr) {
357+
void *L = Game::Lua::State();
358+
if (L != nullptr && HandlerWantsArgs(L, handlerRef) &&
359+
RunModern(handlerRef, frame, /*fmt*/ nullptr, /*vaPtr*/ nullptr))
360+
return;
361+
}
292362
g_origInvoke(handlerRef, frame);
293363
}
294364

0 commit comments

Comments
 (0)