Skip to main content

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}