Skip to content

Points and values

Lesson 12.3

head and ret 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 choose them with clight::at::point("name") and clight::at::value("name").

Values

ci.value<V>() is a reference to the value. V is its type, which the target’s nested type tells you: clight::mx::PlayerAttack::CritMultiplier::Value is float.

// Critical hits do double damage instead of 1.5 times.
clight::mixin::inject<clight::mx::PlayerAttack>(clight::at::value("crit_multiplier"), [](clight::mx::PlayerAttack::Ci& ci) {
    ci.value<float>() = 2.0f;
});

// Sheep are knocked back three times as far.
clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::value("knockback_strength"), [](clight::mx::EntityLivingHurt::Ci& ci) {
    ci.value<double>() *= 3.0;
}, {.filter = "minecraft:sheep"});

These lambdas name the Ci type instead of writing auto& ci. value and locals are templates, and calling a template member on an auto parameter would need the awkward ci.template value<float>(). With the real type, ci.value<float>() just works.

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

ci.locals<S>() is a reference to the point’s struct of local values, where S is the point’s nested type:

// After a shield blocked part of a hit, the rest is halved too.
using Hurt = clight::mx::EntityLivingHurt;
clight::mixin::inject<Hurt>(clight::at::point("after_blocking"), [](Hurt::Ci& ci) {
    auto& p = ci.locals<Hurt::AfterBlocking>();
    if (p.blocked > 0.0f) p.amount *= 0.5f;   // amount is mut, blocked is read-only
});

ci.args() 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.
clight::mixin::inject<clight::mx::EntityLivingHurt>(clight::at::point("before_knockback"), [](auto& ci) {
    ci.cancel();
});

// No sweep attacks: the main target is hit, the mobs around it aren't.
clight::mixin::inject<clight::mx::PlayerAttack>(clight::at::point("before_sweep"), [](auto& ci) {
    ci.cancel();
});

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. The nested types are the names in CamelCase: Hurt::BeforeKnockback, clight::mx::PlayerRespawn::BeforePlace, clight::mx::EntityMobCheckDespawn::FarDistance. A misspelt name in clight::at::point("...") is an error at run time: inject returns CL_E_NOT_FOUND, and the log says CLL-7002: entity.living.hurt has no point 'before_knock'. The static form catches it earlier, in clight check:

// Players respawn 5 blocks higher than vanilla would put them.
CL_MIXIN_SITE(player_respawn, point, before_place, [](clight::mx::PlayerRespawn::Ci& ci) {
    ci.locals<clight::mx::PlayerRespawn::BeforePlace>().y += 5.0;
});

// Mobs only despawn when no player is within 64 blocks (vanilla: 128).
CL_MIXIN_SITE(entity_mob_check_despawn, value, far_distance, [](clight::mx::EntityMobCheckDespawn::Ci& ci) {
    ci.value<double>() = 64.0;
});

Calls are planned

CLight also has a third sub-point kind, clight::at::call("name"), 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").
clight::mixin::inject<clight::mx::ClientRenderHud>(clight::at::point("hotbar"), [](auto& ci) { ci.cancel(); });

// The fog starts twice as far away (only the picture changes, never what is loaded).
using Fog = clight::mx::ClientRenderFog;
clight::mixin::inject<Fog>(clight::at::point("computed"), [](Fog::Ci& ci) {
    ci.locals<Fog::Computed>().render_distance_start *= 2.0f;
});

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).

// Basalt deltas become nether wastes.
using Biomes = clight::mx::WorldgenBiomeFill;
static cl_ident deltas = 0, wastes = 0;  // set in your main with cl_ident_intern
clight::mixin::inject<Biomes>(clight::at::point("each_cell"), [](Biomes::Ci& ci) {
    auto& cell = ci.locals<Biomes::EachCell>();
    if (cell.biome == deltas) cell.biome = wastes;
}, {.filter = "minecraft:the_nether"});

// A world without caves: the carvers are skipped in the Overworld.
clight::mixin::inject<clight::mx::WorldgenTerrainBuild>(clight::at::point("before_carvers"), [](auto& ci) { ci.cancel(); },
                                                        {.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
CL_MIXIN_SITE(entity_living_hurt, value, invulnerability_ticks, [](clight::mx::EntityLivingHurt::Ci& ci) {
    ci.value<int32_t>() = 10;
});

CL_MIXIN_SITE(player_respawn, point, before_place, [](clight::mx::PlayerRespawn::Ci& ci) {
    ci.locals<clight::mx::PlayerRespawn::BeforePlace>().yaw = 180.0f;
});

invulnerability_ticks is a value, so you assign ci.value<int32_t>(). before_place is a point whose yaw is mut, so you change it in the point’s struct.

Recap: clight::at::value("name") replaces one number inside a target (ci.value<V>()), and clight::at::point("name") runs at a named place, where ci.locals<Target::Point>() holds its values and a cancel skips one step.