Live-edit your game (hot reload)
Asobi can swap your match.lua while a match is running. Players stay connected, scores stay intact, the next tick uses the new code. This tutorial walks through editing a behaviour mid-match and watching it take effect without reconnecting.
What you need
- A running Asobi server. The simplest path is the server quickstart — Docker Compose, ~2 minutes.
- A terminal with
wscator any WebSocket client. - A text editor pointed at the mounted game directory.
1. Start with a minimal match
Create game/match.lua with one tick callback that just stamps a counter into state:
function init(state)
state.counter = 0
state.message = "hello"
return state
end
function tick(state)
state.counter = state.counter + 1
return state
end
function get_state(state, player_id)
return { counter = state.counter, message = state.message }
endRestart Asobi (Compose: docker compose restart asobi). This is the only restart you need; everything from here on is hot.
2. Connect a client
Open a WebSocket and join a match:
wscat -c ws://localhost:8080/ws
> {"type":"session.connect","payload":{"token":"<token>"}}
> {"type":"matchmaker.add","payload":{"mode":"hello"}}
> # ... matchmaker.matched arrives, then match.state every tickYou should see match.state frames where counter climbs each tick and message stays at "hello".
3. Edit the script with the match still running
Without touching the WebSocket, edit game/match.lua so tick updates the message:
function tick(state)
state.counter = state.counter + 1
state.message = "tick " .. tostring(state.counter)
return state
endSave the file. Asobi's loader picks up the change on the next reload poll. Within a second or two your wscat stream's message field starts rising with the counter — no reconnect, no lost players, no lost score.
What just happened
Asobi's Lua loader asobi_lua_loader caches each script's bytecode in a per-match Luerl state. When the file mtime advances, the next callback re-compiles into a fresh state, copies the existing state table across, and continues. The previous code keeps running for any in-flight callback to avoid mid-tick swaps.
This means data structures must round-trip through the bridge. Adding a new field to the state map is fine; introducing a Luerl userdata that the new code can't decode is not. Treat hot reload as a code-only path: schema changes still want a deploy.
Caveats
- A syntax error in the new file is rejected; the match keeps running with the old code. The error appears in the structured log with a
reload_failedevent. - The compiler error list is truncated to three entries to avoid blowing up the log pipeline if you accidentally save a binary file under the game dir.
- Mid-callback rollback after a
game.economy.debitis best-effort — see Lua known limitations for the rationale. - Hot reload picks up changes per-script; a
require'd module that changes is reloaded on nextrequire, not retroactively for existing matches. Force the reload by clearing_ASOBI_LOADEDor restarting the match.
What's next
- game.* API reference
- Lua callbacks — all the entry points you can hot-reload.
- Lua sandbox