1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum PieceKind {
39 Armour,
42 Wings,
44 Animal,
47 Item,
50}
51
52impl PieceKind {
53 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#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct Equippable {
68 pub slot: String,
70 #[serde(default)]
72 pub asset_id: Option<String>,
73 #[serde(default)]
75 pub allowed_entities: Vec<String>,
76 pub kind: PieceKind,
78}
79
80impl Equippable {
81 pub fn declared_slot(&self) -> Option<EquipSlot> {
83 EquipSlot::from_nbt(&self.slot)
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum EquippableFact<'a> {
90 Unknown,
92 Plain,
94 Equippable(&'a Equippable),
96}
97
98impl EquippableFact<'_> {
99 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#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
111#[serde(deny_unknown_fields)]
112pub struct ShownSlot {
113 pub kinds: Vec<PieceKind>,
115 #[serde(default)]
117 pub layer: Option<String>,
118 #[serde(default)]
121 pub when: Option<String>,
122}
123
124#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct BodyRow {
128 pub renderer: String,
130 pub slots: BTreeMap<String, ShownSlot>,
133}
134
135impl BodyRow {
136 pub fn shown(&self, slot: EquipSlot) -> Option<&ShownSlot> {
138 self.slots.get(slot.nbt())
139 }
140}
141
142pub 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
152pub fn body_row(entity: &str) -> Option<&'static BodyRow> {
155 body_table().get(&namespaced_entity(entity))
156}
157
158pub 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
165pub fn shows_slot(entity: &str, slot: EquipSlot) -> bool {
167 body_row(entity).and_then(|r| r.shown(slot)).is_some()
168}
169
170pub 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
202pub 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
212pub 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
231pub struct Misfit {
232 pub unshown: bool,
234 pub declared_elsewhere: Option<EquipSlot>,
236 pub excluded_by: Option<Vec<String>>,
239}
240
241impl Misfit {
242 pub fn any(&self) -> bool {
244 self.unshown || self.declared_elsewhere.is_some() || self.excluded_by.is_some()
245 }
246}
247
248pub 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
276pub 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
339pub struct DressedBody<'a> {
343 pub path: String,
345 pub what: &'static str,
347 pub entity: String,
349 pub equipment: &'a MobEquipment,
351}
352
353pub 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
384pub 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; }
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#[derive(Clone, Debug, Default, PartialEq, Eq)]
416pub struct EquipmentBinding {
417 pub bodies: usize,
419 pub entity_types: usize,
421 pub pieces: usize,
423 pub slots: usize,
425 pub declared_slot: usize,
427 pub allowed_list: usize,
429 pub refused: usize,
431}
432
433impl EquipmentBinding {
434 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 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}