Mods: world queries for simulations (render.query-rays, query-sections, world-changes; API level 12): block-exact rays and section occupancy by collision, sight or sound, answered next tick within budgets, and a log of changed sections
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,8 @@ mod mod_models;
|
||||
#[cfg(feature = "mods")]
|
||||
mod mod_recipes;
|
||||
mod mod_render;
|
||||
#[cfg(feature = "mods")]
|
||||
mod mod_world;
|
||||
mod mods;
|
||||
mod mount_menu;
|
||||
mod mounts;
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//! AirDef fork: capability `render`'s world queries for simulations
|
||||
//! (`render.query-rays`, `render.query-sections`, `render.world-changes`).
|
||||
//! Mods ask in a tick and are answered before the next one, from the chunks
|
||||
//! the game holds: a chunk it doesn't have is `unknown`, never air. The
|
||||
//! walk and the bit packing are `horizon_mods::world_query`; this is the
|
||||
//! game's side: which blocks count under each policy, and the chunks.
|
||||
use super::Game;
|
||||
use horizon_client::chunk::{Chunk, MIN_Y, SECTIONS};
|
||||
use horizon_client::world::World;
|
||||
use horizon_core::geometry::Axis;
|
||||
use horizon_mods::world_query::{self, BlockProbe, Cell, SectionBits, Trace};
|
||||
use horizon_mods::{
|
||||
SectionFill, WorldChanges, WorldPolicy, WorldRay, WorldRayAnswer, WorldSection, WorldVerdict,
|
||||
};
|
||||
use horizon_world::registry::BlockStateId;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Block cells all of a tick's segments may walk: the rest are `unknown`.
|
||||
const CELLS_PER_TICK: usize = 200_000;
|
||||
|
||||
/// Policy bits of [`WorldTables::flags`].
|
||||
const COLLISION: u8 = 1;
|
||||
const SIGHT: u8 = 2;
|
||||
const SOUND: u8 = 4;
|
||||
|
||||
/// What each block state is to the three policies, and its boxes.
|
||||
#[derive(Default)]
|
||||
pub(super) struct WorldTables {
|
||||
/// Per state: the policy bits that it stops.
|
||||
flags: Vec<u8>,
|
||||
/// Per state: its collision boxes in block space; none for a full cube
|
||||
/// (or a state that stops nothing).
|
||||
boxes: Vec<Option<Box<[[f64; 6]]>>>,
|
||||
/// The world the tables were made for.
|
||||
epoch: u32,
|
||||
}
|
||||
|
||||
fn mask(policy: WorldPolicy) -> u8 {
|
||||
match policy {
|
||||
WorldPolicy::Collision => COLLISION,
|
||||
WorldPolicy::Sight => SIGHT,
|
||||
WorldPolicy::Sound => SOUND,
|
||||
}
|
||||
}
|
||||
|
||||
/// The blocks of the resident chunks, for [`world_query::trace`].
|
||||
struct Probe<'a> {
|
||||
world: &'a World,
|
||||
tables: &'a WorldTables,
|
||||
mask: u8,
|
||||
last: Option<((i32, i32), &'a Arc<Chunk>)>,
|
||||
cells: usize,
|
||||
}
|
||||
impl BlockProbe for Probe<'_> {
|
||||
fn cell(&mut self, x: i32, y: i32, z: i32) -> Cell<'_> {
|
||||
self.cells += 1;
|
||||
let key = (x >> 4, z >> 4);
|
||||
let chunk = match self.last {
|
||||
Some((k, c)) if k == key => c,
|
||||
_ => {
|
||||
let Some(c) = self.world.chunks.get(&key) else {
|
||||
return Cell::Unknown;
|
||||
};
|
||||
self.last = Some((key, c));
|
||||
c
|
||||
}
|
||||
};
|
||||
let state = chunk.get(x & 15, y, z & 15) as usize;
|
||||
if self
|
||||
.tables
|
||||
.flags
|
||||
.get(state)
|
||||
.is_none_or(|f| f & self.mask == 0)
|
||||
{
|
||||
return Cell::Empty;
|
||||
}
|
||||
Cell::Solid(self.tables.boxes[state].as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// The tables for this world, made when it is new.
|
||||
fn world_tables(&mut self) -> Arc<WorldTables> {
|
||||
let epoch = self.world.epoch;
|
||||
if let Some(t) = &self.mods.world_tables
|
||||
&& t.epoch == epoch
|
||||
&& t.flags.len() == self.res.facts().states.len()
|
||||
{
|
||||
return t.clone();
|
||||
}
|
||||
let facts = self.res.facts();
|
||||
let cached = self.collision.cached_shapes();
|
||||
let mut flags = vec![0u8; facts.states.len()];
|
||||
let mut boxes: Vec<Option<Box<[[f64; 6]]>>> = vec![None; facts.states.len()];
|
||||
for (i, f) in facts.states.iter().enumerate() {
|
||||
let state = BlockStateId(i as u32);
|
||||
if facts.is_air(state) {
|
||||
continue;
|
||||
}
|
||||
let shape = cached.cached_shape(state).ok().flatten();
|
||||
let (solid, list): (bool, Option<Vec<[f64; 6]>>) = match shape {
|
||||
Some(s) if s.is_empty() => (false, None),
|
||||
Some(s) if s.is_block() => (true, None),
|
||||
Some(s) => (
|
||||
true,
|
||||
Some(
|
||||
s.boxes()
|
||||
.iter()
|
||||
.map(|b| {
|
||||
[Axis::X, Axis::Y, Axis::Z, Axis::X, Axis::Y, Axis::Z]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(k, a)| if k < 3 { b.min(a) } else { b.max(a) })
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
// Its shape depends on the neighbours: the outline stands in,
|
||||
// fluids and plants aside.
|
||||
None => (
|
||||
f.fluid == horizon_items::blocks::Fluid::None
|
||||
&& !f.outline.is_empty()
|
||||
&& f.full_collision,
|
||||
None,
|
||||
),
|
||||
};
|
||||
if !solid {
|
||||
continue;
|
||||
}
|
||||
let full = shape.is_some_and(|s| s.is_block()) || (shape.is_none() && f.full_collision);
|
||||
flags[i] = COLLISION
|
||||
| if f.can_occlude { SIGHT } else { 0 }
|
||||
| if full || f.full_collision { SOUND } else { 0 };
|
||||
boxes[i] = list.map(Vec::into_boxed_slice);
|
||||
}
|
||||
let tables = Arc::new(WorldTables {
|
||||
flags,
|
||||
boxes,
|
||||
epoch,
|
||||
});
|
||||
self.mods.world_tables = Some(tables.clone());
|
||||
tables
|
||||
}
|
||||
|
||||
/// Answers what the running mods asked of the world last tick, and tells
|
||||
/// them what changed since.
|
||||
pub(super) fn answer_mod_world(&mut self) {
|
||||
let queries = self.mods.mods.take_world_queries();
|
||||
if queries.is_empty() {
|
||||
self.world.mod_changes.watching = false;
|
||||
return;
|
||||
}
|
||||
self.world.mod_changes.watching = true;
|
||||
let tables = self.world_tables();
|
||||
let (changed, overflow) = self.world.mod_changes.take();
|
||||
let changes = WorldChanges {
|
||||
epoch: self.world.epoch,
|
||||
revision: self.world.revision,
|
||||
overflow,
|
||||
sections: changed,
|
||||
};
|
||||
let mut budget = CELLS_PER_TICK;
|
||||
for (i, q) in queries {
|
||||
let mut rays = Vec::with_capacity(q.rays.len() + q.over.len());
|
||||
for r in &q.rays {
|
||||
let (answer, cells) = self.trace_ray(&tables, r, budget);
|
||||
budget = budget.saturating_sub(cells);
|
||||
rays.push(answer);
|
||||
}
|
||||
rays.extend(q.over.iter().map(|&id| unknown(id, 0.0)));
|
||||
let mut sections = Vec::new();
|
||||
for (policy, list) in &q.sections {
|
||||
for &(x, y, z) in list {
|
||||
sections.push(self.section_of(&tables, mask(*policy), x, y, z));
|
||||
}
|
||||
}
|
||||
self.mods
|
||||
.mods
|
||||
.answer_world(i, rays, sections, changes.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_ray(
|
||||
&self,
|
||||
tables: &WorldTables,
|
||||
r: &WorldRay,
|
||||
budget: usize,
|
||||
) -> (WorldRayAnswer, usize) {
|
||||
if budget == 0 {
|
||||
return (unknown(r.id, 0.0), 0);
|
||||
}
|
||||
let mut probe = Probe {
|
||||
world: &self.world,
|
||||
tables,
|
||||
mask: mask(r.policy),
|
||||
last: None,
|
||||
cells: 0,
|
||||
};
|
||||
let t = world_query::trace(
|
||||
&mut probe,
|
||||
[r.start.0, r.start.1, r.start.2],
|
||||
[r.end.0, r.end.1, r.end.2],
|
||||
);
|
||||
let answer = match t {
|
||||
Trace::Clear => WorldRayAnswer {
|
||||
id: r.id,
|
||||
verdict: WorldVerdict::Clear,
|
||||
fraction: 1.0,
|
||||
normal: (0, 0, 0),
|
||||
block: (0, 0, 0),
|
||||
known_to: 1.0,
|
||||
},
|
||||
Trace::Hit {
|
||||
fraction,
|
||||
normal,
|
||||
block,
|
||||
} => WorldRayAnswer {
|
||||
id: r.id,
|
||||
verdict: WorldVerdict::Hit,
|
||||
fraction,
|
||||
normal: (normal[0], normal[1], normal[2]),
|
||||
block: (block[0], block[1], block[2]),
|
||||
known_to: 1.0,
|
||||
},
|
||||
Trace::Unknown { known_to } => unknown(r.id, known_to),
|
||||
};
|
||||
(answer, probe.cells)
|
||||
}
|
||||
|
||||
fn section_of(&self, tables: &WorldTables, mask: u8, x: i32, y: i32, z: i32) -> WorldSection {
|
||||
let mut s = WorldSection {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
fill: SectionFill::Unknown,
|
||||
bits: Vec::new(),
|
||||
};
|
||||
let Some(chunk) = self.world.chunks.get(&(x, z)) else {
|
||||
return s;
|
||||
};
|
||||
let index = y - (MIN_Y >> 4);
|
||||
if !(0..SECTIONS as i32).contains(&index) {
|
||||
s.fill = SectionFill::Empty;
|
||||
return s;
|
||||
}
|
||||
let section = &chunk.sections[index as usize];
|
||||
let mut states = [0u32; 4096];
|
||||
if let Some(v) = section.blocks.uniform() {
|
||||
states.fill(v);
|
||||
} else {
|
||||
section.unpack(&mut states);
|
||||
}
|
||||
match world_query::section_bits(&states, &tables.flags, mask) {
|
||||
SectionBits::Empty => s.fill = SectionFill::Empty,
|
||||
SectionBits::Full => s.fill = SectionFill::Full,
|
||||
SectionBits::Mixed(bits) => {
|
||||
s.fill = SectionFill::Mixed;
|
||||
s.bits = bits.to_vec();
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
fn unknown(id: u32, known_to: f64) -> WorldRayAnswer {
|
||||
WorldRayAnswer {
|
||||
id,
|
||||
verdict: WorldVerdict::Unknown,
|
||||
fraction: 0.0,
|
||||
normal: (0, 0, 0),
|
||||
block: (0, 0, 0),
|
||||
known_to,
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,9 @@ pub(super) struct ModsState {
|
||||
/// Capability `render`'s camera shakes.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) shake: super::mod_render::Shake,
|
||||
/// What blocks are to capability `render`'s world queries (AirDef fork).
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) world_tables: Option<std::sync::Arc<super::mod_world::WorldTables>>,
|
||||
/// Capability `render`'s world models (AirDef fork), with their last poses.
|
||||
#[cfg(feature = "mods")]
|
||||
pub(super) models: super::mod_models::ModModels,
|
||||
@@ -613,6 +616,7 @@ impl Game<'_> {
|
||||
.collect();
|
||||
self.mods.mods.answer_ground(i, ground);
|
||||
}
|
||||
self.answer_mod_world();
|
||||
let snapshot = self.mod_snapshot();
|
||||
self.mods.mods.set_snapshot(snapshot);
|
||||
let registry = self.pack.assets.registry;
|
||||
|
||||
@@ -99,7 +99,40 @@ pub struct World {
|
||||
/// The moving-piston block entities of the level, by position, copied
|
||||
/// from the block ticks after each tick: collision and rendering read them.
|
||||
pub moving_pistons: Vec<([i32; 3], horizon_world::piston_shape::MovingPistonState)>,
|
||||
/// A number of this world alone (a new one for every level opened): what
|
||||
/// mods cache about the blocks is void when it changes.
|
||||
pub epoch: u32,
|
||||
/// The sections whose blocks changed since a mod last asked, kept only
|
||||
/// while a mod watches (`render.world-changes`).
|
||||
pub mod_changes: ModChanges,
|
||||
}
|
||||
/// The block changes a mod's cache of the world must hear of.
|
||||
#[derive(Default)]
|
||||
pub struct ModChanges {
|
||||
pub watching: bool,
|
||||
pub sections: crate::fast_hash::FxHashSet<(i32, i32, i32)>,
|
||||
/// More sections changed than [`ModChanges::MAX`]: all count as changed.
|
||||
pub overflow: bool,
|
||||
}
|
||||
impl ModChanges {
|
||||
/// Sections kept between two reads.
|
||||
pub const MAX: usize = 256;
|
||||
fn note(&mut self, x: i32, y: i32, z: i32) {
|
||||
if self.sections.len() >= Self::MAX {
|
||||
self.overflow = true;
|
||||
} else {
|
||||
self.sections.insert((x >> 4, y >> 4, z >> 4));
|
||||
}
|
||||
}
|
||||
/// What changed since the last call, as the mod reads it.
|
||||
pub fn take(&mut self) -> (Vec<(i32, i32, i32)>, bool) {
|
||||
(
|
||||
self.sections.drain().collect(),
|
||||
std::mem::take(&mut self.overflow),
|
||||
)
|
||||
}
|
||||
}
|
||||
static NEXT_EPOCH: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1);
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Weather {
|
||||
pub clear_time: i32,
|
||||
@@ -192,6 +225,8 @@ impl World {
|
||||
moving_piston_fallback: Vec::new(),
|
||||
moving_pistons: Vec::new(),
|
||||
pois: Default::default(),
|
||||
epoch: NEXT_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
mod_changes: ModChanges::default(),
|
||||
}
|
||||
}
|
||||
/// A controlled blockstate scene for the native scene probes: material
|
||||
@@ -270,6 +305,9 @@ impl World {
|
||||
self.watched_writes.push(((x, y, z), previous, state.0));
|
||||
}
|
||||
self.revision = self.revision.wrapping_add(1);
|
||||
if self.mod_changes.watching {
|
||||
self.mod_changes.note(x, y, z);
|
||||
}
|
||||
if let Some(log) = &mut self.block_log {
|
||||
log.push((x, y, z));
|
||||
}
|
||||
|
||||
@@ -528,6 +528,107 @@ interface render {
|
||||
/// the highest block that blocks motion or holds a fluid, as the motion
|
||||
/// blocking heightmap; -64 where the chunk isn't loaded.
|
||||
ground: func() -> list<tuple<s32, s32, s32>>;
|
||||
/// What a block is to a query (AirDef fork): the three policies differ
|
||||
/// on purpose, so a sensor and a missile may disagree about a pane of
|
||||
/// glass.
|
||||
enum policy {
|
||||
/// Projectile collision: every block with a collision shape stops it
|
||||
/// (stone, leaves, glass, slabs, fences); air, fluids, plants and
|
||||
/// other shapeless blocks let it pass.
|
||||
collision,
|
||||
/// Sensor line of sight: opaque blocks (those that stop light) only;
|
||||
/// glass, leaves and fluids are seen through.
|
||||
sight,
|
||||
/// Sound occlusion: blocks with a full-cube collision shape.
|
||||
sound,
|
||||
}
|
||||
/// How a query ended. Never `clear` for a chunk that isn't loaded.
|
||||
enum verdict {
|
||||
/// Nothing on the way, everything checked.
|
||||
clear,
|
||||
/// A block stops it; see the other fields.
|
||||
hit,
|
||||
/// A part of the way is in a chunk the game doesn't have (far
|
||||
/// terrain, not yet loaded) or the budget ran out: no verdict. A
|
||||
/// `hit` found before the unknown part still comes back as `hit`.
|
||||
unknown,
|
||||
}
|
||||
/// A segment to trace through the loaded blocks.
|
||||
record ray {
|
||||
/// Echoed in the answer.
|
||||
id: u32,
|
||||
start: tuple<f64, f64, f64>,
|
||||
end: tuple<f64, f64, f64>,
|
||||
policy: policy,
|
||||
}
|
||||
record ray-answer {
|
||||
id: u32,
|
||||
verdict: verdict,
|
||||
/// For a `hit`: how far along the segment, 0 to 1, where it enters
|
||||
/// the shape.
|
||||
fraction: f64,
|
||||
/// For a `hit`: the face normal (the axis it entered along).
|
||||
normal: tuple<s32, s32, s32>,
|
||||
/// For a `hit`: the block hit.
|
||||
block: tuple<s32, s32, s32>,
|
||||
/// For an `unknown`: how far along the segment the unknown part
|
||||
/// begins.
|
||||
known-to: f64,
|
||||
}
|
||||
/// Traces segments through the loaded blocks (AirDef fork): each is
|
||||
/// walked block by block on the host, against the real collision shapes
|
||||
/// (`policy`), and answered by `rays` from the next tick on. At most 256
|
||||
/// segments a tick, each at most 512 blocks long (longer ones are
|
||||
/// answered `unknown` past that); over the budget they are answered
|
||||
/// `unknown` too, never `clear`. A game with no chunk at a place answers
|
||||
/// `unknown` for it.
|
||||
query-rays: func(rays: list<ray>);
|
||||
/// The answers to the last tick's `query-rays`, in the order asked.
|
||||
rays: func() -> list<ray-answer>;
|
||||
/// What a 16 x 16 x 16 section of the world holds, for a policy.
|
||||
enum section-fill {
|
||||
/// The game has no such chunk loaded: nothing is known.
|
||||
unknown,
|
||||
/// Nothing in it stops a query.
|
||||
empty,
|
||||
/// Every block in it does.
|
||||
full,
|
||||
/// Some do: `bits` tells which.
|
||||
mixed,
|
||||
}
|
||||
record section {
|
||||
/// Section coordinates: block x, y, z divided by 16, rounded down.
|
||||
x: s32,
|
||||
y: s32,
|
||||
z: s32,
|
||||
fill: section-fill,
|
||||
/// For `mixed`: 512 bytes, one bit per block, set where it stops
|
||||
/// the policy's query. The index is (y & 15) * 256 + (z & 15) * 16
|
||||
/// + (x & 15); bit i is bit (i & 7) of byte i / 8. Empty otherwise.
|
||||
bits: list<u8>,
|
||||
}
|
||||
/// Asks what these sections hold (AirDef fork), for a mod that traces
|
||||
/// its own paths against a cache of them: at most 64 a tick, answered
|
||||
/// by `sections` from the next tick on. Over the budget they are not
|
||||
/// answered: ask again.
|
||||
query-sections: func(policy: policy, sections: list<tuple<s32, s32, s32>>);
|
||||
/// The answers to the last tick's `query-sections`.
|
||||
sections: func() -> list<section>;
|
||||
/// Changes to the world since the last tick, for a mod's cache.
|
||||
record changes {
|
||||
/// A number that changes when another world or dimension opens:
|
||||
/// everything cached before is void.
|
||||
epoch: u32,
|
||||
/// Counts block changes in the loaded world, ever (wraps).
|
||||
revision: u64,
|
||||
/// More sections changed than fit: treat everything as changed.
|
||||
overflow: bool,
|
||||
/// Sections (as in `section`) where a block changed, at most 256.
|
||||
sections: list<tuple<s32, s32, s32>>,
|
||||
}
|
||||
/// The block changes of the last tick (placed, broken, exploded) and
|
||||
/// the world's epoch. Only changes in chunks the game has loaded.
|
||||
world-changes: func() -> changes;
|
||||
/// 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
|
||||
|
||||
@@ -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 = 11;
|
||||
pub const LEVEL: u32 = 12;
|
||||
|
||||
/// 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); 61] = [
|
||||
pub const FUNCTIONS: [(&str, u32); 66] = [
|
||||
("log.write", 0),
|
||||
("game.get-player", 0),
|
||||
("game.get-clock", 0),
|
||||
@@ -76,6 +76,12 @@ pub const FUNCTIONS: [(&str, u32); 61] = [
|
||||
("render.model-id", 11),
|
||||
("render.model-generation", 11),
|
||||
("render.set-models-by-id", 11),
|
||||
// AirDef fork: the world's blocks for a simulation.
|
||||
("render.query-rays", 12),
|
||||
("render.rays", 12),
|
||||
("render.query-sections", 12),
|
||||
("render.sections", 12),
|
||||
("render.world-changes", 12),
|
||||
];
|
||||
|
||||
/// What the game's program carries so the launcher can read its level
|
||||
|
||||
@@ -17,6 +17,7 @@ pub mod modpack;
|
||||
mod runtime;
|
||||
#[cfg(feature = "runtime")]
|
||||
pub mod testing;
|
||||
pub mod world_query;
|
||||
|
||||
pub use content::Content;
|
||||
pub use manifest::{Capability, Manifest, Package, Side, find};
|
||||
|
||||
@@ -79,10 +79,12 @@ 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::{
|
||||
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,
|
||||
CameraView, Changes as WorldChanges, Corner as RenderCorner, FarTerrain as ModFarTerrain,
|
||||
FillShape, FrameRate, ItemShape, Lighting as ModLighting, Minimap as ModMinimap, ModelInstance,
|
||||
ModelInstanceId, ParticleSpawn, Point as RenderPoint, PointLight, Policy as WorldPolicy,
|
||||
QuadShape, Ray as WorldRay, RayAnswer as WorldRayAnswer, Rect as RenderRect,
|
||||
Section as WorldSection, SectionFill, Shape, SpriteShape, TextShape, Verdict as WorldVerdict,
|
||||
ViewShape, Zoom as ModZoom,
|
||||
};
|
||||
use render_bindings::{RenderHost, horizon::mods::render};
|
||||
/// Capability `recipes`'s imports (docs/mods.md): the game's recipes and items.
|
||||
@@ -112,6 +114,17 @@ const FUEL_INIT: u64 = 2_000_000_000;
|
||||
const FUEL_TICK: u64 = 300_000_000;
|
||||
const FUEL_FRAME: u64 = 10_000_000;
|
||||
const FUEL_EVENT: u64 = 1_000_000_000;
|
||||
/// Segments a mod may trace per tick (`render.query-rays`).
|
||||
pub const RAYS_PER_TICK: usize = 256;
|
||||
/// Sections a mod may ask the contents of per tick (`render.query-sections`).
|
||||
pub const SECTIONS_PER_TICK: usize = 64;
|
||||
/// What a mod asked of the world in a tick (capability `render`, AirDef fork).
|
||||
pub struct WorldQueries {
|
||||
pub rays: Vec<WorldRay>,
|
||||
/// Segments asked beyond the budget: to be answered unknown.
|
||||
pub over: Vec<u32>,
|
||||
pub sections: Vec<(WorldPolicy, Vec<(i32, i32, i32)>)>,
|
||||
}
|
||||
/// Blocks a mod may set per `world.set-blocks` call.
|
||||
pub const BLOCKS_PER_CALL: usize = 4096;
|
||||
/// Chat lines a mod may show per call, HUD lines and their length.
|
||||
@@ -473,6 +486,19 @@ struct State {
|
||||
ground_queries: Vec<(i32, i32)>,
|
||||
/// The game's answers to them.
|
||||
ground: Vec<(i32, i32, i32)>,
|
||||
/// `render.query-rays`' segments, answered before the next tick.
|
||||
ray_queries: Vec<WorldRay>,
|
||||
/// Ids of segments asked beyond the budget.
|
||||
ray_over: Vec<u32>,
|
||||
rays: Vec<WorldRayAnswer>,
|
||||
/// Whether the mod ever used the world queries.
|
||||
world_seen: bool,
|
||||
/// `render.query-sections`: the policy and the sections asked.
|
||||
section_queries: Vec<(WorldPolicy, Vec<(i32, i32, i32)>)>,
|
||||
sections_asked: usize,
|
||||
sections: Vec<WorldSection>,
|
||||
/// The game's note of what changed in the world last tick.
|
||||
world_changes: Option<WorldChanges>,
|
||||
/// `render.set-post`'s source, with a number that changes with it.
|
||||
post: Option<(u64, String)>,
|
||||
post_params: Vec<f32>,
|
||||
@@ -710,6 +736,14 @@ impl Mods {
|
||||
model_table: model_table.clone(),
|
||||
ground_queries: Vec::new(),
|
||||
ground: Vec::new(),
|
||||
ray_queries: Vec::new(),
|
||||
ray_over: Vec::new(),
|
||||
world_seen: false,
|
||||
rays: Vec::new(),
|
||||
section_queries: Vec::new(),
|
||||
sections_asked: 0,
|
||||
sections: Vec::new(),
|
||||
world_changes: None,
|
||||
post: None,
|
||||
post_params: Vec::new(),
|
||||
views: Vec::new(),
|
||||
@@ -1319,6 +1353,40 @@ impl Mods {
|
||||
Some((*v, src.as_str(), d.post_params.as_slice()))
|
||||
})
|
||||
}
|
||||
/// What each running mod asked of the world this tick: segments and
|
||||
/// sections (`render.query-rays`, `render.query-sections`), taken.
|
||||
pub fn take_world_queries(&mut self) -> Vec<(usize, WorldQueries)> {
|
||||
self.entries
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.filter_map(|(i, e)| {
|
||||
let d = e.run.as_mut()?.store.data_mut();
|
||||
d.sections_asked = 0;
|
||||
let q = WorldQueries {
|
||||
rays: std::mem::take(&mut d.ray_queries),
|
||||
over: std::mem::take(&mut d.ray_over),
|
||||
sections: std::mem::take(&mut d.section_queries),
|
||||
};
|
||||
(d.world_seen || !q.rays.is_empty() || !q.sections.is_empty()).then_some((i, q))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
/// Mod `i`'s answers to its world queries and the note of what changed,
|
||||
/// for its next calls (what it was not answered, it gets again empty).
|
||||
pub fn answer_world(
|
||||
&mut self,
|
||||
i: usize,
|
||||
rays: Vec<WorldRayAnswer>,
|
||||
sections: Vec<WorldSection>,
|
||||
changes: WorldChanges,
|
||||
) {
|
||||
if let Some(run) = self.entries.get_mut(i).and_then(|e| e.run.as_mut()) {
|
||||
let d = run.store.data_mut();
|
||||
d.rays = rays;
|
||||
d.sections = sections;
|
||||
d.world_changes = Some(changes);
|
||||
}
|
||||
}
|
||||
/// Mod `i`'s answers to its ground queries, for its next calls.
|
||||
pub fn answer_ground(&mut self, i: usize, ground: Vec<(i32, i32, i32)>) {
|
||||
if let Some(run) = self.entries.get_mut(i).and_then(|e| e.run.as_mut()) {
|
||||
@@ -2176,6 +2244,38 @@ impl render::Host for State {
|
||||
fn ground(&mut self) -> Vec<(i32, i32, i32)> {
|
||||
self.ground.clone()
|
||||
}
|
||||
fn query_rays(&mut self, rays: Vec<WorldRay>) {
|
||||
let room = RAYS_PER_TICK.saturating_sub(self.ray_queries.len());
|
||||
let mut over = rays.into_iter();
|
||||
self.ray_queries.extend(over.by_ref().take(room));
|
||||
// Over the budget: unknown, never clear.
|
||||
self.ray_over.extend(over.take(RAYS_PER_TICK).map(|r| r.id));
|
||||
self.world_seen = true;
|
||||
}
|
||||
fn rays(&mut self) -> Vec<WorldRayAnswer> {
|
||||
self.rays.clone()
|
||||
}
|
||||
fn query_sections(&mut self, policy: WorldPolicy, sections: Vec<(i32, i32, i32)>) {
|
||||
self.world_seen = true;
|
||||
let room = SECTIONS_PER_TICK.saturating_sub(self.sections_asked);
|
||||
let sections: Vec<_> = sections.into_iter().take(room).collect();
|
||||
self.sections_asked += sections.len();
|
||||
if !sections.is_empty() {
|
||||
self.section_queries.push((policy, sections));
|
||||
}
|
||||
}
|
||||
fn sections(&mut self) -> Vec<WorldSection> {
|
||||
self.sections.clone()
|
||||
}
|
||||
fn world_changes(&mut self) -> WorldChanges {
|
||||
self.world_seen = true;
|
||||
self.world_changes.clone().unwrap_or(WorldChanges {
|
||||
epoch: 0,
|
||||
revision: 0,
|
||||
overflow: false,
|
||||
sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
fn play_sound(&mut self, event: String, x: f64, y: f64, z: f64, volume: f32, pitch: f32) {
|
||||
if !(x.is_finite() && y.is_finite() && z.is_finite()) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
//! The world queries of capability `render` (AirDef fork, API level 12): a
|
||||
//! segment walked block by block against collision shapes, and the
|
||||
//! contents of a 16-block section as a bit set. Pure code over a
|
||||
//! [`BlockProbe`], so the game answers from its chunks and the tests from
|
||||
//! a few lines of fixture.
|
||||
|
||||
/// What a probe knows about one block cell.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Cell<'a> {
|
||||
/// No chunk holds this cell.
|
||||
Unknown,
|
||||
/// The cell does not stop the policy's queries.
|
||||
Empty,
|
||||
/// It does; its collision boxes in cell space (0..1), or none for a full
|
||||
/// cube.
|
||||
Solid(Option<&'a [[f64; 6]]>),
|
||||
}
|
||||
|
||||
/// The blocks a query reads.
|
||||
pub trait BlockProbe {
|
||||
fn cell(&mut self, x: i32, y: i32, z: i32) -> Cell<'_>;
|
||||
}
|
||||
|
||||
/// How a segment ended.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Trace {
|
||||
Clear,
|
||||
Hit {
|
||||
/// 0 to 1 along the segment.
|
||||
fraction: f64,
|
||||
normal: [i32; 3],
|
||||
block: [i32; 3],
|
||||
},
|
||||
Unknown {
|
||||
/// 0 to 1: where the unknown part begins.
|
||||
known_to: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Longest segment walked, in blocks; the rest is `Unknown`.
|
||||
pub const MAX_RAY: f64 = 512.0;
|
||||
|
||||
/// The first solid block along `from` to `to`, or `Unknown` where the walk
|
||||
/// reaches a cell the probe doesn't know. A segment starting inside a solid
|
||||
/// block hits it at fraction 0.
|
||||
pub fn trace(probe: &mut impl BlockProbe, from: [f64; 3], to: [f64; 3]) -> Trace {
|
||||
if !from
|
||||
.iter()
|
||||
.chain(&to)
|
||||
.all(|v| v.is_finite() && v.abs() < 3.0e7)
|
||||
{
|
||||
return Trace::Unknown { known_to: 0.0 };
|
||||
}
|
||||
let delta = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
|
||||
let length = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
|
||||
// The part walked, as a fraction of the segment.
|
||||
let walked = if length > MAX_RAY {
|
||||
MAX_RAY / length
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let mut pos = from.map(|v| v.floor() as i32);
|
||||
let step: [i32; 3] = delta.map(|d| if d > 0.0 { 1 } else { -1 });
|
||||
let mut t_max = [f64::INFINITY; 3];
|
||||
let mut t_delta = [f64::INFINITY; 3];
|
||||
for a in 0..3 {
|
||||
if delta[a] != 0.0 {
|
||||
let boundary = f64::from(pos[a]) + if delta[a] > 0.0 { 1.0 } else { 0.0 };
|
||||
t_max[a] = (boundary - from[a]) / delta[a];
|
||||
t_delta[a] = (1.0 / delta[a]).abs();
|
||||
}
|
||||
}
|
||||
let mut face = [0i32; 3];
|
||||
let mut t = 0.0f64;
|
||||
loop {
|
||||
match probe.cell(pos[0], pos[1], pos[2]) {
|
||||
Cell::Unknown => {
|
||||
return Trace::Unknown {
|
||||
known_to: t.min(1.0),
|
||||
};
|
||||
}
|
||||
Cell::Empty => {}
|
||||
Cell::Solid(boxes) => {
|
||||
if let Some((hit, normal)) = enter(boxes, pos, from, delta, face) {
|
||||
return Trace::Hit {
|
||||
fraction: hit.clamp(0.0, 1.0),
|
||||
normal,
|
||||
block: pos,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
let axis = if t_max[0] < t_max[1] {
|
||||
if t_max[0] < t_max[2] { 0 } else { 2 }
|
||||
} else if t_max[1] < t_max[2] {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
t = t_max[axis];
|
||||
if t > walked {
|
||||
return if walked < 1.0 {
|
||||
Trace::Unknown { known_to: walked }
|
||||
} else {
|
||||
Trace::Clear
|
||||
};
|
||||
}
|
||||
t_max[axis] += t_delta[axis];
|
||||
pos[axis] += step[axis];
|
||||
face = [0; 3];
|
||||
face[axis] = -step[axis];
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the segment first meets the cell's boxes (slab method): its
|
||||
/// fraction and the face's normal. `face` is the face it came in by.
|
||||
fn enter(
|
||||
boxes: Option<&[[f64; 6]]>,
|
||||
cell: [i32; 3],
|
||||
from: [f64; 3],
|
||||
delta: [f64; 3],
|
||||
face: [i32; 3],
|
||||
) -> Option<(f64, [i32; 3])> {
|
||||
const FULL: [[f64; 6]; 1] = [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]];
|
||||
let boxes = boxes.unwrap_or(&FULL);
|
||||
let mut best: Option<(f64, [i32; 3])> = None;
|
||||
for b in boxes {
|
||||
let (mut t0, mut t1) = (0.0f64, 1.0f64);
|
||||
let mut normal = face;
|
||||
let mut miss = false;
|
||||
for a in 0..3 {
|
||||
let (lo, hi) = (f64::from(cell[a]) + b[a], f64::from(cell[a]) + b[a + 3]);
|
||||
if delta[a] == 0.0 {
|
||||
miss |= from[a] < lo || from[a] > hi;
|
||||
continue;
|
||||
}
|
||||
let (e, x) = ((lo - from[a]) / delta[a], (hi - from[a]) / delta[a]);
|
||||
let (near, far) = if e < x { (e, x) } else { (x, e) };
|
||||
if near > t0 {
|
||||
t0 = near;
|
||||
normal = [0; 3];
|
||||
normal[a] = if delta[a] > 0.0 { -1 } else { 1 };
|
||||
}
|
||||
t1 = t1.min(far);
|
||||
}
|
||||
if miss || t0 > t1 {
|
||||
continue;
|
||||
}
|
||||
if best.is_none_or(|(bt, _)| t0 < bt) {
|
||||
best = Some((t0, normal));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// A section's contents for a policy: whether every block, none or some stop
|
||||
/// it, and for some the 512 bytes of bits (index `(y & 15) * 256 + (z & 15) *
|
||||
/// 16 + (x & 15)`, bit `i & 7` of byte `i / 8`).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SectionBits {
|
||||
Empty,
|
||||
Full,
|
||||
Mixed(Box<[u8; 512]>),
|
||||
}
|
||||
|
||||
/// The bits of 4096 block states (in the index order above) under a state
|
||||
/// table: `table[state] & mask != 0` stops the query.
|
||||
pub fn section_bits(states: &[u32; 4096], table: &[u8], mask: u8) -> SectionBits {
|
||||
let mut bits = Box::new([0u8; 512]);
|
||||
let mut set = 0u32;
|
||||
for (i, &s) in states.iter().enumerate() {
|
||||
if table.get(s as usize).is_some_and(|f| f & mask != 0) {
|
||||
bits[i >> 3] |= 1 << (i & 7);
|
||||
set += 1;
|
||||
}
|
||||
}
|
||||
match set {
|
||||
0 => SectionBits::Empty,
|
||||
4096 => SectionBits::Full,
|
||||
_ => SectionBits::Mixed(bits),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A world of listed cells; any cell outside `known` columns is unknown.
|
||||
struct Fixture {
|
||||
solid: HashMap<(i32, i32, i32), Option<Vec<[f64; 6]>>>,
|
||||
unknown_from_x: i32,
|
||||
}
|
||||
impl BlockProbe for Fixture {
|
||||
fn cell(&mut self, x: i32, y: i32, z: i32) -> Cell<'_> {
|
||||
if x >= self.unknown_from_x {
|
||||
return Cell::Unknown;
|
||||
}
|
||||
match self.solid.get(&(x, y, z)) {
|
||||
None => Cell::Empty,
|
||||
Some(None) => Cell::Solid(None),
|
||||
Some(Some(b)) => Cell::Solid(Some(b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn wall(x: i32) -> Fixture {
|
||||
let mut solid = HashMap::new();
|
||||
for y in 60..80 {
|
||||
for z in -5..5 {
|
||||
solid.insert((x, y, z), None);
|
||||
}
|
||||
}
|
||||
Fixture {
|
||||
solid,
|
||||
unknown_from_x: 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wall_one_block_thick_is_hit_at_its_face() {
|
||||
let mut w = wall(3);
|
||||
let t = trace(&mut w, [0.5, 70.5, 0.5], [20.5, 70.5, 0.5]);
|
||||
let Trace::Hit {
|
||||
fraction,
|
||||
normal,
|
||||
block,
|
||||
} = t
|
||||
else {
|
||||
panic!("{t:?}")
|
||||
};
|
||||
assert!((fraction - 2.5 / 20.0).abs() < 1e-9);
|
||||
assert_eq!((normal, block), ([-1, 0, 0], [3, 70, 0]));
|
||||
}
|
||||
#[test]
|
||||
fn a_diagonal_segment_does_not_slip_through_a_corner() {
|
||||
let mut w = wall(3);
|
||||
let t = trace(&mut w, [0.2, 65.2, 0.2], [6.2, 71.2, 0.2]);
|
||||
assert!(
|
||||
matches!(
|
||||
t,
|
||||
Trace::Hit {
|
||||
block: [3, 68, 0],
|
||||
..
|
||||
}
|
||||
),
|
||||
"{t:?}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn a_tunnel_is_clear() {
|
||||
let mut w = wall(3);
|
||||
w.solid.remove(&(3, 70, 0));
|
||||
let t = trace(&mut w, [0.5, 70.5, 0.5], [20.5, 70.5, 0.5]);
|
||||
assert_eq!(t, Trace::Clear);
|
||||
}
|
||||
#[test]
|
||||
fn a_slab_is_passed_above_and_hit_below() {
|
||||
let mut w = Fixture {
|
||||
solid: HashMap::new(),
|
||||
unknown_from_x: 10_000,
|
||||
};
|
||||
w.solid
|
||||
.insert((2, 70, 0), Some(vec![[0.0, 0.0, 0.0, 1.0, 0.5, 1.0]]));
|
||||
assert_eq!(
|
||||
trace(&mut w, [0.5, 70.8, 0.5], [5.5, 70.8, 0.5]),
|
||||
Trace::Clear
|
||||
);
|
||||
let t = trace(&mut w, [0.5, 70.3, 0.5], [5.5, 70.3, 0.5]);
|
||||
assert!(
|
||||
matches!(
|
||||
t,
|
||||
Trace::Hit {
|
||||
block: [2, 70, 0],
|
||||
normal: [-1, 0, 0],
|
||||
..
|
||||
}
|
||||
),
|
||||
"{t:?}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn an_unloaded_chunk_is_unknown_not_air() {
|
||||
let mut w = Fixture {
|
||||
solid: HashMap::new(),
|
||||
unknown_from_x: 8,
|
||||
};
|
||||
let t = trace(&mut w, [0.5, 70.5, 0.5], [20.5, 70.5, 0.5]);
|
||||
let Trace::Unknown { known_to } = t else {
|
||||
panic!("{t:?}")
|
||||
};
|
||||
assert!((known_to - 7.5 / 20.0).abs() < 1e-9);
|
||||
// A hit before the unknown part still counts.
|
||||
let mut w = wall(3);
|
||||
w.unknown_from_x = 8;
|
||||
assert!(matches!(
|
||||
trace(&mut w, [0.5, 70.5, 0.5], [20.5, 70.5, 0.5]),
|
||||
Trace::Hit { .. }
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn a_long_segment_is_walked_only_so_far() {
|
||||
let mut w = Fixture {
|
||||
solid: HashMap::new(),
|
||||
unknown_from_x: 10_000,
|
||||
};
|
||||
let t = trace(&mut w, [0.5, 70.5, 0.5], [2000.5, 70.5, 0.5]);
|
||||
let Trace::Unknown { known_to } = t else {
|
||||
panic!("{t:?}")
|
||||
};
|
||||
assert!((known_to - MAX_RAY / 2000.0).abs() < 1e-9);
|
||||
}
|
||||
#[test]
|
||||
fn starting_inside_a_block_hits_at_zero() {
|
||||
let mut w = wall(0);
|
||||
let t = trace(&mut w, [0.5, 70.5, 0.5], [5.5, 70.5, 0.5]);
|
||||
assert!(
|
||||
matches!(t, Trace::Hit { fraction, .. } if fraction == 0.0),
|
||||
"{t:?}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn sections_pack_to_bits() {
|
||||
let table = [0u8, 1, 7];
|
||||
let mut states = [0u32; 4096];
|
||||
assert_eq!(section_bits(&states, &table, 1), SectionBits::Empty);
|
||||
states[256 + 16 * 3 + 5] = 1;
|
||||
let SectionBits::Mixed(bits) = section_bits(&states, &table, 1) else {
|
||||
panic!()
|
||||
};
|
||||
let i = 256 + 16 * 3 + 5;
|
||||
assert_eq!(bits[i / 8], 1 << (i % 8));
|
||||
// A sound-only policy ignores a block marked for collision alone.
|
||||
assert_eq!(section_bits(&states, &table, 4), SectionBits::Empty);
|
||||
states.fill(2);
|
||||
assert_eq!(section_bits(&states, &table, 4), SectionBits::Full);
|
||||
}
|
||||
}
|
||||
@@ -501,6 +501,80 @@ Particles to spawn on this client (AirDef fork).
|
||||
| `color` | `u32` | 0xRRGGBB for dust and coloured types (0: white). |
|
||||
| `scale` | `f32` | Dust size (0.01 to 4). |
|
||||
|
||||
### enum `policy`
|
||||
|
||||
What a block is to a query (AirDef fork): the three policies differ on purpose, so a sensor and a missile may disagree about a pane of glass.
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `collision` | Projectile collision: every block with a collision shape stops it (stone, leaves, glass, slabs, fences); air, fluids, plants and other shapeless blocks let it pass. |
|
||||
| `sight` | Sensor line of sight: opaque blocks (those that stop light) only; glass, leaves and fluids are seen through. |
|
||||
| `sound` | Sound occlusion: blocks with a full-cube collision shape. |
|
||||
|
||||
### enum `verdict`
|
||||
|
||||
How a query ended. Never `clear` for a chunk that isn't loaded.
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `clear` | Nothing on the way, everything checked. |
|
||||
| `hit` | A block stops it; see the other fields. |
|
||||
| `unknown` | A part of the way is in a chunk the game doesn't have (far terrain, not yet loaded) or the budget ran out: no verdict. A `hit` found before the unknown part still comes back as `hit`. |
|
||||
|
||||
### record `ray`
|
||||
|
||||
A segment to trace through the loaded blocks.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `u32` | Echoed in the answer. |
|
||||
| `start` | `tuple<f64, f64, f64>` | |
|
||||
| `end` | `tuple<f64, f64, f64>` | |
|
||||
| `policy` | `policy` | |
|
||||
|
||||
### record `ray-answer`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `u32` | |
|
||||
| `verdict` | `verdict` | |
|
||||
| `fraction` | `f64` | For a `hit`: how far along the segment, 0 to 1, where it enters the shape. |
|
||||
| `normal` | `tuple<s32, s32, s32>` | For a `hit`: the face normal (the axis it entered along). |
|
||||
| `block` | `tuple<s32, s32, s32>` | For a `hit`: the block hit. |
|
||||
| `known-to` | `f64` | For an `unknown`: how far along the segment the unknown part begins. |
|
||||
|
||||
### enum `section-fill`
|
||||
|
||||
What a 16 x 16 x 16 section of the world holds, for a policy.
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `unknown` | The game has no such chunk loaded: nothing is known. |
|
||||
| `empty` | Nothing in it stops a query. |
|
||||
| `full` | Every block in it does. |
|
||||
| `mixed` | Some do: `bits` tells which. |
|
||||
|
||||
### record `section`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `x` | `s32` | Section coordinates: block x, y, z divided by 16, rounded down. |
|
||||
| `y` | `s32` | |
|
||||
| `z` | `s32` | |
|
||||
| `fill` | `section-fill` | |
|
||||
| `bits` | `list<u8>` | For `mixed`: 512 bytes, one bit per block, set where it stops the policy's query. The index is (y & 15) * 256 + (z & 15) * 16 + (x & 15); bit i is bit (i & 7) of byte i / 8. Empty otherwise. |
|
||||
|
||||
### record `changes`
|
||||
|
||||
Changes to the world since the last tick, for a mod's cache.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `epoch` | `u32` | A number that changes when another world or dimension opens: everything cached before is void. |
|
||||
| `revision` | `u64` | Counts block changes in the loaded world, ever (wraps). |
|
||||
| `overflow` | `bool` | More sections changed than fit: treat everything as changed. |
|
||||
| `sections` | `list<tuple<s32, s32, s32>>` | Sections (as in `section`) where a block changed, at most 256. |
|
||||
|
||||
### 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.
|
||||
@@ -561,6 +635,11 @@ The HD lighting (AirDef fork): the world lit per pixel by the mods' lights, in h
|
||||
| `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. |
|
||||
| `query-rays` | `func(rays: list<ray>)` | Traces segments through the loaded blocks (AirDef fork): each is walked block by block on the host, against the real collision shapes (`policy`), and answered by `rays` from the next tick on. At most 256 segments a tick, each at most 512 blocks long (longer ones are answered `unknown` past that); over the budget they are answered `unknown` too, never `clear`. A game with no chunk at a place answers `unknown` for it. |
|
||||
| `rays` | `func() -> list<ray-answer>` | The answers to the last tick's `query-rays`, in the order asked. |
|
||||
| `query-sections` | `func(policy: policy, sections: list<tuple<s32, s32, s32>>)` | Asks what these sections hold (AirDef fork), for a mod that traces its own paths against a cache of them: at most 64 a tick, answered by `sections` from the next tick on. Over the budget they are not answered: ask again. |
|
||||
| `sections` | `func() -> list<section>` | The answers to the last tick's `query-sections`. |
|
||||
| `world-changes` | `func() -> changes` | The block changes of the last tick (placed, broken, exploded) and the world's epoch. Only changes in chunks the game has 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`). |
|
||||
|
||||
Reference in New Issue
Block a user