Edit a view and a simulation method of a C# game while onegame run runs, watch each edit land through the hot_reload live-control tool, see a rude edit restart the game with its world kept and a compile error leave it running, and know what hot reload does to determinism.
New in 1.9.0. In a Debug build, onegame run starts the game under dotnet watch and applies each saved C# edit to the running program: same tick, same entities, same state. There is nothing to set up. Everything below ran on onegame 1.15.2 from NuGet, on the scaffold from onegame new kiln with no changes, headless and driven over live control. The windowed run behaves the same way and wasn’t run for this page.
Start it
onegame run # a window; edit Game.cs and save onegame run -- --headless --live /tmp/onegame/kiln.sock # what this page runs: no window, stepped over live control onegame run --no-hot # without hot reload
hot reload on: edit code and save to see it in the running game (--no-hot turns it off; see docs/hot-reload.md). listening on /tmp/onegame/kiln.sock (MCP, newline-delimited JSON-RPC); connect with: onegame mcp --socket /tmp/onegame/kiln.sock
Batch runs (--frames, --capture, --scenario, --replay, --json), onegame test and --release builds don’t use it.
The calls below come from the MCP client in Drive a running game over MCP, pointed at that socket, with the 200-character cut taken out. First hold the move action right and run a second:
hot_reload {} -> {"active":true,"applied":0,"pending":false,"lastTypes":[],"lastTick":-1,"state":"idle","restarts":0,"resumed":false,"replaySafe":true} actions {"name": "move", "x": 1, "y": 0} -> {"contexts":[{"name":"play","active":true,"screens":[]}],"actions":[…]} step {"frames": 60} -> {"frame":60,"tick":60,"hash":"f56b8fc0a8be9351"} state_query {"entity": "Ship"} -> {"id":1,"type":"Ship","X":90,"Y":0,"Dx":1,"Dy":0,"Boost":false}
Edit a view
In Game.cs, change the ship’s label and save:
[View] public string Label() => $"ship at {X.ToInt()}, {Y.ToInt()}"; // was $"x {X.ToInt()} y {Y.ToInt()}"
The terminal says hot reload: edit applied; the world is unchanged., and without a single step the label is the new one:
hot_reload {} -> {"active":true,"applied":1,"pending":false,"lastTypes":["Kiln.Ship"],"lastTick":60,"state":"applied","restarts":0,"resumed":false,"replaySafe":false} hash {} -> {"frame":60,"tick":60,"hash":"f56b8fc0a8be9351"} ui_query {"selector": "#pos"} -> [{"kind":"Label","id":"pos","text":"ship at 90, 0",…}]
Same tick, same hash. A script that edits a file waits for applied to grow, then reads state or captures a frame.
Edit the simulation
Make the cruising speed 2 instead of 1.5 in OnTick:
var speed = Boost ? Fixed.FromInt(3) : Fixed.FromInt(2); // was Fixed.FromRatio(3, 2)hot_reload {} -> {"active":true,"applied":2,"pending":false,"lastTypes":["Kiln.Ship"],"lastTick":60,"state":"applied","restarts":0,"resumed":false,"replaySafe":false} step {"frames": 60} -> {"frame":120,"tick":120,"hash":"a5e448439a95200f"} state_query {"entity": "Ship"} -> {"id":1,"type":"Ship","X":210,"Y":0,"Dx":1,"Dy":0,"Boost":false}
90 units in the first second, 120 in the next: the new code runs from the next tick on.
A compile error
Delete the semicolon at the end of that line and save. The terminal prints:
hot reload: the edit does not compile; the game keeps running the last good code. hot reload: ; expected [CS1002, Game.cs(26)]
hot_reload {} -> {"active":true,"applied":2,"pending":false,"lastTypes":["Kiln.Ship"],"lastTick":60,"state":"error","message":"The edit does not compile, so the running game is unchanged: ; expected [CS1002, Game.cs(26)]","restarts":0,"resumed":false,"replaySafe":false} step {"frames": 60} -> {"frame":180,"tick":180,"hash":"47059480daee55bf"} state_query {"entity": "Ship"} -> {"id":1,"type":"Ship","X":330,"Y":0,"Dx":1,"Dy":0,"Boost":false}
The game keeps stepping. Put the semicolon back and it says hot reload: the code compiles again and matches what is running.
A rude edit restarts with the world kept
Some edits can’t be applied to a running program: changing the type of a synced member, renaming or removing one, or changing a method’s signature on a loaded type. Rename the synced Boost property to Boosting everywhere in Game.cs and save:
hot reload: this edit cannot be applied to the running game: Renaming field '__p_Boost' requires restarting the application [ENC0020, generated code of Kiln_Ship, from a change to its synced members, commands or views]. hot reload: world saved; restarting and loading it. listening on /tmp/onegame/kiln.sock (MCP, newline-delimited JSON-RPC); connect with: onegame mcp --socket /tmp/onegame/kiln.sock
Reconnect to the same socket:
hot_reload {} -> {"active":true,"applied":0,"pending":false,"lastTypes":[],"lastTick":-1,"state":"restarted","message":"Restarted after an edit that could not be applied live (Renaming field '__p_Boost' requires restarting the application [ENC0020, generated code of Kiln_Ship, from a change to its synced members, commands or views]).","restarts":1,"resumed":true,"replaySafe":true} hash {} -> {"frame":0,"tick":180,"hash":"47059480daee55bf"} state_query {"entity": "Ship"} -> {"id":1,"type":"Ship","X":330,"Y":0,"Dx":1,"Dy":0,"Boosting":false}
The new process loaded the world the old one saved: tick 180, the same hash, the ship at 330. Its frame count starts again at 0, and applied counts this process’s edits only.
Determinism
Hot reload is for iterating, not for recording facts. The session above ended at tick 180 with the ship at 330. The same input from a clean start, with the final code, ends somewhere else:
scenarios/cruise.yaml
name: cruise description: Hold right for 180 ticks from a clean start, as the hot session did. steps: - action: { name: move, state: down, x: 1, y: 0 } - step: 180 expect: - state: { entity: Ship, property: X, equals: 360 }
onegame test --scenario scenarios/cruise.yamlPASS cruise (37 ms, tick 180, hash f2118bbfde6c48e2) 1 passed, 0 failed
It gives the same hash every time; the hot session’s 47059480daee55bf came from two versions of the code and can’t be reproduced. That is what replaySafe: false means. A session recording that runs across an edit is cut into a new segment at the edit. For a recording or a hash you keep, run from a clean start with --no-hot or as a scenario.
Good to know
- Even a view-only edit turns
replaySafefalse: the tool doesn’t tell view edits from sim edits. - After a rude-edit restart,
replaySafereadstrueagain although the loaded world came from edited code. Go by the session, not by that flag. - The restart drops what live control was holding: here the
moveaction was released, so the next steps left the ship at 330. Hold it again after reconnecting. Live-control connections end at a restart too. - Code that ran once at start-up (
Setup,DefineActions,Style,DefineFonts,RegisterEntities) accepts edits but uses them on the next start. Ctrl+R in the terminal restarts the game. - Exports and Release builds contain none of it. Android, iOS and web runs aren’t hot.
- Cluster nodes started with
onegame clusteraren’t hot. A client or server you run withonegame runis, and if you edit its sim code, its peers on the old code report drift. That drift is expected.
More in Hot reload.