Skip to content

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()