Hot reload
Status: Implemented for Lua mods (per file) and WebAssembly mods (the whole module, with state hand-over, API 1.20) in dev mode, and for resources and data through the vanilla reloads.
/clight reload <mod>and automatic resource reloads on save are Planned.
Hot reload replaces a mod’s code while the game keeps running, with the world still loaded. It is a development feature: it is on under clight run (--clight-dev), or for a mod with "hot_reload": true in its manifest, and off for players otherwise.
What triggers a reload
| Trigger | Reloads |
|---|---|
Saving a Lua script while clight run is running |
That file and the files that require it (checked every half second) |
A new .wasm while clight run is running (clight build in another terminal) |
The whole WebAssembly module, once the file stopped changing (checked every half second) |
Editing a config file, /clight reload-config |
The mod’s config files (config.reloaded) |
/reload |
Data packs, including every mod’s datapack/ |
| F3 + T, the pack screen | Resources, only the changed files |
Lua: what happens
- The changed files are found by their time stamps; the files that (transitively)
requirethem are added. - Those files run again, top to bottom, in the existing VM. Other files keep their state.
- Subscriptions, commands, HUD layers and other registrations made by the re-run files are replaced by the ones the new code makes.
- One-shot events that already happened (
lifecycle.register,lifecycle.client_setup,server.command_register, …) are replayed to the handlers the reload added, so commands, HUD layers, keybinds and payload handlers come back with the new code. lifecycle.mod_reloadfires when the reload worked.
If a file fails to load (a syntax error), the reload is dropped, the old code keeps running, and the error is logged. The next save tries again.
WebAssembly: what happens
- The changed
.wasmis compiled and instantiated in a new sandbox. If that fails (a build error left a broken file, the API version is too new), nothing changes and the log says why. - The old module’s state is handed over: the engine calls its
clight_mod_save_state(buf, cap)export (twice: the size, then the bytes) and the new module’sclight_mod_restore_state(data, len), before the newclight_mod_main. Both exports are optional. - The old module’s
clight_mod_unloadruns, and everything it registered at run time goes: event subscriptions, scheduled tasks, commands, keybinds, payload handlers, HUD layers, config handles, render effects, emitters, mixins and HTTP requests. - The new module’s
clight_mod_mainruns and registers again. The one-shot setup events that already happened (lifecycle.common_setup,lifecycle.client_setup/server_setup, and while a server runslifecycle.server_starting,lifecycle.server_started,server.command_register) are replayed to it, so commands and the rest come back. - If the new main fails or traps, the reload is rolled back: the new module’s registrations go and the old module’s main runs again, with its memory as it was.
lifecycle.mod_reloadfires.
typedef struct { int32_t score; int32_t level; } Game;
static Game game;
CLIGHT_MOD_HOT_STATE(game) /* the struct moves to the new module (dropped if its size changed) */
For a state with pointers, write your own pair: CLIGHT_MOD_SAVE_STATE(fn) with uint32_t fn(void* buf, uint32_t cap) (return the size; write when buf is big enough) and CLIGHT_MOD_RESTORE_STATE(fn) with cl_result fn(const void* data, uint32_t len). cl_core_has("hot_reload.wasm") is true when the WebAssembly runtime is there.
Blocks, items, entity types, particle types and goal types are frozen with the registries: their behaviour keeps calling the old module (whose memory stays alive until the game closes) until a restart. Each reload keeps that old module’s memory, so restart once in a while during a long session.
What survives
-
mod.persist: a table that survives reloads (not restarts). Keep session state there:mod.persist.arenas = mod.persist.arenas or {} local arenas = mod.persist.arenas -
Top-level
locals of a re-run file are created again; those of other files stay. -
Behaviour functions of blocks and items, keybinds and payload handlers go through stable trampolines, so the newest function runs without registering again.
What can’t be reloaded
Blocks and items are frozen with the registries. A reload can’t add or change them: the old registrations stay, and the log says “restart required”. Manifest changes (id, side, permissions, depends) need a restart too.
| Change | Hot-reloadable (Lua and WebAssembly)? |
|---|---|
| Handler bodies, command logic, GUI code, HUD drawing | yes |
| Adding an event subscription, command, HUD layer | yes |
| Block and item behaviour function bodies | Lua: yes; WebAssembly: no (they keep the old code until a restart) |
| New blocks or items, changed descriptor fields | no (restart) |
| Config options | yes (the file is re-read) |
Data (datapack/) |
yes, /reload |
| Assets | yes, F3 + T |
Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
State in a top-level local |
Lost on every save | Keep it in mod.persist |
| Relying on reload for registration changes | “restart required” in the log, content unchanged | Restart the game |
| Testing only with hot reload | Bugs on a fresh start (ordering, missing setup) | Do a full restart before releasing |
Planned
/clight reload <mod>, watching assets/ and datapack/ during clight run and reloading them automatically.