use proc_macro2::TokenStream; use pumpkin_nbt::compound::NbtCompound; use crate::version::JavaMinecraftVersion; mod block_state; mod entity_id; mod item_id; mod sound_id; /// Returns the list of remap builder functions paired with their output file names. #[allow(clippy::type_complexity)] pub fn build() -> Vec<(fn() -> TokenStream, &'static str)> { vec![ (block_state::build, "block_state_remap.rs"), (entity_id::build, "entity_id_remap.rs"), (item_id::build, "item_id_remap.rs"), (sound_id::build, "sound_id_remap.rs"), ] } /// A node in a linked chain of ViaVersion mapping files, each describing how IDs changed /// between consecutive Minecraft versions. pub struct MappingNode<'a, P> { /// The Minecraft version this node represents. pub version: JavaMinecraftVersion, /// The path to (or data of) the ViaVersion NBT mapping file for this version hop. pub value: P, /// The previous version node in the chain, or `None` if this is the oldest supported version. pub child: Option<&'a Self>, } /// Drives the recursive processing of a [`MappingNode`] chain, composing intermediate mappings /// into per-version translation tables. pub struct Remapper { /// The target (latest) version that all older mappings are translated toward. pub version: JavaMinecraftVersion, /// Combines the current-version mapping with a child mapping into a composed mapping. pub remapper: fn(&R, &R) -> R, /// Converts the raw path/data `P` stored in a [`MappingNode`] into the mapping type `R`. pub serializer: fn(&P) -> R, } impl Remapper { /// Recursively processes the [`MappingNode`] chain and returns a list of `(version, mapping)` pairs. /// /// # Returns /// A `Vec` where each entry contains a [`JavaMinecraftVersion`] and its composed mapping relative /// to `self.version`. pub fn process(&self, mappings: &MappingNode<'_, P>) -> Vec<(JavaMinecraftVersion, R)> { let current_mapping = (self.serializer)(&mappings.value); let mut remap = if let Some(child) = mappings.child { let mut res = self.process(child); for (_, remap) in &mut res { let new_mapping = (self.remapper)(¤t_mapping, remap); *remap = new_mapping; } res } else { Vec::new() }; remap.push((mappings.version, current_mapping)); remap } } /// A decoded ViaVersion ID mapping with a forward translation table. pub struct ParsedMappings { /// Number of IDs in the mapped (newer) version's namespace. pub mapped_size: usize, /// Forward mapping: index is the old ID, value is the new ID (`-1` means unmapped). pub forward: Vec, } impl ParsedMappings { /// Reads and parses a ViaVersion NBT mapping file, extracting the named section. /// /// # Arguments /// - `path` – Path to the `.nbt` mapping file. /// - `section` – Name of the compound section to extract (e.g. `"blockstates"`, `"items"`). /// /// # Returns /// `Some(ParsedMappings)` if the section exists, or `None` if the section is absent. pub fn parse_mapping_file(path: &str, section: &str) -> Option { use pumpkin_nbt::Nbt; use pumpkin_nbt::deserializer::NbtReadHelperJava; use std::fs; use std::io::Cursor; let bytes = fs::read(path).unwrap_or_else(|_| panic!("Failed to read {path}")); let mut reader = NbtReadHelperJava::new(Cursor::new(bytes)); let nbt = Nbt::read(&mut reader).unwrap_or_else(|_| panic!("Failed to parse NBT at {path}")); let mappings = nbt.root_tag.get_compound(section)?; // .unwrap_or_else(|| panic!("Missing `{section}` compound in {path}")); Some(Self::parse_mappings(mappings, path, section)) } /// Decodes a ViaVersion mapping compound into a forward ID translation table. fn parse_mappings(mappings: &NbtCompound, path: &str, section: &str) -> Self { let mapped_size = mappings .get_int("mappedSize") .unwrap_or_else(|| panic!("Missing `{section}.mappedSize` in {path}")); let strategy = mappings .get_byte("id") .unwrap_or_else(|| panic!("Missing `{section}.id` in {path}")); let forward = match strategy { // Direct 0 => mappings .get_int_array("val") .unwrap_or_else(|| panic!("Missing `{section}.val` for direct mapping in {path}")) .to_vec(), // Shifts 1 => { let shifts_at = mappings.get_int_array("at").unwrap_or_else(|| { panic!("Missing `{section}.at` for shift mapping in {path}") }); let shifts_to = mappings.get_int_array("to").unwrap_or_else(|| { panic!("Missing `{section}.to` for shift mapping in {path}") }); let size = mappings.get_int("size").unwrap_or_else(|| { panic!("Missing `{section}.size` for shift mapping in {path}") }) as usize; assert_eq!( shifts_at.len(), shifts_to.len(), "Shift mapping length mismatch in {path}" ); let mut result = vec![-1; size]; if !shifts_at.is_empty() && shifts_at[0] != 0 { for id in 0..shifts_at[0] { result[id as usize] = id; } } for (index, from) in shifts_at.iter().enumerate() { let to = if index + 1 == shifts_at.len() { size as i32 } else { shifts_at[index + 1] }; for (mapped_id, id) in (shifts_to[index]..).zip(*from..to) { result[id as usize] = mapped_id; } } result } // Changes 2 => { let changes_at = mappings.get_int_array("at").unwrap_or_else(|| { panic!("Missing `{section}.at` for change mapping in {path}") }); let values = mappings.get_int_array("val").unwrap_or_else(|| { panic!("Missing `{section}.val` for change mapping in {path}") }); let size = mappings.get_int("size").unwrap_or_else(|| { panic!("Missing `{section}.size` for change mapping in {path}") }) as usize; let fill_between = mappings.get("nofill").is_none(); assert_eq!( changes_at.len(), values.len(), "Change mapping length mismatch in {path}" ); let mut result = vec![-1; size]; let mut next_unhandled_id = 0; for (index, changed_id) in changes_at.iter().enumerate() { if fill_between { for id in next_unhandled_id..*changed_id { result[id as usize] = id; } next_unhandled_id = changed_id + 1; } result[*changed_id as usize] = values[index]; } result } // Identity 3 => { let size = mappings.get_int("size").unwrap_or_else(|| { panic!("Missing `{section}.size` for identity mapping in {path}") }) as usize; (0..size as i32).collect::>() } _ => panic!("Unknown {section} mapping strategy {strategy} in {path}"), }; Self { mapped_size: mapped_size as usize, forward, } } /// Inverts the forward mapping into a reverse lookup table where index is the new ID and value /// is the corresponding old ID. Unmapped entries default to their own index cast to `u16`. /// /// # Arguments /// - `name` – Descriptive name used in panic messages for better diagnostics. /// /// # Returns /// A `Vec` of length `self.mapped_size` mapping new IDs back to old IDs. pub fn _invert_with_default_to_u16(&self, name: &str) -> Vec { let mut inverse = vec![0u16; self.mapped_size]; let mut seen = vec![false; self.mapped_size]; for (old_id, mapped_id) in self.forward.iter().enumerate() { let Ok(mapped_id) = usize::try_from(*mapped_id) else { continue; }; if mapped_id >= self.mapped_size || seen[mapped_id] { continue; } let old_u16 = u16::try_from(old_id) .unwrap_or_else(|_| panic!("{name}: id {old_id} does not fit in u16")); inverse[mapped_id] = old_u16; seen[mapped_id] = true; } for (mapped_id, mapped_to) in inverse.iter_mut().enumerate() { if !seen[mapped_id] { *mapped_to = u16::try_from(mapped_id) .unwrap_or_else(|_| panic!("{name}: id {mapped_id} does not fit in u16")); } } inverse } /// Converts the forward mapping directly to a u16 table. /// Used with ViaBackwards mappings which are already in new→old direction. pub fn to_u16(&self, name: &str) -> Vec { self.forward .iter() .map(|&id| { if id < 0 { 0 // unmapped → air } else { u16::try_from(id) .unwrap_or_else(|_| panic!("{name}: id {id} does not fit in u16")) } }) .collect() } }