Skip to main content

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}