Skip to content

Debugging

Lesson 11.2

Every mod has bugs at first. The trick is finding them fast. This lesson shows how to read the game’s messages about WASM mods, what happens when a mod crashes or hangs, and the mistakes almost everybody makes once.

The mod doesn’t load

When the game can’t load your mod, the log says why, with a code:

[clight] mod 'my_mod' not loaded: CLL-3007: clight_mod_main returned -6
Message Means Fix
CLL-3001: entry 'bin/my_mod.wasm' not found The .wasm file isn’t where the manifest says. Run clight build, and check runtime.entry (see The manifest).
CLL-3003: missing CLight exports The module has no CLIGHT_MOD_ENTRY, or wasn’t linked with the SDK’s flags. Add CLIGHT_MOD_ENTRY(mod_main) to one file; build with clight build.
CLL-3004: ... The module needs something the sandbox doesn’t have, like an unknown import. Don’t use libraries that need threads, sockets or other system functions.
CLL-2004: built for CLight 2.x The headers are from another major API version. Build with the SDK that came with this game.
CLL-2003: needs CLight 1.3 The headers are newer than the player’s game. The player needs a newer Celestial, or you build with the older SDK.
CLL-3007: clight_mod_main returned -6 Your mod_main returned an error code. The number is the CL_E_ code; -6 is CL_E_NOT_FOUND. Log your setup steps to find which one failed.
CLL-3007: clight_mod_main trapped Your mod_main crashed. See “Traps” below.

Traps: when C code crashes

In a normal program, a bad pointer or a division by zero crashes the program. In the sandbox, it traps: the game stops your callback, writes it to the log, and goes on. The game doesn’t crash, and other mods keep running.

[my_mod] callback trapped: wasm trap: out of bounds memory access

The most common causes:

  • A null or wild pointer: reading e->... with a wrong CL_EVENT type, a pointer to a local variable that is already gone, a pointer into freed memory.
  • Running off the end of an array or the stack, see Memory in the sandbox.
  • Integer division by zero (x / 0 with whole numbers) and __builtin_trap() / abort().
  • A failed assert, which calls abort().

In a development build (plain clight build) the module keeps your function names, so when the message comes with a backtrace, it names them. Read it from the top: the first line that names one of your functions is usually where it went wrong. When there is no backtrace, log lines (below) narrow it down quickly.

The error limit

One trap is forgiven. But if your mod traps 10 times, the game turns it off for the rest of the session, so a broken mod can’t flood the log:

[clight] mod 'my_mod' disabled: CLL-3007: too many traps; last: wasm trap: out of bounds memory access

The Mods screen shows the reason too. Fix the bug, rebuild and restart. A trap in a tick or render callback reaches 10 within a second, because those run so often.

Timeouts: when C code hangs

A callback that never returns, like an endless while loop, would freeze the whole game. The sandbox watches the time: mod_main may run for up to 20 seconds, every other callback for about 2 seconds. Then the call is stopped, and because a hanging mod is never safe to call again, it is turned off at once:

[clight] mod 'my_mod' disabled: CLL-3007: a callback ran too long (infinite loop?)

If your mod really has a lot of work, spread it over ticks with a timer (see Timers and tasks). 2 seconds is a hard limit, but players notice lag long before that: aim for well under a millisecond per callback.

API calls that fail

Failing API calls don’t trap; they return an error code (see Errors). In development mode, the game also logs most misuses for you:

[clight] [my_mod] cl_level_set_block failed with -10

When something “just doesn’t work”, look for these lines first.

Logging to find a bug

The simplest debugger is a log line. Put CL_LOG at the start of a handler to see whether it runs at all, and log the values you are unsure about:

static void on_block_break(void* payload, void* user) {
    (void)user;
    cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
    CL_LOGF(CL_LOG_DEBUG, "break at %d %d %d, state %u, player %s",
            e->pos.x, e->pos.y, e->pos.z, (unsigned)e->state, CL_IS_NULL(e->player) ? "none" : "yes");
}

A small macro switches debug logging off for release builds. clight build --release defines NDEBUG, like most C compilers:

#ifdef NDEBUG
#define DBG(...) ((void)0)
#else
#define DBG(...) CL_LOGF(CL_LOG_DEBUG, __VA_ARGS__)
#endif

assert from <assert.h> works the same way: it checks in development builds and disappears in release builds. A failed assert traps, with the condition in the log.

Common mistakes

Nothing happens at all. Is the mod loaded? Look for its loaded line or a not loaded line in the log, or open the Mods screen. Did mod_main subscribe to the right event, and did you rebuild and restart? C mods don’t reload while the game runs.

The handler runs but reads garbage. The CL_EVENT type doesn’t match the event. CL_EV_BLOCK_PLACE is cl_ev_block_place, not cl_ev_block_break. Check The event list.

A name or message is cut off, or has strange characters at the end. A cl_str from the game has no \0. Print it with %.*s, or copy it first. Or your buffer was too small: check for CL_E_BUFFER_TOO_SMALL.

CL_E_INVALID_ARGUMENT from a register function. The descriptor didn’t start from its _INIT macro, so its size field is 0. Or a required field (an id, a callback) is missing.

CL_E_NOT_FOUND from cl_easy_give or a lookup. A typo in an id. Ids are lowercase, with the namespace in front: minecraft:diamond, not Diamond.

CL_E_WRONG_PHASE when registering an item. Items and blocks must be registered in CL_EV_LIFECYCLE_REGISTER, not in mod_main or later.

CL_E_WRONG_SIDE. A client function on the server or the other way round. Client code belongs in CL_EV_LIFECYCLE_CLIENT_SETUP and client events.

A crash a while after the event. Something kept the payload pointer or a cl_str from it, and used it after the handler returned. Copy what you need.

Memory that only grows. Owned texts, stacks or buffers that are never released, or malloc without free. Check every path out of a function.

Keeping mods fast

Players notice lag before they notice bugs. A few habits keep a mod fast:

  • Do heavy work in ticks, not in render, and not every tick if every second is enough.
  • Look up ids once (cl_block_lookup, cl_item_lookup) and keep the result in a static variable.
  • Unsubscribe from events you no longer need.
  • Spread big jobs over several ticks.
  • Publish release builds: they are smaller and faster.

cl_time_now_ns() measures how long something takes:

uint64_t t0 = cl_time_now_ns();
glass_floor(player);
CL_LOGF(CL_LOG_DEBUG, "glass_floor took %.3f ms", (cl_time_now_ns() - t0) / 1e6);

Try it

This handler should give every new player a map, but the game logs a trap every time a player joins. Find the bug.

static void on_join(void* payload, void* user) {
    cl_ev_player* e = (cl_ev_player*)user;
    cl_easy_give(e->player, "minecraft:map", 1);
}
Show a solution

The handler reads the event from user instead of payload. user is the pointer passed when subscribing, which is NULL with CL_ON, so e->player reads through a null pointer and traps.

static void on_join(void* payload, void* user) {
    (void)user;
    cl_entity player = CL_EVENT(cl_ev_player, payload)->player;
    cl_easy_give(player, "minecraft:map", 1);
}

Using CL_EVENT(type, payload) every time makes this mistake hard to make.

Recap: load failures come with a CLL- code, crashes trap and are logged (10 traps turn the mod off), a callback that runs longer than about 2 seconds turns it off at once, and CL_LOG plus development builds find most bugs.