Skip to content

Project: safe saddles

Lesson 12.6

A small server mod made only of mixins. Riding animals become sturdier:

  • horses, donkeys, mules, camels and the skeleton and zombie horses take no fall damage,
  • they are never knocked back, so a hit doesn’t push you off a cliff,
  • the mod tells you in the log which rules are active.

It uses runtime mixins (cl_mixin_inject) in a loop, one function for many animals, and checks the result with clight check and /clight mixins.

Step 1: the project

> clight new c safe_saddles --name "Safe Saddles"
> cd safe_saddles

All the targets the mod uses run on the server, so the players need nothing installed. Say so in clight.mod.json (The manifest):

{
  "schema": 1,
  "id": "safe_saddles",
  "version": "1.0.0",
  "name": "Safe Saddles",
  "description": "Riding animals take no fall damage and are never knocked back.",
  "authors": ["Alex"],
  "license": "MIT",
  "api": 1,
  "game": ">=26.3 <26.4",
  "side": "server",
  "sync": "server_only",
  "runtime": { "type": "wasm", "entry": "bin/safe_saddles.wasm" },
  "permissions": []
}

"side": "server" and "sync": "server_only" say that the mod runs on the server (and in singleplayer worlds) and that players don’t need it. The runtime line is the one the template wrote: it points at the file clight build makes.

Step 2: the rules

Two functions, one per rule (Head, return and cancel):

  • entity.living.cause_fall_damage is cancellable, and it returns bool (“damage was dealt”). Cancel it with false.
  • entity.living.knockback takes the knockback strength as a mut argument. Set it to 0 and the push is gone, while the hit still hurts.
#include <clight_easy.h>

/* No fall damage: cancel the landing, "no damage was dealt". */
static void no_fall_damage(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_cause_fall_damage_set_return(ci, false);
    cl_mixin_cancel(ci);
}

/* No knockback: the hit still hurts, but the animal stays where it is. */
static void no_knockback(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_knockback_args_of(ci)->strength = 0.0;
}

Step 3: one mixin per animal

A filter takes one id, and there are six animals. Instead of twelve CL_MIXIN lines, mod_main registers them in a loop with cl_mixin_inject (Adding and removing at run time). The filter at run time is a cl_ident:

static const char* const STEEDS[] = {
    "minecraft:horse", "minecraft:donkey", "minecraft:mule",
    "minecraft:camel", "minecraft:skeleton_horse", "minecraft:zombie_horse",
};
#define STEED_COUNT (sizeof STEEDS / sizeof STEEDS[0])

static cl_result add_rule(const char* target, cl_mixin_fn fn, const char* steed) {
    cl_mixin_desc d = CL_MIXIN_DESC_INIT;
    d.target = CL_STR_C(target);
    d.at = CL_AT_HEAD;
    d.fn = fn;
    d.filter = cl_ident_intern(CL_STR_C(steed));
    return cl_mixin_inject(&d, NULL);   /* NULL: keep it for the whole session */
}

static cl_result mod_main(void) {
    for (size_t i = 0; i < STEED_COUNT; ++i) {
        cl_result r = add_rule("entity.living.cause_fall_damage", no_fall_damage, STEEDS[i]);
        if (r == CL_OK) r = add_rule("entity.living.knockback", no_knockback, STEEDS[i]);
        if (r != CL_OK) {
            CL_LOG("Safe Saddles: could not add the rules for %s (error %d)", STEEDS[i], (int)r);
            return r;
        }
    }
    CL_LOG("Safe Saddles: %d riding animals protected", (int)STEED_COUNT);
    return CL_OK;
}
CLIGHT_MOD_ENTRY(mod_main)

CL_STR_C turns a C string into a cl_str; CL_STR only works with string literals. If cl_mixin_inject fails (a target this build doesn’t have, for example), mod_main returns the error and the mod doesn’t load, with the reason in the log.

Step 4: build and check

> clight build
> clight check

clight check reads the static CL_MIXIN records of the .wasm. This mod has none: its mixins are made at run time, so the game checks them when cl_mixin_inject runs. Change one entity.living.knockback to entity.living.knock_back and start the game: the log shows CLL-7001: unknown mixin target 'entity.living.knock_back' and the mod doesn’t load. That is why the loop checks every result.

Step 5: try it

Start the game with clight run, open a world with cheats, and test both rules:

/summon minecraft:horse ~ ~15 ~3

The horse falls 15 blocks and lands without a scratch (vanilla takes most of its health). Hit it: it flinches but doesn’t move back. Then type /clight mixins:

entity.living.knockback
  head  safe_saddles  priority 0  calls 1
  head  safe_saddles  priority 0  calls 0
  ...
entity.living.cause_fall_damage
  head  safe_saddles  priority 0  calls 1
  head  safe_saddles  priority 0  calls 0
  ...

There is one line per animal. Only the horse’s lines count up, because the filters keep the function from running for any other mob.

The whole file

// Safe Saddles: riding animals take no fall damage and are never knocked back.
#include <clight_easy.h>

static const char* const STEEDS[] = {
    "minecraft:horse", "minecraft:donkey", "minecraft:mule",
    "minecraft:camel", "minecraft:skeleton_horse", "minecraft:zombie_horse",
};
#define STEED_COUNT (sizeof STEEDS / sizeof STEEDS[0])

/* No fall damage: cancel the landing, "no damage was dealt". */
static void no_fall_damage(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_cause_fall_damage_set_return(ci, false);
    cl_mixin_cancel(ci);
}

/* No knockback: the hit still hurts, but the animal stays where it is. */
static void no_knockback(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_knockback_args_of(ci)->strength = 0.0;
}

static cl_result add_rule(const char* target, cl_mixin_fn fn, const char* steed) {
    cl_mixin_desc d = CL_MIXIN_DESC_INIT;
    d.target = CL_STR_C(target);
    d.at = CL_AT_HEAD;
    d.fn = fn;
    d.filter = cl_ident_intern(CL_STR_C(steed));
    return cl_mixin_inject(&d, NULL);
}

static cl_result mod_main(void) {
    for (size_t i = 0; i < STEED_COUNT; ++i) {
        cl_result r = add_rule("entity.living.cause_fall_damage", no_fall_damage, STEEDS[i]);
        if (r == CL_OK) r = add_rule("entity.living.knockback", no_knockback, STEEDS[i]);
        if (r != CL_OK) {
            CL_LOG("Safe Saddles: could not add the rules for %s (error %d)", STEEDS[i], (int)r);
            return r;
        }
    }
    CL_LOG("Safe Saddles: %d riding animals protected", (int)STEED_COUNT);
    return CL_OK;
}
CLIGHT_MOD_ENTRY(mod_main)

Ideas

  • Heal a steed by 1 health point every time it would have taken fall damage, with the entity.living.heal invoker (Wrap, overwrite and invokers).
  • Read the list of animals from a config file (Config).
  • Let players switch the rules off with a command: keep the cl_mixin handles from cl_mixin_inject(&d, &handle) and cl_mixin_remove them.

Recap: runtime mixins (cl_mixin_desc + cl_mixin_inject) let one function serve many filters; check every result, and use /clight mixins to see which ones run.