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. clight.h says so above each C function: Side: server, Side: client or Side: both. The wrapper methods have the same side as the C function they call.
| Server only | Client only |
|---|---|
player.give, player.send_message, player.teleport |
clight::hud::add_layer, clight::hud::layer_register |
level.set_block |
clight::screen::open and the widgets |
clight::cmd::add, clight::cmd::run |
cl_keybind_register |
clight::menu::open |
clight::sound::play_ui, clight::sound::music_play |
clight::sched::run_server, clight::sched::repeat_server |
clight::sched::run_client |
clight::entity::server_players, clight::broadcast, clight::level::server_overworld |
clight::entity::client_player, clight::level::client_level |
Calling one on the wrong side fails with CL_E_WRONG_SIDE and changes nothing. Reading, like player.position() or level.get_block(pos), 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 (clight::mods::environment() == CL_ENV_CLIENT) {
// a game with a window (it may also run a singleplayer server)
}
if (clight::mods::environment() == CL_ENV_DEDICATED_SERVER) {
// a server without a window
}
if (clight::mods::server_running()) {
// a world runs in this process
}
clight::core::has("side.client") answers the first question too, and clight::core::has("gui") is false on a dedicated server, which has no window.
The usual pattern
Put client setup into on_client_setup. It only runs in a game with a window, so a dedicated server skips it, and the same mod works in both places:
class SidesMod final : public clight::Mod {
public:
void on_client_setup() override {
clight::log::info("window ready");
// register keybinds and HUD layers here
}
void on_construct() override {
clight::on<clight::ev::LifecycleServerStarted>([](cl_ev_lifecycle&) {
clight::log::info("server ready");
}).detach();
}
};
clight::Mod has functions for the most common steps. For the others, like server_started, subscribe to the lifecycle event itself, as above. A lambda parameter without a name (cl_ev_lifecycle&) is fine when you don’t need it.
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. | on_construct, on_register, the other clight::Mod functions, 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) |
clight.h 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:
on_registerfor items and blocks,server.command_registerfor commands,on_client_setupfor 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.
clight::mods::current_context() tells you where you are, if you ever need to check.
No threads of your own
A WebAssembly mod has no threads of its own. The game calls into your mod, one call at a time, and your mod returns. You can’t start threads (std::thread compiles, but starting one fails and ends your callback), 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 or functions do you use, and in which context does each one run?
Show a solution
#include <clight.hpp>
class TickMod final : public clight::Mod {
public:
void on_construct() override {
clight::on<clight::ev::TickServerPost>([](cl_ev_tick& e) {
if (e.tick_count == 100) clight::log::info("server tick 100 reached");
}).detach();
}
void on_client_setup() override {
clight::log::info("client is ready");
}
};
CLIGHT_MOD(TickMod)
The tick lambda 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 on_client_setup, and callbacks run one at a time and must return quickly.