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