fix(protocol): encode and decode renamed item names (#2301)

This commit is contained in:
Niclas
2026-06-25 17:44:11 +02:00
committed by GitHub
parent 849cb05e07
commit 2fa7a1829b
8 changed files with 190 additions and 22 deletions

View File

@@ -62,6 +62,7 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
PotionContents => Some(PotionContentsImpl::read_data(data)?.to_dyn()),
Fireworks => Some(FireworksImpl::read_data(data)?.to_dyn()),
FireworkExplosion => Some(FireworkExplosionImpl::read_data(data)?.to_dyn()),
CustomName => Some(CustomNameImpl::read_data(data)?.to_dyn()),
ItemModel => Some(ItemModelImpl::read_data(data)?.to_dyn()),
Consumable => Some(ConsumableImpl::read_data(data)?.to_dyn()),
Equippable => Some(EquippableImpl::read_data(data)?.to_dyn()),
@@ -203,16 +204,22 @@ impl DataComponentImpl for UnbreakableImpl {
}
#[derive(Clone, Hash, PartialEq, Eq)]
pub struct CustomNameImpl {
// TODO make TextComponent
pub name: String,
pub name: TextComponent,
}
impl CustomNameImpl {
fn read_data(data: &NbtTag) -> Option<Self> {
data.extract_string().map(|name| Self {
name: TextComponent::text(name.to_string()),
})
}
}
impl DataComponentImpl for CustomNameImpl {
fn write_data(&self) -> NbtTag {
NbtTag::String(self.name.clone().into())
NbtTag::String(self.name.clone().get_text().into())
}
fn get_hash(&self) -> i32 {
get_str_hash(self.name.as_str()) as i32
get_str_hash(self.name.clone().get_text().as_str()) as i32
}
default_impl!(CustomName);

View File

@@ -395,7 +395,12 @@ impl ItemStack {
pub fn set_custom_name(&mut self, name: String) {
use crate::data_component_impl::CustomNameImpl;
let component = Some(CustomNameImpl { name }.to_dyn());
let component = Some(
CustomNameImpl {
name: pumpkin_util::text::TextComponent::text(name),
}
.to_dyn(),
);
if let Some(pos) = self
.patch
.iter()

View File

@@ -13,9 +13,10 @@ use pumpkin_data::data_component_impl::{
use pumpkin_data::effect::StatusEffect;
use pumpkin_data::entity::EntityType;
use pumpkin_data::sound::Sound;
use pumpkin_nbt::{serializer::NbtWriteHelperJava, tag::NbtTag};
use serde::de;
use serde::de::SeqAccess;
use serde::ser::SerializeStruct;
use serde::ser::{SerializeStruct, Serializer};
const MAX_STATUS_EFFECTS: usize = 128;
@@ -349,14 +350,28 @@ impl DataComponentCodec<Self> for ItemModelImpl {
impl DataComponentCodec<Self> for CustomNameImpl {
fn serialize<T: SerializeStruct>(&self, seq: &mut T) -> Result<(), T::Error> {
seq.serialize_field::<String>("", &self.name)
seq.serialize_field("", &NetworkTextNbtString(self.name.clone().get_text()))
}
fn deserialize<'a, A: SeqAccess<'a>>(seq: &mut A) -> Result<Self, A::Error> {
let name = seq
.next_element::<String>()?
.ok_or(de::Error::custom("No CustomNameImpl name string!"))?;
Ok(Self { name })
Ok(Self {
name: pumpkin_util::text::TextComponent::text(name),
})
}
}
struct NetworkTextNbtString(String);
impl serde::Serialize for NetworkTextNbtString {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut bytes = Vec::new();
NbtTag::String(self.0.clone().into_boxed_str())
.serialize(&mut NbtWriteHelperJava::new(&mut bytes))
.map_err(serde::ser::Error::custom)?;
serializer.serialize_bytes(&bytes)
}
}

View File

@@ -1,17 +1,21 @@
use crate::VarInt;
use crate::codec::data_component::{deserialize, serialize};
use crate::ser::{WritingError, serializer};
use crate::ser::{NetworkReadExt, ReadingError, WritingError, deserializer, serializer};
use pumpkin_data::data_component::DataComponent;
use pumpkin_data::data_component_impl::{CustomNameImpl, DataComponentImpl};
use pumpkin_data::item::Item;
use pumpkin_data::item_id_remap::{remap_item_id_for_version, remap_item_id_from_version};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_util::text::TextComponent;
use pumpkin_util::version::JavaMinecraftVersion;
use serde::ser::SerializeStruct;
use serde::{
Deserialize, Serialize, Serializer,
de::{self, SeqAccess},
de::{self, DeserializeSeed, SeqAccess},
};
use std::borrow::Cow;
use std::io::{Cursor, Read};
#[derive(Clone)]
pub struct ItemStackSerializer<'a>(pub Cow<'a, ItemStack>);
@@ -63,6 +67,70 @@ fn serialize_item_stack_with_id<S: Serializer>(
}
}
struct ComponentAccess<R: Read> {
deserializer: deserializer::Deserializer<R>,
}
impl<'de, R: Read> SeqAccess<'de> for ComponentAccess<R> {
type Error = ReadingError;
fn next_element_seed<T: DeserializeSeed<'de>>(
&mut self,
seed: T,
) -> Result<Option<T::Value>, Self::Error> {
seed.deserialize(&mut self.deserializer).map(Some)
}
}
fn read_component_id(read: &mut impl Read) -> Result<DataComponent, ReadingError> {
let id_val = read.get_var_int()?.0;
let id_u8 = id_val
.try_into()
.map_err(|_| ReadingError::Message(format!("Invalid component ID: {id_val}")))?;
DataComponent::try_from_id(id_u8)
.ok_or_else(|| ReadingError::Message(format!("Unknown component ID: {id_val}")))
}
fn decode_custom_name(component_data: &[u8]) -> Result<Box<dyn DataComponentImpl>, ReadingError> {
let mut cursor = Cursor::new(component_data);
let mut nbt_reader = pumpkin_nbt::deserializer::NbtReadHelperJava::new(&mut cursor);
let tag = NbtTag::deserialize(&mut nbt_reader)
.map_err(|err| ReadingError::Message(format!("Failed to decode CustomName NBT: {err}")))?;
let name = match tag {
NbtTag::String(name) => TextComponent::text(name.to_string()),
NbtTag::Compound(compound) => compound
.get_string("text")
.map_or_else(TextComponent::empty, |name| {
TextComponent::text(name.to_string())
}),
_ => TextComponent::empty(),
};
Ok(CustomNameImpl { name }.to_dyn())
}
fn read_length_prefixed_component(
read: &mut impl Read,
) -> Result<(DataComponent, Box<dyn DataComponentImpl>), ReadingError> {
let id = read_component_id(read)?;
let byte_len = read.get_var_int()?.0;
let byte_len = byte_len
.try_into()
.map_err(|_| ReadingError::Message("Negative component data length".into()))?;
let component_data = read.read_boxed_slice(byte_len)?;
let component_impl = if id == DataComponent::CustomName {
decode_custom_name(component_data.as_ref())?
} else {
let cursor = Cursor::new(component_data);
let mut access = ComponentAccess {
deserializer: deserializer::Deserializer::new(cursor),
};
deserialize(id, &mut access)?
};
Ok((id, component_impl))
}
impl<'de> Deserialize<'de> for ItemStackSerializer<'static> {
fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
@@ -159,6 +227,63 @@ impl Serialize for ItemStackSerializer<'_> {
}
impl ItemStackSerializer<'_> {
pub fn read_length_prefixed_optional(
mut read: impl Read,
) -> Result<ItemStackSerializer<'static>, ReadingError> {
const MAX_COMPONENTS: i32 = 256;
let item_count = read.get_var_int()?;
if item_count.0 == 0 {
return Ok(ItemStackSerializer(Cow::Borrowed(ItemStack::EMPTY)));
}
let item_count_u8 = item_count
.0
.try_into()
.map_err(|_| ReadingError::Message("Invalid item count!".into()))?;
let item_id = read.get_var_int()?;
let num_to_add = read.get_var_int()?.0;
let num_to_remove = read.get_var_int()?.0;
if num_to_add < 0 || num_to_remove < 0 {
return Err(ReadingError::Message("Negative component count".into()));
}
let total_components = num_to_add
.checked_add(num_to_remove)
.ok_or_else(|| ReadingError::Message("Component count overflow".into()))?;
if total_components > MAX_COMPONENTS {
return Err(ReadingError::Message(
"Too many components in ItemStack patch".into(),
));
}
let mut patch = Vec::with_capacity(total_components as usize);
for _ in 0..num_to_add {
let (id, component_impl) = read_length_prefixed_component(&mut read)?;
patch.push((id, Some(component_impl)));
}
for _ in 0..num_to_remove {
patch.push((read_component_id(&mut read)?, None));
}
let item_id_u16 = item_id
.0
.try_into()
.map_err(|_| ReadingError::Message("Invalid item id!".into()))?;
Ok(ItemStackSerializer(Cow::Owned(
ItemStack::new_with_component(
item_count_u8,
Item::from_id(item_id_u16).unwrap_or(&Item::AIR),
patch,
),
)))
}
pub fn write_with_version(
&self,
write: impl std::io::Write,

View File

@@ -1,11 +1,26 @@
use pumpkin_data::packet::serverbound::PLAY_SET_CREATIVE_MODE_SLOT;
use pumpkin_macros::java_packet;
use pumpkin_util::version::JavaMinecraftVersion;
use crate::codec::item_stack_seralizer::ItemStackSerializer;
use crate::{
ServerPacket,
codec::item_stack_seralizer::ItemStackSerializer,
ser::{NetworkReadExt, ReadingError},
};
#[derive(serde::Deserialize)]
#[java_packet(PLAY_SET_CREATIVE_MODE_SLOT)]
pub struct SSetCreativeSlot {
pub slot: i16,
pub clicked_item: ItemStackSerializer<'static>,
}
impl ServerPacket for SSetCreativeSlot {
fn read(
mut read: impl std::io::Read,
_version: &JavaMinecraftVersion,
) -> Result<Self, ReadingError> {
let slot = read.get_i16_be()?;
let clicked_item = ItemStackSerializer::read_length_prefixed_optional(read)?;
Ok(Self { slot, clicked_item })
}
}

View File

@@ -7,7 +7,6 @@ use crate::item::{ItemBehaviour, ItemMetadata};
use pumpkin_data::data_component_impl::CustomNameImpl;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_util::text::TextComponent;
pub struct NameTagItem;
@@ -30,7 +29,7 @@ impl ItemBehaviour for NameTagItem {
&& let Some(name) = item.get_data_component::<CustomNameImpl>()
{
// TODO
entity.set_custom_name(TextComponent::text(name.name.clone()));
entity.set_custom_name(name.name.clone());
item.decrement_unless_creative(player.gamemode.load(), 1);
}
})

