From c57c68ddf8cb9e4e8d27cf3e08f267a8a020c1c0 Mon Sep 17 00:00:00 2001 From: EightFactorial <29801334+EightFactorial@users.noreply.github.com> Date: Sat, 11 Mar 2023 14:00:10 -0800 Subject: Add RegistryHolder struct and serde features (#81) * Make RegistryHolder struct * Update deps * Move RegistryHolder to azalea-protocol * Convert bytes to bools and back * Rename and shuffle logic * Move logic into trait, rename methods * Final touchups * Ah, merge mistakes * Add serde support for ResourceLocation * Reuse structs * Error when serde skips values in debug mode Add missing attributes * Strict_registry feature, require packet feature * Add test * Move into packets * Docs and touchups * Reword docs * Move into module inside ClientboundLoginPacket * Add azalea-nbt serde feature * remove duplicate comment and type_ -> kind --------- Co-authored-by: mat --- .../src/packets/game/clientbound_login_packet.rs | 449 ++++++++++++++++++++- 1 file changed, 448 insertions(+), 1 deletion(-) (limited to 'azalea-protocol/src/packets') diff --git a/azalea-protocol/src/packets/game/clientbound_login_packet.rs b/azalea-protocol/src/packets/game/clientbound_login_packet.rs index 149f1e3f..de5444b8 100755 --- a/azalea-protocol/src/packets/game/clientbound_login_packet.rs +++ b/azalea-protocol/src/packets/game/clientbound_login_packet.rs @@ -1,7 +1,12 @@ +use self::registry::RegistryHolder; use azalea_buf::McBuf; use azalea_core::{GameType, GlobalPos, OptionalGameType, ResourceLocation}; use azalea_protocol_macros::ClientboundGamePacket; +/// The first packet sent by the server to the client after login. +/// +/// This packet contains information about the state of the player, the +/// world, and the registry. #[derive(Clone, Debug, McBuf, ClientboundGamePacket)] pub struct ClientboundLoginPacket { pub player_id: u32, @@ -9,7 +14,7 @@ pub struct ClientboundLoginPacket { pub game_type: GameType, pub previous_game_type: OptionalGameType, pub levels: Vec, - pub registry_holder: azalea_nbt::Tag, + pub registry_holder: RegistryHolder, pub dimension_type: ResourceLocation, pub dimension: ResourceLocation, pub seed: i64, @@ -25,3 +30,445 @@ pub struct ClientboundLoginPacket { pub is_flat: bool, pub last_death_location: Option, } + +pub mod registry { + //! [ClientboundLoginPacket](super::ClientboundLoginPacket) Registry + //! Structures + //! + //! This module contains the structures used to represent the registry + //! sent to the client upon login. This contains a lot of information about + //! the game, including the types of chat messages, dimensions, and + //! biomes. + + use azalea_buf::{BufReadError, McBufReadable, McBufWritable}; + use azalea_core::ResourceLocation; + use azalea_nbt::Tag; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use std::{collections::HashMap, io::Cursor}; + + /// The base of the registry. + /// + /// This is the registry that is sent to the client upon login. + /// + /// As a tag, it is a compound tag that only contains a single compound tag. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct RegistryHolder { + #[serde(rename = "")] + pub root: RegistryRoot, + } + + impl TryFrom for RegistryHolder { + type Error = serde_json::Error; + + fn try_from(value: Tag) -> Result { + serde_json::from_value(serde_json::to_value(value)?) + } + } + + impl TryInto for RegistryHolder { + type Error = serde_json::Error; + + fn try_into(self) -> Result { + serde_json::from_value(serde_json::to_value(self)?) + } + } + + impl McBufReadable for RegistryHolder { + fn read_from(buf: &mut Cursor<&[u8]>) -> Result { + RegistryHolder::try_from(Tag::read_from(buf)?) + .map_err(|e| BufReadError::Deserialization { source: e }) + } + } + + impl McBufWritable for RegistryHolder { + fn write_into(&self, buf: &mut impl std::io::Write) -> Result<(), std::io::Error> { + TryInto::::try_into(self.clone())?.write_into(buf) + } + } + + /// The main part of the registry. + /// + /// The only field of [`RegistryHolder`]. + /// Contains information from the server about chat, dimensions, + /// and world generation. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct RegistryRoot { + #[serde(rename = "minecraft:chat_type")] + pub chat_type: RegistryType, + #[serde(rename = "minecraft:dimension_type")] + pub dimension_type: RegistryType, + #[serde(rename = "minecraft:worldgen/biome")] + pub world_type: RegistryType, + } + + /// A collection of values for a certain type of registry data. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct RegistryType { + #[serde(rename = "type")] + pub kind: ResourceLocation, + pub value: Vec>, + } + + /// A value for a certain type of registry data. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct TypeValue { + pub id: u32, + pub name: ResourceLocation, + pub element: T, + } + + /// Data about a kind of chat message + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct ChatTypeElement { + pub chat: ChatTypeData, + pub narration: ChatTypeData, + } + + /// Data about a chat message. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct ChatTypeData { + pub translation_key: String, + pub parameters: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + } + + /// The style of a chat message. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct ChatTypeStyle { + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "Convert")] + pub bold: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "Convert")] + pub italic: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "Convert")] + pub underlined: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "Convert")] + pub strikethrough: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "Convert")] + pub obfuscated: Option, + } + + /// Dimension attributes. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct DimensionTypeElement { + pub ambient_light: f32, + #[serde(with = "Convert")] + pub bed_works: bool, + pub coordinate_scale: f32, + pub effects: ResourceLocation, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub fixed_time: Option, + #[serde(with = "Convert")] + pub has_ceiling: bool, + #[serde(with = "Convert")] + pub has_raids: bool, + #[serde(with = "Convert")] + pub has_skylight: bool, + pub height: u32, + pub infiniburn: ResourceLocation, + pub logical_height: u32, + pub min_y: i32, + pub monster_spawn_block_light_limit: u32, + pub monster_spawn_light_level: MonsterSpawnLightLevel, + #[serde(with = "Convert")] + pub natural: bool, + #[serde(with = "Convert")] + pub piglin_safe: bool, + #[serde(with = "Convert")] + pub respawn_anchor_works: bool, + #[serde(with = "Convert")] + pub ultrawarm: bool, + } + + /// The light level at which monsters can spawn. + /// + /// This can be either a single minimum value, or a formula with a min and + /// max. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(untagged)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub enum MonsterSpawnLightLevel { + /// A simple minimum value. + Simple(u32), + /// A complex value with a type, minimum, and maximum. + /// Vanilla minecraft only uses one type, "minecraft:uniform". + Complex { + #[serde(rename = "type")] + kind: ResourceLocation, + value: MonsterSpawnLightLevelValues, + }, + } + + /// The min and max light levels at which monsters can spawn. + /// + /// Values are inclusive. + #[derive(Debug, Copy, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct MonsterSpawnLightLevelValues { + #[serde(rename = "min_inclusive")] + pub min: u32, + #[serde(rename = "max_inclusive")] + pub max: u32, + } + + /// Biome attributes. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct WorldTypeElement { + pub temperature: f32, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub temperature_modifier: Option, + pub downfall: f32, + pub precipitation: BiomePrecipitation, + pub effects: BiomeEffects, + } + + /// The precipitation of a biome. + #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub enum BiomePrecipitation { + #[serde(rename = "none")] + None, + #[serde(rename = "rain")] + Rain, + #[serde(rename = "snow")] + Snow, + } + + /// The effects of a biome. + /// + /// This includes the sky, fog, water, and grass color, + /// as well as music and other sound effects. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct BiomeEffects { + pub sky_color: u32, + pub fog_color: u32, + pub water_color: u32, + pub water_fog_color: u32, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub foliage_color: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub grass_color: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub grass_color_modifier: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub music: Option, + pub mood_sound: BiomeMoodSound, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub additions_sound: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub ambient_sound: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub particle: Option, + } + + /// The music of the biome. + /// + /// Some biomes have unique music that only play when inside them. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct BiomeMusic { + #[serde(with = "Convert")] + pub replace_current_music: bool, + pub max_delay: u32, + pub min_delay: u32, + pub sound: SoundId, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct BiomeMoodSound { + pub tick_delay: u32, + pub block_search_extent: u32, + pub offset: f32, + pub sound: SoundId, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct AdditionsSound { + pub tick_chance: f32, + pub sound: SoundId, + } + + /// The ID of a sound. + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct SoundId { + pub sound_id: ResourceLocation, + } + + /// Biome particles. + /// + /// Some biomes have particles that spawn in the air. + #[derive(Debug, Clone, Serialize, Deserialize)] + #[cfg_attr(feature = "strict_registry", serde(deny_unknown_fields))] + pub struct BiomeParticle { + pub probability: f32, + pub options: HashMap, + } + + // Using a trait because you can't implement methods for + // types you don't own, in this case Option and bool. + trait Convert: Sized { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer; + + fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>; + } + + // Convert between bool and u8 + impl Convert for bool { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(if *self { 1 } else { 0 }) + } + + fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + convert::(u8::deserialize(deserializer)?) + } + } + + // Convert between Option and u8 + impl Convert for Option { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if let Some(value) = self { + Convert::serialize(value, serializer) + } else { + serializer.serialize_none() + } + } + + fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + if let Some(value) = Option::::deserialize(deserializer)? { + Ok(Some(convert::(value)?)) + } else { + Ok(None) + } + } + } + + // Deserializing logic here to deduplicate code + fn convert<'de, D>(value: u8) -> Result + where + D: Deserializer<'de>, + { + match value { + 0 => Ok(false), + 1 => Ok(true), + other => Err(de::Error::invalid_value( + de::Unexpected::Unsigned(other as u64), + &"zero or one", + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::registry::{ + ChatTypeElement, DimensionTypeElement, RegistryHolder, RegistryRoot, RegistryType, + WorldTypeElement, + }; + use azalea_core::ResourceLocation; + use azalea_nbt::Tag; + + #[test] + fn test_convert() { + let registry = RegistryHolder { + root: RegistryRoot { + chat_type: RegistryType:: { + kind: ResourceLocation::new("minecraft:chat_type").unwrap(), + value: Vec::new(), + }, + dimension_type: RegistryType:: { + kind: ResourceLocation::new("minecraft:dimension_type").unwrap(), + value: Vec::new(), + }, + world_type: RegistryType:: { + kind: ResourceLocation::new("minecraft:worldgen/biome").unwrap(), + value: Vec::new(), + }, + }, + }; + + let tag: Tag = registry.try_into().unwrap(); + let root = tag + .as_compound() + .unwrap() + .get("") + .unwrap() + .as_compound() + .unwrap(); + + let chat = root + .get("minecraft:chat_type") + .unwrap() + .as_compound() + .unwrap(); + let chat_type = chat.get("type").unwrap().as_string().unwrap(); + assert!(chat_type == "minecraft:chat_type"); + + let dimension = root + .get("minecraft:dimension_type") + .unwrap() + .as_compound() + .unwrap(); + let dimension_type = dimension.get("type").unwrap().as_string().unwrap(); + assert!(dimension_type == "minecraft:dimension_type"); + + let world = root + .get("minecraft:worldgen/biome") + .unwrap() + .as_compound() + .unwrap(); + let world_type = world.get("type").unwrap().as_string().unwrap(); + assert!(world_type == "minecraft:worldgen/biome"); + } +} -- cgit v1.2.3