Skip to content

Points and values

Lesson 12.3

head and return work on the edges of a target. Some changes need to happen in the middle: after the shield took its part of the damage, or right before the knockback. For that, targets have sub-points: named places inside them.

There are two kinds:

  • A point is a moment inside the target. Your function can read the values the game has at that moment, change the ones marked mut, and on some points skip the next step.
  • A value is one number (or block) the game uses inside the target, like the knockback strength or the critical hit multiplier. Your function can replace it.

You declare them with CL_MIXIN_SITE(target, point|value, name, function, options...).

Values

cl_mx_<target>_<value>_of(ci) is a pointer to the value, typed as the game uses it:

/* Critical hits do double damage instead of 1.5 times. */
static void strong_crits(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_player_attack_crit_multiplier_of(ci) = 2.0f;        /* a float* */
}
CL_MIXIN_SITE(player_attack, value, crit_multiplier, strong_crits);

/* Sheep are knocked back three times as far. */
static void sheep_fly(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_entity_living_hurt_knockback_strength_of(ci) *= 3.0;  /* a double* */
}
CL_MIXIN_SITE(entity_living_hurt, value, knockback_strength, sheep_fly, .filter = "minecraft:sheep");

A value mixin changes one number and leaves everything around it alone. That makes it the gentlest kind of mixin: the critical hit still needs a falling player, still plays its sound and shows its particles. Only the multiplier is different.

Points

cl_mx_<target>_<point>_of(ci) is a pointer to the point’s struct of local values:

/* After a shield blocked part of a hit, the rest is halved too. */
static void shield_bonus(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_entity_living_hurt_after_blocking* p = cl_mx_entity_living_hurt_after_blocking_of(ci);
    if (p->blocked > 0.0f) p->amount *= 0.5f;   /* amount is mut, blocked is read-only */
}
CL_MIXIN_SITE(entity_living_hurt, point, after_blocking, shield_bonus);

cl_mx_<target>_args_of(ci) still works inside points too.

Some points can be cancelled, and then cancelling skips one step instead of the whole target:

/* Hits do damage as usual, but nobody is knocked back. */
static void no_knockback(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mixin_cancel(ci);
}
CL_MIXIN_SITE(entity_living_hurt, point, before_knockback, no_knockback);

/* No sweep attacks: the main target is hit, the mobs around it aren't. */
static void no_sweep(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mixin_cancel(ci);
}
CL_MIXIN_SITE(player_attack, point, before_sweep, no_sweep);

Compare that with a head cancel on entity.living.hurt, which would stop the damage as well.

Every point and value in API 1.5

Target Points Values
entity.living.hurt after_blocking {amount mut, blocked}, before_knockback {strength mut, dx, dz} (cancel skips the knockback), before_death invulnerability_ticks (int32_t, 20), knockback_strength (double, 0.4)
entity.living.actually_hurt after_armor (float), after_magic (float)
entity.living.cause_fall_damage safe_distance (double)
entity.mob.check_despawn far_distance (double, 128), random_chance (int32_t, 800)
player.attack before_sweep (cancel skips the sweep) crit_multiplier (float, 1.5)
player.food_tick exhaustion_threshold (float, 4.0)
player.respawn before_place {x, y, z, yaw, pitch, all mut}
block.place_item placement_state (cl_block_state)

The numbers are vanilla’s values. A misspelt name doesn’t compile, because the helper cl_mx_..._of doesn’t exist, and clight check reports the record with the names the target has.

/* Players respawn 5 blocks higher than vanilla would put them. */
static void high_respawn(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_player_respawn_before_place_of(ci)->y += 5.0;
}
CL_MIXIN_SITE(player_respawn, point, before_place, high_respawn);

/* Mobs only despawn when no player is within 64 blocks (vanilla: 128). */
static void near_despawn(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_entity_mob_check_despawn_far_distance_of(ci) = 64.0;
}
CL_MIXIN_SITE(entity_mob_check_despawn, value, far_distance, near_despawn);

At run time, set d.at = CL_AT_POINT or CL_AT_VALUE and d.site = CL_STR("before_knockback") in the cl_mixin_desc.

Calls are planned

CLight also has a third sub-point kind, call (CL_AT_CALL), for redirecting one call a target makes (like Java’s @Redirect). No API 1.5 target has a call site yet; they are Planned for the next game-logic batch, together with more points and values. Existing points and values never change, so what you write now keeps working.

Client points (API 1.16)

Client targets have points too. client.render.hud has one point per HUD layer, and cancelling a point skips that layer; client.render.fog and client.camera.setup have a computed point where you change what is drawn. These run only in your own game, and the HUD needs the ui.inject permission in your manifest.

/* No hotbar on this screen (needs "ui.inject"). */
static void no_hotbar(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mixin_cancel(ci);
}
CL_MIXIN_SITE(client_render_hud, point, hotbar, no_hotbar);

/* The fog starts twice as far away (only the picture changes, never what is loaded). */
static void far_fog(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_client_render_fog_computed* f = cl_mx_client_render_fog_computed_of(ci);
    f->render_distance_start *= 2.0f;
}
CL_MIXIN_SITE(client_render_fog, point, computed, far_fog);

World generation points (API 1.17)

World generation runs on the generator’s worker threads, and only WebAssembly mods can attach there. worldgen.biome.fill has a point per 4 x 4 x 4 cell where you can change the biome; worldgen.terrain.build has skip points before the noise fill, the surface rules and the carvers. Filter on the dimension. The same chunk may be generated more than once, so decide only from the frame (never from a counter).

static cl_ident g_deltas, g_wastes;   /* set in mod_main with cl_ident_intern */

/* Basalt deltas become nether wastes. */
static void no_deltas(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_worldgen_biome_fill_each_cell* cell = cl_mx_worldgen_biome_fill_each_cell_of(ci);
    if (cell->biome == g_deltas) cell->biome = g_wastes;
}
CL_MIXIN_SITE(worldgen_biome_fill, point, each_cell, no_deltas, .filter = "minecraft:the_nether");

/* A world without caves: the carvers are skipped in the Overworld. */
static void skip(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mixin_cancel(ci);
}
CL_MIXIN_SITE(worldgen_terrain_build, point, before_carvers, skip, .filter = "minecraft:overworld");

The other world and data points work like the drop points you know: loot.table.roll has each_drop, recipe.match has matched (cancel it to refuse the recipe), command.suggest has each_suggestion.

Try it

Make two rules: with every hit, entities are invulnerable for only 10 ticks instead of 20 (so fast weapons hit more often), and player respawns are rotated to look straight north (yaw 180).

Show a solution
static void short_invulnerability(cl_mixin_ci* ci, void* user) {
    (void)user;
    *cl_mx_entity_living_hurt_invulnerability_ticks_of(ci) = 10;
}
CL_MIXIN_SITE(entity_living_hurt, value, invulnerability_ticks, short_invulnerability);

static void face_north(cl_mixin_ci* ci, void* user) {
    (void)user;
    cl_mx_player_respawn_before_place_of(ci)->yaw = 180.0f;
}
CL_MIXIN_SITE(player_respawn, point, before_place, face_north);

invulnerability_ticks is a value, so you write through its pointer. before_place is a point whose yaw is mut, so you change it in the point’s struct.

Recap: CL_MIXIN_SITE(target, value, name, fn) replaces one number inside a target, and CL_MIXIN_SITE(target, point, name, fn) runs at a named place, where cl_mx_<target>_<point>_of(ci) holds its values and a cancel skips one step.