fix(lua): install game.* before script eval so handle_input closures capture it#44
Merged
Conversation
…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
This was referenced May 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
_ENVat compile time.asobi_lua_api:install/2was called AFTER the script chunk was evaluated, so closures captured a_Gwithoutgame.*.handle_inputbecause everything else goes throughbounded_eval(call/4).handle_inputusescall/3per ADR 0002, exposing the gap —_G.gamewas nil there, silently breakinggame.notify,game.storage,game.broadcast,game.zone.spawn,game.spatial.*, andgame.logfrom the most common server-auth callback.asobi_lua_loader:new/3with aPreInstall :: fun((St) -> St)hook that runs between sandbox setup andluerl:do(scriptCode, St). The fourloader:newcall sites inasobi_lua_worldandasobi_lua_matchnow passfun(St) -> asobi_lua_api:install(Ctx, St) endsogame.*exists before any closure captures_ENV.new/1andnew/2are kept (delegate tonew/3with identity PreInstall) so existing callers are unaffected. A script'sgenerate_worldcallback can now also usegame.*(it couldn't before — same bug, just nobody had tried).Test plan
rebar3 fmt --checkcleanrebar3 xrefcleanrebar3 dialyzerclean~/bin/elp eqwalize-all— no new errors vs main (baseline: 4 in loader, 1 in world)~/bin/elp lint— no new warnings vs mainrebar3 eunit— 209/209 pass (was 202; +7 regression tests)rebar3 ct— 8/8 pass_G.gamevisibility 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_testsadds anew/3pre/no-pre control pair that probes a script function's view of a host-injected globalgame.*, so new function definitions capture itFollow-up (not in this PR)
logger:error/warningfunction calls insrc/lua/should migrate to?LOG_*macros per project convention. Every offending line is on unchanged source; bundling here would muddy the bisect.