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}