From cef8a024e7b8b4d11a1f15ffa52b8a8ba3680852 Mon Sep 17 00:00:00 2001 From: ChocoDev <100467857+chocodev11@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:24:16 +0700 Subject: [PATCH] feat: BlockState Remaping (#1457) * vn translation * feat: blockstate remapping * formatting cleanup * damm dot * damm comma * fix: mapping 1.21.8 tracker * fix: entity metadata * formatting * clippy * fix: item id remap * formatting --- .../data/mappings-1.21.7to1.21.9.nbt | Bin 0 -> 773 bytes .../data/mappings-1.21.9to1.21.11.nbt | Bin 0 -> 1203 bytes pumpkin-data/build/block_state_remap.rs | 210 ++++++++++++++ pumpkin-data/build/build.rs | 4 + pumpkin-data/build/item_id_remap.rs | 266 ++++++++++++++++++ pumpkin-data/build/tracked_data.rs | 2 +- pumpkin-data/src/lib.rs | 8 + .../src/codec/item_stack_seralizer.rs | 107 ++++--- .../src/java/client/play/block_update.rs | 28 +- .../src/java/client/play/chunk_data.rs | 32 ++- .../src/java/client/play/entity_metadata.rs | 64 ++++- .../src/java/client/play/level_event.rs | 35 ++- .../java/client/play/multi_block_update.rs | 35 ++- .../java/client/play/set_container_content.rs | 32 ++- .../java/client/play/set_container_slot.rs | 23 +- .../src/java/client/play/set_cursor_slot.rs | 16 +- .../src/java/client/play/set_equipment.rs | 14 +- .../java/client/play/set_player_inventory.rs | 18 +- .../src/java/client/play/spawn_entity.rs | 202 ++++++++++++- .../src/java/client/play/worldevent.rs | 35 ++- pumpkin/src/net/java/play.rs | 4 +- 21 files changed, 1055 insertions(+), 80 deletions(-) create mode 100644 assets/viaversion/data/mappings-1.21.7to1.21.9.nbt create mode 100644 assets/viaversion/data/mappings-1.21.9to1.21.11.nbt create mode 100644 pumpkin-data/build/block_state_remap.rs create mode 100644 pumpkin-data/build/item_id_remap.rs diff --git a/assets/viaversion/data/mappings-1.21.7to1.21.9.nbt b/assets/viaversion/data/mappings-1.21.7to1.21.9.nbt new file mode 100644 index 0000000000000000000000000000000000000000..68cd7b19e4b0d73827ebe4f8e1cb5b905937c568 GIT binary patch literal 773 zcmZ{iJxjw-7==&TB()zZ7NKATCqZ4LxT>q71(i+?Qqm;av`tf*C{jlqT+~UlE)M<# z5#0P5Md&|pQ5^gep0r#jw>Uh*z3=;;bM8k1h$41iH+{#e15gr3mTI0^^#dcY{U{Qy z(P-FK!D-om|Dzz{SV|PC-`#NMl86`q2V+3I2xM-6Y?)9jPpn7MVN>Asno?|CYma#(QQ0#Uy+(Ee+_)B0*SOXQ4t)SX^Y$ zq04ah;_Z6i1kS*#BPW?*UYIG=n-SvSVwPe<|LL@4ohzXe;b@kG58M4I;l?B>P%qvv nnt@~127~#sU{eT&Su`OQE_5U@VwR2io^2I--;BGg@ZbCa=+cJP literal 0 HcmV?d00001 diff --git a/assets/viaversion/data/mappings-1.21.9to1.21.11.nbt b/assets/viaversion/data/mappings-1.21.9to1.21.11.nbt new file mode 100644 index 0000000000000000000000000000000000000000..06450ff28f31747b52289cba6e1a918963f7ee97 GIT binary patch literal 1203 zcmZY9xo#6-6vgptkH-cZLBSFUgb;M0-~o7sAcQC)Dl@T1VsPv+nJ)R{Brb4a z;#s*FwA*2QZ8RF1B~v-eM|ID$Lh|<)RyIF!VKe)v|9$$=+`mWt&lSrDjyrnH3LDWt zKbnZq#wH9P0nZi&F@#}k#t25S1zWKV+p!C~u?Ksx9|v#{hcJf27{?JD#W5Vm37o=d zOyCU8p@Z|dfQy*KC0xc8T*YEW+Rz+$FEvZuWLB(icm7$V9LZK*F1*0NVfI?JR3i=IH zno3b|3hE>jno3fbD#JlxDR`BlG8Eh={3Nt4tmNe^Ch=gao_#w@D>83q@kjc0&M~vq zAVA|Fk7c~y92y|PFf`aNE+8y_+^qNoU-2z%N{6fgZRPSDpu`tq4BA2sk*>cSOYy<1 zGz+t_n5k!kEf=}yzb~LoX6Cj|I(LEV=Ajo=Or*2ugZWs8K6E(0tgm;JxTuy*##Uu- z=t*zL@y83&Q}sg^C(W$~E~wVxW;<>%bk&Wt!_0pT+mJnSeR`zNDN@8DEJJU4Mf$-^ Kez^qQV6#6ORl?)| literal 0 HcmV?d00001 diff --git a/pumpkin-data/build/block_state_remap.rs b/pumpkin-data/build/block_state_remap.rs new file mode 100644 index 000000000..a51d30696 --- /dev/null +++ b/pumpkin-data/build/block_state_remap.rs @@ -0,0 +1,210 @@ +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use std::{fs, io::Cursor}; + +use pumpkin_nbt::{Nbt, compound::NbtCompound, deserializer::NbtReadHelper}; + +struct ParsedMappings { + mapped_size: usize, + forward: Vec, +} + +fn parse_mapping_file(path: &str) -> ParsedMappings { + let bytes = fs::read(path).unwrap_or_else(|_| panic!("Failed to read {path}")); + let mut reader = NbtReadHelper::new(Cursor::new(bytes)); + let nbt = Nbt::read(&mut reader).unwrap_or_else(|_| panic!("Failed to parse NBT at {path}")); + + let blockstates = nbt + .root_tag + .get_compound("blockstates") + .unwrap_or_else(|| panic!("Missing `blockstates` compound in {path}")); + + parse_blockstate_mappings(blockstates, path) +} + +fn parse_blockstate_mappings(blockstates: &NbtCompound, path: &str) -> ParsedMappings { + let mapped_size = blockstates + .get_int("mappedSize") + .unwrap_or_else(|| panic!("Missing `blockstates.mappedSize` in {path}")); + let strategy = blockstates + .get_byte("id") + .unwrap_or_else(|| panic!("Missing `blockstates.id` in {path}")); + + let forward = match strategy { + // Direct + 0 => blockstates + .get_int_array("val") + .unwrap_or_else(|| panic!("Missing `blockstates.val` for direct mapping in {path}")) + .to_vec(), + // Shifts + 1 => { + let shifts_at = blockstates + .get_int_array("at") + .unwrap_or_else(|| panic!("Missing `blockstates.at` for shift mapping in {path}")); + let shifts_to = blockstates + .get_int_array("to") + .unwrap_or_else(|| panic!("Missing `blockstates.to` for shift mapping in {path}")); + let size = blockstates + .get_int("size") + .unwrap_or_else(|| panic!("Missing `blockstates.size` for shift mapping in {path}")) + as usize; + + assert_eq!( + shifts_at.len(), + shifts_to.len(), + "Shift mapping length mismatch in {path}" + ); + + let mut mappings = vec![-1; size]; + + if !shifts_at.is_empty() && shifts_at[0] != 0 { + for id in 0..shifts_at[0] { + mappings[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] + }; + let mut mapped_id = shifts_to[index]; + for id in *from..to { + mappings[id as usize] = mapped_id; + mapped_id += 1; + } + } + + mappings + } + // Changes + 2 => { + let changes_at = blockstates + .get_int_array("at") + .unwrap_or_else(|| panic!("Missing `blockstates.at` for change mapping in {path}")); + let values = blockstates.get_int_array("val").unwrap_or_else(|| { + panic!("Missing `blockstates.val` for change mapping in {path}") + }); + let size = blockstates.get_int("size").unwrap_or_else(|| { + panic!("Missing `blockstates.size` for change mapping in {path}") + }) as usize; + let fill_between = blockstates.get("nofill").is_none(); + + assert_eq!( + changes_at.len(), + values.len(), + "Change mapping length mismatch in {path}" + ); + + let mut mappings = 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 { + mappings[id as usize] = id; + } + next_unhandled_id = changed_id + 1; + } + mappings[*changed_id as usize] = values[index]; + } + + mappings + } + // Identity + 3 => { + let size = blockstates.get_int("size").unwrap_or_else(|| { + panic!("Missing `blockstates.size` for identity mapping in {path}") + }) as usize; + (0..size as i32).collect::>() + } + _ => panic!("Unknown blockstate mapping strategy {strategy} in {path}"), + }; + + ParsedMappings { + mapped_size: mapped_size as usize, + forward, + } +} + +fn invert_to_u16(forward: &[i32], mapped_size: usize, name: &str) -> Vec { + let mut inverse = vec![0u16; mapped_size]; + let mut seen = vec![false; mapped_size]; + + for (old_id, mapped_id) in forward.iter().enumerate() { + let Ok(mapped_id) = usize::try_from(*mapped_id) else { + continue; + }; + if mapped_id >= 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; + } + + inverse +} + +fn compose(first: &[u16], second: &[u16]) -> Vec { + first + .iter() + .map(|id| second.get(usize::from(*id)).copied().unwrap_or(0)) + .collect() +} + +pub(crate) fn build() -> TokenStream { + println!("cargo:rerun-if-changed=../assets/viaversion/data/mappings-1.21.7to1.21.9.nbt"); + println!("cargo:rerun-if-changed=../assets/viaversion/data/mappings-1.21.9to1.21.11.nbt"); + + let mappings_7_to_9 = + parse_mapping_file("../assets/viaversion/data/mappings-1.21.7to1.21.9.nbt"); + let mappings_9_to_11 = + parse_mapping_file("../assets/viaversion/data/mappings-1.21.9to1.21.11.nbt"); + + let remap_9_to_7 = invert_to_u16( + &mappings_7_to_9.forward, + mappings_7_to_9.mapped_size, + "1.21.9->1.21.7", + ); + let remap_11_to_9 = invert_to_u16( + &mappings_9_to_11.forward, + mappings_9_to_11.mapped_size, + "1.21.11->1.21.9", + ); + let remap_11_to_7 = compose(&remap_11_to_9, &remap_9_to_7); + + let remap_11_to_9_tokens: Vec = remap_11_to_9 + .into_iter() + .map(Literal::u16_unsuffixed) + .collect(); + let remap_11_to_7_tokens: Vec = remap_11_to_7 + .into_iter() + .map(Literal::u16_unsuffixed) + .collect(); + + quote! { + use pumpkin_util::version::MinecraftVersion; + + pub static BLOCK_STATE_REMAP_1_21_11_TO_1_21_9: &[u16] = &[#(#remap_11_to_9_tokens),*]; + pub static BLOCK_STATE_REMAP_1_21_11_TO_1_21_7: &[u16] = &[#(#remap_11_to_7_tokens),*]; + + #[must_use] + pub fn remap_block_state_for_version(state_id: u16, version: MinecraftVersion) -> u16 { + match version { + MinecraftVersion::V_1_21_7 => BLOCK_STATE_REMAP_1_21_11_TO_1_21_7 + .get(usize::from(state_id)) + .copied() + .unwrap_or(0), + MinecraftVersion::V_1_21_9 => BLOCK_STATE_REMAP_1_21_11_TO_1_21_9 + .get(usize::from(state_id)) + .copied() + .unwrap_or(0), + _ => state_id, + } + } + } +} diff --git a/pumpkin-data/build/build.rs b/pumpkin-data/build/build.rs index ad5564254..7c776e184 100644 --- a/pumpkin-data/build/build.rs +++ b/pumpkin-data/build/build.rs @@ -13,6 +13,7 @@ mod attributes; mod biome; mod bitsets; mod block; +mod block_state_remap; mod chunk_gen_settings; mod chunk_status; mod composter_increase_chance; @@ -30,6 +31,7 @@ mod fuels; mod game_event; mod game_rules; mod item; +mod item_id_remap; mod jukebox_song; pub mod loot; mod message_type; @@ -86,7 +88,9 @@ pub fn main() { (message_type::build, "message_type.rs"), (spawn_egg::build, "spawn_egg.rs"), (block::build, "block.rs"), + (block_state_remap::build, "block_state_remap.rs"), (item::build, "item.rs"), + (item_id_remap::build, "item_id_remap.rs"), (structures::build, "structures.rs"), (chunk_gen_settings::build, "chunk_gen_settings.rs"), (fluid::build, "fluid.rs"), diff --git a/pumpkin-data/build/item_id_remap.rs b/pumpkin-data/build/item_id_remap.rs new file mode 100644 index 000000000..55089f7f5 --- /dev/null +++ b/pumpkin-data/build/item_id_remap.rs @@ -0,0 +1,266 @@ +use proc_macro2::{Literal, TokenStream}; +use quote::quote; +use std::{fs, io::Cursor}; + +use pumpkin_nbt::{Nbt, compound::NbtCompound, deserializer::NbtReadHelper}; + +struct ParsedMappings { + mapped_size: usize, + forward: Vec, +} + +fn parse_mapping_file(path: &str, section: &str) -> ParsedMappings { + let bytes = fs::read(path).unwrap_or_else(|_| panic!("Failed to read {path}")); + let mut reader = NbtReadHelper::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}")); + + parse_mappings(mappings, path, section) +} + +fn parse_mappings(mappings: &NbtCompound, path: &str, section: &str) -> ParsedMappings { + 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] + }; + let mut mapped_id = shifts_to[index]; + for id in *from..to { + result[id as usize] = mapped_id; + mapped_id += 1; + } + } + + 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}"), + }; + + ParsedMappings { + mapped_size: mapped_size as usize, + forward, + } +} + +fn invert_with_default_to_u16(forward: &[i32], mapped_size: usize, name: &str) -> Vec { + let mut inverse = vec![0u16; mapped_size]; + let mut seen = vec![false; mapped_size]; + + for (old_id, mapped_id) in forward.iter().enumerate() { + let Ok(mapped_id) = usize::try_from(*mapped_id) else { + continue; + }; + if mapped_id >= 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 +} + +fn forward_with_default_to_u16(forward: &[i32], name: &str) -> Vec { + forward + .iter() + .enumerate() + .map(|(old_id, mapped_id)| { + let mapped = usize::try_from(*mapped_id).ok().unwrap_or(old_id); + u16::try_from(mapped).unwrap_or_else(|_| { + u16::try_from(old_id).unwrap_or_else(|_| { + panic!("{name}: mapped id {mapped} and fallback id {old_id} do not fit in u16") + }) + }) + }) + .collect() +} + +fn compose_with_default(first: &[u16], second: &[u16]) -> Vec { + first + .iter() + .map(|id| second.get(usize::from(*id)).copied().unwrap_or(*id)) + .collect() +} + +pub(crate) fn build() -> TokenStream { + println!("cargo:rerun-if-changed=../assets/viaversion/data/mappings-1.21.7to1.21.9.nbt"); + println!("cargo:rerun-if-changed=../assets/viaversion/data/mappings-1.21.9to1.21.11.nbt"); + + let mappings_7_to_9 = parse_mapping_file( + "../assets/viaversion/data/mappings-1.21.7to1.21.9.nbt", + "items", + ); + let mappings_9_to_11 = parse_mapping_file( + "../assets/viaversion/data/mappings-1.21.9to1.21.11.nbt", + "items", + ); + + let remap_9_to_7 = invert_with_default_to_u16( + &mappings_7_to_9.forward, + mappings_7_to_9.mapped_size, + "1.21.9->1.21.7 items", + ); + let remap_7_to_9 = + forward_with_default_to_u16(&mappings_7_to_9.forward, "1.21.7->1.21.9 items"); + let remap_11_to_9 = invert_with_default_to_u16( + &mappings_9_to_11.forward, + mappings_9_to_11.mapped_size, + "1.21.11->1.21.9 items", + ); + let remap_9_to_11 = + forward_with_default_to_u16(&mappings_9_to_11.forward, "1.21.9->1.21.11 items"); + let remap_11_to_7 = compose_with_default(&remap_11_to_9, &remap_9_to_7); + let remap_7_to_11 = compose_with_default(&remap_7_to_9, &remap_9_to_11); + + let remap_11_to_9_tokens: Vec = remap_11_to_9 + .into_iter() + .map(Literal::u16_unsuffixed) + .collect(); + let remap_9_to_11_tokens: Vec = remap_9_to_11 + .into_iter() + .map(Literal::u16_unsuffixed) + .collect(); + let remap_11_to_7_tokens: Vec = remap_11_to_7 + .into_iter() + .map(Literal::u16_unsuffixed) + .collect(); + let remap_7_to_11_tokens: Vec = remap_7_to_11 + .into_iter() + .map(Literal::u16_unsuffixed) + .collect(); + + quote! { + use pumpkin_util::version::MinecraftVersion; + + pub static ITEM_ID_REMAP_1_21_11_TO_1_21_9: &[u16] = &[#(#remap_11_to_9_tokens),*]; + pub static ITEM_ID_REMAP_1_21_11_TO_1_21_7: &[u16] = &[#(#remap_11_to_7_tokens),*]; + pub static ITEM_ID_REMAP_1_21_9_TO_1_21_11: &[u16] = &[#(#remap_9_to_11_tokens),*]; + pub static ITEM_ID_REMAP_1_21_7_TO_1_21_11: &[u16] = &[#(#remap_7_to_11_tokens),*]; + + #[must_use] + pub fn remap_item_id_for_version(item_id: u16, version: MinecraftVersion) -> u16 { + match version { + MinecraftVersion::V_1_21_7 => ITEM_ID_REMAP_1_21_11_TO_1_21_7 + .get(usize::from(item_id)) + .copied() + .unwrap_or(item_id), + MinecraftVersion::V_1_21_9 => ITEM_ID_REMAP_1_21_11_TO_1_21_9 + .get(usize::from(item_id)) + .copied() + .unwrap_or(item_id), + _ => item_id, + } + } + + #[must_use] + pub fn remap_item_id_from_version(item_id: u16, version: MinecraftVersion) -> u16 { + match version { + MinecraftVersion::V_1_21_7 => ITEM_ID_REMAP_1_21_7_TO_1_21_11 + .get(usize::from(item_id)) + .copied() + .unwrap_or(item_id), + MinecraftVersion::V_1_21_9 => ITEM_ID_REMAP_1_21_9_TO_1_21_11 + .get(usize::from(item_id)) + .copied() + .unwrap_or(item_id), + _ => item_id, + } + } + } +} diff --git a/pumpkin-data/build/tracked_data.rs b/pumpkin-data/build/tracked_data.rs index f9d08f78c..d561db060 100644 --- a/pumpkin-data/build/tracked_data.rs +++ b/pumpkin-data/build/tracked_data.rs @@ -45,7 +45,7 @@ pub(crate) fn build() -> TokenStream { impl TrackedId { pub fn get(&self, version: &MinecraftVersion) -> u8 { match version { - MinecraftVersion::V_1_21_7 => self.v1_21_7, + MinecraftVersion::V_1_21_7 | MinecraftVersion::V_1_21_9 => self.v1_21_7, _ => self.latest, } } diff --git a/pumpkin-data/src/lib.rs b/pumpkin-data/src/lib.rs index 90d774650..f5c83cdb8 100644 --- a/pumpkin-data/src/lib.rs +++ b/pumpkin-data/src/lib.rs @@ -150,6 +150,14 @@ pub mod fluid; #[path = "generated/block.rs"] pub mod block_properties; +#[rustfmt::skip] +#[path = "generated/block_state_remap.rs"] +pub mod block_state_remap; + +#[rustfmt::skip] +#[path = "generated/item_id_remap.rs"] +pub mod item_id_remap; + #[rustfmt::skip] #[path = "generated/tag.rs"] pub mod tag; diff --git a/pumpkin-protocol/src/codec/item_stack_seralizer.rs b/pumpkin-protocol/src/codec/item_stack_seralizer.rs index 177e6d5a1..79e843052 100644 --- a/pumpkin-protocol/src/codec/item_stack_seralizer.rs +++ b/pumpkin-protocol/src/codec/item_stack_seralizer.rs @@ -1,7 +1,10 @@ use crate::VarInt; use crate::codec::data_component::{deserialize, serialize}; +use crate::ser::{WritingError, serializer}; use pumpkin_data::data_component::DataComponent; use pumpkin_data::item::Item; +use pumpkin_data::item_id_remap::{remap_item_id_for_version, remap_item_id_from_version}; +use pumpkin_util::version::MinecraftVersion; use pumpkin_world::item::ItemStack; use serde::ser::SerializeStruct; use serde::{ @@ -12,6 +15,53 @@ use std::borrow::Cow; pub struct ItemStackSerializer<'a>(pub Cow<'a, ItemStack>); +fn item_component_counts(stack: &ItemStack) -> (u8, u8) { + let mut to_add = 0u8; + let mut to_remove = 0u8; + + for (_id, data) in &stack.patch { + if data.is_none() { + to_remove += 1; + } else { + to_add += 1; + } + } + + (to_add, to_remove) +} + +fn serialize_item_stack_with_id( + stack: &ItemStack, + item_id: u16, + serializer: S, +) -> Result { + if stack.is_empty() { + VarInt(0).serialize(serializer) + } else { + let (to_add, to_remove) = item_component_counts(stack); + let mut seq = serializer.serialize_struct("", 0)?; + seq.serialize_field::("", &VarInt::from(stack.item_count))?; + seq.serialize_field::("", &VarInt::from(item_id))?; + seq.serialize_field::("", &VarInt::from(to_add))?; + seq.serialize_field::("", &VarInt::from(to_remove))?; + + for (id, data) in &stack.patch { + if let Some(data) = data { + seq.serialize_field::("", &VarInt::from(id.to_id()))?; + serialize(*id, data.as_ref(), &mut seq)?; + } + } + + for (id, data) in &stack.patch { + if data.is_none() { + seq.serialize_field::("", &VarInt::from(id.to_id()))?; + } + } + + seq.end() + } +} + impl<'de> Deserialize<'de> for ItemStackSerializer<'static> { fn deserialize>(deserializer: D) -> Result { struct Visitor; @@ -93,48 +143,37 @@ impl<'de> Deserialize<'de> for ItemStackSerializer<'static> { impl Serialize for ItemStackSerializer<'_> { fn serialize(&self, serializer: S) -> Result { - if self.0.is_empty() { - VarInt(0).serialize(serializer) - } else { - let calc = || { - let mut to_add = 0u8; - let mut to_remove = 0u8; - for (_id, data) in &self.0.patch { - if data.is_none() { - to_remove += 1; - } else { - to_add += 1; - } - } - (to_add, to_remove) - }; - let (to_add, to_remove) = calc(); - let mut seq = serializer.serialize_struct("", 0)?; - seq.serialize_field::("", &VarInt::from(self.0.item_count))?; - seq.serialize_field::("", &VarInt::from(self.0.item.id))?; - seq.serialize_field::("", &VarInt::from(to_add))?; - seq.serialize_field::("", &VarInt::from(to_remove))?; - for (id, data) in &self.0.patch { - if let Some(data) = data { - seq.serialize_field::("", &VarInt::from(id.to_id()))?; - serialize(*id, data.as_ref(), &mut seq)?; - } - } - for (id, data) in &self.0.patch { - if data.is_none() { - seq.serialize_field::("", &VarInt::from(id.to_id()))?; - } - } - seq.end() - } + serialize_item_stack_with_id(self.0.as_ref(), self.0.item.id, serializer) } } impl ItemStackSerializer<'_> { + pub fn write_with_version( + &self, + write: impl std::io::Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let remapped_item_id = remap_item_id_for_version(self.0.item.id, *version); + let mut network_serializer = serializer::Serializer::new(write); + serialize_item_stack_with_id(self.0.as_ref(), remapped_item_id, &mut network_serializer) + } + #[must_use] pub fn to_stack(self) -> ItemStack { self.0.into_owned() } + + #[must_use] + pub fn to_stack_for_version(self, version: &MinecraftVersion) -> ItemStack { + let mut stack = self.0.into_owned(); + if stack.is_empty() { + return stack; + } + + let remapped_item_id = remap_item_id_from_version(stack.item.id, *version); + stack.item = Item::from_id(remapped_item_id).unwrap_or(&Item::AIR); + stack + } } impl From for ItemStackSerializer<'_> { diff --git a/pumpkin-protocol/src/java/client/play/block_update.rs b/pumpkin-protocol/src/java/client/play/block_update.rs index 1f4c92232..92bf45160 100644 --- a/pumpkin-protocol/src/java/client/play/block_update.rs +++ b/pumpkin-protocol/src/java/client/play/block_update.rs @@ -1,16 +1,20 @@ +use pumpkin_data::block_state_remap::remap_block_state_for_version; use pumpkin_data::packet::clientbound::PLAY_BLOCK_UPDATE; use pumpkin_util::math::position::BlockPos; +use pumpkin_util::version::MinecraftVersion; use pumpkin_macros::java_packet; -use serde::Serialize; +use std::io::Write; -use crate::VarInt; +use crate::{ + ClientPacket, VarInt, + ser::{NetworkWriteExt, WritingError}, +}; /// Updates a single block state at a specific location in the world. /// /// This is the most common way to sync world changes to the client, such as /// when a player places a block, a fluid flows, or a redstone component toggles. -#[derive(Serialize)] #[java_packet(PLAY_BLOCK_UPDATE)] pub struct CBlockUpdate { /// The world coordinates of the block being updated. @@ -25,3 +29,21 @@ impl CBlockUpdate { Self { location, state_id } } } + +impl ClientPacket for CBlockUpdate { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + write.write_block_pos(&self.location)?; + + let remapped_state = u16::try_from(self.state_id.0).map_or(self.state_id.0, |state_id| { + i32::from(remap_block_state_for_version(state_id, *version)) + }); + write.write_var_int(&VarInt(remapped_state))?; + + Ok(()) + } +} diff --git a/pumpkin-protocol/src/java/client/play/chunk_data.rs b/pumpkin-protocol/src/java/client/play/chunk_data.rs index 2baec1655..27fc9e98a 100644 --- a/pumpkin-protocol/src/java/client/play/chunk_data.rs +++ b/pumpkin-protocol/src/java/client/play/chunk_data.rs @@ -1,6 +1,7 @@ use crate::WritingError; use crate::codec::bit_set::BitSet; use crate::{ClientPacket, VarInt, ser::NetworkWriteExt}; +use pumpkin_data::block_state_remap::remap_block_state_for_version; use pumpkin_data::packet::clientbound::PLAY_LEVEL_CHUNK_WITH_LIGHT; use pumpkin_macros::java_packet; use pumpkin_nbt::END_ID; @@ -23,7 +24,7 @@ impl ClientPacket for CChunkData<'_> { fn write_packet_data( &self, write: impl Write, - _version: &MinecraftVersion, + version: &MinecraftVersion, ) -> Result<(), WritingError> { let mut write = write; @@ -57,7 +58,34 @@ impl ClientPacket for CChunkData<'_> { let non_empty_block_count = block_palette.non_air_block_count() as i16; blocks_and_biomes_buf.write_i16_be(non_empty_block_count)?; - let block_network = block_palette.convert_network(); + let mut block_network = block_palette.convert_network(); + match &mut block_network.palette { + NetworkPalette::Single(registry_id) => { + *registry_id = remap_block_state_for_version(*registry_id, *version); + } + NetworkPalette::Indirect(palette) => { + for registry_id in palette.iter_mut() { + *registry_id = remap_block_state_for_version(*registry_id, *version); + } + } + NetworkPalette::Direct => { + let bits_per_entry = usize::from(block_network.bits_per_entry); + let values_per_i64 = 64 / bits_per_entry; + let id_mask = (1u64 << bits_per_entry) - 1; + + for packed_word in &mut block_network.packed_data { + let mut remapped_word = 0u64; + let packed_word_u64 = *packed_word as u64; + for index in 0..values_per_i64 { + let shift = index * bits_per_entry; + let state_id = ((packed_word_u64 >> shift) & id_mask) as u16; + let remapped_id = remap_block_state_for_version(state_id, *version); + remapped_word |= u64::from(remapped_id) << shift; + } + *packed_word = remapped_word as i64; + } + } + } blocks_and_biomes_buf.write_u8(block_network.bits_per_entry)?; match block_network.palette { diff --git a/pumpkin-protocol/src/java/client/play/entity_metadata.rs b/pumpkin-protocol/src/java/client/play/entity_metadata.rs index 6c2302337..7c55b72a7 100644 --- a/pumpkin-protocol/src/java/client/play/entity_metadata.rs +++ b/pumpkin-protocol/src/java/client/play/entity_metadata.rs @@ -1,6 +1,8 @@ +use std::io::Cursor; + use pumpkin_data::{ - meta_data_type::MetaDataType, packet::clientbound::PLAY_SET_ENTITY_DATA, - tracked_data::TrackedId, + block_state_remap::remap_block_state_for_version, meta_data_type::MetaDataType, + packet::clientbound::PLAY_SET_ENTITY_DATA, tracked_data::TrackedId, }; use pumpkin_macros::java_packet; use serde::Serialize; @@ -10,6 +12,36 @@ use crate::{ ser::{NetworkWriteExt, WritingError, network_serialize_no_prefix, serializer}, }; +const fn remap_metadata_type_id_for_version( + type_id: i32, + version: pumpkin_util::version::MinecraftVersion, +) -> Option { + use pumpkin_util::version::MinecraftVersion; + + match version { + // 1.21.7 / 1.21.8 (protocol 772) has a different metadata type table than 1.21.11. + MinecraftVersion::V_1_21_7 => match type_id { + // `compound_tag` exists at 16 in 1.21.7, so later ids are shifted. + 16..=27 => Some(type_id + 1), + // 1.21.7 has no copper/weathering/profile/arm metadata types. + 28 | 33 | 34 | 37 | 38 => None, + // `vector_3f` and `quaternion_f` are lower in 1.21.7. + 35 => Some(33), + 36 => Some(34), + _ => Some(type_id), + }, + // 1.21.9 / 1.21.10 (protocol 773) is close to latest but lacks some tail variants. + MinecraftVersion::V_1_21_9 => match type_id { + // Everything after that is shifted by one. + 29..=37 => Some(type_id - 1), + // 1.21.11-only variants. + 28 | 38 => None, + _ => Some(type_id), + }, + _ => Some(type_id), + } +} + /// Updates the "Data Tracker" values for an entity. /// /// Entity Metadata (or `DataWatchers`) controls persistent visual states that @@ -65,8 +97,34 @@ impl Metadata { return Ok(()); } + let Some(remapped_type_id) = remap_metadata_type_id_for_version(self.r#type.0, *version) + else { + // Metadata type does not exist in this protocol version. + return Ok(()); + }; + writer.write_u8(resolved_index)?; - self.r#type.encode(&mut writer)?; + writer.write_var_int(&VarInt(remapped_type_id))?; + + if self.r#type.0 == MetaDataType::BlockState as i32 { + let mut serialized_value = Vec::new(); + { + let mut serializer = serializer::Serializer::new(&mut serialized_value); + self.value + .serialize(&mut serializer) + .map_err(|e| WritingError::Serde(e.to_string()))?; + }; + + let mut cursor = Cursor::new(serialized_value); + let decoded_state = VarInt::decode(&mut cursor).map_err(|e| { + WritingError::Message(format!("Failed to decode block state metadata: {e}")) + })?; + let remapped_state = u16::try_from(decoded_state.0).map_or(decoded_state, |state_id| { + VarInt(i32::from(remap_block_state_for_version(state_id, *version))) + }); + writer.write_var_int(&remapped_state)?; + return Ok(()); + } let mut serializer = serializer::Serializer::new(&mut writer); self.value diff --git a/pumpkin-protocol/src/java/client/play/level_event.rs b/pumpkin-protocol/src/java/client/play/level_event.rs index 0a06db80f..2b1af1fbb 100644 --- a/pumpkin-protocol/src/java/client/play/level_event.rs +++ b/pumpkin-protocol/src/java/client/play/level_event.rs @@ -1,14 +1,21 @@ +use std::io::Write; + +use crate::{ + ClientPacket, + ser::{NetworkWriteExt, WritingError}, +}; +use pumpkin_data::block_state_remap::remap_block_state_for_version; use pumpkin_data::packet::clientbound::PLAY_LEVEL_EVENT; +use pumpkin_data::world::WorldEvent; use pumpkin_macros::java_packet; use pumpkin_util::math::position::BlockPos; -use serde::{Deserialize, Serialize}; +use pumpkin_util::version::MinecraftVersion; /// Triggers a specific sound or particle effect at a world location. /// /// This packet handles a wide variety of "world-level" events, such as /// block breaking particles, firework explosions, or ambient sounds /// like doors opening and portals humming. -#[derive(Serialize, Deserialize)] #[java_packet(PLAY_LEVEL_EVENT)] pub struct CLevelEvent { /// The ID of the event to trigger. @@ -41,3 +48,27 @@ impl CLevelEvent { } } } + +impl ClientPacket for CLevelEvent { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + write.write_i32_be(self.event)?; + write.write_block_pos(&self.location)?; + + let data = if self.event == WorldEvent::BlockBroken as i32 { + u16::try_from(self.data).map_or(self.data, |state_id| { + i32::from(remap_block_state_for_version(state_id, *version)) + }) + } else { + self.data + }; + write.write_i32_be(data)?; + write.write_bool(self.disable_relative_volume)?; + + Ok(()) + } +} diff --git a/pumpkin-protocol/src/java/client/play/multi_block_update.rs b/pumpkin-protocol/src/java/client/play/multi_block_update.rs index c647c4bdf..c1c84e947 100644 --- a/pumpkin-protocol/src/java/client/play/multi_block_update.rs +++ b/pumpkin-protocol/src/java/client/play/multi_block_update.rs @@ -1,13 +1,19 @@ +use pumpkin_data::block_state_remap::remap_block_state_for_version; use pumpkin_data::packet::clientbound::PLAY_SECTION_BLOCKS_UPDATE; use pumpkin_util::math::{ position::{BlockPos, chunk_section_from_pos, pack_local_chunk_section}, vector3::{self}, }; +use pumpkin_util::version::MinecraftVersion; use pumpkin_macros::java_packet; -use serde::{Serialize, ser::SerializeTuple}; +use std::io::Write; -use crate::codec::{var_int::VarInt, var_long::VarLong}; +use crate::{ + ClientPacket, + codec::{var_int::VarInt, var_long::VarLong}, + ser::{NetworkWriteExt, WritingError}, +}; /// Updates multiple blocks within a single 16x16x16 chunk section. /// @@ -45,18 +51,25 @@ impl CMultiBlockUpdate { } } } - -impl Serialize for CMultiBlockUpdate { - fn serialize(&self, serializer: S) -> Result { - let mut tuple = serializer.serialize_tuple(2 + self.updates.len())?; - - tuple.serialize_element(&self.chunk_section)?; - tuple.serialize_element(&VarInt(self.updates.len() as i32))?; +impl ClientPacket for CMultiBlockUpdate { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + write.write_i64_be(self.chunk_section)?; + write.write_var_int(&VarInt(self.updates.len() as i32))?; for update in &self.updates { - tuple.serialize_element(update)?; + let packed_update = update.0 as u64; + let local_pos = packed_update & 0xFFF; + let state_id = (packed_update >> 12) as u16; + let remapped_state_id = remap_block_state_for_version(state_id, *version); + let remapped_packed = (u64::from(remapped_state_id) << 12) | local_pos; + write.write_var_long(&VarLong(remapped_packed as i64))?; } - tuple.end() + Ok(()) } } diff --git a/pumpkin-protocol/src/java/client/play/set_container_content.rs b/pumpkin-protocol/src/java/client/play/set_container_content.rs index 3e8192651..e7f124b19 100644 --- a/pumpkin-protocol/src/java/client/play/set_container_content.rs +++ b/pumpkin-protocol/src/java/client/play/set_container_content.rs @@ -1,11 +1,13 @@ +use std::io::Write; + use crate::VarInt; use crate::codec::item_stack_seralizer::ItemStackSerializer; +use crate::{ClientPacket, WritingError, ser::NetworkWriteExt}; use pumpkin_data::packet::clientbound::PLAY_CONTAINER_SET_CONTENT; use pumpkin_macros::java_packet; -use serde::Serialize; +use pumpkin_util::version::MinecraftVersion; -#[derive(Serialize)] #[java_packet(PLAY_CONTAINER_SET_CONTENT)] pub struct CSetContainerContent<'a> { pub window_id: VarInt, @@ -30,3 +32,29 @@ impl<'a> CSetContainerContent<'a> { } } } + +impl ClientPacket for CSetContainerContent<'_> { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + + write.write_var_int(&self.window_id)?; + write.write_var_int(&self.state_id)?; + let slot_count = i32::try_from(self.slot_data.len()).map_err(|_| { + WritingError::Message(format!( + "{} slot entries do not fit in VarInt", + self.slot_data.len() + )) + })?; + write.write_var_int(&VarInt(slot_count))?; + for stack in self.slot_data { + stack.write_with_version(&mut write, version)?; + } + self.carried_item.write_with_version(&mut write, version)?; + + Ok(()) + } +} diff --git a/pumpkin-protocol/src/java/client/play/set_container_slot.rs b/pumpkin-protocol/src/java/client/play/set_container_slot.rs index 17f4e56fa..a17f73f76 100644 --- a/pumpkin-protocol/src/java/client/play/set_container_slot.rs +++ b/pumpkin-protocol/src/java/client/play/set_container_slot.rs @@ -1,11 +1,13 @@ +use std::io::Write; + use crate::VarInt; use crate::codec::item_stack_seralizer::ItemStackSerializer; +use crate::{ClientPacket, WritingError, ser::NetworkWriteExt}; use pumpkin_data::packet::clientbound::PLAY_CONTAINER_SET_SLOT; use pumpkin_macros::java_packet; -use serde::Serialize; +use pumpkin_util::version::MinecraftVersion; -#[derive(Serialize)] #[java_packet(PLAY_CONTAINER_SET_SLOT)] pub struct CSetContainerSlot<'a> { pub window_id: i8, @@ -30,3 +32,20 @@ impl<'a> CSetContainerSlot<'a> { } } } + +impl ClientPacket for CSetContainerSlot<'_> { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + + write.write_i8(self.window_id)?; + write.write_var_int(&self.state_id)?; + write.write_i16_be(self.slot)?; + self.slot_data.write_with_version(&mut write, version)?; + + Ok(()) + } +} diff --git a/pumpkin-protocol/src/java/client/play/set_cursor_slot.rs b/pumpkin-protocol/src/java/client/play/set_cursor_slot.rs index d1b7a8c57..00f8165cc 100644 --- a/pumpkin-protocol/src/java/client/play/set_cursor_slot.rs +++ b/pumpkin-protocol/src/java/client/play/set_cursor_slot.rs @@ -1,10 +1,12 @@ +use std::io::Write; + use crate::codec::item_stack_seralizer::ItemStackSerializer; +use crate::{ClientPacket, WritingError}; use pumpkin_data::packet::clientbound::PLAY_SET_CURSOR_ITEM; use pumpkin_macros::java_packet; -use serde::Serialize; +use pumpkin_util::version::MinecraftVersion; -#[derive(Serialize)] #[java_packet(PLAY_SET_CURSOR_ITEM)] pub struct CSetCursorItem<'a> { pub stack: &'a ItemStackSerializer<'a>, @@ -16,3 +18,13 @@ impl<'a> CSetCursorItem<'a> { Self { stack } } } + +impl ClientPacket for CSetCursorItem<'_> { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + self.stack.write_with_version(write, version) + } +} diff --git a/pumpkin-protocol/src/java/client/play/set_equipment.rs b/pumpkin-protocol/src/java/client/play/set_equipment.rs index 7f4fb1836..08bf30a14 100644 --- a/pumpkin-protocol/src/java/client/play/set_equipment.rs +++ b/pumpkin-protocol/src/java/client/play/set_equipment.rs @@ -1,13 +1,9 @@ use std::io::Write; -use crate::{ - WritingError, - ser::{NetworkWriteExt, serializer::Serializer}, -}; +use crate::{WritingError, ser::NetworkWriteExt}; use pumpkin_data::packet::clientbound::PLAY_SET_EQUIPMENT; use pumpkin_macros::java_packet; use pumpkin_util::version::MinecraftVersion; -use serde::Serialize; use crate::{ ClientPacket, @@ -37,7 +33,7 @@ impl ClientPacket for CSetEquipment { fn write_packet_data( &self, write: impl Write, - _version: &MinecraftVersion, + version: &MinecraftVersion, ) -> Result<(), WritingError> { let mut write = write; @@ -50,11 +46,7 @@ impl ClientPacket for CSetEquipment { } else { write.write_i8(*slot | -128)?; } - let mut serializer = Serializer::new(&mut write); - equipment - .1 - .serialize(&mut serializer) - .expect("Could not serialize `EquipmentSlot`"); + equipment.1.write_with_version(&mut write, version)?; } Ok(()) diff --git a/pumpkin-protocol/src/java/client/play/set_player_inventory.rs b/pumpkin-protocol/src/java/client/play/set_player_inventory.rs index 6bbcf340f..107dc6783 100644 --- a/pumpkin-protocol/src/java/client/play/set_player_inventory.rs +++ b/pumpkin-protocol/src/java/client/play/set_player_inventory.rs @@ -1,11 +1,13 @@ +use std::io::Write; + use crate::VarInt; use crate::codec::item_stack_seralizer::ItemStackSerializer; +use crate::{ClientPacket, WritingError, ser::NetworkWriteExt}; use pumpkin_data::packet::clientbound::PLAY_SET_PLAYER_INVENTORY; use pumpkin_macros::java_packet; -use serde::Serialize; +use pumpkin_util::version::MinecraftVersion; -#[derive(Serialize)] #[java_packet(PLAY_SET_PLAYER_INVENTORY)] pub struct CSetPlayerInventory<'a> { pub slot: VarInt, @@ -18,3 +20,15 @@ impl<'a> CSetPlayerInventory<'a> { Self { slot, item } } } + +impl ClientPacket for CSetPlayerInventory<'_> { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + write.write_var_int(&self.slot)?; + self.item.write_with_version(write, version) + } +} diff --git a/pumpkin-protocol/src/java/client/play/spawn_entity.rs b/pumpkin-protocol/src/java/client/play/spawn_entity.rs index 8222450a2..cad34acf9 100644 --- a/pumpkin-protocol/src/java/client/play/spawn_entity.rs +++ b/pumpkin-protocol/src/java/client/play/spawn_entity.rs @@ -1,5 +1,7 @@ use std::io::Write; +use pumpkin_data::block_state_remap::remap_block_state_for_version; +use pumpkin_data::entity::EntityType; use pumpkin_data::packet::clientbound::PLAY_ADD_ENTITY; use pumpkin_macros::java_packet; use pumpkin_util::{math::vector3::Vector3, version::MinecraftVersion}; @@ -10,6 +12,44 @@ use crate::{ ser::{NetworkWriteExt, WritingError}, }; +const fn remap_entity_type_for_version(type_id: i32, version: MinecraftVersion) -> i32 { + use MinecraftVersion::{V_1_21_7, V_1_21_9}; + + match version { + // ViaVersion mappings-1.21.9to1.21.11.nbt (inverse, latest -> 1.21.9) + // + // mapped by shifts: + // 0..=19 => +0 + // 21..=87 => -1 + // 89..=96 => -2 + // 98..=151 => -3 + // 153..=156 => -4 + // unsupported ids (20, 88, 97, 152) fall back to identity. + // getNewIdOrDefault(id, id) behavior for unmapped ids. + V_1_21_9 => match type_id { + 21..=87 => type_id - 1, + 89..=96 => type_id - 2, + 98..=151 => type_id - 3, + 153..=156 => type_id - 4, + _ => type_id, + }, + // ViaVersion mappings-1.21.7to1.21.9.nbt + mappings-1.21.9to1.21.11.nbt + // (inverse composition, latest -> 1.21.7/1.21.8 protocol 772). + // + // unsupported ids (20, 28, 83, 88, 97, 152) keep identity, same rationale as above. + V_1_21_7 => match type_id { + 21..=27 => type_id - 1, + 29..=82 => type_id - 2, + 84..=87 => type_id - 3, + 89..=96 => type_id - 4, + 98..=151 => type_id - 5, + 153..=156 => type_id - 6, + _ => type_id, + }, + _ => type_id, + } +} + #[java_packet(PLAY_ADD_ENTITY)] pub struct CSpawnEntity { pub entity_id: VarInt, @@ -61,7 +101,8 @@ impl ClientPacket for CSpawnEntity { write.write_var_int(&self.entity_id)?; write.write_uuid(&self.entity_uuid)?; - write.write_var_int(&self.r#type)?; + let remapped_type = VarInt(remap_entity_type_for_version(self.r#type.0, *version)); + write.write_var_int(&remapped_type)?; write.write_f64_be(self.position.x)?; write.write_f64_be(self.position.y)?; @@ -75,7 +116,14 @@ impl ClientPacket for CSpawnEntity { write.write_u8(self.yaw)?; write.write_u8(self.head_yaw)?; - write.write_var_int(&self.data)?; + let data = if self.r#type.0 == i32::from(EntityType::FALLING_BLOCK.id) { + u16::try_from(self.data.0).map_or(self.data, |state_id| { + VarInt(i32::from(remap_block_state_for_version(state_id, *version))) + }) + } else { + self.data + }; + write.write_var_int(&data)?; if version <= &MinecraftVersion::V_1_21_7 { write.write_i16_be(self.velocity.0.x as i16)?; @@ -86,3 +134,153 @@ impl ClientPacket for CSpawnEntity { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::remap_entity_type_for_version; + use pumpkin_util::version::MinecraftVersion; + + #[test] + fn remaps_entity_types_for_1_21_9_like_viaversion() { + assert_eq!( + remap_entity_type_for_version(0, MinecraftVersion::V_1_21_9), + 0 + ); + assert_eq!( + remap_entity_type_for_version(19, MinecraftVersion::V_1_21_9), + 19 + ); + assert_eq!( + remap_entity_type_for_version(20, MinecraftVersion::V_1_21_9), + 20 + ); + assert_eq!( + remap_entity_type_for_version(21, MinecraftVersion::V_1_21_9), + 20 + ); + assert_eq!( + remap_entity_type_for_version(87, MinecraftVersion::V_1_21_9), + 86 + ); + assert_eq!( + remap_entity_type_for_version(88, MinecraftVersion::V_1_21_9), + 88 + ); + assert_eq!( + remap_entity_type_for_version(89, MinecraftVersion::V_1_21_9), + 87 + ); + assert_eq!( + remap_entity_type_for_version(96, MinecraftVersion::V_1_21_9), + 94 + ); + assert_eq!( + remap_entity_type_for_version(97, MinecraftVersion::V_1_21_9), + 97 + ); + assert_eq!( + remap_entity_type_for_version(98, MinecraftVersion::V_1_21_9), + 95 + ); + assert_eq!( + remap_entity_type_for_version(151, MinecraftVersion::V_1_21_9), + 148 + ); + assert_eq!( + remap_entity_type_for_version(152, MinecraftVersion::V_1_21_9), + 152 + ); + assert_eq!( + remap_entity_type_for_version(153, MinecraftVersion::V_1_21_9), + 149 + ); + assert_eq!( + remap_entity_type_for_version(156, MinecraftVersion::V_1_21_9), + 152 + ); + } + + #[test] + fn remaps_entity_types_for_1_21_7_like_viaversion() { + assert_eq!( + remap_entity_type_for_version(0, MinecraftVersion::V_1_21_7), + 0 + ); + assert_eq!( + remap_entity_type_for_version(19, MinecraftVersion::V_1_21_7), + 19 + ); + assert_eq!( + remap_entity_type_for_version(20, MinecraftVersion::V_1_21_7), + 20 + ); + assert_eq!( + remap_entity_type_for_version(21, MinecraftVersion::V_1_21_7), + 20 + ); + assert_eq!( + remap_entity_type_for_version(27, MinecraftVersion::V_1_21_7), + 26 + ); + assert_eq!( + remap_entity_type_for_version(28, MinecraftVersion::V_1_21_7), + 28 + ); + assert_eq!( + remap_entity_type_for_version(29, MinecraftVersion::V_1_21_7), + 27 + ); + assert_eq!( + remap_entity_type_for_version(82, MinecraftVersion::V_1_21_7), + 80 + ); + assert_eq!( + remap_entity_type_for_version(83, MinecraftVersion::V_1_21_7), + 83 + ); + assert_eq!( + remap_entity_type_for_version(84, MinecraftVersion::V_1_21_7), + 81 + ); + assert_eq!( + remap_entity_type_for_version(87, MinecraftVersion::V_1_21_7), + 84 + ); + assert_eq!( + remap_entity_type_for_version(88, MinecraftVersion::V_1_21_7), + 88 + ); + assert_eq!( + remap_entity_type_for_version(89, MinecraftVersion::V_1_21_7), + 85 + ); + assert_eq!( + remap_entity_type_for_version(96, MinecraftVersion::V_1_21_7), + 92 + ); + assert_eq!( + remap_entity_type_for_version(97, MinecraftVersion::V_1_21_7), + 97 + ); + assert_eq!( + remap_entity_type_for_version(98, MinecraftVersion::V_1_21_7), + 93 + ); + assert_eq!( + remap_entity_type_for_version(151, MinecraftVersion::V_1_21_7), + 146 + ); + assert_eq!( + remap_entity_type_for_version(152, MinecraftVersion::V_1_21_7), + 152 + ); + assert_eq!( + remap_entity_type_for_version(153, MinecraftVersion::V_1_21_7), + 147 + ); + assert_eq!( + remap_entity_type_for_version(156, MinecraftVersion::V_1_21_7), + 150 + ); + } +} diff --git a/pumpkin-protocol/src/java/client/play/worldevent.rs b/pumpkin-protocol/src/java/client/play/worldevent.rs index 1aee184fe..24ee932be 100644 --- a/pumpkin-protocol/src/java/client/play/worldevent.rs +++ b/pumpkin-protocol/src/java/client/play/worldevent.rs @@ -1,14 +1,21 @@ +use std::io::Write; + +use crate::{ + ClientPacket, + ser::{NetworkWriteExt, WritingError}, +}; +use pumpkin_data::block_state_remap::remap_block_state_for_version; use pumpkin_data::packet::clientbound::PLAY_LEVEL_EVENT; +use pumpkin_data::world::WorldEvent; use pumpkin_util::math::position::BlockPos; +use pumpkin_util::version::MinecraftVersion; use pumpkin_macros::java_packet; -use serde::Serialize; /// Sent by the server to trigger a specific sound or particle effect at a world location. /// /// This is used for a wide variety of effects, from breaking blocks and firework /// explosions to splashing water or record playing. -#[derive(Serialize)] #[java_packet(PLAY_LEVEL_EVENT)] pub struct CWorldEvent { /// The ID of the event to trigger (e.g., 1000 for a bow shoot, 2001 for block break). @@ -42,3 +49,27 @@ impl CWorldEvent { } } } + +impl ClientPacket for CWorldEvent { + fn write_packet_data( + &self, + write: impl Write, + version: &MinecraftVersion, + ) -> Result<(), WritingError> { + let mut write = write; + write.write_i32_be(self.event)?; + write.write_block_pos(&self.location)?; + + let data = if self.event == WorldEvent::BlockBroken as i32 { + u16::try_from(self.data).map_or(self.data, |state_id| { + i32::from(remap_block_state_for_version(state_id, *version)) + }) + } else { + self.data + }; + write.write_i32_be(data)?; + write.write_bool(self.disable_relative_volume)?; + + Ok(()) + } +} diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 2059ceef3..65c7a82fa 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -1847,7 +1847,9 @@ impl JavaClient { } let is_negative = packet.slot < 0; let valid_slot = packet.slot >= 1 && packet.slot as usize <= 45; - let item_stack = packet.clicked_item.to_stack(); + let item_stack = packet + .clicked_item + .to_stack_for_version(&self.version.load()); let is_legal = item_stack.is_empty() || item_stack.item_count <= item_stack.get_max_stack_size();