delvewright_dsl/validate/mod.rs
1//! Campaign validation: all six rule groups from spec-0001.
2//!
3//! [`validate_campaign`] uses the vendored v0 registries; the compiler injects
4//! full registries via [`validate_campaign_with`].
5
6use crate::Verb;
7use std::collections::{BTreeMap, BTreeSet};
8
9use crate::QuestEffect;
10use crate::diagnostic::{Diagnostic, codes};
11use crate::envelope::Campaign;
12use crate::metrics::Metrics;
13use crate::registry::{
14 AnchorRegistry, BlockRegistry, EntityRegistry, ItemRegistry, VendoredAnchorRegistry,
15 VendoredBlockRegistry, VendoredEffectRegistry, VendoredEntityRegistry, VendoredItemRegistry,
16};
17
18/// Validate a campaign against all spec-0001 rules using the vendored v0
19/// registries (subset item + entity registries + hello-world anchor metadata).
20pub fn validate_campaign(c: &Campaign) -> Vec<Diagnostic> {
21 let items = VendoredItemRegistry::v1_21_11();
22 let entities = VendoredEntityRegistry::v1_21_11();
23 let anchors = VendoredAnchorRegistry::hello_world();
24 validate_campaign_with(c, &items, &anchors, &entities)
25}
26
27/// Validate a campaign with caller-supplied registries. The `entities` registry
28/// (DSL v0.3) validates stage-5 wave mobs; the compiler injects the full
29/// 1.21.11 item/entity registries and real prefab metadata.
30pub fn validate_campaign_with(
31 c: &Campaign,
32 items: &dyn ItemRegistry,
33 anchors: &dyn AnchorRegistry,
34 entities: &dyn EntityRegistry,
35) -> Vec<Diagnostic> {
36 let mut d: Vec<Diagnostic> = Vec::new();
37
38 crate::envelope::envelope_checks(c, &mut d);
39 // Id syntax (`DW0110`), each collection in turn.
40 crate::world::world_id_syntax(c, &mut d);
41 crate::npc::npc_id_syntax(c, &mut d);
42 crate::class::class_id_syntax(c, &mut d);
43 crate::quest_plan::plan_id_syntax(c, &mut d);
44 crate::quest::check::quest_id_syntax(c, &mut d);
45 crate::actor::actor_id_syntax(c, &mut d);
46 crate::assembly::assembly_id_syntax(c, &mut d);
47 crate::dialogue::dialogue_id_syntax(c, &mut d);
48 // Id uniqueness (`DW0111`), each namespace in turn.
49 crate::world::world_id_uniqueness(c, &mut d);
50 crate::npc::npc_id_uniqueness(c, &mut d);
51 crate::class::class_id_uniqueness(c, &mut d);
52 crate::quest_plan::plan_id_uniqueness(c, &mut d);
53 crate::quest::check::quest_id_uniqueness(c, &mut d);
54 crate::actor::actor_id_uniqueness(c, &mut d);
55 crate::assembly::assembly_id_uniqueness(c, &mut d);
56 crate::dialogue::dialogue_id_uniqueness(c, &mut d);
57 // Dangling references (`DW0112`) outside the dialogue graph and the finale.
58 crate::npc::npc_dangling_refs(c, &mut d);
59 crate::world::atmosphere_dangling_refs(c, &mut d);
60 crate::quest_plan::plan_dangling_refs(c, &mut d);
61 crate::quest::check::quest_dangling_refs(c, &mut d);
62 crate::dialogue::dialogue_graph_checks(c, &mut d);
63 crate::quest_plan::plan_checks(c, &mut d);
64 crate::quest::check::after_ordering(c, &mut d);
65 crate::trigger::press_answer_checks(c, &mut d);
66 crate::trigger::press_obligation_checks(c, &mut d);
67 crate::world::horizon_param_checks(c, &mut d);
68 crate::world::world_checks(c, &mut d);
69 crate::world::lighting_range_checks(c, &mut d);
70 // spec-0031: the runtime-state surface. Every loop inside is empty for a
71 // campaign that declares no datum and no comparison.
72 crate::state::state_checks(c, &mut d);
73 // A gate that contradicts itself can never open (`DW0847`). Unconditional
74 // and over the whole closed consumer set — an ungated site contributes no
75 // terms and cannot contradict.
76 crate::state::gate_contradiction_checks(c, &mut d);
77 // `DW0527`: a gate read after a bundle's own conditional write. Over every
78 // effect root of every campaign — not inside `economy_checks`, whose early
79 // return on a campaign with no stakes and no shops would skip it.
80 crate::state::read_after_write_checks(c, &mut d);
81 crate::lethal::lethal_stage_checks(c, &mut d);
82 // spec-0031: the status-effect verbs. Every walk inside is empty for a
83 // campaign that declares neither verb. The status-effect registry is the
84 // fixed vanilla list wave-mob effects are validated against, so no injected
85 // registry is needed.
86 crate::quest::check::status_effect_checks(c, &VendoredEffectRegistry::v1_21_11(), &mut d);
87 // spec-0032: the trade and recovery-stake surface. Every loop inside is
88 // empty for a campaign that declares neither.
89 crate::economy::economy_checks(c, &mut d);
90 // spec-0034: the per-body traversal declaration. The walk is empty for a
91 // campaign that declares none.
92 crate::body::body_traversal_checks(c, &mut d);
93 // spec-0101: a watch names a class stage 3 declares. Empty for a campaign
94 // whose bodies declare no class watch.
95 crate::body::body_watch_checks(c, &mut d);
96 // spec-0097: which overlay layers a skinned body hides. Empty for a campaign
97 // that hides none.
98 crate::npc::skin_layer_checks(c, &mut d);
99 crate::world::prefab_binding(c, anchors, &mut d);
100 // Anchors and items: an NPC's station, a quest's objective and effect
101 // anchors (resolved against its planned area), a trigger's effect anchors
102 // (against every area: a trigger is global), and the class kits. A site-plan
103 // campaign's anchors are DERIVED from the graph and the plan, so the set is
104 // knowable here exactly as a prefab's is (see [`AnchorProviders`]).
105 let providers = AnchorProviders::build(c, anchors);
106 crate::npc::npc_anchor_checks(c, &providers, &mut d);
107 crate::quest::check::quest_anchor_checks(c, &providers, &mut d);
108 crate::trigger::trigger_anchor_checks(c, &providers, &mut d);
109 crate::class::kit_item_checks(c, items, &mut d);
110 crate::quest::check::cross_stage(c, &mut d);
111 crate::dialogue::npc_tree_checks(c, &mut d);
112 // `DW0849`: an item gate no class can bring. The walk is empty for a
113 // campaign with no `requires_item`; the rule judges a contradiction between
114 // two authored documents (a quest's item gate against the class kits).
115 // Bound HERE rather than to a step someone runs, because this is the
116 // function every `delvec` subcommand's validation stage calls.
117 crate::class::item_gate_class_checks(c, &mut d);
118 // Waves, and the references a quest and a trigger's effects make to waves,
119 // flags, items and anchors.
120 crate::wave::wave_decl_checks(c, entities, &mut d);
121 crate::quest::check::quest_reference_checks(c, items, anchors, &mut d);
122 crate::trigger::trigger_effect_flag_checks(c, &mut d);
123 // Skins, wave-mob effects, props, set-block, triggers, dialogue flags and the
124 // NPC lifecycle. The status-effect and block registries are fixed vanilla lists
125 // (see [`VendoredBlockRegistry`]), so no caller-supplied registry is needed.
126 let blocks = VendoredBlockRegistry;
127 let effects_reg = VendoredEffectRegistry::v1_21_11();
128 crate::npc::npc_skin_checks(c, &mut d);
129 crate::wave::mob_effect_checks(c, &effects_reg, &mut d);
130 crate::trigger::trigger_prop_checks(c, &blocks, &mut d);
131 crate::quest::check::quest_prop_checks(c, &blocks, &mut d);
132 // Declared flags across quest / dialogue / trigger `set-flag` effects and
133 // trap and timed-gate disarms (a disarm's `sets_flag` is a first-class
134 // declared flag other objectives/triggers may gate on).
135 let flags = collect_declared_flags(c);
136 crate::trigger::trigger_decl_checks(c, anchors, &blocks, &flags, &mut d);
137 crate::dialogue::dialogue_flag_checks(c, &flags, &mut d);
138 let npc_ids: BTreeSet<&str> = c.npcs.content.npcs.iter().map(|n| n.id.as_str()).collect();
139 crate::npc::despawned_ref_check(c, &npc_ids, &mut d);
140 crate::npc::deferred_npc_checks(c, &npc_ids, &mut d);
141 // Cutscene shot styles, scripted actors and their staging effects
142 // (spec-0014), wave-mob and actor equipment, declared drops and the rest
143 // re-seat. Actor entity ids validate against the injected entity registry
144 // and equipment slots against the item registry.
145 crate::cutscene::cutscene_style_checks(c, &mut d);
146 crate::actor::actor_checks(c, anchors, entities, &mut d);
147 crate::quest::check::carrier_one_checks(c, &mut d);
148 crate::wave::wave_equipment_checks(c, items, &mut d);
149 crate::actor::actor_equipment_checks(c, items, &mut d);
150 // spec-0067: every piece is put where the pinned game shows it on the body
151 // that wears it (`DW0898`).
152 crate::equipment::fit_checks(c, items, &mut d);
153 // Declared drops — the subset an elite/boss leaves behind.
154 crate::wave::check_drops(c, &c.quests.content, items, &mut d);
155 crate::wave::rest_reseat_checks(c, &mut d);
156 crate::class::bonfire_flask_checks(c, declares_bonfire(c), &mut d);
157 // spec-0082: assemblies, their rigs and the verbs and trigger that name
158 // them. Every loop inside is empty for a campaign that declares none.
159 crate::assembly::assembly_checks(c, anchors, &mut d);
160 crate::trap::trap_checks(c, items, entities, anchors, &mut d);
161 crate::shortcut::shortcut_checks(c, anchors, &mut d);
162 crate::ambush::ambush_checks(c, &mut d);
163 crate::timed_gate::timed_gate_checks(c, anchors, &mut d);
164 crate::loot::loot_checks(c, items, anchors, &mut d);
165 crate::quest::check::give_item_enchantment_checks(c, &mut d);
166 crate::wave::lane_checks(c, anchors, &mut d);
167 crate::wave::difficulty_checks(c, &mut d);
168 crate::firework::firework_checks(c, &mut d);
169 // spec-0085: the perception bundle — a particle's id, the envelope's audience
170 // on a party fact, and a sight grant that ends under a camera.
171 crate::perception::perception_checks(c, &mut d);
172 // spec-0073: a fight's health bar. The walk is over every wave and actor;
173 // a campaign that declares no bar and bills no fight `boss` gets nothing.
174 crate::healthbar::health_bar_checks(c, &mut d);
175 // Stage 7 (spec-0017): the map-editor edit script. Structural
176 // checks only — frame/region *resolution* happens at build time against the
177 // solved layout (the compiler's `DW0323`).
178 if c.world_edits.is_some() {
179 let blocks = VendoredBlockRegistry;
180 crate::world_edits::world_edits_checks(c, &blocks, &mut d);
181 }
182 // spec-0049: the map-pipeline documents. Bound to the EVENT it guards
183 // rather than to a step someone runs — a campaign directory holding a
184 // `layout-graph.json` has no path to a verdict that does not come through
185 // here, because this is the function every `delvec` subcommand's validation
186 // stage calls. The reads ledger is what gives `DW0813` its document-side
187 // binding: every building metric these checks rest a verdict on records
188 // that it did.
189 if c.geometry_brief.is_some() || c.layout_graph.is_some() || c.site_plan.is_some() {
190 // ONE run-scoped ledger across stages 2, 3 and 4. That is what closes
191 // the residual `dsl::metrics` names in its own module docs: a check
192 // cannot rest a verdict on an uncalibrated standard without the notice
193 // seeing it, because there is no second ledger for a read to go into.
194 let mut reads = crate::metrics::Reads::new();
195 crate::layout::check(c, &mut reads, &mut d);
196 crate::siteplan::check(c, &mut reads, &mut d);
197 // The notice names the newest document present, because that is the one
198 // whose checks read the most of the provisional half.
199 let stage = if c.site_plan.is_some() {
200 "site-plan"
201 } else if c.layout_graph.is_some() {
202 "layout-graph"
203 } else {
204 "geometry-brief"
205 };
206 if let Some(notice) = Metrics::table().notice(&reads, stage) {
207 d.push(notice);
208 }
209 }
210 // spec-0025: the declared story forks and the per-node
211 // `happening`. Structural only — the branch proofs themselves (`DW048x`) are
212 // compiler-tier, because they need the branch/flag flow model.
213 crate::quest_plan::branch_point_checks(c, &mut d);
214 // A `collect` may adopt a prefab container, which puts a
215 // second positional filler on the same anchor a `loot` entry can name.
216 crate::loot::collect_container_claim_checks(c, &mut d);
217 crate::quest_plan::happening_subject_checks(c, &mut d);
218 // spec-0016 §1: what is really in the
219 // flask. The status-effect registry is the same fixed vanilla list,
220 // and the potion registry is complete in-crate, so no injected registry is
221 // needed.
222 let effects_reg = VendoredEffectRegistry::v1_21_11();
223 crate::class::kit_potion_checks(c, &effects_reg, &mut d);
224 // spec-0031: the stage-5 lethal volumes. Structural only — the
225 // completability half (`DW0510` the forced route, `DW0511` the respawn seat)
226 // is compiler-tier, because it needs the solved layout.
227 crate::lethal::lethal_volume_checks(c, anchors, &mut d);
228 crate::r#loop::loop_checks(c, anchors, &mut d);
229 crate::pulse::pulse_checks(c, anchors, &mut d);
230 // spec-0032: a shop stands on a prefab anchor, and an anchor
231 // no bound prefab provides is the same defect a lethal volume's is.
232 crate::economy::shop_anchor_checks(c, anchors, &mut d);
233 // spec-0071 §2: a price has no field, so the copies of it — the gate term,
234 // the charge, the ceiling on the arm that answers below it — are compared
235 // here. Quantified over every effect list that charges a datum, never over
236 // shops.
237 crate::economy::purchase_checks(c, &mut d);
238 // spec-0074 §8.1: an `on_kill` bundle a credited kill can never reach
239 // (`DW0913`), and an empty one (`DW0100`). The pair that needs the rest
240 // points (`DW0914`/`DW0915`) is compiler-side.
241 crate::onkill::on_kill_checks(c, &mut d);
242 // spec-0061: the design record's own document-level refusals — an empty
243 // `references`, a name that is not a path under `design/`, two rows for one
244 // picture. The comparison against the world's reachable skies (`DW0890`) is
245 // compiler-tier, because it reads the same reachable-state scan `DW0210`
246 // and `DW0496` do and needs the campaign's `design/` directory beside it.
247 // No-op for a campaign that ships no `design.json`.
248 crate::design::check(c, &mut d);
249 // spec-0081: the shape of every celestial time the documents state
250 // (`DW0931`). Empty for a campaign of keywords.
251 crate::celestial::check(c, &mut d);
252
253 d
254}
255
256// ---------------------------------------------------------------------------
257// What anchors does this campaign have? — ONE authority
258// ---------------------------------------------------------------------------
259
260/// Every anchor name this campaign's areas provide, and how much of that answer
261/// is known at validation time.
262///
263/// **This is the only place the question is asked.** Eleven checks used to walk
264/// `world.areas` and call [`AnchorRegistry::anchors_for`] themselves — the shape
265/// `CLAUDE.md` names as *a hand-rolled walk enumerating three of five effect
266/// roots: a defect of expressibility, not of care*. It cost exactly what that
267/// shape always costs. When a site plan became a second placement authority
268/// (spec-0049 §5.2), the derivation began synthesizing a campaign's whole anchor
269/// vocabulary — a `node-` per place, a `seam-` per barred way, an `unlock-` on
270/// the openable side of a one-sided one — and **one** of the eleven walks was
271/// taught to ask [`crate::siteplan::synthesized_anchors`] about it. The other ten
272/// went on enumerating prefabs a derived world does not have, so every stage-5
273/// verb but the ones that walk went unauthorable on a derived map: a `shortcut`
274/// naming the very `unlock-` anchor the derivation places for it was refused as
275/// an invented name, and so were a trap, a shop, a lane, a loot chest, a lethal
276/// volume, a timed gate, a cutscene camera and an actor.
277///
278/// Nothing was red, because a check that resolves against a smaller world than
279/// the campaign has refuses *content*, not itself.
280///
281/// Two properties every consumer needs and neither should re-derive:
282///
283/// * **Leniency.** A pool area's prefab is chosen by the compiler, so an anchor
284/// name it might provide cannot be refused here. [`Self::resolvable`] is
285/// therefore true for every name once any pool area exists — the long-standing
286/// policy, stated once instead of ten times.
287/// * **Completeness.** [`Self::all_areas_known`] is true only when every area
288/// contributed a set: no pool, no prefab the registry has never heard of. The
289/// checks with no owning area to resolve against (a global trigger's effects, a
290/// cutscene camera that legitimately flies between areas) need this rather than
291/// leniency.
292pub(crate) struct AnchorProviders {
293 /// area id → the anchor names that area provides.
294 per_area: BTreeMap<String, BTreeSet<String>>,
295 /// **What SHAPE each derived name is**, for a site-plan campaign
296 /// (spec-0052 §4) — taken from
297 /// [`crate::siteplan::synthesized_anchor_kinds`], the same authority the
298 /// names themselves come from.
299 ///
300 /// Empty for a prefab campaign, and that emptiness is load-bearing rather
301 /// than a gap: a prefab's shape lives in its metadata, which
302 /// [`AnchorRegistry`] deliberately does not carry — it answers names only.
303 /// So [`Self::wrong_kind`] returns `None` there and the compiler keeps
304 /// answering the question at placement time, exactly as it always has. This
305 /// check is about names the ENGINE derives, which are knowable here.
306 kinds: BTreeMap<String, crate::layout::StationKind>,
307 /// The union of every known area's set.
308 union: BTreeSet<String>,
309 /// **This campaign's answer is not knowable at this tier**, so no name is
310 /// refused for being outside the set. Two causes, one consequence: some area
311 /// binds a pool whose draw the compiler makes later, or the campaign hands
312 /// its space to a site plan whose `layout-graph.json` is absent — see
313 /// [`crate::placement::anchor_vocabulary_unknowable`], which is where that
314 /// second question is asked and the only place it is answered.
315 deferred: bool,
316 /// Every area contributed a set — the union is the whole truth.
317 all_areas_known: bool,
318 /// What this campaign hands its space to. It decides which names an author
319 /// may write, and therefore what a refusal is allowed to prescribe.
320 placement: crate::placement::Placement,
321}
322
323impl AnchorProviders {
324 /// Ask the campaign, once.
325 pub(crate) fn build(c: &Campaign, anchors: &dyn AnchorRegistry) -> Self {
326 let mut per_area: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
327 let mut deferred = false;
328 // A site-plan campaign has no prefab to ask, and it does not need one:
329 // its anchors are DERIVED from the graph and the plan, so the set is
330 // knowable here exactly as a prefab's is — and it is the same function
331 // the derivation itself places them by, so a name that resolves here
332 // cannot fail to exist in the built world.
333 let mut declared_areas = c.world.content.areas.len();
334 let mut kinds: BTreeMap<String, crate::layout::StationKind> = BTreeMap::new();
335 if c.site_plan.is_some() {
336 declared_areas += 1;
337 // A derivation with no graph to read names NOTHING, and an empty set
338 // here would make every anchor reference in the campaign a refusal —
339 // of names that are correct, with a remedy that cannot be taken. The
340 // set is unknown, not empty, and that is exactly the pool area's
341 // situation, so it takes the pool area's path: contribute no set and
342 // let the whole campaign defer.
343 if crate::placement::anchor_vocabulary_unknowable(c) {
344 deferred = true;
345 } else {
346 kinds = crate::siteplan::synthesized_anchor_kinds(c);
347 per_area.insert(
348 crate::siteplan::SITE_AREA.to_string(),
349 kinds.keys().cloned().collect(),
350 );
351 }
352 }
353 for a in &c.world.content.areas {
354 if let Some(prefab) = &a.prefab {
355 if let Some(set) = anchors.anchors_for(prefab) {
356 per_area.insert(a.id.as_str().to_string(), set.clone());
357 }
358 } else if a.prefab_pool.is_some() {
359 deferred = true;
360 }
361 }
362 let union: BTreeSet<String> = per_area.values().flatten().cloned().collect();
363 let all_areas_known = declared_areas == per_area.len();
364 Self {
365 per_area,
366 kinds,
367 union,
368 deferred,
369 all_areas_known,
370 placement: crate::placement::Placement::of(c),
371 }
372 }
373
374 /// **The shape this campaign declared for `name`, when it disagrees with what
375 /// the reference site demands** (spec-0052 §7.3); `None` when they agree,
376 /// when the name has no declared shape, or when this campaign has no site
377 /// plan.
378 ///
379 /// Judged from the DECLARATION, at validation, with zero pieces bound —
380 /// which is the whole point of putting the kind on the station rather than
381 /// discovering it when a piece arrives. When a piece does arrive, the same
382 /// kind is demanded of the piece anchor it binds to (spec-0052 §6), so the
383 /// two readings cannot drift.
384 pub(crate) fn wrong_kind(
385 &self,
386 name: &str,
387 demands: crate::layout::StationKind,
388 ) -> Option<crate::layout::StationKind> {
389 let declared = *self.kinds.get(name)?;
390 (declared != demands).then_some(declared)
391 }
392
393 /// The anchor set `area` provides, or `None` when that area's provider is not
394 /// known here (a pool the compiler resolves later, or an unknown prefab).
395 pub(crate) fn for_area(&self, area: &str) -> Option<&BTreeSet<String>> {
396 self.per_area.get(area)
397 }
398
399 /// Whether `name` may be referenced. Lenient by design: a pool area defers the
400 /// answer to the compiler, so nothing is refused while one exists.
401 pub(crate) fn resolvable(&self, name: &str) -> bool {
402 self.deferred || self.union.contains(name)
403 }
404
405 /// Every area contributed a set, so [`Self::union`] is the whole truth.
406 pub(crate) fn all_areas_known(&self) -> bool {
407 self.all_areas_known
408 }
409
410 /// Every anchor name any known area provides.
411 pub(crate) fn union(&self) -> &BTreeSet<String> {
412 &self.union
413 }
414
415 /// **What to write instead**, in the vocabulary THIS campaign has.
416 ///
417 /// One authority for every anchor refusal in this file. `prefab` is the
418 /// sentence the site has always printed, returned unchanged where prefabs
419 /// place the world and dropped where they do not: a derived map has no
420 /// prefab metadata for an author to go and read, so a refusal sending them
421 /// to read some was prescribing an act the campaign cannot perform. See
422 /// [`crate::placement`].
423 pub(crate) fn anchor_remedy<'a>(&self, prefab: &'a str) -> &'a str {
424 self.placement.anchor_remedy(prefab)
425 }
426}
427
428/// **`DW0871` for one reference site** (spec-0052 §7.3).
429///
430/// One builder rather than a message per call site, for the reason the live
431/// command rule gives: a correct rule living inside one caller gives the next
432/// two nothing to reuse, and the wording an author meets must not depend on
433/// which verb happened to catch them.
434///
435/// The remedy it names is **reachable**, and that is checked rather than
436/// assumed: it tells the author to change the station's `kind` in the layout
437/// graph, or to name a station of the demanded kind. Both are things the graph
438/// can say — `kind` is a required field with two writable values, and nothing
439/// refuses a node for declaring either.
440pub(crate) fn station_kind_diag(
441 providers: &AnchorProviders,
442 name: &str,
443 demands: impl Into<Option<crate::layout::StationKind>>,
444 what: &str,
445 stage: &'static str,
446 path: String,
447) -> Option<Diagnostic> {
448 // `None` is a site that names a LOCATION rather than demanding a shape, and
449 // it is the majority: see `QuestEffect::anchor_refs` for where the line is
450 // drawn and for the gallery content an earlier, stricter draft refused.
451 let demands = demands.into()?;
452 let declared = providers.wrong_kind(name, demands)?;
453 Some(Diagnostic::error(
454 crate::layout::DW_STATION_KIND,
455 stage,
456 path,
457 format!(
458 "`{name}` is declared as a {got} station, and {what} demands a {want}. A station's \
459 `kind` is its SHAPE: a `point` is a cell a body is put at — a seat, a subject, an \
460 affordance, the centre of an anchor-centred volume — and a `gate` is a region that \
461 seals and clears, which is what `open-gate`, `close-gate`, a `shortcut` and a \
462 `timed-gate` address. This is read from the layout graph's declaration, so it is \
463 answered before any piece is bound and the same shape is demanded of the piece \
464 anchor when one is. Either change this station's `kind` to `{want}` in the layout \
465 graph, or name a station that is already one.",
466 got = declared.word(),
467 want = demands.word(),
468 ),
469 ))
470}
471
472// ---------------------------------------------------------------------------
473// Shared helpers: what more than one object module asks of the campaign
474// ---------------------------------------------------------------------------
475
476/// Does the campaign declare a `bonfire` — a rest point that re-seats fights
477/// (spec-0016 §1)? Read at the roots the compiler collects rest points from: a
478/// quest's bundles and an environment trigger's effects, at any nesting depth.
479/// The match over the site is exhaustive, so a new root answers here. `DW0370`
480/// asks it, and so does the compiler's `fight_comes_back` where no plan exists
481/// yet (`DW0914`/`DW0915`).
482pub fn declares_bonfire(c: &Campaign) -> bool {
483 collected_effect_any(c, |eff| eff.bonfire().is_some())
484}
485
486/// Does the campaign declare any checkpoint a fallen player respawns at — a
487/// `set-checkpoint` or a `bonfire` — at a root the compiler collects checkpoints
488/// from: the roots [`declares_bonfire`] reads, plus a dialogue option's
489/// `set-checkpoint`. The reading `DW0925` needs.
490pub fn declares_checkpoint(c: &Campaign) -> bool {
491 collected_effect_any(c, |eff| {
492 eff.bonfire().is_some() || eff.set_checkpoint().is_some()
493 }) || c.dialogue.content.dialogues.iter().any(|tree| {
494 tree.nodes.iter().any(|node| {
495 node.options
496 .iter()
497 .any(|opt| opt.effects.iter().any(|e| e.set_checkpoint().is_some()))
498 })
499 })
500}
501
502/// Does any quest-effect at a root the compiler collects checkpoints and rest
503/// points from (a quest's bundles and an environment trigger's effects, at any
504/// nesting depth) satisfy `pred`? The match over the site is exhaustive, so a
505/// new root answers here.
506fn collected_effect_any(c: &Campaign, pred: impl Fn(&QuestEffect) -> bool) -> bool {
507 use crate::EffectSite;
508 let mut found = false;
509 crate::for_each_campaign_effect(c, &mut |_, site, eff| {
510 let collected = match site {
511 EffectSite::Objective { .. }
512 | EffectSite::QuestComplete { .. }
513 | EffectSite::Trigger { .. } => true,
514 EffectSite::Trap { .. }
515 | EffectSite::DialogueRespawn { .. }
516 | EffectSite::ShortcutUnlock { .. }
517 | EffectSite::ShopOffer { .. }
518 | EffectSite::OnDeath
519 | EffectSite::OnKill { .. }
520 | EffectSite::AssemblyLand { .. }
521 | EffectSite::LoopCross { .. } => false,
522 };
523 found |= collected && pred(eff);
524 });
525 found
526}
527
528/// Visit every quest effect **and every transitively-nested effect** (a `sequence`
529/// step, an `on_respawn`/`on_caught`/`on_arrive` bundle) with its relative path
530/// fragment, threading the JSON-pointer path through
531/// [`QuestEffect::nested_effect_lists_labeled`] (`steps/<step>/effects`,
532/// `on_respawn`, …). The deep counterpart of [`for_each_effect`] — the effect-ref
533/// consumer checks (unknown wave / item / block / npc references) use this so a bad
534/// ref nested in a timeline is caught, not shipped unvalidated (mirroring how the
535/// flag/wave *producer* scans and emission already descend). Top-level paths are
536/// unchanged, so a nesting-free campaign is validated identically.
537pub(crate) fn for_each_effect_deep(q: &crate::Quest, mut f: impl FnMut(String, &QuestEffect)) {
538 fn descend(path: String, eff: &QuestEffect, f: &mut dyn FnMut(String, &QuestEffect)) {
539 f(path.clone(), eff);
540 for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
541 for (j, inner) in list.iter().enumerate() {
542 descend(format!("{path}/{pseg}/{j}"), inner, f);
543 }
544 }
545 }
546 for (key, effs) in &q.on_objective_complete {
547 for (m, eff) in effs.iter().enumerate() {
548 descend(format!("on_objective_complete/{key}/{m}"), eff, &mut f);
549 }
550 }
551 for (m, eff) in q.on_complete.iter().enumerate() {
552 descend(format!("on_complete/{m}"), eff, &mut f);
553 }
554}
555
556/// Visit an environment trigger's effects **and every transitively-nested effect**
557/// with a relative path fragment (`effects/<m>`, then nested segments) — the
558/// trigger analogue of [`for_each_effect_deep`].
559pub(crate) fn for_each_trigger_effect_deep(
560 t: &crate::EnvTrigger,
561 mut f: impl FnMut(String, &QuestEffect),
562) {
563 fn descend(path: String, eff: &QuestEffect, f: &mut dyn FnMut(String, &QuestEffect)) {
564 f(path.clone(), eff);
565 for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
566 for (j, inner) in list.iter().enumerate() {
567 descend(format!("{path}/{pseg}/{j}"), inner, f);
568 }
569 }
570 }
571 for (m, eff) in t.effects.iter().enumerate() {
572 descend(format!("effects/{m}"), eff, &mut f);
573 }
574}
575
576/// The trap-payload analogue of [`for_each_trigger_effect_deep`] (spec-0022):
577/// visit every effect of `t.payload`, descending into nested effect lists, with
578/// the JSON pointer relative to the trap. A trap payload is an effect ROOT — the
579/// same standing as a quest bundle or a trigger bundle — so every consumer scan
580/// that walks the other two walks this one too. Empty for a pure spec-0011
581/// redstone trap.
582pub(crate) fn for_each_trap_payload_deep(t: &crate::Trap, mut f: impl FnMut(String, &QuestEffect)) {
583 fn descend(path: String, eff: &QuestEffect, f: &mut dyn FnMut(String, &QuestEffect)) {
584 f(path.clone(), eff);
585 for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
586 for (j, inner) in list.iter().enumerate() {
587 descend(format!("{path}/{pseg}/{j}"), inner, f);
588 }
589 }
590 }
591 for (m, eff) in t.payload.iter().enumerate() {
592 descend(format!("payload/{m}"), eff, &mut f);
593 }
594}
595
596/// Split a block field into its base id and (optional) blockstate suffix,
597/// validating the suffix's syntax (DSL v0.6). Returns the base id to
598/// check against the block registry; `Err(reason)` when a `[...]` suffix is
599/// present but malformed (unbalanced brackets, empty, or a token that is not a
600/// lowercase `key=value`). A well-formed state string is passed through verbatim
601/// to `setblock` — vanilla validates the property names/values against the
602/// block's own state definition, so the compiler only guards the surface syntax.
603pub(crate) fn split_blockstate(block: &str) -> Result<&str, String> {
604 let Some(open) = block.find('[') else {
605 return Ok(block);
606 };
607 let rest = &block[open..];
608 if !rest.ends_with(']') {
609 return Err(format!(
610 "malformed blockstate in `{block}` — the `[...]` suffix must close with `]` \
611 (e.g. `minecraft:grindstone[face=floor]`)"
612 ));
613 }
614 let inner = &rest[1..rest.len() - 1];
615 if inner.trim().is_empty() {
616 return Err(format!(
617 "malformed blockstate in `{block}` — the `[...]` suffix is empty; drop the brackets or \
618 add a `key=value` property"
619 ));
620 }
621 let is_token = |s: &str| {
622 !s.is_empty()
623 && s.chars()
624 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
625 };
626 for prop in inner.split(',') {
627 let mut kv = prop.splitn(2, '=');
628 let key = kv.next().unwrap_or("").trim();
629 match kv.next().map(str::trim) {
630 Some(val) if is_token(key) && is_token(val) => {}
631 _ => {
632 return Err(format!(
633 "malformed blockstate in `{block}` — each property must be `key=value` with \
634 lowercase `[a-z0-9_]` tokens (e.g. `face=floor`)"
635 ));
636 }
637 }
638 }
639 Ok(&block[..open])
640}
641
642/// **The one rest rule at the entry points** (spec-0100 §4.1): a block state an
643/// author places — a `set-block`/`fill-region` block, an `interact`/trigger
644/// prop, a world-edit recipe or scatter item — is judged by
645/// [`crate::blocks::sculk_rest`] where it is typed, so the refusal names the
646/// document path (`DW0998`, `DW0999`). The build judges the assembled world
647/// again with the same function, for the pieces no document typed.
648pub(crate) fn check_sculk_rest(block: &str, stage: &str, path: String, d: &mut Vec<Diagnostic>) {
649 if let Err(fault) = crate::blocks::sculk_rest(block, None) {
650 d.push(Diagnostic::error(
651 fault.code(),
652 stage,
653 path.clone(),
654 format!("{fault} (entered at `{path}`)"),
655 ));
656 }
657}
658
659/// Validate a block field (interact prop / set-block) allowing an optional
660/// verbatim blockstate suffix (DSL v0.6). The base id must be in the block
661/// registry (`DW0193`); a malformed `[...]` suffix reuses `DW0193` with a clear
662/// message. Always a quests-stage diagnostic.
663pub(crate) fn check_block_field(
664 blocks: &dyn BlockRegistry,
665 block: &str,
666 path: String,
667 kind: &str,
668 example: &str,
669 d: &mut Vec<Diagnostic>,
670) {
671 match split_blockstate(block) {
672 Ok(base) => {
673 if !blocks.contains(base) {
674 d.push(Diagnostic::error(
675 codes::BLOCK_UNKNOWN,
676 "quests",
677 path.clone(),
678 format!(
679 "`{kind}` block `{block}` is not a known 1.21.11 block id — use a valid \
680 namespaced block id (e.g. `{example}`)"
681 ),
682 ));
683 }
684 check_sculk_rest(block, "quests", path, d);
685 }
686 Err(reason) => {
687 d.push(Diagnostic::error(
688 codes::BLOCK_UNKNOWN,
689 "quests",
690 path,
691 reason,
692 ));
693 }
694 }
695}
696
697/// Every flag id declared anywhere in the campaign — the union of `set-flag`
698/// effects (quest / dialogue / trigger) and v0.6 trap disarm `sets_flag`
699/// (spec-0011). The authoritative declared-flag set every `requires_flags`
700/// resolves against.
701pub(crate) fn collect_declared_flags(c: &Campaign) -> BTreeSet<&str> {
702 // Every root, every depth — inherited, not listed. This was the shallowest and
703 // narrowest of the three answers `dsl::validate` used to give to "what flags does
704 // this campaign produce": three roots and no descent at all, so a `set-flag`
705 // in a `sequence` step was invisible to it while the main pass saw it.
706 let mut flags: BTreeSet<&str> = BTreeSet::new();
707 crate::for_each_campaign_effect(c, &mut |_path, _site, e| {
708 if let Some(f) = e.set_flag() {
709 flags.insert(f.as_str());
710 }
711 });
712 for t in &c.quests.content.traps {
713 if let Some(dis) = &t.disarm {
714 flags.insert(dis.sets_flag.as_str());
715 }
716 }
717 // A timed gate's disarm produces a first-class flag exactly as a
718 // trap's does — the party jammed the portcullis, and the rest of the campaign
719 // may read that fact.
720 for g in &c.quests.content.timed_gates {
721 if let Some(dis) = &g.disarm {
722 flags.insert(dis.sets_flag.as_str());
723 }
724 }
725 for tree in &c.dialogue.content.dialogues {
726 for node in &tree.nodes {
727 for opt in &node.options {
728 for e in &opt.effects {
729 if let Some(f) = e.set_flag() {
730 flags.insert(f.as_str());
731 }
732 }
733 }
734 }
735 }
736 flags
737}
738
739/// Transitive stage-4 quest ancestors: `q -> {every quest that must complete before
740/// q starts}` (the `depends_on` closure). Acyclicity is guaranteed by `DW0130`.
741pub(crate) fn quest_ancestors(c: &Campaign) -> BTreeMap<&str, BTreeSet<&str>> {
742 let deps: BTreeMap<&str, &Vec<crate::ids::QuestId>> = c
743 .quest_plan
744 .content
745 .quests
746 .iter()
747 .map(|q| (q.id.as_str(), &q.depends_on))
748 .collect();
749 let mut out = BTreeMap::new();
750 for q in c.quest_plan.content.quests.iter() {
751 let mut anc: BTreeSet<&str> = BTreeSet::new();
752 let mut stack = vec![q.id.as_str()];
753 while let Some(cur) = stack.pop() {
754 if let Some(ds) = deps.get(cur) {
755 for dep in ds.iter() {
756 if anc.insert(dep.as_str()) {
757 stack.push(dep.as_str());
758 }
759 }
760 }
761 }
762 out.insert(q.id.as_str(), anc);
763 }
764 out
765}
766
767/// Directed-graph cycle detection (three-color DFS).
768pub(crate) fn graph_has_cycle<'a>(
769 nodes: &[&'a str],
770 edges: &BTreeMap<&'a str, Vec<&'a str>>,
771) -> bool {
772 let mut color: BTreeMap<&'a str, u8> = BTreeMap::new();
773 for &n in nodes {
774 if color.get(n).copied().unwrap_or(0) == 0 && dfs_cycle(n, edges, &mut color) {
775 return true;
776 }
777 }
778 false
779}
780
781fn dfs_cycle<'a>(
782 node: &'a str,
783 edges: &BTreeMap<&'a str, Vec<&'a str>>,
784 color: &mut BTreeMap<&'a str, u8>,
785) -> bool {
786 color.insert(node, 1);
787 if let Some(neis) = edges.get(node) {
788 for &n in neis {
789 match color.get(n).copied().unwrap_or(0) {
790 0 => {
791 if dfs_cycle(n, edges, color) {
792 return true;
793 }
794 }
795 1 => return true,
796 _ => {}
797 }
798 }
799 }
800 color.insert(node, 2);
801 false
802}
803
804/// **The one inventory of flags this campaign produces.**
805///
806/// A `FlagId` has no declaration list — the set of flags is exactly those the
807/// campaign produces — so every rule that asks *does this flag exist?* has to
808/// reconstruct that set, and a rule that reconstructs it differently is asking a
809/// different question under the same name. This is the answer all of them read:
810/// `DW0172`'s unknown-flag refusals over objectives, effects, triggers, traps,
811/// dialogue options and cast placements; `DW0480`'s over a declared branch's
812/// `forks_on`; and `DW0818`'s over a layout-graph edge's gating.
813///
814/// Three producers, and the third is the one a second inventory forgets. A
815/// `set-flag` fires from any effect root at any nesting depth, so the quest-side
816/// walk is [`crate::for_each_campaign_effect`], which inherits both axes
817/// rather than listing either. A dialogue option's `set-flag` is a flat outcome
818/// of a conversation in the dialogue vocabulary, which that walk neither reaches
819/// nor should. And a **trap's `disarm.sets_flag`** is a flag no effect anywhere
820/// sets: the field's own documentation says other objectives and triggers may
821/// read it through `requires_flags`, and until this became the single authority
822/// `DW0172` was computing its own inventory that did not include it — so a
823/// campaign gating on a disarm was refused for naming a flag "no `set-flag`
824/// effect ever produces", which it never claimed to be.
825pub fn produced_flags(c: &Campaign) -> BTreeSet<String> {
826 let mut out = BTreeSet::new();
827 crate::for_each_campaign_effect(c, &mut |_p, _site, eff| {
828 if let Verb::SetFlag { flag, .. } = &eff.verb {
829 out.insert(flag.as_str().to_string());
830 }
831 });
832 for t in &c.dialogue.content.dialogues {
833 for n in &t.nodes {
834 for o in &n.options {
835 for e in &o.effects {
836 if let crate::DialogueEffect::SetFlag { flag } = e {
837 out.insert(flag.as_str().to_string());
838 }
839 }
840 }
841 }
842 }
843 for trap in &c.quests.content.traps {
844 if let Some(dis) = &trap.disarm {
845 out.insert(dis.sets_flag.as_str().to_string());
846 }
847 }
848 out
849}