use std::collections::{BTreeMap, BTreeSet};
use serde::Deserialize;
use crate::diagnostic::{Diagnostic, codes};
use crate::envelope::Campaign;
use crate::registry::{ItemRegistry, entity_in_tag, entity_tags, namespaced_entity};
use crate::{BodyRef, EquipSlot, MobEquipment};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PieceKind {
Armour,
Wings,
Animal,
Item,
}
impl PieceKind {
pub fn name(self) -> &'static str {
match self {
PieceKind::Armour => "armour",
PieceKind::Wings => "wings",
PieceKind::Animal => "animal",
PieceKind::Item => "item",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Equippable {
pub slot: String,
#[serde(default)]
pub asset_id: Option<String>,
#[serde(default)]
pub allowed_entities: Vec<String>,
pub kind: PieceKind,
}
impl Equippable {
pub fn declared_slot(&self) -> Option<EquipSlot> {
EquipSlot::from_nbt(&self.slot)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EquippableFact<'a> {
Unknown,
Plain,
Equippable(&'a Equippable),
}
impl EquippableFact<'_> {
pub fn kind(self) -> Option<PieceKind> {
match self {
EquippableFact::Unknown => None,
EquippableFact::Plain => Some(PieceKind::Item),
EquippableFact::Equippable(e) => Some(e.kind),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ShownSlot {
pub kinds: Vec<PieceKind>,
#[serde(default)]
pub layer: Option<String>,
#[serde(default)]
pub when: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BodyRow {
pub renderer: String,
pub slots: BTreeMap<String, ShownSlot>,
}
impl BodyRow {
pub fn shown(&self, slot: EquipSlot) -> Option<&ShownSlot> {
self.slots.get(slot.nbt())
}
}
pub fn body_table() -> &'static BTreeMap<String, BodyRow> {
static TABLE: std::sync::LazyLock<BTreeMap<String, BodyRow>> = std::sync::LazyLock::new(|| {
serde_json::from_str(include_str!("../data/entity-slots-1.21.11.json"))
.expect("the vendored body table is valid JSON of its own shape")
});
&TABLE
}
pub fn body_row(entity: &str) -> Option<&'static BodyRow> {
body_table().get(&namespaced_entity(entity))
}
pub fn shows(entity: &str, slot: EquipSlot, kind: PieceKind) -> bool {
body_row(entity)
.and_then(|r| r.shown(slot))
.is_some_and(|s| s.kinds.contains(&kind))
}
pub fn shows_slot(entity: &str, slot: EquipSlot) -> bool {
body_row(entity).and_then(|r| r.shown(slot)).is_some()
}
pub fn shown_slots_phrase(entity: &str) -> Option<String> {
let row = body_row(entity)?;
let all = [
PieceKind::Armour,
PieceKind::Wings,
PieceKind::Animal,
PieceKind::Item,
];
let parts: Vec<String> = EquipSlot::ALL
.into_iter()
.filter_map(|s| {
let shown = row.shown(s)?;
let kinds = if all.iter().all(|k| shown.kinds.contains(k)) {
"any piece".to_string()
} else {
let mut ks: Vec<PieceKind> = shown.kinds.clone();
ks.sort();
ks.iter().map(|k| k.name()).collect::<Vec<_>>().join(" or ")
};
let when = shown
.when
.as_ref()
.map(|w| format!(", drawn only while {w}"))
.unwrap_or_default();
Some(format!("`{}` ({kinds}{when})", s.field()))
})
.collect();
(!parts.is_empty()).then(|| parts.join(", "))
}
pub fn admitted(entity: &str, allowed: &[String]) -> bool {
let id = namespaced_entity(entity);
allowed.iter().any(|a| match a.strip_prefix('#') {
Some(tag) => entity_in_tag(&id, tag),
None => namespaced_entity(a) == id,
})
}
pub fn admitted_entities(allowed: &[String]) -> Vec<String> {
let mut out: BTreeSet<String> = BTreeSet::new();
for a in allowed {
match a.strip_prefix('#') {
Some(tag) => {
out.extend(entity_tags().get(tag).into_iter().flatten().cloned());
}
None => {
out.insert(namespaced_entity(a));
}
}
}
out.into_iter().collect()
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Misfit {
pub unshown: bool,
pub declared_elsewhere: Option<EquipSlot>,
pub excluded_by: Option<Vec<String>>,
}
impl Misfit {
pub fn any(&self) -> bool {
self.unshown || self.declared_elsewhere.is_some() || self.excluded_by.is_some()
}
}
pub fn judge(
entity: &str,
slot: EquipSlot,
item: &str,
items: &dyn ItemRegistry,
) -> Option<Misfit> {
let fact = items.equippable(item);
let kind = fact.kind()?;
let mut m = Misfit {
unshown: !shows(entity, slot, kind),
..Misfit::default()
};
if let EquippableFact::Equippable(e) = fact {
if let Some(declared) = e.declared_slot()
&& !slot.is_hand()
&& declared != slot
{
m.declared_elsewhere = Some(declared);
}
if !e.allowed_entities.is_empty() && !admitted(entity, &e.allowed_entities) {
m.excluded_by = Some(admitted_entities(&e.allowed_entities));
}
}
Some(m)
}
pub fn misfit_message(
what: &str,
entity: &str,
slot: EquipSlot,
item: &str,
kind: PieceKind,
m: &Misfit,
) -> String {
let body = namespaced_entity(entity);
let mut shapes: Vec<String> = Vec::new();
if m.unshown {
let shown = match (body_row(&body), shown_slots_phrase(&body)) {
(None, _) => format!(
"`{body}` is not a living entity of the pinned 1.21.11 registry, so no slot of it \
is ever drawn"
),
(Some(_), None) => format!("`{body}` draws no equipment slot at all"),
(Some(_), Some(p)) => format!("`{body}` draws {p}"),
};
let weapon = if slot.is_hand() {
" A held weapon meant as a number is written as `attributes`, which applies whether \
or not anything is drawn."
} else {
""
};
shapes.push(format!(
"the body does not show it: `{item}` is a piece of kind {} in `{}`, and {shown}. \
Dress another body, or choose a piece this body draws.{weapon}",
kind.name(),
slot.field()
));
}
if let Some(declared) = m.declared_elsewhere {
shapes.push(format!(
"the wrong slot: `{item}` declares the `{}` slot and is written in `{}`. Move it to \
`{}`.",
declared.field(),
slot.field(),
declared.field()
));
}
if let Some(admits) = &m.excluded_by {
shapes.push(format!(
"the wrong body: `{item}` is worn only by {}, and this body is `{body}`. Change the \
entity, or choose a piece `{body}` wears.",
admits
.iter()
.map(|e| format!("`{e}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
format!(
"{what} equipment `{}` puts `{item}` where the pinned game will not show it on this body \
({} shape(s)): {}",
slot.field(),
shapes.len(),
shapes.join(" Also, ")
)
}
pub struct DressedBody<'a> {
pub path: String,
pub what: &'static str,
pub entity: String,
pub equipment: &'a MobEquipment,
}
pub fn dressed_bodies(c: &Campaign) -> Vec<DressedBody<'_>> {
let q = &c.quests.content;
let mut out = Vec::new();
for (i, w) in q.waves.iter().enumerate() {
for (k, m) in w.mobs.iter().enumerate() {
if let Some(eq) = &m.equipment {
out.push(DressedBody {
path: format!("/content/waves/{i}/mobs/{k}/equipment"),
what: "wave-mob",
entity: m.entity.clone(),
equipment: eq,
});
}
}
}
for (i, a) in q.actors.iter().enumerate() {
if let Some(eq) = &a.equipment {
out.push(DressedBody {
path: format!("/content/actors/{i}/equipment"),
what: "actor",
entity: BodyRef::Actor(a).worn_entity().to_string(),
equipment: eq,
});
}
}
out
}
pub fn fit_checks(c: &Campaign, items: &dyn ItemRegistry, d: &mut Vec<Diagnostic>) {
for b in dressed_bodies(c) {
for (slot, piece) in b.equipment.pieces() {
let Some(piece) = piece else { continue };
let item = piece.item();
if !items.contains(item) {
continue; }
let Some(m) = judge(&b.entity, slot, item, items) else {
continue;
};
if !m.any() {
continue;
}
let kind = items
.equippable(item)
.kind()
.expect("judged, so the kind is known");
d.push(Diagnostic::error(
codes::EQUIPMENT_UNSHOWN,
"quests",
format!("{}/{}", b.path, slot.field()),
misfit_message(b.what, &b.entity, slot, item, kind, &m),
));
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EquipmentBinding {
pub bodies: usize,
pub entity_types: usize,
pub pieces: usize,
pub slots: usize,
pub declared_slot: usize,
pub allowed_list: usize,
pub refused: usize,
}
impl EquipmentBinding {
pub fn of(c: &Campaign, items: &dyn ItemRegistry) -> Self {
let mut b = EquipmentBinding::default();
let mut types: BTreeSet<String> = BTreeSet::new();
let mut slots: BTreeSet<EquipSlot> = BTreeSet::new();
for body in dressed_bodies(c) {
b.bodies += 1;
types.insert(namespaced_entity(&body.entity));
for (slot, piece) in body.equipment.pieces() {
let Some(piece) = piece else { continue };
b.pieces += 1;
slots.insert(slot);
let item = piece.item();
if let EquippableFact::Equippable(e) = items.equippable(item) {
b.declared_slot += 1;
if !e.allowed_entities.is_empty() {
b.allowed_list += 1;
}
}
if items.contains(item)
&& judge(&body.entity, slot, item, items).is_some_and(|m| m.any())
{
b.refused += 1;
}
}
}
b.entity_types = types.len();
b.slots = slots.len();
b
}
pub fn line(&self) -> String {
format!(
"equipment binding: {} body(ies) dressed over {} entity type(s), {} piece(s) declared \
over {} slot(s) in use, {} piece(s) with a registry-declared slot, {} with an \
allowed-entity list, {} refused (DW0898).",
self.bodies,
self.entity_types,
self.pieces,
self.slots,
self.declared_slot,
self.allowed_list,
self.refused
)
}
}