From f676bdd2224e90af00ff715f0dc29acf09b68212 Mon Sep 17 00:00:00 2001 From: Megalith <74655120+MegalithOfficial@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:14:18 +0300 Subject: [PATCH] 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. --- pumpkin-codegen/src/remap/mod.rs | 2 + pumpkin-codegen/src/remap/particle_id.rs | 125 +++++++++++++++ pumpkin-data/Cargo.toml | 1 + .../src/generated/particle_id_remap.rs | 144 ++++++++++++++++++ pumpkin-data/src/lib.rs | 5 + pumpkin-protocol/Cargo.toml | 2 +- .../src/java/client/play/entity_metadata.rs | 86 +++++++++++ .../src/java/client/play/explode.rs | 52 ++++++- .../src/java/client/play/particle.rs | 67 +++++++- 9 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 pumpkin-codegen/src/remap/particle_id.rs create mode 100644 pumpkin-data/src/generated/particle_id_remap.rs diff --git a/pumpkin-codegen/src/remap/mod.rs b/pumpkin-codegen/src/remap/mod.rs index 523c09545..c5a2211d1 100644 --- a/pumpkin-codegen/src/remap/mod.rs +++ b/pumpkin-codegen/src/remap/mod.rs @@ -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"), ] } diff --git a/pumpkin-codegen/src/remap/particle_id.rs b/pumpkin-codegen/src/remap/particle_id.rs new file mode 100644 index 000000000..b435fb90b --- /dev/null +++ b/pumpkin-codegen/src/remap/particle_id.rs @@ -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>> = 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, + } + } + } +} diff --git a/pumpkin-data/Cargo.toml b/pumpkin-data/Cargo.toml index 2797c72bb..ef40c9558 100644 --- a/pumpkin-data/Cargo.toml +++ b/pumpkin-data/Cargo.toml @@ -106,6 +106,7 @@ block = [] item_id_remap = [] entity_id_remap = [] sound_id_remap = [] +particle_id_remap = [] potion_brewing = [] [dependencies] diff --git a/pumpkin-data/src/generated/particle_id_remap.rs b/pumpkin-data/src/generated/particle_id_remap.rs new file mode 100644 index 000000000..db7013dfd --- /dev/null +++ b/pumpkin-data/src/generated/particle_id_remap.rs @@ -0,0 +1,144 @@ +/* This file is generated. Do not edit manually. */ +use pumpkin_util::version::JavaMinecraftVersion; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_20_5: &[u16] = &[ + 0, 1, 2, 3, 68, 71, 4, 4, 0, 0, 0, 4, 37, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 41, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 20, 40, 40, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, + 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 1, 78, 46, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21: &[u16] = &[ + 0, 1, 2, 3, 68, 71, 4, 4, 0, 0, 0, 4, 37, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 41, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 20, 40, 40, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, + 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, + 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 1, 78, 46, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_2: &[u16] = &[ + 0, 1, 2, 3, 69, 72, 4, 4, 0, 0, 0, 4, 37, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 33, 41, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 40, 40, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, + 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, + 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 79, 47, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_4: &[u16] = &[ + 0, 1, 2, 3, 70, 73, 4, 4, 0, 0, 0, 4, 38, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 42, 35, 36, 37, 38, 39, 40, 41, + 42, 43, 44, 45, 46, 47, 41, 41, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, + 109, 110, 111, 80, 48, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_5: &[u16] = &[ + 0, 1, 2, 3, 71, 74, 4, 4, 0, 0, 0, 4, 39, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 42, 42, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 49, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_6: &[u16] = &[ + 0, 1, 2, 3, 71, 74, 4, 4, 0, 0, 0, 4, 39, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 42, 42, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 49, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_7: &[u16] = &[ + 0, 1, 2, 3, 71, 74, 4, 4, 0, 0, 0, 4, 39, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 42, 42, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, + 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, + 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, + 110, 111, 112, 113, 49, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_9: &[u16] = &[ + 0, 1, 2, 3, 72, 75, 4, 4, 0, 0, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 43, 43, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, + 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, + 111, 112, 113, 114, 50, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_11: &[u16] = &[ + 0, 1, 2, 3, 72, 75, 4, 4, 0, 0, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 43, 43, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, + 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, + 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, + 111, 112, 113, 114, 50, +]; +const PARTICLE_ID_REMAP_V_26_2_TO_V_26_1: &[u16] = &[ + 0, 1, 2, 3, 74, 77, 4, 4, 0, 0, 0, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, + 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, + 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 52, +]; +#[must_use] +pub fn remap_particle_id_for_version(particle_id: u16, version: JavaMinecraftVersion) -> u16 { + match version { + pumpkin_util::version::JavaMinecraftVersion::V_1_20_5 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_20_5 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21 => PARTICLE_ID_REMAP_V_26_2_TO_V_1_21 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id), + pumpkin_util::version::JavaMinecraftVersion::V_1_21_2 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_2 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21_4 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_4 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21_5 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_5 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21_6 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_6 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21_7 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_7 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21_9 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_9 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_1_21_11 => { + PARTICLE_ID_REMAP_V_26_2_TO_V_1_21_11 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id) + } + pumpkin_util::version::JavaMinecraftVersion::V_26_1 => PARTICLE_ID_REMAP_V_26_2_TO_V_26_1 + .get(usize::from(particle_id)) + .copied() + .unwrap_or(particle_id), + _ => particle_id, + } +} diff --git a/pumpkin-data/src/lib.rs b/pumpkin-data/src/lib.rs index 630927fe5..777e976a1 100644 --- a/pumpkin-data/src/lib.rs +++ b/pumpkin-data/src/lib.rs @@ -250,6 +250,11 @@ pub mod entity_id_remap; #[path = "generated/sound_id_remap.rs"] pub mod sound_id_remap; +#[cfg(feature = "particle_id_remap")] +#[rustfmt::skip] +#[path = "generated/particle_id_remap.rs"] +pub mod particle_id_remap; + #[cfg(feature = "bedrock_creative")] #[rustfmt::skip] #[path = "generated/bedrock_creative.rs"] diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index 60b05f628..828538c10 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -13,7 +13,7 @@ query = [] [dependencies] pumpkin-nbt.workspace = true -pumpkin-data = { workspace = true, features = ["packet", "item_id_remap", "entity_id_remap", "sound_id_remap", "recipes", "bedrock_creative"] } +pumpkin-data = { workspace = true, features = ["packet", "item_id_remap", "entity_id_remap", "sound_id_remap", "particle_id_remap", "recipes", "bedrock_creative"] } pumpkin-macros.workspace = true pumpkin-world.workspace = true pumpkin-util.workspace = true diff --git a/pumpkin-protocol/src/java/client/play/entity_metadata.rs b/pumpkin-protocol/src/java/client/play/entity_metadata.rs index 0c7817af0..0c4c04c40 100644 --- a/pumpkin-protocol/src/java/client/play/entity_metadata.rs +++ b/pumpkin-protocol/src/java/client/play/entity_metadata.rs @@ -13,6 +13,8 @@ use crate::{ ser::{NetworkWriteExt, WritingError}, }; +use super::particle::particle_id_for_version; + pub trait MetadataSerializer { fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError>; } @@ -138,6 +140,22 @@ impl Metadata { return Ok(()); } + if self.r#type == MetaDataType::PARTICLE { + let mut serialized_value = Vec::new(); + self.value.write_metadata(&mut serialized_value)?; + + let mut cursor = Cursor::new(serialized_value); + let particle_id = VarInt::decode(&mut cursor).map_err(|e| { + WritingError::Message(format!("Failed to decode particle metadata: {e}")) + })?; + writer.write_var_int(&particle_id_for_version(particle_id, *version))?; + + let remainder_start = cursor.position() as usize; + let inner = cursor.into_inner(); + writer.write_slice(&inner[remainder_start..])?; + return Ok(()); + } + self.value.write_metadata(&mut writer)?; Ok(()) @@ -264,3 +282,71 @@ impl MetadataSerializer for crate::codec::optional_int::OptionalInt { writer.write_var_int(&VarInt(val)) } } + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Read}; + + use pumpkin_data::{ + meta_data_type::MetaDataType, particle::Particle, tracked_data::TrackedData, + }; + use pumpkin_util::version::JavaMinecraftVersion; + + use crate::{VarInt, ser::NetworkWriteExt}; + + use super::{Metadata, MetadataSerializer}; + + struct ParticleMetadata { + particle_id: VarInt, + data: [u8; 4], + } + + impl MetadataSerializer for ParticleMetadata { + fn write_metadata( + &self, + writer: &mut impl std::io::Write, + ) -> Result<(), crate::WritingError> { + writer.write_var_int(&self.particle_id)?; + writer.write_slice(&self.data) + } + } + + fn encoded_particle(version: JavaMinecraftVersion) -> (VarInt, Vec) { + let particle_data = [0x12, 0x34, 0x56, 0x78]; + let metadata = Metadata::new( + TrackedData::PARTICLE, + MetaDataType::PARTICLE, + ParticleMetadata { + particle_id: VarInt(Particle::ExplosionEmitter as i32), + data: particle_data, + }, + ); + let mut bytes = Vec::new(); + metadata.write(&mut bytes, &version).unwrap(); + + assert_eq!(bytes[0], TrackedData::PARTICLE.get(&version)); + assert_eq!(bytes[1], MetaDataType::PARTICLE.id(version) as u8); + + let mut cursor = Cursor::new(&bytes[2..]); + let particle_id = VarInt::decode(&mut cursor).unwrap(); + let mut remainder = Vec::new(); + cursor.read_to_end(&mut remainder).unwrap(); + (particle_id, remainder) + } + + #[test] + fn particle_metadata_id_remaps_for_1_21_11() { + let (particle_id, data) = encoded_particle(JavaMinecraftVersion::V_1_21_11); + + assert_eq!(particle_id, VarInt(22)); + assert_eq!(data, [0x12, 0x34, 0x56, 0x78]); + } + + #[test] + fn particle_metadata_id_stays_latest_for_26_2() { + let (particle_id, data) = encoded_particle(JavaMinecraftVersion::V_26_2); + + assert_eq!(particle_id, VarInt(29)); + assert_eq!(data, [0x12, 0x34, 0x56, 0x78]); + } +} diff --git a/pumpkin-protocol/src/java/client/play/explode.rs b/pumpkin-protocol/src/java/client/play/explode.rs index d710e2a13..835c4dcbc 100644 --- a/pumpkin-protocol/src/java/client/play/explode.rs +++ b/pumpkin-protocol/src/java/client/play/explode.rs @@ -7,6 +7,8 @@ use crate::ser::NetworkWriteExt; use crate::{IdOr, SoundEvent, codec::var_int::VarInt}; use pumpkin_util::version::JavaMinecraftVersion; +use super::particle::particle_id_for_version; + /// Notifies the client that an explosion has occurred. /// /// This is a high-level packet that handles the visual, auditory, and physical @@ -60,7 +62,7 @@ impl ClientPacket for CExplosion { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { write.write_f64_be(self.center.x)?; write.write_f64_be(self.center.y)?; @@ -73,7 +75,8 @@ impl ClientPacket for CExplosion { w.write_f64_be(k.z)?; Ok(()) })?; - write.write_var_int(&self.particle)?; + let particle = particle_id_for_version(self.particle, *version); + write.write_var_int(&particle)?; match &self.sound { IdOr::Id(id) => write.write_var_int(&VarInt((*id + 1) as i32))?, IdOr::Value(event) => { @@ -86,3 +89,48 @@ impl ClientPacket for CExplosion { Ok(()) } } + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Seek, SeekFrom}; + + use pumpkin_data::particle::Particle; + use pumpkin_util::{math::vector3::Vector3, version::JavaMinecraftVersion}; + + use crate::{ClientPacket, IdOr, VarInt}; + + use super::CExplosion; + + fn encoded_particle_id(version: JavaMinecraftVersion) -> VarInt { + let packet = CExplosion::new( + Vector3::new(0.0, 0.0, 0.0), + 4.0, + 0, + None, + VarInt(Particle::ExplosionEmitter as i32), + IdOr::Id(0), + ); + let mut bytes = Vec::new(); + packet.write_packet_data(&mut bytes, &version).unwrap(); + + let mut cursor = Cursor::new(bytes); + cursor.seek(SeekFrom::Start(33)).unwrap(); + VarInt::decode(&mut cursor).unwrap() + } + + #[test] + fn explosion_particle_id_remaps_for_1_21_11() { + assert_eq!( + encoded_particle_id(JavaMinecraftVersion::V_1_21_11), + VarInt(22) + ); + } + + #[test] + fn explosion_particle_id_stays_latest_for_26_2() { + assert_eq!( + encoded_particle_id(JavaMinecraftVersion::V_26_2), + VarInt(29) + ); + } +} diff --git a/pumpkin-protocol/src/java/client/play/particle.rs b/pumpkin-protocol/src/java/client/play/particle.rs index 0695cc41c..b06769305 100644 --- a/pumpkin-protocol/src/java/client/play/particle.rs +++ b/pumpkin-protocol/src/java/client/play/particle.rs @@ -1,6 +1,8 @@ use std::io::Write; -use pumpkin_data::packet::clientbound::PLAY_LEVEL_PARTICLES; +use pumpkin_data::{ + packet::clientbound::PLAY_LEVEL_PARTICLES, particle_id_remap::remap_particle_id_for_version, +}; use pumpkin_macros::java_packet; use pumpkin_util::{math::vector3::Vector3, version::JavaMinecraftVersion}; @@ -64,11 +66,23 @@ impl<'a> CParticle<'a> { } } +pub(super) fn particle_id_for_version( + particle_id: VarInt, + version: JavaMinecraftVersion, +) -> VarInt { + u16::try_from(particle_id.0).map_or(particle_id, |particle_id| { + VarInt(i32::from(remap_particle_id_for_version( + particle_id, + version, + ))) + }) +} + impl ClientPacket for CParticle<'_> { fn write_packet_data( &self, write: impl Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), WritingError> { let mut write = write; @@ -85,8 +99,55 @@ impl ClientPacket for CParticle<'_> { write.write_f32_be(self.max_speed)?; write.write_i32_be(self.particle_count)?; - write.write_var_int(&self.particle_id)?; + write.write_var_int(&particle_id_for_version(self.particle_id, *version))?; write.write_all(self.data).map_err(WritingError::IoError) } } + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Seek, SeekFrom}; + + use pumpkin_data::particle::Particle; + use pumpkin_util::{math::vector3::Vector3, version::JavaMinecraftVersion}; + + use crate::{ClientPacket, VarInt}; + + use super::CParticle; + + fn encoded_particle_id(version: JavaMinecraftVersion) -> VarInt { + let packet = CParticle::new( + false, + false, + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 0.0), + 0.0, + 1, + VarInt(Particle::ExplosionEmitter as i32), + &[], + ); + let mut bytes = Vec::new(); + packet.write_packet_data(&mut bytes, &version).unwrap(); + + let mut cursor = Cursor::new(bytes); + cursor.seek(SeekFrom::Start(46)).unwrap(); + VarInt::decode(&mut cursor).unwrap() + } + + #[test] + fn particle_id_remaps_for_1_21_11() { + assert_eq!( + encoded_particle_id(JavaMinecraftVersion::V_1_21_11), + VarInt(22) + ); + } + + #[test] + fn particle_id_stays_latest_for_26_2() { + assert_eq!( + encoded_particle_id(JavaMinecraftVersion::V_26_2), + VarInt(29) + ); + } +}