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_damageis cancellable, and it returnsbool(“damage was dealt”). Cancel it withfalse.entity.living.knockbacktakes the knockbackstrengthas amutargument. 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.healinvoker (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_mixinhandles fromcl_mixin_inject(&d, &handle)andcl_mixin_removethem.
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.