Wrap, overwrite and invokers
Lesson 17.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, ci:call_original() runs the target’s real work and returns its result:
-- Count how many AI steps zombies take.
local steps = 0
cl.mixin.inject("entity.mob.ai_step", "wrap", function(ci)
steps = steps + 1
ci:call_original() -- the zombie thinks and moves as usual
end, { filter = "minecraft:zombie" })
You can run code before and after, change the arguments before calling, or look at the result:
-- Log every hit that really dealt damage.
cl.mixin.inject("entity.living.hurt", "wrap", function(ci)
local hurt = ci:call_original()
if hurt then
cl.log("%s took %.1f damage", ci.self:type(), ci.args.amount)
end
end)
ci:call_original() returns the target’s result, and after it ci.ret holds the same value, which you may change.
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 errors 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 ci.ret for targets that return something:
-- Every mob drops exactly 5 experience, whatever vanilla says.
cl.mixin.inject("entity.mob.xp_reward", "overwrite", function(ci)
ci.ret = 5
end)
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, pass { optional = true }.
Overwrites have to be made while the mod loads (at the top level of your script), not later from an event or a command.
Invokers
Some targets can be called like a function. That is an invoker:
-- Heal an entity by 4 health points the way the game heals.
cl.mixin.invoke("entity.living.heal", entity, 4.0)
-- Give a player 30 experience points.
cl.mixin.invoke("player.give_xp", player, 30)
The first argument is the target, the second the object it belongs to, then the target’s arguments in the order of the target list. It returns the target’s result, if it has one.
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.
cl.mixin.inject("player.give_xp", "head", function(ci)
if ci.args.points == 77 then
ci.args.points = 154
cl.mixin.invoke("entity.living.heal", ci.self, 3.0)
end
end)
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("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 cl.accessor raises a not_found error.
Try it
Write a rule with an invoker: when a player attacks a cow, the player loses 1 extra exhaustion (they get hungry faster), through player.exhaust. Use a head mixin on player.attack; the attacked entity is ci.args.target.
Show a solution
cl.mixin.inject("player.attack", "head", function(ci)
local target = ci.args.target
if target and target:type() == "minecraft:cow" then
cl.mixin.invoke("player.exhaust", ci.self, 1.0)
end
end)
player.attack has no filter key (it would filter on the player), so the type check happens in your function. 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 cl.mixin.invoke calls an invokable target like the game would.