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.