Head, return and cancel
Lesson 17.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. It can read the arguments in ci.args, and change the ones the target allows:
-- Every living entity takes half damage.
cl.mixin.inject("entity.living.hurt", "head", function(ci)
ci.args.amount = ci.args.amount / 2
end)
What a target’s arguments are, and which ones you may change, is in the mixin target list. A ! after a name marks the changeable ones: entity.living.hurt has source: damage_source, amount!: f32, so amount can be changed and source can only be read. Assigning a read-only argument raises an error.
Some arguments are tables. source describes the damage: its type (an id like "minecraft:fall"), the attacker (the entity responsible, or nil), the direct entity (an arrow, or the attacker itself in melee), and a position for explosions:
-- Fall damage is halved, everything else stays.
cl.mixin.inject("entity.living.hurt", "head", function(ci)
if ci.args.source.type == "minecraft:fall" then
ci.args.amount = ci.args.amount / 2
end
end)
ci.self is the object the target belongs to: here the entity being hurt. For block and world targets (block.*, level.*) it is the world.
Filters
Most mixins only care about one kind of mob, block or item. Pass a filter, and your function only runs for it:
-- Only goats: they jump twice as high.
cl.mixin.inject("entity.living.jump_power", "return", function(ci)
ci.ret = ci.ret * 2
end, { filter = "minecraft:goat" })
The game checks the filter before it calls any Lua, 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. The target list says which key each target has. A target without one ignores the filter.
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. ci.ret is the result, and assigning it changes what the game gets back:
-- Spiders drop three times the experience.
cl.mixin.inject("entity.mob.xp_reward", "return", function(ci)
ci.ret = ci.ret * 3
end, { filter = "minecraft:spider" })
A return mixin runs once per call, however the target finished. Targets that return nothing (-> void in the list) have no ci.ret.
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 means “no fall damage”:
-- Horses take no fall damage.
cl.mixin.inject("entity.living.cause_fall_damage", "head", function(ci)
ci:cancel()
end, { filter = "minecraft:horse" })
When a cancelled target returns something, the game needs a result. Pass it to cancel:
-- Pigs can't be hurt: entity.living.hurt returns false ("no damage was dealt").
cl.mixin.inject("entity.living.hurt", "head", function(ci)
ci:cancel(false)
end, { filter = "minecraft:pig" })
-- Right-clicking with a stick gives an apple instead, and counts as used (true).
cl.mixin.inject("item.use", "head", function(ci)
ci.self:give("minecraft:apple")
ci:cancel(true)
end, { filter = "minecraft:stick" })
Without a value, the target’s default result is used (the list says which, for example “default false”). Cancelling a target that can’t be cancelled raises an error.
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:
-- Foxes never die.
cl.mixin.inject("entity.living.die", "head", function(ci) ci:cancel() end, { filter = "minecraft:fox" })
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.
cl.mixin.inject("player.give_xp", "head", function(ci)
cl.log("%s gets %d experience", ci.self.name, ci.args.points)
end)
Removing a mixin
cl.mixin.inject returns a handle. h:remove() takes the mixin away again:
local h = cl.mixin.inject("entity.mob.xp_reward", "return", function(ci) ci.ret = ci.ret * 2 end)
-- later, for example when an event ends:
h:remove()
There are also short forms: cl.mixin.head(target, fn, opts) and cl.mixin.ret(target, fn, opts) do the same as inject with "head" and "return".
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
cl.mixin.inject("entity.mob.xp_reward", "return", function(ci)
ci.ret = 0
end, { filter = "minecraft:chicken" })
cl.mixin.inject("entity.living.hurt", "head", function(ci)
if ci.args.source.type == "minecraft:arrow" then
ci.args.amount = ci.args.amount * 2
end
end, { filter = "minecraft:zombie" })
The filter picks the mob, and the if picks the kind of damage, which no filter covers.
Recap: "head" runs first and may change ! arguments or ci:cancel(result), "return" runs last and may change ci.ret, and a filter keeps it cheap.