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()
}
}