delvewright_dsl/blocks.rs
1//! The pinned 1.21.11 **block-state registry**, and the check every emitter of a
2//! structure template owes it.
3//!
4//! # Why this exists
5//!
6//! CLAUDE.md, on the `delvec prefab` finding: *an EMITTED command is
7//! checked against the pinned command tree by the emitter, not by a test, because
8//! the operator running the tool does not run `cargo test`.* Blocks had no such
9//! rule. The consequence is measured, not hypothetical: 1.21.11 renamed
10//! `minecraft:chain` to `minecraft:iron_chain`, and
11//! `prefabs/tidal-keep-generator` kept placing the old id — eight cells of
12//! bell-rope in `tk-bell-tower.nbt` that do not exist. A structure template
13//! carrying an unknown block loads it as **air**: the generator exits 0, the
14//! `.nbt` is well-formed, the byte-identity gate passes, the tower simply has no
15//! ropes, and nothing anywhere says so. The same class is in the shipped
16//! library: `hero-temple-ruin-arch.nbt` carries `minecraft:chain` at `[4, 9, 13]`
17//! (1 of 36 prefabs, measured 2026-08-11 with `delvec prefab audit`).
18//!
19//! So this module is the block half of the command rule. It lives here, beside
20//! the other pinned registries, because the registry is a fact about the pinned
21//! game rather than about any one reader of it: the structure-template writer
22//! (`delvec::schem`, which re-exports this module), the grammar back end,
23//! the admission audit and the compiler's own render surface all check against
24//! this one table. And no crate is the only site that turns a palette into
25//! `.nbt` bytes — reasoning as though one were is what left the sixth emitter
26//! unguarded. The seven `prefabs/*-generator` packages may not depend on
27//! `delvec` and reach the same rule through `prefab-invariants`, which depends
28//! on this crate. Which sites owe it is therefore not remembered: it
29//! is discovered from the ingredient by `tools/check-structure-emitters.py`,
30//! which treats every tracked file that calls the NBT serialiser as a
31//! candidate, and requires each to name the rule or declare why it need not.
32//!
33//! # What it validates
34//!
35//! A full block state: the id, every property name, and every property value,
36//! against `crates/dsl/data/blocks-1.21.11.json` (see that directory's
37//! `PROVENANCE.md`). Nothing here is a heuristic and nothing is a warning —
38//! either 1.21.11 has that state or it does not.
39//!
40//! It deliberately does **not** validate that a state is *sensible* (a floating
41//! stair, a waterlogged block in the sky). That is craft, and craft is judged by
42//! the gates over the expanded model; this is spelling.
43
44use std::collections::BTreeMap;
45use std::fmt;
46use std::sync::OnceLock;
47
48/// The pinned registry's JSON text, vendored beside the other 1.21.11 data.
49///
50/// One authority, shared by this crate, the grammar back end and the
51/// out-of-workspace prefab generators, which read the same file
52/// (`prefabs/invariants/src/invariants.rs`) because none of them may depend on
53/// `delvec`. A moved data file is a compile error, which is the loud failure.
54const REGISTRY_JSON: &str = include_str!("../data/blocks-1.21.11.json");
55
56/// The shape-carrying properties per block: the properties named by `multipart`
57/// selectors in the block's own blockstate definition, derived from the 1.21.11
58/// client jar by `tools/extract-shape-properties.py` (see
59/// `crates/delvec/data/PROVENANCE.md`). A `variants` property picks one
60/// complete model, so omitting it renders the author's default; a `multipart`
61/// property *assembles* the model, so omitting it drops geometry — wall arms,
62/// pane connections, vine faces. That is the class line `DW0735` fires on.
63const SHAPE_JSON: &str = include_str!("../data/blockstate-shape-props-1.21.11.json");
64
65/// The pinned **default state** of every block: the value the game resolves each
66/// unwritten property to.
67///
68/// A third table because it answers a third question. The registry says which
69/// properties are legal; the shape table says which of them the model is
70/// assembled from; this says what a palette entry that leaves one out actually
71/// MEANS. A structure template may leave properties out — vanilla fills them on
72/// load, so the file is legal and the server places the right block — and every
73/// reader that is not a running server then has to work it out. Guessing is not
74/// close: a bare `minecraft:cobblestone_wall` is a wall POST (`up=true`, every
75/// side `none`), and "the first legal value" yields `up=false` with `east=low`,
76/// which is a different block.
77const DEFAULTS_JSON: &str = include_str!("../data/block-defaults-1.21.11.json");
78
79/// The block-id **renames** the game's DataFixerUpper applies on load: an id the
80/// pin does not have -> the id it becomes, with the greatest `DataVersion` at
81/// which the old id still existed.
82///
83/// A fourth table because there is a fourth question, and the repo was answering
84/// it twice with two different answers. The registry says *whether the pin has
85/// this id*; it cannot say *what the pin will hold instead* — and a check that
86/// judges a pre-pin template's id AS WRITTEN is judging a name in the wrong
87/// vocabulary. `delvec prefab audit` did exactly that: the spelling rule passed
88/// `minecraft:chain` in a DataVersion-2975 template because the fixer migrates
89/// it, and the palette allowlist refused the same cell in the next breath
90/// because `minecraft:chain` is not a name at the pin.
91///
92/// Derived from Mojang's own published data by
93/// `tools/extract-block-renames.py` (see `crates/delvec/data/PROVENANCE.md`):
94/// which ids left the registry and when, from the per-version block registries;
95/// what each became, from the crafting recipe whose ingredient side is
96/// unchanged across the version step. A removal the recipe graph cannot pair is
97/// deliberately ABSENT rather than guessed, and absence fails closed — the
98/// audit still refuses the id, so an incomplete table costs a false red and
99/// never a false pass.
100const RENAMES_JSON: &str = include_str!("../data/block-renames-1.21.11.json");
101
102/// The Minecraft version this registry describes (ADR-0009).
103pub const MC_VERSION: &str = "1.21.11";
104
105/// The pinned `DataVersion` (ADR-0009), duplicated from `convert::DATA_VERSION`
106/// deliberately — a drift between the two is a compile-time-checkable bug, and
107/// `judge_at`'s tests pin them equal.
108pub const PIN_DATA_VERSION: i32 = 4671;
109
110// ---------------------------------------------------------------------------
111// The blockstate diagnostic family (one model, five rules). Codes are defined
112// here — the crate every emitter and auditor of a block state already depends
113// on — so the next consumer reuses the rule instead of rewriting the unchecked
114// version. A rule that lives, correct, inside ONE caller leaves the next one
115// nothing to reuse (CLAUDE.md).
116// Documented in docs/reference/compiler.md §diagnostics.
117// ---------------------------------------------------------------------------
118
119/// A pre-pin structure template carries a block state the pin does not know:
120/// the game's DataFixerUpper is expected to migrate it on load (warning).
121pub const DW_STATE_PRE_PIN: &str = "DW0734";
122/// A block state omits a shape-carrying (multipart) property (error).
123pub const DW_SHAPE_OMITTED: &str = "DW0735";
124/// A grammar fill wrote an orientation-sensitive block state into a reoriented
125/// scope with no `orientation` guard pinning it (error).
126pub const DW_ORIENTED_FILL_UNGUARDED: &str = "DW0736";
127/// An authored block state omits a property the block has, so its geometry is
128/// whatever a 1.21.11 server derives and no other reader can know it (error).
129pub const DW_STATE_UNDER_SPECIFIED: &str = "DW0737";
130/// A block state written in a scope's own axis names carries a property the
131/// pinned vocabulary cannot map onto the world frame the scope was given
132/// (error).
133pub const DW_LOCAL_FRAME_UNRESOLVABLE: &str = "DW0738";
134/// A world-frame fill of a frame-sensitive block state stood under a
135/// reorientation that this region resolved to the identity, so `DW0736` had no
136/// frame to judge it against. **Undecided**, neither pass nor fail.
137pub const DW_ORIENTED_FILL_UNDECIDED: &str = "DW0742";
138
139/// The verdict on one block state, judged against the pin **and** the
140/// `DataVersion` of the file that carries it.
141///
142/// Minecraft datafixes every structure `.nbt` it loads, against the
143/// `DataVersion` the file declares — so "this id does not exist at the pin" is
144/// only a defect when no datafix will run. A file declaring the pinned
145/// `DataVersion` (or later) gets no fixes at all: its unknown block really does
146/// load as AIR, which is how `tk-bell-tower.nbt` shipped a bell tower with no
147/// bell ropes. A file declaring an older `DataVersion` is DataFixerUpper's
148/// business: `prefabs/hero-temple-ruin-arch.nbt` (DataVersion 2975) carries
149/// `minecraft:chain`, which schema 4541 renames `iron_chain`, and loads
150/// correctly — refusing it is a false positive, not rigor.
151///
152/// The rule is deliberately conservative in the one direction that stays
153/// sound: an invalid id in a file whose `DataVersion` sits *between* the
154/// responsible fixer's schema and the pin would also load as air, but the
155/// fixer schedule lives inside the proprietary jar and nothing in this repo
156/// can read it — so pre-pin invalidity is a **warning** (`DW0734`), loud
157/// enough to catch a typo that no fixer will ever map, and never a refusal of
158/// a file the game loads fine.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum StateJudgement {
161 /// The pin has this exact state.
162 Valid,
163 /// Not a pinned state, and the file claims the pin (or later): no datafix
164 /// will run, so the block loads as air. An error.
165 InvalidAtPin(BlockError),
166 /// Not a pinned state, but the file pre-dates the pin: load-time
167 /// datafixing is expected to migrate it. A warning (`DW_STATE_PRE_PIN`).
168 PrePin(BlockError),
169}
170
171/// Why a block state is not a 1.21.11 block state.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum BlockError {
174 /// No block with this id exists.
175 UnknownBlock {
176 /// The id as written.
177 name: String,
178 /// The registry ids closest to it, best first — usually the rename.
179 suggestions: Vec<String>,
180 },
181 /// The block exists but has no such property.
182 UnknownProperty {
183 /// The block id.
184 name: String,
185 /// The property as written.
186 property: String,
187 /// Every property the block does have.
188 known: Vec<String>,
189 },
190 /// The property exists but not with this value.
191 BadValue {
192 /// The block id.
193 name: String,
194 /// The property.
195 property: String,
196 /// The value as written.
197 value: String,
198 /// Every legal value.
199 legal: Vec<String>,
200 },
201}
202
203impl fmt::Display for BlockError {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 match self {
206 BlockError::UnknownBlock { name, suggestions } => {
207 write!(f, "{name} is not a block in Minecraft {MC_VERSION}")?;
208 if !suggestions.is_empty() {
209 write!(f, " — did you mean {}?", suggestions.join(", "))?;
210 }
211 write!(
212 f,
213 " (a structure template loads an unknown block as AIR, so this would ship a \
214 hole rather than fail)"
215 )
216 }
217 BlockError::UnknownProperty {
218 name,
219 property,
220 known,
221 } => write!(
222 f,
223 "{name} has no property {property:?} in Minecraft {MC_VERSION}; it has {}",
224 if known.is_empty() {
225 "none".to_string()
226 } else {
227 known.join(", ")
228 }
229 ),
230 BlockError::BadValue {
231 name,
232 property,
233 value,
234 legal,
235 } => write!(
236 f,
237 "{name}[{property}={value}] is not legal in Minecraft {MC_VERSION}; {property} is \
238 one of {}",
239 legal.join(", ")
240 ),
241 }
242 }
243}
244
245impl std::error::Error for BlockError {}
246
247/// One entry of the vendored rename table.
248#[derive(Debug, Clone, serde::Deserialize)]
249pub struct BlockRename {
250 /// The id the pinned version holds instead.
251 pub to: String,
252 /// The greatest `DataVersion` at which the OLD id still existed — a lower
253 /// bound on the schema that performs the rename, because the fix landed
254 /// somewhere inside the development cycle that follows and the fixer
255 /// schedule lives in the game jar. A file at or below this certainly
256 /// pre-dates the fix; above it, this table says nothing.
257 pub valid_through: i32,
258}
259
260/// Where a written block id ends up once the game has datafixed a file that
261/// declares `data_version` — the question an allowlist, a palette screen or a
262/// render surface has to ask before it judges a NAME.
263#[derive(Debug, Clone, PartialEq, Eq)]
264pub enum LoadedId<'a> {
265 /// The pin holds this id as written. Also the answer for a non-`minecraft:`
266 /// namespace, which is a datapack's own block and none of this registry's
267 /// business.
268 AsWritten,
269 /// The pin does not have the id, and a derived rename reaches it from this
270 /// file's `DataVersion`: the game loads `to`.
271 Renamed {
272 /// The id the pin will hold.
273 to: &'a str,
274 /// The bound the resolution turned on.
275 valid_through: i32,
276 },
277 /// The pin does not have the id and no rename in the vendored table reaches
278 /// it from this `DataVersion`. **Not** a claim that the game loads nothing —
279 /// a claim that this repo cannot say what it loads, which is why every
280 /// caller treats it as the id as written and refuses rather than passes.
281 Unresolved,
282}
283
284/// Every block id in the pinned version, with every property's legal values —
285/// plus, per block, which of those properties are shape-carrying.
286pub struct BlockRegistry {
287 blocks: BTreeMap<String, BTreeMap<String, Vec<String>>>,
288 shape: BTreeMap<String, Vec<String>>,
289 defaults: BTreeMap<String, BTreeMap<String, String>>,
290 renames: BTreeMap<String, BlockRename>,
291}
292
293impl BlockRegistry {
294 /// The pinned 1.21.11 registry, parsed once per process.
295 pub fn v1_21_11() -> &'static BlockRegistry {
296 static REGISTRY: OnceLock<BlockRegistry> = OnceLock::new();
297 REGISTRY.get_or_init(|| BlockRegistry {
298 blocks: serde_json::from_str(REGISTRY_JSON)
299 .expect("the vendored block registry is valid JSON"),
300 shape: serde_json::from_str(SHAPE_JSON)
301 .expect("the vendored shape-property table is valid JSON"),
302 defaults: serde_json::from_str(DEFAULTS_JSON)
303 .expect("the vendored block default-state table is valid JSON"),
304 renames: serde_json::from_str(RENAMES_JSON)
305 .expect("the vendored block rename table is valid JSON"),
306 })
307 }
308
309 /// The derived rename table, keyed by the id that no longer exists.
310 pub fn renames(&self) -> &BTreeMap<String, BlockRename> {
311 &self.renames
312 }
313
314 /// **Which id the game will actually load**, for a template that declares
315 /// `data_version`.
316 ///
317 /// Minecraft datafixes every structure `.nbt` it loads against the file's
318 /// own `DataVersion`, so a pre-pin template's id is written in an older
319 /// vocabulary and nothing may judge it as a name at the pin without first
320 /// resolving it. This is that resolution, and it is deliberately
321 /// conservative: a rename applies only where the file is at or below the
322 /// last `DataVersion` the old id existed at, so a file in the gap between
323 /// that bound and the pin resolves to [`LoadedId::Unresolved`] and is
324 /// refused rather than waved through.
325 pub fn loaded_id_at(&self, name: &str, data_version: i32) -> LoadedId<'_> {
326 let namespaced = namespace(name).into_owned();
327 if !namespaced.starts_with("minecraft:") || self.blocks.contains_key(&namespaced) {
328 return LoadedId::AsWritten;
329 }
330 match self.renames.get(&namespaced) {
331 Some(r) if data_version <= r.valid_through => LoadedId::Renamed {
332 to: &r.to,
333 valid_through: r.valid_through,
334 },
335 _ => LoadedId::Unresolved,
336 }
337 }
338
339 /// How many blocks the pinned version has. A binding count: a check that
340 /// reports zero here examined nothing.
341 pub fn len(&self) -> usize {
342 self.blocks.len()
343 }
344
345 /// Never true for the pinned registry; present because `len` is public.
346 pub fn is_empty(&self) -> bool {
347 self.blocks.is_empty()
348 }
349
350 /// True if `name` (namespaced, e.g. `minecraft:stone`) is a block.
351 pub fn has(&self, name: &str) -> bool {
352 self.blocks.contains_key(name)
353 }
354
355 /// Check an id plus its properties.
356 ///
357 /// A non-`minecraft:` namespace is accepted without inspection: a datapack
358 /// may legitimately define its own blocks, and this registry has nothing to
359 /// say about them. A bare id is read as `minecraft:`-namespaced, which is
360 /// how every emitter in this repo writes one.
361 pub fn validate(
362 &self,
363 name: &str,
364 properties: &BTreeMap<String, String>,
365 ) -> Result<(), BlockError> {
366 let namespaced = namespace(name).into_owned();
367 if !namespaced.starts_with("minecraft:") {
368 return Ok(());
369 }
370 let Some(known) = self.blocks.get(&namespaced) else {
371 return Err(BlockError::UnknownBlock {
372 suggestions: self.suggest(&namespaced),
373 name: namespaced,
374 });
375 };
376 for (property, value) in properties {
377 let Some(legal) = known.get(property) else {
378 return Err(BlockError::UnknownProperty {
379 name: namespaced,
380 property: property.clone(),
381 known: known.keys().cloned().collect(),
382 });
383 };
384 if !legal.contains(value) {
385 return Err(BlockError::BadValue {
386 name: namespaced,
387 property: property.clone(),
388 value: value.clone(),
389 legal: legal.clone(),
390 });
391 }
392 }
393 Ok(())
394 }
395
396 /// Check a `name[k=v,...]` state string.
397 pub fn validate_state_string(&self, state: &str) -> Result<(), BlockError> {
398 let (name, properties) = parse_state(state);
399 self.validate(name, &properties)
400 }
401
402 /// Judge a state against the pin **and** the carrying file's `DataVersion`.
403 /// See [`StateJudgement`] for the rule and its derivation.
404 pub fn judge_at(
405 &self,
406 name: &str,
407 properties: &BTreeMap<String, String>,
408 data_version: i32,
409 ) -> StateJudgement {
410 match self.validate(name, properties) {
411 Ok(()) => StateJudgement::Valid,
412 Err(e) if data_version >= PIN_DATA_VERSION => StateJudgement::InvalidAtPin(e),
413 Err(e) => StateJudgement::PrePin(e),
414 }
415 }
416
417 /// Every property of `name` with its legal values, or `None` for an id the
418 /// pinned version does not have.
419 pub fn properties(&self, name: &str) -> Option<&BTreeMap<String, Vec<String>>> {
420 self.blocks.get(namespace(name).as_ref())
421 }
422
423 /// The block's default state — what the game resolves each unwritten
424 /// property to. `None` for an id the pinned version does not have; an empty
425 /// map for a block that has no properties at all.
426 ///
427 /// **This table says what a reader must ASSUME, never what an emitter may
428 /// WRITE.** A reader that is not a running server — the review page, a
429 /// diff, a walk — has to fill an omitted property from somewhere, and this
430 /// is where. An emitter filling one from here is a different act: it turns
431 /// a state that said nothing into a state that explicitly asserts the
432 /// default, and for every connection property the default is
433 /// *disconnected*. So a completion pass over a library would answer
434 /// [`Self::omitted_shape_carrying`] with the empty set for every block,
435 /// forever — the shape rule (`DW0735`) and its whole library sweep would go
436 /// green by ceasing to bind, over a library whose walls are still isolated
437 /// posts. What an emitter owes instead is the connection derived from the
438 /// blocks beside the cell (`prefabs/invariants/src/connections.rs`), and
439 /// `tools/check-structure-emitters.py` is what holds every emitter to it.
440 pub fn default_state(&self, name: &str) -> Option<&BTreeMap<String, String>> {
441 self.defaults.get(namespace(name).as_ref())
442 }
443
444 /// The properties `written` leaves out, each with the value the game would
445 /// fill it with. Empty when the state is complete — and empty, too, for an
446 /// id this registry does not know, which has no defaults to offer and is
447 /// already an [`BlockError::UnknownBlock`] to [`Self::validate`].
448 ///
449 /// Broader than [`Self::omitted_shape_carrying`] on purpose, and the two
450 /// answer different questions: that one asks whether the block's *model* is
451 /// assembled from parts the property selects, this one asks what a reader
452 /// that is not a running server would have to fill in to know what the file
453 /// means at all. Being broader is exactly why it is not the repair for the
454 /// narrower one — see [`Self::default_state`], whose table this reads.
455 pub fn unwritten(
456 &self,
457 name: &str,
458 written: &BTreeMap<String, String>,
459 ) -> BTreeMap<String, String> {
460 let Some(default) = self.defaults.get(namespace(name).as_ref()) else {
461 return BTreeMap::new();
462 };
463 default
464 .iter()
465 .filter(|(k, _)| !written.contains_key(*k))
466 .map(|(k, v)| (k.clone(), v.clone()))
467 .collect()
468 }
469
470 /// The shape-carrying properties of `name` — the properties its blockstate
471 /// definition's `multipart` selectors test. Empty for a block whose model
472 /// is not assembled from parts, for a foreign namespace, and for an unknown
473 /// id (the unknown-block diagnostic owns that case).
474 pub fn shape_carrying(&self, name: &str) -> &[String] {
475 let namespaced = namespace(name);
476 self.shape
477 .get(namespaced.as_ref())
478 .map(Vec::as_slice)
479 .unwrap_or(&[])
480 }
481
482 /// The legal values of one property of one block, empty when either is
483 /// unknown or the namespace is foreign.
484 pub fn values(&self, name: &str, property: &str) -> &[String] {
485 let namespaced = namespace(name);
486 self.blocks
487 .get(namespaced.as_ref())
488 .and_then(|props| props.get(property))
489 .map(Vec::as_slice)
490 .unwrap_or(&[])
491 }
492
493 /// **True when `name` is a stair block**, derived from the pinned registry
494 /// rather than from a list: a stair is the block whose `shape` property
495 /// takes vanilla's five stair values, and nothing else in the game has one.
496 ///
497 /// The derivation matters because the property it feeds
498 /// (`delvec::schem::stairs::derive_shape`) tests *any* stair against *any* other
499 /// — an oak stair mitres against a stone-brick one — so a hand-kept list
500 /// would be wrong the day a version adds a stair, in the silent direction.
501 pub fn is_stairs(&self, name: &str) -> bool {
502 let values = self.values(name, "shape");
503 values.len() == 5
504 && [
505 "straight",
506 "inner_left",
507 "inner_right",
508 "outer_left",
509 "outer_right",
510 ]
511 .iter()
512 .all(|v| values.iter().any(|has| has == v))
513 }
514
515 /// **Every** property of `name` the state omits, sorted — the `DW0737`
516 /// predicate, and a superset of [`Self::omitted_shape_carrying`].
517 ///
518 /// Vanilla's `BlockState` codec fills an omitted property from the block's
519 /// default state, so a partial state is a legal thing to write and the game
520 /// resolves it correctly. Nothing else can: a renderer, a review image, a
521 /// navigation walk or a diff has to guess, and the guesses disagree with
522 /// each other and with the server. The shape half of that (`DW0735`) drops
523 /// geometry outright and is the harder error; this is the whole class, and
524 /// it is the rule an AUTHORED program is held to — a state whose meaning
525 /// only a running server knows cannot be reviewed before it runs.
526 ///
527 /// Empty for a propertyless block, for a foreign namespace and for an id
528 /// the pin does not know (the unknown-block diagnostics own that case).
529 pub fn omitted_properties(
530 &self,
531 name: &str,
532 properties: &BTreeMap<String, String>,
533 ) -> Vec<String> {
534 let namespaced = namespace(name);
535 let Some(known) = self.blocks.get(namespaced.as_ref()) else {
536 return Vec::new();
537 };
538 known
539 .keys()
540 .filter(|p| !properties.contains_key(*p))
541 .cloned()
542 .collect()
543 }
544
545 /// The shape-carrying properties a state omits, sorted. Empty when the
546 /// state is complete, when the block has none, and when the id is foreign
547 /// or unknown at the pin.
548 pub fn omitted_shape_carrying(
549 &self,
550 name: &str,
551 properties: &BTreeMap<String, String>,
552 ) -> Vec<String> {
553 self.shape_carrying(name)
554 .iter()
555 .filter(|p| !properties.contains_key(*p))
556 .cloned()
557 .collect()
558 }
559
560 /// The first property of a state that lands wrong when the state is written
561 /// under the frame `local_to_world` / `reflected` without being rewritten —
562 /// the `DW0736` predicate.
563 ///
564 /// A frame has two halves and the check needs both. `local_to_world[i]` is
565 /// the world axis index (0 = X, 1 = Y, 2 = Z) that a scope's local axis `i`
566 /// names; `reflected[i]` says local axis `i` runs *backwards* along it. A
567 /// grammar frame permutes and reflects the *geometry* a rule describes and
568 /// never touches block-state properties
569 /// (`crates/delvec/src/grammar/orient.rs`), so a literal `facing`/`axis`/
570 /// connection property is correct only if the frame fixes the direction it
571 /// names. The check transforms the state through the frame — mapping
572 /// direction-valued properties, axis-valued properties, direction-*named*
573 /// connection flags and two-direction `orientation` values, all derived
574 /// from the registry's own value vocabulary — and reports the first
575 /// property whose transform differs from its literal, in key order
576 /// (deterministic, ADR-0006).
577 ///
578 /// A reflection is a *sign* on the axis, so it is exactly what the existing
579 /// `(axis, sign)` vocabulary already speaks: local `north` under a
580 /// reflected local Z is world `south`. An axis-valued property carries no
581 /// sign and is therefore untouched by a reflection — `axis=x` means the
582 /// same pillar either way. `rotation` (the 16-step yaw of signs, skulls and
583 /// banners), `hinge` and a non-`straight` stair `shape` are facing-relative
584 /// or sub-cardinal and cannot be transformed by axis vocabulary; they are
585 /// the minimal, documented residue and count as mismatched whenever the
586 /// frame moves **or reflects** a horizontal axis. A reflection is the case
587 /// that matters most for `hinge` and a corner `shape`: those are chiral,
588 /// and a mirror is what a rotation cannot reproduce.
589 ///
590 /// `None` for the identity frame (nothing moves and nothing reflects), for
591 /// a foreign namespace, and for an id or property the pin does not know
592 /// (the unknown-state diagnostics own those).
593 pub fn oriented_mismatch(
594 &self,
595 name: &str,
596 properties: &BTreeMap<String, String>,
597 local_to_world: [usize; 3],
598 reflected: [bool; 3],
599 ) -> Option<String> {
600 if local_to_world == [0, 1, 2] && reflected == [false; 3] {
601 return None;
602 }
603 let namespaced = namespace(name);
604 let known = self.blocks.get(namespaced.as_ref())?;
605
606 for (k, v) in properties {
607 if known.get(k).is_none() {
608 continue; // unknown property: `validate` owns it
609 }
610 match property_image(known, k, v, local_to_world, reflected) {
611 // The frame provably leaves this property alone.
612 PropertyImage::Fixed => {}
613 // It moves. A moved KEY is still satisfied when the state
614 // already gives the destination key the same value (a
615 // symmetric run of bars): the frame maps the state onto
616 // itself.
617 PropertyImage::Moved { key, value } => {
618 if key != *k {
619 if properties.get(&key) != Some(v) {
620 return Some(format!("{k}={v}"));
621 }
622 } else if value != *v {
623 return Some(format!("{k}={v}"));
624 }
625 }
626 PropertyImage::Undetermined => return Some(format!("{k}={v}")),
627 }
628 }
629 None
630 }
631
632 /// **The same question asked of a set of frames instead of one**: which
633 /// properties of this state ANY of `frames` would land wrong.
634 ///
635 /// [`Self::oriented_mismatch`] answers "does this state survive THIS
636 /// frame", and its first act is to return `None` for the identity. That is
637 /// correct, and it is also why a `None` from it is two different facts
638 /// wearing one answer: *judged against a frame and found sound*, or *never
639 /// judged at all*. Telling those apart is not a question about the state
640 /// alone either — it is a question about which frames the scope could have
641 /// stood in, which is why the caller supplies them
642 /// (`delvec::grammar::orient::FrameSet`).
643 ///
644 /// The answer is the union of what `oriented_mismatch` reports over
645 /// `frames`, so the two can never disagree about what frame-sensitivity
646 /// means. That matters more than the cost of asking 48 times: a hand-kept
647 /// list of sensitive property NAMES would call a symmetric run of bars
648 /// sensitive (its `east` moves to `south`, and the state already gives
649 /// `south` the same value, so no frame lands it wrong), would call an
650 /// `axis=y` pillar sensitive under a request that pins the vertical, and
651 /// would go stale the moment the pin adds a block. Asking the judge is what
652 /// keeps the two definitions one definition.
653 ///
654 /// Each frame is `(local_to_world, reflected)`, exactly as
655 /// `oriented_mismatch` reads them. Sorted `key=value`, deterministic
656 /// (ADR-0006). Empty for a state none of these frames disturbs — a plain
657 /// `minecraft:stone`, a `waterlogged` slab, a symmetric pane — for a foreign
658 /// namespace and for an id the pin does not know.
659 pub fn frame_sensitive(
660 &self,
661 name: &str,
662 properties: &BTreeMap<String, String>,
663 frames: impl IntoIterator<Item = ([usize; 3], [bool; 3])>,
664 ) -> Vec<String> {
665 let mut found: std::collections::BTreeSet<String> = Default::default();
666 for (perm, reflected) in frames {
667 if let Some(hit) = self.oriented_mismatch(name, properties, perm, reflected) {
668 found.insert(hit);
669 }
670 }
671 found.into_iter().collect()
672 }
673
674 /// **The same transform, applied instead of judged**: the image of
675 /// `properties` when the state was written in a scope's own axis names and
676 /// the scope's frame is `local_to_world` / `reflected`.
677 ///
678 /// [`Self::oriented_mismatch`] asks whether a state written for the world
679 /// frame survives this one; it computes the intended state to answer, and
680 /// throws it away. This returns it. Both go through one classifier, so a
681 /// property either has an image both of them agree on or has none, and
682 /// there is no state the judge calls wrong that the resolver quietly writes
683 /// anyway. That is the whole reason the classifier is a single function:
684 /// a judge and a rewriter derived from two tables would disagree exactly
685 /// where it matters, and the disagreement would be invisible.
686 ///
687 /// `Err` names the first property (as `key=value`, in key order) whose
688 /// image the pinned vocabulary does not determine — a yaw or a chirality
689 /// under any frame but a pure turn about the vertical, a `top`/`bottom`
690 /// half under a frame that moves or reverses the vertical, a direction
691 /// whose image is not a legal value of the block, a rail's
692 /// direction-composed shape. **A refusal, never a best guess**: a
693 /// local-frame state that cannot be resolved has no correct block to write.
694 ///
695 /// Unchanged for the identity frame, for a foreign namespace and for an id
696 /// the pin does not know (the unknown-block diagnostics own that).
697 pub fn permuted_properties(
698 &self,
699 name: &str,
700 properties: &BTreeMap<String, String>,
701 local_to_world: [usize; 3],
702 reflected: [bool; 3],
703 ) -> Result<BTreeMap<String, String>, String> {
704 if local_to_world == [0, 1, 2] && reflected == [false; 3] {
705 return Ok(properties.clone());
706 }
707 let namespaced = namespace(name);
708 let Some(known) = self.blocks.get(namespaced.as_ref()) else {
709 return Ok(properties.clone());
710 };
711 let mut out = BTreeMap::new();
712 for (k, v) in properties {
713 if known.get(k).is_none() {
714 out.insert(k.clone(), v.clone()); // `validate` owns it
715 continue;
716 }
717 match property_image(known, k, v, local_to_world, reflected) {
718 PropertyImage::Fixed => {
719 out.insert(k.clone(), v.clone());
720 }
721 PropertyImage::Moved { key, value } => {
722 out.insert(key, value);
723 }
724 PropertyImage::Undetermined => return Err(format!("{k}={v}")),
725 }
726 }
727 Ok(out)
728 }
729
730 /// Registry ids most likely to be what an unknown id meant.
731 ///
732 /// The rename that motivated this module (`chain` → `iron_chain`) is a
733 /// **qualification**, not an edit-distance neighbour: the new id is the old
734 /// one with a material word put in front of it. So candidates are ranked by
735 /// how a rename actually looks — the unknown id as a whole-word *suffix*
736 /// first (`iron_chain`), then as a prefix, then anywhere inside — and within
737 /// a rank by how few words were added, then alphabetically so the answer is
738 /// deterministic (ADR-0006). A wrong suggestion costs nothing; the error is
739 /// already fatal.
740 fn suggest(&self, name: &str) -> Vec<String> {
741 let path = name.split_once(':').map(|(_, p)| p).unwrap_or(name);
742 let words: Vec<&str> = path.split('_').collect();
743
744 let mut ranked: Vec<(u8, usize, &String)> = Vec::new();
745 for id in self.blocks.keys() {
746 let candidate = id.split_once(':').map(|(_, p)| p).unwrap_or(id);
747 if candidate == path {
748 continue;
749 }
750 let candidate_words: Vec<&str> = candidate.split('_').collect();
751 if candidate_words.len() <= words.len() {
752 continue;
753 }
754 let extra = candidate_words.len() - words.len();
755 let rank = if candidate_words.ends_with(words.as_slice()) {
756 0
757 } else if candidate_words.starts_with(words.as_slice()) {
758 1
759 } else if candidate_words
760 .windows(words.len())
761 .any(|w| w == words.as_slice())
762 {
763 2
764 } else {
765 continue;
766 };
767 ranked.push((rank, extra, id));
768 }
769 ranked.sort();
770 ranked
771 .into_iter()
772 .take(3)
773 .map(|(_, _, id)| id.clone())
774 .collect()
775 }
776}
777
778/// What a frame does to one block-state property.
779///
780/// The one classifier behind both [`BlockRegistry::oriented_mismatch`] and
781/// [`BlockRegistry::permuted_properties`]. Keeping it single is the point: a
782/// judge and a rewriter derived from different tables would disagree exactly
783/// where it matters, and the disagreement would be invisible — the judge would
784/// pass a state the rewriter mangled, or refuse one it wrote correctly.
785enum PropertyImage {
786 /// The frame provably leaves this property where it is.
787 Fixed,
788 /// It becomes this key and this value.
789 Moved {
790 /// The destination key.
791 key: String,
792 /// The destination value.
793 value: String,
794 },
795 /// The pinned vocabulary does not determine an image.
796 Undetermined,
797}
798
799/// The image of `key=value` on `known` under the frame `local_to_world` /
800/// `reflected`, which is never the identity here (both callers short-circuit
801/// it).
802///
803/// A frame has two halves. `local_to_world[i]` is the world axis a scope's
804/// local axis `i` names; `reflected[i]` says local axis `i` runs *backwards*
805/// along it. A reflection is a **sign**, which is what the `(axis, sign)`
806/// direction vocabulary already speaks, so the first four classes below carry
807/// it exactly: local `north` under a reflected local Z is world `south`. An
808/// axis carries no sign, so the reflection half cannot disturb it.
809///
810/// The classes are tried in order and each is decided from the block's **own**
811/// legal-value vocabulary rather than from a list of block ids, so a block the
812/// pin adds is classified without touching this function.
813///
814/// The last three classes are *frame-relative*: a 16-step yaw, a chirality
815/// (`left`/`right`) and a vertical position (`top`/`bottom`, `upper`/`lower`)
816/// are stated against a fixed up-axis and, for the first two, a fixed
817/// handedness. A yaw and a chirality therefore have an image only under a
818/// **pure turn about the vertical** — a frame that reflects nothing and keeps
819/// the local `Y` on the world `Y`, which is the identity (short-circuited by
820/// both callers) or the horizontal transposition `x↔z`. That transposition is
821/// itself a reflection of the horizontal plane: a yaw θ becomes 270° − θ, and
822/// left becomes right. Reflect any axis and the frame leaves that vocabulary,
823/// which is exactly the residue `DW0736` counts as mismatched whenever the
824/// frame moves *or reflects* a horizontal axis — so the answer is
825/// [`PropertyImage::Undetermined`], a refusal in the resolver and a mismatch in
826/// the judge, rather than a guess that would make the two disagree.
827///
828/// A vertical position survives any purely horizontal frame untouched, and has
829/// no image at all once the vertical moves or runs backwards: `half=top` cannot
830/// mean "the top half" of a horizontal axis, and a frame whose local `Y` counts
831/// down the world's has no `top` to name.
832fn property_image(
833 known: &BTreeMap<String, Vec<String>>,
834 key: &str,
835 value: &str,
836 local_to_world: [usize; 3],
837 reflected: [bool; 3],
838) -> PropertyImage {
839 let legal = match known.get(key) {
840 Some(l) => l,
841 None => return PropertyImage::Fixed,
842 };
843 // The frame's image of one local direction: the world axis the local one
844 // names, with the sign flipped when that axis runs backwards.
845 let image = |axis: usize, sign: i8| {
846 let sign = if reflected[axis] { -sign } else { sign };
847 axis_sign_direction(local_to_world[axis], sign)
848 };
849 // A direction-*named* property: the connection flags of fences, walls,
850 // panes and vines. The frame moves the KEY.
851 if let Some((axis, sign)) = direction_axis_sign(key) {
852 let moved = image(axis, sign);
853 if moved == key {
854 return PropertyImage::Fixed;
855 }
856 // A pane has no `up`/`down` flag: turning a horizontal connection onto
857 // the vertical has nowhere to land.
858 return match known.get(moved) {
859 Some(l) if l.iter().any(|v| v == value) => PropertyImage::Moved {
860 key: moved.to_string(),
861 value: value.to_string(),
862 },
863 _ => PropertyImage::Undetermined,
864 };
865 }
866 // A direction-valued property (`facing`, `vertical_direction`, …).
867 if !legal.is_empty() && legal.iter().all(|l| direction_axis_sign(l).is_some()) {
868 let Some((axis, sign)) = direction_axis_sign(value) else {
869 return PropertyImage::Undetermined;
870 };
871 return image_of_value(key, value, image(axis, sign), legal);
872 }
873 // An axis-valued property (`axis` of logs, pillars, chains). An axis has no
874 // sign, so only the permutation half can disturb it.
875 if !legal.is_empty() && legal.iter().all(|l| axis_index(l).is_some()) {
876 let Some(axis) = axis_index(value) else {
877 return PropertyImage::Undetermined;
878 };
879 let moved = ["x", "y", "z"][local_to_world[axis]];
880 return image_of_value(key, value, moved, legal);
881 }
882 // A two-direction value (`orientation` of jigsaws and crafters).
883 if !legal.is_empty() && legal.iter().all(|l| is_direction_pair(l)) {
884 let Some((a, b)) = value.split_once('_') else {
885 return PropertyImage::Undetermined;
886 };
887 let (Some((aa, asig)), Some((ba, bsig))) = (direction_axis_sign(a), direction_axis_sign(b))
888 else {
889 return PropertyImage::Undetermined;
890 };
891 let moved = format!("{}_{}", image(aa, asig), image(ba, bsig));
892 return image_of_value(key, value, &moved, legal);
893 }
894
895 // Everything below is stated against a fixed vertical; the first two are
896 // stated against a fixed handedness as well.
897 let turn_about_the_vertical = local_to_world[1] == 1 && reflected == [false; 3];
898 let vertical_kept = local_to_world[1] == 1 && !reflected[1];
899
900 // A 16-step yaw (signs, banners, skulls): `rotation` 0 is south and the
901 // segments run with the yaw, so a reflection sends r to (12 - r) mod 16.
902 if key == "rotation" && legal.iter().all(|l| l.parse::<u8>().is_ok()) {
903 let (true, Ok(r)) = (turn_about_the_vertical, value.parse::<u32>()) else {
904 return PropertyImage::Undetermined;
905 };
906 let moved = ((28 - r % 16) % 16).to_string();
907 return image_of_value(key, value, &moved, legal);
908 }
909 // A chirality: a door's `hinge`, a stair's `shape`, a double chest's
910 // `type`. Handedness is what a reflection swaps — but a value that names
911 // no handedness (`straight`, `single`) is its own image under EVERY frame,
912 // and that case is settled before the frame is consulted at all. Deciding
913 // it the other way round would refuse every straight stair in a mirrored
914 // body.
915 if legal
916 .iter()
917 .any(|l| l.split('_').any(|w| w == "left" || w == "right"))
918 {
919 let moved: String = value
920 .split('_')
921 .map(|w| match w {
922 "left" => "right",
923 "right" => "left",
924 other => other,
925 })
926 .collect::<Vec<_>>()
927 .join("_");
928 if moved == value {
929 return PropertyImage::Fixed;
930 }
931 if !turn_about_the_vertical {
932 return PropertyImage::Undetermined;
933 }
934 return image_of_value(key, value, &moved, legal);
935 }
936 // A vertical position: a slab's `type`, a stair's or a door's `half`. A
937 // `double` slab has no vertical half to lose, so it too is settled before
938 // the frame is consulted.
939 if !legal.is_empty()
940 && legal
941 .iter()
942 .all(|l| matches!(l.as_str(), "top" | "bottom" | "double" | "upper" | "lower"))
943 {
944 return if value == "double" || vertical_kept {
945 PropertyImage::Fixed
946 } else {
947 PropertyImage::Undetermined
948 };
949 }
950 // A value that spells a direction or an axis inside a compound word — a
951 // rail's `shape=ascending_north`, and anything the pin adds in that shape.
952 // The vocabulary says it carries a direction and does not say how to map
953 // it, which is exactly the case to refuse rather than to pass through.
954 if legal.iter().any(|l| {
955 l.split('_')
956 .any(|w| direction_axis_sign(w).is_some() || axis_index(w).is_some())
957 }) {
958 return PropertyImage::Undetermined;
959 }
960 PropertyImage::Fixed
961}
962
963/// A moved value, or `Fixed` when the frame sent it to itself, or
964/// `Undetermined` when the destination is not a legal value of the property.
965fn image_of_value(key: &str, value: &str, moved: &str, legal: &[String]) -> PropertyImage {
966 if moved == value {
967 PropertyImage::Fixed
968 } else if legal.iter().any(|l| l == moved) {
969 PropertyImage::Moved {
970 key: key.to_string(),
971 value: moved.to_string(),
972 }
973 } else {
974 PropertyImage::Undetermined
975 }
976}
977
978/// `name` as a namespaced id: a bare id is read as `minecraft:`-namespaced,
979/// which is how every emitter in this repo writes one.
980fn namespace(name: &str) -> std::borrow::Cow<'_, str> {
981 if name.contains(':') {
982 std::borrow::Cow::Borrowed(name)
983 } else {
984 std::borrow::Cow::Owned(format!("minecraft:{name}"))
985 }
986}
987
988/// A cardinal/vertical direction word as `(axis index, sign)`, with the vanilla
989/// convention: north = −Z, south = +Z, west = −X, east = +X, down = −Y,
990/// up = +Y.
991fn direction_axis_sign(word: &str) -> Option<(usize, i8)> {
992 match word {
993 "west" => Some((0, -1)),
994 "east" => Some((0, 1)),
995 "down" => Some((1, -1)),
996 "up" => Some((1, 1)),
997 "north" => Some((2, -1)),
998 "south" => Some((2, 1)),
999 _ => None,
1000 }
1001}
1002
1003/// The inverse of [`direction_axis_sign`].
1004fn axis_sign_direction(axis: usize, sign: i8) -> &'static str {
1005 match (axis, sign) {
1006 (0, -1) => "west",
1007 (0, 1) => "east",
1008 (1, -1) => "down",
1009 (1, 1) => "up",
1010 (2, -1) => "north",
1011 (2, 1) => "south",
1012 _ => unreachable!("axis index is always 0..3 and sign ±1"),
1013 }
1014}
1015
1016/// An axis word (`x`/`y`/`z`) as its index.
1017fn axis_index(word: &str) -> Option<usize> {
1018 match word {
1019 "x" => Some(0),
1020 "y" => Some(1),
1021 "z" => Some(2),
1022 _ => None,
1023 }
1024}
1025
1026/// True for a `<direction>_<direction>` value (jigsaw/crafter `orientation`).
1027fn is_direction_pair(value: &str) -> bool {
1028 value
1029 .split_once('_')
1030 .is_some_and(|(a, b)| direction_axis_sign(a).is_some() && direction_axis_sign(b).is_some())
1031}
1032
1033/// Split `name[k=v,k=v]` into its id and its properties.
1034///
1035/// Tolerant on purpose: this is a *validator's* front door, so a malformed
1036/// state must reach the registry and be reported as an unknown block rather
1037/// than be rejected by a parser with a different vocabulary.
1038fn parse_state(state: &str) -> (&str, BTreeMap<String, String>) {
1039 let Some(open) = state.find('[') else {
1040 return (state.trim(), BTreeMap::new());
1041 };
1042 let name = state[..open].trim();
1043 let inner = state[open + 1..].trim_end().trim_end_matches(']');
1044 let mut properties = BTreeMap::new();
1045 for pair in inner.split(',') {
1046 if let Some((k, v)) = pair.split_once('=') {
1047 properties.insert(k.trim().to_string(), v.trim().to_string());
1048 }
1049 }
1050 (name, properties)
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056
1057 fn props(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
1058 pairs
1059 .iter()
1060 .map(|(k, v)| (k.to_string(), v.to_string()))
1061 .collect()
1062 }
1063
1064 /// The default-state table is only useful if it covers the same blocks the
1065 /// property registry does and agrees with it on every value. A binding count
1066 /// is asserted, because a table that failed to cover anything would make
1067 /// every completion a silent no-op.
1068 #[test]
1069 fn every_block_has_a_legal_default_state() {
1070 let reg = BlockRegistry::v1_21_11();
1071 let mut with_properties = 0usize;
1072 for (name, properties) in ®.blocks {
1073 let default = reg
1074 .default_state(name)
1075 .unwrap_or_else(|| panic!("{name} has no default state"));
1076 assert_eq!(
1077 default.keys().collect::<Vec<_>>(),
1078 properties.keys().collect::<Vec<_>>(),
1079 "{name}: the default state and the property list name different properties"
1080 );
1081 if !properties.is_empty() {
1082 with_properties += 1;
1083 }
1084 for (property, value) in default {
1085 assert!(
1086 properties[property].contains(value),
1087 "{name}[{property}={value}] is not one of that property's legal values"
1088 );
1089 }
1090 }
1091 assert_eq!(reg.defaults.len(), reg.blocks.len());
1092 assert_eq!(
1093 with_properties, 777,
1094 "1.21.11 has 777 blocks with at least one property"
1095 );
1096 }
1097
1098 /// The two completion questions live on one registry and must not disagree.
1099 /// `omitted_shape_carrying` names the properties that change the MODEL;
1100 /// `unwritten` names every property with the value the game fills it with.
1101 /// The first is a subset of the second on every block at the pin — a
1102 /// shape-carrying property the defaults table has no value for would let
1103 /// `DW0735` name a property `DW0791` cannot say anything about, and the two
1104 /// tables come from different sources (the client jar's blockstate
1105 /// definitions, and Mojang's generated block report), so nothing but this
1106 /// ties them.
1107 #[test]
1108 fn every_shape_carrying_property_has_a_default_to_complete_it() {
1109 let reg = BlockRegistry::v1_21_11();
1110 let mut bound = 0usize;
1111 for name in reg.shape.keys() {
1112 let omitted = reg.omitted_shape_carrying(name, &BTreeMap::new());
1113 let unwritten = reg.unwritten(name, &BTreeMap::new());
1114 assert!(
1115 !omitted.is_empty(),
1116 "{name} is in the shape table with no properties"
1117 );
1118 for property in &omitted {
1119 assert!(
1120 unwritten.contains_key(property),
1121 "{name}[{property}] carries shape but the default-state table cannot complete it"
1122 );
1123 bound += 1;
1124 }
1125 }
1126 assert_eq!(
1127 reg.shape.len(),
1128 95,
1129 "1.21.11 has 95 blocks whose model is assembled from parts"
1130 );
1131 assert!(
1132 bound >= 95,
1133 "only {bound} property pairs examined — the scan has come unbound"
1134 );
1135 }
1136
1137 /// The instance that makes this table worth vendoring: a bare
1138 /// `cobblestone_wall` is a POST, and the guess a reader would otherwise make
1139 /// — the first legal value of each property — is a different block.
1140 #[test]
1141 fn a_bare_wall_completes_to_a_post_not_to_the_first_legal_value() {
1142 let reg = BlockRegistry::v1_21_11();
1143 let unwritten = reg.unwritten("minecraft:cobblestone_wall", &BTreeMap::new());
1144 assert_eq!(unwritten["up"], "true");
1145 assert_eq!(unwritten["east"], "none");
1146 // The alphabetically-first legal value disagrees on both.
1147 let properties = reg.properties("minecraft:cobblestone_wall").unwrap();
1148 assert_eq!(properties["up"].first().unwrap(), "false");
1149 assert_eq!(properties["east"].first().unwrap(), "low");
1150 // A property the palette DID write is never reported as unwritten.
1151 let written = props(&[("up", "false")]);
1152 assert!(
1153 !reg.unwritten("minecraft:cobblestone_wall", &written)
1154 .contains_key("up")
1155 );
1156 // A block with no properties is complete the moment it is named.
1157 assert!(
1158 reg.unwritten("minecraft:stone", &BTreeMap::new())
1159 .is_empty()
1160 );
1161 }
1162
1163 #[test]
1164 fn the_registry_binds_to_the_whole_pinned_version() {
1165 let reg = BlockRegistry::v1_21_11();
1166 assert_eq!(reg.len(), 1166, "1.21.11 has 1166 blocks");
1167 for id in [
1168 "minecraft:air",
1169 "minecraft:stone",
1170 "minecraft:water",
1171 "minecraft:oak_stairs",
1172 ] {
1173 assert!(reg.has(id), "{id} missing from the pinned registry");
1174 }
1175 }
1176
1177 /// The finding this module exists for: 1.21.11 renamed `chain`, and eight
1178 /// cells of the old id shipped inside `tk-bell-tower.nbt`.
1179 #[test]
1180 fn the_renamed_chain_is_caught_and_the_rename_is_suggested() {
1181 let reg = BlockRegistry::v1_21_11();
1182 assert!(!reg.has("minecraft:chain"));
1183 assert!(reg.has("minecraft:iron_chain"));
1184 let err = reg
1185 .validate(
1186 "minecraft:chain",
1187 &props(&[("axis", "y"), ("waterlogged", "false")]),
1188 )
1189 .unwrap_err();
1190 let BlockError::UnknownBlock { suggestions, .. } = &err else {
1191 panic!("expected an unknown-block refusal, got {err}");
1192 };
1193 assert!(
1194 suggestions.contains(&"minecraft:iron_chain".to_string()),
1195 "the rename was not suggested: {suggestions:?}"
1196 );
1197 assert!(err.to_string().contains("loads an unknown block as AIR"));
1198 }
1199
1200 #[test]
1201 fn properties_and_values_are_checked_too() {
1202 let reg = BlockRegistry::v1_21_11();
1203 assert!(
1204 reg.validate(
1205 "minecraft:oak_stairs",
1206 &props(&[("facing", "east"), ("half", "top")])
1207 )
1208 .is_ok()
1209 );
1210 assert_eq!(
1211 reg.validate("minecraft:oak_stairs", &props(&[("orientation", "east")]))
1212 .unwrap_err(),
1213 BlockError::UnknownProperty {
1214 name: "minecraft:oak_stairs".into(),
1215 property: "orientation".into(),
1216 known: vec![
1217 "facing".into(),
1218 "half".into(),
1219 "shape".into(),
1220 "waterlogged".into()
1221 ],
1222 }
1223 );
1224 let err = reg
1225 .validate("minecraft:oak_stairs", &props(&[("facing", "up")]))
1226 .unwrap_err();
1227 assert!(matches!(err, BlockError::BadValue { .. }), "{err}");
1228 assert!(err.to_string().contains("north"), "{err}");
1229 }
1230
1231 #[test]
1232 fn a_state_string_validates_the_same_way_as_its_parts() {
1233 let reg = BlockRegistry::v1_21_11();
1234 assert!(reg.validate_state_string("minecraft:stone").is_ok());
1235 assert!(reg.validate_state_string("stone").is_ok());
1236 assert!(
1237 reg.validate_state_string("minecraft:iron_chain[axis=y,waterlogged=false]")
1238 .is_ok()
1239 );
1240 assert!(
1241 reg.validate_state_string("minecraft:chain[axis=y,waterlogged=false]")
1242 .is_err()
1243 );
1244 }
1245
1246 /// A datapack's own block is not this registry's business, and refusing it
1247 /// would make the check a wall instead of a spell-checker.
1248 #[test]
1249 fn a_foreign_namespace_is_left_alone() {
1250 let reg = BlockRegistry::v1_21_11();
1251 assert!(
1252 reg.validate("delvewright:nonesuch", &BTreeMap::new())
1253 .is_ok()
1254 );
1255 }
1256
1257 /// The DataVersion-aware rule: `minecraft:chain` at the pin is the
1258 /// tk-bell-tower defect (loads as air, error); the same id at DataVersion
1259 /// 2975 is `hero-temple-ruin-arch.nbt`, which the game datafixes on load
1260 /// (`chain` → `iron_chain`) — a warning, never a refusal.
1261 #[test]
1262 fn judge_at_separates_the_bell_tower_defect_from_the_ruin_arch_false_positive() {
1263 let reg = BlockRegistry::v1_21_11();
1264 let chain = props(&[("axis", "y")]);
1265 assert!(matches!(
1266 reg.judge_at("minecraft:chain", &chain, PIN_DATA_VERSION),
1267 StateJudgement::InvalidAtPin(_)
1268 ));
1269 assert!(matches!(
1270 reg.judge_at("minecraft:chain", &chain, 2975),
1271 StateJudgement::PrePin(_)
1272 ));
1273 // A post-pin DataVersion gets no fixes from the pinned game either.
1274 assert!(matches!(
1275 reg.judge_at("minecraft:chain", &chain, PIN_DATA_VERSION + 1),
1276 StateJudgement::InvalidAtPin(_)
1277 ));
1278 assert_eq!(
1279 reg.judge_at("minecraft:iron_chain", &chain, PIN_DATA_VERSION),
1280 StateJudgement::Valid
1281 );
1282 }
1283
1284 /// **Which id the game will hold** — the question `judge_at` deliberately
1285 /// does not answer and every name-judging check downstream of it needs.
1286 #[test]
1287 fn loaded_id_at_resolves_a_pre_pin_rename_and_refuses_to_guess_past_its_bound() {
1288 let reg = BlockRegistry::v1_21_11();
1289 // hero-temple-ruin-arch.nbt: below the old id's last DataVersion, so
1290 // the fixer certainly runs.
1291 assert_eq!(
1292 reg.loaded_id_at("minecraft:chain", 2975),
1293 LoadedId::Renamed {
1294 to: "minecraft:iron_chain",
1295 valid_through: 4440,
1296 }
1297 );
1298 assert_eq!(
1299 reg.loaded_id_at("chain", 4440),
1300 LoadedId::Renamed {
1301 to: "minecraft:iron_chain",
1302 valid_through: 4440,
1303 }
1304 );
1305 // Above the bound the schedule is unknown to this repo, so the table
1306 // says nothing rather than guessing — which is what makes a caller
1307 // REFUSE instead of pass.
1308 assert_eq!(
1309 reg.loaded_id_at("minecraft:chain", 4441),
1310 LoadedId::Unresolved
1311 );
1312 assert_eq!(
1313 reg.loaded_id_at("minecraft:chain", PIN_DATA_VERSION),
1314 LoadedId::Unresolved
1315 );
1316 // An id nothing renamed, at any DataVersion.
1317 assert_eq!(
1318 reg.loaded_id_at("minecraft:nonesuch", 2975),
1319 LoadedId::Unresolved
1320 );
1321 // Ids the pin has, and a datapack's own block, are held as written.
1322 assert_eq!(
1323 reg.loaded_id_at("minecraft:iron_chain", 2975),
1324 LoadedId::AsWritten
1325 );
1326 assert_eq!(
1327 reg.loaded_id_at("minecraft:stone", PIN_DATA_VERSION),
1328 LoadedId::AsWritten
1329 );
1330 assert_eq!(
1331 reg.loaded_id_at("delvewright:nonesuch", 2975),
1332 LoadedId::AsWritten
1333 );
1334 }
1335
1336 /// The vendored rename table's own contract, asserted against the registry
1337 /// beside it rather than trusted: a row whose `from` still exists is not a
1338 /// rename, a row whose `to` does not exist points at nothing, and a bound
1339 /// at or above the pin would resolve a file the fixer never touches.
1340 ///
1341 /// The binding count is asserted too — an empty table would satisfy every
1342 /// clause above by examining nothing, which is the shape CLAUDE.md names.
1343 #[test]
1344 fn every_vendored_rename_leaves_the_registry_and_lands_inside_it() {
1345 let reg = BlockRegistry::v1_21_11();
1346 assert!(
1347 !reg.renames().is_empty(),
1348 "binding count is zero: the rename table has no rows, so nothing below examined anything"
1349 );
1350 for (from, rename) in reg.renames() {
1351 assert!(!reg.has(from), "{from} is still a block at the pin");
1352 assert!(
1353 reg.has(&rename.to),
1354 "{from} -> {} is not a block at the pin",
1355 rename.to
1356 );
1357 assert!(
1358 rename.valid_through < PIN_DATA_VERSION,
1359 "{from}: valid_through {} is not below the pin",
1360 rename.valid_through
1361 );
1362 }
1363 }
1364
1365 /// The shape class: connection properties of multipart-assembled blocks
1366 /// are shape-carrying; variant-picking properties (`waterlogged`, `snowy`,
1367 /// `powered`, a lantern's `hanging`, a chain's `axis`) are not.
1368 #[test]
1369 fn shape_carrying_is_the_multipart_class_not_a_hand_list() {
1370 let reg = BlockRegistry::v1_21_11();
1371 assert_eq!(
1372 reg.shape_carrying("minecraft:cobblestone_wall"),
1373 ["east", "north", "south", "up", "west"]
1374 );
1375 assert_eq!(
1376 reg.shape_carrying("iron_bars"),
1377 ["east", "north", "south", "west"]
1378 );
1379 assert!(!reg.shape_carrying("minecraft:vine").is_empty());
1380 assert!(!reg.shape_carrying("minecraft:glow_lichen").is_empty());
1381 // Variant-picking properties: complete model, benign omission.
1382 assert!(reg.shape_carrying("minecraft:lantern").is_empty());
1383 assert!(reg.shape_carrying("minecraft:grass_block").is_empty());
1384 assert!(reg.shape_carrying("minecraft:spruce_button").is_empty());
1385 assert!(reg.shape_carrying("minecraft:oak_stairs").is_empty());
1386 assert!(reg.shape_carrying("minecraft:deepslate").is_empty());
1387 assert!(reg.shape_carrying("minecraft:iron_chain").is_empty());
1388 // Foreign/unknown ids belong to other diagnostics.
1389 assert!(reg.shape_carrying("delvewright:nonesuch").is_empty());
1390 assert!(reg.shape_carrying("minecraft:chain").is_empty());
1391 // Binding: the table covers the multipart blocks of the pin.
1392 assert_eq!(reg.shape.len(), 95, "95 blocks assemble their model");
1393 }
1394
1395 #[test]
1396 fn omitted_shape_carrying_reports_exactly_the_missing_ones() {
1397 let reg = BlockRegistry::v1_21_11();
1398 assert_eq!(
1399 reg.omitted_shape_carrying("minecraft:iron_bars", &BTreeMap::new()),
1400 ["east", "north", "south", "west"]
1401 );
1402 assert_eq!(
1403 reg.omitted_shape_carrying(
1404 "minecraft:vine",
1405 &props(&[("north", "true"), ("waterlogged", "false")])
1406 ),
1407 ["east", "south", "up", "west"]
1408 );
1409 assert!(
1410 reg.omitted_shape_carrying(
1411 "minecraft:iron_bars",
1412 &props(&[
1413 ("east", "false"),
1414 ("north", "true"),
1415 ("south", "true"),
1416 ("west", "false")
1417 ])
1418 )
1419 .is_empty()
1420 );
1421 assert!(
1422 reg.omitted_shape_carrying("minecraft:lantern", &BTreeMap::new())
1423 .is_empty()
1424 );
1425 }
1426
1427 /// Nothing reflected — the frame's second half at rest.
1428 const STRAIGHT: [bool; 3] = [false, false, false];
1429
1430 /// The `DW0736` predicate: a state is safe under a frame exactly when
1431 /// transforming it through the frame changes nothing.
1432 #[test]
1433 fn oriented_mismatch_transforms_through_the_registry_vocabulary() {
1434 let reg = BlockRegistry::v1_21_11();
1435 let keep = [0, 1, 2];
1436 let swap_xz = [2, 1, 0]; // local X is world Z, local Z is world X
1437 let move_y = [0, 2, 1]; // local Y is world Z
1438
1439 // Identity: nothing can land wrong.
1440 assert_eq!(
1441 reg.oriented_mismatch(
1442 "minecraft:oak_stairs",
1443 &props(&[("facing", "north")]),
1444 keep,
1445 STRAIGHT
1446 ),
1447 None
1448 );
1449 // A horizontal facing under a horizontal swap is the defect.
1450 assert_eq!(
1451 reg.oriented_mismatch(
1452 "minecraft:oak_stairs",
1453 &props(&[("facing", "north")]),
1454 swap_xz,
1455 STRAIGHT
1456 ),
1457 Some("facing=north".to_string())
1458 );
1459 // A vertical facing survives a horizontal swap but not a moved Y.
1460 assert_eq!(
1461 reg.oriented_mismatch(
1462 "minecraft:barrel",
1463 &props(&[("facing", "up")]),
1464 swap_xz,
1465 STRAIGHT
1466 ),
1467 None
1468 );
1469 assert_eq!(
1470 reg.oriented_mismatch(
1471 "minecraft:barrel",
1472 &props(&[("facing", "up")]),
1473 move_y,
1474 STRAIGHT
1475 ),
1476 Some("facing=up".to_string())
1477 );
1478 // `axis=y` is invariant under the swap; `axis=x` is not.
1479 assert_eq!(
1480 reg.oriented_mismatch(
1481 "minecraft:spruce_log",
1482 &props(&[("axis", "y")]),
1483 swap_xz,
1484 STRAIGHT
1485 ),
1486 None
1487 );
1488 assert_eq!(
1489 reg.oriented_mismatch(
1490 "minecraft:spruce_log",
1491 &props(&[("axis", "x")]),
1492 swap_xz,
1493 STRAIGHT
1494 ),
1495 Some("axis=x".to_string())
1496 );
1497 // Connection flags: an asymmetric run turns; a symmetric one does not.
1498 assert_eq!(
1499 reg.oriented_mismatch(
1500 "minecraft:iron_bars",
1501 &props(&[
1502 ("east", "false"),
1503 ("north", "true"),
1504 ("south", "true"),
1505 ("west", "false")
1506 ]),
1507 swap_xz,
1508 STRAIGHT
1509 ),
1510 Some("east=false".to_string())
1511 );
1512 assert_eq!(
1513 reg.oriented_mismatch(
1514 "minecraft:iron_bars",
1515 &props(&[
1516 ("east", "true"),
1517 ("north", "true"),
1518 ("south", "true"),
1519 ("west", "true")
1520 ]),
1521 swap_xz,
1522 STRAIGHT
1523 ),
1524 None
1525 );
1526 // The documented residue: a 16-step yaw cannot be transformed by axis
1527 // vocabulary, so it is mismatched whenever a horizontal axis moves.
1528 assert_eq!(
1529 reg.oriented_mismatch(
1530 "minecraft:skeleton_skull",
1531 &props(&[("rotation", "8")]),
1532 swap_xz,
1533 STRAIGHT
1534 ),
1535 Some("rotation=8".to_string())
1536 );
1537 assert_eq!(
1538 reg.oriented_mismatch(
1539 "minecraft:skeleton_skull",
1540 &props(&[("rotation", "8")]),
1541 move_y,
1542 STRAIGHT
1543 ),
1544 Some("rotation=8".to_string()),
1545 "a moved Z scrambles a yaw too — the residue is conservative on purpose"
1546 );
1547 // Yaw-invariant properties never mismatch.
1548 assert_eq!(
1549 reg.oriented_mismatch(
1550 "minecraft:oak_slab",
1551 &props(&[("type", "top"), ("waterlogged", "false")]),
1552 swap_xz,
1553 STRAIGHT
1554 ),
1555 None
1556 );
1557 }
1558
1559 /// The reflection half of the frame. A mirror is not a permutation — no
1560 /// rotation reproduces it — so a predicate that reads only the permutation
1561 /// answers `None` for every mirrored scope, which is the answer that says
1562 /// "safe".
1563 #[test]
1564 fn oriented_mismatch_reads_the_reflection_half_of_the_frame() {
1565 let reg = BlockRegistry::v1_21_11();
1566 let keep = [0, 1, 2];
1567 let flip_z = [false, false, true];
1568 let flip_x = [true, false, false];
1569 let flip_y = [false, true, false];
1570
1571 // A reflected identity frame is NOT the identity frame: local north
1572 // now runs the other way, so a literal `north` lands south.
1573 assert_eq!(
1574 reg.oriented_mismatch(
1575 "minecraft:oak_stairs",
1576 &props(&[("facing", "north")]),
1577 keep,
1578 flip_z
1579 ),
1580 Some("facing=north".to_string())
1581 );
1582 // ...and the axis the mirror does not touch is untouched: the mirror
1583 // image of a north-facing stair across the east-west axis still faces
1584 // north. This is the assertion that keeps the check from degenerating
1585 // into "any mirror is wrong".
1586 assert_eq!(
1587 reg.oriented_mismatch(
1588 "minecraft:oak_stairs",
1589 &props(&[("facing", "north")]),
1590 keep,
1591 flip_x
1592 ),
1593 None
1594 );
1595 assert_eq!(
1596 reg.oriented_mismatch(
1597 "minecraft:oak_stairs",
1598 &props(&[("facing", "east")]),
1599 keep,
1600 flip_x
1601 ),
1602 Some("facing=east".to_string())
1603 );
1604 // A vertical reflection is the one that moves `up`.
1605 assert_eq!(
1606 reg.oriented_mismatch(
1607 "minecraft:barrel",
1608 &props(&[("facing", "up")]),
1609 keep,
1610 flip_y
1611 ),
1612 Some("facing=up".to_string())
1613 );
1614 assert_eq!(
1615 reg.oriented_mismatch(
1616 "minecraft:barrel",
1617 &props(&[("facing", "up")]),
1618 keep,
1619 flip_x
1620 ),
1621 None
1622 );
1623 // An axis carries no sign, so a reflection cannot disturb it: a pillar
1624 // reflected along its own axis is the same pillar.
1625 assert_eq!(
1626 reg.oriented_mismatch(
1627 "minecraft:spruce_log",
1628 &props(&[("axis", "x")]),
1629 keep,
1630 flip_x
1631 ),
1632 None
1633 );
1634 // Connection flags: the mirror image of a run that ends at the north
1635 // is a run that ends at the south.
1636 assert_eq!(
1637 reg.oriented_mismatch(
1638 "minecraft:iron_bars",
1639 &props(&[
1640 ("east", "true"),
1641 ("north", "false"),
1642 ("south", "true"),
1643 ("west", "true")
1644 ]),
1645 keep,
1646 flip_z
1647 ),
1648 Some("north=false".to_string())
1649 );
1650 // ...and a run symmetric about the mirror is not disturbed by it.
1651 assert_eq!(
1652 reg.oriented_mismatch(
1653 "minecraft:iron_bars",
1654 &props(&[
1655 ("east", "true"),
1656 ("north", "false"),
1657 ("south", "false"),
1658 ("west", "true")
1659 ]),
1660 keep,
1661 flip_z
1662 ),
1663 None
1664 );
1665 // The chiral residue. A reflection is exactly what flips a door's
1666 // hinge and a stair's corner, and exactly what a permutation cannot
1667 // express — so the residue counts a reflected horizontal axis as a
1668 // move, as it counts a permuted one.
1669 assert_eq!(
1670 reg.oriented_mismatch(
1671 "minecraft:oak_door",
1672 &props(&[("hinge", "left")]),
1673 keep,
1674 flip_x
1675 ),
1676 Some("hinge=left".to_string())
1677 );
1678 assert_eq!(
1679 reg.oriented_mismatch(
1680 "minecraft:skeleton_skull",
1681 &props(&[("rotation", "8")]),
1682 keep,
1683 flip_z
1684 ),
1685 Some("rotation=8".to_string())
1686 );
1687 // A two-direction value transforms component-wise through the sign.
1688 assert_eq!(
1689 reg.oriented_mismatch(
1690 "minecraft:jigsaw",
1691 &props(&[("orientation", "north_up")]),
1692 keep,
1693 flip_z
1694 ),
1695 Some("orientation=north_up".to_string())
1696 );
1697 // Reflecting an axis nothing in the state names changes nothing —
1698 // the check is not a blanket refusal of mirrored scopes.
1699 assert_eq!(
1700 reg.oriented_mismatch(
1701 "minecraft:oak_slab",
1702 &props(&[("type", "top"), ("waterlogged", "false")]),
1703 keep,
1704 flip_x
1705 ),
1706 None
1707 );
1708 // A permutation and a reflection compose, and the composite is a
1709 // different map from either half: swapping X and Z is itself a mirror
1710 // of the horizontal plane, and adding a Z reflection turns it into a
1711 // quarter turn. Local north lands east here where the bare swap lands
1712 // it west — a different wrong answer from the same literal, which is
1713 // why the sign cannot be dropped on the way in.
1714 assert_eq!(
1715 reg.oriented_mismatch(
1716 "minecraft:oak_stairs",
1717 &props(&[("facing", "north")]),
1718 [2, 1, 0],
1719 [false, false, true]
1720 ),
1721 Some("facing=north".to_string())
1722 );
1723 // The vertical rides through both halves of a composite frame
1724 // untouched, so a composite is not a blanket refusal either.
1725 assert_eq!(
1726 reg.oriented_mismatch(
1727 "minecraft:barrel",
1728 &props(&[("facing", "up")]),
1729 [2, 1, 0],
1730 [false, false, true]
1731 ),
1732 None
1733 );
1734 }
1735
1736 /// **The judge and the rewriter are one transform, checked from both
1737 /// ends**: whatever `oriented_mismatch` calls wrong, `permuted_properties`
1738 /// rewrites, and the rewrite is what the state would have had to say.
1739 #[test]
1740 fn permuted_properties_is_the_state_the_mismatch_predicate_wanted() {
1741 let reg = BlockRegistry::v1_21_11();
1742 let swap_xz = [2, 1, 0];
1743
1744 // Connection flags move by KEY: a run along local Z becomes a run
1745 // along world X.
1746 let bars = props(&[
1747 ("east", "false"),
1748 ("north", "true"),
1749 ("south", "true"),
1750 ("waterlogged", "false"),
1751 ("west", "false"),
1752 ]);
1753 assert_eq!(
1754 reg.oriented_mismatch("minecraft:iron_bars", &bars, swap_xz, STRAIGHT),
1755 Some("east=false".to_string()),
1756 "the literal is wrong under the swap…"
1757 );
1758 assert_eq!(
1759 reg.permuted_properties("minecraft:iron_bars", &bars, swap_xz, STRAIGHT),
1760 Ok(props(&[
1761 ("east", "true"),
1762 ("north", "false"),
1763 ("south", "false"),
1764 ("waterlogged", "false"),
1765 ("west", "true"),
1766 ])),
1767 "…and this is what it had to say instead"
1768 );
1769
1770 // A facing moves by VALUE, and a vertical one does not move at all.
1771 assert_eq!(
1772 reg.permuted_properties(
1773 "minecraft:oak_stairs",
1774 &props(&[
1775 ("facing", "north"),
1776 ("half", "bottom"),
1777 ("shape", "straight"),
1778 ("waterlogged", "false"),
1779 ]),
1780 swap_xz,
1781 STRAIGHT
1782 ),
1783 Ok(props(&[
1784 ("facing", "west"),
1785 ("half", "bottom"),
1786 ("shape", "straight"),
1787 ("waterlogged", "false"),
1788 ]))
1789 );
1790 assert_eq!(
1791 reg.permuted_properties(
1792 "minecraft:barrel",
1793 &props(&[("facing", "up")]),
1794 swap_xz,
1795 STRAIGHT
1796 ),
1797 Ok(props(&[("facing", "up")]))
1798 );
1799
1800 // A 16-step yaw: the swap is a REFLECTION of the horizontal plane, so
1801 // r becomes (12 - r) mod 16. Rotation 8 is north, 4 is west — which is
1802 // where the swap sends north.
1803 assert_eq!(
1804 reg.permuted_properties(
1805 "minecraft:skeleton_skull",
1806 &props(&[("powered", "false"), ("rotation", "8")]),
1807 swap_xz,
1808 STRAIGHT
1809 ),
1810 Ok(props(&[("powered", "false"), ("rotation", "4")]))
1811 );
1812 // A yaw on the reflection's own diagonal is its own image — and the
1813 // mismatch predicate agrees, because both read the one transform.
1814 assert_eq!(
1815 reg.permuted_properties(
1816 "minecraft:skeleton_skull",
1817 &props(&[("rotation", "6")]),
1818 swap_xz,
1819 STRAIGHT
1820 ),
1821 Ok(props(&[("rotation", "6")]))
1822 );
1823 assert_eq!(
1824 reg.oriented_mismatch(
1825 "minecraft:skeleton_skull",
1826 &props(&[("rotation", "6")]),
1827 swap_xz,
1828 STRAIGHT
1829 ),
1830 None
1831 );
1832
1833 // Handedness is what a reflection swaps.
1834 assert_eq!(
1835 reg.permuted_properties(
1836 "minecraft:oak_door",
1837 &props(&[("facing", "north"), ("hinge", "left")]),
1838 swap_xz,
1839 STRAIGHT
1840 ),
1841 Ok(props(&[("facing", "west"), ("hinge", "right")]))
1842 );
1843 }
1844
1845 /// **The refusal, and what secures it.** A frame that moves the vertical
1846 /// leaves a yaw, a handedness and a `top`/`bottom` half with nothing to
1847 /// mean, and a horizontal connection with nowhere to land. The answer is
1848 /// `DW0738` — no image — and never a plausible substitute.
1849 #[test]
1850 fn a_property_with_no_image_is_refused_rather_than_guessed() {
1851 let reg = BlockRegistry::v1_21_11();
1852 let move_y = [0, 2, 1]; // local Y is world Z
1853
1854 assert_eq!(DW_LOCAL_FRAME_UNRESOLVABLE, "DW0738");
1855 assert_eq!(
1856 reg.permuted_properties(
1857 "minecraft:skeleton_skull",
1858 &props(&[("rotation", "8")]),
1859 move_y,
1860 STRAIGHT
1861 ),
1862 Err("rotation=8".to_string())
1863 );
1864 assert_eq!(
1865 reg.permuted_properties(
1866 "minecraft:oak_slab",
1867 &props(&[("type", "top")]),
1868 move_y,
1869 STRAIGHT
1870 ),
1871 Err("type=top".to_string())
1872 );
1873 // A pane has no `up` flag, so a connection turned onto the vertical
1874 // has no key to land on.
1875 assert_eq!(
1876 reg.permuted_properties(
1877 "minecraft:iron_bars",
1878 &props(&[("north", "true")]),
1879 move_y,
1880 STRAIGHT
1881 ),
1882 Err("north=true".to_string())
1883 );
1884 // A rail's shape spells its directions inside a compound word. The
1885 // vocabulary says it carries a direction and does not say how to map
1886 // it, which is the case to refuse.
1887 assert_eq!(
1888 reg.permuted_properties(
1889 "minecraft:rail",
1890 &props(&[("shape", "ascending_north")]),
1891 [2, 1, 0],
1892 STRAIGHT
1893 ),
1894 Err("shape=ascending_north".to_string())
1895 );
1896 // The identity frame moves nothing, so nothing is ever refused under
1897 // it.
1898 assert_eq!(
1899 reg.permuted_properties(
1900 "minecraft:skeleton_skull",
1901 &props(&[("rotation", "8")]),
1902 [0, 1, 2],
1903 STRAIGHT
1904 ),
1905 Ok(props(&[("rotation", "8")]))
1906 );
1907 }
1908
1909 /// **A local frame inside a MIRRORED body** — the case that exists only
1910 /// where the resolver and the reflected frame meet, and that neither the
1911 /// reflection work nor the local-frame work could have had.
1912 ///
1913 /// The trap is the short circuit. A pure reflection has the identity axis
1914 /// permutation, so a resolver keyed on the permutation alone answers "the
1915 /// identity moves nothing" and writes the state through unchanged — the
1916 /// same short circuit to "safe" that the `DW0736` judge had before it grew
1917 /// its reflection half, and here it does not merely miss a defect, it
1918 /// WRITES one.
1919 #[test]
1920 fn a_local_frame_resolves_through_the_reflection_half_too() {
1921 let reg = BlockRegistry::v1_21_11();
1922 let keep = [0, 1, 2];
1923 let flip_x = [true, false, false];
1924 let flip_z = [false, false, true];
1925
1926 // The identity permutation is NOT the identity frame once an axis runs
1927 // backwards: a bar spanning the scope's local X spans it the other way
1928 // round, so `east`/`west` swap. They carry the same value here, so the
1929 // resolved state is equal to the literal — and the interesting one is
1930 // the ASYMMETRIC run below.
1931 assert_eq!(
1932 reg.permuted_properties(
1933 "minecraft:oak_stairs",
1934 &props(&[
1935 ("facing", "east"),
1936 ("half", "bottom"),
1937 ("shape", "straight"),
1938 ("waterlogged", "false"),
1939 ]),
1940 keep,
1941 flip_x
1942 ),
1943 Ok(props(&[
1944 ("facing", "west"),
1945 ("half", "bottom"),
1946 ("shape", "straight"),
1947 ("waterlogged", "false"),
1948 ])),
1949 "a reflected local X sends the scope's east to the world's west"
1950 );
1951 // An asymmetric run of bars: the local run ends at the scope's north,
1952 // and under a reflected local Z that end is the world's south.
1953 assert_eq!(
1954 reg.permuted_properties(
1955 "minecraft:iron_bars",
1956 &props(&[
1957 ("east", "true"),
1958 ("north", "true"),
1959 ("south", "false"),
1960 ("waterlogged", "false"),
1961 ("west", "true"),
1962 ]),
1963 keep,
1964 flip_z
1965 ),
1966 Ok(props(&[
1967 ("east", "true"),
1968 ("north", "false"),
1969 ("south", "true"),
1970 ("waterlogged", "false"),
1971 ("west", "true"),
1972 ]))
1973 );
1974 // The axis half is sign-free, so a pillar is the same pillar in a
1975 // mirrored body — a reflection is not a blanket rewrite.
1976 assert_eq!(
1977 reg.permuted_properties(
1978 "minecraft:spruce_log",
1979 &props(&[("axis", "x")]),
1980 keep,
1981 flip_x
1982 ),
1983 Ok(props(&[("axis", "x")]))
1984 );
1985 // And a vertical facing rides a horizontal reflection untouched.
1986 assert_eq!(
1987 reg.permuted_properties(
1988 "minecraft:barrel",
1989 &props(&[("facing", "up")]),
1990 keep,
1991 flip_x
1992 ),
1993 Ok(props(&[("facing", "up")]))
1994 );
1995
1996 // A frame that both reflects AND permutes composes the two halves:
1997 // local north is world east here, where the bare swap would send it
1998 // west.
1999 assert_eq!(
2000 reg.permuted_properties(
2001 "minecraft:oak_stairs",
2002 &props(&[
2003 ("facing", "north"),
2004 ("half", "bottom"),
2005 ("shape", "straight"),
2006 ("waterlogged", "false"),
2007 ]),
2008 [2, 1, 0],
2009 flip_z
2010 ),
2011 Ok(props(&[
2012 ("facing", "east"),
2013 ("half", "bottom"),
2014 ("shape", "straight"),
2015 ("waterlogged", "false"),
2016 ]))
2017 );
2018 }
2019
2020 /// The yaw and the handedness are the residue, and a reflected frame is
2021 /// **outside** the vocabulary that determines them — so the resolver
2022 /// refuses rather than writing a plausible skull, and refuses exactly where
2023 /// the judge calls the same state wrong.
2024 ///
2025 /// One verdict read from two ends is the invariant that makes the refusal
2026 /// safe: were the resolver to guess here, it would write states the
2027 /// `DW0736` gate reports as mismatched, and the build would be red about a
2028 /// block the build itself had chosen.
2029 #[test]
2030 fn the_frame_relative_residue_refuses_under_a_reflection_and_the_judge_agrees() {
2031 let reg = BlockRegistry::v1_21_11();
2032 let keep = [0, 1, 2];
2033 let flip_x = [true, false, false];
2034 let swap_xz = [2, 1, 0];
2035
2036 for (perm, refl, state, prop) in [
2037 (keep, flip_x, "minecraft:skeleton_skull", "rotation=8"),
2038 (swap_xz, flip_x, "minecraft:skeleton_skull", "rotation=8"),
2039 (keep, flip_x, "minecraft:oak_door", "hinge=left"),
2040 (swap_xz, flip_x, "minecraft:oak_door", "hinge=left"),
2041 ] {
2042 let (k, v) = prop.split_once('=').unwrap();
2043 let p = props(&[(k, v)]);
2044 assert_eq!(
2045 reg.permuted_properties(state, &p, perm, refl),
2046 Err(prop.to_string()),
2047 "{state} {prop} under {perm:?}/{refl:?} must be refused, not guessed"
2048 );
2049 assert_eq!(
2050 reg.oriented_mismatch(state, &p, perm, refl),
2051 Some(prop.to_string()),
2052 "…and the judge must call the same state wrong"
2053 );
2054 }
2055
2056 // A vertical position has no image once the vertical itself runs
2057 // backwards, and it is untouched by a horizontal reflection.
2058 assert_eq!(
2059 reg.permuted_properties(
2060 "minecraft:oak_slab",
2061 &props(&[("type", "top")]),
2062 keep,
2063 [false, true, false]
2064 ),
2065 Err("type=top".to_string())
2066 );
2067 assert_eq!(
2068 reg.permuted_properties(
2069 "minecraft:oak_slab",
2070 &props(&[("type", "top")]),
2071 keep,
2072 flip_x
2073 ),
2074 Ok(props(&[("type", "top")]))
2075 );
2076 }
2077
2078 /// The two entry points cannot drift apart: over a corpus of real states
2079 /// and every frame the grammar can produce, `permuted_properties` succeeds
2080 /// exactly when `oriented_mismatch` is silent, and its output is a state
2081 /// the pin accepts.
2082 ///
2083 /// Binding count is asserted, so a corpus or a frame list that quietly
2084 /// stopped being enumerated is a red rather than a green over nothing.
2085 #[test]
2086 fn the_judge_and_the_resolver_agree_over_every_frame_the_grammar_can_make() {
2087 let reg = BlockRegistry::v1_21_11();
2088 let states: [(&str, &[(&str, &str)]); 8] = [
2089 (
2090 "minecraft:oak_stairs",
2091 &[
2092 ("facing", "east"),
2093 ("half", "bottom"),
2094 ("shape", "straight"),
2095 ("waterlogged", "false"),
2096 ],
2097 ),
2098 (
2099 "minecraft:iron_bars",
2100 &[
2101 ("east", "true"),
2102 ("north", "true"),
2103 ("south", "false"),
2104 ("waterlogged", "false"),
2105 ("west", "false"),
2106 ],
2107 ),
2108 ("minecraft:spruce_log", &[("axis", "x")]),
2109 ("minecraft:barrel", &[("facing", "up"), ("open", "false")]),
2110 (
2111 "minecraft:skeleton_skull",
2112 &[("powered", "false"), ("rotation", "3")],
2113 ),
2114 (
2115 "minecraft:oak_door",
2116 &[
2117 ("facing", "north"),
2118 ("half", "lower"),
2119 ("hinge", "left"),
2120 ("open", "false"),
2121 ("powered", "false"),
2122 ],
2123 ),
2124 (
2125 "minecraft:oak_slab",
2126 &[("type", "top"), ("waterlogged", "false")],
2127 ),
2128 ("minecraft:jigsaw", &[("orientation", "north_up")]),
2129 ];
2130 let perms = [
2131 [0usize, 1, 2],
2132 [2, 1, 0],
2133 [0, 2, 1],
2134 [1, 0, 2],
2135 [1, 2, 0],
2136 [2, 0, 1],
2137 ];
2138 let mut checked = 0usize;
2139 let mut resolved = 0usize;
2140 for (name, pairs) in states {
2141 let properties = props(pairs);
2142 for perm in perms {
2143 for bits in 0..8u8 {
2144 let refl = [bits & 1 != 0, bits & 2 != 0, bits & 4 != 0];
2145 checked += 1;
2146 let judged = reg.oriented_mismatch(name, &properties, perm, refl);
2147 match reg.permuted_properties(name, &properties, perm, refl) {
2148 Ok(out) => {
2149 resolved += 1;
2150 // The resolver produced a state the pin accepts —
2151 // a rewrite that invented an illegal value would
2152 // pass every gate above this one and fail on a
2153 // server.
2154 assert!(
2155 reg.validate(name, &out).is_ok(),
2156 "{name} under {perm:?}/{refl:?} resolved to {out:?}, which \
2157 the pin does not accept"
2158 );
2159 // One transform, read from two ends: the judge is
2160 // silent exactly when the transform is the
2161 // identity on this state. Either direction failing
2162 // means a state one end calls wrong is one the
2163 // other quietly writes.
2164 assert_eq!(
2165 judged.is_none(),
2166 out == properties,
2167 "{name} under {perm:?}/{refl:?}: judge said {judged:?} while \
2168 the resolver wrote {out:?} for {properties:?}"
2169 );
2170 }
2171 Err(refused) => assert!(
2172 judged.is_some(),
2173 "{name} under {perm:?}/{refl:?} was refused as {refused} while \
2174 the judge called the state safe — the two ends disagree"
2175 ),
2176 }
2177 }
2178 }
2179 }
2180 assert_eq!(
2181 checked,
2182 8 * 6 * 8,
2183 "binding count: states x perms x mirrors"
2184 );
2185 assert!(
2186 resolved > 0 && resolved < checked,
2187 "binding count {resolved} of {checked}: the sweep must contain both \
2188 resolutions and refusals, or it discriminates nothing"
2189 );
2190 }
2191}