Skip to content

Contexts and sides

Lesson 4.4

Every Minecraft game has two halves, and the game does its work on several threads. Knowing which code runs where saves a lot of confusion. This lesson explains the two sides and the contexts your code is called in.

Two sides: server and client

  • The server owns the world. It moves mobs, grows crops, decides what happens when a block breaks, and saves everything.
  • The client is the game window: it draws the world, plays sounds, reads the keyboard and mouse, and shows menus.

On a multiplayer server, the server is another computer and every player has a client. In singleplayer, both halves run inside your game: the client and a small built-in server. Your mod is loaded once and gets the events of both.

You play… Server events Client events
Singleplayer yes yes
On a server that has your mod on the server in your game
On a server without your mod no yes

That last row is why client-only mods exist: a HUD, a keybind or a sound tweak works on any server, even a vanilla one.

Which functions belong to which side

Most functions belong to one side. The header says so above each function: Side: server, Side: client or Side: both.

Server only Client only
cl_player_give, cl_player_send_message, cl_entity_teleport cl_hud_layer_register
cl_level_set_block cl_screen_open, cl_widget_*
cl_cmd_register, cl_cmd_run cl_keybind_register
cl_menu_open cl_sound_play_ui, cl_music_play
cl_sched_run_server, cl_sched_repeat_server cl_sched_run_client
cl_server_players, cl_server_broadcast, cl_server_overworld cl_client_player, cl_client_level

Calling one on the wrong side returns CL_E_WRONG_SIDE and changes nothing. Reading, like cl_entity_position or cl_level_get_block, works on both.

The client can’t change the world by itself. To ask the server to do something (“give me a ruby, I pressed R”), the client sends a message. Payloads and buffers shows how.

Asking where you are

if (cl_environment() == CL_ENV_CLIENT) {
    // a game with a window (it may also run a singleplayer server)
}
if (cl_environment() == CL_ENV_DEDICATED_SERVER) {
    // a server without a window
}
if (cl_server_running()) {
    // a world runs in this process
}

cl_core_has(CL_STR("side.client")) answers the first question too, and cl_core_has(CL_STR("gui")) is false on a dedicated server, which has no window.

The usual pattern

Put client setup into the lifecycle.client_setup event. It only runs in a game with a window, so a dedicated server skips it, and the same mod works in both places:

static void on_client_setup(void* payload, void* user) {
    (void)payload;
    (void)user;
    CL_LOG("window ready");
    // register keybinds and HUD layers here
}

static void on_server_started(void* payload, void* user) {
    (void)payload;
    (void)user;
    CL_LOG("server ready");
}

static cl_result mod_main(void) {
    cl_result r = CL_ON(CL_EV_LIFECYCLE_CLIENT_SETUP, on_client_setup);
    if (r == CL_OK) r = CL_ON(CL_EV_LIFECYCLE_SERVER_STARTED, on_server_started);
    return r;
}

Contexts: where your code is called

The game calls your code from different places, and each one has rules. That place is the context:

Context What it is Your code runs here in
CL_CTX_MAIN Startup and setup, before or between worlds. mod_main, lifecycle.* events, server.command_register
CL_CTX_TICK The game’s tick: 20 times a second, on the server’s thread or the client’s. tick events, player, block and entity events, commands, timers, payload handlers
CL_CTX_RENDER Drawing a frame. Runs as often as the screen refreshes. HUD layers, screen drawing, key and chat events on the client
CL_CTX_ANY Safe anywhere. (a function attribute, not a place)

The header tells you each function’s context: Context: TICK means “call this from tick-time code”. The most important rules:

  • Change the world only in tick code on the server: events, commands, timers. Never from render code.
  • Register content and commands in setup code: lifecycle.register for items and blocks, server.command_register for commands, lifecycle.client_setup for keybinds and HUD layers.
  • Render code only draws. It runs every frame, maybe 300 times a second. Work out values in a tick and only draw them in render.

cl_current_context() tells you where you are, if you ever need to check.

No threads of your own

A WebAssembly mod has exactly one thread of its own: none. The game calls into your mod, one call at a time, and your mod returns. You can’t start threads, and you don’t need locks: two of your callbacks never run at the same moment.

That also means your callbacks must return quickly. While your code runs, the game waits. A callback that runs longer than about 2 seconds is stopped, and the mod is turned off (see Debugging). For big jobs, do a slice per tick with a timer.

Try it

You want a mod that (a) logs “server tick 100 reached” on the server’s 100th tick and (b) logs “client is ready” once the game window is set up. Which events do you use, and in which context does each handler run?

Show a solution
static void on_server_tick(void* payload, void* user) {
    (void)user;
    cl_ev_tick* e = CL_EVENT(cl_ev_tick, payload);
    if (e->tick_count == 100) CL_LOG("server tick 100 reached");
}

static void on_client_setup(void* payload, void* user) {
    (void)payload;
    (void)user;
    CL_LOG("client is ready");
}

static cl_result mod_main(void) {
    cl_result r = CL_ON(CL_EV_TICK_SERVER_POST, on_server_tick);
    if (r == CL_OK) r = CL_ON(CL_EV_LIFECYCLE_CLIENT_SETUP, on_client_setup);
    return r;
}

on_server_tick runs in the TICK context on the server, on_client_setup in the MAIN context on the client. On a dedicated server, only the first line ever appears.

Recap: the server owns the world and the client draws it, functions are marked with their side and context, client setup goes into lifecycle.client_setup, and callbacks run one at a time and must return quickly.