mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
* fix(protocol): remap item/block state IDs and entity metadata for older Java clients The server was using reversed ViaVersion mappings (designed for old→new) to downgrade packets for older clients, which produced ID collisions and incorrect fallbacks for 26.1-exclusive items and block states. Changes: - Replace ViaVersion with ViaBackwards mapping files for item ID and block state remapping, which provide the correct new→old direction with proper closest-item fallbacks for 26.1-exclusive content - Fix CSetEntityMetadata to remap ItemStack item IDs per client version, matching the version-aware handling already done for inventory packets and block state metadata - Fix TrackedData codegen to resolve Java JSON keys correctly by trying both bare names and DATA_-prefixed names, and iterate the union of all version keys instead of only the latest (Bedrock) version - Fix LootCondition::MatchTool to default to false instead of true, preventing silk-touch loot table branches from always winning and causing ores to drop themselves instead of their correct items Fixes: block drops invisible on older Java clients Fixes: leather showing as milk bucket on 1.21.11 Fixes: coal ore dropping coal ore instead of coal Fixes: 26.1-exclusive blocks showing as wrong blocks on older clients * fix(codegen): remove #[must_use] from trait impl methods in generated code * fix(codegen): switch entity, sound and block state remapping to ViaBackwards mappings --------- Co-authored-by: Missing_Love <42416195+Q2297045667@users.noreply.github.com>
257 lines
9.7 KiB
Rust
257 lines
9.7 KiB
Rust
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<P, R> {
|
||
/// 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<P, R> Remapper<P, R> {
|
||
/// 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<i32>,
|
||
}
|
||
|
||
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<Self> {
|
||
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::<Vec<_>>()
|
||
}
|
||
_ => 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<u16>` of length `self.mapped_size` mapping new IDs back to old IDs.
|
||
pub fn _invert_with_default_to_u16(&self, name: &str) -> Vec<u16> {
|
||
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<u16> {
|
||
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()
|
||
}
|
||
}
|