Skip to main content

delvewright_dsl/
body.rs

1//! Body traversal — the declaration every body that moves carries (DSL v0.11,
2//! spec-0034), and the one enumeration of the campaign's bodies.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7use crate::{Actor, AreaId, Mark, Npc, NpcSkin};
8
9#[cfg(doc)]
10use crate::WaveMob;
11
12/// How a body gets around.
13///
14/// The compiler DERIVES this from the entity id for every body (spiders climb,
15/// ghasts fly, `#minecraft:aquatic` swims, and everything else — including every
16/// id the table has never heard of — is [`Locomotion::Ground`], the checked
17/// class). [`BodyTraversal`] is the author's side of the same vocabulary: one
18/// enum, so a declaration and a derivation can never mean different things.
19///
20/// The vocabulary lives in this crate rather than in the compiler because it is
21/// now DSL surface; the compiler re-exports it and owns the derivation table
22/// (`compiler::traversal`).
23#[derive(
24    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
25)]
26#[serde(rename_all = "kebab-case")]
27pub enum Locomotion {
28    /// Walks, steps and jumps — the default and the CHECKED class.
29    Ground,
30    /// Climbs sheer vertical surfaces (vanilla's `Spider` class).
31    Climber,
32    /// Leaves the ground under its own power.
33    Flier,
34    /// A member of vanilla's `#minecraft:aquatic` tag. A ledger classification
35    /// that exempts nothing, which is why it may not be **declared**
36    /// (`DW0455`) — see [`BodyTraversal`].
37    Aquatic,
38}
39
40impl Locomotion {
41    /// The stable kebab token this class is written and reported under.
42    pub fn token(self) -> &'static str {
43        match self {
44            Locomotion::Ground => "ground",
45            Locomotion::Climber => "climber",
46            Locomotion::Flier => "flier",
47            Locomotion::Aquatic => "aquatic",
48        }
49    }
50
51    /// Every class, in ledger order — so a report can never silently drop a row
52    /// when a class is added.
53    pub const ALL: [Locomotion; 4] = [
54        Locomotion::Ground,
55        Locomotion::Climber,
56        Locomotion::Flier,
57        Locomotion::Aquatic,
58    ];
59}
60
61/// What a body can do when it moves, **declared by the author** (DSL v0.11,
62/// spec-0034).
63///
64/// Carried by every object class in the DSL that has a body and a position and
65/// is walked by a compiler-emitted route — the stage-2 [`Npc`] and the stage-5
66/// [`Actor`]. It is deliberately one shared type on both rather than a field
67/// per consumer: traversal is a property of a body that moves, not of the verb
68/// that first needed it (CLAUDE.md), and a second bespoke field would be the
69/// defect rather than the fix.
70///
71/// **A declaration is a claim the build holds you to, never an opt-out.** The
72/// compiler compares the verdicts this body earns under the declared class
73/// against the ones it earns under its species' derived class; a declaration
74/// that changes no verdict is inert and is `DW0454`. So declaring `climber` on
75/// a sheep is only accepted where that sheep's route really does go over a
76/// barrier line — the exception is authored and proven, instead of happening by
77/// accident and merely rendering.
78///
79/// **What is deliberately NOT here: `opens_gates`.** Passing a closed fence gate
80/// is a right-click, a scripted walk is a compiler-emitted `tp` polyline whose
81/// puppet performs no interaction at all, and no runtime verb changes a fence
82/// gate's block state. Declaring it would not make it true, so the error tier
83/// (`DW0452`) has no authorable exemption and a declaration can never reach it.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
85#[serde(deny_unknown_fields)]
86pub struct BodyTraversal {
87    /// How this body gets around, overriding what its entity id implies.
88    pub locomotion: Locomotion,
89}
90
91/// One object class that has a **body the compiler stages**: a declared
92/// position, a declared species, and a compiler-emitted route.
93///
94/// A sum type rather than a flattened tuple, so **adding a body class is a
95/// compile error at every consumer** until each one says what it does with it.
96/// The alternative — each rule walking the classes it happens to remember — is
97/// the defect class CLAUDE.md names: a hand-rolled walk that
98/// enumerated three of five effect roots.
99///
100/// **Keyed to the object, not to the verb that first needed it.** This type was
101/// introduced for [`BodyTraversal`] and read, for a while, as "one consumer of
102/// `BodyTraversal`" — which is why its only enumeration
103/// ([`body_traversal_sites`]) was filtered to bodies declaring a traversal, and
104/// why the second property a body carries, its [`NpcSkin`], had no enumeration
105/// at all: the bake walked the stage-2 npc list by hand, and an actor's skin was
106/// emitted into the summon, never baked into the pack, and never refused.
107/// A body's properties belong to the body. [`body_sites`] is the unfiltered
108/// walk; a per-property enumeration is a *filter* over it, never a second walk.
109///
110/// Deliberately NOT a member: [`WaveMob`]. A wave mob has a body and a position,
111/// but it is driven by **native vanilla AI**, never by a compiler-emitted route,
112/// so the compiler makes no claim about the moves it makes and a locomotion
113/// declaration on it could change no verdict. It declares no `skin` either — the
114/// exclusion holds for both properties, and the schema says so
115/// (`crates/dsl/tests/body_skin_sites.rs`). It becomes a member the day the lane
116/// proof reasons about how its bodies move — and it joins here, through this
117/// same type, rather than through a field of its own.
118#[derive(Clone, Copy, Debug)]
119pub enum BodyRef<'a> {
120    /// A stage-2 NPC, walked by `move-npc`.
121    Npc(&'a Npc),
122    /// A stage-5 scripted actor, walked by `move-actor`.
123    Actor(&'a Actor),
124}
125
126impl<'a> BodyRef<'a> {
127    /// The declaring stage's wire name (`npcs` / `quests`) — also the stage
128    /// whose `dsl_version` fences this body's declaration.
129    pub fn stage(self) -> &'static str {
130        match self {
131            BodyRef::Npc(_) => "npcs",
132            BodyRef::Actor(_) => "quests",
133        }
134    }
135
136    /// The body's declared id.
137    pub fn id(self) -> &'a str {
138        match self {
139            BodyRef::Npc(n) => n.id.as_str(),
140            BodyRef::Actor(a) => a.id.as_str(),
141        }
142    }
143
144    /// The entity id written on the body. **Not necessarily the body that
145    /// ships**: a `skin` re-dresses it as a `minecraft:mannequin` — see
146    /// [`Self::worn_entity`].
147    pub fn declared_entity(self) -> &'a str {
148        match self {
149            BodyRef::Npc(n) => n.base_entity.as_str(),
150            BodyRef::Actor(a) => a.entity.as_str(),
151        }
152    }
153
154    /// **The entity id the body ships as**: `minecraft:mannequin` when it
155    /// declares a `skin`, else the declared entity. The one authority for that
156    /// rule — the compiler's geometric proofs (`nav::npc_body_entity`,
157    /// `nav::actor_body_entity`) and the equipment fit rule (`DW0898`) all read
158    /// it.
159    pub fn worn_entity(self) -> &'a str {
160        match self.skin() {
161            Some(_) => "minecraft:mannequin",
162            None => self.declared_entity(),
163        }
164    }
165
166    /// **The mark this body is placed on** — the anchor and offset the engine
167    /// summons it at, for every class alike (spec-0066).
168    ///
169    /// A body's placement is a property of the body, not of the stage list that
170    /// happens to declare it: a mark is a cell, and a cell holds one body. The
171    /// rule that reads this ([`crate::compiler`]'s `DW0896`, via
172    /// [`body_sites`]) therefore quantifies over npcs and actors in one pass
173    /// rather than over `actors[]`, which is where the seven-men-one-anchor
174    /// muster came from.
175    pub fn mark(self) -> Mark {
176        let (anchor, offset) = match self {
177            BodyRef::Npc(n) => (&n.anchor, n.offset),
178            BodyRef::Actor(a) => (&a.anchor, a.offset),
179        };
180        Mark {
181            anchor: anchor.clone(),
182            offset,
183        }
184    }
185
186    /// The area whose anchor table resolves [`Self::mark`]'s anchor first, when this
187    /// class declares one.
188    ///
189    /// A stage-2 npc names its area and is resolved inside it; a stage-5 actor
190    /// names none and is resolved across every placed piece, exactly as an
191    /// `open-gate` / `move-actor` destination is. Stated here so the resolution
192    /// rule is one rule over both classes and not a per-call-site habit.
193    pub fn area(self) -> Option<&'a AreaId> {
194        match self {
195            BodyRef::Npc(n) => Some(&n.area),
196            BodyRef::Actor(_) => None,
197        }
198    }
199
200    /// Whether this body stands on its mark from **world init**, with no effect
201    /// having to fire.
202    ///
203    /// A stage-2 npc does unless it is `deferred`; a stage-5 actor never does —
204    /// a puppet exists only from the `spawn-actor` that summons it, which is why
205    /// an actor no `spawn-actor` names never exists at all.
206    pub fn at_world_init(self) -> bool {
207        match self {
208            BodyRef::Npc(n) => !n.deferred,
209            BodyRef::Actor(_) => false,
210        }
211    }
212
213    /// Whether a **player** can end this body's life.
214    ///
215    /// An npc body is emitted `Invulnerable:1b` unconditionally, so nothing a
216    /// player does removes it; an actor's puppet is `Invulnerable` unless it
217    /// declares [`Actor::vulnerable`]. A body a player can kill is one whose
218    /// lifetime the compiler cannot bound, which is the whole of what this
219    /// answers.
220    pub fn killable_by_players(self) -> bool {
221        match self {
222            BodyRef::Npc(_) => false,
223            BodyRef::Actor(a) => a.vulnerable,
224        }
225    }
226
227    /// This body's traversal declaration, if it carries one.
228    pub fn traversal(self) -> Option<&'a BodyTraversal> {
229        match self {
230            BodyRef::Npc(n) => n.traversal.as_ref(),
231            BodyRef::Actor(a) => a.traversal.as_ref(),
232        }
233    }
234
235    /// This body's skin declaration, if it carries one.
236    ///
237    /// A skinned body of **either** class ships as a `minecraft:mannequin`
238    /// whose `profile.texture` resolves to `delvewright:npc/<campaign_id>/<texture_id>`,
239    /// so either one owes the same `skins/<texture_id>.png` under the same refusal
240    /// (`DW0309`). Answering it here is what stops the bake from being a
241    /// property of one class.
242    pub fn skin(self) -> Option<&'a NpcSkin> {
243        match self {
244            BodyRef::Npc(n) => n.skin.as_ref(),
245            BodyRef::Actor(a) => a.skin.as_ref(),
246        }
247    }
248
249    /// This class's name in the JSON Schema export (`delvec schema --stage all`).
250    ///
251    /// The join between the closed Rust set and the schema, which is the only
252    /// authority on *which object classes declare what*. `body_skin_sites.rs`
253    /// compares the two.
254    pub fn class(self) -> &'static str {
255        match self {
256            BodyRef::Npc(_) => "Npc",
257            BodyRef::Actor(_) => "Actor",
258        }
259    }
260
261    /// Every body class, by schema name. The closed set, stated once.
262    pub const ALL_CLASSES: [&'static str; 2] = ["Npc", "Actor"];
263}
264
265/// A staged body declaration, with the JSON pointer at the declaration itself.
266///
267/// The pointer is at the OBJECT (`/content/npcs/3`), not at any one of its
268/// fields: a per-property site appends its own field name. A pointer built per
269/// property is how two walks of one population start disagreeing about where a
270/// thing was declared.
271#[derive(Clone, Debug)]
272pub struct BodySite<'a> {
273    /// Which object class declared it, and the object itself.
274    pub body: BodyRef<'a>,
275    /// JSON pointer at the declaration, for a diagnostic path.
276    pub path: String,
277}
278
279/// **Every** body the campaign declares, in stage order: stage-2 npcs in
280/// declaration order, then stage-5 actors in declaration order.
281///
282/// The one walk of the campaign's bodies. A rule about a property a body carries
283/// is a *filter* over this ([`body_traversal_sites`], [`body_skin_sites`]) — never
284/// a second traversal, and never a hand-written loop over one stage's list,
285/// which is exactly how an actor's skin came to be emitted but never baked.
286pub fn body_sites(c: &crate::envelope::Campaign) -> Vec<BodySite<'_>> {
287    let mut out: Vec<BodySite<'_>> = Vec::new();
288    for (i, n) in c.npcs.content.npcs.iter().enumerate() {
289        out.push(BodySite {
290            body: BodyRef::Npc(n),
291            path: format!("/content/npcs/{i}"),
292        });
293    }
294    for (i, a) in c.quests.content.actors.iter().enumerate() {
295        out.push(BodySite {
296            body: BodyRef::Actor(a),
297            path: format!("/content/actors/{i}"),
298        });
299    }
300    out
301}
302
303/// A body that carries a [`BodyTraversal`] declaration, with the JSON pointer at
304/// it.
305#[derive(Clone, Debug)]
306pub struct BodyTraversalSite<'a> {
307    /// Which object class declared it, and the object itself.
308    pub body: BodyRef<'a>,
309    /// JSON pointer at the `traversal` field, for a diagnostic path.
310    pub path: String,
311    /// The declaration.
312    pub traversal: &'a BodyTraversal,
313}
314
315/// Every body in the campaign that DECLARES a traversal, in stage order.
316///
317/// The one enumeration of the declaration's consumers, shared by the DSL's value
318/// check (`DW0455`) and by the compiler's proof (`DW0454`), so "which object
319/// classes carry this" is answered in exactly one place.
320pub fn body_traversal_sites(c: &crate::envelope::Campaign) -> Vec<BodyTraversalSite<'_>> {
321    body_sites(c)
322        .into_iter()
323        .filter_map(|s| {
324            s.body.traversal().map(|t| BodyTraversalSite {
325                body: s.body,
326                path: format!("{}/traversal", s.path),
327                traversal: t,
328            })
329        })
330        .collect()
331}
332
333/// A body that carries an [`NpcSkin`] declaration, with the JSON pointer at it.
334#[derive(Clone, Debug)]
335pub struct BodySkinSite<'a> {
336    /// Which object class declared it, and the object itself.
337    pub body: BodyRef<'a>,
338    /// JSON pointer at the `skin` field, for a diagnostic path.
339    pub path: String,
340    /// The declaration.
341    pub skin: &'a NpcSkin,
342}
343
344/// Every body in the campaign that DECLARES a skin, in stage order.
345///
346/// The one enumeration of what the resource-pack bake must serve and what
347/// `DW0309` must refuse, for every class alike — a skinned npc and a skinned
348/// actor are the same fact about two bodies.
349///
350/// **Declarations, not textures.** Two bodies may name one `texture_id` (an npc
351/// and the puppet that plays it), and the caller decides what that means; the
352/// bake reads each file once.
353pub fn body_skin_sites(c: &crate::envelope::Campaign) -> Vec<BodySkinSite<'_>> {
354    body_sites(c)
355        .into_iter()
356        .filter_map(|s| {
357            s.body.skin().map(|k| BodySkinSite {
358                body: s.body,
359                path: format!("{}/skin", s.path),
360                skin: k,
361            })
362        })
363        .collect()
364}
365
366/// The **mutable mirror** of [`body_skin_sites`]: every skin declaration in the
367/// campaign, in the identical order, exposed mutably so one pass can rewrite what
368/// every emitter will read ([`crate::l10n::namespace_skin_textures`]).
369///
370/// It carries no [`BodyRef`] and no pointer, because a rewrite needs neither and a
371/// borrow of the whole body would forbid the field it is there to change. What it
372/// does owe is the **same population**: a body class that declares a skin and is
373/// missing here would keep an un-namespaced texture and collide with every other
374/// delve, silently. `body_skin_sites_mut_is_the_same_walk`
375/// (`crates/dsl/tests/body_skin_sites.rs`) pins that over a campaign carrying one
376/// body of every class in [`BodyRef::ALL_CLASSES`] — the closed set the schema
377/// export is compared against in the same file, so a new body class turns that
378/// coverage red and both walks are visited together.
379pub fn body_skins_mut(c: &mut crate::envelope::Campaign) -> Vec<&mut NpcSkin> {
380    let mut out: Vec<&mut NpcSkin> = Vec::new();
381    out.extend(
382        c.npcs
383            .content
384            .npcs
385            .iter_mut()
386            .filter_map(|n| n.skin.as_mut()),
387    );
388    out.extend(
389        c.quests
390            .content
391            .actors
392            .iter_mut()
393            .filter_map(|a| a.skin.as_mut()),
394    );
395    out
396}
397
398// ---------------------------------------------------------------------------
399// Validation — the checks `dsl::validate` runs over this object (ADR-0031)
400// ---------------------------------------------------------------------------
401
402use crate::diagnostic::{Diagnostic, DwCode, ExitTier};
403use crate::envelope::Campaign;
404
405crate::dw_code! {
406    /// (v0.11, spec-0034) **A declared locomotion the engine cannot hold the
407    /// body to** — today exactly one value, `aquatic`.
408    ///
409    /// The declaration surface exists so an author can claim a capability and
410    /// have the claim PROVEN. `aquatic` is the one
411    /// class that carries no exemption and governs no rule: it is a ledger
412    /// label the compiler derives from vanilla's own `#minecraft:aquatic` tag.
413    /// Declaring it could therefore never change a verdict, so it would always
414    /// land in `DW0454` — and a value whose only possible outcome is another
415    /// diagnostic is a trap, not a surface.
416    ///
417    /// The gap it names, stated rather than left to folklore (CLAUDE.md's
418    /// no-hack rule): the compiler routes **every** body on standable ground,
419    /// and `flooded` cells are impassable and never floor for every body. There
420    /// is no water-traversal model for a declaration to feed, so there is
421    /// nothing to hold an aquatic claim to. When routing grows one, this
422    /// refusal is what has to be deleted to enable the value.
423    ///
424    /// Error tier, raised in `validate_campaign_with`, so the run ends at the
425    /// validation tier (exit 1). Prescription: remove the declaration — a body whose
426    /// route crosses water is governed by the flooded-cell rules already, and
427    /// the derived aquatic class still reaches the binding ledger.
428    pub const TRAVERSAL_UNPROVABLE: DwCode = DwCode::new("DW0455", ExitTier::Build);
429}
430
431/// DSL v0.11 (spec-0034): a declared locomotion the engine cannot hold the body
432/// to is refused at declaration time (`DW0455`).
433///
434/// Today that is exactly `aquatic`, and the reason is structural rather than a
435/// taste call: `aquatic` carries no exemption and governs no rule, so declaring
436/// it could never change a verdict — it would land in `DW0454` every time. A
437/// value whose only outcome is another diagnostic is a trap, so it is refused
438/// here with the gap named.
439pub(crate) fn body_traversal_checks(c: &Campaign, d: &mut Vec<Diagnostic>) {
440    for site in body_traversal_sites(c) {
441        if site.traversal.locomotion != Locomotion::Aquatic {
442            continue;
443        }
444        let (stage, path, id) = (site.body.stage(), &site.path, site.body.id());
445        d.push(Diagnostic::error(
446            TRAVERSAL_UNPROVABLE,
447            stage,
448            format!("{path}/locomotion"),
449            format!(
450                "`{id}` declares `locomotion: aquatic`, which the compiler cannot hold it to. \
451                 `aquatic` is the one class that carries no exemption and governs no rule — it is \
452                 a ledger label derived from vanilla's own `#minecraft:aquatic` tag — so the \
453                 declaration could never change a verdict and would be reported inert (`DW0454`). \
454                 The gap, stated rather than left to folklore: routing has ONE reachability model, \
455                 standable ground, and water-flooded cells are impassable and never floor for \
456                 EVERY body, so there is nothing for an aquatic claim to feed. Prescription: \
457                 remove the declaration — a route that crosses water is already governed by the \
458                 flooded-cell rules, and a body vanilla itself calls aquatic still reaches the \
459                 traversal proof's binding ledger under its derived class."
460            ),
461        ));
462    }
463}