What is a mixin?
Lesson 12.1
Events tell you about the moments the game announces: a block breaks, a player joins, an entity takes damage. That covers most mods. But some changes happen inside the game’s own code, where no event looks: how far a hit knocks a sheep back, how much a critical hit multiplies the damage, how much experience a spider drops. For those, CLight has mixins.
The idea
A mixin attaches your function to a target: a named piece of the game’s behaviour. entity.living.hurt is “a living entity is asked to take damage”. entity.mob.xp_reward is “how much experience this mob drops”. When the game runs that behaviour, it calls your function too, at the place you asked for:
#include <clight_easy.h>
/* Every mob drops twice the experience. */
static void double_xp(cl_mixin_ci* ci, void* user) {
(void)user;
*cl_mx_entity_mob_xp_reward_ret_of(ci) *= 2;
}
CL_MIXIN(entity_mob_xp_reward, return, double_xp);
Read it as: “when the game has worked out a mob’s experience (return), take the result and double it”.
CL_MIXIN(target, at, function)declares the mixin. The target is written with_instead of.:entity.mob.xp_rewardbecomesentity_mob_xp_reward.- The second argument says where your function runs.
returnmeans “after the game’s code, when the result is known”. The next lessons show the others. - Your function gets
ci, the callback info (cl_mixin_ci): the target’s arguments, its result, the object it belongs to, and ways to cancel or change things. cl_mx_entity_mob_xp_reward_ret_of(ci)is a typed pointer to the result, anint32_t*here. The header has such helpers for every target.- The mixin API is in
clight.h. The examples includeclight_easy.h, which includesclight.hand addsCL_LOGand the other short helpers (The easy header).
CL_MIXIN goes at the top level of a .c file, outside any function. It writes a small record into your .wasm file, and the game registers the mixin when your mod loads, before mod_main runs. You don’t call anything. clight check reads the same records, so a misspelt target is reported before you start the game.
Mixins are safe. Nothing in the game is patched or rewritten: the game has its targets built in, and your function still runs in the WebAssembly sandbox with the same rules as every event callback. A target nobody hooks costs the game next to nothing, and a mixin that changes nothing leaves the game exactly vanilla.
Target names describe what happens, not how the game’s code is written, so they stay the same from one Celestial build to the next. A mod compiled today keeps working on later builds.
Events or mixins?
Many things can be done both ways. Damage is an example: the entity.damage event (CL_EV_ENTITY_DAMAGE) lets you change the amount too. Pick the simplest tool that does the job:
| You want to | Use |
|---|---|
| React when something happens, or stop it | An event |
Change a field the event offers (amount, message, drop_items) |
An event |
| Change a value the game uses inside its code (knockback strength, critical multiplier, despawn distance) | A mixin |
| Change a result that has no event (experience reward, jump power, fall damage) | A mixin |
| Do the job differently, or skip one step of it (no sweep attack, no knockback) | A mixin |
Which targets exist?
Since CLight API 1.17, the game has 93 targets. 56 game-logic targets run on the server, so your players need nothing installed (41 since API 1.5, 15 more in API 1.14):
- Entities:
entity.living.hurt,entity.living.actually_hurt,entity.living.damage_after_armor,entity.living.damage_after_magic,entity.living.heal,entity.living.check_totem,entity.living.die,entity.living.knockback,entity.living.cause_fall_damage,entity.living.add_effect,entity.living.tick_effects,entity.living.tick,entity.living.travel,entity.living.jump_power,entity.living.jump,entity.mob.ai_step,entity.mob.check_despawn,entity.mob.finalize_spawn,entity.mob.do_hurt_target,entity.mob.xp_reward,entity.mob.pick_up_items,spawn.check_rules. - Players:
player.attack,player.attack_damage,player.interact_entity,player.food_tick,player.exhaust,player.give_xp,player.respawn,player.teleport,player.destroy_block,player.move(1.14),player.destroy_progress(1.14). - Blocks and the world:
block.player_destroy,block.place_item,block.use_without_item,block.use_with_item,block.random_tick,level.set_block,level.explode. - Items:
item.use,item.use_on,item.finish_using,item.hurt(1.14),item.entity_pickup(1.14),item.entity_tick(1.14). - Inventories and menus (1.14):
inventory.add_item,menu.open,menu.click,menu.quick_move,menu.may_place,container.hopper_transfer. - Projectiles and loot (1.14):
projectile.shoot,projectile.hit,loot.block_drops,loot.entity_drops.
19 more run on the client (API 1.16) and change only the game they run in:
- Rendering:
client.render.world,client.render.sky,client.render.fog,client.render.entity,client.camera.setup. - HUD and screens:
client.render.hud(with a point for every HUD layer),client.screen.init,client.screen.render,client.screen.key. - Input, chat, sound, particles:
client.input.key,client.input.mouse_move,client.chat.receive,client.chat.send,client.sound.play,client.particle.add. - The client game and the network:
client.tick,client.player.attack_cooldown,net.packet.receive,net.packet.send.
entity.living.travel, entity.living.jump, entity.living.jump_power, player.move and player.destroy_progress also fire on the client for your own player (variant local_player). Those are predicted: the server simulates the same thing, so on a server they only run when the server has your mod too (in singleplayer always). Some client targets need a permission the player grants: ui.inject for the HUD and screens, input.synthesize for input, packets.raw for packets.
18 more change the world and its data (API 1.17), on the server:
- World generation:
chunk.generate.stage,worldgen.structure.start,worldgen.biome.fill,worldgen.terrain.build,worldgen.feature.place,worldgen.structure.place. They run on the world generator’s worker threads, so only C and C++ (WebAssembly) mods can use them. - Chunks:
chunk.load,chunk.unload,chunk.save. - Commands:
command.execute,command.register,command.suggest. - Recipes, loot and tags:
recipe.match,recipe.assemble,loot.table.roll,tag.contains. - Data:
data.reload,data.registry_freeze.
The mixin target list describes each one: its arguments, its result, what cancelling means, and the named places inside it. In clight.h, search for CL_MX_ and the target name: next to each target are its argument struct (cl_mx_<target>_args) and its helpers.
Planned
call: sites (redirecting one call inside a target) are Planned. New targets and new points are only ever added; the ones above never change.
Asking the game
A mod can ask whether this build has a target, for example to log what it can do:
static cl_result mod_main(void) {
if (!cl_core_has(CL_STR("mixin:entity.living.knockback"))) {
CL_LOG("this build has no knockback target");
return CL_OK;
}
cl_mixin_target_info info = CL_MIXIN_TARGET_INFO_INIT;
cl_mixin_info(CL_STR("entity.living.knockback"), &info);
CL_LOG("knockback: cancellable=%d, since API 1.%u", (info.flags & CL_MXT_CANCELLABLE) != 0, info.since_minor);
return CL_OK;
}
CLIGHT_MOD_ENTRY(mod_main)
cl_mixin_info fills a cl_mixin_target_info: the target’s number, its flags (CL_MXT_CANCELLABLE, CL_MXT_INVOKABLE, …) and since which API version it exists. It returns CL_E_NOT_FOUND for an unknown name.
Try it
Write a mod that doubles the experience of every mob, like the first example, and logs in mod_main whether entity.mob.xp_reward exists and whether it is invokable (CL_MXT_INVOKABLE).
Show a solution
#include <clight_easy.h>
static void double_xp(cl_mixin_ci* ci, void* user) {
(void)user;
*cl_mx_entity_mob_xp_reward_ret_of(ci) *= 2;
}
CL_MIXIN(entity_mob_xp_reward, return, double_xp);
static cl_result mod_main(void) {
cl_mixin_target_info info = CL_MIXIN_TARGET_INFO_INIT;
if (cl_mixin_info(CL_STR("entity.mob.xp_reward"), &info) != CL_OK) {
CL_LOG("no xp_reward target in this build");
return CL_OK;
}
CL_LOG("xp_reward: invokable=%d", (info.flags & CL_MXT_INVOKABLE) != 0);
return CL_OK;
}
CLIGHT_MOD_ENTRY(mod_main)
It logs invokable=0: entity.mob.xp_reward can’t be called by mods, only hooked.
Recap: a mixin runs your function inside a named game target; CL_MIXIN(target, at, fn) declares it, and ci gives you typed access to the arguments and the result.