Mod lifecycle
Status: Implemented. Every
lifecycle.*event on this page fires. Runtime dimensions are Planned.
A mod passes through fixed phases. Each phase is announced by a lifecycle.* event, and some APIs are only valid in certain phases. Calling them at the wrong time returns CL_E_WRONG_PHASE.
Startup order
discover ─► resolve ─► load code ─► construct ─► register ─► registries_frozen
(loader) (loader) (loader) clight_mod_main (ids assigned)
│
┌────────────────────────────────────────────┘
▼
common_setup ─► client_setup (physical client)
│ server_setup (dedicated server)
▼
resources_reloaded (client) / data_reloaded (server)
▼
server_starting ─► level_load (per dimension) ─► server_started
▼
... game runs: ticks, events ...
▼
server_stopping ─► level_save / level_unload ─► server_stopped
▼
shutdown
| Phase / event | Thread | What to do here | Allowed APIs (beyond logging, config and services) |
|---|---|---|---|
| discover / resolve | loader | Nothing. The loader reads manifests, checks versions and dependencies, and sorts the load order. | n/a |
construct (clight_mod_main, the Lua entry script) |
main | Subscribe to events, attach mixins, open config. Do not touch the world. | cl_event_*, cl_mixin_*, cl_config_* |
lifecycle.register |
main | Register content: blocks, items, payload channels, keybinds (client). | cl_block_register, cl_item_register, cl_payload_register, cl_keybind_register |
lifecycle.registries_frozen |
main | Registries are frozen and numeric ids are assigned. Read raw ids from your holders. | cl_holder_value, read-only registry queries |
lifecycle.common_setup |
main | Logic that needs the final ids on both sides. | all non-world APIs |
lifecycle.client_setup |
client main | HUD layers, keybinds, client-only subscriptions. | client APIs |
lifecycle.server_setup |
server main | Dedicated-server setup. | server APIs |
lifecycle.resources_reloaded |
client main | Fires after every resource-pack reload (F3+T, pack screen). Rebuild caches that depend on assets. | client APIs |
lifecycle.data_reloaded |
server main | Fires after /reload or a data-pack change. Recipes, tags and loot are fresh. |
server APIs |
lifecycle.server_starting |
server main | The server object exists, but levels are not loaded yet. | server APIs |
lifecycle.level_load |
server main | Once per level (dimension). | world APIs on that level |
lifecycle.server_started |
server main | The world is ready and ticking is about to begin. | all |
lifecycle.level_save |
server tick | Autosave or /save-all. Flush your saved data. |
world APIs |
lifecycle.server_stopping |
server main | Stop your tasks and kick external connections. | all |
lifecycle.level_unload |
server main | Once per level. | world APIs on that level |
lifecycle.server_stopped |
server main | Fires in the client too, when leaving a singleplayer world. Clear per-world state. | non-world APIs |
lifecycle.mod_reload |
main | After a hot reload that worked (dev mode, Lua) (Hot reload). | as in common_setup |
lifecycle.shutdown |
main | The process is exiting. Save what you need. | logging, config |
In singleplayer the server phases repeat every time a world is opened. Your mod must handle server_starting → server_stopped several times in one process.
The entry point
Every code mod exports one entry function. The SDK provides a macro (C/C++) or a convention (Lua), so you never write the raw export.
// C: clight_mod_main is generated by the macro. It binds the API table and calls your function.
static cl_result my_main(void) {
return CL_ON(CL_EV_LIFECYCLE_REGISTER, on_register); // clight_easy.h
}
CLIGHT_MOD_ENTRY(my_main)
// C++
class RubyTools final : public clight::Mod {
public:
void on_construct() override; // = construct phase
void on_register() override; // = lifecycle.register
void on_client_setup() override; // optional overrides for each phase
};
CLIGHT_MOD(RubyTools)
-- Lua: the entry script (manifest runtime.entry) runs top to bottom in the construct phase.
cl.on("lifecycle.register", function() ... end)
A non-CL_OK return from the entry function (or an error in Lua, or a WebAssembly trap) disables the mod and shows the error on the Mods screen (CLL-3007). The game keeps loading.
Unload
WASM mods can export clight_mod_unload (C CLIGHT_MOD_UNLOAD(fn), C++ Mod::on_unload; Lua uses cl.on("lifecycle.shutdown", ...)). It runs at shutdown. Every subscription, mixin, task, HUD layer, screen and keybind the mod owns is released by the engine afterwards, so unload only needs to free your own resources.
Phase queries
| C | C++ | Lua |
|---|---|---|
cl_phase cl_lifecycle_phase(void) |
clight::mods::lifecycle_phase() |
cl.mod.lifecycle_phase() ("construct", "register", "frozen", "setup", "running", "shutdown") |
bool cl_registries_frozen(void) |
clight::registry::registries_frozen() |
cl.registry.registries_frozen() |
bool cl_server_running(void) |
clight::mods::server_running() |
cl.mod.server_running() |