Skip to content

Head, return and cancel

Lesson 12.2

The two places you will use most are the start and the end of a target: head and ret. This lesson shows both, how to change arguments and results, and how to cancel.

The examples go into on_construct and keep their handles with detach(), as in the last lesson. To save space, they only show the inject call:

auto h = clight::mixin::inject<...>(...);
if (h) h->detach();

Head: before the game’s code

A head mixin runs before the target does its work. ci.args() is a reference to the arguments struct:

// Every living entity takes half damage.
clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::head, [](auto& ci) {
    ci.args().amount *= 0.5f;
});

The struct comes from clight.h (clight::mx::EntityLivingHurt::Args is cl_mx_entity_living_hurt_args). Fields you may change are marked /* mut */; for entity.living.hurt that is amount, while source is read-only. Writes to other fields are simply dropped: the game copies back only what may change. The mixin target list marks the same fields with !.

source is a cl_damage_source: the damage type (a cl_ident), the attacker (cl_entity, null when nobody is responsible), the direct entity (an arrow, or the attacker in melee), and a position for explosions. Ids in mixins are cl_ident numbers, so look the id up once and compare:

// Fall damage is halved, everything else stays.
const cl_ident fall = clight::core::ident_intern("minecraft:fall");
clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::head, [fall](auto& ci) {
    if (ci.args().source.type == fall) ci.args().amount *= 0.5f;
});

ci.self() is the object the target belongs to, as a number. For entity targets it is the entity: clight::Entity e = cl_entity{ci.self()};. 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 options as the third argument, and the filter makes your function run only for that id:

// Only goats: they jump twice as high.
clight::mixin::inject<clight::mx::EntityLivingJumpPower>(clight::at::ret, [](auto& ci) {
    ci.ret() *= 2.0f;
}, {.filter = "minecraft:goat"});

The game checks the filter before it calls your mod, 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 options are clight::mixin::Opts: .priority, .flags, .filter, .before, .after, in that order; leave out the ones you don’t need. For CL_MIXIN they come after the function: CL_MIXIN(entity_living_jump_power, return, fn, .filter = "minecraft:goat").

block.random_tick and level.set_block run thousands of times per second. Always give them a filter.

Ret: after the game’s code

A ret mixin runs after the target finished. ci.ret() is a reference to the result:

// Spiders drop three times the experience.
clight::mixin::inject<clight::mx::EntityMobXpReward>(clight::at::ret, [](auto& ci) {
    ci.ret() *= 3;
}, {.filter = "minecraft:spider"});

Targets that return nothing (Ret is void) 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 is “no fall damage”. When the target returns something, pass the result to cancel:

// Horses take no fall damage (cause_fall_damage returns false: "no damage was dealt").
clight::mixin::inject<clight::mx::EntityLivingCauseFallDamage>(clight::at::head, [](auto& ci) {
    ci.cancel(false);
}, {.filter = "minecraft:horse"});

// Rabbits can't be hurt.
clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::head, [](auto& ci) {
    ci.cancel(false);
}, {.filter = "minecraft:rabbit"});

// Sugar cane never grows: its random tick (returns nothing) is cancelled.
clight::mixin::inject<clight::mx::BlockRandomTick>(clight::at::head, [](auto& ci) {
    ci.cancel();
}, {.filter = "minecraft:sugar_cane"});

Without a value, the target’s default result is used (the list says which, for example “default false”). cancel returns false on a target that can’t be cancelled, and the call runs normally.

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.

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.

clight::mixin::inject<clight::mx::PlayerGiveXp>(clight::at::head, [](auto& ci) {
    clight::Entity player = cl_entity{ci.self()};
    clight::log::info("{} gets {} experience", player.name(), ci.args().points);
});

Keeping and removing a mixin

detach() keeps a mixin for the whole session. To remove it later, keep the handle in a member instead; resetting or destroying it removes the mixin:

class Mod final : public clight::Mod {
    clight::mixin::Handle double_xp_;
public:
    void on_construct() override {
        if (auto h = clight::mixin::inject<clight::mx::EntityMobXpReward>(clight::at::ret, [](auto& ci) { ci.ret() *= 2; }))
            double_xp_ = std::move(*h);
    }
    void stop_double_xp() { double_xp_.reset(); }
};

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
void on_construct() override {
    auto a = clight::mixin::inject<clight::mx::EntityMobXpReward>(clight::at::ret, [](auto& ci) {
        ci.ret() = 0;
    }, {.filter = "minecraft:chicken"});
    if (a) a->detach();

    const cl_ident arrow = clight::core::ident_intern("minecraft:arrow");
    auto b = clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::head, [arrow](auto& ci) {
        if (ci.args().source.type == arrow) ci.args().amount *= 2.0f;
    }, {.filter = "minecraft:zombie"});
    if (b) b->detach();
}

The filter picks the mob, and the if picks the kind of damage, which no filter covers. The lambda captures arrow by value, so it stays valid after on_construct returns.

Recap: head runs first and may change mut arguments or ci.cancel(result), ret runs last and may change ci.ret(), and {.filter = ...} keeps it cheap.