Skip to main content

delvewright_dsl/
equipment.rs

1//! **What a body shows** (spec-0067 §4): the equipment slots each living entity
2//! type of pinned Minecraft Java 1.21.11 draws, the kind of piece each slot
3//! draws, and the fit rule `DW0898` holds every declared piece to.
4//!
5//! The server stores all eight slots on every living entity; the client draws
6//! only the ones the renderer registered for that entity type carries an
7//! equipment layer for. A piece in a slot the body does not draw is structurally
8//! perfect NBT nobody sees, and it is refused where it is declared.
9//!
10//! Two pinned tables answer the two halves:
11//!
12//! - **The body table** (`crates/dsl/data/entity-slots-1.21.11.json`): one row
13//!   per living entity type of the pinned registry, slot → the piece kinds its
14//!   layers draw, the body/saddle layer type, and the state a conditional hand
15//!   is drawn in; each row names the client renderer it was read from. Authored
16//!   from the pinned client's renderers under Mojang's published mappings, and
17//!   held to the entity-type tags and the equipment assets by the cross-checks
18//!   in `crates/delvec/tests/equipment_tables.rs`.
19//! - **The item facts** ([`Equippable`]), injected through
20//!   [`crate::registry::ItemRegistry::equippable`]: the compiler vendors them as
21//!   `crates/delvec/data/item-equippable-1.21.11.json`
22//!   (`tools/maintenance/extract-item-equippable.py`).
23
24use std::collections::{BTreeMap, BTreeSet};
25
26use serde::Deserialize;
27
28use crate::diagnostic::{Diagnostic, codes};
29use crate::envelope::Campaign;
30use crate::registry::{ItemRegistry, entity_in_tag, entity_tags, namespaced_entity};
31use crate::{BodyRef, EquipSlot, MobEquipment};
32
33/// The kind of piece an item is, derived from its `equippable` component and
34/// the equipment asset it draws through (spec-0067 §4.1). The kind decides
35/// which of a body's shown slots can draw it.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum PieceKind {
39    /// An asset and a `head` / `chest` / `legs` / `feet` slot: drawn by
40    /// `HumanoidArmorLayer`.
41    Armour,
42    /// An asset with a `wings` layer (the elytra): drawn by `WingsLayer`.
43    Wings,
44    /// An asset with a `*_body` or `*_saddle` layer: drawn by a body's own
45    /// equipment layer.
46    Animal,
47    /// No asset — a head, the carved pumpkin, the shield, and every item with
48    /// no `equippable` component at all.
49    Item,
50}
51
52impl PieceKind {
53    /// The word a diagnostic prints.
54    pub fn name(self) -> &'static str {
55        match self {
56            PieceKind::Armour => "armour",
57            PieceKind::Wings => "wings",
58            PieceKind::Animal => "animal",
59            PieceKind::Item => "item",
60        }
61    }
62}
63
64/// An item's `minecraft:equippable` facts in the pinned item data.
65#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct Equippable {
68    /// The slot the item declares, in the game's spelling (`body`, `offhand`).
69    pub slot: String,
70    /// The equipment asset the item draws through, if any.
71    #[serde(default)]
72    pub asset_id: Option<String>,
73    /// The entity ids and `#tag`s the item admits; empty when it names none.
74    #[serde(default)]
75    pub allowed_entities: Vec<String>,
76    /// The kind of piece this is.
77    pub kind: PieceKind,
78}
79
80impl Equippable {
81    /// The declared slot as the DSL's slot type.
82    pub fn declared_slot(&self) -> Option<EquipSlot> {
83        EquipSlot::from_nbt(&self.slot)
84    }
85}
86
87/// What an item registry knows about an item's `equippable` component.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum EquippableFact<'a> {
90    /// The registry carries no equippable data; nothing is judged.
91    Unknown,
92    /// The item carries no `equippable` component: a plain item.
93    Plain,
94    /// The item's component.
95    Equippable(&'a Equippable),
96}
97
98impl EquippableFact<'_> {
99    /// The kind of piece, or `None` when the registry does not know.
100    pub fn kind(self) -> Option<PieceKind> {
101        match self {
102            EquippableFact::Unknown => None,
103            EquippableFact::Plain => Some(PieceKind::Item),
104            EquippableFact::Equippable(e) => Some(e.kind),
105        }
106    }
107}
108
109/// One slot a body draws.
110#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct ShownSlot {
113    /// The piece kinds this slot's layers draw.
114    pub kinds: Vec<PieceKind>,
115    /// The equipment layer type a `body` / `saddle` slot draws through.
116    #[serde(default)]
117    pub layer: Option<String>,
118    /// The state a conditional hand is drawn in (an illager's, the panda's);
119    /// absent when the slot is drawn in every state.
120    #[serde(default)]
121    pub when: Option<String>,
122}
123
124/// One living entity type's row in the body table.
125#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct BodyRow {
128    /// The client renderer class the row was read from.
129    pub renderer: String,
130    /// Slot (the game's spelling) → what it draws. Empty for a body that
131    /// draws no equipment.
132    pub slots: BTreeMap<String, ShownSlot>,
133}
134
135impl BodyRow {
136    /// What this body draws in `slot`, if it draws it at all.
137    pub fn shown(&self, slot: EquipSlot) -> Option<&ShownSlot> {
138        self.slots.get(slot.nbt())
139    }
140}
141
142/// The body table: namespaced entity id → its row, one per living entity type
143/// of the pinned registry.
144pub fn body_table() -> &'static BTreeMap<String, BodyRow> {
145    static TABLE: std::sync::LazyLock<BTreeMap<String, BodyRow>> = std::sync::LazyLock::new(|| {
146        serde_json::from_str(include_str!("../data/entity-slots-1.21.11.json"))
147            .expect("the vendored body table is valid JSON of its own shape")
148    });
149    &TABLE
150}
151
152/// The row for `entity`, or `None` for an entity type that is not a living
153/// entity of the pinned registry (a boat, a marker, a display).
154pub fn body_row(entity: &str) -> Option<&'static BodyRow> {
155    body_table().get(&namespaced_entity(entity))
156}
157
158/// Whether `entity` draws `slot` for a piece of `kind`.
159pub fn shows(entity: &str, slot: EquipSlot, kind: PieceKind) -> bool {
160    body_row(entity)
161        .and_then(|r| r.shown(slot))
162        .is_some_and(|s| s.kinds.contains(&kind))
163}
164
165/// Whether `entity` draws anything at all in `slot`.
166pub fn shows_slot(entity: &str, slot: EquipSlot) -> bool {
167    body_row(entity).and_then(|r| r.shown(slot)).is_some()
168}
169
170/// The slots `entity` shows, spelled for a creator: `` `body` (animal) ``,
171/// `` `main_hand` (any piece) `` — or `None` when it shows none.
172pub fn shown_slots_phrase(entity: &str) -> Option<String> {
173    let row = body_row(entity)?;
174    let all = [
175        PieceKind::Armour,
176        PieceKind::Wings,
177        PieceKind::Animal,
178        PieceKind::Item,
179    ];
180    let parts: Vec<String> = EquipSlot::ALL
181        .into_iter()
182        .filter_map(|s| {
183            let shown = row.shown(s)?;
184            let kinds = if all.iter().all(|k| shown.kinds.contains(k)) {
185                "any piece".to_string()
186            } else {
187                let mut ks: Vec<PieceKind> = shown.kinds.clone();
188                ks.sort();
189                ks.iter().map(|k| k.name()).collect::<Vec<_>>().join(" or ")
190            };
191            let when = shown
192                .when
193                .as_ref()
194                .map(|w| format!(", drawn only while {w}"))
195                .unwrap_or_default();
196            Some(format!("`{}` ({kinds}{when})", s.field()))
197        })
198        .collect();
199    (!parts.is_empty()).then(|| parts.join(", "))
200}
201
202/// Whether `entity` is admitted by an `allowed_entities` list: an entry is an
203/// entity id or a `#tag` of the vendored entity-type tag table.
204pub fn admitted(entity: &str, allowed: &[String]) -> bool {
205    let id = namespaced_entity(entity);
206    allowed.iter().any(|a| match a.strip_prefix('#') {
207        Some(tag) => entity_in_tag(&id, tag),
208        None => namespaced_entity(a) == id,
209    })
210}
211
212/// An `allowed_entities` list spelled out: every tag expanded to its members,
213/// sorted and de-duplicated, so a creator never opens the tag file.
214pub fn admitted_entities(allowed: &[String]) -> Vec<String> {
215    let mut out: BTreeSet<String> = BTreeSet::new();
216    for a in allowed {
217        match a.strip_prefix('#') {
218            Some(tag) => {
219                out.extend(entity_tags().get(tag).into_iter().flatten().cloned());
220            }
221            None => {
222                out.insert(namespaced_entity(a));
223            }
224        }
225    }
226    out.into_iter().collect()
227}
228
229/// The shapes of `DW0898` one piece meets (spec-0067 §4.2).
230#[derive(Clone, Debug, Default, PartialEq, Eq)]
231pub struct Misfit {
232    /// Shape 1: the body does not show this piece in this slot.
233    pub unshown: bool,
234    /// Shape 2: the item declares this other slot.
235    pub declared_elsewhere: Option<EquipSlot>,
236    /// Shape 3: the item's allowed-entities list excludes the body; the list,
237    /// expanded.
238    pub excluded_by: Option<Vec<String>>,
239}
240
241impl Misfit {
242    /// Whether any shape holds.
243    pub fn any(&self) -> bool {
244        self.unshown || self.declared_elsewhere.is_some() || self.excluded_by.is_some()
245    }
246}
247
248/// Judge one piece `item` in `slot` on a body of type `entity`. `None` when the
249/// registry does not know the item's equippable facts (nothing is judged).
250pub fn judge(
251    entity: &str,
252    slot: EquipSlot,
253    item: &str,
254    items: &dyn ItemRegistry,
255) -> Option<Misfit> {
256    let fact = items.equippable(item);
257    let kind = fact.kind()?;
258    let mut m = Misfit {
259        unshown: !shows(entity, slot, kind),
260        ..Misfit::default()
261    };
262    if let EquippableFact::Equippable(e) = fact {
263        if let Some(declared) = e.declared_slot()
264            && !slot.is_hand()
265            && declared != slot
266        {
267            m.declared_elsewhere = Some(declared);
268        }
269        if !e.allowed_entities.is_empty() && !admitted(entity, &e.allowed_entities) {
270            m.excluded_by = Some(admitted_entities(&e.allowed_entities));
271        }
272    }
273    Some(m)
274}
275
276/// The `DW0898` message for one misfit piece: every shape that holds, each with
277/// its remedy.
278pub fn misfit_message(
279    what: &str,
280    entity: &str,
281    slot: EquipSlot,
282    item: &str,
283    kind: PieceKind,
284    m: &Misfit,
285) -> String {
286    let body = namespaced_entity(entity);
287    let mut shapes: Vec<String> = Vec::new();
288    if m.unshown {
289        let shown = match (body_row(&body), shown_slots_phrase(&body)) {
290            (None, _) => format!(
291                "`{body}` is not a living entity of the pinned 1.21.11 registry, so no slot of it \
292                 is ever drawn"
293            ),
294            (Some(_), None) => format!("`{body}` draws no equipment slot at all"),
295            (Some(_), Some(p)) => format!("`{body}` draws {p}"),
296        };
297        let weapon = if slot.is_hand() {
298            " A held weapon meant as a number is written as `attributes`, which applies whether \
299             or not anything is drawn."
300        } else {
301            ""
302        };
303        shapes.push(format!(
304            "the body does not show it: `{item}` is a piece of kind {} in `{}`, and {shown}. \
305             Dress another body, or choose a piece this body draws.{weapon}",
306            kind.name(),
307            slot.field()
308        ));
309    }
310    if let Some(declared) = m.declared_elsewhere {
311        shapes.push(format!(
312            "the wrong slot: `{item}` declares the `{}` slot and is written in `{}`. Move it to \
313             `{}`.",
314            declared.field(),
315            slot.field(),
316            declared.field()
317        ));
318    }
319    if let Some(admits) = &m.excluded_by {
320        shapes.push(format!(
321            "the wrong body: `{item}` is worn only by {}, and this body is `{body}`. Change the \
322             entity, or choose a piece `{body}` wears.",
323            admits
324                .iter()
325                .map(|e| format!("`{e}`"))
326                .collect::<Vec<_>>()
327                .join(", ")
328        ));
329    }
330    format!(
331        "{what} equipment `{}` puts `{item}` where the pinned game will not show it on this body \
332         ({} shape(s)): {}",
333        slot.field(),
334        shapes.len(),
335        shapes.join(" Also, ")
336    )
337}
338
339/// One body that wears an `equipment` declaration: where it is, what it is
340/// called in a diagnostic, the entity type the puppet actually wears, and the
341/// declaration.
342pub struct DressedBody<'a> {
343    /// JSON pointer of the `equipment` object.
344    pub path: String,
345    /// `actor` / `wave-mob`, for a diagnostic.
346    pub what: &'static str,
347    /// The entity type the body ships as.
348    pub entity: String,
349    /// The declaration.
350    pub equipment: &'a MobEquipment,
351}
352
353/// Every equipment declaration the campaign makes, in declaration order: wave
354/// mobs, then actors (judged as the body the puppet wears — a skinned actor is a
355/// mannequin).
356pub fn dressed_bodies(c: &Campaign) -> Vec<DressedBody<'_>> {
357    let q = &c.quests.content;
358    let mut out = Vec::new();
359    for (i, w) in q.waves.iter().enumerate() {
360        for (k, m) in w.mobs.iter().enumerate() {
361            if let Some(eq) = &m.equipment {
362                out.push(DressedBody {
363                    path: format!("/content/waves/{i}/mobs/{k}/equipment"),
364                    what: "wave-mob",
365                    entity: m.entity.clone(),
366                    equipment: eq,
367                });
368            }
369        }
370    }
371    for (i, a) in q.actors.iter().enumerate() {
372        if let Some(eq) = &a.equipment {
373            out.push(DressedBody {
374                path: format!("/content/actors/{i}/equipment"),
375                what: "actor",
376                entity: BodyRef::Actor(a).worn_entity().to_string(),
377                equipment: eq,
378            });
379        }
380    }
381    out
382}
383
384/// `DW0898`: every declared piece is put where the pinned game shows it on
385/// that body.
386pub fn fit_checks(c: &Campaign, items: &dyn ItemRegistry, d: &mut Vec<Diagnostic>) {
387    for b in dressed_bodies(c) {
388        for (slot, piece) in b.equipment.pieces() {
389            let Some(piece) = piece else { continue };
390            let item = piece.item();
391            if !items.contains(item) {
392                continue; // `DW0143` names it.
393            }
394            let Some(m) = judge(&b.entity, slot, item, items) else {
395                continue;
396            };
397            if !m.any() {
398                continue;
399            }
400            let kind = items
401                .equippable(item)
402                .kind()
403                .expect("judged, so the kind is known");
404            d.push(Diagnostic::error(
405                codes::EQUIPMENT_UNSHOWN,
406                "quests",
407                format!("{}/{}", b.path, slot.field()),
408                misfit_message(b.what, &b.entity, slot, item, kind, &m),
409            ));
410        }
411    }
412}
413
414/// What the fit rule examined, zeroes included (spec-0067 §5).
415#[derive(Clone, Debug, Default, PartialEq, Eq)]
416pub struct EquipmentBinding {
417    /// Equipment declarations (a wave-mob stack or an actor).
418    pub bodies: usize,
419    /// Distinct entity types among them.
420    pub entity_types: usize,
421    /// Pieces declared.
422    pub pieces: usize,
423    /// Distinct slots in use.
424    pub slots: usize,
425    /// Pieces whose item declares a slot in the pinned item data.
426    pub declared_slot: usize,
427    /// Pieces whose item carries an allowed-entities list.
428    pub allowed_list: usize,
429    /// Pieces refused (`DW0898`).
430    pub refused: usize,
431}
432
433impl EquipmentBinding {
434    /// Count what [`fit_checks`] examines on `c`.
435    pub fn of(c: &Campaign, items: &dyn ItemRegistry) -> Self {
436        let mut b = EquipmentBinding::default();
437        let mut types: BTreeSet<String> = BTreeSet::new();
438        let mut slots: BTreeSet<EquipSlot> = BTreeSet::new();
439        for body in dressed_bodies(c) {
440            b.bodies += 1;
441            types.insert(namespaced_entity(&body.entity));
442            for (slot, piece) in body.equipment.pieces() {
443                let Some(piece) = piece else { continue };
444                b.pieces += 1;
445                slots.insert(slot);
446                let item = piece.item();
447                if let EquippableFact::Equippable(e) = items.equippable(item) {
448                    b.declared_slot += 1;
449                    if !e.allowed_entities.is_empty() {
450                        b.allowed_list += 1;
451                    }
452                }
453                if items.contains(item)
454                    && judge(&body.entity, slot, item, items).is_some_and(|m| m.any())
455                {
456                    b.refused += 1;
457                }
458            }
459        }
460        b.entity_types = types.len();
461        b.slots = slots.len();
462        b
463    }
464
465    /// The one line this rule owes its reader.
466    pub fn line(&self) -> String {
467        format!(
468            "equipment binding: {} body(ies) dressed over {} entity type(s), {} piece(s) declared \
469             over {} slot(s) in use, {} piece(s) with a registry-declared slot, {} with an \
470             allowed-entity list, {} refused (DW0898).",
471            self.bodies,
472            self.entity_types,
473            self.pieces,
474            self.slots,
475            self.declared_slot,
476            self.allowed_list,
477            self.refused
478        )
479    }
480}