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 it in your mod, and define each option with a default:

class ConfigMod final : public clight::Mod {
public:
    void on_construct() override {
        auto cfg = clight::config::open(CL_CONFIG_COMMON);
        if (!cfg) {
            clight::log::error("no config: {}", cfg.error());
            return;
        }
        cfg_ = *cfg;
        cfg_.define_bool("welcome.enabled", true);
        cfg_.define_str("welcome.text", "Welcome to the server!");
        cfg_.define_i32("rubies.on_join", 1, 0, 64);
        cfg_.define_f64("rubies.chance", 0.25, 0.0, 1.0);
    }

private:
    clight::Config cfg_;
};

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.

Method Option type
cfg.define_bool(path, def) true or false
cfg.define_i32(path, def, min, max) A whole number between min and max
cfg.define_f64(path, def, min, max) A number between min and max
cfg.define_str(path, def) A string

Reading options

void on_player_join(clight::Config cfg, clight::Entity p) {
    if (cfg.get_bool("welcome.enabled")) {
        p.send_message(cfg.get_str("welcome.text").value_or("Welcome!"));
    }
    int32_t n = cfg.get_i32("rubies.on_join");
    if (n > 0) p.give("my_mod:ruby", n);
}

Use the getter that matches the type: get_bool, get_i32, get_f64, and get_str, which returns an Expected<std::string>. 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.

clight::Config is a borrowed handle, like clight::Entity: copying it is fine, and it doesn’t need releasing.

Changing options

cfg.set_i32("rubies.on_join", 3);
cfg.save();

set_* changes the value in memory; save() writes the file. Values outside the min and max you defined are refused with CL_E_INVALID_ARGUMENT in the Expected that set_i32 returns.

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

Three files

clight::config::open(scope) 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 code can’t open the config folder with std::ifstream: 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. cfg_.comment("rubies.on_join", "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 cfg_.reload(). Getters always read the current value. To react to a change, listen to config.reloaded; it fires once per changed file:

clight::on<clight::ev::ConfigReloaded>([](cl_ev_config_reloaded& e) {
    clight::log::info("{} changed", std::string_view(e.file.ptr, e.file.len));
}, {.filter = "my_mod"});

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.hpp>

class ColorWelcomeMod final : public clight::Mod {
public:
    void on_construct() override {
        auto cfg = clight::config::open(CL_CONFIG_COMMON);
        if (!cfg) return;
        cfg_ = *cfg;
        cfg_.define_i32("join_message.color", 0x55FF55, 0, 0xFFFFFF);

        clight::on<clight::ev::PlayerJoin>([this](cl_ev_player& e) {
            clight::Entity p = e.player;
            clight::Text msg = clight::literal("Welcome, " + p.name() + "!");
            msg.color(static_cast<cl_color>(cfg_.get_i32("join_message.color")));
            p.send_message(msg);
        }).detach();
    }

private:
    clight::Config cfg_;
};

CLIGHT_MOD(ColorWelcomeMod)

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: clight::config::open(CL_CONFIG_COMMON) opens your mod’s config file, define_* sets options with defaults and limits, get_* reads them, and set_* plus save() changes them.