Wrap, overwrite and invokers
Lesson 12.4
Head and ret 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, ci.call_original() runs the target’s real work; afterwards the result is in ci.ret():
// Count the hits on players that really dealt damage.
static int g_hits = 0;
// in on_construct:
clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::wrap, [](auto& ci) {
ci.call_original(); // the hit happens as usual
if (ci.ret()) ++g_hits; // true: damage was dealt
}, {.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.
Captures live as long as the mixin
The lambda is kept until the handle removes the mixin, which for a detached handle is the end of the session. A capture by reference ([&count]) must point at something that lives that long: a member of your Mod class or a static like g_hits above, never a local variable of on_construct. Capturing by value ([fall]) is always safe.
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.
CL_MIXIN(entity_mob_xp_reward, overwrite, [](clight::mx::EntityMobXpReward::Ci& ci) {
ci.set_return(int32_t{5});
});
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, ret, 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, or inject in on_construct. 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 clight::mixin::invoke with the target’s number and the object it belongs to:
// Heal an entity by some health points the way the game heals.
inline void heal(clight::Entity e, float amount) {
clight::mx::EntityLivingHeal::Args a{sizeof(clight::mx::EntityLivingHeal::Args), amount};
(void)clight::mixin::invoke(clight::mx::EntityLivingHeal::id, e.raw().h, &a, nullptr);
}
The last argument points at space for the result; it can be nullptr for targets that return nothing. invoke returns an Expected<void> with the error, if any.
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.
clight::mixin::inject<clight::mx::PlayerGiveXp>(clight::at::head, [](auto& ci) {
if (ci.args().points == 77) {
ci.args().points = 154;
heal(cl_entity{ci.self()}, 3.0f);
}
});
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: clight::accessor<int32_t>("entity.living.invulnerable_time"). The function exists, but no accessor fields are in the game yet: they are Planned for the next game-logic batch. Until then the returned accessor is empty (false).
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(clight::mx::PlayerAttack::Ci& ci) {
clight::mx::PlayerExhaust::Args a{sizeof(clight::mx::PlayerExhaust::Args), 0.5f};
(void)clight::mixin::invoke(clight::mx::PlayerExhaust::id, ci.self(), &a, nullptr);
}
CL_MIXIN(player_attack, head, hungry_fighter);
The function is a named one because CL_MIXIN is a macro: the comma inside a{..., 0.5f} is only inside braces, and the macro would split its arguments there. A comma inside (...) is safe.
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 ci.call_original(), overwrite replaces it (one mod per target, so use it rarely), and clight::mixin::invoke calls an invokable target like the game would.