delvewright_dsl/blockshape.rs
1//! **What a block state does to a body that walks into it** (spec-0056) — the
2//! one authority, for every crate in this workspace.
3//!
4//! One question: *given a vanilla block state, may a body occupy its cell, may a
5//! body stand on it, and if so at what height?* Every walk in this repository
6//! asks it — `delvec`'s route proofs, the grammar back end's contract gates, the
7//! admission pipeline's light probe — and until spec-0056 each of them answered
8//! it privately. Three copies, measured disagreeing in both directions: the
9//! grammar walk read *air or a skull* and called a torch a wall; the light probe
10//! kept a nine-id list that called open water walkable; `delvec` alone had a real
11//! collision table and could not lend it to either.
12//!
13//! # Why this module is in `delvewright-dsl`
14//!
15//! The same argument that placed [`crate::metrics::step_allowed`], and it has two
16//! halves.
17//!
18//! *Reachability.* Two readers ask this table the same question and share no
19//! dependency edge with each other: the engine (`delvec::schem`'s walk,
20//! `delvec::grammar`'s contract checker, the compiler's navigation model) and
21//! the prefab generators, which are a workspace of their own that may not depend
22//! on `delvec`. `delvewright-dsl` is the one crate both can depend on, and both
23//! do — the generators through `prefab-invariants`, which re-exports this
24//! module under the name their invariants already spell.
25//!
26//! *Object class.* A collision box is a fact about **a vanilla block state under
27//! the pinned game version** (ADR-0009, Minecraft Java 1.21.11) — the same kind of
28//! pinned-physics fact, in the same sixteenths, as the auto-step and jump-apex
29//! budgets already in [`crate::metrics`]. [`collision_top_16`] is what a correct
30//! rise measurement is made *of*: `metrics` says how far a body may climb, this
31//! module says from what height to what height.
32//!
33//! # The rule, in one table
34//!
35//! [`Collision`] is the classification and [`collision_class`] computes it. Its
36//! three answers are what a walk needs and all a walk gets:
37//!
38//! | class | [`Collision::passes_body`] | [`Collision::supports_body`] | [`Collision::floor_top_16`] |
39//! |---|---|---|---|
40//! | [`Collision::Air`] | yes | no | — |
41//! | [`Collision::Thin`] (top < 8/16) | yes | no | — (the body rests on the block below) |
42//! | [`Collision::PartialFloor`] (8..16) | no | yes | the measured top |
43//! | [`Collision::FullCube`] | no | yes | 16 |
44//! | [`Collision::TallBarrier`] (fence, wall) | no | **no** | — |
45//! | [`Collision::FenceGate`] | **yes** | no | — |
46//! | [`Collision::Fluid`] | no | no | — |
47//! | [`Collision::Climbable`] (ladder, vines) | yes | no | — (what holds the body is the climb) |
48//!
49//! Two rows are not the naive complement of each other, and that is the whole
50//! reason there are two columns. A **fluid** cell is a cell whose water or lava
51//! the *block* brings — a free `water`/`lava` block, and equally a `seagrass`
52//! tuft or a `kelp` stem, whose vanilla block carries a water source in its own
53//! cell ([`is_submerged_by_nature`]). A **tall barrier** is 1.5 blocks on a 1-block
54//! cell: a body neither passes through it nor reaches its top face by jumping. A
55//! **fence gate** is the mirror: adventure mode permits the right-click that
56//! opens it, so a body gets through — and for a *closure* claim a gate was never
57//! a seal anyway, so reading it as a hole is the sound direction. A **fluid** is
58//! neither, from the other side: spec-0038 forbids a route crediting water, and
59//! nothing stands on a surface.
60//!
61//! Sub-8/16 is [`THIN_HEIGHT_16`], and it is not a rounding: vanilla's auto-step
62//! is 0.6 blocks ([`crate::metrics::MAX_AUTO_STEP_16`] = 9/16), so a carpet, a
63//! shallow snow drift, a candle or a pressure plate is stepped **over** rather
64//! than onto and never constitutes a floor level of its own.
65//!
66//! # What one boolean cannot carry, said out loud
67//!
68//! A walk's `passable` answers for a body *and* for a sightline, and for the
69//! class this module changed most — empty collision — the two agree: glow lichen
70//! neither collides nor occludes. They part for partial-height blocks, where a
71//! slab stops a body and not an eye. This module answers the **body** question;
72//! a caller asking about a sightline gets the full-cube reading, which refuses
73//! claims it should sometimes grant. That is the refusing direction, and it is
74//! recorded here rather than hidden (spec-0056 §3.3). Splitting the two needs a
75//! second per-cell answer with its own conservative direction per claim sign, and
76//! that surface is not proposed.
77//!
78//! # The direction of every error here is stated per entry
79//!
80//! Anything no rule here names is read from the pinned jar's own collision
81//! boxes ([`measured_collision`], `crates/dsl/data/collision-tops-1.21.11.tsv`):
82//! a box that rests on the cell floor and tops out at 8..15 sixteenths is a
83//! [`Collision::PartialFloor`] at that height — an upward dripstone tip at 11, a
84//! chest at 14. Everything else is a [`Collision::FullCube`], which
85//! **over-blocks**: it can only make a walk refuse a step vanilla admits, never
86//! admit one vanilla refuses. So a box this module does not route through is
87//! left a wall on purpose rather than guessed at, and the places that happens
88//! say so ([`is_no_collision_fixture`]).
89
90/// A full block's collision height in sixteenths — the unit [`collision_top_16`]
91/// reports in. Vanilla builds every partial collision box out of sixteenths, so
92/// integer sixteenths represent every case exactly (no float ordering, ADR-0006).
93pub const FULL_HEIGHT_16: u8 = 16;
94
95/// Below this collision height a block is **stepped over, not onto**: the walker's
96/// feet stay on whatever supports it. 8/16 = half a block, the slab step. Anything
97/// thinner (carpet 1/16, a candle 6/16, a 1–4-layer snow drift ≤ 6/16) is under the
98/// vanilla 0.6-block auto-step, so modelling it as a floor *level* of its own would
99/// be noise; modelling it as a full cube is a lie.
100pub const THIN_HEIGHT_16: u8 = 8;
101
102/// The bare block id of a (possibly blockstate-carrying) block name: strips a
103/// trailing `[state]` / `{nbt}` suffix, keeping the namespace
104/// (`minecraft:oak_slab[type=top]` → `minecraft:oak_slab`).
105///
106/// Every classifier below matches on this, and the state-sensitive ones read the
107/// property they need with [`state_value`]. Waterlogging, slab halves and
108/// snow-layer counts are block *state*, and a model that throws the state away
109/// cannot tell a half-step from a full cube.
110pub fn base_id(name: &str) -> &str {
111 match name.find(['[', '{']) {
112 Some(i) => &name[..i],
113 None => name,
114 }
115}
116
117/// The value of blockstate property `key` in an `id[k=v,…]` block name, or
118/// `None` when the name carries no state or lacks that property.
119pub fn state_value<'a>(name: &'a str, key: &str) -> Option<&'a str> {
120 let open = name.find('[')?;
121 let close = name[open..].find(']')? + open;
122 name[open + 1..close].split(',').find_map(|kv| {
123 let (k, v) = kv.split_once('=')?;
124 (k.trim() == key).then_some(v.trim())
125 })
126}
127
128/// The namespace-free bare id (`minecraft:oak_slab[type=top]` → `oak_slab`).
129///
130/// Public because the namespace is optional everywhere a human writes a block:
131/// a prefab palette always carries `minecraft:`, an author's `fill-region` block
132/// is a hand-written string, and both are legal. Every classifier here goes
133/// through this so the two spellings cannot get different answers.
134pub fn bare_id(name: &str) -> &str {
135 let id = base_id(name);
136 id.strip_prefix("minecraft:").unwrap_or(id)
137}
138
139/// The air block variants that count as "no block" — passable, transparent, and
140/// never a floor.
141///
142/// Namespace-insensitive, unlike the predicate this replaced: a bare `air` in a
143/// hand-written `fill-region` is the same block as `minecraft:air`, and a
144/// namespace-sensitive reading classified it by the full-cube default, i.e. as
145/// floor a route proof would walk on.
146pub fn is_air(name: &str) -> bool {
147 matches!(bare_id(name), "air" | "cave_air" | "void_air")
148}
149
150/// **The blocks whose use vanilla reports** (spec-0093 §6.5): the lever, every
151/// button and the bell of the pinned registry. A right-click on one is the
152/// block's own default interaction — the lever's `powered` toggles, the
153/// button's pulses, the bell rings — and the pinned game's
154/// `minecraft:default_block_use` advancement criterion fires for the player who
155/// did it, at the block's cell [cited — the pinned registries dump,
156/// `tools/spike-area-effect-arrow/registries-1.21.11.json`, lists the criterion;
157/// the *Lever*, *Button* and *Bell* wiki pages: use flips, presses, rings]. A
158/// pressure plate is stepped on, not used, and belongs to the step machinery; a
159/// lamp, a sign, a lectern's shelf are objects vanilla reports no use of, so an
160/// act on one keeps its hit area fitted over the visible block.
161///
162/// Read by suffix rather than listed where the registry has a family, so a pin
163/// that adds a button adds it here: `#buttons` is every id ending in `_button`.
164pub fn is_hand_pressed(name: &str) -> bool {
165 let id = bare_id(name);
166 id == "lever" || id == "bell" || id.ends_with("_button")
167}
168
169/// Whether a cell's block is a **free fluid** — water or lava occupying the whole
170/// cell with no host block. **The one answer to "is this block id a fluid"** in
171/// this workspace.
172///
173/// What it covers, and why each case is the way it is:
174/// - **Block state is irrelevant.** A flowing `minecraft:water[level=3]` answers
175/// the same as a source. Both leave a body swimming rather than standing, which
176/// is the only question a collision model asks; the *reach* of a flow is a
177/// world question, not this predicate's.
178/// - **So is the namespace**, and that is not cosmetic: a bare `water` passes DSL
179/// block validation and is emitted verbatim, and a namespace-sensitive
180/// comparison would read it as an ordinary solid and prove a floor made of it.
181/// - **`minecraft:lava` counts.** Nothing stands on lava either, and a model that
182/// answered only for water would prove a lava surface walkable.
183/// - **A waterlogged block does NOT count.** `oak_stairs[waterlogged=true]` is a
184/// cell occupied by its *host* block — solid, standable, and simultaneously a
185/// flood source for its neighbours. Folding it in here would delete a floor the
186/// game plainly has.
187pub fn is_fluid(name: &str) -> bool {
188 matches!(bare_id(name), "water" | "lava")
189}
190
191/// Whether a block **fills its own cell with water by nature** — a plant or a
192/// column whose block has no `waterlogged` property to set because its cell is
193/// unconditionally a water source.
194///
195/// This is not [`is_fluid`]'s question and it is not the waterlogging one
196/// either, which is why it is a third predicate rather than an arm of one of
197/// them. [`is_fluid`] asks whether the cell holds a *free* fluid with no host
198/// block; waterlogging is a block *state* an author sets on an ordinary solid.
199/// These ids are neither: each has a host block, the host has an empty collision
200/// shape, and vanilla gives the cell a water source regardless of state
201/// (`SeagrassBlock`, `TallSeagrassBlock`, `KelpBlock`, `KelpPlantBlock` and
202/// `BubbleColumnBlock` all answer `Fluids.WATER` from `getFluidState`, Minecraft
203/// Java 1.21.11). A body put into one of these cells is in water: it swims, and
204/// it stands on nothing.
205///
206/// # The measurement that made this a predicate rather than a footnote
207///
208/// The shipped island and cave tilesets both scatter seagrass across the top
209/// water block of a shore — `cave-generator` says so in its own words, *seagrass
210/// is a water-filled block in vanilla, so it stands IN the sea's own cell rather
211/// than above it* — and the collision table disagreed with the generator that
212/// wrote the bytes. Three cells in the whole content library, and they were
213/// enough to make two pieces' walk planes measure one course below their floors,
214/// which put a pool's members into disagreement and refused it at build after
215/// the seating command had called it seatable. A tuft of grass standing in the
216/// sea is not a floor, and until this predicate existed nothing said so.
217///
218/// **Excluded, and not because they are dry.** `sea_pickle` and the coral fans
219/// carry a `waterlogged` property, so their cell's fluid is a state an author
220/// wrote and reads correctly through it; folding them in here would call a dry
221/// coral fan on a museum shelf a body of water. Anything whose fluid state was
222/// not read out of the pin is left in the collision default, which over-blocks
223/// (module header) rather than admitting a step the game refuses.
224///
225/// Takes a full block name (state suffix allowed), like every predicate here.
226pub fn is_submerged_by_nature(name: &str) -> bool {
227 matches!(
228 bare_id(name),
229 "seagrass" | "tall_seagrass" | "kelp" | "kelp_plant" | "bubble_column"
230 )
231}
232
233/// Whether a block is a **1.5-block-tall barrier**: fences (`*_fence`, incl.
234/// `nether_brick_fence`) and walls (`*_wall`). Vanilla gives these a collision box
235/// 1.5 blocks tall on a 1-block cell, which breaks the full-cube assumption in
236/// BOTH directions:
237///
238/// - **Not standable on top by a walking player**: a normal jump rises ~1.25
239/// blocks ([`crate::metrics::MAX_JUMP_RISE_16`]), so a 1.5-tall top face is
240/// unreachable by walking or jumping — a "legal +1 step onto a fence top" is a
241/// proof of a route no player and no bot can walk.
242/// - **Not passable through**: the barrier fills its cell for a walker, and its
243/// top half also blocks the cell above.
244///
245/// Fence **gates** are excluded — they are the openable case, see
246/// [`is_fence_gate`].
247pub fn is_tall_barrier(name: &str) -> bool {
248 let id = bare_id(name);
249 id.ends_with("_fence") || id.ends_with("_wall")
250}
251
252/// Whether a block is a fence gate (`*_fence_gate` — every vanilla fence gate is
253/// a wooden, right-click-openable one).
254///
255/// Closed, it is a 1.5-tall barrier like a fence but **passable-with-use**:
256/// opening it is a right-click USE interaction vanilla permits in adventure mode,
257/// the same action a human player performs. So a body gets through, and a caller
258/// that needs to know whether a *non-player* walker does asks this predicate by
259/// name rather than reading it out of the passability answer.
260pub fn is_fence_gate(name: &str) -> bool {
261 bare_id(name).ends_with("_fence_gate")
262}
263
264/// Whether a player can open this block by hand, in adventure mode — every
265/// fence gate, door and trapdoor except the two iron ones, which open only to
266/// redstone. A player who opens one can leave it open, so a proof about where a
267/// mob can walk once the party has been through asks this (`DW0923`).
268pub fn is_player_openable(name: &str) -> bool {
269 let id = bare_id(name);
270 if id == "iron_door" || id == "iron_trapdoor" {
271 return false;
272 }
273 is_fence_gate(name) || id.ends_with("_door") || id.ends_with("_trapdoor")
274}
275
276/// Thin, walkable trap-trigger blocks (spec-0011) a player steps *onto* rather
277/// than being blocked by: pressure plates and tripwire.
278///
279/// Named separately from the rest of the empty-collision class because a caller
280/// needs the *trap* fact as well as the collision fact — nav must route a player
281/// ONTO a critical-path trap trigger (the hazard `DW0342` reasons about) instead
282/// of routing around a "solid" plate and calling every trap avoidable. Their
283/// collision shape is empty, so [`collision_top_16`] answers 0 for them too and
284/// the two facts cannot disagree.
285pub fn is_passable_trap_trigger(name: &str) -> bool {
286 let id = bare_id(name);
287 id.ends_with("_pressure_plate") || matches!(id, "tripwire" | "tripwire_hook")
288}
289
290/// Vanilla's **no-collision vegetation class**: blocks whose collision shape is
291/// EMPTY — a walker passes straight through and stands on whatever is below
292/// (they are visual/light-model content only).
293///
294/// Modelling one as a full cube is wrong in both directions. A `short_grass` tuft
295/// on a valley terrace splits a deliberate 2-block riser into two climbable
296/// 1-block steps, so `DW0854` refuses a landform vanilla cannot climb
297/// (rejects-valid); and, worse, any walkability proof that stands a body ON a
298/// tuft or flower cell is unsound (accepts-invalid).
299///
300/// The list is the **class**, not the three ids one generator happens to scatter.
301/// Sources: Minecraft Java 1.21.11 block shapes — every id here has an empty
302/// collision shape. Deliberately excluded because they DO collide (or attach in
303/// ways this model does not represent): `azalea`/`flowering_azalea`,
304/// `big_dripleaf`, `bamboo`, `cactus`, `chorus_*`, `pointed_dripstone`,
305/// `scaffolding`, `sea_pickle`, `cocoa`, lily `pad` (a platform), all leaves, and
306/// anything not certainly collision-free — the conservative full-cube default
307/// keeps those sound.
308///
309/// **Five members of this list never reach [`Collision::Thin`]**, and the reason
310/// is not their collision box: `seagrass`, `tall_seagrass`, `kelp`, `kelp_plant`
311/// and `bubble_column` bring a water source with them, so
312/// [`collision_class`] answers [`Collision::Fluid`] for them one arm earlier
313/// ([`is_submerged_by_nature`]). They stay in this list because the statement it
314/// makes about them — an empty collision shape — is true and is what a caller
315/// asking about collision alone should get.
316///
317/// Takes a **bare** id ([`bare_id`]), not a full block name.
318pub fn is_no_collision_plant(id: &str) -> bool {
319 id.ends_with("_sapling")
320 || matches!(
321 id,
322 // grasses + ground cover
323 "short_grass"
324 | "tall_grass"
325 | "fern"
326 | "large_fern"
327 | "dead_bush"
328 | "bush"
329 | "firefly_bush"
330 | "short_dry_grass"
331 | "tall_dry_grass"
332 | "seagrass"
333 | "tall_seagrass"
334 | "pink_petals"
335 | "wildflowers"
336 | "leaf_litter"
337 | "hanging_roots"
338 | "mangrove_propagule"
339 // small + tall flowers
340 | "dandelion"
341 | "poppy"
342 | "blue_orchid"
343 | "allium"
344 | "azure_bluet"
345 | "red_tulip"
346 | "orange_tulip"
347 | "white_tulip"
348 | "pink_tulip"
349 | "oxeye_daisy"
350 | "cornflower"
351 | "lily_of_the_valley"
352 | "wither_rose"
353 | "torchflower"
354 | "sunflower"
355 | "lilac"
356 | "rose_bush"
357 | "peony"
358 | "pitcher_plant"
359 // mushrooms + nether flora
360 | "brown_mushroom"
361 | "red_mushroom"
362 | "crimson_fungus"
363 | "warped_fungus"
364 | "crimson_roots"
365 | "warped_roots"
366 | "nether_sprouts"
367 | "nether_wart"
368 // crops
369 | "wheat"
370 | "carrots"
371 | "potatoes"
372 | "beetroots"
373 | "melon_stem"
374 | "pumpkin_stem"
375 | "attached_melon_stem"
376 | "attached_pumpkin_stem"
377 | "torchflower_crop"
378 | "sweet_berry_bush"
379 | "sugar_cane"
380 | "bamboo_sapling"
381 // climbing / hanging plants
382 | "vine"
383 | "glow_lichen"
384 // glow lichen's sculk twin: every one of its 128 states has an
385 // empty collision shape in the pinned jar's table
386 | "sculk_vein"
387 | "spore_blossom"
388 | "small_dripleaf"
389 | "kelp"
390 | "kelp_plant"
391 | "cave_vines"
392 | "cave_vines_plant"
393 | "twisting_vines"
394 | "twisting_vines_plant"
395 | "weeping_vines"
396 | "weeping_vines_plant"
397 )
398}
399
400/// Vanilla's **no-collision fixture class**: the things a builder hangs on a wall
401/// or lays on a floor, whose collision shape is EMPTY.
402///
403/// This is the class the owner's report named — *a torch counting as a solid
404/// block is obviously an engine bug*. A wall torch occupies the air cell beside
405/// the wall it is fixed to; a body walks through that cell in the game, and a
406/// model that calls it a full cube severs a corridor for every proof downstream.
407/// The same is true of a sign, a banner, a lever, a button, a rail and a
408/// pressure plate.
409///
410/// Sources: Minecraft Java 1.21.11 block shapes — every id here is declared
411/// `noCollission()`.
412///
413/// **Deliberately excluded, and the reason is not that they collide.** `fire`,
414/// `soul_fire`, `cobweb`, `nether_portal` and `end_portal` have empty collision
415/// boxes too, and are left in the full-cube default on purpose: a body passing
416/// through one is not a body that may be *routed* through one, and this module's
417/// answer is consumed by proofs that would credit the step. Reading them as walls
418/// over-blocks, which is the direction this module's errors are allowed to run.
419///
420/// Also excluded because their shapes were not read out of the pin: lanterns,
421/// chains, end rods, ladders. The full-cube default keeps them sound.
422///
423/// Takes a **bare** id ([`bare_id`]), not a full block name.
424pub fn is_no_collision_fixture(id: &str) -> bool {
425 id.ends_with("_torch")
426 || id.ends_with("_sign")
427 || id.ends_with("_banner")
428 || id.ends_with("_button")
429 || id.ends_with("_rail")
430 || matches!(
431 id,
432 "torch"
433 | "lever"
434 | "rail"
435 | "redstone_wire"
436 // A cell a data pack lights or leaves undecided: neither is a
437 // block a body meets.
438 | "light"
439 | "structure_void"
440 )
441}
442
443/// The height of a block's **collision box top face**, in sixteenths of a block
444/// (0 = no collision at all, 16 = a full cube). Anything not listed is the
445/// pinned jar's measured floor height where its box is one
446/// ([`measured_collision`]: from the cell floor to 8..15), else a full cube —
447/// the conservative default.
448///
449/// Modelling a slab or a snow layer as a full 1×1×1 cube misplaces the surface a
450/// walker stands on by up to a whole block, which makes the step rule prove
451/// step-ups vanilla refuses (from a bottom slab up onto a full block is a
452/// **1.5-block** rise, above the ~1.25-block jump apex) and refuse step-ups
453/// vanilla allows (onto a bottom slab is a 0.5-block auto-step needing no jump
454/// headroom at all).
455///
456/// Sources (Minecraft Java 1.21.11 block shapes):
457/// - **Slabs**: `type=bottom` occupies the lower half → 8; `type=top` and
458/// `type=double` reach the cell top → 16. `type` **defaults to `bottom`**, so a
459/// bare `minecraft:oak_slab` is a half-step.
460/// - **Snow layers** (`minecraft:snow`): collision height is `(layers - 1) * 2`
461/// sixteenths (`SnowLayerBlock` indexes its shape table at `layers - 1`), so
462/// `layers=1` has **no** collision box (you walk through it) and `layers=8` is
463/// 14/16. The *outline* shape is `layers * 2`, which is what the block looks
464/// like — the collision box is what a walker stands on. `layers` defaults to 1.
465/// - **Carpets**: 1/16 (`pale_moss_carpet` only when `bottom=true`; its default
466/// wall-vine form has no collision box at all).
467/// - **Candles**: 6/16 for every count (`CandleBlock`'s four shapes are all
468/// `…,0,…→…,6,…`), so a candle on a floor is stepped over, never onto.
469/// - **Flower pots** (`flower_pot`, `potted_*`): 6/16.
470/// - **`dirt_path` / `farmland`**: 15/16 (the one-pixel dip you step down into).
471/// - **Empty**: [`is_no_collision_plant`] and [`is_no_collision_fixture`] → 0.
472pub fn collision_top_16(name: &str) -> u8 {
473 let id = bare_id(name);
474 if id.ends_with("_slab") {
475 return match state_value(name, "type") {
476 Some("top") | Some("double") => FULL_HEIGHT_16,
477 // `bottom` — and the default state, which omits the property.
478 _ => 8,
479 };
480 }
481 if id == "snow" {
482 let layers: u8 = state_value(name, "layers")
483 .and_then(|v| v.parse().ok())
484 .unwrap_or(1);
485 return layers.clamp(1, 8).saturating_sub(1) * 2;
486 }
487 if id == "pale_moss_carpet" {
488 // The odd one out: a floor carpet only when `bottom=true` (1/16); its
489 // wall-vine form (`bottom=false`, the default) has NO collision box.
490 return u8::from(state_value(name, "bottom") == Some("true"));
491 }
492 if id.ends_with("_carpet") || id == "moss_carpet" {
493 return 1;
494 }
495 if id == "candle" || id.ends_with("_candle") {
496 return 6;
497 }
498 if id == "flower_pot" || id.starts_with("potted_") {
499 return 6;
500 }
501 if matches!(id, "dirt_path" | "farmland") {
502 return 15;
503 }
504 // A lantern's collision is two stacked boxes, the cap's top at 9/16 standing
505 // and 10/16 hanging (one sixteenth lower and higher). Measured standing on
506 // the pinned server: a mob on a floor lantern stood at its cell floor plus
507 // 0.5625. A full cube here made the step onto one a 1.5-block rise from a
508 // bottom slab, where in game it is a jump a mob takes.
509 if matches!(id, "lantern" | "soul_lantern") || id.ends_with("copper_lantern") {
510 return if state_value(name, "hanging") == Some("true") {
511 10
512 } else {
513 9
514 };
515 }
516 if is_air(name)
517 || is_passable_trap_trigger(name)
518 || is_no_collision_plant(id)
519 || is_no_collision_fixture(id)
520 {
521 return 0;
522 }
523 // Every block no rule above names: the pinned jar's own collision box, where
524 // it rests on the cell floor and tops out at a floor height (an upward
525 // dripstone tip at 11, a chest at 14, a bed at 9, soul sand at 14). A box
526 // under the thin line, one that does not start at the floor, or one the
527 // table does not hold stays the full-cube default, which over-blocks.
528 measured_partial_floor_16(name).unwrap_or(FULL_HEIGHT_16)
529}
530
531/// The pinned jar's collision-box extent per blockstate, in sixteenths:
532/// `crates/dsl/data/collision-tops-1.21.11.tsv`, written by
533/// `tools/maintenance/dump-collision-tops.py` from
534/// `BlockState.getCollisionShape` inside the pinned server jar (provenance in
535/// `crates/delvec/data/PROVENANCE.md`).
536const COLLISION_TSV: &str = include_str!("../data/collision-tops-1.21.11.tsv");
537
538/// A collision-box bound in sixteenths of a block above the cell floor,
539/// exactly: every vanilla box is built in sixteenths, and the few that are not
540/// (a chain's 6.5) are kept as the fraction the table spells rather than
541/// rounded. Negative for a box that reaches into the cell below (a piston
542/// head's arm, a pitcher crop's root).
543#[derive(Debug, Clone, Copy, PartialEq, Eq)]
544pub struct Sixteenths {
545 /// Numerator.
546 pub num: i32,
547 /// Denominator (1 for a whole sixteenth).
548 pub den: u32,
549}
550
551impl Sixteenths {
552 /// The bound as a whole number of sixteenths, or `None` for a fraction.
553 pub fn whole(self) -> Option<i32> {
554 (self.den == 1).then_some(self.num)
555 }
556
557 fn parse(s: &str) -> Option<Sixteenths> {
558 match s.split_once('/') {
559 Some((n, d)) => Some(Sixteenths {
560 num: n.parse().ok()?,
561 den: d.parse().ok()?,
562 }),
563 None => Some(Sixteenths {
564 num: s.parse().ok()?,
565 den: 1,
566 }),
567 }
568 }
569}
570
571/// One blockstate's collision box, vertically: `None` for an empty shape.
572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
573pub struct MeasuredExtent {
574 /// The bottom and top of the box above the cell floor, or `None` when the
575 /// game gives the state no collision at all.
576 pub span: Option<(Sixteenths, Sixteenths)>,
577}
578
579/// A per-blockstate table measured from the pinned jar, keyed by namespaced
580/// block id: each row's distinguishing properties and the value they select.
581/// Every measured per-blockstate table in this crate is this shape, written by
582/// the one collapse in `tools/maintenance/dump-collision-tops.py` (the support
583/// tables, `crate::support`, then write each block's most common value once,
584/// last and bare), and read by the one lookup [`pinned_row`].
585pub(crate) type PinnedRows<T> = std::collections::BTreeMap<String, Vec<(Vec<(String, String)>, T)>>;
586
587/// Parse a measured table: `#` lines skipped, every other line
588/// `block[props]<TAB>a<TAB>b<TAB>count`, the two value columns handed to `value`.
589pub(crate) fn pinned_rows<T>(
590 tsv: &str,
591 what: &str,
592 value: impl Fn(&str, &str) -> T,
593) -> PinnedRows<T> {
594 let mut out = PinnedRows::new();
595 for line in tsv.lines().filter(|l| !l.starts_with('#')) {
596 let cols: Vec<&str> = line.split('\t').collect();
597 let [state, a, b, _count] = cols[..] else {
598 panic!("{what} table row is not four columns: {line:?}");
599 };
600 let id = base_id(state).to_string();
601 let props: Vec<(String, String)> = state
602 .find('[')
603 .map(|open| {
604 state[open + 1..state.len() - 1]
605 .split(',')
606 .filter_map(|kv| kv.split_once('='))
607 .map(|(k, v)| (k.to_string(), v.to_string()))
608 .collect()
609 })
610 .unwrap_or_default();
611 out.entry(id).or_default().push((props, value(a, b)));
612 }
613 out
614}
615
616/// The row of a measured table this block state selects — the FIRST row of its
617/// block whose properties it matches ([`state_has`]) — or `None` when the table
618/// does not hold the block.
619pub(crate) fn pinned_row<'r, T>(rows: &'r PinnedRows<T>, name: &str) -> Option<&'r T> {
620 let id = namespaced_id(name);
621 rows.get(id.as_ref())?
622 .iter()
623 .find(|(props, _)| state_has(name, props))
624 .map(|(_, e)| e)
625}
626
627/// The namespaced block id of a block name (`oak_slab[type=top]` →
628/// `minecraft:oak_slab`).
629pub(crate) fn namespaced_id(name: &str) -> std::borrow::Cow<'_, str> {
630 let id = base_id(name);
631 if id.contains(':') {
632 std::borrow::Cow::Borrowed(id)
633 } else {
634 std::borrow::Cow::Owned(format!("minecraft:{id}"))
635 }
636}
637
638/// Whether a block state carries every one of `props`. A property the name
639/// leaves out is read at the block's pinned default
640/// ([`crate::blocks::BlockRegistry::default_state`]) — what the game resolves
641/// it to. The one property comparison every measured-table reader makes.
642pub(crate) fn state_has(name: &str, props: &[(String, String)]) -> bool {
643 if props.is_empty() {
644 return true;
645 }
646 let defaults = crate::blocks::BlockRegistry::v1_21_11().default_state(&namespaced_id(name));
647 props.iter().all(|(k, v)| {
648 state_value(name, k).or_else(|| defaults.and_then(|d| d.get(k)).map(String::as_str))
649 == Some(v.as_str())
650 })
651}
652
653fn measured_rows() -> &'static PinnedRows<MeasuredExtent> {
654 static ROWS: std::sync::OnceLock<PinnedRows<MeasuredExtent>> = std::sync::OnceLock::new();
655 ROWS.get_or_init(|| {
656 pinned_rows(COLLISION_TSV, "collision", |lo, hi| MeasuredExtent {
657 span: (lo != "-").then(|| {
658 (
659 Sixteenths::parse(lo).expect("collision table bottom parses"),
660 Sixteenths::parse(hi).expect("collision table top parses"),
661 )
662 }),
663 })
664 })
665}
666
667/// **The pinned jar's collision box for this block state**, or `None` when the
668/// table does not hold the block (a non-`minecraft:` id, or one the pin lacks).
669///
670/// A property the name leaves out is read at the block's pinned default
671/// ([`crate::blocks::BlockRegistry::default_state`]) — what the game resolves it
672/// to — so a bare `minecraft:pointed_dripstone` is the upward tip it places.
673pub fn measured_collision(name: &str) -> Option<MeasuredExtent> {
674 pinned_row(measured_rows(), name).copied()
675}
676
677/// The measured floor height of a block whose collision box rests on its cell
678/// floor and tops out in [`THIN_HEIGHT_16`]..[`FULL_HEIGHT_16`] — a body stands
679/// on it lower than on a full cube and cannot walk through it. `None` for any
680/// other box.
681fn measured_partial_floor_16(name: &str) -> Option<u8> {
682 let (lo, hi) = measured_collision(name)?.span?;
683 let top = hi.whole()?;
684 if lo.whole() != Some(0)
685 || !(i32::from(THIN_HEIGHT_16)..i32::from(FULL_HEIGHT_16)).contains(&top)
686 {
687 return None;
688 }
689 u8::try_from(top).ok()
690}
691
692// ---------------------------------------------------------------------------
693// What a body climbs, and what holds it there (spec-0099)
694// ---------------------------------------------------------------------------
695
696/// **The blocks a body climbs**, in pinned Minecraft Java 1.21.11: the members
697/// of the block tag `#minecraft:climbable`, namespaced, in the tag's own order.
698///
699/// **Cited**: `data/minecraft/tags/block/climbable.json` inside the pinned server
700/// jar (`versions.toml` `server_jar_sha256` `f83b8e09…1726`). Being in this tag
701/// is the whole of what makes a body climb: `LivingEntity.onClimbable()` answers
702/// true when the block in the body's FEET cell (`getInBlockState()` at
703/// `blockPosition()`) is in it, and the one other case — an open trapdoor over a
704/// ladder of the same facing (`trapdoorUsableAsLadder`) — is not modelled
705/// (spec-0099 §3.4).
706pub const CLIMBABLE_1_21_11: &[&str] = &[
707 "minecraft:ladder",
708 "minecraft:vine",
709 "minecraft:scaffolding",
710 "minecraft:weeping_vines",
711 "minecraft:weeping_vines_plant",
712 "minecraft:twisting_vines",
713 "minecraft:twisting_vines_plant",
714 "minecraft:cave_vines",
715 "minecraft:cave_vines_plant",
716];
717
718/// One of a block's six faces, in the order the face table spells them
719/// (`d u n s w e`).
720#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
721pub enum Face {
722 /// `-y`.
723 Down,
724 /// `+y`.
725 Up,
726 /// `-z`.
727 North,
728 /// `+z`.
729 South,
730 /// `-x`.
731 West,
732 /// `+x`.
733 East,
734}
735
736impl Face {
737 /// The four horizontal faces, in the table's order.
738 pub const HORIZONTAL: [Face; 4] = [Face::North, Face::South, Face::West, Face::East];
739
740 /// The unit step from a cell to the neighbour across this face.
741 pub fn offset(self) -> [i32; 3] {
742 match self {
743 Face::Down => [0, -1, 0],
744 Face::Up => [0, 1, 0],
745 Face::North => [0, 0, -1],
746 Face::South => [0, 0, 1],
747 Face::West => [-1, 0, 0],
748 Face::East => [1, 0, 0],
749 }
750 }
751
752 /// The face on the other side.
753 pub fn opposite(self) -> Face {
754 match self {
755 Face::Down => Face::Up,
756 Face::Up => Face::Down,
757 Face::North => Face::South,
758 Face::South => Face::North,
759 Face::West => Face::East,
760 Face::East => Face::West,
761 }
762 }
763
764 /// The face's name as a blockstate property spells it (`north`, …).
765 pub fn name(self) -> &'static str {
766 match self {
767 Face::Down => "down",
768 Face::Up => "up",
769 Face::North => "north",
770 Face::South => "south",
771 Face::West => "west",
772 Face::East => "east",
773 }
774 }
775
776 /// The face a blockstate value names, or `None`.
777 pub fn from_name(name: &str) -> Option<Face> {
778 [
779 Face::Down,
780 Face::Up,
781 Face::North,
782 Face::South,
783 Face::West,
784 Face::East,
785 ]
786 .into_iter()
787 .find(|f| f.name() == name)
788 }
789
790 fn letter(self) -> char {
791 self.name().chars().next().expect("a face has a name")
792 }
793}
794
795/// The three plants that grow a column of themselves, a head and a body block
796/// each (`GrowingPlantHeadBlock` / `GrowingPlantBodyBlock`).
797#[derive(Debug, Clone, Copy, PartialEq, Eq)]
798pub enum GrowingVine {
799 /// `weeping_vines` / `weeping_vines_plant` — grows down.
800 Weeping,
801 /// `twisting_vines` / `twisting_vines_plant` — grows up.
802 Twisting,
803 /// `cave_vines` / `cave_vines_plant` — grows down.
804 Cave,
805}
806
807impl GrowingVine {
808 /// The plant's two block ids, head first.
809 pub fn members(self) -> [&'static str; 2] {
810 match self {
811 GrowingVine::Weeping => ["minecraft:weeping_vines", "minecraft:weeping_vines_plant"],
812 GrowingVine::Twisting => ["minecraft:twisting_vines", "minecraft:twisting_vines_plant"],
813 GrowingVine::Cave => ["minecraft:cave_vines", "minecraft:cave_vines_plant"],
814 }
815 }
816
817 /// The direction the plant grows. **Cited**: the `growthDirection` each
818 /// head and body block hands `GrowingPlantBlock`'s constructor in the pinned
819 /// jar — `Direction.DOWN` for weeping and cave vines, `UP` for twisting.
820 pub fn growth(self) -> Face {
821 match self {
822 GrowingVine::Twisting => Face::Up,
823 GrowingVine::Weeping | GrowingVine::Cave => Face::Down,
824 }
825 }
826}
827
828/// **What holds a climbable where it is** — the rule each kind's
829/// `canSurvive` states in the pinned jar. A climbable whose rule fails is
830/// removed by the game at the next shape update it receives, so a model that
831/// credits it credits a climb that is not there.
832#[derive(Debug, Clone, Copy, PartialEq, Eq)]
833pub enum Climbable {
834 /// `ladder[facing=F]`. **Cited**, `LadderBlock.canSurvive`: the block at
835 /// `pos.relative(F.getOpposite())` answers `isFaceSturdy(…, F)` — the ladder
836 /// hangs on the face of the block behind it. Its collision box is a
837 /// 3/16-thick panel against that face (`Block.boxZ(16, 13, 16)` rotated to
838 /// the facing), so a body centred in the cell is clear of it.
839 Ladder {
840 /// The way the ladder faces: away from the block that holds it.
841 facing: Face,
842 },
843 /// `vine`, with the horizontal faces its state sets. **Cited**,
844 /// `VineBlock.canSupportAtFace`: a face is held when the neighbour across
845 /// it is a full face to `MultifaceBlock.canAttachTo` (its support shape's
846 /// face, or its collision shape's), or when the vine above carries the same
847 /// face. The `up` face is not counted as a hold here (spec-0099 §3.2).
848 Vine {
849 /// The horizontal faces the state sets, in [`Face::HORIZONTAL`] order.
850 faces: [bool; 4],
851 },
852 /// A growing plant's head or body. **Cited**, `GrowingPlantBlock.canSurvive`:
853 /// the block it grows from — `pos.relative(growth.getOpposite())` — is the
854 /// same plant, or answers `isFaceSturdy(…, growth)`.
855 Growing(GrowingVine),
856 /// `scaffolding`: in the tag, and **not** modelled as a climb (spec-0099
857 /// §3.3) — the nav model reads it as the full cube its stable shape is.
858 Scaffolding,
859}
860
861/// The [`Climbable`] a block state is, or `None` when it is not in
862/// [`CLIMBABLE_1_21_11`].
863pub fn climbable(name: &str) -> Option<Climbable> {
864 let id = base_id(name);
865 let ns = if id.contains(':') {
866 std::borrow::Cow::Borrowed(id)
867 } else {
868 std::borrow::Cow::Owned(format!("minecraft:{id}"))
869 };
870 if !CLIMBABLE_1_21_11.contains(&ns.as_ref()) {
871 return None;
872 }
873 Some(match ns.as_ref() {
874 "minecraft:ladder" => Climbable::Ladder {
875 // `facing` defaults to north (the pinned default state).
876 facing: state_value(name, "facing")
877 .and_then(Face::from_name)
878 .unwrap_or(Face::North),
879 },
880 "minecraft:vine" => Climbable::Vine {
881 faces: Face::HORIZONTAL.map(|f| state_value(name, f.name()) == Some("true")),
882 },
883 "minecraft:scaffolding" => Climbable::Scaffolding,
884 other => Climbable::Growing(
885 [
886 GrowingVine::Weeping,
887 GrowingVine::Twisting,
888 GrowingVine::Cave,
889 ]
890 .into_iter()
891 .find(|g| g.members().contains(&other))
892 .expect("every other member of the tag is a growing vine"),
893 ),
894 })
895}
896
897/// The pinned jar's full-face table: `crates/dsl/data/faces-1.21.11.tsv`,
898/// written by `tools/maintenance/dump-faces.py` (provenance in
899/// `crates/delvec/data/PROVENANCE.md`).
900const FACES_TSV: &str = include_str!("../data/faces-1.21.11.tsv");
901
902/// One state's two face answers, as six-bit masks over [`Face`] order.
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
904struct FullFaces {
905 sturdy: u8,
906 full: u8,
907}
908
909fn face_mask(letters: &str) -> u8 {
910 if letters == "-" {
911 return 0;
912 }
913 [
914 Face::Down,
915 Face::Up,
916 Face::North,
917 Face::South,
918 Face::West,
919 Face::East,
920 ]
921 .iter()
922 .enumerate()
923 .filter(|(_, f)| letters.contains(f.letter()))
924 .fold(0, |m, (i, _)| m | (1 << i))
925}
926
927fn face_rows() -> &'static PinnedRows<FullFaces> {
928 static ROWS: std::sync::OnceLock<PinnedRows<FullFaces>> = std::sync::OnceLock::new();
929 ROWS.get_or_init(|| {
930 pinned_rows(FACES_TSV, "faces", |s, f| FullFaces {
931 sturdy: face_mask(s),
932 full: face_mask(f),
933 })
934 })
935}
936
937fn face_bit(face: Face) -> u8 {
938 1 << (face as u8)
939}
940
941/// **Is this face of this block state sturdy?** — the pinned jar's
942/// `BlockState.isFaceSturdy(level, pos, face)` (`SupportType.FULL`), read from
943/// the measured table. `false` for a block the table does not hold, which is
944/// the refusing answer for every rule that asks it: nothing hangs there.
945pub fn face_is_sturdy(name: &str, face: Face) -> bool {
946 pinned_row(face_rows(), name).is_some_and(|r| r.sturdy & face_bit(face) != 0)
947}
948
949/// **Is this face of this block state's collision shape the whole square?** —
950/// the pinned jar's `Block.isFaceFull(getCollisionShape(…), face)`, read from the
951/// measured table. `false` for a block the table does not hold.
952pub fn face_is_full(name: &str, face: Face) -> bool {
953 pinned_row(face_rows(), name).is_some_and(|r| r.full & face_bit(face) != 0)
954}
955
956// ---------------------------------------------------------------------------
957// What redstone does to a block (spec-0100 §4.2)
958// ---------------------------------------------------------------------------
959
960/// The pinned jar's redstone table: `crates/dsl/data/redstone-1.21.11.tsv`,
961/// written by `tools/maintenance/dump-redstone.py` (provenance in
962/// `crates/delvec/data/PROVENANCE.md`). One row per block id.
963const REDSTONE_TSV: &str = include_str!("../data/redstone-1.21.11.tsv");
964
965/// Whether a block conducts a strong signal to its neighbours, as the game's
966/// `isRedstoneConductor` answers it over every state.
967#[derive(Debug, Clone, Copy, PartialEq, Eq)]
968pub enum Conductor {
969 /// Every state conducts, whatever its shape (mud, soul sand).
970 Always,
971 /// No state conducts, whatever its shape (glass, leaves, a redstone block).
972 Never,
973 /// The game's default: a state conducts exactly when its collision shape is
974 /// the full block, which the dumper measured equal to "all six collision
975 /// faces full" on every state, so it is read from the face table.
976 Shape,
977}
978
979/// One block's three measured redstone facts.
980#[derive(Debug, Clone, Copy, PartialEq, Eq)]
981pub struct RedstoneRow {
982 /// Whether it conducts a strong signal.
983 pub conductor: Conductor,
984 /// Whether any state is a signal source (`isSignalSource`).
985 pub source: bool,
986 /// Whether any of its classes invokes a `SignalGetter` read: a block whose
987 /// behaviour power can change.
988 pub reads: bool,
989}
990
991fn redstone_rows() -> &'static std::collections::BTreeMap<String, RedstoneRow> {
992 static ROWS: std::sync::OnceLock<std::collections::BTreeMap<String, RedstoneRow>> =
993 std::sync::OnceLock::new();
994 ROWS.get_or_init(|| {
995 let flag = |s: &str, line: &str| match s {
996 "true" => true,
997 "false" => false,
998 _ => panic!("redstone table flag is neither true nor false: {line:?}"),
999 };
1000 REDSTONE_TSV
1001 .lines()
1002 .filter(|l| !l.starts_with('#'))
1003 .map(|line| {
1004 let cols: Vec<&str> = line.split('\t').collect();
1005 let [id, conductor, source, reads] = cols[..] else {
1006 panic!("redstone table row is not four columns: {line:?}");
1007 };
1008 let conductor = match conductor {
1009 "always" => Conductor::Always,
1010 "never" => Conductor::Never,
1011 "shape" => Conductor::Shape,
1012 _ => panic!("redstone table conductor is not always/never/shape: {line:?}"),
1013 };
1014 (
1015 id.to_string(),
1016 RedstoneRow {
1017 conductor,
1018 source: flag(source, line),
1019 reads: flag(reads, line),
1020 },
1021 )
1022 })
1023 .collect()
1024 })
1025}
1026
1027/// **The pinned jar's redstone row for this block**, or `None` when the table
1028/// does not hold it (a foreign namespace, an id the pin lacks).
1029pub fn redstone_row(name: &str) -> Option<RedstoneRow> {
1030 let id = base_id(name);
1031 let id = if id.contains(':') {
1032 std::borrow::Cow::Borrowed(id)
1033 } else {
1034 std::borrow::Cow::Owned(format!("minecraft:{id}"))
1035 };
1036 redstone_rows().get(id.as_ref()).copied()
1037}
1038
1039/// The number of rows the redstone table holds — one per pinned block id.
1040pub fn redstone_row_count() -> usize {
1041 redstone_rows().len()
1042}
1043
1044/// **Does this block state conduct a strong signal to its neighbours?** —
1045/// `isRedstoneConductor` as the pinned jar answers it: `always`/`never` per
1046/// block, else the state's collision shape being the full block (all six faces
1047/// full in the face table). `false` for a block the table does not hold.
1048pub fn redstone_conductor(name: &str) -> bool {
1049 match redstone_row(name).map(|r| r.conductor) {
1050 Some(Conductor::Always) => true,
1051 Some(Conductor::Never) | None => false,
1052 Some(Conductor::Shape) => [
1053 Face::Down,
1054 Face::Up,
1055 Face::North,
1056 Face::South,
1057 Face::West,
1058 Face::East,
1059 ]
1060 .into_iter()
1061 .all(|f| face_is_full(name, f)),
1062 }
1063}
1064
1065/// **Is this block a signal source?** (`isSignalSource` on any state). `false`
1066/// for a block the table does not hold.
1067pub fn redstone_source(name: &str) -> bool {
1068 redstone_row(name).is_some_and(|r| r.source)
1069}
1070
1071/// **Does this block read a redstone signal?** — whether its block, block-entity
1072/// or nested classes invoke a `SignalGetter` read, so power reaching it can
1073/// change what it does. `false` for a block the table does not hold.
1074pub fn redstone_reader(name: &str) -> bool {
1075 redstone_row(name).is_some_and(|r| r.reads)
1076}
1077
1078/// Whether a block is thin enough to be **walked over rather than onto**
1079/// ([`THIN_HEIGHT_16`]): its cell is passable and never a floor level of its own,
1080/// so a walker standing there rests on the block below it. Vanilla agrees — none
1081/// of these blocks obstructs a 1.8-block-tall walker, and every one of them is
1082/// under the 0.6-block auto-step.
1083pub fn is_thin_decoration(name: &str) -> bool {
1084 collision_top_16(name) < THIN_HEIGHT_16
1085}
1086
1087/// Whether a block is a **partial-height floor**: it fills its cell for passage
1088/// purposes but its walkable top face sits below the cell top (a bottom slab, a
1089/// deep snow drift). Its height is [`collision_top_16`].
1090pub fn is_partial_floor(name: &str) -> bool {
1091 let h = collision_top_16(name);
1092 (THIN_HEIGHT_16..FULL_HEIGHT_16).contains(&h)
1093}
1094
1095/// **What a block state does to a body.** The classification every walk in this
1096/// workspace reads; see the module header for the table it stands for.
1097///
1098/// Deliberately an enum rather than a pair of booleans: the six classes are not
1099/// the four corners of two independent questions, and naming them is what lets a
1100/// caller that needs more than passability — a gate is openable, a fluid drowns —
1101/// ask for the distinction instead of re-deriving it from a block name.
1102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1103pub enum Collision {
1104 /// One of the three air blocks: nothing here at all.
1105 Air,
1106 /// An empty or sub-auto-step collision box — a torch, a carpet, a candle, a
1107 /// pressure plate, a tuft of grass. Carries the measured top face, which is
1108 /// under [`THIN_HEIGHT_16`] by construction and is **not** a floor level.
1109 Thin(u8),
1110 /// A floor whose walkable top face is below the cell top: a bottom slab, a
1111 /// deep snow drift, a `dirt_path`. Carries that height in sixteenths.
1112 PartialFloor(u8),
1113 /// A full 1×1×1 cube — the conservative default for everything unrecognised.
1114 FullCube,
1115 /// A fence or a wall: 1.5 blocks tall on a 1-block cell, so a body neither
1116 /// passes through it nor reaches its top.
1117 TallBarrier,
1118 /// A fence gate: a body opens it with the adventure-legal right-click and
1119 /// walks through. Never a floor.
1120 FenceGate,
1121 /// Water or lava filling the cell: a body swims, and stands on nothing.
1122 Fluid,
1123 /// A block a body **climbs** ([`climbable`], every member of
1124 /// `#minecraft:climbable` but scaffolding): a ladder's 3/16 panel against its
1125 /// support, or a vine's empty box. A body centred in the cell is clear of
1126 /// it, and stands on nothing there — what holds a body in this cell is the
1127 /// climb, and the climb is the nav model's to judge, with the hold its
1128 /// support gives it (spec-0099).
1129 Climbable,
1130}
1131
1132impl Collision {
1133 /// **Can a body's own volume occupy this cell?**
1134 pub fn passes_body(self) -> bool {
1135 matches!(
1136 self,
1137 Collision::Air | Collision::Thin(_) | Collision::FenceGate | Collision::Climbable
1138 )
1139 }
1140
1141 /// **Can a body stand on this cell?** Not the complement of
1142 /// [`Collision::passes_body`], and that is the whole reason it is a separate
1143 /// question — a torch is neither, and lava is neither from the other side.
1144 pub fn supports_body(self) -> bool {
1145 matches!(self, Collision::PartialFloor(_) | Collision::FullCube)
1146 }
1147
1148 /// The walkable top face of this block, in sixteenths above its own cell
1149 /// floor, for a class that supports a body. `None` for every class that does
1150 /// not, so a caller cannot silently read a height off a torch.
1151 pub fn floor_top_16(self) -> Option<u8> {
1152 match self {
1153 Collision::PartialFloor(h) => Some(h),
1154 Collision::FullCube => Some(FULL_HEIGHT_16),
1155 _ => None,
1156 }
1157 }
1158}
1159
1160/// Classify a vanilla block state — **the one rule**.
1161///
1162/// The order of the arms is the rule: a fluid is a fluid whatever else it looks
1163/// like; a thin decoration is stepped over before anything asks whether it is a
1164/// gate; and only then do the two 1.5-tall classes separate from the ordinary
1165/// floor.
1166///
1167/// **A cell whose water the block brings with it is a fluid cell** — the
1168/// [`is_submerged_by_nature`] arm sits beside [`is_fluid`] and above the
1169/// thin-decoration arm, because a seagrass tuft is both a no-collision plant and
1170/// a body of water, and the collision model's answer is the water's. Reading it
1171/// as a thin decoration made the sea's own cell report as a place a body's feet
1172/// go.
1173pub fn collision_class(name: &str) -> Collision {
1174 if is_air(name) {
1175 return Collision::Air;
1176 }
1177 if is_fluid(name) || is_submerged_by_nature(name) {
1178 return Collision::Fluid;
1179 }
1180 if climbable(name).is_some_and(|c| c != Climbable::Scaffolding) {
1181 return Collision::Climbable;
1182 }
1183 let top = collision_top_16(name);
1184 if top < THIN_HEIGHT_16 {
1185 return Collision::Thin(top);
1186 }
1187 if is_fence_gate(name) {
1188 return Collision::FenceGate;
1189 }
1190 if is_tall_barrier(name) {
1191 return Collision::TallBarrier;
1192 }
1193 if top < FULL_HEIGHT_16 {
1194 return Collision::PartialFloor(top);
1195 }
1196 Collision::FullCube
1197}
1198
1199/// **Can a body's own volume occupy a cell holding this block?** —
1200/// [`collision_class`] then [`Collision::passes_body`].
1201pub fn passes_body(name: &str) -> bool {
1202 collision_class(name).passes_body()
1203}
1204
1205/// **Can a body stand on a cell holding this block?** —
1206/// [`collision_class`] then [`Collision::supports_body`].
1207pub fn supports_body(name: &str) -> bool {
1208 collision_class(name).supports_body()
1209}
1210
1211/// The walkable top face of this block in sixteenths, or `None` when a body
1212/// cannot stand on it at all — [`collision_class`] then
1213/// [`Collision::floor_top_16`].
1214pub fn floor_top_16(name: &str) -> Option<u8> {
1215 collision_class(name).floor_top_16()
1216}
1217
1218// ---------------------------------------------------------------------------
1219// What a block does to a body that touches it (spec-0062 §3)
1220// ---------------------------------------------------------------------------
1221
1222/// **The blocks vanilla hurts a body with**, in pinned Minecraft Java 1.21.11 —
1223/// bare ids, sorted, one authority.
1224///
1225/// Provenance is [`crate::metrics::Provenance::VanillaRule`] and the qualifier
1226/// matters: this repository has **not** measured a running server for every row.
1227/// The rule each row states is that standing in, on or against the block deals
1228/// damage to a player with no armour enchantment and no status effect, per the
1229/// Minecraft Wiki's own per-block pages for the pinned version (`Lava`, `Fire`,
1230/// `Soul Fire`, `Magma Block`, `Cactus`, `Sweet Berry Bush`, `Wither Rose`,
1231/// `Pointed Dripstone`, `Campfire`, `Soul Campfire`, `Powder Snow`).
1232///
1233/// It is a table of **signals**, not of hazards the engine models. Nothing here
1234/// kills anybody in a delve — the killing is a declared `lethal_volumes[]` box
1235/// and a `/damage` on the death edge. What this list answers is the one question
1236/// spec-0062 §3 asks: *would a player looking at this floor read it as dangerous
1237/// before they stood on it?* A block that hurts is a block that reads that way;
1238/// `minecraft:stone` is not, whatever a declaration claims.
1239///
1240/// Two rows never meet a cell a body can stand in, and they are in the list
1241/// deliberately rather than by oversight: `lava` and `powder_snow` are a fluid
1242/// and a body-swallowing block, so no walked cell holds either — but a killing
1243/// volume drawn one course under a lava surface is the ordinary lava lake, and
1244/// its declaration may say so.
1245pub const HURTING_BLOCKS_1_21_11: &[&str] = &[
1246 "minecraft:cactus",
1247 "minecraft:campfire",
1248 "minecraft:fire",
1249 "minecraft:lava",
1250 "minecraft:magma_block",
1251 "minecraft:pointed_dripstone",
1252 "minecraft:powder_snow",
1253 "minecraft:soul_campfire",
1254 "minecraft:soul_fire",
1255 "minecraft:sweet_berry_bush",
1256 "minecraft:wither_rose",
1257];
1258
1259/// **Does this block state damage a body that meets it?** —
1260/// [`HURTING_BLOCKS_1_21_11`], asked of the bare id.
1261///
1262/// State-insensitive by construction, and the direction is the sound one for
1263/// what asks: a `campfire[lit=false]` is a cold campfire, but a rule that reads
1264/// a *signal* off the bytes wants the block a player recognises, and a player
1265/// reads a fire pit as a fire pit. The reverse error — accepting `stone` because
1266/// somebody wrote it in a `shown_by` — is the one this predicate exists to make
1267/// impossible.
1268/// The list is written the way a creator writes a block — namespaced, because
1269/// that is what a `shown_by` entry and a diagnostic's own printed set both are —
1270/// and matched the way every other classifier here matches, on the bare id. One
1271/// list, both readings.
1272#[must_use]
1273pub fn hurts_body(name: &str) -> bool {
1274 let bare = bare_id(name);
1275 HURTING_BLOCKS_1_21_11.iter().any(|id| bare_id(id) == bare)
1276}
1277
1278#[cfg(test)]
1279mod tests {
1280 use super::*;
1281
1282 /// The table in the module header, asserted row by row. Every other test here
1283 /// is a measurement of one block; this one is the RULE, and it is what reds if
1284 /// somebody decides a fence gate should seal or a fluid should hold a body up.
1285 #[test]
1286 fn the_rule_is_the_table() {
1287 let rows: &[(Collision, bool, bool, Option<u8>)] = &[
1288 (Collision::Air, true, false, None),
1289 (Collision::Thin(0), true, false, None),
1290 (Collision::Thin(6), true, false, None),
1291 (Collision::PartialFloor(8), false, true, Some(8)),
1292 (Collision::PartialFloor(15), false, true, Some(15)),
1293 (Collision::FullCube, false, true, Some(16)),
1294 (Collision::TallBarrier, false, false, None),
1295 (Collision::FenceGate, true, false, None),
1296 (Collision::Fluid, false, false, None),
1297 (Collision::Climbable, true, false, None),
1298 ];
1299 assert_eq!(rows.len(), 10, "the table lost a row");
1300 for &(class, passes, supports, top) in rows {
1301 assert_eq!(class.passes_body(), passes, "{class:?}: passes_body");
1302 assert_eq!(class.supports_body(), supports, "{class:?}: supports_body");
1303 assert_eq!(class.floor_top_16(), top, "{class:?}: floor_top_16");
1304 }
1305 }
1306
1307 /// **A tuft of grass standing in the sea is not a floor.** Every id whose
1308 /// vanilla block brings its own water source answers [`Collision::Fluid`],
1309 /// so nothing stands on one and nothing walks through one — the same answer
1310 /// the water it replaced gave.
1311 ///
1312 /// Stated with its denominator, and with the neighbours that must NOT move:
1313 /// a dry tuft of the same shape is still a thin decoration a body steps
1314 /// over, and a coral fan carries its water as a `waterlogged` state that is
1315 /// read where states are read.
1316 #[test]
1317 fn a_block_that_brings_its_own_water_is_a_fluid_cell() {
1318 let submerged = [
1319 "minecraft:seagrass",
1320 "minecraft:tall_seagrass",
1321 "minecraft:kelp",
1322 "minecraft:kelp_plant",
1323 "minecraft:bubble_column",
1324 ];
1325 assert_eq!(submerged.len(), 5, "the class lost a member");
1326 for id in submerged {
1327 assert!(is_submerged_by_nature(id), "{id}");
1328 assert_eq!(collision_class(id), Collision::Fluid, "{id}");
1329 assert!(!passes_body(id), "{id}: a body does not walk through water");
1330 assert!(!supports_body(id), "{id}: a body does not stand on water");
1331 }
1332 // The dry members of the same no-collision plant class are unmoved.
1333 for id in [
1334 "minecraft:short_grass",
1335 "minecraft:fern",
1336 "minecraft:dead_bush",
1337 ] {
1338 assert!(!is_submerged_by_nature(id), "{id}");
1339 assert_eq!(collision_class(id), Collision::Thin(0), "{id}");
1340 }
1341 // And a waterlogged host block is still its host: a floor, not a sea.
1342 assert!(!is_submerged_by_nature(
1343 "minecraft:oak_stairs[waterlogged=true]"
1344 ));
1345 assert!(supports_body("minecraft:oak_stairs[waterlogged=true]"));
1346 }
1347
1348 /// **The owner's case.** A torch, a candle, a carpet and a pressure plate are
1349 /// things a builder puts in a room she expects to walk across. Every one of
1350 /// them answered "full cube" before spec-0056 — the wall a body could not get
1351 /// past.
1352 #[test]
1353 fn a_torch_is_not_a_wall() {
1354 for id in [
1355 "minecraft:torch",
1356 "minecraft:wall_torch",
1357 "minecraft:soul_torch",
1358 "minecraft:soul_wall_torch",
1359 "minecraft:redstone_torch",
1360 "minecraft:redstone_wall_torch",
1361 "minecraft:white_candle",
1362 "minecraft:candle[candles=4,lit=true]",
1363 "minecraft:red_carpet",
1364 "minecraft:stone_pressure_plate",
1365 "minecraft:oak_pressure_plate",
1366 "minecraft:lever",
1367 "minecraft:stone_button",
1368 "minecraft:oak_sign",
1369 "minecraft:oak_wall_sign",
1370 "minecraft:white_wall_banner",
1371 "minecraft:rail",
1372 "minecraft:powered_rail",
1373 "minecraft:redstone_wire",
1374 "minecraft:flower_pot",
1375 "minecraft:potted_cactus",
1376 "minecraft:glow_lichen",
1377 "minecraft:sculk_vein",
1378 "minecraft:snow[layers=1]",
1379 // Namespaceless spellings reach the same answer.
1380 "torch",
1381 "red_carpet",
1382 ] {
1383 assert!(passes_body(id), "a body must get past {id}");
1384 assert!(!supports_body(id), "and must not stand on {id}");
1385 assert!(collision_top_16(id) < THIN_HEIGHT_16, "{id} is not thin");
1386 }
1387 }
1388
1389 /// The other direction: the conservative default is still a wall, and the
1390 /// no-collision blocks this module deliberately declines to admit are still
1391 /// walls. A green here that came from admitting everything would be no gate
1392 /// at all.
1393 #[test]
1394 fn the_conservative_default_still_refuses() {
1395 for id in [
1396 "minecraft:stone",
1397 "minecraft:oak_stairs[facing=north]",
1398 "minecraft:oak_door[half=lower]",
1399 "minecraft:chain",
1400 "minecraft:end_rod",
1401 "minecraft:iron_bars",
1402 // Empty collision in vanilla, refused here on purpose: a body that
1403 // passes through is not a body that may be routed through.
1404 "minecraft:fire",
1405 "minecraft:cobweb",
1406 "minecraft:nether_portal",
1407 ] {
1408 assert!(!passes_body(id), "{id} must still stop a body");
1409 assert_eq!(collision_class(id), Collision::FullCube, "{id}");
1410 }
1411 }
1412
1413 /// **The climbable class is the tag, and the tag is the jar's.** Every
1414 /// member answers a [`Climbable`]; every member but scaffolding is a cell a
1415 /// body passes and does not stand on; scaffolding stays the full cube its
1416 /// stable shape is; and a block that merely looks like a climb — glow lichen
1417 /// on a wall, a chain, a hanging root — is not one.
1418 #[test]
1419 fn the_climbable_class_is_the_pinned_tag() {
1420 assert_eq!(CLIMBABLE_1_21_11.len(), 9, "the tag lost a member");
1421 for id in CLIMBABLE_1_21_11 {
1422 let c = climbable(id).unwrap_or_else(|| panic!("{id} is in the tag"));
1423 if c == Climbable::Scaffolding {
1424 assert_eq!(collision_class(id), Collision::FullCube, "{id}");
1425 } else {
1426 assert_eq!(collision_class(id), Collision::Climbable, "{id}");
1427 assert!(passes_body(id) && !supports_body(id), "{id}");
1428 }
1429 }
1430 for id in [
1431 "minecraft:glow_lichen",
1432 "minecraft:chain",
1433 "minecraft:hanging_roots",
1434 "minecraft:stone",
1435 ] {
1436 assert_eq!(climbable(id), None, "{id} is not climbed");
1437 }
1438 // The state is read: a ladder's facing, a vine's faces, and the default.
1439 assert_eq!(
1440 climbable("minecraft:ladder[facing=east,waterlogged=false]"),
1441 Some(Climbable::Ladder { facing: Face::East })
1442 );
1443 assert_eq!(
1444 climbable("ladder"),
1445 Some(Climbable::Ladder {
1446 facing: Face::North
1447 })
1448 );
1449 assert_eq!(
1450 climbable("minecraft:vine[north=true,west=true]"),
1451 Some(Climbable::Vine {
1452 faces: [true, false, true, false]
1453 })
1454 );
1455 assert_eq!(
1456 climbable("minecraft:cave_vines_plant[berries=true]"),
1457 Some(Climbable::Growing(GrowingVine::Cave))
1458 );
1459 assert_eq!(GrowingVine::Twisting.growth(), Face::Up);
1460 assert_eq!(GrowingVine::Weeping.growth(), Face::Down);
1461 }
1462
1463 /// **The face table answers the two questions apart.** A full block is
1464 /// sturdy and full on every face; a bottom slab only underneath; leaves have
1465 /// an EMPTY support shape and a full collision box — which is the whole
1466 /// reason a vine hangs on leaves and a ladder does not; a ladder's own panel
1467 /// is full on the face it shows the wall.
1468 #[test]
1469 fn the_face_table_reads_the_jar() {
1470 let all = [
1471 Face::Down,
1472 Face::Up,
1473 Face::North,
1474 Face::South,
1475 Face::West,
1476 Face::East,
1477 ];
1478 for f in all {
1479 assert!(face_is_sturdy("minecraft:stone", f), "stone {f:?}");
1480 assert!(face_is_full("minecraft:oak_planks", f), "planks {f:?}");
1481 assert!(
1482 !face_is_sturdy("minecraft:oak_leaves", f),
1483 "leaves {f:?} hold no ladder"
1484 );
1485 assert!(
1486 face_is_full("minecraft:oak_leaves", f),
1487 "leaves {f:?} hold a vine"
1488 );
1489 assert!(!face_is_sturdy("minecraft:air", f), "air {f:?}");
1490 }
1491 assert!(face_is_sturdy("minecraft:oak_slab", Face::Down));
1492 assert!(!face_is_sturdy("minecraft:oak_slab", Face::Up));
1493 assert!(face_is_sturdy("minecraft:oak_slab[type=top]", Face::Up));
1494 assert!(face_is_sturdy(
1495 "minecraft:ladder[facing=north]",
1496 Face::South
1497 ));
1498 assert!(!face_is_sturdy(
1499 "minecraft:ladder[facing=north]",
1500 Face::North
1501 ));
1502 // An id the pin does not hold holds nothing.
1503 assert!(!face_is_sturdy("minecraft:not_a_block", Face::Up));
1504 }
1505
1506 /// Two classes that are neither passable nor floor, and one that is passable
1507 /// and not floor — the three rows the naive complement gets wrong.
1508 #[test]
1509 fn tall_barriers_gates_and_fluids_are_not_the_complement_of_each_other() {
1510 for fence in ["minecraft:oak_fence", "minecraft:cobblestone_wall"] {
1511 assert_eq!(collision_class(fence), Collision::TallBarrier, "{fence}");
1512 assert!(!passes_body(fence) && !supports_body(fence), "{fence}");
1513 }
1514 for gate in [
1515 "minecraft:oak_fence_gate",
1516 "minecraft:oak_fence_gate[open=false]",
1517 ] {
1518 assert_eq!(collision_class(gate), Collision::FenceGate, "{gate}");
1519 assert!(passes_body(gate) && !supports_body(gate), "{gate}");
1520 }
1521 for fluid in [
1522 "minecraft:water",
1523 "water[level=3]",
1524 "minecraft:lava",
1525 "lava",
1526 ] {
1527 assert_eq!(collision_class(fluid), Collision::Fluid, "{fluid}");
1528 assert!(!passes_body(fluid) && !supports_body(fluid), "{fluid}");
1529 }
1530 // A waterlogged block is its host block, and a body stands on it.
1531 let stair = "minecraft:oak_stairs[facing=north,waterlogged=true]";
1532 assert_eq!(collision_class(stair), Collision::FullCube);
1533 assert!(supports_body(stair));
1534 }
1535
1536 /// State sensitivity: the same block id, two collision boxes. A model that
1537 /// dropped the state would answer one of each pair wrong.
1538 #[test]
1539 fn the_state_decides_the_height() {
1540 let cases: &[(&str, u8)] = &[
1541 ("minecraft:oak_slab", 8),
1542 ("minecraft:oak_slab[type=bottom]", 8),
1543 ("minecraft:oak_slab[type=top]", 16),
1544 ("minecraft:oak_slab[type=double]", 16),
1545 ("minecraft:snow", 0),
1546 ("minecraft:snow[layers=1]", 0),
1547 ("minecraft:snow[layers=5]", 8),
1548 ("minecraft:snow[layers=8]", 14),
1549 ("minecraft:red_carpet", 1),
1550 ("minecraft:moss_carpet", 1),
1551 ("minecraft:pale_moss_carpet", 0),
1552 ("minecraft:pale_moss_carpet[bottom=true]", 1),
1553 ("minecraft:dirt_path", 15),
1554 ("minecraft:farmland", 15),
1555 ("minecraft:stone", 16),
1556 ];
1557 for &(name, want) in cases {
1558 assert_eq!(collision_top_16(name), want, "{name}");
1559 }
1560 assert_eq!(
1561 floor_top_16("minecraft:oak_slab[type=bottom]"),
1562 Some(8),
1563 "a bottom slab is a floor at half height, not a full cube"
1564 );
1565 assert_eq!(
1566 floor_top_16("minecraft:snow[layers=5]"),
1567 Some(8),
1568 "a five-layer drift is a floor at half height"
1569 );
1570 assert_eq!(
1571 floor_top_16("minecraft:torch"),
1572 None,
1573 "nothing stands on a torch, so it has no floor height to read"
1574 );
1575 }
1576
1577 /// A block whose name merely contains a class's name is not in that class.
1578 #[test]
1579 fn a_name_that_contains_a_class_is_not_in_it() {
1580 for id in [
1581 "minecraft:water_cauldron",
1582 "minecraft:lava_cauldron",
1583 "minecraft:torchflower",
1584 "minecraft:white_candle_cake",
1585 ] {
1586 assert!(!is_fluid(id), "{id}");
1587 }
1588 // `torchflower` is a plant with an empty collision box; it reaches that
1589 // answer through the plant class, not through the `_torch` suffix.
1590 assert!(is_no_collision_plant("torchflower"));
1591 assert!(!is_no_collision_fixture("torchflower"));
1592 // A candle CAKE is a cake: the jar's 14/16 cake box, not a 6/16 candle.
1593 assert_eq!(
1594 collision_class("minecraft:white_candle_cake"),
1595 Collision::PartialFloor(14)
1596 );
1597 }
1598
1599 /// **The hurting-block set is the set spec-0062 §3 names, and it is spelled
1600 /// in ids the pinned game has** (spec-0062 criterion 2).
1601 ///
1602 /// Three assertions, and the third is the one that pins the table rather
1603 /// than restating it: every row resolves in the 1.21.11 block registry, so a
1604 /// typo or a renamed id is a red here instead of a `shown_by` nobody can
1605 /// satisfy. The denominator is stated because a list that silently lost a
1606 /// row would otherwise pass every membership assertion it still had.
1607 #[test]
1608 fn the_hurting_block_set_is_the_blocks_vanilla_hurts_with() {
1609 let named = [
1610 "minecraft:lava",
1611 "minecraft:fire",
1612 "minecraft:soul_fire",
1613 "minecraft:magma_block",
1614 "minecraft:cactus",
1615 "minecraft:sweet_berry_bush",
1616 "minecraft:wither_rose",
1617 "minecraft:pointed_dripstone",
1618 "minecraft:campfire",
1619 "minecraft:soul_campfire",
1620 "minecraft:powder_snow",
1621 ];
1622 assert_eq!(
1623 HURTING_BLOCKS_1_21_11.len(),
1624 named.len(),
1625 "the hurting-block table and spec-0062 §3 disagree about how many blocks vanilla \
1626 hurts a body with"
1627 );
1628 for id in named {
1629 assert!(hurts_body(id), "{id} is a block vanilla hurts a body with");
1630 // The bare spelling and a state suffix are the same block.
1631 assert!(hurts_body(bare_id(id)), "{id}, bare");
1632 }
1633 assert!(hurts_body("minecraft:campfire[lit=true]"));
1634 // The floor a player reads as safe, whatever a declaration claims.
1635 for id in [
1636 "minecraft:stone",
1637 "minecraft:air",
1638 "minecraft:oak_planks",
1639 "minecraft:water",
1640 ] {
1641 assert!(!hurts_body(id), "{id} shows a player nothing");
1642 }
1643 let registry = crate::blocks::BlockRegistry::v1_21_11();
1644 for id in HURTING_BLOCKS_1_21_11 {
1645 assert!(
1646 registry.properties(id).is_some(),
1647 "{id} is not a block of the pinned 1.21.11 registry"
1648 );
1649 }
1650 assert!(
1651 HURTING_BLOCKS_1_21_11.windows(2).all(|w| w[0] < w[1]),
1652 "the hurting-block table is not sorted, so its printed set is not deterministic"
1653 );
1654 }
1655
1656 /// A lantern is two stacked boxes, not a cube: its cap tops out at 9/16
1657 /// standing and 10/16 hanging, for every lantern the pinned version has, and
1658 /// the full-cube lanterns that only share the name stay full cubes.
1659 #[test]
1660 fn a_lantern_is_a_partial_floor() {
1661 for id in [
1662 "minecraft:lantern",
1663 "minecraft:soul_lantern",
1664 "minecraft:copper_lantern",
1665 "minecraft:waxed_oxidized_copper_lantern",
1666 ] {
1667 assert!(!passes_body(id), "{id} still stops a body");
1668 assert_eq!(collision_top_16(id), 9, "{id}");
1669 assert_eq!(collision_class(id), Collision::PartialFloor(9), "{id}");
1670 assert_eq!(
1671 collision_top_16(&format!("{id}[hanging=true,waterlogged=false]")),
1672 10,
1673 "{id}"
1674 );
1675 }
1676 for id in ["minecraft:sea_lantern", "minecraft:jack_o_lantern"] {
1677 assert_eq!(collision_top_16(id), FULL_HEIGHT_16, "{id}");
1678 }
1679 }
1680
1681 /// **A body stands on an upward dripstone tip 11/16 into its cell**, where
1682 /// the pinned jar's collision box tops out
1683 /// (`collision-tops-1.21.11.tsv`: `thickness=tip,vertical_direction=up`
1684 /// is `0..11`). The default state is that tip. Every other thickness, and a
1685 /// tip hanging down, reaches the cell top and stays a full height.
1686 #[test]
1687 fn a_dripstone_tip_is_a_partial_floor() {
1688 for id in [
1689 "minecraft:pointed_dripstone",
1690 "minecraft:pointed_dripstone[thickness=tip,vertical_direction=up]",
1691 "pointed_dripstone[thickness=tip,vertical_direction=up,waterlogged=true]",
1692 ] {
1693 assert_eq!(collision_top_16(id), 11, "{id}");
1694 assert_eq!(collision_class(id), Collision::PartialFloor(11), "{id}");
1695 assert!(!passes_body(id), "{id} still stops a body");
1696 }
1697 for id in [
1698 "minecraft:pointed_dripstone[thickness=tip,vertical_direction=down]",
1699 "minecraft:pointed_dripstone[thickness=base,vertical_direction=up]",
1700 "minecraft:pointed_dripstone[thickness=frustum,vertical_direction=up]",
1701 "minecraft:pointed_dripstone[thickness=middle,vertical_direction=up]",
1702 "minecraft:pointed_dripstone[thickness=tip_merge,vertical_direction=up]",
1703 ] {
1704 assert_eq!(collision_top_16(id), FULL_HEIGHT_16, "{id}");
1705 }
1706 }
1707
1708 /// **Every height this module answers is the pinned jar's, or the full-cube
1709 /// default — and no block the jar gives a floor height is left a full cube.**
1710 ///
1711 /// Over every row of the measured table (each a blockstate class the jar's
1712 /// `getCollisionShape` answers one way), [`collision_top_16`] must either
1713 /// equal the measured top (0 for an empty box), or be 16. Where it is 16, the
1714 /// measured box must not be one a body stands on at a floor height — a box
1715 /// from the cell floor to a whole sixteenth in 8..15. That class is the
1716 /// defect this table closed: a dripstone tip modelled as a full block puts a
1717 /// body's feet a course above where vanilla puts them. Anything else the
1718 /// default keeps (an empty box this module refuses to route through, a
1719 /// sub-thin box, a box off the floor, a fence's 24) over-blocks, the
1720 /// direction this module's errors may run.
1721 #[test]
1722 fn every_height_is_the_jars_or_the_full_cube_default() {
1723 let mut judged = 0usize;
1724 let mut exact = 0usize;
1725 let mut bad: Vec<String> = Vec::new();
1726 let mut open_defect: Vec<String> = Vec::new();
1727 for (id, rows) in measured_rows() {
1728 for (props, extent) in rows {
1729 let state = if props.is_empty() {
1730 id.clone()
1731 } else {
1732 let kv: Vec<String> = props.iter().map(|(k, v)| format!("{k}={v}")).collect();
1733 format!("{id}[{}]", kv.join(","))
1734 };
1735 judged += 1;
1736 let e = i32::from(collision_top_16(&state));
1737 let top = match extent.span {
1738 None => Some(0),
1739 Some((_, hi)) => hi.whole(),
1740 };
1741 if Some(e) == top {
1742 exact += 1;
1743 continue;
1744 }
1745 let floor_height = matches!(extent.span,
1746 Some((lo, hi)) if lo.whole() == Some(0)
1747 && hi.whole().is_some_and(|t| (8..16).contains(&t)));
1748 // An open defect, recorded rather than closed here: a wall
1749 // hanging sign's bracket is a 14..16 box at the cell top, and
1750 // the `_sign` fixture rule reads it as empty — the direction
1751 // that admits a step the game refuses. Named and counted, never
1752 // widened.
1753 if e == 0 && id.ends_with("_wall_hanging_sign") {
1754 open_defect.push(state);
1755 continue;
1756 }
1757 if e != i32::from(FULL_HEIGHT_16) || floor_height {
1758 bad.push(format!("{state}: module {e}, jar {:?}", extent.span));
1759 }
1760 }
1761 }
1762 assert!(judged > 5000, "the table bound {judged} row(s)");
1763 assert!(
1764 bad.is_empty(),
1765 "{} of {judged} row(s) disagree: {bad:#?}",
1766 bad.len()
1767 );
1768 assert_eq!(
1769 open_defect.len(),
1770 12,
1771 "the wall-hanging-sign exception moved: {open_defect:#?}"
1772 );
1773 eprintln!(
1774 "collision heights: {judged} measured row(s), {exact} answered exactly, {} wall \
1775 hanging sign row(s) read as empty against a 14..16 bracket (open defect)",
1776 open_defect.len()
1777 );
1778 }
1779
1780 /// **The redstone table reads the jar** (spec-0100 §4.2): every spot check the
1781 /// spec names, and one row per pinned block. A missing row answers `false` to
1782 /// every question, so `oak_door` reading and `stone` not is what proves the
1783 /// table is read at all.
1784 #[test]
1785 fn the_redstone_table_reads_the_jar() {
1786 for id in [
1787 "minecraft:oak_door",
1788 "minecraft:iron_door",
1789 "minecraft:rail",
1790 "minecraft:bell",
1791 "minecraft:redstone_lamp",
1792 "minecraft:copper_bulb",
1793 "minecraft:big_dripleaf",
1794 "minecraft:calibrated_sculk_sensor",
1795 ] {
1796 assert!(redstone_reader(id), "{id} reads a signal");
1797 }
1798 for id in [
1799 "minecraft:stone",
1800 "minecraft:lever",
1801 "minecraft:stone_pressure_plate",
1802 "minecraft:sculk_sensor",
1803 "minecraft:sculk_shrieker",
1804 "minecraft:sculk_catalyst",
1805 ] {
1806 assert!(!redstone_reader(id), "{id} reads no signal");
1807 }
1808 for id in [
1809 "minecraft:lever",
1810 "minecraft:sculk_sensor",
1811 "minecraft:lightning_rod",
1812 ] {
1813 assert!(redstone_source(id), "{id} is a source");
1814 }
1815 assert!(!redstone_source("minecraft:stone"));
1816 assert_eq!(
1817 redstone_row("minecraft:stone").map(|r| r.conductor),
1818 Some(Conductor::Shape)
1819 );
1820 assert_eq!(
1821 redstone_row("minecraft:glass").map(|r| r.conductor),
1822 Some(Conductor::Never)
1823 );
1824 assert!(redstone_conductor("minecraft:stone"));
1825 assert!(!redstone_conductor("minecraft:glass"));
1826 // `shape` is read per state: a bottom slab does not conduct, a double does.
1827 assert!(!redstone_conductor("minecraft:stone_slab[type=bottom]"));
1828 assert!(redstone_conductor("minecraft:stone_slab[type=double]"));
1829 // An id the pin does not hold answers false to every question.
1830 assert!(redstone_row("minecraft:not_a_block").is_none());
1831 assert!(!redstone_reader("minecraft:not_a_block"));
1832 assert!(!redstone_conductor("minecraft:not_a_block"));
1833 assert_eq!(
1834 redstone_row_count(),
1835 crate::blocks::BlockRegistry::v1_21_11().len(),
1836 "one row per pinned block id"
1837 );
1838 }
1839
1840 /// **The redstone table agrees with the Minecraft Wiki** — the second method,
1841 /// sharing nothing with the dumper's reflection and bytecode read. Each row
1842 /// below is compared against the wording of the wiki's *Conductivity* page
1843 /// (<https://minecraft.wiki/w/Conductivity>) and the navbox of its
1844 /// *Mechanism* page (<https://minecraft.wiki/w/Mechanism>):
1845 ///
1846 /// - conductive: "Stone" (listed); "most conductive blocks are full solid
1847 /// blocks with the exception of soul sand and mud" — the two `always` rows;
1848 /// - non-conductive: "Glass", "Leaves", "Glowstone", "Ice", "TNT",
1849 /// "Observer", "Piston", "Block of redstone", "Copper bulb" (listed); the
1850 /// page also says "Blocks of redstone, observers, and pistons are full
1851 /// solid blocks … but are non-conductive";
1852 /// - power emission (navbox): block of redstone, buttons, daylight detector,
1853 /// detector rail, jukebox, lectern, lever, lightning rod, observer,
1854 /// pressure plates, comparator, redstone torch, sculk sensor, target,
1855 /// trapped chest, tripwire hook;
1856 /// - mechanisms (navbox): bell, big dripleaf, copper bulb, doors, fence gate,
1857 /// head, note block, redstone lamp, shelf, TNT, trapdoors, crafter,
1858 /// dispenser, dropper, hopper, piston, activator and powered rails. The
1859 /// navbox's "misc." group also lists the armor stand and the creaking
1860 /// heart, neither of which reacts to power (an entity, and a block that
1861 /// only emits a comparator signal); the table's `false` for
1862 /// `creaking_heart` is the one row compared that disagrees with the
1863 /// group's wording, and the group is not a claim that it reads power.
1864 #[test]
1865 fn the_redstone_table_agrees_with_the_wiki() {
1866 for id in ["stone", "soul_sand", "mud"] {
1867 assert!(redstone_conductor(id), "wiki: {id} is conductive");
1868 }
1869 assert_eq!(
1870 redstone_row("soul_sand").map(|r| r.conductor),
1871 Some(Conductor::Always)
1872 );
1873 assert_eq!(
1874 redstone_row("mud").map(|r| r.conductor),
1875 Some(Conductor::Always)
1876 );
1877 for id in [
1878 "glass",
1879 "oak_leaves",
1880 "glowstone",
1881 "ice",
1882 "tnt",
1883 "observer",
1884 "piston",
1885 "redstone_block",
1886 "copper_bulb",
1887 ] {
1888 assert!(!redstone_conductor(id), "wiki: {id} is non-conductive");
1889 }
1890 for id in [
1891 "redstone_block",
1892 "oak_button",
1893 "daylight_detector",
1894 "detector_rail",
1895 "jukebox",
1896 "lectern",
1897 "lever",
1898 "lightning_rod",
1899 "observer",
1900 "stone_pressure_plate",
1901 "comparator",
1902 "redstone_torch",
1903 "sculk_sensor",
1904 "target",
1905 "trapped_chest",
1906 "tripwire_hook",
1907 ] {
1908 assert!(redstone_source(id), "wiki power emission: {id}");
1909 }
1910 for id in [
1911 "bell",
1912 "big_dripleaf",
1913 "copper_bulb",
1914 "oak_door",
1915 "iron_door",
1916 "copper_door",
1917 "oak_fence_gate",
1918 "creeper_head",
1919 "note_block",
1920 "redstone_lamp",
1921 "oak_shelf",
1922 "tnt",
1923 "oak_trapdoor",
1924 "iron_trapdoor",
1925 "crafter",
1926 "dispenser",
1927 "dropper",
1928 "hopper",
1929 "piston",
1930 "activator_rail",
1931 "powered_rail",
1932 ] {
1933 assert!(redstone_reader(id), "wiki mechanism: {id}");
1934 }
1935 assert!(!redstone_reader("creaking_heart"));
1936 }
1937
1938 /// **The sculk blocks are half floors** (spec-0100 §2.1): the three 8/16
1939 /// blocks are a bottom slab to the nav model and the catalyst a full cube,
1940 /// read from the measured collision table.
1941 #[test]
1942 fn the_sculk_blocks_are_half_floors() {
1943 for id in [
1944 "minecraft:sculk_sensor",
1945 "minecraft:calibrated_sculk_sensor",
1946 "minecraft:sculk_shrieker",
1947 ] {
1948 assert_eq!(collision_class(id), Collision::PartialFloor(8), "{id}");
1949 }
1950 assert_eq!(
1951 collision_class("minecraft:sculk_catalyst"),
1952 Collision::FullCube
1953 );
1954 assert_eq!(
1955 collision_class("minecraft:stone_slab[type=bottom]"),
1956 collision_class("minecraft:sculk_sensor"),
1957 "a sensor is a bottom slab to a body"
1958 );
1959 }
1960
1961 /// The barriers a hand opens: every fence gate, door and trapdoor except the
1962 /// two iron ones.
1963 #[test]
1964 fn the_player_opens_wood_and_copper_and_not_iron() {
1965 for id in [
1966 "minecraft:oak_fence_gate[open=false]",
1967 "minecraft:spruce_door[half=lower]",
1968 "minecraft:copper_door",
1969 "minecraft:dark_oak_trapdoor",
1970 "minecraft:waxed_copper_trapdoor",
1971 ] {
1972 assert!(is_player_openable(id), "{id}");
1973 }
1974 for id in [
1975 "minecraft:iron_door",
1976 "minecraft:iron_trapdoor",
1977 "minecraft:oak_fence",
1978 "minecraft:iron_bars",
1979 "minecraft:stone",
1980 ] {
1981 assert!(!is_player_openable(id), "{id}");
1982 }
1983 }
1984}