Skip to content

Head, return and cancel

Lesson 12.2

The two places you will use most are the start and the end of a target: head and return. This lesson shows both, how to change arguments and results, and how to cancel.

Head: before the game’s code

A head mixin runs before the target does its work. cl_mx_<target>_args_of(ci) gives you the arguments as a struct:

/* Every living entity takes half damage. */
static void half_damage(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_hurt_args* a = cl_mx_entity_living_hurt_args_of(ci);
    a->amount *= 0.5f;
}
CL_MIXIN(entity_living_hurt, head, half_damage);

The struct is in clight.h. Fields you may change are marked /* mut */; for entity.living.hurt that is amount, while source is read-only:

typedef struct cl_mx_entity_living_hurt_args {
    uint32_t struct_size;
    cl_damage_source source;
    float amount; /* mut */
} cl_mx_entity_living_hurt_args;

Writes to other fields are simply dropped: the game copies back only what may change. The mixin target list marks the same fields with !.

source is a cl_damage_source: the damage type (a cl_ident), the attacker (cl_entity, null when nobody is responsible), the direct entity (an arrow, or the attacker in melee), and a position for explosions. Ids in mixins are cl_ident numbers, so compare against an id you looked up once:

static cl_ident g_fall;

/* Fall damage is halved, everything else stays. */
static void half_fall(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_hurt_args* a = cl_mx_entity_living_hurt_args_of(ci);
    if (a->source.type == g_fall) a->amount *= 0.5f;
}
CL_MIXIN(entity_living_hurt, head, half_fall);

static cl_result mod_main(void) {
    g_fall = cl_ident_intern(CL_STR("minecraft:fall"));
    return CL_OK;
}
CLIGHT_MOD_ENTRY(mod_main)

The static mixin is registered before mod_main runs, but no damage happens before the world is loaded, so g_fall is always set in time.

ci->self is the object the target belongs to, as a number. For entity targets it is the entity: cl_entity e = { ci->self };. For block and world targets (block.*, level.*) it is the world.

Filters

Most mixins only care about one kind of mob, block or item. Add a filter, and your function only runs for it:

/* Only goats: they jump twice as high. */
static void goat_jump(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_entity_living_jump_power_ret_of(ci) *= 2.0f;
}
CL_MIXIN(entity_living_jump_power, return, goat_jump, .filter = "minecraft:goat");

The game checks the filter before it calls your mod, so a filtered mixin costs almost nothing for every other mob. Entity targets filter on the entity type, block.* and level.set_block on the block, and item.* and block.place_item on the item. Options like .filter come after the function, in the order .priority, .flags, .filter, .before, .after; leave out the ones you don’t need.

block.random_tick and level.set_block run thousands of times per second. Always give them a filter.

Return: after the game’s code

A return mixin runs after the target finished. cl_mx_<target>_ret_of(ci) points at the result, and writing through it changes what the game gets back:

/* Spiders drop three times the experience. */
static void spider_xp(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_entity_mob_xp_reward_ret_of(ci) *= 3;
}
CL_MIXIN(entity_mob_xp_reward, return, spider_xp, .filter = "minecraft:spider");

Targets that return nothing (-> void in the list) have no _ret_of helper.

Cancel

A head mixin can cancel the call on targets marked cancellable: the game’s code then doesn’t run at all. What that means is written next to every target. For entity.living.cause_fall_damage it is “no fall damage”:

/* Horses take no fall damage. */
static void horse_no_fall(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mixin_cancel(ci);
}
CL_MIXIN(entity_living_cause_fall_damage, head, horse_no_fall, .filter = "minecraft:horse");

When a cancelled target returns something, set the result first with cl_mx_<target>_set_return:

/* Rabbits can't be hurt: entity.living.hurt returns false ("no damage was dealt"). */
static void rabbit_safe(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_hurt_set_return(ci, false);
    cl_mixin_cancel(ci);
}
CL_MIXIN(entity_living_hurt, head, rabbit_safe, .filter = "minecraft:rabbit");

/* Diamond blocks can't be placed by anything: level.set_block returns false and the old block stays. */
static void no_diamond_blocks(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_level_set_block_set_return(ci, false);
    cl_mixin_cancel(ci);
}
CL_MIXIN(level_set_block, head, no_diamond_blocks, .filter = "minecraft:diamond_block");

Without a value, the target’s default result is used (the list says which, for example “default false”). cl_mixin_cancel returns CL_E_UNSUPPORTED on a target that can’t be cancelled, and the call runs normally.

A few cancels have a special meaning. Cancelling entity.living.die keeps the entity alive, and if its health is 0 or less it gets 1, the way a totem saves you.

Mixins that only look

A mixin doesn’t have to change anything. Reading the arguments is useful on its own, and it is always safe: when your function only reads, the game’s values stay bit for bit the same.

static void log_xp(cl_mixin_ci* ci, void* user) {
    (void)user;
    CL_LOG("a player gets %d experience", (int)cl_mx_player_give_xp_args_of(ci)->points);
}
CL_MIXIN(player_give_xp, head, log_xp);

Adding and removing at run time

CL_MIXIN mixins stay for the whole session. To add one later, or remove it again, fill a cl_mixin_desc and call cl_mixin_inject:

static cl_mixin g_double;

static cl_result double_xp_on(void) {
    cl_mixin_desc d = CL_MIXIN_DESC_INIT;
    d.target = CL_STR("entity.mob.xp_reward");
    d.at = CL_AT_RETURN;
    d.fn = spider_xp;                                   /* any function with the mixin signature */
    d.filter = cl_ident_intern(CL_STR("minecraft:cave_spider"));
    return cl_mixin_inject(&d, &g_double);
}

static void double_xp_off(void) {
    cl_mixin_remove(g_double);
}

Here the filter is a cl_ident, not a string. d.user is passed to your function as user, so one function can serve several mixins (the project at the end of this chapter does that). The kinds are CL_AT_HEAD, CL_AT_RETURN, CL_AT_POINT, CL_AT_VALUE, CL_AT_CALL, CL_AT_WRAP and CL_AT_OVERWRITE.

Try it

Make chickens drop no experience at all, and make every zombie that is hurt by an arrow (minecraft:arrow damage) take double damage.

Show a solution
#include <clight_easy.h>

static cl_ident g_arrow;

static void no_chicken_xp(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_entity_mob_xp_reward_ret_of(ci) = 0;
}
CL_MIXIN(entity_mob_xp_reward, return, no_chicken_xp, .filter = "minecraft:chicken");

static void zombie_arrows(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_hurt_args* a = cl_mx_entity_living_hurt_args_of(ci);
    if (a->source.type == g_arrow) a->amount *= 2.0f;
}
CL_MIXIN(entity_living_hurt, head, zombie_arrows, .filter = "minecraft:zombie");

static cl_result mod_main(void) {
    g_arrow = cl_ident_intern(CL_STR("minecraft:arrow"));
    return CL_OK;
}
CLIGHT_MOD_ENTRY(mod_main)

The filter picks the mob, and the if picks the kind of damage, which no filter covers.

Recap: head runs first and may change mut arguments or cancel (set_return + cl_mixin_cancel), return runs last and may change the result, and .filter keeps it cheap.