Mods: model handles (render.model-id, model-generation, set-models-by-id; API level 11) so mods send no model names each tick; clear-post and set-lighting documented

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 17:02:02 +03:00
committed by airdef
parent a321460f1c
commit 17d10d1d8c
6 changed files with 351 additions and 43 deletions
+16 -5
View File
@@ -11,7 +11,7 @@ use {
particles::{ParticleOptions, ParticleType},
},
horizon_core::random::RandomSource,
horizon_mods::{CameraView, ParticleSpawn, PointLight},
horizon_mods::{CameraView, ModelRef, ParticleSpawn, PointLight},
horizon_world::registry::BlockStateId,
std::collections::HashMap,
};
@@ -84,10 +84,21 @@ impl Game<'_> {
let registry = &self.pack.assets.registry;
let models = &mut self.mods.models;
for (owner, m) in self.mods.mods.models() {
let state = *models
.states
.entry(m.model.clone())
.or_insert_with(|| super::render_control::parse_state(registry, &m.model).ok());
let state = match &m.model {
ModelRef::Handle(h) => {
let id = BlockStateId(*h);
registry.state(id).map(|_| id)
}
ModelRef::Name(name) => {
if let Some(s) = models.states.get(name.as_str()) {
*s
} else {
let s = super::render_control::parse_state(registry, name).ok();
models.states.insert(name.clone(), s);
s
}
}
};
let Some(state) = state else {
continue;
};
+10
View File
@@ -615,6 +615,16 @@ impl Game<'_> {
}
let snapshot = self.mod_snapshot();
self.mods.mods.set_snapshot(snapshot);
let registry = self.pack.assets.registry;
self.mods
.mods
.set_model_resolver(registry as *const _ as usize, || {
Box::new(move |name| {
super::render_control::parse_state(registry, name)
.ok()
.map(|s| s.0)
})
});
self.mods.mods.tick(self.ticks);
self.update_mod_models();
self.show_mod_output();
+31
View File
@@ -467,6 +467,34 @@ interface render {
/// each id between its last two positions and rotations every frame.
/// An empty list removes them.
set-models: func(models: list<model-instance>);
/// A handle for a block state name, for `set-models-by-id` (AirDef
/// fork, API level 11): the name crosses once, not every tick. None for
/// a name that is no block state of the game's registry (nothing is
/// remembered for it). Handles are valid until `model-generation`
/// changes (the registry changed: a resource reload, another world's
/// data); ask again for every name then.
model-id: func(model: string) -> option<u32>;
/// Moves on whenever the handles `model-id` gave stop being valid. A
/// mod reads it before `set-models-by-id` and re-asks when it changed.
model-generation: func() -> u32;
/// `model-instance` with the model as a handle.
record model-instance-id {
id: u32,
/// From `model-id`.
model: u32,
x: f64,
y: f64,
z: f64,
rotation: tuple<f32, f32, f32, f32>,
scale: f32,
glow: bool,
}
/// `set-models` with handles instead of names: the same models, limits
/// and interpolation, fewer bytes across the boundary. `generation` is
/// the `model-generation` the handles were asked under; if it is not
/// the current one the call is ignored and returns false (re-ask the
/// handles and call again). Handles the game doesn't know draw nothing.
set-models-by-id: func(generation: u32, models: list<model-instance-id>) -> bool;
/// Particles to spawn on this client (AirDef fork).
record particle-spawn {
/// As `/particle` names the type: `minecraft:flame`, `minecraft:dust`.
@@ -508,6 +536,7 @@ interface render {
/// Compile errors come back in the chat and the log; the last good pass
/// keeps running.
set-post: func(wgsl: string);
/// Removes this mod's post pass: the world is shown as the game draws it.
clear-post: func();
/// The pass's `frame.params` (up to 1024 floats, as 256 vec4), kept until
/// replaced; call it once a tick (the pass gets the partial tick in
@@ -561,6 +590,8 @@ interface render {
/// How much bright things glow (0 none, 1 strong).
bloom: f32,
}
/// Switches the HD lighting on or off for this mod's world, with its
/// exposure and bloom; off, the world is drawn as the game draws it.
set-lighting: func(lighting: lighting);
}
+5 -2
View File
@@ -7,13 +7,13 @@
//! version's level so the launcher can tell before it installs.
/// The newest level this Horizon serves.
pub const LEVEL: u32 = 10;
pub const LEVEL: u32 = 11;
/// Every function the game offers mods, as `interface.function`, with its
/// level. Level 0 is the contract of Horizon 0.2.67 to 0.2.69. A new
/// function takes the next level and raises [`LEVEL`] (the `api_reference`
/// test fails until it is listed here).
pub const FUNCTIONS: [(&str, u32); 58] = [
pub const FUNCTIONS: [(&str, u32); 61] = [
("log.write", 0),
("game.get-player", 0),
("game.get-clock", 0),
@@ -73,6 +73,9 @@ pub const FUNCTIONS: [(&str, u32); 58] = [
("game.world-id", 9),
("render.set-lights", 10),
("render.set-lighting", 10),
("render.model-id", 11),
("render.model-generation", 11),
("render.set-models-by-id", 11),
];
/// What the game's program carries so the launcher can read its level
+176 -36
View File
@@ -79,9 +79,10 @@ use render_bindings::horizon::mods::input;
pub use render_bindings::horizon::mods::input::{Event as InputEvent, HeldItem, ItemUse};
/// What capability `render` asks the game to draw (WIT `interface render`).
pub use render_bindings::horizon::mods::render::{
Corner as RenderCorner, FarTerrain as ModFarTerrain, FillShape, FrameRate, ItemShape,
Minimap as ModMinimap, ModelInstance, ParticleSpawn, Point as RenderPoint, QuadShape,
CameraView, Lighting as ModLighting, PointLight, Rect as RenderRect, Shape, SpriteShape, TextShape, ViewShape, Zoom as ModZoom,
CameraView, Corner as RenderCorner, FarTerrain as ModFarTerrain, FillShape, FrameRate,
ItemShape, Lighting as ModLighting, Minimap as ModMinimap, ModelInstance, ModelInstanceId,
ParticleSpawn, Point as RenderPoint, PointLight, QuadShape, Rect as RenderRect, Shape,
SpriteShape, TextShape, ViewShape, Zoom as ModZoom,
};
use render_bindings::{RenderHost, horizon::mods::render};
/// Capability `recipes`'s imports (docs/mods.md): the game's recipes and items.
@@ -278,6 +279,42 @@ pub trait WorldView: std::any::Any + Send {
/// The view lent for the current calls, shared by every mod's store.
type WorldSlot = Arc<Mutex<Option<Box<dyn WorldView>>>>;
/// How the game turns a block state name into a handle (`render.model-id`):
/// None for a name that is no state of its registry.
pub type ModelResolver = Box<dyn Fn(&str) -> Option<u32> + Send>;
/// The handles of `render.model-id`: the game's registry decides which names
/// have one, so nothing grows with what mods ask; `generation` moves on
/// when the registry changes and the handles with it.
#[derive(Default)]
pub struct ModelTable {
key: usize,
generation: u32,
resolve: Option<ModelResolver>,
}
type ModelSlot = Arc<Mutex<ModelTable>>;
/// A model as a mod set it: by name, or by a handle of the [`ModelTable`].
#[derive(Clone, Debug)]
pub enum ModelRef {
Name(String),
Handle(u32),
}
/// One world model (`render.set-models` or `set-models-by-id`).
#[derive(Clone, Debug)]
pub struct HostModel {
pub id: u32,
pub model: ModelRef,
pub x: f64,
pub y: f64,
pub z: f64,
pub rotation: (f32, f32, f32, f32),
pub scale: f32,
pub glow: bool,
}
/// A mod's text panel, drawn every frame until it changes.
#[derive(Clone, Debug)]
pub struct Panel {
@@ -429,7 +466,9 @@ struct State {
/// Capability `render`'s far terrain, when the mod draws one.
far_terrain: Option<ModFarTerrain>,
/// Capability `render`'s world models (AirDef fork), drawn until replaced.
models: Vec<ModelInstance>,
models: Vec<HostModel>,
/// The game's block state names -> handles, shared by every mod's store.
model_table: ModelSlot,
/// `render.query-ground`'s columns, answered before the next tick.
ground_queries: Vec<(i32, i32)>,
/// The game's answers to them.
@@ -464,6 +503,7 @@ pub struct Mods {
snapshot: Arc<Snapshot>,
out: Vec<Output>,
world: WorldSlot,
model_table: ModelSlot,
/// Moves on whenever a mod starts or stops: the game resends the server
/// mods' commands to players when it changes.
generation: u64,
@@ -492,6 +532,26 @@ impl Mods {
*slot = Some(view);
}
}
/// The game's registry for `render.model-id`. `key` names the registry
/// (its address): the same key keeps the handles, another one makes
/// `model-generation` move on and drops every mod's models. `make` runs
/// only then.
pub fn set_model_resolver(&mut self, key: usize, make: impl FnOnce() -> ModelResolver) {
let Ok(mut t) = self.model_table.lock() else {
return;
};
if t.key != key || t.resolve.is_none() {
t.key = key;
t.generation = t.generation.wrapping_add(1);
t.resolve = Some(make());
drop(t);
for e in &mut self.entries {
if let Some(r) = e.run.as_mut() {
r.store.data_mut().models.clear();
}
}
}
}
/// The view [`Mods::lend_world`] lent, back to the game.
pub fn take_world(&mut self) -> Option<Box<dyn WorldView>> {
self.world.lock().ok()?.take()
@@ -613,6 +673,7 @@ impl Mods {
.collect();
let snapshot = self.snapshot.clone();
let world = self.world.clone();
let model_table = self.model_table.clone();
let (engine, linker) = self.engine()?;
let component =
Component::new(engine, wasm).map_err(|e| format!("not a component: {e}"))?;
@@ -646,6 +707,7 @@ impl Mods {
minimap: None,
far_terrain: None,
models: Vec::new(),
model_table: model_table.clone(),
ground_queries: Vec::new(),
ground: Vec::new(),
post: None,
@@ -1209,7 +1271,7 @@ impl Mods {
}
/// Every running mod's world models (AirDef fork), with the mod's index
/// so ids of different mods don't meet.
pub fn models(&self) -> impl Iterator<Item = (usize, &ModelInstance)> {
pub fn models(&self) -> impl Iterator<Item = (usize, &HostModel)> {
self.entries.iter().enumerate().flat_map(|(i, e)| {
e.run
.iter()
@@ -1234,7 +1296,9 @@ impl Mods {
}
/// The first running mod's lighting, if it set one.
pub fn lighting(&self) -> Option<ModLighting> {
self.entries.iter().find_map(|e| e.run.as_ref()?.store.data().lighting)
self.entries
.iter()
.find_map(|e| e.run.as_ref()?.store.data().lighting)
}
/// The columns each running mod asked the ground height of, taken.
pub fn take_ground_queries(&mut self) -> Vec<(usize, Vec<(i32, i32)>)> {
@@ -1435,6 +1499,29 @@ fn reason(e: &wasmtime::Error) -> String {
}
}
/// The models a mod set, limited: finite, normalised, at most [`MODELS`].
fn clean_models(models: impl Iterator<Item = HostModel>) -> Vec<HostModel> {
let finite = |v: f64| v.is_finite() && v.abs() < 30_000_000.0;
models
.filter(|m| finite(m.x) && finite(m.y) && finite(m.z))
.take(MODELS)
.map(|mut m| {
let (x, y, z, w) = m.rotation;
let len = (x * x + y * y + z * z + w * w).sqrt();
m.rotation = if len.is_finite() && len > 1e-6 {
(x / len, y / len, z / len, w / len)
} else {
(0.0, 0.0, 0.0, 1.0)
};
m.scale = if m.scale.is_finite() {
m.scale.clamp(0.0, MODEL_SCALE)
} else {
1.0
};
m
})
.collect()
}
fn clip(text: String) -> String {
clip_to(text, LINE_CHARS)
}
@@ -1949,28 +2036,45 @@ impl render::Host for State {
}
}
fn set_models(&mut self, models: Vec<ModelInstance>) {
let finite = |v: f64| v.is_finite() && v.abs() < 30_000_000.0;
self.models = models
.into_iter()
.filter(|m| finite(m.x) && finite(m.y) && finite(m.z))
.take(MODELS)
.map(|mut m| {
let (x, y, z, w) = m.rotation;
let len = (x * x + y * y + z * z + w * w).sqrt();
m.rotation = if len.is_finite() && len > 1e-6 {
(x / len, y / len, z / len, w / len)
} else {
(0.0, 0.0, 0.0, 1.0)
};
m.scale = if m.scale.is_finite() {
m.scale.clamp(0.0, MODEL_SCALE)
} else {
1.0
};
m.model = clip(m.model);
m
})
.collect();
let models = models.into_iter().map(|m| HostModel {
id: m.id,
model: ModelRef::Name(clip(m.model)),
x: m.x,
y: m.y,
z: m.z,
rotation: m.rotation,
scale: m.scale,
glow: m.glow,
});
self.models = clean_models(models);
}
fn model_id(&mut self, model: String) -> Option<u32> {
let t = self.model_table.lock().ok()?;
(t.resolve.as_ref()?)(&model)
}
fn model_generation(&mut self) -> u32 {
self.model_table.lock().map_or(0, |t| t.generation)
}
fn set_models_by_id(&mut self, generation: u32, models: Vec<ModelInstanceId>) -> bool {
if self
.model_table
.lock()
.map_or(true, |t| t.generation != generation)
{
return false;
}
let models = models.into_iter().map(|m| HostModel {
id: m.id,
model: ModelRef::Handle(m.model),
x: m.x,
y: m.y,
z: m.z,
rotation: m.rotation,
scale: m.scale,
glow: m.glow,
});
self.models = clean_models(models);
true
}
fn spawn_particles(&mut self, particles: Vec<ParticleSpawn>) {
let particles: Vec<ParticleSpawn> = particles
@@ -2012,7 +2116,11 @@ impl render::Host for State {
.map(|mut v| {
v.yaw = angle(v.yaw);
v.pitch = angle(v.pitch).clamp(-90.0, 90.0);
v.fov = if v.fov.is_finite() { v.fov.clamp(1.0, 110.0) } else { 70.0 };
v.fov = if v.fov.is_finite() {
v.fov.clamp(1.0, 110.0)
} else {
70.0
};
v.width = v.width.clamp(16, 1920);
v.height = v.height.clamp(16, 1080);
v
@@ -2021,22 +2129,44 @@ impl render::Host for State {
}
fn set_lights(&mut self, lights: Vec<PointLight>) {
let finite = |v: f64| v.is_finite() && v.abs() < 30_000_000.0;
let unit = |v: f32| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 };
let unit = |v: f32| {
if v.is_finite() {
v.clamp(0.0, 1.0)
} else {
0.0
}
};
self.lights = lights
.into_iter()
.filter(|l| finite(l.x) && finite(l.y) && finite(l.z))
.take(512)
.map(|mut l| {
l.color = (unit(l.color.0), unit(l.color.1), unit(l.color.2));
l.intensity = if l.intensity.is_finite() { l.intensity.clamp(0.0, 100_000.0) } else { 0.0 };
l.radius = if l.radius.is_finite() { l.radius.clamp(1.0, 256.0) } else { 1.0 };
l.intensity = if l.intensity.is_finite() {
l.intensity.clamp(0.0, 100_000.0)
} else {
0.0
};
l.radius = if l.radius.is_finite() {
l.radius.clamp(1.0, 256.0)
} else {
1.0
};
l
})
.collect();
}
fn set_lighting(&mut self, mut lighting: ModLighting) {
lighting.exposure = if lighting.exposure.is_finite() { lighting.exposure.clamp(0.05, 8.0) } else { 1.0 };
lighting.bloom = if lighting.bloom.is_finite() { lighting.bloom.clamp(0.0, 1.0) } else { 0.0 };
lighting.exposure = if lighting.exposure.is_finite() {
lighting.exposure.clamp(0.05, 8.0)
} else {
1.0
};
lighting.bloom = if lighting.bloom.is_finite() {
lighting.bloom.clamp(0.0, 1.0)
} else {
0.0
};
self.lighting = Some(lighting);
}
fn query_ground(&mut self, columns: Vec<(i32, i32)>) {
@@ -2050,8 +2180,18 @@ impl render::Host for State {
if !(x.is_finite() && y.is_finite() && z.is_finite()) {
return;
}
let ok = |v: f32, max: f32| if v.is_finite() { v.clamp(0.0, max) } else { 1.0 };
let sounds = self.out.iter().filter(|o| matches!(o, Output::Sound { .. })).count();
let ok = |v: f32, max: f32| {
if v.is_finite() {
v.clamp(0.0, max)
} else {
1.0
}
};
let sounds = self
.out
.iter()
.filter(|o| matches!(o, Output::Sound { .. }))
.count();
if sounds < 64 {
self.out.push(Output::Sound {
event: clip(event),
+113
View File
@@ -125,6 +125,7 @@ A block along the player's view.
| --- | --- | --- |
| `get-player` | `func() -> option<player>` | None on the title screen. |
| `get-clock` | `func() -> option<clock>` | None on the title screen. |
| `world-id` | `func() -> option<string>` | Which world is open (AirDef fork): `world:<folder>` for a saved world, `server:<address>` on a server; none on the title screen. Mods keep their state apart by it. |
| `raycast` | `func(distance: f32) -> option<hit>` | The first block along the player's view (fluids pass), up to `distance` blocks (at most 1024; past the loaded chunks, across the distant terrain when LOD Distance is on), as of this tick. None when nothing is hit, and on the title screen. |
## `chat`
@@ -399,6 +400,16 @@ Any four-cornered shape (a wheel's segment, an arrow): the corners in order roun
| `centered` | `bool` | Centred on `at.x` instead of starting there. |
| `shadow` | `bool` | |
### record `view-shape`
A picture of one of the mod's `set-views` cameras (AirDef fork).
| Field | Type | |
| --- | --- | --- |
| `view` | `u32` | The camera's id; an unknown one draws nothing. |
| `rect` | `rect` | |
| `mode` | `u32` | 0 as seen, 1 thermal (white hot), 2 night vision (green). |
### variant `shape`
| Case | Payload | |
@@ -408,6 +419,7 @@ Any four-cornered shape (a wheel's segment, an arrow): the corners in order roun
| `sprite` | `sprite-shape` | |
| `item` | `item-shape` | |
| `text` | `text-shape` | |
| `view` | `view-shape` | |
### record `frame-rate`
@@ -442,6 +454,93 @@ Any four-cornered shape (a wheel's segment, an arrow): the corners in order roun
| `distance` | `u32` | How far the distant terrain reaches, in chunks: 64 to 1024. On integrated graphics other than Apple's the game caps it lower. |
| `reduced` | `bool` | Coarser far tiles, as the Far Detail setting's Reduced: 2 to 4 ms less GPU time per frame on integrated graphics. |
### record `model-instance`
A block model drawn in the world (AirDef fork).
| Field | Type | |
| --- | --- | --- |
| `id` | `u32` | Stays the same while the object lives: the game moves an id smoothly from where the last `set-models` put it. |
| `model` | `string` | A block state as commands write it (`airdef:shahed`, `minecraft:stone`, `minecraft:furnace[facing=east]`): its block model is drawn. An unknown one draws nothing. |
| `x` | `f64` | Where the model's centre is, in blocks. |
| `y` | `f64` | |
| `z` | `f64` | |
| `rotation` | `tuple<f32, f32, f32, f32>` | Rotation about the model's centre: a quaternion (x, y, z, w). |
| `scale` | `f32` | Uniform scale, 1.0 a block (0 to 64). |
| `glow` | `bool` | Drawn at full brightness, whatever the light where it is. |
### record `model-instance-id`
`model-instance` with the model as a handle.
| Field | Type | |
| --- | --- | --- |
| `id` | `u32` | |
| `model` | `u32` | From `model-id`. |
| `x` | `f64` | |
| `y` | `f64` | |
| `z` | `f64` | |
| `rotation` | `tuple<f32, f32, f32, f32>` | |
| `scale` | `f32` | |
| `glow` | `bool` | |
### record `particle-spawn`
Particles to spawn on this client (AirDef fork).
| Field | Type | |
| --- | --- | --- |
| `particle` | `string` | As `/particle` names the type: `minecraft:flame`, `minecraft:dust`. |
| `x` | `f64` | |
| `y` | `f64` | |
| `z` | `f64` | |
| `spread` | `tuple<f32, f32, f32>` | Gaussian spread round the point, per axis, in blocks. |
| `velocity` | `tuple<f32, f32, f32>` | Every particle's starting velocity, blocks a tick. |
| `speed` | `f32` | Gaussian speed added to it per axis. |
| `count` | `u32` | 1 to 256. |
| `color` | `u32` | 0xRRGGBB for dust and coloured types (0: white). |
| `scale` | `f32` | Dust size (0.01 to 4). |
### record `camera-view`
A camera the game renders the world from besides the player's eye (AirDef fork): the land, the sky and the entities near the player.
| Field | Type | |
| --- | --- | --- |
| `id` | `u32` | Stays the same while the camera lives: the game turns it smoothly between the last two `set-views`. |
| `x` | `f64` | |
| `y` | `f64` | |
| `z` | `f64` | |
| `yaw` | `f32` | Degrees, as the player's: yaw 0 looks south (+z), pitch 90 down. |
| `pitch` | `f32` | |
| `fov` | `f32` | Vertical field of view, degrees (1 to 110). |
| `width` | `u32` | The picture's size in pixels (16 to 1920 × 16 to 1080). |
| `height` | `u32` | |
### record `point-light`
A light the mod's things give off (AirDef fork): rocket flames, flares, blasts. Drawn by the HD lighting (`set-lighting`).
| Field | Type | |
| --- | --- | --- |
| `id` | `u32` | Stays the same while the light lives: the game moves it smoothly. |
| `x` | `f64` | |
| `y` | `f64` | |
| `z` | `f64` | |
| `color` | `tuple<f32, f32, f32>` | Linear RGB, 0 to 1. |
| `intensity` | `f32` | Brightness: about what it lights one block away (0 to 100000). |
| `radius` | `f32` | Nothing past this many blocks (1 to 256). |
### record `lighting`
The HD lighting (AirDef fork): the world lit per pixel by the mods' lights, in high dynamic range, with bloom and tone mapping.
| Field | Type | |
| --- | --- | --- |
| `hd` | `bool` | |
| `exposure` | `f32` | 1 as is; more brightens. |
| `bloom` | `f32` | How much bright things glow (0 none, 1 strong). |
| Function | Signature | What it does |
| --- | --- | --- |
| `set-hud` | `func(shapes: list<shape>)` | Replaces the mod's HUD shapes: up to 1024, drawn every frame in order over the HUD (after the hotbar, under chat and screens), hidden with the HUD (F1). An empty list removes them. |
@@ -454,6 +553,20 @@ Any four-cornered shape (a wheel's segment, an arrow): the corners in order roun
| `set-zoom` | `func(zoom: zoom)` | Zooms the camera until the next call (`factor` 1 ends it), only while playing: a screen opening or the spyglass's own view (which wins) leave the camera vanilla's. With several mods, the strongest zoom counts. |
| `set-minimap` | `func(minimap: option<minimap>)` | Shows a map of the world around the player in `corner`, stacked with the game's overlays and the mods' panels, until the next call (none hides it). It looks like a map item: vanilla's map colours with their height shading and water depth, and the player's arrow. Underground, under a roof and in the Nether it maps the floor around the player's height. The game keeps the map's tiles and refreshes them within its own frame budget; hidden, they are dropped. One map shows at a time: the first loaded mod's. |
| `set-far-terrain` | `func(far-terrain: option<far-terrain>)` | Draws the world beyond the render distance as distant terrain, out to `distance` chunks, until the next call (none ends it). The game builds it on its own workers, from the chunks already loaded or saved and, where nobody has been, from the world's generator; the simulation distance doesn't change. Only worlds whose generator the game has (single player and a LAN world you host) show it. The player's own LOD Distance setting, when on, wins. One far terrain shows at a time: the first loaded mod's. |
| `set-models` | `func(models: list<model-instance>)` | Replaces the mod's models: up to 4096, drawn with the world's entities until replaced. Call it once a tick: the game interpolates each id between its last two positions and rotations every frame. An empty list removes them. |
| `model-id` | `func(model: string) -> option<u32>` | A handle for a block state name, for `set-models-by-id` (AirDef fork, API level 11): the name crosses once, not every tick. None for a name that is no block state of the game's registry (nothing is remembered for it). Handles are valid until `model-generation` changes (the registry changed: a resource reload, another world's data); ask again for every name then. |
| `model-generation` | `func() -> u32` | Moves on whenever the handles `model-id` gave stop being valid. A mod reads it before `set-models-by-id` and re-asks when it changed. |
| `set-models-by-id` | `func(generation: u32, models: list<model-instance-id>) -> bool` | `set-models` with handles instead of names: the same models, limits and interpolation, fewer bytes across the boundary. `generation` is the `model-generation` the handles were asked under; if it is not the current one the call is ignored and returns false (re-ask the handles and call again). Handles the game doesn't know draw nothing. |
| `spawn-particles` | `func(particles: list<particle-spawn>)` | Spawns particles on this client only: no command, no server. At most 512 spawns a call. |
| `play-sound` | `func(event: string, x: f64, y: f64, z: f64, volume: f32, pitch: f32)` | Plays a sound event (`minecraft:entity.generic.explode`, `<mod>:<name>`) at a point on this client. |
| `query-ground` | `func(columns: list<tuple<s32, s32>>)` | Asks for the ground height of these columns (x, z): at most 512 a tick, answered by `ground` from the next tick on. |
| `ground` | `func() -> list<tuple<s32, s32, s32>>` | The answers to the last tick's `query-ground`: (x, z, y), y one above the highest block that blocks motion or holds a fluid, as the motion blocking heightmap; -64 where the chunk isn't loaded. |
| `set-post` | `func(wgsl: string)` | A full-screen post pass (AirDef fork): WGSL defining `fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32>`, after a prelude with `frame` (camera matrices, time, `params`), the world's colour and depth and helpers (`scene`, `depth-at`, `view-dir`, `scene-distance`, `project`). It runs after the world, before the hand and the HUD. Compile errors come back in the chat and the log; the last good pass keeps running. |
| `clear-post` | `func()` | Removes this mod's post pass: the world is shown as the game draws it. |
| `set-params` | `func(params: list<f32>)` | The pass's `frame.params` (up to 1024 floats, as 256 vec4), kept until replaced; call it once a tick (the pass gets the partial tick in `frame.time.y`). |
| `set-views` | `func(views: list<camera-view>)` | Replaces the mod's cameras: up to 8. A `view` shape shows one; the game renders up to three a frame, in turn. |
| `set-lights` | `func(lights: list<point-light>)` | Replaces the mod's lights: up to 512, interpolated per id between the last two calls. |
| `set-lighting` | `func(lighting: lighting)` | Switches the HD lighting on or off for this mod's world, with its exposure and bloom; off, the world is drawn as the game draws it. |
## `events`