delvewright_dsl/registry.rs
1//! Registries for data that lives outside the DSL: the vanilla item registry and
2//! prefab anchor metadata.
3//!
4//! v0 ships small vendored lists covering only what the M1 hello-world fixtures
5//! use. The **full** 1.21.11 item registry and the real prefab anchor metadata
6//! are vendored/loaded by the compiler (spec-0002 / ADR-0004); the compiler
7//! injects them via [`crate::validate::validate_campaign_with`].
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::ids::{PoolId, PrefabId};
15
16/// Membership test for vanilla item ids (`minecraft:iron_sword`, …).
17pub trait ItemRegistry {
18 /// True if `item_id` is a known item in the pinned MC version.
19 fn contains(&self, item_id: &str) -> bool;
20
21 /// The item's `minecraft:max_stack_size` in the pinned MC version, if this
22 /// registry carries stack-size data for it.
23 ///
24 /// Needed because a single-slot fill (`item replace block … container.<n> with
25 /// <item> <count>`) fails **silently** above the cap — rabbit stew caps at 1, so
26 /// `count: 2` puts nothing in the chest (`DW0436`). `None` means "this registry
27 /// does not know", and the check is then skipped rather than guessed: the small
28 /// vendored DSL-side subset carries ids only, while the compiler injects the full
29 /// 1.21.11 table (`crates/delvec/data/item-stack-sizes-1.21.11.json`).
30 fn max_stack_size(&self, _item_id: &str) -> Option<u32> {
31 None
32 }
33
34 /// The item's `minecraft:equippable` facts in the pinned MC version
35 /// (spec-0067), which the equipment fit rule (`DW0898`) reads.
36 ///
37 /// [`crate::equipment::EquippableFact::Unknown`] means "this registry does
38 /// not know", and the fit rule then judges nothing rather than guessing:
39 /// the small vendored DSL-side subset carries ids only, while the compiler
40 /// injects the full 1.21.11 table
41 /// (`crates/delvec/data/item-equippable-1.21.11.json`).
42 fn equippable(&self, _item_id: &str) -> crate::equipment::EquippableFact<'_> {
43 crate::equipment::EquippableFact::Unknown
44 }
45}
46
47/// Membership test for vanilla entity ids (`minecraft:zombie`, …), used to
48/// validate stage-5 wave mobs (DSL v0.3). Like [`ItemRegistry`], the crate ships
49/// a small vendored subset; the compiler injects the full 1.21.11 entity registry
50/// via [`crate::validate::validate_campaign_with`].
51pub trait EntityRegistry {
52 /// True if `entity_id` is a known entity in the pinned MC version.
53 fn contains(&self, entity_id: &str) -> bool;
54}
55
56/// The vendored `entity_type` **tag** table: tag id → its member entity ids,
57/// from Mojang's own generated reports (`crates/dsl/data/entity-tags-1.21.11.json`,
58/// regenerated per MC pin by `tools/maintenance/extract-entity-tags.py`;
59/// `crates/delvec/data/PROVENANCE.md`).
60///
61/// Vanilla's answer to every question of the form "which entity types do X" that
62/// vanilla itself answers. It lives in this crate — not beside the compiler's
63/// other vendored tables — because both validation tiers ask such questions:
64/// `DW0382` (which bodies honour patrol NBT) is decided from the declaration
65/// alone and therefore here, while `DW0496` (which bodies burn in daylight) and
66/// `DW0452`/`DW0453` (which bodies are aquatic) need the assembled world and are
67/// decided in the compiler, which re-exports these three functions unchanged. A
68/// second copy is how a hand table gets started, and a hand table is how the
69/// engine comes to disagree with the game.
70pub fn entity_tags() -> &'static BTreeMap<String, BTreeSet<String>> {
71 static TAGS: std::sync::LazyLock<BTreeMap<String, BTreeSet<String>>> =
72 std::sync::LazyLock::new(|| {
73 let raw = include_str!("../data/entity-tags-1.21.11.json");
74 let parsed: BTreeMap<String, Vec<String>> =
75 serde_json::from_str(raw).expect("vendored entity-type tags are valid JSON");
76 parsed
77 .into_iter()
78 .map(|(k, v)| (k, v.into_iter().collect()))
79 .collect()
80 });
81 &TAGS
82}
83
84/// Normalize a DSL entity id to its namespaced form (`zombie` →
85/// `minecraft:zombie`), which is how the vendored tags spell every member.
86pub fn namespaced_entity(entity: &str) -> String {
87 if entity.contains(':') {
88 entity.to_string()
89 } else {
90 format!("minecraft:{entity}")
91 }
92}
93
94/// The members of the vanilla `entity_type` tag `tag`, in registry order, bare
95/// (`minecraft:pillager` → `pillager`) — what a diagnostic prints when it has to
96/// tell an author which species a rule admits.
97///
98/// Empty for a tag the pinned game does not carry, which is the same "not a
99/// member" reading [`entity_in_tag`] takes.
100pub fn entity_tag_members_bare(tag: &str) -> Vec<&'static str> {
101 entity_tags()
102 .get(tag)
103 .into_iter()
104 .flatten()
105 .map(|id| id.strip_prefix("minecraft:").unwrap_or(id))
106 .collect()
107}
108
109/// Whether `entity` is a member of the vanilla `entity_type` tag `tag`.
110///
111/// Membership only — this deliberately does **not** expand a nested `#tag`
112/// reference (a tag's member list may name another tag). Every caller so far
113/// reads a flat tag, and silently under-expanding would be the dangerous
114/// direction for a proof that grants exemptions, so the narrowing is stated
115/// rather than assumed: a nested reference reads as "not a member", which puts
116/// the entity in the *checked* class.
117pub fn entity_in_tag(entity: &str, tag: &str) -> bool {
118 let id = namespaced_entity(entity);
119 entity_tags()
120 .get(tag)
121 .is_some_and(|members| members.contains(&id))
122}
123
124/// The entity types the pinned game **discards while the world is peaceful**,
125/// namespaced (`crates/dsl/data/peaceful-despawn-1.21.11.json`, regenerated per
126/// MC pin by `tools/maintenance/extract-peaceful-despawn.py`;
127/// `crates/delvec/data/PROVENANCE.md`).
128///
129/// Read from the 1.21.11 server jar, never written by hand. Every ticked entity
130/// runs `Entity#checkDespawn()`; the `Mob` and `WitherBoss` implementations
131/// discard the body when the difficulty is peaceful and its type's
132/// `EntityType#isAllowedInPeaceful()` is false (`EntityType.Builder#notInPeaceful()`
133/// clears it), `ShulkerBullet`'s discards it on peaceful unconditionally, and
134/// `Entity`'s and `EnderDragon`'s never do. `NoAI`, `PersistenceRequired` and a
135/// `/summon` origin are not consulted. The extractor proves that list of five
136/// implementations is the whole list, and cross-checks the flag read at runtime
137/// against the `notInPeaceful()` calls in `EntityType`'s static initialiser.
138pub fn peaceful_despawn() -> &'static BTreeSet<String> {
139 static SET: std::sync::LazyLock<BTreeSet<String>> = std::sync::LazyLock::new(|| {
140 let raw = include_str!("../data/peaceful-despawn-1.21.11.json");
141 let ids: Vec<String> =
142 serde_json::from_str(raw).expect("vendored peaceful-despawn table is valid JSON");
143 ids.into_iter().collect()
144 });
145 &SET
146}
147
148/// Whether the pinned game discards a body of type `entity` while the world is
149/// peaceful — membership of [`peaceful_despawn`]. Accepts a bare or namespaced id.
150pub fn removed_on_peaceful(entity: &str) -> bool {
151 peaceful_despawn().contains(&namespaced_entity(entity))
152}
153
154/// A prefab lighting profile (spec-0001 "Lighting contract"). `lit` = floor
155/// light ≥ 7; `dim` = 3–6 (needs a rationale); `dark` = < 3 (valid only where
156/// analysis proves a night-vision mitigation — the compiler's `DW0210` check).
157///
158/// The first three are **measurements**, taken by the live probe loop. The
159/// fourth, [`LightingProfile::Unmeasured`], is the honest absence of one.
160#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
161#[serde(rename_all = "lowercase")]
162pub enum LightingProfile {
163 /// Floor light ≥ 7 (default requirement).
164 Lit,
165 /// Floor light 3–6, with an atmosphere rationale.
166 Dim,
167 /// Floor light < 3, only usable with a proven mitigation.
168 Dark,
169 /// The piece has never been probed, and says so.
170 ///
171 /// A generated prefab (spec-0027's grammar back end) knows where it put
172 /// blocks and nothing about the light they end up in, so it declares this
173 /// rather than fabricating a number. It is *not* a synonym for "no
174 /// `lighting` block at all": absence means legacy metadata that predates the
175 /// field, while this is a positive statement that the measurement is owed.
176 /// An unmeasured piece carries no measurement fields — a claim and its
177 /// absence cannot both be true — and is treated as not-`lit` everywhere a
178 /// profile is consumed.
179 Unmeasured,
180}
181
182impl LightingProfile {
183 /// True for the three profiles that assert a measured light level.
184 pub fn is_measurement(self) -> bool {
185 !matches!(self, LightingProfile::Unmeasured)
186 }
187}
188
189/// A prefab's declared `lighting` metadata block (measured once at library
190/// admission). Field names match `prefabs/<name>.json`.
191///
192/// `measured_min_light` / `measured` are `Option` in the type but **not**
193/// optional in the data: a measured profile without them, or an `unmeasured`
194/// profile with them, is refused at deserialisation (see the hand-written
195/// `Deserialize` below). The optionality expresses which profile is being
196/// declared, never a licence to omit a measurement that was claimed.
197#[derive(Clone, Debug, PartialEq, Serialize, JsonSchema)]
198#[serde(deny_unknown_fields)]
199pub struct Lighting {
200 /// The declared profile.
201 pub profile: LightingProfile,
202 /// The measured minimum floor light level. Present iff the profile is a
203 /// measurement.
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub measured_min_light: Option<i64>,
206 /// The date the level was measured (`YYYY-MM-DD`). Present iff the profile
207 /// is a measurement.
208 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub measured: Option<String>,
210 /// Why `dim`/`dark` was chosen (required for `dim`/`dark` by review).
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub rationale: Option<String>,
213 /// How the minimum was measured (provenance breadcrumb; optional).
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub method: Option<String>,
216}
217
218impl Lighting {
219 /// A never-probed declaration — what a generated prefab exports.
220 pub fn unmeasured() -> Lighting {
221 Lighting {
222 profile: LightingProfile::Unmeasured,
223 measured_min_light: None,
224 measured: None,
225 rationale: None,
226 method: None,
227 }
228 }
229}
230
231/// The wire shape of [`Lighting`], before the profile/measurement agreement is
232/// checked. Split out so the check runs on every deserialisation path there is.
233#[derive(Deserialize)]
234#[serde(deny_unknown_fields)]
235struct RawLighting {
236 profile: LightingProfile,
237 #[serde(default)]
238 measured_min_light: Option<i64>,
239 #[serde(default)]
240 measured: Option<String>,
241 #[serde(default)]
242 rationale: Option<String>,
243 #[serde(default)]
244 method: Option<String>,
245}
246
247impl<'de> Deserialize<'de> for Lighting {
248 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Lighting, D::Error> {
249 use serde::de::Error as _;
250 let raw = RawLighting::deserialize(de)?;
251 let has_measurement = raw.measured_min_light.is_some() && raw.measured.is_some();
252 if raw.profile.is_measurement() {
253 // Exactly the requirement the previously-mandatory fields enforced:
254 // claiming `lit`/`dim`/`dark` means claiming a probe ran.
255 if !has_measurement {
256 return Err(D::Error::custom(
257 "a `lit`/`dim`/`dark` lighting profile is a measurement and must carry both \
258 `measured_min_light` and `measured`; declare `\"profile\": \"unmeasured\"` \
259 if the piece has not been probed yet",
260 ));
261 }
262 } else if raw.measured_min_light.is_some() || raw.measured.is_some() {
263 return Err(D::Error::custom(
264 "an `unmeasured` lighting profile cannot carry `measured_min_light` or \
265 `measured` — declare the profile the probe actually found instead",
266 ));
267 }
268 Ok(Lighting {
269 profile: raw.profile,
270 measured_min_light: raw.measured_min_light,
271 measured: raw.measured,
272 rationale: raw.rationale,
273 method: raw.method,
274 })
275 }
276}
277
278/// The prefab-metadata surface DSL validation resolves refs against: declared
279/// anchors, prefab-pool existence, and lighting profiles. v0 ships a small
280/// vendored implementation; the compiler injects the real one from `prefabs/`.
281pub trait AnchorRegistry {
282 /// The DSL anchor ids (`anchor/exit`, …) the prefab provides, or `None` if
283 /// the prefab is unknown to this registry (defer to the compiler).
284 fn anchors_for(&self, prefab: &PrefabId) -> Option<&BTreeSet<String>>;
285
286 /// True if a prefab pool with this id exists in the metadata surface.
287 fn has_pool(&self, _pool: &PoolId) -> bool {
288 false
289 }
290
291 /// Whether a prefab with this id exists in the metadata surface, when this
292 /// registry is in a position to answer: `Some(true)`/`Some(false)` from a
293 /// registry that IS the whole library, `None` from one that is not.
294 ///
295 /// [`Self::anchors_for`] cannot answer this and must not be made to. Its
296 /// `None` is deliberately lenient — *this registry has not heard of the
297 /// piece, so defer* — and every anchor proof in `validate` reads it that
298 /// way, skipping the area rather than refusing it. That leniency is
299 /// correct for a partial registry and catastrophic for a complete one: a
300 /// mistyped `prefab` then removes the area from the anchor index, and the
301 /// per-area anchor proof (`DW0142`) over every quest in it examines
302 /// nothing and passes. The two questions are different and they are asked
303 /// separately for exactly that reason (`DW0856`).
304 ///
305 /// The default is `None` — the safe answer for a test double or a subset
306 /// registry, which refuses nothing it cannot vouch for. The compiler's
307 /// `PrefabRegistry` loads a whole `prefabs/` directory and answers.
308 fn has_prefab(&self, _prefab: &PrefabId) -> Option<bool> {
309 None
310 }
311
312 /// The declared lighting for a prefab, if known.
313 fn lighting_for(&self, _prefab: &PrefabId) -> Option<Lighting> {
314 None
315 }
316
317 /// What the library holds for a rig id (`rig/<name>`, spec-0082 §3.1).
318 ///
319 /// The default is [`RigLookup::Unknown`](crate::rig::RigLookup::Unknown) —
320 /// the answer of a registry that is not the whole library, on whose word
321 /// nothing is refused, exactly as [`Self::has_prefab`] defaults to `None`.
322 /// The compiler's `PrefabRegistry` reads `<library>/rigs/` and answers.
323 fn rig(&self, _rig: &crate::ids::RigId) -> crate::rig::RigLookup<'_> {
324 crate::rig::RigLookup::Unknown
325 }
326}
327
328/// Vendored item registry loaded from an embedded JSON array of item ids.
329#[derive(Debug, Clone)]
330pub struct VendoredItemRegistry {
331 ids: BTreeSet<String>,
332}
333
334impl VendoredItemRegistry {
335 /// The v0 subset of the 1.21.11 item registry used by the M1 fixtures, plus
336 /// the four [`crate::POTION_BEARING_ITEMS`] — a kit item's potion
337 /// `contents` is DSL surface with its own rules (`DW0486`/`DW0487`), so the
338 /// crate's own tests must be able to name the items those rules are about
339 /// without the compiler's full injected registry.
340 pub fn v1_21_11() -> Self {
341 let raw = include_str!("../data/items-1.21.11.json");
342 let ids: Vec<String> =
343 serde_json::from_str(raw).expect("embedded item registry is valid JSON");
344 Self {
345 ids: ids.into_iter().collect(),
346 }
347 }
348}
349
350impl ItemRegistry for VendoredItemRegistry {
351 fn contains(&self, item_id: &str) -> bool {
352 self.ids.contains(item_id)
353 }
354}
355
356/// Vendored entity registry loaded from an embedded JSON array of entity ids
357/// (the common hostile mobs used by v0.3 wave fixtures).
358#[derive(Debug, Clone)]
359pub struct VendoredEntityRegistry {
360 ids: BTreeSet<String>,
361}
362
363impl VendoredEntityRegistry {
364 /// The v0 subset of the 1.21.11 entity registry used by wave fixtures.
365 pub fn v1_21_11() -> Self {
366 let raw = include_str!("../data/entities-1.21.11.json");
367 let ids: Vec<String> =
368 serde_json::from_str(raw).expect("embedded entity registry is valid JSON");
369 Self {
370 ids: ids.into_iter().collect(),
371 }
372 }
373}
374
375impl EntityRegistry for VendoredEntityRegistry {
376 fn contains(&self, entity_id: &str) -> bool {
377 self.ids.contains(entity_id)
378 }
379}
380
381// ---------------------------------------------------------------------------
382// DSL v0.4 registries: blocks (props / set-block) and status effects
383// ---------------------------------------------------------------------------
384
385/// Membership test for vanilla block ids (`minecraft:lever`, …), used to
386/// validate DSL v0.4 `set-block` / `interact.prop` block ids (`DW0193`).
387pub trait BlockRegistry {
388 /// True if `block_id` is a known placeable block in the pinned MC version.
389 fn contains(&self, block_id: &str) -> bool;
390}
391
392/// The complete 1.21.11 **enchantment** id list. Same rationale as
393/// [`EFFECT_IDS_1_21_11`]: 43 ids, stable across the pinned MC version, so it is
394/// inlined rather than vendored as a data file and injected. Extracted from the
395/// `enchantment` registry of the same misode/mcmeta 1.21.11 registries summary
396/// the item/entity registries come from (SHA-256
397/// `7efb184902cfef62b431bc9826ebcbcde2c23746e5624326ffcf922e15cf28f9`, pinned in
398/// `crates/delvec/data/PROVENANCE.md`) — Mojang's own generated data, not a
399/// third-party reconstruction.
400pub const ENCHANTMENT_IDS_1_21_11: &[&str] = &[
401 "minecraft:aqua_affinity",
402 "minecraft:bane_of_arthropods",
403 "minecraft:binding_curse",
404 "minecraft:blast_protection",
405 "minecraft:breach",
406 "minecraft:channeling",
407 "minecraft:density",
408 "minecraft:depth_strider",
409 "minecraft:efficiency",
410 "minecraft:feather_falling",
411 "minecraft:fire_aspect",
412 "minecraft:fire_protection",
413 "minecraft:flame",
414 "minecraft:fortune",
415 "minecraft:frost_walker",
416 "minecraft:impaling",
417 "minecraft:infinity",
418 "minecraft:knockback",
419 "minecraft:looting",
420 "minecraft:loyalty",
421 "minecraft:luck_of_the_sea",
422 "minecraft:lunge",
423 "minecraft:lure",
424 "minecraft:mending",
425 "minecraft:multishot",
426 "minecraft:piercing",
427 "minecraft:power",
428 "minecraft:projectile_protection",
429 "minecraft:protection",
430 "minecraft:punch",
431 "minecraft:quick_charge",
432 "minecraft:respiration",
433 "minecraft:riptide",
434 "minecraft:sharpness",
435 "minecraft:silk_touch",
436 "minecraft:smite",
437 "minecraft:soul_speed",
438 "minecraft:sweeping_edge",
439 "minecraft:swift_sneak",
440 "minecraft:thorns",
441 "minecraft:unbreaking",
442 "minecraft:vanishing_curse",
443 "minecraft:wind_burst",
444];
445
446/// Membership test for vanilla enchantment ids (`minecraft:sharpness`, …), used
447/// to validate actor/wave `equipment` and `loot` enchantments (`DW0433`).
448pub trait EnchantmentRegistry {
449 /// True if `enchantment_id` is a known enchantment in the pinned MC version.
450 fn contains(&self, enchantment_id: &str) -> bool;
451}
452
453/// Vendored enchantment registry built from [`ENCHANTMENT_IDS_1_21_11`].
454#[derive(Debug, Clone)]
455pub struct VendoredEnchantmentRegistry {
456 ids: BTreeSet<&'static str>,
457}
458
459impl VendoredEnchantmentRegistry {
460 /// The full 1.21.11 enchantment registry.
461 pub fn v1_21_11() -> Self {
462 Self {
463 ids: ENCHANTMENT_IDS_1_21_11.iter().copied().collect(),
464 }
465 }
466}
467
468impl Default for VendoredEnchantmentRegistry {
469 fn default() -> Self {
470 Self::v1_21_11()
471 }
472}
473
474impl EnchantmentRegistry for VendoredEnchantmentRegistry {
475 fn contains(&self, enchantment_id: &str) -> bool {
476 let norm = if enchantment_id.contains(':') {
477 enchantment_id.to_string()
478 } else {
479 format!("minecraft:{enchantment_id}")
480 };
481 self.ids.contains(norm.as_str())
482 }
483}
484
485/// The complete 1.21.11 **potion** id list — the `potion` registry a
486/// `minecraft:potion_contents` component's `potion` field names
487/// (`minecraft:strong_healing`, …). 46 ids, extracted from the same
488/// misode/mcmeta 1.21.11 registries summary the item/entity/enchantment lists
489/// come from (SHA-256
490/// `7efb184902cfef62b431bc9826ebcbcde2c23746e5624326ffcf922e15cf28f9`, pinned in
491/// `crates/delvec/data/PROVENANCE.md`) — Mojang's own generated data.
492///
493/// Complete and stable for the pinned version, so unlike the item/entity
494/// registries there is **nothing for the compiler to inject**: this const IS the
495/// registry, and [`is_potion_id`] is the whole membership test (same treatment as
496/// [`VendoredBlockRegistry`]).
497pub const POTION_IDS_1_21_11: &[&str] = &[
498 "minecraft:awkward",
499 "minecraft:fire_resistance",
500 "minecraft:harming",
501 "minecraft:healing",
502 "minecraft:infested",
503 "minecraft:invisibility",
504 "minecraft:leaping",
505 "minecraft:long_fire_resistance",
506 "minecraft:long_invisibility",
507 "minecraft:long_leaping",
508 "minecraft:long_night_vision",
509 "minecraft:long_poison",
510 "minecraft:long_regeneration",
511 "minecraft:long_slow_falling",
512 "minecraft:long_slowness",
513 "minecraft:long_strength",
514 "minecraft:long_swiftness",
515 "minecraft:long_turtle_master",
516 "minecraft:long_water_breathing",
517 "minecraft:long_weakness",
518 "minecraft:luck",
519 "minecraft:mundane",
520 "minecraft:night_vision",
521 "minecraft:oozing",
522 "minecraft:poison",
523 "minecraft:regeneration",
524 "minecraft:slow_falling",
525 "minecraft:slowness",
526 "minecraft:strength",
527 "minecraft:strong_harming",
528 "minecraft:strong_healing",
529 "minecraft:strong_leaping",
530 "minecraft:strong_poison",
531 "minecraft:strong_regeneration",
532 "minecraft:strong_slowness",
533 "minecraft:strong_strength",
534 "minecraft:strong_swiftness",
535 "minecraft:strong_turtle_master",
536 "minecraft:swiftness",
537 "minecraft:thick",
538 "minecraft:turtle_master",
539 "minecraft:water",
540 "minecraft:water_breathing",
541 "minecraft:weakness",
542 "minecraft:weaving",
543 "minecraft:wind_charged",
544];
545
546/// True if `id` (optionally un-namespaced) is a 1.21.11 potion id.
547pub fn is_potion_id(id: &str) -> bool {
548 let norm = if id.contains(':') {
549 id.to_string()
550 } else {
551 format!("minecraft:{id}")
552 };
553 POTION_IDS_1_21_11.contains(&norm.as_str())
554}
555
556/// Membership test for vanilla status-effect ids (`minecraft:slowness`, …),
557/// used to validate DSL v0.4 wave-mob `effects` (`DW0192`) and a kit item's
558/// potion `contents` effects (`DW0486`).
559pub trait EffectRegistry {
560 /// True if `effect_id` is a known status effect in the pinned MC version.
561 fn contains(&self, effect_id: &str) -> bool;
562}
563
564/// The complete 1.21.11 mob status-effect id list (the canonical source both the
565/// vendored and full [`EffectRegistry`]s build from). Small, stable registry, so
566/// it is inlined rather than a data file.
567pub const EFFECT_IDS_1_21_11: &[&str] = &[
568 "minecraft:speed",
569 "minecraft:slowness",
570 "minecraft:haste",
571 "minecraft:mining_fatigue",
572 "minecraft:strength",
573 "minecraft:instant_health",
574 "minecraft:instant_damage",
575 "minecraft:jump_boost",
576 "minecraft:nausea",
577 "minecraft:regeneration",
578 "minecraft:resistance",
579 "minecraft:fire_resistance",
580 "minecraft:water_breathing",
581 "minecraft:invisibility",
582 "minecraft:blindness",
583 "minecraft:night_vision",
584 "minecraft:hunger",
585 "minecraft:weakness",
586 "minecraft:poison",
587 "minecraft:wither",
588 "minecraft:health_boost",
589 "minecraft:absorption",
590 "minecraft:saturation",
591 "minecraft:glowing",
592 "minecraft:levitation",
593 "minecraft:luck",
594 "minecraft:unluck",
595 "minecraft:slow_falling",
596 "minecraft:conduit_power",
597 "minecraft:dolphins_grace",
598 "minecraft:bad_omen",
599 "minecraft:hero_of_the_village",
600 "minecraft:darkness",
601 "minecraft:trial_omen",
602 "minecraft:raid_omen",
603 "minecraft:wind_charged",
604 "minecraft:weaving",
605 "minecraft:oozing",
606 "minecraft:infested",
607 // Registry-fidelity fix (2026-08-03): the pinned 1.21.11 `mob_effect`
608 // registry has 40 entries and this list had 39 — the one 1.21.11 addition
609 // was missing, so a campaign naming it was rejected as unknown by a check
610 // that was simply out of date. Re-derived from the same pinned summary
611 // (SHA-256 `7efb1849…`) the potion list above comes from.
612 "minecraft:breath_of_the_nautilus",
613];
614
615/// A status-effect id in its canonical namespaced form: `blindness` and
616/// `minecraft:blindness` are the same effect to vanilla and must be the same
617/// effect to the compiler.
618///
619/// One spelling for one fact. This normalization existed twice — inside
620/// [`VendoredEffectRegistry::contains`] and inside
621/// [`crate::MobEffect::is_instant`] — before `DW0540` needed a third
622/// copy to decide whether a `clear-effect` removes the effect a `give-effect`
623/// granted. Two ids that the registry accepts as the same must not be two ids to
624/// a rule that pairs them, and a private third copy is how they drift apart.
625pub fn namespaced_effect_id(id: &str) -> String {
626 if id.contains(':') {
627 id.to_string()
628 } else {
629 format!("minecraft:{id}")
630 }
631}
632
633/// The [`BlockRegistry`] of the pinned 1.21.11 jar: every block id the game has,
634/// from `crates/dsl/data/blocks-1.21.11.json` ([`crate::blocks::BlockRegistry`]).
635///
636/// Blocks and items are different registries. A block with no item form
637/// (`minecraft:wall_torch`, wall signs, `minecraft:fire`, crops) is a valid
638/// placement target, and an item that is not a block (`minecraft:diamond`) is
639/// not; every site that places a block by id asks this registry, never the item
640/// registry. A bare id is read as `minecraft:`-namespaced, as the game does.
641#[derive(Debug, Clone, Copy, Default)]
642pub struct VendoredBlockRegistry;
643
644impl BlockRegistry for VendoredBlockRegistry {
645 fn contains(&self, block_id: &str) -> bool {
646 let reg = crate::blocks::BlockRegistry::v1_21_11();
647 if block_id.contains(':') {
648 reg.has(block_id)
649 } else {
650 reg.has(&format!("minecraft:{block_id}"))
651 }
652 }
653}
654
655/// Vendored status-effect registry built from [`EFFECT_IDS_1_21_11`].
656#[derive(Debug, Clone)]
657pub struct VendoredEffectRegistry {
658 ids: BTreeSet<&'static str>,
659}
660
661impl VendoredEffectRegistry {
662 /// The full 1.21.11 status-effect registry.
663 pub fn v1_21_11() -> Self {
664 Self {
665 ids: EFFECT_IDS_1_21_11.iter().copied().collect(),
666 }
667 }
668}
669
670impl Default for VendoredEffectRegistry {
671 fn default() -> Self {
672 Self::v1_21_11()
673 }
674}
675
676impl EffectRegistry for VendoredEffectRegistry {
677 fn contains(&self, effect_id: &str) -> bool {
678 self.ids.contains(namespaced_effect_id(effect_id).as_str())
679 }
680}
681
682/// Vendored anchor registry loaded from embedded prefab metadata.
683#[derive(Debug, Clone)]
684pub struct VendoredAnchorRegistry {
685 by_prefab: BTreeMap<String, BTreeSet<String>>,
686 pools: BTreeSet<String>,
687}
688
689impl VendoredAnchorRegistry {
690 /// The anchors declared by the M1 hello-world prefab(s), plus a fixture pool
691 /// so prefab-pool existence checks are exercised.
692 pub fn hello_world() -> Self {
693 let raw = include_str!("../data/anchors.json");
694 let by_prefab: BTreeMap<String, BTreeSet<String>> =
695 serde_json::from_str(raw).expect("embedded anchor metadata is valid JSON");
696 let pools_raw = include_str!("../data/pools.json");
697 let pools: BTreeSet<String> =
698 serde_json::from_str(pools_raw).expect("embedded pool metadata is valid JSON");
699 Self { by_prefab, pools }
700 }
701}
702
703impl AnchorRegistry for VendoredAnchorRegistry {
704 fn anchors_for(&self, prefab: &PrefabId) -> Option<&BTreeSet<String>> {
705 self.by_prefab.get(prefab.as_str())
706 }
707
708 fn has_pool(&self, pool: &PoolId) -> bool {
709 self.pools.contains(pool.as_str())
710 }
711
712 /// Closed, exactly as [`Self::has_pool`] already is: the embedded metadata
713 /// is the whole library this registry stands for, so a piece it does not
714 /// hold does not exist as far as anything validating against it is
715 /// concerned. Answering `None` here would leave the crate's own validation
716 /// entry point unable to see the defect it validates for.
717 fn has_prefab(&self, prefab: &PrefabId) -> Option<bool> {
718 Some(self.by_prefab.contains_key(prefab.as_str()))
719 }
720}
721
722#[cfg(test)]
723mod lighting_tests {
724 use super::*;
725
726 /// A prefab's lighting profile is a claim about a measurement, so the
727 /// measurement must be there. This was enforced by two mandatory fields
728 /// until `unmeasured` needed them absent; the rule itself did not move.
729 #[test]
730 fn a_measured_profile_still_cannot_omit_its_measurement() {
731 for json in [
732 r#"{ "profile": "lit" }"#,
733 r#"{ "profile": "lit", "measured_min_light": 9 }"#,
734 r#"{ "profile": "lit", "measured": "2026-07-30" }"#,
735 r#"{ "profile": "dim", "measured": "2026-07-30" }"#,
736 r#"{ "profile": "dark", "measured_min_light": 1 }"#,
737 ] {
738 let err = serde_json::from_str::<Lighting>(json).unwrap_err();
739 assert!(
740 err.to_string().contains("must carry both"),
741 "{json} was accepted or misreported: {err}"
742 );
743 }
744 }
745
746 /// ...and the converse, which is the new way to lie: declaring the probe
747 /// never ran while quoting what it found.
748 #[test]
749 fn an_unmeasured_profile_cannot_quote_a_measurement() {
750 for json in [
751 r#"{ "profile": "unmeasured", "measured_min_light": 9 }"#,
752 r#"{ "profile": "unmeasured", "measured": "2026-07-30" }"#,
753 r#"{ "profile": "unmeasured", "measured_min_light": 9, "measured": "2026-07-30" }"#,
754 ] {
755 let err = serde_json::from_str::<Lighting>(json).unwrap_err();
756 assert!(err.to_string().contains("cannot carry"), "{json}: {err}");
757 }
758 }
759
760 /// The two shapes that are true: a hand-probed piece and a generated one.
761 #[test]
762 fn both_honest_declarations_round_trip() {
763 let measured: Lighting = serde_json::from_str(
764 r#"{ "profile": "lit", "measured_min_light": 8, "measured": "2026-07-30",
765 "method": "live 1.21.11 probe" }"#,
766 )
767 .unwrap();
768 assert_eq!(measured.profile, LightingProfile::Lit);
769 assert_eq!(measured.measured_min_light, Some(8));
770 assert!(measured.profile.is_measurement());
771
772 let unmeasured: Lighting = serde_json::from_str(r#"{ "profile": "unmeasured" }"#).unwrap();
773 assert_eq!(unmeasured, Lighting::unmeasured());
774 assert!(!unmeasured.profile.is_measurement());
775
776 // Serialisation omits what is absent rather than writing `null`, so a
777 // re-serialised file is the file that was read.
778 for l in [measured, unmeasured] {
779 let text = serde_json::to_string(&l).unwrap();
780 assert!(!text.contains("null"), "{text}");
781 assert_eq!(serde_json::from_str::<Lighting>(&text).unwrap(), l);
782 }
783 }
784
785 /// An unknown profile string is still refused outright — `unmeasured` is a
786 /// named state, not an escape hatch for anything the reader does not know.
787 #[test]
788 fn an_unknown_profile_is_still_refused() {
789 assert!(serde_json::from_str::<Lighting>(r#"{ "profile": "unknown" }"#).is_err());
790 assert!(
791 serde_json::from_str::<Lighting>(
792 r#"{ "profile": "lit", "measured_min_light": 8, "measured": "x", "extra": 1 }"#
793 )
794 .is_err(),
795 "deny_unknown_fields must survive the hand-written Deserialize"
796 );
797 }
798}
799
800#[cfg(test)]
801mod block_registry_tests {
802 use super::*;
803
804 #[test]
805 fn the_block_registry_is_not_the_item_registry() {
806 let r = VendoredBlockRegistry;
807 for block_without_item in [
808 "minecraft:wall_torch",
809 "minecraft:fire",
810 "minecraft:oak_wall_sign",
811 "minecraft:air",
812 "minecraft:water",
813 "lava",
814 ] {
815 assert!(r.contains(block_without_item), "{block_without_item}");
816 }
817 for not_a_block in [
818 "minecraft:diamond",
819 "minecraft:iron_sword",
820 "minecraft:nope",
821 ] {
822 assert!(!r.contains(not_a_block), "{not_a_block}");
823 }
824 }
825}