use std::collections::{BTreeMap, BTreeSet};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::ids::{PoolId, PrefabId};
pub trait ItemRegistry {
fn contains(&self, item_id: &str) -> bool;
fn max_stack_size(&self, _item_id: &str) -> Option<u32> {
None
}
fn equippable(&self, _item_id: &str) -> crate::equipment::EquippableFact<'_> {
crate::equipment::EquippableFact::Unknown
}
}
pub trait EntityRegistry {
fn contains(&self, entity_id: &str) -> bool;
}
pub fn entity_tags() -> &'static BTreeMap<String, BTreeSet<String>> {
static TAGS: std::sync::LazyLock<BTreeMap<String, BTreeSet<String>>> =
std::sync::LazyLock::new(|| {
let raw = include_str!("../data/entity-tags-1.21.11.json");
let parsed: BTreeMap<String, Vec<String>> =
serde_json::from_str(raw).expect("vendored entity-type tags are valid JSON");
parsed
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
});
&TAGS
}
pub fn namespaced_entity(entity: &str) -> String {
if entity.contains(':') {
entity.to_string()
} else {
format!("minecraft:{entity}")
}
}
pub fn entity_tag_members_bare(tag: &str) -> Vec<&'static str> {
entity_tags()
.get(tag)
.into_iter()
.flatten()
.map(|id| id.strip_prefix("minecraft:").unwrap_or(id))
.collect()
}
pub fn entity_in_tag(entity: &str, tag: &str) -> bool {
let id = namespaced_entity(entity);
entity_tags()
.get(tag)
.is_some_and(|members| members.contains(&id))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum LightingProfile {
Lit,
Dim,
Dark,
Unmeasured,
}
impl LightingProfile {
pub fn is_measurement(self) -> bool {
!matches!(self, LightingProfile::Unmeasured)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Lighting {
pub profile: LightingProfile,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measured_min_light: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub measured: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationale: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
}
impl Lighting {
pub fn unmeasured() -> Lighting {
Lighting {
profile: LightingProfile::Unmeasured,
measured_min_light: None,
measured: None,
rationale: None,
method: None,
}
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLighting {
profile: LightingProfile,
#[serde(default)]
measured_min_light: Option<i64>,
#[serde(default)]
measured: Option<String>,
#[serde(default)]
rationale: Option<String>,
#[serde(default)]
method: Option<String>,
}
impl<'de> Deserialize<'de> for Lighting {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Lighting, D::Error> {
use serde::de::Error as _;
let raw = RawLighting::deserialize(de)?;
let has_measurement = raw.measured_min_light.is_some() && raw.measured.is_some();
if raw.profile.is_measurement() {
if !has_measurement {
return Err(D::Error::custom(
"a `lit`/`dim`/`dark` lighting profile is a measurement and must carry both \
`measured_min_light` and `measured`; declare `\"profile\": \"unmeasured\"` \
if the piece has not been probed yet",
));
}
} else if raw.measured_min_light.is_some() || raw.measured.is_some() {
return Err(D::Error::custom(
"an `unmeasured` lighting profile cannot carry `measured_min_light` or \
`measured` — declare the profile the probe actually found instead",
));
}
Ok(Lighting {
profile: raw.profile,
measured_min_light: raw.measured_min_light,
measured: raw.measured,
rationale: raw.rationale,
method: raw.method,
})
}
}
pub trait AnchorRegistry {
fn anchors_for(&self, prefab: &PrefabId) -> Option<&BTreeSet<String>>;
fn has_pool(&self, _pool: &PoolId) -> bool {
false
}
fn has_prefab(&self, _prefab: &PrefabId) -> Option<bool> {
None
}
fn lighting_for(&self, _prefab: &PrefabId) -> Option<Lighting> {
None
}
fn rig(&self, _rig: &crate::ids::RigId) -> crate::rig::RigLookup<'_> {
crate::rig::RigLookup::Unknown
}
}
#[derive(Debug, Clone)]
pub struct VendoredItemRegistry {
ids: BTreeSet<String>,
}
impl VendoredItemRegistry {
pub fn v1_21_11() -> Self {
let raw = include_str!("../data/items-1.21.11.json");
let ids: Vec<String> =
serde_json::from_str(raw).expect("embedded item registry is valid JSON");
Self {
ids: ids.into_iter().collect(),
}
}
}
impl ItemRegistry for VendoredItemRegistry {
fn contains(&self, item_id: &str) -> bool {
self.ids.contains(item_id)
}
}
#[derive(Debug, Clone)]
pub struct VendoredEntityRegistry {
ids: BTreeSet<String>,
}
impl VendoredEntityRegistry {
pub fn v1_21_11() -> Self {
let raw = include_str!("../data/entities-1.21.11.json");
let ids: Vec<String> =
serde_json::from_str(raw).expect("embedded entity registry is valid JSON");
Self {
ids: ids.into_iter().collect(),
}
}
}
impl EntityRegistry for VendoredEntityRegistry {
fn contains(&self, entity_id: &str) -> bool {
self.ids.contains(entity_id)
}
}
pub trait BlockRegistry {
fn contains(&self, block_id: &str) -> bool;
}
pub const ENCHANTMENT_IDS_1_21_11: &[&str] = &[
"minecraft:aqua_affinity",
"minecraft:bane_of_arthropods",
"minecraft:binding_curse",
"minecraft:blast_protection",
"minecraft:breach",
"minecraft:channeling",
"minecraft:density",
"minecraft:depth_strider",
"minecraft:efficiency",
"minecraft:feather_falling",
"minecraft:fire_aspect",
"minecraft:fire_protection",
"minecraft:flame",
"minecraft:fortune",
"minecraft:frost_walker",
"minecraft:impaling",
"minecraft:infinity",
"minecraft:knockback",
"minecraft:looting",
"minecraft:loyalty",
"minecraft:luck_of_the_sea",
"minecraft:lunge",
"minecraft:lure",
"minecraft:mending",
"minecraft:multishot",
"minecraft:piercing",
"minecraft:power",
"minecraft:projectile_protection",
"minecraft:protection",
"minecraft:punch",
"minecraft:quick_charge",
"minecraft:respiration",
"minecraft:riptide",
"minecraft:sharpness",
"minecraft:silk_touch",
"minecraft:smite",
"minecraft:soul_speed",
"minecraft:sweeping_edge",
"minecraft:swift_sneak",
"minecraft:thorns",
"minecraft:unbreaking",
"minecraft:vanishing_curse",
"minecraft:wind_burst",
];
pub trait EnchantmentRegistry {
fn contains(&self, enchantment_id: &str) -> bool;
}
#[derive(Debug, Clone)]
pub struct VendoredEnchantmentRegistry {
ids: BTreeSet<&'static str>,
}
impl VendoredEnchantmentRegistry {
pub fn v1_21_11() -> Self {
Self {
ids: ENCHANTMENT_IDS_1_21_11.iter().copied().collect(),
}
}
}
impl Default for VendoredEnchantmentRegistry {
fn default() -> Self {
Self::v1_21_11()
}
}
impl EnchantmentRegistry for VendoredEnchantmentRegistry {
fn contains(&self, enchantment_id: &str) -> bool {
let norm = if enchantment_id.contains(':') {
enchantment_id.to_string()
} else {
format!("minecraft:{enchantment_id}")
};
self.ids.contains(norm.as_str())
}
}
pub const POTION_IDS_1_21_11: &[&str] = &[
"minecraft:awkward",
"minecraft:fire_resistance",
"minecraft:harming",
"minecraft:healing",
"minecraft:infested",
"minecraft:invisibility",
"minecraft:leaping",
"minecraft:long_fire_resistance",
"minecraft:long_invisibility",
"minecraft:long_leaping",
"minecraft:long_night_vision",
"minecraft:long_poison",
"minecraft:long_regeneration",
"minecraft:long_slow_falling",
"minecraft:long_slowness",
"minecraft:long_strength",
"minecraft:long_swiftness",
"minecraft:long_turtle_master",
"minecraft:long_water_breathing",
"minecraft:long_weakness",
"minecraft:luck",
"minecraft:mundane",
"minecraft:night_vision",
"minecraft:oozing",
"minecraft:poison",
"minecraft:regeneration",
"minecraft:slow_falling",
"minecraft:slowness",
"minecraft:strength",
"minecraft:strong_harming",
"minecraft:strong_healing",
"minecraft:strong_leaping",
"minecraft:strong_poison",
"minecraft:strong_regeneration",
"minecraft:strong_slowness",
"minecraft:strong_strength",
"minecraft:strong_swiftness",
"minecraft:strong_turtle_master",
"minecraft:swiftness",
"minecraft:thick",
"minecraft:turtle_master",
"minecraft:water",
"minecraft:water_breathing",
"minecraft:weakness",
"minecraft:weaving",
"minecraft:wind_charged",
];
pub fn is_potion_id(id: &str) -> bool {
let norm = if id.contains(':') {
id.to_string()
} else {
format!("minecraft:{id}")
};
POTION_IDS_1_21_11.contains(&norm.as_str())
}
pub trait EffectRegistry {
fn contains(&self, effect_id: &str) -> bool;
}
pub const EFFECT_IDS_1_21_11: &[&str] = &[
"minecraft:speed",
"minecraft:slowness",
"minecraft:haste",
"minecraft:mining_fatigue",
"minecraft:strength",
"minecraft:instant_health",
"minecraft:instant_damage",
"minecraft:jump_boost",
"minecraft:nausea",
"minecraft:regeneration",
"minecraft:resistance",
"minecraft:fire_resistance",
"minecraft:water_breathing",
"minecraft:invisibility",
"minecraft:blindness",
"minecraft:night_vision",
"minecraft:hunger",
"minecraft:weakness",
"minecraft:poison",
"minecraft:wither",
"minecraft:health_boost",
"minecraft:absorption",
"minecraft:saturation",
"minecraft:glowing",
"minecraft:levitation",
"minecraft:luck",
"minecraft:unluck",
"minecraft:slow_falling",
"minecraft:conduit_power",
"minecraft:dolphins_grace",
"minecraft:bad_omen",
"minecraft:hero_of_the_village",
"minecraft:darkness",
"minecraft:trial_omen",
"minecraft:raid_omen",
"minecraft:wind_charged",
"minecraft:weaving",
"minecraft:oozing",
"minecraft:infested",
"minecraft:breath_of_the_nautilus",
];
pub const TECHNICAL_BLOCK_IDS: &[&str] = &[
"minecraft:air",
"minecraft:cave_air",
"minecraft:void_air",
"minecraft:water",
"minecraft:lava",
];
pub fn namespaced_effect_id(id: &str) -> String {
if id.contains(':') {
id.to_string()
} else {
format!("minecraft:{id}")
}
}
pub fn is_technical_block(id: &str) -> bool {
let norm = if id.contains(':') {
id.to_string()
} else {
format!("minecraft:{id}")
};
TECHNICAL_BLOCK_IDS.contains(&norm.as_str())
}
pub struct ItemBackedBlockRegistry<'a> {
items: &'a dyn ItemRegistry,
}
impl<'a> ItemBackedBlockRegistry<'a> {
pub fn new(items: &'a dyn ItemRegistry) -> Self {
Self { items }
}
}
impl BlockRegistry for ItemBackedBlockRegistry<'_> {
fn contains(&self, block_id: &str) -> bool {
is_technical_block(block_id) || self.items.contains(block_id)
}
}
#[derive(Debug, Clone)]
pub struct VendoredEffectRegistry {
ids: BTreeSet<&'static str>,
}
impl VendoredEffectRegistry {
pub fn v1_21_11() -> Self {
Self {
ids: EFFECT_IDS_1_21_11.iter().copied().collect(),
}
}
}
impl Default for VendoredEffectRegistry {
fn default() -> Self {
Self::v1_21_11()
}
}
impl EffectRegistry for VendoredEffectRegistry {
fn contains(&self, effect_id: &str) -> bool {
self.ids.contains(namespaced_effect_id(effect_id).as_str())
}
}
#[derive(Debug, Clone)]
pub struct VendoredAnchorRegistry {
by_prefab: BTreeMap<String, BTreeSet<String>>,
pools: BTreeSet<String>,
}
impl VendoredAnchorRegistry {
pub fn hello_world() -> Self {
let raw = include_str!("../data/anchors.json");
let by_prefab: BTreeMap<String, BTreeSet<String>> =
serde_json::from_str(raw).expect("embedded anchor metadata is valid JSON");
let pools_raw = include_str!("../data/pools.json");
let pools: BTreeSet<String> =
serde_json::from_str(pools_raw).expect("embedded pool metadata is valid JSON");
Self { by_prefab, pools }
}
}
impl AnchorRegistry for VendoredAnchorRegistry {
fn anchors_for(&self, prefab: &PrefabId) -> Option<&BTreeSet<String>> {
self.by_prefab.get(prefab.as_str())
}
fn has_pool(&self, pool: &PoolId) -> bool {
self.pools.contains(pool.as_str())
}
fn has_prefab(&self, prefab: &PrefabId) -> Option<bool> {
Some(self.by_prefab.contains_key(prefab.as_str()))
}
}
#[cfg(test)]
mod lighting_tests {
use super::*;
#[test]
fn a_measured_profile_still_cannot_omit_its_measurement() {
for json in [
r#"{ "profile": "lit" }"#,
r#"{ "profile": "lit", "measured_min_light": 9 }"#,
r#"{ "profile": "lit", "measured": "2026-07-30" }"#,
r#"{ "profile": "dim", "measured": "2026-07-30" }"#,
r#"{ "profile": "dark", "measured_min_light": 1 }"#,
] {
let err = serde_json::from_str::<Lighting>(json).unwrap_err();
assert!(
err.to_string().contains("must carry both"),
"{json} was accepted or misreported: {err}"
);
}
}
#[test]
fn an_unmeasured_profile_cannot_quote_a_measurement() {
for json in [
r#"{ "profile": "unmeasured", "measured_min_light": 9 }"#,
r#"{ "profile": "unmeasured", "measured": "2026-07-30" }"#,
r#"{ "profile": "unmeasured", "measured_min_light": 9, "measured": "2026-07-30" }"#,
] {
let err = serde_json::from_str::<Lighting>(json).unwrap_err();
assert!(err.to_string().contains("cannot carry"), "{json}: {err}");
}
}
#[test]
fn both_honest_declarations_round_trip() {
let measured: Lighting = serde_json::from_str(
r#"{ "profile": "lit", "measured_min_light": 8, "measured": "2026-07-30",
"method": "live 1.21.11 probe" }"#,
)
.unwrap();
assert_eq!(measured.profile, LightingProfile::Lit);
assert_eq!(measured.measured_min_light, Some(8));
assert!(measured.profile.is_measurement());
let unmeasured: Lighting = serde_json::from_str(r#"{ "profile": "unmeasured" }"#).unwrap();
assert_eq!(unmeasured, Lighting::unmeasured());
assert!(!unmeasured.profile.is_measurement());
for l in [measured, unmeasured] {
let text = serde_json::to_string(&l).unwrap();
assert!(!text.contains("null"), "{text}");
assert_eq!(serde_json::from_str::<Lighting>(&text).unwrap(), l);
}
}
#[test]
fn an_unknown_profile_is_still_refused() {
assert!(serde_json::from_str::<Lighting>(r#"{ "profile": "unknown" }"#).is_err());
assert!(
serde_json::from_str::<Lighting>(
r#"{ "profile": "lit", "measured_min_light": 8, "measured": "x", "extra": 1 }"#
)
.is_err(),
"deny_unknown_fields must survive the hand-written Deserialize"
);
}
}