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
This commit is contained in:
ChocoDev
2026-02-08 20:24:16 +07:00
committed by GitHub
parent 8015b73882
commit cef8a024e7
21 changed files with 1055 additions and 80 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -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<i32>,
}
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::<Vec<_>>()
}
_ => 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<u16> {
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<u16> {
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<Literal> = remap_11_to_9
.into_iter()
.map(Literal::u16_unsuffixed)
.collect();
let remap_11_to_7_tokens: Vec<Literal> = 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,
}
}
}
}

View File

@@ -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"),

View File

@@ -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<i32>,
}
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::<Vec<_>>()
}
_ => 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<u16> {
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<u16> {
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<u16> {
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<Literal> = remap_11_to_9
.into_iter()
.map(Literal::u16_unsuffixed)
.collect();
let remap_9_to_11_tokens: Vec<Literal> = remap_9_to_11
.into_iter()
.map(Literal::u16_unsuffixed)
.collect();
let remap_11_to_7_tokens: Vec<Literal> = remap_11_to_7
.into_iter()
.map(Literal::u16_unsuffixed)
.collect();
let remap_7_to_11_tokens: Vec<Literal> = 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,
}
}
}
}

View File

@@ -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,
}
}

View File

@@ -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;

View File

@@ -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<S: Serializer>(
stack: &ItemStack,
item_id: u16,
serializer: S,
) -> Result<S::Ok, S::Error> {
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>("", &VarInt::from(stack.item_count))?;
seq.serialize_field::<VarInt>("", &VarInt::from(item_id))?;
seq.serialize_field::<VarInt>("", &VarInt::from(to_add))?;
seq.serialize_field::<VarInt>("", &VarInt::from(to_remove))?;
for (id, data) in &stack.patch {
if let Some(data) = data {
seq.serialize_field::<VarInt>("", &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>("", &VarInt::from(id.to_id()))?;
}
}
seq.end()
}
}
impl<'de> Deserialize<'de> for ItemStackSerializer<'static> {
fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
@@ -93,48 +143,37 @@ impl<'de> Deserialize<'de> for ItemStackSerializer<'static> {
impl Serialize for ItemStackSerializer<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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>("", &VarInt::from(self.0.item_count))?;
seq.serialize_field::<VarInt>("", &VarInt::from(self.0.item.id))?;
seq.serialize_field::<VarInt>("", &VarInt::from(to_add))?;
seq.serialize_field::<VarInt>("", &VarInt::from(to_remove))?;
for (id, data) in &self.0.patch {
if let Some(data) = data {
seq.serialize_field::<VarInt>("", &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>("", &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<ItemStack> for ItemStackSerializer<'_> {

View File

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

View File

@@ -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 {

View File

@@ -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<i32> {
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<T> Metadata<T> {
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

View File

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

View File

@@ -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<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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(())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
);
}
}

View File

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

View File

@@ -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();