Skip to content

Priorities, conflicts and debugging

Lesson 17.5

Players install many mods at once, and several of them may hook the same target. This lesson shows how mixins share a target, how to choose your place in the line, and how to see what is going on.

A chain, not a fight

All mixins on a target form a chain. Each one sees what the one before it left:

  • Two mods that halve entity.living.hurt damage in a head give a quarter.
  • A value mixin that doubles the knockback after another that tripled it gives six times.
  • After a head cancelled, the heads after it are skipped.

The order is the same on every computer and every run, so a server and its players always agree.

Priorities

You choose your place with priority, the same scale as for events:

Priority Number Use it for
"first" -2000 Rules that must win, like protection.
"early" -1000
"normal" 0 The default.
"late" 1000
"last" 2000 Final adjustments after everybody else.
"monitor" 10000 Only watching. A monitor mixin is read-only: its changes are dropped.
-- A protection mod: nobody hurts villagers, whatever other mods do.
cl.mixin.inject("entity.living.hurt", "head", function(ci)
  ci:cancel(false)
end, { filter = "minecraft:villager", priority = "first" })

-- A statistics mod: count real hits, after every mod had its say.
cl.mixin.inject("entity.living.hurt", "return", function(ci)
  if ci.ret then hits = hits + 1 end
end, { priority = "monitor" })

Within the same priority, the mods run in load order. If your mod must run right before or after a particular mod, say so with its id:

cl.mixin.inject("entity.living.hurt", "head", my_rule, { before = "armor_plus" })
cl.mixin.inject("entity.living.hurt", "head", my_rule2, { after = { "armor_plus", "magic_shields" } })

To see cancelled calls too (to log them, for example), add receive_cancelled = true. Such a head then runs read-only after a cancel.

For wraps, “first” means outermost: the first wrap’s code runs before all others and its call_original runs the rest of the chain.

Conflicts

Most kinds of mixin simply chain. Two situations are real conflicts:

Situation What happens
Two mods overwrite one target The one that loads first wins. The other one’s overwrite is off (CLL-7005); the rest of that mod keeps working.
One mod overwrites a target that another mod has points or values in Those points and values never run, because the code they live in doesn’t (CLL-7010).
A wrap never calls ci:call_original() The mods after it are hidden. The Mods screen shows “mod A hides mod B on target”.

When a mixin breaks

If your function raises an error, the game drops whatever that call changed, turns that one mixin off for the rest of the session, and carries on with vanilla behaviour. The log says why:

[spider_bounty] CLL-7009: disabled for entity.mob.xp_reward after an error: scripts/main.lua:12: attempt to perform arithmetic on a nil value

Your other mixins and events keep running. As with events, many errors in a short time disable the whole mod. Fix the bug, save, and the hot reload puts your mixins back.

/clight mixins

In a world with cheats on (or as an operator on a server), type /clight mixins in chat. It lists every target that has mixins, in the order they run, with the mod, the priority and how often each was called:

entity.living.hurt
  head  protect_villagers  priority -2000  calls 12
  head  spider_bounty  priority 0  calls 40
  return  stats_mod  priority 10000  calls 52
entity.mob.xp_reward
  return  spider_bounty  priority 0  calls 3

More detail:

  • /clight mixins entity.living.hurt shows one target.
  • /clight mixins conflicts shows overwrite conflicts, mixins turned off after errors, and mods hiding other mods. It says no mixin conflicts when everything is fine.
  • /clight mixins stats shows calls, cancels and errors per mixin.

A mixin with calls 0 never ran: maybe the filter is misspelt ("minecraft:spiders"), or the thing hasn’t happened yet.

When you start the game in development mode (clight run does that), it also writes the whole table and the conflicts to logs/clight-mixins.txt in the game folder, and rewrites it whenever the mixins change.

Declaring mixins in the manifest

You can list mixins in clight.mod.json instead of calling cl.mixin.inject. The handler is "<file>#<function>": a function in the table a module returns. Put the handlers in their own module, not in your main script:

"mixins": [
  { "target": "entity.mob.xp_reward", "at": "return", "handler": "scripts/rules.lua#double_xp", "filter": "minecraft:spider" }
]
-- scripts/rules.lua
local rules = {}

function rules.double_xp(ci)
  ci.ret = ci.ret * 2
end

return rules

The game registers them before your script runs, and clight check validates them: a misspelt target or point is reported before you ever start the game, with a “did you mean”. The Celestial website shows them on your project page, so players can see what your mod changes.

Try it

Two mods both change player.attack crits: one sets the multiplier to 2.0, yours wants to add 0.5 to whatever the result is. Write your mixin so that it always comes after the other mod (crit_master), and check with a command what the order is.

Show a solution
cl.mixin.inject("player.attack", "value:crit_multiplier", function(ci)
  ci.value = ci.value + 0.5
end, { after = "crit_master" })

/clight mixins player.attack then shows value:crit_multiplier crit_master above your line. priority = "late" would also work, but after says exactly what you mean and doesn’t push you behind unrelated mods.

Recap: mixins chain in priority order (first … monitor, then before/after, then load order), only overwrites truly conflict, a failing mixin turns only itself off, and /clight mixins shows it all.