diff --git a/src/biome.rs b/src/biome.rs index afc26de..db3deae 100644 --- a/src/biome.rs +++ b/src/biome.rs @@ -26,7 +26,7 @@ pub struct BiomeId(pub(crate) u16); pub struct Biome { /// The unique name for this biome. The name can be /// seen in the F3 debug menu. - pub name: Ident, + pub name: Ident<'static>, pub precipitation: BiomePrecipitation, pub sky_color: u32, pub water_fog_color: u32, @@ -36,7 +36,7 @@ pub struct Biome { pub grass_color: Option, pub grass_color_modifier: BiomeGrassColorModifier, pub music: Option, - pub ambient_sound: Option, + pub ambient_sound: Option>, pub additions_sound: Option, pub mood_sound: Option, pub particle: Option, @@ -202,20 +202,20 @@ pub enum BiomeGrassColorModifier { #[derive(Clone, Debug)] pub struct BiomeMusic { pub replace_current_music: bool, - pub sound: Ident, + pub sound: Ident<'static>, pub min_delay: i32, pub max_delay: i32, } #[derive(Clone, Debug)] pub struct BiomeAdditionsSound { - pub sound: Ident, + pub sound: Ident<'static>, pub tick_chance: f64, } #[derive(Clone, Debug)] pub struct BiomeMoodSound { - pub sound: Ident, + pub sound: Ident<'static>, pub tick_delay: i32, pub offset: f64, pub block_search_extent: i32, @@ -224,5 +224,5 @@ pub struct BiomeMoodSound { #[derive(Clone, Debug)] pub struct BiomeParticle { pub probability: f32, - pub kind: Ident, + pub kind: Ident<'static>, } diff --git a/src/client.rs b/src/client.rs index 373f65e..ffa148c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -505,7 +505,7 @@ impl Client { /// Plays a sound to the client at a given position. pub fn play_sound( &mut self, - name: Ident, + name: Ident<'static>, category: SoundCategory, pos: Vec3, volume: f32, @@ -514,7 +514,7 @@ impl Client { self.send_packet(CustomSoundEffect { name, category, - position: pos.iter().map(|x| *x as i32 * 8).collect(), + position: pos.as_() * 8, volume, pitch, seed: 0, diff --git a/src/dimension.rs b/src/dimension.rs index 6ef009c..68c1c2f 100644 --- a/src/dimension.rs +++ b/src/dimension.rs @@ -17,11 +17,11 @@ use crate::{ident, LIBRARY_NAMESPACE}; pub struct DimensionId(pub(crate) u16); impl DimensionId { - pub(crate) fn dimension_type_name(self) -> Ident { + pub(crate) fn dimension_type_name(self) -> Ident<'static> { ident!("{LIBRARY_NAMESPACE}:dimension_type_{}", self.0) } - pub(crate) fn dimension_name(self) -> Ident { + pub(crate) fn dimension_name(self) -> Ident<'static> { ident!("{LIBRARY_NAMESPACE}:dimension_{}", self.0) } } diff --git a/src/ident.rs b/src/ident.rs index f9b83a2..ddabee8 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -1,29 +1,33 @@ -//! Namespaced identifiers. +//! Resource identifiers. use std::borrow::Cow; -use std::hash; +use std::cmp::Ordering; use std::io::Write; use std::str::FromStr; +use std::{fmt, hash}; use ascii::{AsAsciiStr, AsciiChar, AsciiStr, IntoAsciiString}; +use hash::Hash; use serde::de::Visitor; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize}; use thiserror::Error; use crate::nbt; -use crate::protocol::{encode_string_bounded, BoundedString, Decode, Encode}; +use crate::protocol::{Decode, Encode}; -/// An identifier is a string split into a "namespace" part and a "path" part. -/// For instance `minecraft:apple` and `apple` are both valid identifiers. +/// A resource identifier is a string divided into a "namespace" part and a +/// "path" part. For instance `minecraft:apple` and `valence:frobnicator` are +/// both valid identifiers. /// /// If the namespace part is left off (the part before and including the colon) -/// the namespace is considered to be "minecraft" for the purposes of equality. +/// the namespace is considered to be "minecraft" for the purposes of equality, +/// ordering, and hashing. /// /// A string must match the regex `^([a-z0-9_-]+:)?[a-z0-9_\/.-]+$` to be a /// valid identifier. #[derive(Clone, Eq)] -pub struct Ident { - ident: Cow<'static, AsciiStr>, +pub struct Ident<'a> { + string: Cow<'a, AsciiStr>, /// The index of the ':' character in the string. /// If there is no namespace then it is `usize::MAX`. /// @@ -32,90 +36,98 @@ pub struct Ident { colon_idx: usize, } -/// The error type which is created when an [`Ident`] cannot be parsed from a -/// string. +/// The error type created when an [`Ident`] cannot be parsed from a +/// string. Contains the offending string. #[derive(Clone, Debug, Error)] -#[error("invalid identifier \"{src}\"")] -pub struct ParseError { - src: Cow<'static, str>, -} +#[error("invalid resource identifier \"{0}\"")] +pub struct IdentParseError<'a>(pub Cow<'a, str>); -impl Ident { +impl<'a> Ident<'a> { /// Parses a new identifier from a string. /// - /// An error is returned if the string is not a valid identifier. - pub fn new(str: impl Into>) -> Result { + /// An error is returned containing the input string if it is not a valid + /// resource identifier. + pub fn new(string: impl Into>) -> Result, IdentParseError<'a>> { #![allow(bindings_with_variant_name)] - let cow = match str.into() { + let cow = match string.into() { Cow::Borrowed(s) => { - Cow::Borrowed(s.as_ascii_str().map_err(|_| ParseError { src: s.into() })?) + Cow::Borrowed(s.as_ascii_str().map_err(|_| IdentParseError(s.into()))?) } - Cow::Owned(s) => Cow::Owned(s.into_ascii_string().map_err(|e| ParseError { - src: e.into_source().into(), - })?), + Cow::Owned(s) => Cow::Owned( + s.into_ascii_string() + .map_err(|e| IdentParseError(e.into_source().into()))?, + ), }; - let s = cow.as_ref(); + let str = cow.as_ref(); let check_namespace = |s: &AsciiStr| { !s.is_empty() && s.chars() .all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '-')) }; - let check_name = |s: &AsciiStr| { + let check_path = |s: &AsciiStr| { !s.is_empty() && s.chars() .all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '/' | '.' | '-')) }; - if let Some(colon_idx) = s.chars().position(|c| c == AsciiChar::Colon) { - if check_namespace(&s[..colon_idx]) && check_name(&s[colon_idx + 1..]) { + match str.chars().position(|c| c == AsciiChar::Colon) { + Some(colon_idx) + if check_namespace(&str[..colon_idx]) && check_path(&str[colon_idx + 1..]) => + { Ok(Self { - ident: cow, + string: cow, colon_idx, }) - } else { - Err(ParseError { - src: ascii_cow_to_str_cow(cow), - }) } - } else if check_name(s) { - Ok(Self { - ident: cow, + None if check_path(str) => Ok(Self { + string: cow, colon_idx: usize::MAX, - }) - } else { - Err(ParseError { - src: ascii_cow_to_str_cow(cow), - }) + }), + _ => Err(IdentParseError(ascii_cow_to_str_cow(cow))), } } - /// Returns the namespace part of this namespaced identifier. + /// Returns the namespace part of this resource identifier. /// /// If this identifier was constructed from a string without a namespace, - /// then `None` is returned. - pub fn namespace(&self) -> Option<&str> { - if self.colon_idx == usize::MAX { - None + /// then "minecraft" is returned. + pub fn namespace(&self) -> &str { + if self.colon_idx != usize::MAX { + self.string[..self.colon_idx].as_str() } else { - Some(self.ident[..self.colon_idx].as_str()) + "minecraft" } } - /// Returns the path part of this namespaced identifier. + /// Returns the path part of this resource identifier. pub fn path(&self) -> &str { if self.colon_idx == usize::MAX { - self.ident.as_str() + self.string.as_str() } else { - self.ident[self.colon_idx + 1..].as_str() + self.string[self.colon_idx + 1..].as_str() } } /// Returns the underlying string as a `str`. pub fn as_str(&self) -> &str { - self.ident.as_str() + self.string.as_str() + } + + /// Consumes the identifier and returns the underlying string. + pub fn into_inner(self) -> Cow<'a, str> { + ascii_cow_to_str_cow(self.string) + } + + /// Used as the argument to `#[serde(deserialize_with = "...")]` when you + /// don't want to borrow data from the `'de` lifetime. + pub fn deserialize_to_owned<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Ident::new(String::deserialize(deserializer)?).map_err(de::Error::custom) } } @@ -126,129 +138,138 @@ fn ascii_cow_to_str_cow(cow: Cow) -> Cow { } } -impl ParseError { - /// Gets the string that caused the parse error. - pub fn into_source(self) -> Cow<'static, str> { - self.src +impl<'a> fmt::Debug for Ident<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Ident").field(&self.as_str()).finish() } } -impl std::fmt::Debug for Ident { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Identifier").field(&self.as_str()).finish() - } -} - -impl FromStr for Ident { - type Err = ParseError; +impl<'a> FromStr for Ident<'a> { + type Err = IdentParseError<'a>; fn from_str(s: &str) -> Result { Ident::new(s.to_owned()) } } -impl From for String { +impl<'a> From> for String { fn from(id: Ident) -> Self { - id.ident.into_owned().into() + id.string.into_owned().into() } } -impl From for Cow<'static, str> { - fn from(id: Ident) -> Self { - ascii_cow_to_str_cow(id.ident) +impl<'a> From> for Cow<'a, str> { + fn from(id: Ident<'a>) -> Self { + ascii_cow_to_str_cow(id.string) } } -impl From for nbt::Value { - fn from(id: Ident) -> Self { - Self::String(id.into()) - } -} - -impl AsRef for Ident { +impl<'a> AsRef for Ident<'a> { fn as_ref(&self) -> &str { self.as_str() } } -impl TryFrom for Ident { - type Error = ParseError; +impl<'a, 'b> PartialEq> for Ident<'a> { + fn eq(&self, other: &Ident<'b>) -> bool { + (self.namespace(), self.path()) == (other.namespace(), other.path()) + } +} + +impl<'a, 'b> PartialOrd> for Ident<'a> { + fn partial_cmp(&self, other: &Ident<'b>) -> Option { + (self.namespace(), self.path()).partial_cmp(&(other.namespace(), other.path())) + } +} + +impl<'a> Hash for Ident<'a> { + fn hash(&self, state: &mut H) { + self.namespace().hash(state); + self.path().hash(state); + } +} + +impl<'a> fmt::Display for Ident<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.namespace(), self.path()) + } +} + +impl<'a> TryFrom for Ident<'a> { + type Error = IdentParseError<'a>; fn try_from(value: String) -> Result { Ident::new(value) } } -impl TryFrom<&'static str> for Ident { - type Error = ParseError; +impl<'a> TryFrom<&'a str> for Ident<'a> { + type Error = IdentParseError<'a>; - fn try_from(value: &'static str) -> Result { + fn try_from(value: &'a str) -> Result { Ident::new(value) } } -impl std::fmt::Display for Ident { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.as_str()) +impl<'a> From> for nbt::Value { + fn from(id: Ident<'a>) -> Self { + String::from(id).into() } } -/// Equality for identifiers respects the fact that "minecraft:apple" and -/// "apple" have the same meaning. -impl PartialEq for Ident { - fn eq(&self, other: &Self) -> bool { - self.namespace().unwrap_or("minecraft") == other.namespace().unwrap_or("minecraft") - && self.path() == other.path() - } -} - -impl hash::Hash for Ident { - fn hash(&self, state: &mut H) { - self.namespace().unwrap_or("minecraft").hash(state); - self.path().hash(state); - } -} - -impl Encode for Ident { +impl<'a> Encode for Ident<'a> { fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - encode_string_bounded(self.as_str(), 0, 32767, w) + self.as_str().encode(w) } } -impl Decode for Ident { +impl<'a> Decode for Ident<'a> { fn decode(r: &mut &[u8]) -> anyhow::Result { - let string = BoundedString::<0, 32767>::decode(r)?.0; - Ok(Ident::new(string)?) + Ok(Ident::new(String::decode(r)?)?) } } -impl Serialize for Ident { +impl<'a> Serialize for Ident<'a> { fn serialize(&self, serializer: S) -> Result { self.as_str().serialize(serializer) } } -impl<'de> Deserialize<'de> for Ident { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_str(IdentifierVisitor) +/// This uses borrowed data from the `'de` lifetime. If you just want owned +/// data, see [`Ident::deserialize_to_owned`]. +impl<'de> Deserialize<'de> for Ident<'de> { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_string(IdentVisitor) } } -/// An implementation of `serde::de::Visitor` for Minecraft identifiers. -struct IdentifierVisitor; +struct IdentVisitor; -impl<'de> Visitor<'de> for IdentifierVisitor { - type Value = Ident; +impl<'de> Visitor<'de> for IdentVisitor { + type Value = Ident<'de>; - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "a valid Minecraft identifier") + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "a valid Minecraft resource identifier") } - fn visit_str(self, s: &str) -> Result { + fn visit_str(self, s: &str) -> Result { + dbg!("foo"); + Ident::from_str(s).map_err(E::custom) } - fn visit_string(self, s: String) -> Result { + fn visit_borrowed_str(self, v: &'de str) -> Result + where + E: de::Error, + { + dbg!("bar"); + + Ident::new(v).map_err(E::custom) + } + + fn visit_string(self, s: String) -> Result { + dbg!("baz"); + Ident::new(s).map_err(E::custom) } } @@ -268,15 +289,15 @@ impl<'de> Visitor<'de> for IdentifierVisitor { /// use valence::ident; /// /// let namespace = "my_namespace"; -/// let apple = ident!("{namespace}:apple"); +/// let path = ident!("{namespace}:my_path"); /// -/// assert_eq!(apple.namespace(), Some("my_namespace")); -/// assert_eq!(apple.path(), "apple"); +/// assert_eq!(path.namespace(), "my_namespace"); +/// assert_eq!(path.path(), "my_path"); /// ``` #[macro_export] macro_rules! ident { ($($arg:tt)*) => {{ - let errmsg = "invalid identifier in `ident` macro"; + let errmsg = "invalid resource identifier in `ident` macro"; #[allow(clippy::redundant_closure_call)] (|args: ::std::fmt::Arguments| match args.as_str() { Some(s) => $crate::ident::Ident::new(s).expect(errmsg), @@ -288,12 +309,15 @@ macro_rules! ident { #[cfg(test)] mod tests { use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; + use std::hash::Hasher; + + use super::*; #[test] fn parse_valid() { ident!("minecraft:whatever"); ident!("_what-ever55_:.whatever/whatever123456789_"); + ident!("valence:frobnicator"); } #[test] @@ -329,4 +353,26 @@ mod tests { assert_eq!(h1.finish(), h2.finish()); } + + fn check_borrowed(id: Ident) { + if let Cow::Owned(_) = id.into_inner() { + panic!("not borrowed!"); + } + } + + #[test] + fn literal_is_borrowed() { + check_borrowed(ident!("akjghsjkhebf")); + } + + #[test] + fn visit_borrowed_str_works() { + let data = String::from("valence:frobnicator"); + + check_borrowed( + IdentVisitor + .visit_borrowed_str::(data.as_ref()) + .unwrap(), + ); + } } diff --git a/src/protocol.rs b/src/protocol.rs index 36ebad4..0c7a323 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -13,6 +13,7 @@ //! //! [`send_packet`]: crate::client::Client::send_packet +use std::borrow::Cow; use std::io::{Read, Write}; use std::mem; @@ -344,18 +345,36 @@ where } } -impl Encode for String { +impl Encode for str { fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { encode_string_bounded(self, 0, 32767, w) } } +impl Encode for String { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.as_str().encode(w) + } +} + impl Decode for String { fn decode(r: &mut &[u8]) -> anyhow::Result { decode_string_bounded(0, 32767, r) } } +impl<'a> Encode for Cow<'a, str> { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.as_ref().encode(w) + } +} + +impl Decode for Cow<'static, str> { + fn decode(r: &mut &[u8]) -> anyhow::Result { + Ok(String::decode(r)?.into()) + } +} + /// A string with a minimum and maximum character length known at compile time. /// /// If the string is not in bounds, an error is generated while diff --git a/src/protocol/packets/c2s.rs b/src/protocol/packets/c2s.rs index 6ab34a5..d9a8abe 100644 --- a/src/protocol/packets/c2s.rs +++ b/src/protocol/packets/c2s.rs @@ -281,7 +281,7 @@ pub mod play { def_struct! { PluginMessageC2s { - channel: Ident, + channel: Ident<'static>, data: RawBytes, } } @@ -407,7 +407,7 @@ pub mod play { def_struct! { PlaceRecipe { window_id: i8, - recipe: Ident, + recipe: Ident<'static>, make_all: bool, } } @@ -520,7 +520,7 @@ pub mod play { def_struct! { SetSeenRecipe { - recipe_id: Ident, + recipe_id: Ident<'static>, } } @@ -542,7 +542,7 @@ pub mod play { def_enum! { SeenAdvancements: VarInt { - OpenedTab: Ident = 0, + OpenedTab: Ident<'static> = 0, ClosedScreen = 1, } } @@ -610,9 +610,9 @@ pub mod play { def_struct! { ProgramJigsawBlock { location: BlockPos, - name: Ident, - target: Ident, - pool: Ident, + name: Ident<'static>, + target: Ident<'static>, + pool: Ident<'static>, final_state: String, joint_type: String, } diff --git a/src/protocol/packets/s2c.rs b/src/protocol/packets/s2c.rs index b67cdfd..b6ea24a 100644 --- a/src/protocol/packets/s2c.rs +++ b/src/protocol/packets/s2c.rs @@ -62,7 +62,7 @@ pub mod login { def_struct! { LoginPluginRequest { message_id: VarInt, - channel: Ident, + channel: Ident<'static>, data: RawBytes, } } @@ -277,7 +277,7 @@ pub mod play { def_struct! { CustomSoundEffect { - name: Ident, + name: Ident<'static>, category: SoundCategory, position: Vec3, volume: f32, @@ -388,13 +388,13 @@ pub mod play { is_hardcore: bool, gamemode: GameMode, previous_gamemode: GameMode, - dimension_names: Vec, + dimension_names: Vec>, /// Contains information about dimensions, biomes, and chats. registry_codec: Compound, /// The name of the dimension type being spawned into. - dimension_type_name: Ident, + dimension_type_name: Ident<'static>, /// The name of the dimension being spawned into. - dimension_name: Ident, + dimension_name: Ident<'static>, /// Hash of the world's seed used for client biome noise. hashed_seed: i64, /// No longer used by the client. @@ -409,7 +409,7 @@ pub mod play { /// If this is a superflat world. /// Superflat worlds have different void fog and horizon levels. is_flat: bool, - last_death_location: Option<(Ident, BlockPos)>, + last_death_location: Option<(Ident<'static>, BlockPos)>, } } @@ -529,15 +529,15 @@ pub mod play { def_struct! { Respawn { - dimension_type_name: Ident, - dimension_name: Ident, + dimension_type_name: Ident<'static>, + dimension_name: Ident<'static>, hashed_seed: u64, game_mode: GameMode, previous_game_mode: GameMode, is_debug: bool, is_flat: bool, copy_metadata: bool, - last_death_location: Option<(Ident, BlockPos)>, + last_death_location: Option<(Ident<'static>, BlockPos)>, } } @@ -708,7 +708,7 @@ pub mod play { def_struct! { EntityAttributesProperty { - key: Ident, + key: Ident<'static>, value: f64, modifiers: Vec } diff --git a/src/text.rs b/src/text.rs index 78d736e..d05fda5 100644 --- a/src/text.rs +++ b/src/text.rs @@ -5,7 +5,7 @@ use std::fmt; use std::io::Write; use serde::de::Visitor; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use crate::ident::Ident; use crate::protocol::{BoundedString, Decode, Encode}; @@ -378,14 +378,15 @@ enum ClickEvent { enum HoverEvent { ShowText(Box), ShowItem { - id: Ident, + #[serde(deserialize_with = "Ident::deserialize_to_owned")] + id: Ident<'static>, count: Option, // TODO: tag }, ShowEntity { name: Box, - #[serde(rename = "type")] - kind: Ident, + #[serde(rename = "type", deserialize_with = "Ident::deserialize_to_owned")] + kind: Ident<'static>, // TODO: id (hyphenated entity UUID as a string) }, } @@ -513,7 +514,7 @@ impl<'de> Visitor<'de> for ColorVisitor { write!(f, "a hex color of the form #rrggbb") } - fn visit_str(self, s: &str) -> Result { + fn visit_str(self, s: &str) -> Result { color_from_str(s).ok_or_else(|| E::custom("invalid hex color")) } }