View File

@@ -293,7 +293,12 @@ impl JavaClient {
.await;
}
}
e.log();
error!(
"Failed to handle play packet id {} (payload {} bytes): {}",
packet.id,
packet.payload.len(),
e
);
}
}
}

View File

@@ -246,8 +246,7 @@ impl HostItemStack for PluginHostState {
.find(|(id, _)| *id == DataComponent::CustomName)
&& let Some(name_impl) = data.as_any().downcast_ref::<CustomNameImpl>()
{
let text = pumpkin_util::text::TextComponent::text(name_impl.name.clone());
return Ok(Some(self.add_text_component(text)?));
return Ok(Some(self.add_text_component(name_impl.name.clone())?));
}
Ok(None)
}
@@ -260,19 +259,17 @@ impl HostItemStack for PluginHostState {
let stack = self.get_item_stack(&res)?;
let mut stack = stack.lock().await;
if let Some(name_res) = name {
let text = text_component_from_resource(self, &name_res);
// This is a hack to get some string representation.
let name_str = format!("{:?}", text.0.content);
let name = text_component_from_resource(self, &name_res);
if let Some((_, data)) = stack
.patch
.iter_mut()
.find(|(id, _)| *id == DataComponent::CustomName)
{
*data = Some(Box::new(CustomNameImpl { name: name_str }));
*data = Some(Box::new(CustomNameImpl { name }));
} else {
stack.patch.push((
DataComponent::CustomName,
Some(Box::new(CustomNameImpl { name: name_str })),
Some(Box::new(CustomNameImpl { name })),
));
}
} else {