Skip to content

Your first mod in C++

Status: Implemented. The complete mod from this page is clight-sdk/examples/ruby_cpp (it also draws a HUD counter).

The same Ruby mod as the C version, using the header-only wrapper clight.hpp (C++23: RAII handles, clight::Expected results, lambdas with captures, std::format; no exceptions). C++ mods compile to WebAssembly and run sandboxed.

clight setup                      # once
clight new cpp ruby_cpp --name "Ruby (C++ example)"

The manifest, assets and data are the same as the C version, with the id ruby_cpp and runtime.entry bin/ruby_cpp.wasm.

src/main.cpp

#include <clight.hpp>
#include <format>

namespace {
constexpr std::string_view kRuby = "ruby_cpp:ruby";
constexpr std::string_view kRubyBlock = "ruby_cpp:ruby_block";

int32_t count_rubies(clight::Entity player) {
    int32_t n = 0;
    for (int32_t slot = 0; slot <= 40; ++slot)
        if (clight::ItemStack s = player.inventory_get(slot); s && s.is(kRuby)) n += s.count();
    return n;
}
}  // namespace

class RubyMod final : public clight::Mod {
public:
    void on_construct() override {
        clight::on<clight::ev::PlayerJoin>([](cl_ev_player& e) {
            clight::Entity player = e.player;
            player.give(kRuby);                                                    // an item id; give(kRuby, 5) gives five
            player.send_message(clight::text::translatable("ruby_cpp.welcome"));  // or a plain string: "Welcome!"
        }).detach();

        clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
            if (!cl_block_state_is(e.state, clight::s(kRubyBlock))) return;
            const cl_vec3 at{e.pos.x + 0.5, e.pos.y + 0.5, e.pos.z + 0.5};
            clight::Level(e.level).sound_play(at, "ruby_cpp:ruby_chime", CL_SOUND_BLOCKS, 1.0f, 1.0f);
        }).detach();

        clight::on<clight::ev::ServerCommandRegister>([](cl_ev_command_register&) {
            clight::cmd::add("rubycpp", [](clight::CmdCtx c) -> int32_t {
                clight::Entity p = c.player();
                if (!p) { c.send_failure(clight::text::literal("Only players have pockets.")); return -1; }
                const int32_t n = count_rubies(p);
                auto msg = clight::text::literal(std::format("You have {} {}.", n, n == 1 ? "ruby" : "rubies"));
                msg.color(0xE0115F);
                c.send_success(msg, false);
                return n;
            });
        }).detach();
    }

    void on_register() override {   // lifecycle.register
        cl_block_desc b = CL_BLOCK_DESC_INIT;
        b.id = clight::s(kRubyBlock);
        b.copy_properties_from = CL_STR("minecraft:emerald_block");
        b.vanilla_fallback = CL_STR("minecraft:emerald_block");
        b.item = clight::s(kRubyBlock);
        clight::registry::block_register(&b);

        cl_item_desc i = CL_ITEM_DESC_INIT;
        i.id = clight::s(kRuby);
        i.vanilla_fallback = CL_STR("minecraft:emerald");
        i.creative_tab = CL_STR("minecraft:ingredients");
        clight::registry::item_register(&i);
    }
};

CLIGHT_MOD(RubyMod)

Build and run

clight build                      # dev build (with debug info)
clight build --release            # for publishing
clight run

What the wrapper gives you

Feature How
No manual release ItemStack, Text and Buf are RAII types (copy = retain, destructor = release)
Errors Fallible calls return clight::Expected<T> (std::expected<T, cl_result> where the library has it); test it with if (!r), read r.error()
Lambdas as callbacks Captures allowed. The wrapper keeps each lambda in a per-mod table and gives the engine an opaque id; it is freed when the registration ends
Typed events clight::ev::PlayerJoin, ev::BlockBreak, … carry the event id and the payload type
Subscriptions Subscription RAII: unsubscribes on destruction unless .detach()ed
Lifecycle clight::Mod virtuals (on_construct, on_register, on_client_setup, …), wired by CLIGHT_MOD(Class)

The wrapper is header-only and calls the same C API, so a C++ mod has the same ABI stability as a C mod. No C++ type ever crosses the boundary.

Build flags

clight build compiles C++ with -std=c++23 -fno-exceptions -fvisibility=hidden for wasm32-wasip1 (the full list is in clight-sdk/wasm/flags.txt). WebAssembly floating point is strict IEEE, like Java’s, so vanilla maths ports give the same results (Determinism and vanilla parity).