From 7574fa33c516ec2bed7233c01263fcaabd4066d3 Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Sat, 31 Dec 2022 22:59:22 -0800 Subject: [PATCH] Commands and recipe book packets (#183) Implement the commands and recipe book packets. Also reorganizes some modules in valence_protocol. --- crates/valence/src/client.rs | 2 +- crates/valence/src/lib.rs | 2 +- crates/valence/src/player_list.rs | 4 +- crates/valence_protocol/src/lib.rs | 3 - crates/valence_protocol/src/packets/s2c.rs | 33 +- .../src/packets/s2c/commands.rs | 432 ++++++++++++++++++ .../s2c/declare_recipes.rs} | 0 .../src/{ => packets/s2c}/particle.rs | 0 .../s2c/player_info_update.rs} | 0 .../src/packets/s2c/update_recipe_book.rs | 90 ++++ crates/valence_protocol/src/text.rs | 7 +- 11 files changed, 563 insertions(+), 10 deletions(-) create mode 100644 crates/valence_protocol/src/packets/s2c/commands.rs rename crates/valence_protocol/src/{recipe.rs => packets/s2c/declare_recipes.rs} (100%) rename crates/valence_protocol/src/{ => packets/s2c}/particle.rs (100%) rename crates/valence_protocol/src/{player_list.rs => packets/s2c/player_info_update.rs} (100%) create mode 100644 crates/valence_protocol/src/packets/s2c/update_recipe_book.rs diff --git a/crates/valence/src/client.rs b/crates/valence/src/client.rs index 298b191..a7ae287 100644 --- a/crates/valence/src/client.rs +++ b/crates/valence/src/client.rs @@ -13,6 +13,7 @@ use rayon::iter::ParallelIterator; use tokio::sync::OwnedSemaphorePermit; use tracing::{info, warn}; use uuid::Uuid; +use valence_protocol::packets::s2c::particle::{Particle, ParticleS2c}; use valence_protocol::packets::s2c::play::{ AcknowledgeBlockChange, ClearTitles, CloseContainerS2c, CombatDeath, DisconnectPlay, EntityAnimationS2c, EntityEvent, GameEvent, KeepAliveS2c, LoginPlayOwned, OpenScreen, @@ -22,7 +23,6 @@ use valence_protocol::packets::s2c::play::{ SetSubtitleText, SetTitleAnimationTimes, SetTitleText, SynchronizePlayerPosition, SystemChatMessage, UnloadChunk, UpdateAttributes, UpdateTime, }; -use valence_protocol::particle::{Particle, ParticleS2c}; use valence_protocol::types::{ AttributeProperty, DisplayedSkinParts, GameEventKind, GameMode, SyncPlayerPosLookFlags, }; diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index c43a9a9..9d5d06b 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -135,8 +135,8 @@ pub mod prelude { pub use valence_protocol::block::{PropName, PropValue}; pub use valence_protocol::entity_meta::Pose; pub use valence_protocol::ident::IdentError; + pub use valence_protocol::packets::s2c::particle::Particle; pub use valence_protocol::packets::s2c::play::SetTitleAnimationTimes; - pub use valence_protocol::particle::Particle; pub use valence_protocol::text::Color; pub use valence_protocol::types::{GameMode, Hand, SoundCategory}; pub use valence_protocol::{ diff --git a/crates/valence/src/player_list.rs b/crates/valence/src/player_list.rs index 164c90a..27be8d2 100644 --- a/crates/valence/src/player_list.rs +++ b/crates/valence/src/player_list.rs @@ -6,7 +6,9 @@ use std::ops::{Deref, DerefMut, Index, IndexMut}; use uuid::Uuid; use valence_protocol::packets::s2c::play::{PlayerInfoRemove, SetTabListHeaderAndFooter}; -use valence_protocol::player_list::{Actions, Entry as PacketEntry, PlayerInfoUpdate}; +use valence_protocol::packets::s2c::player_info_update::{ + Actions, Entry as PacketEntry, PlayerInfoUpdate, +}; use valence_protocol::types::{GameMode, SignedProperty}; use valence_protocol::Text; diff --git a/crates/valence_protocol/src/lib.rs b/crates/valence_protocol/src/lib.rs index 3dfd3e9..63d11e3 100644 --- a/crates/valence_protocol/src/lib.rs +++ b/crates/valence_protocol/src/lib.rs @@ -109,10 +109,7 @@ mod impls; mod inventory; mod item; pub mod packets; -pub mod particle; -pub mod player_list; mod raw_bytes; -pub mod recipe; pub mod text; pub mod translation_key; pub mod types; diff --git a/crates/valence_protocol/src/packets/s2c.rs b/crates/valence_protocol/src/packets/s2c.rs index 3fbb8b9..d9519d8 100644 --- a/crates/valence_protocol/src/packets/s2c.rs +++ b/crates/valence_protocol/src/packets/s2c.rs @@ -7,7 +7,6 @@ use crate::byte_angle::ByteAngle; use crate::ident::Ident; use crate::item::ItemStack; use crate::raw_bytes::RawBytes; -use crate::recipe::DeclaredRecipe; use crate::text::Text; use crate::types::{ AttributeProperty, BossBarAction, ChunkDataBlockEntity, Difficulty, GameEventKind, GameMode, @@ -19,6 +18,12 @@ use crate::var_int::VarInt; use crate::var_long::VarLong; use crate::LengthPrefixedArray; +pub mod commands; +pub mod declare_recipes; +pub mod particle; +pub mod player_info_update; +pub mod update_recipe_book; + pub mod status { use super::*; @@ -95,9 +100,13 @@ pub mod login { } pub mod play { + use commands::Node; + pub use particle::ParticleS2c; + pub use player_info_update::PlayerInfoUpdate; + pub use update_recipe_book::UpdateRecipeBook; + use super::*; - pub use crate::particle::ParticleS2c; - pub use crate::player_list::PlayerInfoUpdate; + use crate::packets::s2c::declare_recipes::DeclaredRecipe; #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x00] @@ -189,6 +198,13 @@ pub mod play { pub reset: bool, } + #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] + #[packet_id = 0x0e] + pub struct Commands<'a> { + pub commands: Vec>, + pub root_index: VarInt, + } + #[derive(Copy, Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x0f] pub struct CloseContainerS2c { @@ -527,6 +543,14 @@ pub mod play { pub blocks: &'a [VarLong], } + #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] + #[packet_id = 0x41] + pub struct ServerData<'a> { + pub motd: Option, + pub icon: Option<&'a str>, + pub enforce_secure_chat: bool, + } + #[derive(Clone, Debug, Encode, EncodePacket, Decode, DecodePacket)] #[packet_id = 0x42] pub struct SetActionBarText(pub Text); @@ -698,6 +722,7 @@ pub mod play { BossBar, SetDifficulty, ClearTitles, + Commands<'a>, CloseContainerS2c, SetContainerContent, SetContainerProperty, @@ -723,11 +748,13 @@ pub mod play { PlayerInfoRemove, PlayerInfoUpdate<'a>, SynchronizePlayerPosition, + UpdateRecipeBook<'a>, RemoveEntities, ResourcePackS2c<'a>, Respawn<'a>, SetHeadRotation, UpdateSectionBlocks, + ServerData<'a>, SetActionBarText, SetHeldItemS2c, SetCenterChunk, diff --git a/crates/valence_protocol/src/packets/s2c/commands.rs b/crates/valence_protocol/src/packets/s2c/commands.rs new file mode 100644 index 0000000..45fd94a --- /dev/null +++ b/crates/valence_protocol/src/packets/s2c/commands.rs @@ -0,0 +1,432 @@ +use std::io::Write; + +use anyhow::bail; +use byteorder::WriteBytesExt; + +use crate::{Decode, Encode, Ident, VarInt}; + +#[derive(Clone, Debug)] +pub struct Node<'a> { + pub children: Vec, + pub data: NodeData<'a>, + pub executable: bool, + pub redirect_node: Option, +} + +#[derive(Clone, Debug)] +pub enum NodeData<'a> { + Root, + Literal { + name: &'a str, + }, + Argument { + name: &'a str, + parser: Parser<'a>, + suggestion: Option, + }, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum Suggestion { + AskServer, + AllRecipes, + AvailableSounds, + AvailableBiomes, + SummonableEntities, +} + +#[derive(Clone, Debug)] +pub enum Parser<'a> { + Bool, + Float { min: Option, max: Option }, + Double { min: Option, max: Option }, + Integer { min: Option, max: Option }, + Long { min: Option, max: Option }, + String(StringArg), + Entity { single: bool, only_players: bool }, + GameProfile, + BlockPos, + ColumnPos, + Vec3, + Vec2, + BlockState, + BlockPredicate, + ItemStack, + ItemPredicate, + Color, + Component, + Message, + NbtCompoundTag, + NbtTag, + NbtPath, + Objective, + ObjectiveCriteria, + Operation, + Particle, + Angle, + Rotation, + ScoreboardSlot, + ScoreHolder { allow_multiple: bool }, + Swizzle, + Team, + ItemSlot, + ResourceLocation, + Function, + EntityAnchor, + IntRange, + FloatRange, + Dimension, + GameMode, + Time, + ResourceOrTag { registry: Ident<&'a str> }, + ResourceOrTagKey { registry: Ident<&'a str> }, + Resource { registry: Ident<&'a str> }, + ResourceKey { registry: Ident<&'a str> }, + TemplateMirror, + TemplateRotation, + Uuid, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub enum StringArg { + SingleWord, + QuotablePhrase, + GreedyPhrase, +} + +impl Encode for Node<'_> { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + let node_type = match &self.data { + NodeData::Root => 0, + NodeData::Literal { .. } => 1, + NodeData::Argument { .. } => 2, + }; + + let has_suggestion = matches!( + &self.data, + NodeData::Argument { + suggestion: Some(_), + .. + } + ); + + let flags: u8 = node_type + | (self.executable as u8 * 0x04) + | (self.redirect_node.is_some() as u8 * 0x08) + | (has_suggestion as u8 * 0x10); + + w.write_u8(flags)?; + + self.children.encode(&mut w)?; + + if let Some(redirect_node) = self.redirect_node { + redirect_node.encode(&mut w)?; + } + + match &self.data { + NodeData::Root => {} + NodeData::Literal { name } => { + name.encode(&mut w)?; + } + NodeData::Argument { + name, + parser, + suggestion, + } => { + name.encode(&mut w)?; + parser.encode(&mut w)?; + + if let Some(suggestion) = suggestion { + match suggestion { + Suggestion::AskServer => "ask_server", + Suggestion::AllRecipes => "all_recipes", + Suggestion::AvailableSounds => "available_sounds", + Suggestion::AvailableBiomes => "available_biomes", + Suggestion::SummonableEntities => "summonable_entities", + } + .encode(&mut w)?; + } + } + } + + Ok(()) + } +} + +impl<'a> Decode<'a> for Node<'a> { + fn decode(r: &mut &'a [u8]) -> anyhow::Result { + let flags = u8::decode(r)?; + + let children = Vec::decode(r)?; + + let redirect_node = if flags & 0x08 != 0 { + Some(VarInt::decode(r)?) + } else { + None + }; + + let node_data = match flags & 0x3 { + 0 => NodeData::Root, + 1 => NodeData::Literal { + name: <&str>::decode(r)?, + }, + 2 => NodeData::Argument { + name: <&str>::decode(r)?, + parser: Parser::decode(r)?, + suggestion: if flags & 0x10 != 0 { + Some(match Ident::<&str>::decode(r)?.path() { + "ask_server" => Suggestion::AskServer, + "all_recipes" => Suggestion::AllRecipes, + "available_sounds" => Suggestion::AvailableSounds, + "available_biomes" => Suggestion::AvailableBiomes, + "summonable_entities" => Suggestion::SummonableEntities, + other => bail!("unknown command suggestion type of \"{other}\""), + }) + } else { + None + }, + }, + n => bail!("invalid node type of {n}"), + }; + + Ok(Self { + children, + data: node_data, + executable: flags & 0x04 != 0, + redirect_node, + }) + } +} + +impl Encode for Parser<'_> { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + match self { + Parser::Bool => 0u8.encode(&mut w)?, + Parser::Float { min, max } => { + 1u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::Double { min, max } => { + 2u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::Integer { min, max } => { + 3u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::Long { min, max } => { + 4u8.encode(&mut w)?; + + (min.is_some() as u8 | (max.is_some() as u8 * 0x2)).encode(&mut w)?; + + if let Some(min) = min { + min.encode(&mut w)?; + } + + if let Some(max) = max { + max.encode(&mut w)?; + } + } + Parser::String(arg) => { + 5u8.encode(&mut w)?; + arg.encode(&mut w)?; + } + Parser::Entity { + single, + only_players, + } => { + 6u8.encode(&mut w)?; + (*single as u8 | (*only_players as u8 * 0x2)).encode(&mut w)?; + } + Parser::GameProfile => 7u8.encode(&mut w)?, + Parser::BlockPos => 8u8.encode(&mut w)?, + Parser::ColumnPos => 9u8.encode(&mut w)?, + Parser::Vec3 => 10u8.encode(&mut w)?, + Parser::Vec2 => 11u8.encode(&mut w)?, + Parser::BlockState => 12u8.encode(&mut w)?, + Parser::BlockPredicate => 13u8.encode(&mut w)?, + Parser::ItemStack => 14u8.encode(&mut w)?, + Parser::ItemPredicate => 15u8.encode(&mut w)?, + Parser::Color => 16u8.encode(&mut w)?, + Parser::Component => 17u8.encode(&mut w)?, + Parser::Message => 18u8.encode(&mut w)?, + Parser::NbtCompoundTag => 19u8.encode(&mut w)?, + Parser::NbtTag => 20u8.encode(&mut w)?, + Parser::NbtPath => 21u8.encode(&mut w)?, + Parser::Objective => 22u8.encode(&mut w)?, + Parser::ObjectiveCriteria => 23u8.encode(&mut w)?, + Parser::Operation => 24u8.encode(&mut w)?, + Parser::Particle => 25u8.encode(&mut w)?, + Parser::Angle => 26u8.encode(&mut w)?, + Parser::Rotation => 27u8.encode(&mut w)?, + Parser::ScoreboardSlot => 28u8.encode(&mut w)?, + Parser::ScoreHolder { allow_multiple } => { + 29u8.encode(&mut w)?; + allow_multiple.encode(&mut w)?; + } + Parser::Swizzle => 30u8.encode(&mut w)?, + Parser::Team => 31u8.encode(&mut w)?, + Parser::ItemSlot => 32u8.encode(&mut w)?, + Parser::ResourceLocation => 33u8.encode(&mut w)?, + Parser::Function => 34u8.encode(&mut w)?, + Parser::EntityAnchor => 35u8.encode(&mut w)?, + Parser::IntRange => 36u8.encode(&mut w)?, + Parser::FloatRange => 37u8.encode(&mut w)?, + Parser::Dimension => 38u8.encode(&mut w)?, + Parser::GameMode => 39u8.encode(&mut w)?, + Parser::Time => 40u8.encode(&mut w)?, + Parser::ResourceOrTag { registry } => { + 41u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::ResourceOrTagKey { registry } => { + 42u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::Resource { registry } => { + 43u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::ResourceKey { registry } => { + 44u8.encode(&mut w)?; + registry.encode(&mut w)?; + } + Parser::TemplateMirror => 45u8.encode(&mut w)?, + Parser::TemplateRotation => 46u8.encode(&mut w)?, + Parser::Uuid => 47u8.encode(&mut w)?, + } + + Ok(()) + } +} + +impl<'a> Decode<'a> for Parser<'a> { + fn decode(r: &mut &'a [u8]) -> anyhow::Result { + fn decode_min_max<'a, T: Decode<'a>>( + r: &mut &'a [u8], + ) -> anyhow::Result<(Option, Option)> { + let flags = u8::decode(r)?; + + let min = if flags & 0x1 != 0 { + Some(T::decode(r)?) + } else { + None + }; + + let max = if flags & 0x2 != 0 { + Some(T::decode(r)?) + } else { + None + }; + + Ok((min, max)) + } + + Ok(match u8::decode(r)? { + 0 => Self::Bool, + 1 => { + let (min, max) = decode_min_max(r)?; + Self::Float { min, max } + } + 2 => { + let (min, max) = decode_min_max(r)?; + Self::Double { min, max } + } + 3 => { + let (min, max) = decode_min_max(r)?; + Self::Integer { min, max } + } + 4 => { + let (min, max) = decode_min_max(r)?; + Self::Long { min, max } + } + 5 => Self::String(StringArg::decode(r)?), + 6 => { + let flags = u8::decode(r)?; + Self::Entity { + single: flags & 0x1 != 0, + only_players: flags & 0x2 != 0, + } + } + 7 => Self::GameProfile, + 8 => Self::BlockPos, + 9 => Self::ColumnPos, + 10 => Self::Vec3, + 11 => Self::Vec2, + 12 => Self::BlockState, + 13 => Self::BlockPredicate, + 14 => Self::ItemStack, + 15 => Self::ItemPredicate, + 16 => Self::Color, + 17 => Self::Component, + 18 => Self::Message, + 19 => Self::NbtCompoundTag, + 20 => Self::NbtTag, + 21 => Self::NbtPath, + 22 => Self::Objective, + 23 => Self::ObjectiveCriteria, + 24 => Self::Operation, + 25 => Self::Particle, + 26 => Self::Angle, + 27 => Self::Rotation, + 28 => Self::ScoreboardSlot, + 29 => Self::ScoreHolder { + allow_multiple: bool::decode(r)?, + }, + 30 => Self::Swizzle, + 31 => Self::Team, + 32 => Self::ItemSlot, + 33 => Self::ResourceLocation, + 34 => Self::Function, + 35 => Self::EntityAnchor, + 36 => Self::IntRange, + 37 => Self::FloatRange, + 38 => Self::Dimension, + 39 => Self::GameMode, + 40 => Self::Time, + 41 => Self::ResourceOrTag { + registry: Ident::decode(r)?, + }, + 42 => Self::ResourceOrTagKey { + registry: Ident::decode(r)?, + }, + 43 => Self::Resource { + registry: Ident::decode(r)?, + }, + 44 => Self::ResourceKey { + registry: Ident::decode(r)?, + }, + 45 => Self::TemplateMirror, + 46 => Self::TemplateRotation, + 47 => Self::Uuid, + n => bail!("unknown command parser ID of {n}"), + }) + } +} diff --git a/crates/valence_protocol/src/recipe.rs b/crates/valence_protocol/src/packets/s2c/declare_recipes.rs similarity index 100% rename from crates/valence_protocol/src/recipe.rs rename to crates/valence_protocol/src/packets/s2c/declare_recipes.rs diff --git a/crates/valence_protocol/src/particle.rs b/crates/valence_protocol/src/packets/s2c/particle.rs similarity index 100% rename from crates/valence_protocol/src/particle.rs rename to crates/valence_protocol/src/packets/s2c/particle.rs diff --git a/crates/valence_protocol/src/player_list.rs b/crates/valence_protocol/src/packets/s2c/player_info_update.rs similarity index 100% rename from crates/valence_protocol/src/player_list.rs rename to crates/valence_protocol/src/packets/s2c/player_info_update.rs diff --git a/crates/valence_protocol/src/packets/s2c/update_recipe_book.rs b/crates/valence_protocol/src/packets/s2c/update_recipe_book.rs new file mode 100644 index 0000000..a7bcd61 --- /dev/null +++ b/crates/valence_protocol/src/packets/s2c/update_recipe_book.rs @@ -0,0 +1,90 @@ +use std::io::Write; + +use anyhow::bail; + +use crate::{Decode, DecodePacket, Encode, EncodePacket, Ident, VarInt}; + +#[derive(Clone, PartialEq, Eq, Debug, EncodePacket, DecodePacket)] +#[packet_id = 0x39] +pub struct UpdateRecipeBook<'a> { + pub action: UpdateRecipeBookAction<'a>, + pub crafting_recipe_book_open: bool, + pub crafting_recipe_book_filter_active: bool, + pub smelting_recipe_book_open: bool, + pub smelting_recipe_book_filter_active: bool, + pub blast_furnace_recipe_book_open: bool, + pub blast_furnace_recipe_book_filter_active: bool, + pub smoker_recipe_book_open: bool, + pub smoker_recipe_book_filter_active: bool, + pub recipe_ids: Vec>, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum UpdateRecipeBookAction<'a> { + Init { recipe_ids: Vec> }, + Add, + Remove, +} + +impl Encode for UpdateRecipeBook<'_> { + fn encode(&self, mut w: impl Write) -> anyhow::Result<()> { + VarInt(match &self.action { + UpdateRecipeBookAction::Init { .. } => 0, + UpdateRecipeBookAction::Add => 1, + UpdateRecipeBookAction::Remove => 2, + }) + .encode(&mut w)?; + + self.crafting_recipe_book_open.encode(&mut w)?; + self.crafting_recipe_book_filter_active.encode(&mut w)?; + self.smelting_recipe_book_open.encode(&mut w)?; + self.smelting_recipe_book_filter_active.encode(&mut w)?; + self.blast_furnace_recipe_book_open.encode(&mut w)?; + self.blast_furnace_recipe_book_filter_active + .encode(&mut w)?; + self.smoker_recipe_book_open.encode(&mut w)?; + self.smoker_recipe_book_filter_active.encode(&mut w)?; + self.recipe_ids.encode(&mut w)?; + if let UpdateRecipeBookAction::Init { recipe_ids } = &self.action { + recipe_ids.encode(&mut w)?; + } + + Ok(()) + } +} + +impl<'a> Decode<'a> for UpdateRecipeBook<'a> { + fn decode(r: &mut &'a [u8]) -> anyhow::Result { + let action_id = VarInt::decode(r)?.0; + + let crafting_recipe_book_open = bool::decode(r)?; + let crafting_recipe_book_filter_active = bool::decode(r)?; + let smelting_recipe_book_open = bool::decode(r)?; + let smelting_recipe_book_filter_active = bool::decode(r)?; + let blast_furnace_recipe_book_open = bool::decode(r)?; + let blast_furnace_recipe_book_filter_active = bool::decode(r)?; + let smoker_recipe_book_open = bool::decode(r)?; + let smoker_recipe_book_filter_active = bool::decode(r)?; + let recipe_ids = Vec::decode(r)?; + + Ok(Self { + action: match action_id { + 0 => UpdateRecipeBookAction::Init { + recipe_ids: Vec::decode(r)?, + }, + 1 => UpdateRecipeBookAction::Add, + 2 => UpdateRecipeBookAction::Remove, + n => bail!("unknown recipe book action of {n}"), + }, + crafting_recipe_book_open, + crafting_recipe_book_filter_active, + smelting_recipe_book_open, + smelting_recipe_book_filter_active, + blast_furnace_recipe_book_open, + blast_furnace_recipe_book_filter_active, + smoker_recipe_book_open, + smoker_recipe_book_filter_active, + recipe_ids, + }) + } +} diff --git a/crates/valence_protocol/src/text.rs b/crates/valence_protocol/src/text.rs index 80a3ba1..b573946 100644 --- a/crates/valence_protocol/src/text.rs +++ b/crates/valence_protocol/src/text.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use std::io::Write; use std::{fmt, ops}; +use anyhow::Context; use serde::de::Visitor; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; @@ -740,7 +741,11 @@ impl Encode for Text { impl Decode<'_> for Text { fn decode(r: &mut &[u8]) -> Result { let string = <&str>::decode(r)?; - Ok(serde_json::from_str(string)?) + if string.is_empty() { + Ok(Self::default()) + } else { + serde_json::from_str(string).context("decoding text JSON") + } } }