Docs / Tutorials / Live-edit your game

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 wscat or 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 }
end

Restart 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 tick

You 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
end

Save 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_failed event.
  • 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.debit is 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 next require, not retroactively for existing matches. Force the reload by clearing _ASOBI_LOADED or restarting the match.

What's next