Wrap, overwrite and invokers
Lesson 12.4
Head and return run before and after the game’s code. Two more kinds take the game’s code into your hands: wrap runs around it, and overwrite replaces it. And with invokers you call a target yourself.
Wrap
A wrap mixin decides when, and whether, the game’s code runs. Inside it, cl_mixin_call_original(ci) runs the target’s real work; afterwards the result is in cl_mx_<target>_ret_of(ci):
static int g_hits;
/* Count the hits on players that really dealt damage. */
static void count_hits(cl_mixin_ci* ci, void* user) {
(void)user;
cl_mixin_call_original(ci); /* the hit happens as usual */
if (*cl_mx_entity_living_hurt_ret_of(ci)) ++g_hits; /* true: damage was dealt */
}
CL_MIXIN(entity_living_hurt, wrap, count_hits, .filter = "minecraft:player");
You can run code before and after, change the arguments before calling, or change the result after it.
Call the original. If your wrap returns without calling it, the game’s code doesn’t run, and neither do the mixins of other mods that come after yours. The game notices and shows “your mod hides the other mod” on the Mods screen. Skip the original only when skipping is the point of your mod, and prefer a head cancel for that: it says so more clearly.
If your function traps before it called the original, the game runs the original itself, so the vanilla behaviour still happens exactly once.
Overwrite
An overwrite replaces the target’s code with yours. Set the result for targets that return something:
/* Every mob drops exactly 5 experience, whatever vanilla says. */
static void flat_xp(cl_mixin_ci* ci, void* user) {
(void)user;
cl_mx_entity_mob_xp_reward_set_return(ci, 5);
}
CL_MIXIN(entity_mob_xp_reward, overwrite, flat_xp);
Only one mod can overwrite a target. When two try, the one that loads first wins, and the other one’s overwrite is turned off (the rest of that mod keeps working). The log and /clight mixins conflicts say so:
CLL-7005: overwrite on entity.mob.xp_reward conflicts with first_mod; this mixin is off (first_mod wins by load order)
An overwrite also hides the points and values of other mods inside the target, because that code never runs. So overwrite rarely. Most of the time a head, return, value or wrap does the same job and works together with other mods. If your mod still works without its overwrite, add .flags = CL_MXF_OPTIONAL.
Overwrites must be made while the mod loads: a CL_MIXIN record, or cl_mixin_inject in mod_main. Later the game refuses them with CL_E_WRONG_PHASE.
Invokers
Some targets can be called like a function. That is an invoker. Fill the target’s argument struct (the first field is its size) and call cl_mixin_invoke with the object it belongs to:
/* Heal an entity by 4 health points the way the game heals. */
static void heal(cl_entity e, float amount) {
cl_mx_entity_living_heal_args a = { (uint32_t)sizeof(cl_mx_entity_living_heal_args), amount };
cl_mixin_invoke(CL_MX_ENTITY_LIVING_HEAL, e.h, &a, NULL);
}
The last argument points at space for the result; it can be NULL for targets that return nothing. CL_MX_ENTITY_LIVING_HEAL is the target’s number from the header.
An invoker is a normal call of the game’s behaviour, so other mods’ mixins on that target run too, just as if the game itself had healed the entity. The invokable targets in API 1.5 are entity.living.hurt, entity.living.heal, entity.living.knockback, player.exhaust and player.give_xp.
/* Players who pick up 77 experience at once get 154, and are healed a little. */
static void lucky_xp(cl_mixin_ci* ci, void* user) {
(void)user;
cl_mx_player_give_xp_args* a = cl_mx_player_give_xp_args_of(ci);
if (a->points == 77) {
a->points = 154;
cl_entity player = { ci->self };
heal(player, 3.0f);
}
}
CL_MIXIN(player_give_xp, head, lucky_xp);
You may invoke from inside a mixin, even the same target. The game stops runaway chains after 16 levels of nesting.
Accessors are planned
CLight has an accessor API for reading and writing game fields that have no normal function, such as a mob’s invulnerability timer (cl_accessor_lookup, cl_accessor_get, cl_accessor_set). The functions exist, but no accessor fields are in the game yet: they are Planned for the next game-logic batch. Until then cl_accessor_lookup returns CL_E_NOT_FOUND.
Try it
Write a rule with an invoker: whenever a player attacks anything, they lose 0.5 extra exhaustion (they get hungry faster), through player.exhaust. Use a head mixin on player.attack.
Show a solution
static void hungry_fighter(cl_mixin_ci* ci, void* user) {
(void)user;
cl_mx_player_exhaust_args a = { (uint32_t)sizeof(cl_mx_player_exhaust_args), 0.5f };
cl_mixin_invoke(CL_MX_PLAYER_EXHAUST, ci->self, &a, NULL);
}
CL_MIXIN(player_attack, head, hungry_fighter);
Going through the invoker means another mod that halves exhaustion with a player.exhaust mixin also halves yours.
Recap: wrap runs around the game’s code with cl_mixin_call_original, overwrite replaces it (one mod per target, so use it rarely), and cl_mixin_invoke calls an invokable target like the game would.