use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use crate::nbt::NbtMap;
use super::types::{MapExt, ValueExt};
mod data;
pub const FILTER_ME: &str = "%%FILTER_ME%%";
#[derive(Clone, Copy)]
pub struct FlatState {
pub name: &'static str,
pub props: &'static [(&'static str, &'static str)],
}
impl FlatState {
pub fn to_nbt(&self) -> NbtMap {
let mut m = NbtMap::new();
m.set_string("Name", self.name);
if !self.props.is_empty() {
let mut props = NbtMap::new();
for (k, v) in self.props {
props.set_string(k, *v);
}
m.set_map("Properties", props);
}
m
}
}
pub struct Registration {
pub id: u16,
pub flat: FlatState,
pub pres: &'static [FlatState],
}
fn canon_key(name: &str, props: &[(&str, &str)]) -> String {
let mut sorted: Vec<(&str, &str)> = props.to_vec();
sorted.sort_unstable();
let mut s = String::with_capacity(name.len() + props.len() * 8);
s.push_str(name);
for (k, v) in sorted {
s.push('\u{1}');
s.push_str(k);
s.push('\u{2}');
s.push_str(v);
}
s
}
fn canon_key_from_map(m: &NbtMap) -> Option<String> {
let mut name: Option<&str> = None;
let mut props: Vec<(&str, &str)> = Vec::new();
for (k, v) in m.iter() {
match k.as_str() {
"Name" => name = Some(v.as_str()?),
"Properties" => {
for (pk, pv) in v.as_compound_ref()?.iter() {
props.push((pk.as_str(), pv.as_str()?));
}
}
_ => return None,
}
}
Some(canon_key(name?, &props))
}
struct Tables {
flattened_by_id: Vec<Option<FlatState>>,
id_by_old_nbt: HashMap<String, u16>,
id_by_old_name: HashMap<&'static str, u16>,
pre_by_flat: HashMap<String, &'static [FlatState]>,
variants_by_flat_name: HashMap<
&'static str,
Vec<(
&'static [(&'static str, &'static str)],
&'static [FlatState],
)>,
>,
old_name_by_new: HashMap<&'static str, &'static str>,
}
static TABLES: LazyLock<Tables> = LazyLock::new(|| {
let mut flattened_by_id: Vec<Option<FlatState>> = vec![None; 4096];
let mut block_defaults: Vec<Option<FlatState>> = vec![None; 256];
let mut id_by_old_nbt: HashMap<String, u16> = HashMap::new();
let mut id_by_old_name: HashMap<&'static str, u16> = HashMap::new();
let mut pre_by_flat: HashMap<String, &'static [FlatState]> = HashMap::new();
let mut variants_by_flat_name: HashMap<
&'static str,
Vec<(
&'static [(&'static str, &'static str)],
&'static [FlatState],
)>,
> = HashMap::new();
let mut old_name_by_new: HashMap<&'static str, &'static str> = HashMap::new();
for r in data::REGISTRATIONS {
flattened_by_id[r.id as usize] = Some(r.flat);
let block = (r.id >> 4) as usize;
if block_defaults[block].is_none() {
block_defaults[block] = Some(r.flat);
}
for pre in r.pres {
id_by_old_name.entry(pre.name).or_insert(r.id);
id_by_old_nbt.insert(canon_key(pre.name, pre.props), r.id);
}
if let Some(first) = r.pres.first() {
pre_by_flat
.entry(canon_key(r.flat.name, r.flat.props))
.or_insert(r.pres);
variants_by_flat_name
.entry(r.flat.name)
.or_default()
.push((r.flat.props, r.pres));
old_name_by_new.entry(r.flat.name).or_insert(first.name);
}
}
for i in 0..4096 {
if flattened_by_id[i].is_none() {
flattened_by_id[i] = block_defaults[i >> 4];
}
}
for variants in variants_by_flat_name.values_mut() {
variants.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
}
Tables {
flattened_by_id,
id_by_old_nbt,
id_by_old_name,
pre_by_flat,
variants_by_flat_name,
old_name_by_new,
}
});
fn nbt_for_id_raw(block: i32) -> Option<FlatState> {
if block >= 0 && (block as usize) < 4096 {
TABLES.flattened_by_id[block as usize]
} else {
None
}
}
pub fn flatten_nbt(old: &NbtMap) -> Option<NbtMap> {
let key = canon_key_from_map(old)?;
let id = *TABLES.id_by_old_nbt.get(&key)?;
nbt_for_id_raw(id as i32).map(|fs| fs.to_nbt())
}
pub fn get_new_block_name(old: &str) -> String {
match TABLES.id_by_old_name.get(old) {
Some(&id) => nbt_for_id_raw(id as i32)
.map(|fs| fs.name.to_string())
.unwrap_or_else(|| old.to_string()),
None => old.to_string(),
}
}
pub fn get_name_for_id(block: i32) -> String {
match nbt_for_id_raw(block) {
Some(fs) => fs.name.to_string(),
None => "minecraft:air".to_string(),
}
}
pub fn get_nbt_for_id(block: i32) -> NbtMap {
match nbt_for_id_raw(block) {
Some(fs) => fs.to_nbt(),
None => nbt_for_id_raw(0)
.expect("id 0 (air) is always registered")
.to_nbt(),
}
}
pub enum Unflatten {
Exact(NbtMap),
Approximated(NbtMap),
Unknown,
}
pub fn unflatten_nbt(modern: &NbtMap) -> Unflatten {
let modern_props: Vec<(&str, &str)> = match modern.get_map("Properties") {
Some(p) => p
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.as_str(), s)))
.collect(),
None => Vec::new(),
};
if let Some(key) = canon_key_from_map(modern) {
if let Some(pres) = TABLES.pre_by_flat.get(&key) {
return Unflatten::Exact(best_pre(pres, &modern_props).to_nbt());
}
}
let name = match modern.get_string("Name") {
Some(n) => n,
None => return Unflatten::Unknown,
};
if let Some(variants) = TABLES.variants_by_flat_name.get(name) {
for (flat_props, pres) in variants {
let is_subset = flat_props
.iter()
.all(|(k, v)| modern_props.iter().any(|(mk, mv)| mk == k && mv == v));
if is_subset {
return Unflatten::Approximated(best_pre(pres, &modern_props).to_nbt());
}
}
}
Unflatten::Unknown
}
fn best_pre(pres: &'static [FlatState], modern_props: &[(&str, &str)]) -> &'static FlatState {
let score = |pre: &FlatState| -> usize {
pre.props
.iter()
.filter(|(k, v)| modern_props.iter().any(|(mk, mv)| mk == k && mv == v))
.count()
};
let mut best = &pres[0];
let mut best_score = score(best);
for pre in &pres[1..] {
let s = score(pre);
if s > best_score {
best = pre;
best_score = s;
}
}
best
}
pub fn get_old_block_name(new: &str) -> String {
TABLES
.old_name_by_new
.get(new)
.map(|s| s.to_string())
.unwrap_or_else(|| new.to_string())
}
pub fn flat_to_numeric(modern: &NbtMap) -> Option<i32> {
let old = match unflatten_nbt(modern) {
Unflatten::Exact(o) | Unflatten::Approximated(o) => o,
Unflatten::Unknown => return None,
};
let key = canon_key_from_map(&old)?;
TABLES.id_by_old_nbt.get(&key).map(|&id| id as i32)
}
static FLATTEN_ITEM: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| data::FLATTEN_ITEM_MAP.iter().copied().collect());
static IDS_REQUIRING_FLATTENING: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
data::FLATTEN_ITEM_MAP
.iter()
.map(|(k, _)| &k[..k.find('.').expect("FLATTEN_MAP key has a '.'")])
.collect()
});
static ITEMS_WITH_DAMAGE: LazyLock<HashSet<&'static str>> =
LazyLock::new(|| data::ITEMS_WITH_DAMAGE.iter().copied().collect());
pub fn flatten_item(old_name: &str, data: i32) -> Option<&'static str> {
if !IDS_REQUIRING_FLATTENING.contains(old_name) {
return None;
}
let exact = FLATTEN_ITEM
.get(format!("{old_name}.{data}").as_str())
.copied();
exact.or_else(|| FLATTEN_ITEM.get(format!("{old_name}.0").as_str()).copied())
}
static UNFLATTEN_ITEM: LazyLock<HashMap<&'static str, (&'static str, i32)>> = LazyLock::new(|| {
let mut m: HashMap<&'static str, (&'static str, i32)> = HashMap::new();
for (key, new_id) in data::FLATTEN_ITEM_MAP {
if let Some((old_id, data_str)) = key.rsplit_once('.') {
if let Ok(data) = data_str.parse::<i32>() {
m.entry(new_id).or_insert((old_id, data));
}
}
}
m
});
pub fn unflatten_item(new_id: &str) -> Option<(&'static str, i32)> {
UNFLATTEN_ITEM.get(new_id).copied()
}
pub fn id_requires_flattening(id: &str) -> bool {
IDS_REQUIRING_FLATTENING.contains(id)
}
pub fn item_has_damage(id: &str) -> bool {
ITEMS_WITH_DAMAGE.contains(id)
}
static SPAWN_EGG: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| data::SPAWN_EGG_MAP.iter().copied().collect());
pub fn spawn_egg_for_entity(id: &str) -> &'static str {
SPAWN_EGG
.get(id)
.copied()
.unwrap_or("minecraft:pig_spawn_egg")
}
static ENTITY_BY_SPAWN_EGG: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
let mut m: HashMap<&'static str, &'static str> = HashMap::new();
for (entity, egg) in data::SPAWN_EGG_MAP {
m.entry(*egg).or_insert(*entity);
}
m
});
pub fn entity_for_spawn_egg(egg_id: &str) -> Option<&'static str> {
ENTITY_BY_SPAWN_EGG.get(egg_id).copied()
}
static ENTITY_BLOCK_NAME_TO_ID: LazyLock<HashMap<&'static str, i32>> =
LazyLock::new(|| data::ENTITY_BLOCK_NAME_TO_ID.iter().copied().collect());
pub fn entity_block_id(name: &str) -> i32 {
ENTITY_BLOCK_NAME_TO_ID.get(name).copied().unwrap_or(0)
}
static ITEM_NAMES_BY_ID: LazyLock<HashMap<i32, &'static str>> =
LazyLock::new(|| data::ITEM_NAMES_BY_ID.iter().copied().collect());
pub fn get_name_from_id(id: i32) -> Option<&'static str> {
ITEM_NAMES_BY_ID.get(&id).copied()
}
static ID_BY_ITEM_NAME: LazyLock<HashMap<&'static str, i32>> = LazyLock::new(|| {
let mut m: HashMap<&'static str, i32> = HashMap::new();
for (id, name) in data::ITEM_NAMES_BY_ID {
m.entry(*name).or_insert(*id);
}
m
});
pub fn id_from_item_name(name: &str) -> Option<i32> {
ID_BY_ITEM_NAME.get(name).copied()
}
static POTION_NAMES_BY_ID: LazyLock<HashMap<i32, &'static str>> =
LazyLock::new(|| data::POTION_NAMES_BY_ID.iter().copied().collect());
pub fn get_potion_name_from_id(id: i16) -> Option<&'static str> {
POTION_NAMES_BY_ID.get(&((id & 127) as i32)).copied()
}
static SPAWN_EGG_NAME_BY_ID: LazyLock<HashMap<i32, &'static str>> =
LazyLock::new(|| data::SPAWN_EGG_NAME_BY_ID.iter().copied().collect());
pub fn get_spawn_name_from_id(id: i16) -> Option<&'static str> {
SPAWN_EGG_NAME_BY_ID.get(&((id & 255) as i32)).copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_state_flatten_and_default() {
let mut stone = NbtMap::new();
stone.set_string("Name", "minecraft:stone");
let mut props = NbtMap::new();
props.set_string("variant", "granite");
stone.set_map("Properties", props);
let out = flatten_nbt(&stone).expect("granite flattens");
assert_eq!(out.get_string("Name"), Some("minecraft:granite"));
assert!(out.get_map("Properties").is_none());
let mut weird = NbtMap::new();
weird.set_string("Name", "minecraft:not_a_block");
assert!(flatten_nbt(&weird).is_none());
}
#[test]
fn numeric_id_lookups() {
assert_eq!(get_name_for_id(16), "minecraft:stone");
assert_eq!(get_name_for_id(17), "minecraft:granite");
assert_eq!(get_name_for_id(99999), "minecraft:air");
assert_eq!(get_name_for_id(-1), "minecraft:air");
assert_eq!(get_nbt_for_id(-1).get_string("Name"), Some("minecraft:air"));
assert_eq!(
get_nbt_for_id(128).get_string("Name"),
Some("minecraft:water")
);
}
#[test]
fn block_default_fill() {
assert_eq!(get_name_for_id(31), "minecraft:stone");
}
#[test]
fn skull_uses_filter_me_placeholder() {
assert_eq!(get_name_for_id(2304), FILTER_ME);
}
#[test]
fn name_lookup() {
assert_eq!(
get_new_block_name("minecraft:grass"),
"minecraft:grass_block"
);
assert_eq!(
get_new_block_name("minecraft:unknown_zzz"),
"minecraft:unknown_zzz"
);
}
#[test]
fn item_flatten() {
assert_eq!(
flatten_item("minecraft:wool", 14),
Some("minecraft:red_wool")
);
assert_eq!(
flatten_item("minecraft:wool", 99),
Some("minecraft:white_wool")
); assert_eq!(flatten_item("minecraft:diamond_sword", 0), None); assert!(item_has_damage("minecraft:diamond_sword"));
assert!(!item_has_damage("minecraft:wool"));
}
#[test]
fn spawn_egg_and_entity_block_id() {
assert_eq!(
spawn_egg_for_entity("minecraft:creeper"),
"minecraft:creeper_spawn_egg"
);
assert_eq!(
spawn_egg_for_entity("minecraft:unknown"),
"minecraft:pig_spawn_egg"
);
assert_eq!(entity_block_id("minecraft:chest"), 54);
assert_eq!(entity_block_id("minecraft:unknown"), 0);
}
#[test]
fn unflatten_exact_round_trips_forward() {
let mut stone = NbtMap::new();
stone.set_string("Name", "minecraft:stone");
let mut props = NbtMap::new();
props.set_string("variant", "granite");
stone.set_map("Properties", props);
let flat = flatten_nbt(&stone).expect("granite flattens");
match unflatten_nbt(&flat) {
Unflatten::Exact(old) => {
assert_eq!(old.get_string("Name"), Some("minecraft:stone"));
assert_eq!(
old.get_map("Properties").unwrap().get_string("variant"),
Some("granite")
);
}
_ => panic!("granite should reverse exactly"),
}
}
#[test]
fn unflatten_drops_modern_only_props_via_subset() {
let mut stairs = NbtMap::new();
stairs.set_string("Name", "minecraft:oak_stairs");
let mut props = NbtMap::new();
props.set_string("facing", "east");
props.set_string("half", "bottom");
props.set_string("shape", "straight");
props.set_string("waterlogged", "false");
stairs.set_map("Properties", props);
match unflatten_nbt(&stairs) {
Unflatten::Exact(old) | Unflatten::Approximated(old) => {
assert_eq!(old.get_string("Name"), Some("minecraft:oak_stairs"));
let p = old
.get_map("Properties")
.expect("stairs keep facing/half/shape");
assert_eq!(p.get_string("facing"), Some("east"));
assert_eq!(p.get_string("half"), Some("bottom"));
assert_eq!(p.get_string("shape"), Some("straight"));
assert!(
p.get_string("waterlogged").is_none(),
"modern-only waterlogged dropped"
);
}
Unflatten::Unknown => panic!("oak_stairs should reverse"),
}
}
#[test]
fn unflatten_unknown_modern_block() {
let mut blackstone = NbtMap::new();
blackstone.set_string("Name", "minecraft:blackstone");
assert!(matches!(unflatten_nbt(&blackstone), Unflatten::Unknown));
}
#[test]
fn unflatten_item_inverts_subtype() {
assert_eq!(
flatten_item("minecraft:wool", 14),
Some("minecraft:red_wool")
);
assert_eq!(
unflatten_item("minecraft:red_wool"),
Some(("minecraft:wool", 14))
);
assert_eq!(unflatten_item("minecraft:diamond_sword"), None);
}
}