use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fmt::Write as _;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use serde_json::{Map, Value};
use ussr_nbt::owned::{Compound, List, Nbt, Tag};
const VERSIONS: &[&str] = &["26.1.2"];
const REGISTRIES: &[(&str, &str)] = &[
("minecraft:damage_type", "damage_type"),
("minecraft:dimension_type", "dimension_type"),
("minecraft:painting_variant", "painting_variant"),
("minecraft:worldgen/biome", "worldgen/biome"),
("minecraft:cat_variant", "cat_variant"),
("minecraft:cat_sound_variant", "cat_sound_variant"),
("minecraft:chicken_variant", "chicken_variant"),
("minecraft:chicken_sound_variant", "chicken_sound_variant"),
("minecraft:cow_variant", "cow_variant"),
("minecraft:cow_sound_variant", "cow_sound_variant"),
("minecraft:frog_variant", "frog_variant"),
("minecraft:pig_variant", "pig_variant"),
("minecraft:pig_sound_variant", "pig_sound_variant"),
("minecraft:wolf_variant", "wolf_variant"),
("minecraft:wolf_sound_variant", "wolf_sound_variant"),
(
"minecraft:zombie_nautilus_variant",
"zombie_nautilus_variant",
),
("minecraft:timeline", "timeline"),
("minecraft:world_clock", "world_clock"),
("minecraft:trim_material", "trim_material"),
("minecraft:trim_pattern", "trim_pattern"),
("minecraft:banner_pattern", "banner_pattern"),
("minecraft:instrument", "instrument"),
("minecraft:chat_type", "chat_type"),
("minecraft:jukebox_song", "jukebox_song"),
];
const TAG_REGISTRIES: &[(&str, &str)] = &[
("minecraft:damage_type", "tags/damage_type"),
("minecraft:painting_variant", "tags/painting_variant"),
("minecraft:timeline", "tags/timeline"),
("minecraft:worldgen/biome", "tags/worldgen/biome"),
("minecraft:banner_pattern", "tags/banner_pattern"),
("minecraft:instrument", "tags/instrument"),
];
fn main() {
println!("cargo:rerun-if-changed=assets");
println!("cargo:rerun-if-changed=build.rs");
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"));
let nbt_root = out_dir.join("nbt");
let mut codegen = String::new();
codegen.push_str("// @generated by build.rs — do not edit\n\n");
codegen.push_str("pub static REGISTRIES: &[(&str, &[(&str, &[(&str, &[u8])])])] = &[\n");
for version in VERSIONS {
codegen.push_str(&format!(" ({version:?}, &[\n"));
for (registry_id, subpath) in REGISTRIES {
let src_dir = crate_dir.join("assets").join(version).join(subpath);
if !src_dir.is_dir() {
panic!("missing asset directory: {}", src_dir.display());
}
let mut entries: BTreeMap<String, PathBuf> = BTreeMap::new();
collect_jsons(&src_dir, &src_dir, &mut entries);
codegen.push_str(&format!(" ({registry_id:?}, &[\n"));
for (entry_id, json_path) in &entries {
let json_text = fs::read_to_string(json_path)
.unwrap_or_else(|e| panic!("read {}: {e}", json_path.display()));
let value: Value = serde_json::from_str(&json_text)
.unwrap_or_else(|e| panic!("parse {}: {e}", json_path.display()));
let nbt = json_to_nbt(&value, registry_id);
let nbt_rel = Path::new("nbt")
.join(version)
.join(subpath)
.join(format!("{entry_id}.nbt"));
let nbt_path = out_dir.join(&nbt_rel);
fs::create_dir_all(nbt_path.parent().unwrap()).unwrap();
let mut buf = Vec::new();
nbt.write(&mut buf).unwrap();
let mut f = fs::File::create(&nbt_path).unwrap();
f.write_all(&buf).unwrap();
let full_id = format!("minecraft:{entry_id}");
let include_path = nbt_rel.to_string_lossy().replace('\\', "/");
codegen.push_str(&format!(
" ({full_id:?}, include_bytes!({include_path:?})),\n"
));
}
codegen.push_str(" ]),\n");
}
codegen.push_str(" ]),\n");
}
codegen.push_str("];\n\n");
codegen.push_str("pub static TAGS: &[(&str, &[(&str, &[(&str, &[&str])])])] = &[\n");
for version in VERSIONS {
let _ = writeln!(codegen, " ({version:?}, &[");
for (registry_id, subpath) in TAG_REGISTRIES {
let src_dir = crate_dir.join("assets").join(version).join(subpath);
if !src_dir.is_dir() {
continue;
}
let mut raw: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut paths: BTreeMap<String, PathBuf> = BTreeMap::new();
collect_jsons(&src_dir, &src_dir, &mut paths);
for (tag_short_id, json_path) in &paths {
let json_text = fs::read_to_string(json_path)
.unwrap_or_else(|e| panic!("read {}: {e}", json_path.display()));
let value: Value = serde_json::from_str(&json_text)
.unwrap_or_else(|e| panic!("parse {}: {e}", json_path.display()));
let values = parse_tag_values(&value, json_path);
let full_tag_id = format!("minecraft:{tag_short_id}");
raw.insert(full_tag_id, values);
}
let _ = writeln!(codegen, " ({registry_id:?}, &[");
for tag_id in raw.keys() {
let resolved = resolve_tag(tag_id, &raw, &mut HashSet::new());
let _ = write!(codegen, " ({tag_id:?}, &[");
for (i, entry_id) in resolved.iter().enumerate() {
if i > 0 {
codegen.push_str(", ");
}
let _ = write!(codegen, "{entry_id:?}");
}
codegen.push_str("]),\n");
}
codegen.push_str(" ]),\n");
}
codegen.push_str(" ]),\n");
}
codegen.push_str("];\n");
let out_file = out_dir.join("registries.rs");
fs::write(&out_file, codegen).unwrap();
drop(nbt_root);
}
fn parse_tag_values(value: &Value, path: &Path) -> Vec<String> {
let arr = value
.get("values")
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("tag {} has no values array", path.display()));
arr.iter()
.map(|v| match v {
Value::String(s) => s.clone(),
Value::Object(o) => o
.get("id")
.and_then(|v| v.as_str())
.unwrap_or_else(|| panic!("tag {} object value missing id", path.display()))
.to_string(),
_ => panic!("tag {} has non-string value: {v}", path.display()),
})
.collect()
}
fn resolve_tag(
tag_id: &str,
all_tags: &BTreeMap<String, Vec<String>>,
seen: &mut HashSet<String>,
) -> Vec<String> {
if !seen.insert(tag_id.to_string()) {
return Vec::new();
}
let mut out: BTreeSet<String> = BTreeSet::new();
if let Some(values) = all_tags.get(tag_id) {
for v in values {
if let Some(referenced) = v.strip_prefix('#') {
for inner in resolve_tag(referenced, all_tags, seen) {
out.insert(inner);
}
} else {
out.insert(v.clone());
}
}
}
seen.remove(tag_id);
out.into_iter().collect()
}
fn collect_jsons(root: &Path, dir: &Path, out: &mut BTreeMap<String, PathBuf>) {
for entry in fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
collect_jsons(root, &path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("json") {
let rel = path.strip_prefix(root).unwrap().with_extension("");
let id = rel.to_string_lossy().replace('\\', "/");
out.insert(id, path);
}
}
}
#[derive(Clone)]
#[allow(dead_code)]
enum Hint {
Auto,
Byte,
Int,
Long,
Float,
Double,
String,
List(Box<Hint>),
Compound(&'static [(&'static str, &'static Hint)]),
}
fn json_to_nbt(value: &Value, registry: &str) -> Nbt {
let hint = root_hint(registry);
let tag = convert(value, hint);
let compound = match tag {
Tag::Compound(c) => c,
_ => panic!("registry {registry} root is not a compound"),
};
Nbt {
name: Default::default(),
compound,
}
}
fn root_hint(registry: &str) -> &'static Hint {
match registry {
"minecraft:dimension_type" => &DIMENSION_TYPE_HINT,
_ => &Hint::Auto,
}
}
static DT_MOOD: Hint = Hint::Compound(&[("offset", &Hint::Double)]);
static DT_AMBIENT_SOUNDS: Hint = Hint::Compound(&[("mood", &DT_MOOD)]);
static DT_ATTRIBUTES: Hint =
Hint::Compound(&[("minecraft:audio/ambient_sounds", &DT_AMBIENT_SOUNDS)]);
static DIMENSION_TYPE_HINT: Hint = Hint::Compound(&[
("coordinate_scale", &Hint::Double),
("fixed_time", &Hint::Long),
("attributes", &DT_ATTRIBUTES),
]);
fn convert(value: &Value, hint: &Hint) -> Tag {
match (hint, value) {
(Hint::Auto, v) => convert_auto(v),
(Hint::Byte, Value::Bool(b)) => Tag::Byte(if *b { 1 } else { 0 }),
(Hint::Byte, Value::Number(n)) => Tag::Byte(n.as_i64().unwrap_or(0) as u8),
(Hint::Int, Value::Number(n)) => Tag::Int(n.as_i64().unwrap_or(0) as i32),
(Hint::Long, Value::Number(n)) => Tag::Long(n.as_i64().unwrap_or(0)),
(Hint::Float, Value::Number(n)) => Tag::Float(n.as_f64().unwrap_or(0.0) as f32),
(Hint::Double, Value::Number(n)) => Tag::Double(n.as_f64().unwrap_or(0.0)),
(Hint::String, Value::String(s)) => Tag::String(s.clone().into()),
(Hint::List(inner), Value::Array(arr)) => Tag::List(convert_list(arr, inner)),
(Hint::Compound(fields), Value::Object(obj)) => {
Tag::Compound(convert_compound(obj, fields))
}
_ => convert_auto(value),
}
}
fn convert_auto(value: &Value) -> Tag {
match value {
Value::Bool(b) => Tag::Byte(if *b { 1 } else { 0 }),
Value::Number(n) => {
if n.is_i64() && !has_decimal(n) {
let i = n.as_i64().unwrap();
if (i32::MIN as i64..=i32::MAX as i64).contains(&i) {
Tag::Int(i as i32)
} else {
Tag::Long(i)
}
} else {
Tag::Float(n.as_f64().unwrap_or(0.0) as f32)
}
}
Value::String(s) => Tag::String(s.clone().into()),
Value::Array(arr) => Tag::List(convert_list_auto(arr)),
Value::Object(obj) => Tag::Compound(convert_compound_auto(obj)),
Value::Null => Tag::Byte(0),
}
}
fn has_decimal(n: &serde_json::Number) -> bool {
!n.is_i64() && !n.is_u64()
}
fn convert_compound(obj: &Map<String, Value>, fields: &[(&str, &Hint)]) -> Compound {
let tags = obj
.iter()
.map(|(k, v)| {
let hint = fields
.iter()
.find(|(name, _)| *name == k)
.map(|(_, h)| *h)
.unwrap_or(&Hint::Auto);
(k.clone().into(), convert(v, hint))
})
.collect();
Compound { tags }
}
fn convert_compound_auto(obj: &Map<String, Value>) -> Compound {
let tags = obj
.iter()
.map(|(k, v)| (k.clone().into(), convert_auto(v)))
.collect();
Compound { tags }
}
fn convert_list(arr: &[Value], inner: &Hint) -> List {
if arr.is_empty() {
return List::Empty;
}
let tags: Vec<Tag> = arr.iter().map(|v| convert(v, inner)).collect();
pack_list(tags)
}
fn convert_list_auto(arr: &[Value]) -> List {
if arr.is_empty() {
return List::Empty;
}
let tags: Vec<Tag> = arr.iter().map(convert_auto).collect();
pack_list(tags)
}
fn pack_list(tags: Vec<Tag>) -> List {
let first = &tags[0];
let kind = std::mem::discriminant(first);
if tags.iter().any(|t| std::mem::discriminant(t) != kind) {
panic!("heterogeneous NBT list — needs explicit Hint");
}
match first {
Tag::Byte(_) => List::Byte(
tags.into_iter()
.map(|t| if let Tag::Byte(b) = t { b } else { 0 })
.collect(),
),
Tag::Short(_) => List::Short(
tags.into_iter()
.map(|t| if let Tag::Short(b) = t { b } else { 0 })
.collect::<Vec<_>>()
.into(),
),
Tag::Int(_) => List::Int(
tags.into_iter()
.map(|t| if let Tag::Int(b) = t { b } else { 0 })
.collect::<Vec<_>>()
.into(),
),
Tag::Long(_) => List::Long(
tags.into_iter()
.map(|t| if let Tag::Long(b) = t { b } else { 0 })
.collect::<Vec<_>>()
.into(),
),
Tag::Float(_) => List::Float(
tags.into_iter()
.map(|t| if let Tag::Float(b) = t { b } else { 0.0 })
.collect::<Vec<_>>()
.into(),
),
Tag::Double(_) => List::Double(
tags.into_iter()
.map(|t| if let Tag::Double(b) = t { b } else { 0.0 })
.collect::<Vec<_>>()
.into(),
),
Tag::String(_) => List::String(
tags.into_iter()
.filter_map(|t| {
if let Tag::String(s) = t {
Some(s)
} else {
None
}
})
.collect(),
),
Tag::List(_) => List::List(
tags.into_iter()
.filter_map(|t| if let Tag::List(l) = t { Some(l) } else { None })
.collect(),
),
Tag::Compound(_) => List::Compound(
tags.into_iter()
.filter_map(|t| {
if let Tag::Compound(c) = t {
Some(c)
} else {
None
}
})
.collect(),
),
_ => panic!("unsupported list element type"),
}
}