fix(protocol): remap particle IDs for older clients (#2451)

* feat(protocol): generate particle ID remap tables

Generate version-specific particle registry mappings from the existing ViaBackwards data for every supported Java protocol version.\n\nExpose the generated remapper through pumpkin-data and enable it for pumpkin-protocol so packet serializers can translate current registry IDs for older clients.

* fix(protocol): remap explosion particles for older clients

Translate the explosion particle registry ID for each client's negotiated Java version before serializing the explode packet.\n\nWithout this remap, 1.21.11 interpreted the 26.2 explosion-emitter ID as falling dust, consumed the following bytes as particle data, and disconnected with a decoder exception. Add packet-level coverage for both 1.21.11 and 26.2 encodings.

* fix(protocol): remap particle IDs for older clients

Level-particle packets and particle entity metadata still encoded raw 26.2 registry IDs, causing older clients to decode the wrong particle schema and disconnect. Route every active outbound particle path through the version remapper, preserve metadata payloads, and cover 1.21.11 and 26.2 encodings with regression tests.
This commit is contained in:
Megalith
2026-07-24 19:14:18 +03:00
committed by GitHub
parent 3ff3eeaf74
commit f676bdd222
9 changed files with 478 additions and 6 deletions

View File

@@ -6,6 +6,7 @@ use crate::version::JavaMinecraftVersion;
mod block_state;
mod entity_id;
mod item_id;
mod particle_id;
mod sound_id;
/// Returns the list of remap builder functions paired with their output file names.
@@ -15,6 +16,7 @@ pub fn build() -> Vec<(fn() -> TokenStream, &'static str)> {
(block_state::build, "block_state_remap.rs"),
(entity_id::build, "entity_id_remap.rs"),
(item_id::build, "item_id_remap.rs"),
(particle_id::build, "particle_id_remap.rs"),
(sound_id::build, "sound_id_remap.rs"),
]
}

View File

@@ -0,0 +1,125 @@
use proc_macro2::{Literal, TokenStream};
use quote::{format_ident, quote};
use crate::remap::{MappingNode, ParsedMappings, Remapper};
use crate::version::JavaMinecraftVersion;
/// Generates the `TokenStream` for per-version particle ID remap tables and the
/// `remap_particle_id_for_version` function.
pub fn build() -> TokenStream {
let node_1_20_5 = MappingNode {
version: JavaMinecraftVersion::V_1_20_5,
value: "../assets/viabackwards/data/mappings-1.21to1.20.5.nbt",
child: None,
};
let node_1_21 = MappingNode {
version: JavaMinecraftVersion::V_1_21,
value: "../assets/viabackwards/data/mappings-1.21.2to1.21.nbt",
child: Some(&node_1_20_5),
};
let node_1_21_2 = MappingNode {
version: JavaMinecraftVersion::V_1_21_2,
value: "../assets/viabackwards/data/mappings-1.21.4to1.21.2.nbt",
child: Some(&node_1_21),
};
let node_1_21_4 = MappingNode {
version: JavaMinecraftVersion::V_1_21_4,
value: "../assets/viabackwards/data/mappings-1.21.5to1.21.4.nbt",
child: Some(&node_1_21_2),
};
let node_1_21_5 = MappingNode {
version: JavaMinecraftVersion::V_1_21_5,
value: "../assets/viabackwards/data/mappings-1.21.6to1.21.5.nbt",
child: Some(&node_1_21_4),
};
let node_1_21_6 = MappingNode {
version: JavaMinecraftVersion::V_1_21_6,
value: "../assets/viabackwards/data/mappings-1.21.7to1.21.6.nbt",
child: Some(&node_1_21_5),
};
let node_1_21_7 = MappingNode {
version: JavaMinecraftVersion::V_1_21_7,
value: "../assets/viabackwards/data/mappings-1.21.9to1.21.7.nbt",
child: Some(&node_1_21_6),
};
let node_1_21_9 = MappingNode {
version: JavaMinecraftVersion::V_1_21_9,
value: "../assets/viabackwards/data/mappings-1.21.11to1.21.9.nbt",
child: Some(&node_1_21_7),
};
let node_1_21_11 = MappingNode {
version: JavaMinecraftVersion::V_1_21_11,
value: "../assets/viabackwards/data/mappings-26.1to1.21.11.nbt",
child: Some(&node_1_21_9),
};
let node_26_1 = MappingNode {
version: JavaMinecraftVersion::V_26_1,
value: "../assets/viabackwards/data/mappings-26.2to26.1.nbt",
child: Some(&node_1_21_11),
};
let remapper: Remapper<_, Option<Vec<u16>>> = Remapper {
version: JavaMinecraftVersion::V_26_2,
remapper: |first, second| match (first, second) {
(Some(first), Some(second)) => Some(
first
.iter()
.map(|id| second.get(usize::from(*id)).copied().unwrap_or(0))
.collect(),
),
(None, Some(second)) => Some(
(0..second.len())
.map(|id| second.get(id).copied().unwrap_or(0))
.collect(),
),
(Some(first), None) => Some(first.clone()),
_ => None,
},
serializer: |&file| {
ParsedMappings::parse_mapping_file(file, "particles")
.map(|mappings| mappings.to_u16(file))
},
};
let all_mappings = remapper.process(&node_26_1);
let mut static_values = TokenStream::new();
let mut match_arms = TokenStream::new();
for (ver, mapping) in &all_mappings {
let ident = format_ident!(
"{}",
format!("PARTICLE_ID_REMAP_{:?}_TO_{:?}", remapper.version, ver).to_uppercase()
);
let mapping_tokens: Vec<_> = mapping
.as_ref()
.unwrap()
.iter()
.copied()
.map(Literal::u16_unsuffixed)
.collect();
static_values.extend(quote! {
const #ident: &[u16] = &[#(#mapping_tokens),*];
});
match_arms.extend(quote! {
#ver => #ident
.get(usize::from(particle_id))
.copied()
.unwrap_or(particle_id),
});
}
quote! {
use pumpkin_util::version::JavaMinecraftVersion;
#static_values
#[must_use]
pub fn remap_particle_id_for_version(
particle_id: u16,
version: JavaMinecraftVersion,
) -> u16 {
match version {
#match_arms
_ => particle_id,
}
}
}
}