diff options
| author | mat <27899617+mat-1@users.noreply.github.com> | 2025-08-10 18:55:23 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-10 18:55:23 -0500 |
| commit | 7120842f9d2c659a2f12d8922299c2a761bc5582 (patch) | |
| tree | 0d7976ceec82d914e4c75f23adcdd5839f9960a4 /azalea-chat/src | |
| parent | 3b659833c1ad4cca89b4cd553193edcb6d223163 (diff) | |
| download | azalea-drasl-7120842f9d2c659a2f12d8922299c2a761bc5582.tar.xz | |
Send correct data component checksums (#234)
* start implementing data component crc32 hashes
* start doing serde impls for checksums
* make more components hashable
* make all data components serializable
* support recursive components
* fix simdnbt dep
* update changelog
* clippy
Diffstat (limited to 'azalea-chat/src')
| -rw-r--r-- | azalea-chat/src/base_component.rs | 12 | ||||
| -rw-r--r-- | azalea-chat/src/click_event.rs | 16 | ||||
| -rw-r--r-- | azalea-chat/src/component.rs | 6 | ||||
| -rw-r--r-- | azalea-chat/src/hover_event.rs | 20 | ||||
| -rw-r--r-- | azalea-chat/src/lib.rs | 2 | ||||
| -rw-r--r-- | azalea-chat/src/style.rs | 132 | ||||
| -rw-r--r-- | azalea-chat/src/text_component.rs | 18 | ||||
| -rw-r--r-- | azalea-chat/src/translatable_component.rs | 10 |
8 files changed, 166 insertions, 50 deletions
diff --git a/azalea-chat/src/base_component.rs b/azalea-chat/src/base_component.rs index cbc5ae8b..366904c7 100644 --- a/azalea-chat/src/base_component.rs +++ b/azalea-chat/src/base_component.rs @@ -2,20 +2,26 @@ use serde::Serialize; use crate::{FormattedText, style::Style}; -#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct BaseComponent { // implements mutablecomponent #[serde(skip_serializing_if = "Vec::is_empty")] pub siblings: Vec<FormattedText>, #[serde(flatten)] - pub style: Style, + pub style: Box<Style>, } impl BaseComponent { pub fn new() -> Self { Self { siblings: Vec::new(), - style: Style::default(), + style: Default::default(), + } + } + pub fn with_style(self, style: Style) -> Self { + Self { + style: Box::new(style), + ..self } } } diff --git a/azalea-chat/src/click_event.rs b/azalea-chat/src/click_event.rs new file mode 100644 index 00000000..765ef3ef --- /dev/null +++ b/azalea-chat/src/click_event.rs @@ -0,0 +1,16 @@ +use serde::Serialize; +use simdnbt::owned::Nbt; + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "snake_case", tag = "action")] +pub enum ClickEvent { + OpenUrl { url: String }, + OpenFile { path: String }, + RunCommand { command: String }, + SuggestCommand { command: String }, + // TODO: this uses Dialog.CODEC + ShowDialog, + ChangePage { page: i32 }, + CopyToClipboard { value: String }, + Custom { id: String, payload: Nbt }, +} diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index 2a7e588e..e1652cf1 100644 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -21,7 +21,7 @@ use crate::{ }; /// A chat component, basically anything you can see in chat. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(untagged)] pub enum FormattedText { Text(TextComponent), @@ -348,7 +348,7 @@ impl<'de> Deserialize<'de> for FormattedText { } let style = Style::deserialize(&json); - component.get_base_mut().style = style; + component.get_base_mut().style = Box::new(style); return Ok(component); } @@ -539,7 +539,7 @@ impl FormattedText { let base_style = Style::from_compound(compound).ok()?; let new_style = &mut component.get_base_mut().style; - *new_style = new_style.merged_with(&base_style); + *new_style = Box::new(new_style.merged_with(&base_style)); Some(component) } diff --git a/azalea-chat/src/hover_event.rs b/azalea-chat/src/hover_event.rs new file mode 100644 index 00000000..a18a3047 --- /dev/null +++ b/azalea-chat/src/hover_event.rs @@ -0,0 +1,20 @@ +use serde::Serialize; + +use crate::FormattedText; + +#[derive(Clone, Debug, PartialEq, Serialize)] +#[serde(rename_all = "snake_case", tag = "action")] +pub enum HoverEvent { + ShowText { + value: Box<FormattedText>, + }, + // TODO + ShowItem { + // item: ItemStack, + }, + ShowEntity { + id: i32, + // uuid: Uuid, + name: Box<FormattedText>, + }, +} diff --git a/azalea-chat/src/lib.rs b/azalea-chat/src/lib.rs index f01d8835..a0361c86 100644 --- a/azalea-chat/src/lib.rs +++ b/azalea-chat/src/lib.rs @@ -1,7 +1,9 @@ #![doc = include_str!("../README.md")] pub mod base_component; +mod click_event; mod component; +pub mod hover_event; #[cfg(feature = "numbers")] pub mod numbers; pub mod style; diff --git a/azalea-chat/src/style.rs b/azalea-chat/src/style.rs index 18176a3d..ebab6874 100644 --- a/azalea-chat/src/style.rs +++ b/azalea-chat/src/style.rs @@ -7,6 +7,8 @@ use serde_json::Value; #[cfg(feature = "simdnbt")] use simdnbt::owned::{NbtCompound, NbtTag}; +use crate::{click_event::ClickEvent, hover_event::HoverEvent}; + #[derive(Clone, PartialEq, Eq, Debug, Hash)] pub struct TextColor { pub value: u32, @@ -30,13 +32,16 @@ impl simdnbt::ToNbtTag for TextColor { } impl TextColor { - pub fn parse(value: String) -> Option<TextColor> { + /// Parse a text component in the same way that Minecraft does. + /// + /// This supports named colors and hex codes. + pub fn parse(value: &str) -> Option<TextColor> { if value.starts_with('#') { let n = value.chars().skip(1).collect::<String>(); let n = u32::from_str_radix(&n, 16).ok()?; return Some(TextColor::from_rgb(n)); } - let color_option = NAMED_COLORS.get(&value.to_ascii_uppercase()); + let color_option = NAMED_COLORS.get(&value.to_ascii_lowercase()); if let Some(color) = color_option { return Some(color.clone()); } @@ -146,28 +151,28 @@ impl ChatFormatting { pub fn name(&self) -> &'static str { match self { - ChatFormatting::Black => "BLACK", - ChatFormatting::DarkBlue => "DARK_BLUE", - ChatFormatting::DarkGreen => "DARK_GREEN", - ChatFormatting::DarkAqua => "DARK_AQUA", - ChatFormatting::DarkRed => "DARK_RED", - ChatFormatting::DarkPurple => "DARK_PURPLE", - ChatFormatting::Gold => "GOLD", - ChatFormatting::Gray => "GRAY", - ChatFormatting::DarkGray => "DARK_GRAY", - ChatFormatting::Blue => "BLUE", - ChatFormatting::Green => "GREEN", - ChatFormatting::Aqua => "AQUA", - ChatFormatting::Red => "RED", - ChatFormatting::LightPurple => "LIGHT_PURPLE", - ChatFormatting::Yellow => "YELLOW", - ChatFormatting::White => "WHITE", - ChatFormatting::Obfuscated => "OBFUSCATED", - ChatFormatting::Strikethrough => "STRIKETHROUGH", - ChatFormatting::Bold => "BOLD", - ChatFormatting::Underline => "UNDERLINE", - ChatFormatting::Italic => "ITALIC", - ChatFormatting::Reset => "RESET", + ChatFormatting::Black => "black", + ChatFormatting::DarkBlue => "dark_blue", + ChatFormatting::DarkGreen => "dark_green", + ChatFormatting::DarkAqua => "dark_aqua", + ChatFormatting::DarkRed => "dark_red", + ChatFormatting::DarkPurple => "dark_purple", + ChatFormatting::Gold => "gold", + ChatFormatting::Gray => "gray", + ChatFormatting::DarkGray => "dark_gray", + ChatFormatting::Blue => "blue", + ChatFormatting::Green => "green", + ChatFormatting::Aqua => "aqua", + ChatFormatting::Red => "red", + ChatFormatting::LightPurple => "light_purple", + ChatFormatting::Yellow => "yellow", + ChatFormatting::White => "white", + ChatFormatting::Obfuscated => "obfuscated", + ChatFormatting::Strikethrough => "strikethrough", + ChatFormatting::Bold => "bold", + ChatFormatting::Underline => "underline", + ChatFormatting::Italic => "italic", + ChatFormatting::Reset => "reset", } } @@ -298,18 +303,77 @@ impl TryFrom<ChatFormatting> for TextColor { } } -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq)] +#[non_exhaustive] pub struct Style { - // These are options instead of just bools because None is different than false in this case pub color: Option<TextColor>, + pub shadow_color: Option<u32>, pub bold: Option<bool>, pub italic: Option<bool>, pub underlined: Option<bool>, pub strikethrough: Option<bool>, pub obfuscated: Option<bool>, + pub click_event: Option<ClickEvent>, + pub hover_event: Option<HoverEvent>, + pub insertion: Option<String>, + /// Represented as a `ResourceLocation`. + pub font: Option<String>, /// Whether formatting should be reset before applying these styles pub reset: bool, } +impl Style { + pub fn new() -> Self { + Self::default() + } + pub fn color(mut self, color: impl Into<Option<TextColor>>) -> Self { + self.color = color.into(); + self + } + pub fn shadow_color(mut self, color: impl Into<Option<u32>>) -> Self { + self.shadow_color = color.into(); + self + } + pub fn bold(mut self, bold: impl Into<Option<bool>>) -> Self { + self.bold = bold.into(); + self + } + pub fn italic(mut self, italic: impl Into<Option<bool>>) -> Self { + self.italic = italic.into(); + self + } + pub fn underlined(mut self, underlined: impl Into<Option<bool>>) -> Self { + self.underlined = underlined.into(); + self + } + pub fn strikethrough(mut self, strikethrough: impl Into<Option<bool>>) -> Self { + self.strikethrough = strikethrough.into(); + self + } + pub fn obfuscated(mut self, obfuscated: impl Into<Option<bool>>) -> Self { + self.obfuscated = obfuscated.into(); + self + } + pub fn click_event(mut self, click_event: impl Into<Option<ClickEvent>>) -> Self { + self.click_event = click_event.into(); + self + } + pub fn hover_event(mut self, hover_event: impl Into<Option<HoverEvent>>) -> Self { + self.hover_event = hover_event.into(); + self + } + pub fn insertion(mut self, insertion: impl Into<Option<String>>) -> Self { + self.insertion = insertion.into(); + self + } + pub fn font(mut self, font: impl Into<Option<String>>) -> Self { + self.font = font.into(); + self + } + pub fn reset(mut self, reset: bool) -> Self { + self.reset = reset; + self + } +} fn serde_serialize_field<S: serde::ser::SerializeStruct>( state: &mut S, @@ -443,7 +507,7 @@ impl Style { let color: Option<TextColor> = json_object .get("color") .and_then(|v| v.as_str()) - .and_then(|v| TextColor::parse(v.to_string())); + .and_then(TextColor::parse); Style { color, bold, @@ -565,11 +629,16 @@ impl Style { pub fn merged_with(&self, other: &Style) -> Style { Style { color: other.color.clone().or(self.color.clone()), + shadow_color: other.shadow_color.or(self.shadow_color), bold: other.bold.or(self.bold), italic: other.italic.or(self.italic), underlined: other.underlined.or(self.underlined), strikethrough: other.strikethrough.or(self.strikethrough), obfuscated: other.obfuscated.or(self.obfuscated), + click_event: other.click_event.clone().or(self.click_event.clone()), + hover_event: other.hover_event.clone().or(self.hover_event.clone()), + insertion: other.insertion.clone().or(self.insertion.clone()), + font: other.font.clone().or(self.font.clone()), reset: other.reset, // if reset is true in the new style, that takes precedence } } @@ -647,7 +716,7 @@ impl simdnbt::Deserialize for Style { let obfuscated = compound.byte("obfuscated").map(|v| v != 0); let color: Option<TextColor> = compound .string("color") - .and_then(|v| TextColor::parse(v.to_string())); + .and_then(|v| TextColor::parse(&v.to_str())); Ok(Style { color, bold, @@ -667,14 +736,11 @@ mod tests { #[test] fn text_color_named_colors() { - assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525); + assert_eq!(TextColor::parse("red").unwrap().value, 16733525); } #[test] fn text_color_hex_colors() { - assert_eq!( - TextColor::parse("#a1b2c3".to_string()).unwrap().value, - 10597059 - ); + assert_eq!(TextColor::parse("#a1b2c3").unwrap().value, 10597059); } #[test] diff --git a/azalea-chat/src/text_component.rs b/azalea-chat/src/text_component.rs index bd598e16..eab7ee79 100644 --- a/azalea-chat/src/text_component.rs +++ b/azalea-chat/src/text_component.rs @@ -5,21 +5,24 @@ use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::Seria use crate::{ FormattedText, base_component::BaseComponent, - style::{ChatFormatting, TextColor}, + style::{ChatFormatting, Style, TextColor}, }; /// A component that contains text that's the same in all locales. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct TextComponent { pub base: BaseComponent, pub text: String, } - impl Serialize for TextComponent { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { + if self.base == BaseComponent::default() { + return serializer.serialize_str(&self.text); + } + let mut state = serializer.serialize_map(None)?; state.serialize_entry("text", &self.text)?; Serialize::serialize(&self.base, FlatMapSerializer(&mut state))?; @@ -85,7 +88,7 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo components.push(TextComponent::new("".to_string())); } let style = &mut components.last_mut().unwrap().base.style; - style.color = TextColor::parse(color); + style.color = TextColor::parse(&color); i += 6; } else if let Some(formatter) = ChatFormatting::from_code(formatting_code) { @@ -124,7 +127,8 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo } impl TextComponent { - pub fn new(text: String) -> Self { + pub fn new(text: impl Into<String>) -> Self { + let text = text.into(); // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) { legacy_color_code_to_text_component(&text) @@ -139,6 +143,10 @@ impl TextComponent { fn get(self) -> FormattedText { FormattedText::Text(self) } + pub fn with_style(mut self, style: Style) -> Self { + self.base.style = Box::new(style); + self + } } impl Display for TextComponent { diff --git a/azalea-chat/src/translatable_component.rs b/azalea-chat/src/translatable_component.rs index 80d5e8e2..10c502a8 100644 --- a/azalea-chat/src/translatable_component.rs +++ b/azalea-chat/src/translatable_component.rs @@ -4,11 +4,9 @@ use serde::{__private::ser::FlatMapSerializer, Serialize, Serializer, ser::Seria #[cfg(feature = "simdnbt")] use simdnbt::Serialize as _; -use crate::{ - FormattedText, base_component::BaseComponent, style::Style, text_component::TextComponent, -}; +use crate::{FormattedText, base_component::BaseComponent, text_component::TextComponent}; -#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Serialize)] #[serde(untagged)] pub enum StringOrComponent { String(String), @@ -26,7 +24,7 @@ impl simdnbt::ToNbtTag for StringOrComponent { } /// A message whose content depends on the client's language. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq)] pub struct TranslatableComponent { pub base: BaseComponent, pub key: String, @@ -181,7 +179,7 @@ impl TranslatableComponent { Ok(TextComponent { base: BaseComponent { siblings: components.into_iter().map(FormattedText::Text).collect(), - style: Style::default(), + style: Default::default(), }, text: "".to_string(), }) |
