Skip to content

Config

Lesson 8.3

Players like to adjust mods: a different reward, a feature turned off, a longer cooldown. A config file lets them do that without touching your code. CLight keeps one small JSON file per mod in the game’s config folder.

Defining options

Open the config once, keep the handle, and define each option with a default:

static cl_config g_cfg;

static cl_result setup_config(void) {
    cl_result r = cl_config_open(CL_CONFIG_COMMON, &g_cfg);
    if (r != CL_OK) return r;
    cl_config_define_bool(g_cfg, CL_STR("welcome.enabled"), true);
    cl_config_define_str(g_cfg, CL_STR("welcome.text"), CL_STR("Welcome to the server!"));
    cl_config_define_i32(g_cfg, CL_STR("rubies.on_join"), 1, 0, 64);
    cl_config_define_f64(g_cfg, CL_STR("rubies.chance"), 0.25, 0.0, 1.0);
    return CL_OK;
}

Call setup_config() from mod_main. The first time, this creates config/my_mod.json in the Celestial folder:

{
  "rubies.chance": 0.25,
  "rubies.on_join": 1,
  "welcome.enabled": true,
  "welcome.text": "Welcome to the server!"
}

The names are paths with dots, so related options sort together. Defining an option that is already in the file keeps the player’s value; only missing options get the default and are written to the file straight away. A value in the file that is the wrong type or outside min and max is replaced by the default.

Function Option type
cl_config_define_bool(cfg, path, def) true or false
cl_config_define_i32(cfg, path, def, min, max) A whole number between min and max
cl_config_define_f64(cfg, path, def, min, max) A number between min and max
cl_config_define_str(cfg, path, def) A string

Reading options

static void on_player_join(void* payload, void* user) {
    (void)user;
    cl_entity p = CL_EVENT(cl_ev_player, payload)->player;

    if (cl_config_get_bool(g_cfg, CL_STR("welcome.enabled"))) {
        char text[256];
        size_t len = 0;
        cl_config_get_str(g_cfg, CL_STR("welcome.text"), text, sizeof text, &len);
        cl_easy_send_message(p, text);
    }
    int32_t n = cl_config_get_i32(g_cfg, CL_STR("rubies.on_join"));
    if (n > 0) cl_easy_give(p, "my_mod:ruby", n);
}

Use the getter that matches the type: cl_config_get_bool, _i32, _f64, and cl_config_get_str with the usual buffer pattern. Read the value when you need it, as here, rather than copying it once at the start. Then a changed value (below) is used straight away.

Changing options

cl_config_set_i32(g_cfg, CL_STR("rubies.on_join"), 3);
cl_config_save(g_cfg);

cl_config_set_* changes the value in memory; cl_config_save writes the file. Values outside the min and max you defined are refused with CL_E_INVALID_ARGUMENT.

A good place for this is an admin command, for example /myconfig rubies 3 with permission_level = 2.

Three files

cl_config_open(scope, &cfg) takes one of three scopes. Each has its own file:

Scope File For
CL_CONFIG_COMMON config/my_mod.json Most options.
CL_CONFIG_CLIENT config/my_mod-client.json Options about this player’s screen and sound, like the HUD position.
CL_CONFIG_SERVER config/my_mod-server.json Rules for the world, like rewards.

The scopes only decide the file name; a server doesn’t send its config to players.

When players edit the file

CLight notices when a player edits the file (below).

Your own C code can’t open the config folder with fopen: the sandbox only lets a mod see its own folder (see Memory in the sandbox). The config functions are the way in.

The config screen and reloads

Players don’t have to edit the file. In the Mods screen they pick your mod and press Config: a toggle for each bool, a slider for numbers with a range, a text box for strings. The label is your language entry config.<mod id>.<path>, or the path written nicely. cl_config_comment(g_cfg, CL_STR("rubies.on_join"), CL_STR("Rubies every player gets when they join")) adds a tooltip.

The values can change while the game runs: from that screen, when a player edits the file (CLight reads it again within about a second), with the operator command /clight reload-config, or cl_config_reload(g_cfg). Getters always read the current value. To react to a change, listen to config.reloaded; it fires once per changed file:

static void on_config(void* payload, void* user) {
    const cl_ev_config_reloaded* e = CL_EVENT(cl_ev_config_reloaded, payload);
    (void)user;
    CL_LOG("%.*s changed", (int)e->file.len, e->file.ptr);
}
// in mod_main, after setup_config():
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.filter = cl_ident_intern(CL_STR("my_mod"));   // only this mod's files
cl_event_subscribe(CL_EV_CONFIG_RELOADED, &o, on_config, NULL, NULL);

Try it

Add a config option join_message.color (a whole number, default 0x55FF55, from 0 to 0xFFFFFF) and use it as the colour of a welcome message.

Show a solution
#include <clight_easy.h>

static cl_config g_cfg;

static void on_player_join(void* payload, void* user) {
    (void)user;
    cl_entity p = CL_EVENT(cl_ev_player, payload)->player;
    char name[64], line[128];
    cl_easy_player_name(p, name, sizeof name);
    snprintf(line, sizeof line, "Welcome, %s!", name);

    cl_text msg = cl_text_literal(CL_STR_C(line));
    cl_text_color(msg, (cl_color)cl_config_get_i32(g_cfg, CL_STR("join_message.color")));
    cl_player_send_message(p, msg);
    cl_text_release(msg);
}

static cl_result mod_main(void) {
    cl_result r = cl_config_open(CL_CONFIG_COMMON, &g_cfg);
    if (r == CL_OK) r = cl_config_define_i32(g_cfg, CL_STR("join_message.color"), 0x55FF55, 0, 0xFFFFFF);
    if (r == CL_OK) r = CL_ON(CL_EV_PLAYER_JOIN, on_player_join);
    return r;
}

CLIGHT_MOD_ENTRY(mod_main)

In the file the colour appears as a plain number (5635925 for 0x55FF55), because JSON has no hex numbers. Players can write any colour as a decimal number there.

Recap: cl_config_open(CL_CONFIG_COMMON, &cfg) opens your mod’s config file, cl_config_define_* sets options with defaults and limits, cl_config_get_* reads them, and cl_config_set_* plus cl_config_save changes them.