Skip to content

fix(lua): install game.* before script eval so handle_input closures capture it#44

Merged
Taure merged 1 commit into
mainfrom
fix/game-api-in-handle-input
May 14, 2026
Merged

fix(lua): install game.* before script eval so handle_input closures capture it#44
Taure merged 1 commit into
mainfrom
fix/game-api-in-handle-input

Conversation

@Taure
Copy link
Copy Markdown
Contributor

@Taure Taure commented May 14, 2026

Summary

  • Lua function closures capture _ENV at compile time. asobi_lua_api:install/2 was called AFTER the script chunk was evaluated, so closures captured a _G without game.*.
  • The bug was hidden in every callback except handle_input because everything else goes through bounded_eval (call/4). handle_input uses call/3 per ADR 0002, exposing the gap — _G.game was nil there, silently breaking game.notify, game.storage, game.broadcast, game.zone.spawn, game.spatial.*, and game.log from the most common server-auth callback.
  • Fix: add asobi_lua_loader:new/3 with a PreInstall :: fun((St) -> St) hook that runs between sandbox setup and luerl:do(scriptCode, St). The four loader:new call sites in asobi_lua_world and asobi_lua_match now pass fun(St) -> asobi_lua_api:install(Ctx, St) end so game.* exists before any closure captures _ENV.

new/1 and new/2 are kept (delegate to new/3 with identity PreInstall) so existing callers are unaffected. A script's generate_world callback can now also use game.* (it couldn't before — same bug, just nobody had tried).

Test plan

  • rebar3 fmt --check clean
  • rebar3 xref clean
  • rebar3 dialyzer clean
  • ~/bin/elp eqwalize-all — no new errors vs main (baseline: 4 in loader, 1 in world)
  • ~/bin/elp lint — no new warnings vs main
  • rebar3 eunit — 209/209 pass (was 202; +7 regression tests)
  • rebar3 ct — 8/8 pass
  • New regression tests cover _G.game visibility from every world callback (init, join, leave, post_tick, zone_tick, handle_input) and every match callback (init, join, leave, handle_input, tick)
  • asobi_lua_loader_tests adds a new/3 pre/no-pre control pair that probes a script function's view of a host-injected global
  • Hot-reload path reasoned through (unchanged) — reload re-evaluates against the existing LuaSt which already has game.*, so new function definitions capture it

Follow-up (not in this PR)

  • Pre-existing logger:error/warning function calls in src/lua/ should migrate to ?LOG_* macros per project convention. Every offending line is on unchanged source; bundling here would muddy the bisect.

…capture it

Lua function closures capture `_ENV` at compile time. `asobi_lua_api:install/2`
ran AFTER `asobi_lua_loader:new/1` evaluated the script chunk, so functions
the script defined captured a `_G` without the `game` namespace.

Every callback except `handle_input` hid the bug behind `bounded_eval`'s
spawn round-trip (`call/4`). `handle_input` uses `call/3` per ADR 0002, so
its closure-bound `_ENV` was the bare `_G` from before install — making
`game.*` (notify, storage, broadcast, zone.spawn, spatial.*, log) silently
nil for the most common server-auth callback.

Fix: add `asobi_lua_loader:new/3` with a `PreInstall :: fun((St) -> St)`
hook that runs between sandbox setup and `luerl:do(scriptCode, St)`.
`asobi_lua_world:init/1`, `asobi_lua_world:generate_world/2`,
`asobi_lua_world:inject_per_zone_lua/3`, and `asobi_lua_match:init/1`
pass `fun(St) -> asobi_lua_api:install(Ctx, St) end` so `game.*` is
populated into `_G` before any script-defined function captures its env.

`new/1` and `new/2` are kept (delegate to `new/3` with identity
PreInstall) so existing callers are unaffected.

Behaviour change: a script's `generate_world` callback can now use `game.*`,
which previously also silently saw nil. Same Ctx is installed, so all
existing scripts continue to work — the fix only extends visibility to
every closure rather than just the bounded_eval ones.

Regression coverage:
- world: init, join, leave, post_tick, zone_tick + handle_input each
  assert `_G.game` is a table and `game.id()` is callable
- match: one consolidated test across init, join, leave, handle_input,
  tick, get_state
- loader: new/3 PreInstall-runs-before-script + new/2 control pair
@Taure Taure merged commit 345c131 into main May 14, 2026
15 checks passed
@Taure Taure deleted the fix/game-api-in-handle-input branch May 14, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant