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