diff options
| author | mat <git@matdoes.dev> | 2025-08-12 17:39:05 +1200 |
|---|---|---|
| committer | mat <git@matdoes.dev> | 2025-08-12 17:39:05 +1200 |
| commit | 12aeae07d2667cd188acf88190ed5c3f7983926a (patch) | |
| tree | 4f4549b8e37d76f098d4164398517813bb0dfcc6 | |
| parent | fa1050d6eedf80bbc20e91163b99c0306414e627 (diff) | |
| download | azalea-drasl-12aeae07d2667cd188acf88190ed5c3f7983926a.tar.xz | |
fix wrong chat styling sometimes when 'extra' field is used
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | azalea-chat/src/base_component.rs | 1 | ||||
| -rw-r--r-- | azalea-chat/src/click_event.rs | 31 | ||||
| -rw-r--r-- | azalea-chat/src/component.rs | 161 | ||||
| -rw-r--r-- | azalea-chat/src/style.rs | 202 | ||||
| -rw-r--r-- | azalea-chat/src/text_component.rs | 47 | ||||
| -rw-r--r-- | azalea-chat/tests/integration_test.rs | 8 | ||||
| -rw-r--r-- | azalea-core/src/checksum.rs | 157 | ||||
| -rw-r--r-- | azalea-protocol/src/packets/game/c_system_chat.rs | 16 |
9 files changed, 233 insertions, 393 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a03250c..979a1ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,8 @@ is breaking anyways, semantic versioning is not followed. - Fix packet order for loading (`PlayerLoaded`/`MovePlayerPos`) and sprinting (`PlayerInput`/`PlayerCommand`). - Clients no longer send invalid look directions if the server teleports us with one. - Movement code was updated with the changes from 1.21.5, so it no longer flags Grim. -- `azalea-chat` now correctly handles arrays of integers in the `with` field. (@qwqawawow) +- `azalea-chat` now handles arrays of integers in the `with` field. (@qwqawawow) +- `azalea-chat` no longer incorrectly persists styles of components in the "extra" field. - Inventories now use the correct max stack sizes. - Clients now send the correct data component checksums when interacting with items. - Fix parsing some metadata fields of Display entities. diff --git a/azalea-chat/src/base_component.rs b/azalea-chat/src/base_component.rs index 366904c7..27666c17 100644 --- a/azalea-chat/src/base_component.rs +++ b/azalea-chat/src/base_component.rs @@ -5,6 +5,7 @@ use crate::{FormattedText, style::Style}; #[derive(Clone, Debug, PartialEq, Serialize)] pub struct BaseComponent { // implements mutablecomponent + /// Components in the "extra" field. #[serde(skip_serializing_if = "Vec::is_empty")] pub siblings: Vec<FormattedText>, #[serde(flatten)] diff --git a/azalea-chat/src/click_event.rs b/azalea-chat/src/click_event.rs index 765ef3ef..dddaff74 100644 --- a/azalea-chat/src/click_event.rs +++ b/azalea-chat/src/click_event.rs @@ -1,16 +1,33 @@ use serde::Serialize; +#[cfg(feature = "simdnbt")] 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 }, + 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 }, + ChangePage { + page: i32, + }, + CopyToClipboard { + value: String, + }, + Custom { + id: String, + #[cfg(feature = "simdnbt")] + payload: Nbt, + }, } diff --git a/azalea-chat/src/component.rs b/azalea-chat/src/component.rs index e1652cf1..bf064c8f 100644 --- a/azalea-chat/src/component.rs +++ b/azalea-chat/src/component.rs @@ -99,18 +99,18 @@ impl FormattedText { /// })).unwrap(); /// /// let ansi = component.to_custom_format( - /// |running, new, default| (running.compare_ansi(new, default), String::new()), + /// |running, new| (running.compare_ansi(new), "".to_string()), /// |text| text.to_string(), /// |style| { /// if !style.is_empty() { /// "\u{1b}[m".to_string() /// } else { - /// String::new() + /// "".to_string() /// } /// }, /// &DEFAULT_STYLE, /// ); - /// println!("{}", ansi); + /// println!("{ansi}"); /// ``` pub fn to_custom_format<F, S, C>( &self, @@ -120,42 +120,72 @@ impl FormattedText { default_style: &Style, ) -> String where - F: FnMut(&Style, &Style, &Style) -> (String, String), + F: FnMut(&Style, &Style) -> (String, String), S: FnMut(&str) -> String, C: FnMut(&Style) -> String, { let mut output = String::new(); + let mut running_style = Style::default(); + self.to_custom_format_recursive( + &mut output, + &mut style_formatter, + &mut text_formatter, + &mut cleanup_formatter, + &default_style.clone(), + &mut running_style, + ); + output.push_str(&cleanup_formatter(&running_style)); - for component in self.clone().into_iter() { - let component_text = match &component { - Self::Text(c) => c.text.to_string(), - Self::Translatable(c) => match c.read() { - Ok(c) => c.to_string(), - Err(_) => c.key.to_string(), - }, - }; + output + } + + fn to_custom_format_recursive<F, S, C>( + &self, + output: &mut String, + style_formatter: &mut F, + text_formatter: &mut S, + cleanup_formatter: &mut C, + parent_style: &Style, + running_style: &mut Style, + ) where + F: FnMut(&Style, &Style) -> (String, String), + S: FnMut(&str) -> String, + C: FnMut(&Style) -> String, + { + let component_text = match &self { + Self::Text(c) => c.text.to_string(), + Self::Translatable(c) => match c.read() { + Ok(c) => c.to_string(), + Err(_) => c.key.to_string(), + }, + }; - let component_style = &component.get_base().style; + let component_style = &self.get_base().style; + let new_style = parent_style.merged_with(component_style); - let formatted_style = style_formatter(&running_style, component_style, default_style); + if !component_text.is_empty() { + let (formatted_style_prefix, formatted_style_suffix) = + style_formatter(&running_style, &new_style); let formatted_text = text_formatter(&component_text); - output.push_str(&formatted_style.0); + output.push_str(&formatted_style_prefix); output.push_str(&formatted_text); - output.push_str(&formatted_style.1); + output.push_str(&formatted_style_suffix); - // Reset running style if required - if component_style.reset { - running_style = default_style.clone(); - } else { - running_style.apply(component_style); - } + *running_style = new_style.clone(); } - output.push_str(&cleanup_formatter(&running_style)); - - output + for sibling in &self.get_base().siblings { + sibling.to_custom_format_recursive( + output, + style_formatter, + text_formatter, + cleanup_formatter, + &new_style, + running_style, + ); + } } /// Convert this component into an @@ -165,7 +195,7 @@ impl FormattedText { /// default [`Style`] to use. pub fn to_ansi_with_custom_style(&self, default_style: &Style) -> String { self.to_custom_format( - |running, new, default| (running.compare_ansi(new, default), "".to_owned()), + |running, new| (running.compare_ansi(new), "".to_owned()), |text| text.to_string(), |style| if !style.is_empty() { "\u{1b}[m" } else { "" }.to_string(), default_style, @@ -180,6 +210,9 @@ impl FormattedText { /// [`FormattedText::to_ansi_with_custom_style`] with a default [`Style`] /// colored white. /// + /// If you don't want the result to be styled at all, use + /// [`Self::to_string`]. + /// /// # Examples /// /// ```rust @@ -199,7 +232,7 @@ impl FormattedText { pub fn to_html(&self) -> String { self.to_custom_format( - |running, new, _| { + |running, new| { ( format!( "<span style=\"{}\">", @@ -222,6 +255,9 @@ impl FormattedText { } impl IntoIterator for FormattedText { + type Item = FormattedText; + type IntoIter = std::vec::IntoIter<Self::Item>; + /// Recursively call the function for every component in this component fn into_iter(self) -> Self::IntoIter { let base = self.get_base(); @@ -234,9 +270,6 @@ impl IntoIterator for FormattedText { v.into_iter() } - - type Item = FormattedText; - type IntoIter = std::vec::IntoIter<Self::Item>; } impl<'de> Deserialize<'de> for FormattedText { @@ -388,7 +421,7 @@ impl simdnbt::FromNbtTag for FormattedText { fn from_nbt_tag(tag: simdnbt::borrow::NbtTag) -> Option<Self> { // if it's a string, return a text component with that string if let Some(string) = tag.string() { - Some(FormattedText::from(string)) + Some(FormattedText::from_nbt_string(string)) } // if it's a compound, make it do things with { text } and stuff // simdnbt::borrow::NbtTag::Compound(compound) => { @@ -397,22 +430,7 @@ impl simdnbt::FromNbtTag for FormattedText { } // ok so it's not a compound, if it's a list deserialize every item else if let Some(list) = tag.list() { - let mut component; - if let Some(compounds) = list.compounds() { - component = FormattedText::from_nbt_compound(compounds.first()?)?; - for compound in compounds.into_iter().skip(1) { - component.append(FormattedText::from_nbt_compound(compound)?); - } - } else if let Some(strings) = list.strings() { - component = FormattedText::from(*(strings.first()?)); - for &string in strings.iter().skip(1) { - component.append(FormattedText::from(string)); - } - } else { - debug!("couldn't parse {list:?} as FormattedText"); - return None; - } - Some(component) + FormattedText::from_nbt_list(list) } else { Some(FormattedText::Text(TextComponent::new("".to_owned()))) } @@ -421,6 +439,28 @@ impl simdnbt::FromNbtTag for FormattedText { #[cfg(feature = "simdnbt")] impl FormattedText { + fn from_nbt_string(s: &simdnbt::Mutf8Str) -> Self { + FormattedText::from(s) + } + fn from_nbt_list(list: simdnbt::borrow::NbtList) -> Option<FormattedText> { + let mut component; + if let Some(compounds) = list.compounds() { + component = FormattedText::from_nbt_compound(compounds.first()?)?; + for compound in compounds.into_iter().skip(1) { + component.append(FormattedText::from_nbt_compound(compound)?); + } + } else if let Some(strings) = list.strings() { + component = FormattedText::from(*(strings.first()?)); + for &string in strings.iter().skip(1) { + component.append(FormattedText::from(string)); + } + } else { + debug!("couldn't parse {list:?} as FormattedText"); + return None; + } + Some(component) + } + pub fn from_nbt_compound(compound: simdnbt::borrow::NbtCompound) -> Option<Self> { let mut component: FormattedText; @@ -534,7 +574,28 @@ impl FormattedText { return None; } if let Some(extra) = compound.get("extra") { - component.append(FormattedText::from_nbt_tag(extra)?); + // if it's an array, deserialize every item + if let Some(items) = extra.list() { + if let Some(items) = items.compounds() { + for item in items { + component.append(FormattedText::from_nbt_compound(item)?); + } + } else if let Some(items) = items.strings() { + for item in items { + component.append(FormattedText::from_nbt_string(item)); + } + } else if let Some(items) = items.lists() { + for item in items { + component.append(FormattedText::from_nbt_list(item)?); + } + } else { + warn!( + "couldn't parse {items:?} as FormattedText because it's not a list of compounds or strings" + ); + } + } else { + component.append(FormattedText::from_nbt_tag(extra)?); + } } let base_style = Style::from_compound(compound).ok()?; @@ -556,6 +617,10 @@ impl From<&simdnbt::Mutf8Str> for FormattedText { impl AzaleaRead for FormattedText { fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> { let nbt = simdnbt::borrow::read_optional_tag(buf)?; + trace!( + "Reading NBT for FormattedText: {:?}", + nbt.as_ref().map(|n| n.as_tag().to_owned()) + ); match nbt { Some(nbt) => FormattedText::from_nbt_tag(nbt.as_tag()).ok_or(BufReadError::Custom( "couldn't convert nbt to chat message".to_owned(), diff --git a/azalea-chat/src/style.rs b/azalea-chat/src/style.rs index ebab6874..1ae0e3db 100644 --- a/azalea-chat/src/style.rs +++ b/azalea-chat/src/style.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, fmt, sync::LazyLock}; #[cfg(feature = "azalea-buf")] use azalea_buf::AzBuf; -use serde::{Serialize, Serializer, ser::SerializeStruct}; +use serde::{Serialize, Serializer}; use serde_json::Value; #[cfg(feature = "simdnbt")] use simdnbt::owned::{NbtCompound, NbtTag}; @@ -83,6 +83,7 @@ impl Ansi { pub const ITALIC: &'static str = "\u{1b}[3m"; pub const UNDERLINED: &'static str = "\u{1b}[4m"; pub const STRIKETHROUGH: &'static str = "\u{1b}[9m"; + // "Conceal or hide" pub const OBFUSCATED: &'static str = "\u{1b}[8m"; pub const RESET: &'static str = "\u{1b}[m"; @@ -303,23 +304,32 @@ impl TryFrom<ChatFormatting> for TextColor { } } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, serde::Serialize)] #[non_exhaustive] pub struct Style { + #[serde(skip_serializing_if = "Option::is_none")] pub color: Option<TextColor>, + #[serde(skip_serializing_if = "Option::is_none")] pub shadow_color: Option<u32>, + #[serde(skip_serializing_if = "Option::is_none")] pub bold: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub italic: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub underlined: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub strikethrough: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub obfuscated: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub click_event: Option<ClickEvent>, + #[serde(skip_serializing_if = "Option::is_none")] pub hover_event: Option<HoverEvent>, + #[serde(skip_serializing_if = "Option::is_none")] pub insertion: Option<String>, /// Represented as a `ResourceLocation`. + #[serde(skip_serializing_if = "Option::is_none")] pub font: Option<String>, - /// Whether formatting should be reset before applying these styles - pub reset: bool, } impl Style { pub fn new() -> Self { @@ -369,25 +379,6 @@ impl Style { 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, - name: &'static str, - value: &Option<impl serde::Serialize>, - default: &(impl serde::Serialize + ?Sized), - reset: bool, -) -> Result<(), S::Error> { - if let Some(value) = value { - state.serialize_field(name, value)?; - } else if reset { - state.serialize_field(name, default)?; - } - Ok(()) } #[cfg(feature = "simdnbt")] @@ -395,64 +386,9 @@ fn simdnbt_serialize_field( compound: &mut simdnbt::owned::NbtCompound, name: &'static str, value: Option<impl simdnbt::ToNbtTag>, - default: impl simdnbt::ToNbtTag, - reset: bool, ) { - match value { - Some(value) => { - compound.insert(name, value); - } - _ => { - if reset { - compound.insert(name, default); - } - } - } -} - -impl Serialize for Style { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - let len = if self.reset { - 6 - } else { - usize::from(self.color.is_some()) - + usize::from(self.bold.is_some()) - + usize::from(self.italic.is_some()) - + usize::from(self.underlined.is_some()) - + usize::from(self.strikethrough.is_some()) - + usize::from(self.obfuscated.is_some()) - }; - let mut state = serializer.serialize_struct("Style", len)?; - - serde_serialize_field(&mut state, "color", &self.color, "white", self.reset)?; - serde_serialize_field(&mut state, "bold", &self.bold, &false, self.reset)?; - serde_serialize_field(&mut state, "italic", &self.italic, &false, self.reset)?; - serde_serialize_field( - &mut state, - "underlined", - &self.underlined, - &false, - self.reset, - )?; - serde_serialize_field( - &mut state, - "strikethrough", - &self.strikethrough, - &false, - self.reset, - )?; - serde_serialize_field( - &mut state, - "obfuscated", - &self.obfuscated, - &false, - self.reset, - )?; - - state.end() + if let Some(value) = value { + compound.insert(name, value); } } @@ -461,30 +397,12 @@ impl simdnbt::Serialize for Style { fn to_compound(self) -> NbtCompound { let mut compound = NbtCompound::new(); - simdnbt_serialize_field(&mut compound, "color", self.color, "white", self.reset); - simdnbt_serialize_field(&mut compound, "bold", self.bold, false, self.reset); - simdnbt_serialize_field(&mut compound, "italic", self.italic, false, self.reset); - simdnbt_serialize_field( - &mut compound, - "underlined", - self.underlined, - false, - self.reset, - ); - simdnbt_serialize_field( - &mut compound, - "strikethrough", - self.strikethrough, - false, - self.reset, - ); - simdnbt_serialize_field( - &mut compound, - "obfuscated", - self.obfuscated, - false, - self.reset, - ); + simdnbt_serialize_field(&mut compound, "color", self.color); + simdnbt_serialize_field(&mut compound, "bold", self.bold); + simdnbt_serialize_field(&mut compound, "italic", self.italic); + simdnbt_serialize_field(&mut compound, "underlined", self.underlined); + simdnbt_serialize_field(&mut compound, "strikethrough", self.strikethrough); + simdnbt_serialize_field(&mut compound, "obfuscated", self.obfuscated); compound } @@ -530,55 +448,49 @@ impl Style { } /// find the necessary ansi code to get from this style to another - pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String { - let should_reset = after.reset || + pub fn compare_ansi(&self, after: &Style) -> String { + let should_reset = // if it used to be bold and now it's not, reset - (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) || + (self.bold.unwrap_or_default() && !after.bold.unwrap_or_default()) || // if it used to be italic and now it's not, reset - (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) || + (self.italic.unwrap_or_default() && !after.italic.unwrap_or_default()) || // if it used to be underlined and now it's not, reset - (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) || + (self.underlined.unwrap_or_default() && !after.underlined.unwrap_or_default()) || // if it used to be strikethrough and now it's not, reset - (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) || + (self.strikethrough.unwrap_or_default() && !after.strikethrough.unwrap_or_default()) || // if it used to be obfuscated and now it's not, reset - (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)); + (self.obfuscated.unwrap_or_default() && !after.obfuscated.unwrap_or_default()); let mut ansi_codes = String::new(); let empty_style = Style::empty(); - let (before, after) = if should_reset { + let before = if should_reset { ansi_codes.push_str(Ansi::RESET); - let mut updated_after = if after.reset { - default_style.clone() - } else { - self.clone() - }; - updated_after.apply(after); - (&empty_style, updated_after) + &empty_style } else { - (self, after.clone()) + self }; // if bold used to be false/default and now it's true, set bold - if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) { + if !before.bold.unwrap_or_default() && after.bold.unwrap_or_default() { ansi_codes.push_str(Ansi::BOLD); } // if italic used to be false/default and now it's true, set italic - if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) { + if !before.italic.unwrap_or_default() && after.italic.unwrap_or_default() { ansi_codes.push_str(Ansi::ITALIC); } // if underlined used to be false/default and now it's true, set underlined - if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) { + if !before.underlined.unwrap_or_default() && after.underlined.unwrap_or_default() { ansi_codes.push_str(Ansi::UNDERLINED); } // if strikethrough used to be false/default and now it's true, set // strikethrough - if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) { + if !before.strikethrough.unwrap_or_default() && after.strikethrough.unwrap_or_default() { ansi_codes.push_str(Ansi::STRIKETHROUGH); } // if obfuscated used to be false/default and now it's true, set obfuscated - if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) { + if !before.obfuscated.unwrap_or_default() && after.obfuscated.unwrap_or_default() { ansi_codes.push_str(Ansi::OBFUSCATED); } @@ -639,7 +551,6 @@ impl Style { 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 } } @@ -651,7 +562,14 @@ impl Style { ChatFormatting::Underline => self.underlined = Some(true), ChatFormatting::Strikethrough => self.strikethrough = Some(true), ChatFormatting::Obfuscated => self.obfuscated = Some(true), - ChatFormatting::Reset => self.reset = true, + ChatFormatting::Reset => { + self.color = None; + self.bold = None; + self.italic = None; + self.underlined = None; + self.strikethrough = None; + self.obfuscated = None; + } formatter => { // if it's a color, set it if let Some(color) = formatter.color() { @@ -732,7 +650,6 @@ impl simdnbt::Deserialize for Style { #[cfg(test)] mod tests { use super::*; - use crate::component::DEFAULT_STYLE; #[test] fn text_color_named_colors() { @@ -752,9 +669,10 @@ mod tests { }; let style_b = Style { bold: Some(false), + italic: Some(true), ..Style::default() }; - let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); + let ansi_difference = style_a.compare_ansi(&style_b); assert_eq!( ansi_difference, format!( @@ -771,37 +689,15 @@ mod tests { ..Style::default() }; let style_b = Style { + bold: Some(true), italic: Some(true), ..Style::default() }; - let ansi_difference = style_a.compare_ansi(&style_b, &Style::default()); + let ansi_difference = style_a.compare_ansi(&style_b); assert_eq!(ansi_difference, Ansi::ITALIC) } #[test] - fn ansi_difference_explicit_reset() { - let style_a = Style { - bold: Some(true), - ..Style::empty() - }; - let style_b = Style { - italic: Some(true), - reset: true, - ..Style::empty() - }; - let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE); - assert_eq!( - ansi_difference, - format!( - "{reset}{italic}{white}", - reset = Ansi::RESET, - white = Ansi::rgb(ChatFormatting::White.color().unwrap()), - italic = Ansi::ITALIC - ) - ) - } - - #[test] fn test_from_code() { assert_eq!( ChatFormatting::from_code('a').unwrap(), diff --git a/azalea-chat/src/text_component.rs b/azalea-chat/src/text_component.rs index eab7ee79..962ec46e 100644 --- a/azalea-chat/src/text_component.rs +++ b/azalea-chat/src/text_component.rs @@ -61,11 +61,17 @@ const LEGACY_FORMATTING_CODE_SYMBOL: char = 'ยง'; /// Technically in Minecraft this is done when displaying the text, but AFAIK /// it's the same as just doing it in TextComponent pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent { + if legacy_color_code.is_empty() { + return TextComponent::new(""); + } + let mut components: Vec<TextComponent> = Vec::with_capacity(1); // iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL // then read the next character and get the style from that otherwise, add // the character to the text + let mut cur_component = TextComponent::new(""); + // we don't use a normal for loop since we need to be able to skip after reading // the formatter code symbol let mut i = 0; @@ -84,41 +90,35 @@ pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextCompo .take(7) .collect::<String>(); - if components.is_empty() || !components.last().unwrap().text.is_empty() { - components.push(TextComponent::new("".to_string())); - } - let style = &mut components.last_mut().unwrap().base.style; - style.color = TextColor::parse(&color); + if !cur_component.text.is_empty() { + // we need to split this into a new component + components.push(cur_component.clone()); + cur_component.text = "".to_string(); + }; + cur_component.base.style.color = TextColor::parse(&color); i += 6; } else if let Some(formatter) = ChatFormatting::from_code(formatting_code) { - if components.is_empty() || !components.last().unwrap().text.is_empty() { - components.push(TextComponent::new("".to_string())); - } - let style = &mut components.last_mut().unwrap().base.style; - style.apply_formatting(&formatter); + if !cur_component.text.is_empty() || formatter == ChatFormatting::Reset { + // we need to split this into a new component + components.push(cur_component.clone()); + cur_component.text = "".to_string(); + }; + cur_component.base.style.apply_formatting(&formatter); } i += 1; } else { - if components.is_empty() { - components.push(TextComponent::new("".to_string())); - } - components - .last_mut() - .unwrap() + cur_component .text .push(legacy_color_code.chars().nth(i).unwrap()); }; i += 1; } - if components.is_empty() { - return TextComponent::new("".to_string()); - } + components.push(cur_component); - // create the final component by using the first one as the base, and then - // adding the rest as siblings - let mut final_component = components.remove(0); + // create the final component by adding all of the components as siblings + let mut final_component = TextComponent::new(""); for component in components { final_component.base.siblings.push(component.get()); } @@ -227,8 +227,9 @@ mod tests { assert_eq!( component.to_ansi(), format!( - "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", + "{BOLD}{WHITE}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}", BOLD = Ansi::BOLD, + WHITE = Ansi::rgb(ChatFormatting::White.color().unwrap()), RESET = Ansi::RESET, DARK_BLUE = Ansi::rgb(ChatFormatting::DarkBlue.color().unwrap()), DARK_GREEN = Ansi::rgb(ChatFormatting::DarkGreen.color().unwrap()), diff --git a/azalea-chat/tests/integration_test.rs b/azalea-chat/tests/integration_test.rs index 37e4620f..08f59d48 100644 --- a/azalea-chat/tests/integration_test.rs +++ b/azalea-chat/tests/integration_test.rs @@ -41,7 +41,7 @@ fn complex_ansi_test() { { "text": " asdf", "italic": false, - "obfuscated": "true", + "obfuscated": true, "strikethrough": false }, { @@ -52,16 +52,18 @@ fn complex_ansi_test() { ) .unwrap(); let component = FormattedText::deserialize(&j).unwrap(); + assert_eq!( component.to_ansi(), format!( - "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}", + "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{underlined}{strikethrough}{abcdef}world{reset}{bold}{underlined}{obfuscated}{red} asdf{reset}{bold}{italic}{underlined}{red}!{reset}", bold = Ansi::BOLD, italic = Ansi::ITALIC, underlined = Ansi::UNDERLINED, red = Ansi::rgb(ChatFormatting::Red.color().unwrap()), reset = Ansi::RESET, strikethrough = Ansi::STRIKETHROUGH, + obfuscated = Ansi::OBFUSCATED, abcdef = Ansi::rgb(TextColor::parse("#abcdef").unwrap().value), ) ); @@ -71,5 +73,5 @@ fn complex_ansi_test() { fn component_from_string() { let j: Value = serde_json::from_str("\"foo\"").unwrap(); let component = FormattedText::deserialize(&j).unwrap(); - assert_eq!(component.to_ansi(), "foo"); + assert_eq!(component.to_ansi(), "\u{1b}[38;2;255;255;255mfoo\u{1b}[m"); } diff --git a/azalea-core/src/checksum.rs b/azalea-core/src/checksum.rs index 8265906f..4661d171 100644 --- a/azalea-core/src/checksum.rs +++ b/azalea-core/src/checksum.rs @@ -143,7 +143,6 @@ impl<'a, 'r> ser::Serializer for ChecksumSerializer<'a, 'r> { fn serialize_none(self) -> Result<()> { assert!(self.hasher.finish() == 0); - println!("serialize none"); self.hasher.write_u8(1); Ok(()) } @@ -217,7 +216,6 @@ impl<'a, 'r> ser::Serializer for ChecksumSerializer<'a, 'r> { fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq> { assert!(self.hasher.finish() == 0); - println!("serialize seq with len: {:?}", len); Ok(ChecksumListSerializer { hasher: self.hasher, registries: self.registries, @@ -654,7 +652,6 @@ fn update_hasher_for_list(h: &mut Crc32cHasher, values: &[Checksum]) { h.write_u8(5); } fn update_hasher_for_map(h: &mut Crc32cHasher, entries: &[(Checksum, Checksum)]) { - println!("getting checksum for map with {} entries", entries.len()); h.write_u8(2); let mut entries = entries.to_vec(); entries.sort_by(|a, b| match a.0.cmp(&b.0) { @@ -667,157 +664,3 @@ fn update_hasher_for_map(h: &mut Crc32cHasher, entries: &[(Checksum, Checksum)]) } h.write_u8(3); } - -// impl AzaleaChecksum for i8 { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(6); -// h.write(&self.to_le_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for i16 { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(7); -// h.write(&self.to_le_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for i32 { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(8); -// h.write(&self.to_le_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for i64 { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(9); -// h.write(&self.to_le_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for f32 { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(10); -// h.write(&self.to_le_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for f64 { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(11); -// h.write(&self.to_le_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for &str { -// fn azalea_checksum(&self) -> HashCode { -// println!("doing checksum for str: {self:?}"); -// let mut h = Crc32cHasher::default(); -// h.write_u8(12); -// h.write(&(self.len() as u32).to_le_bytes()); -// h.write(&self.as_bytes()); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for String { -// fn azalea_checksum(&self) -> HashCode { -// println!("doing checksum for String: {self:?}"); -// let mut h = Crc32cHasher::default(); -// h.write_u8(12); - -// let utf16 = self.encode_utf16().collect::<Vec<_>>(); -// h.write(&(utf16.len() as u32).to_le_bytes()); -// for c in utf16 { -// h.write(&c.to_le_bytes()); -// } - -// println!("doing checksum for string: {self:?}"); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for bool { -// fn azalea_checksum(&self) -> HashCode { -// println!("doing checksum for bool: {self:?}"); -// let mut h = Crc32cHasher::default(); -// h.write_u8(13); -// h.write_u8(*self as u8); -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for Vec<u8> { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(14); -// h.write(self); -// h.write_u8(15); - -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for Vec<i8> { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(14); -// for item in self { -// h.write(&[*item as u8]); -// } -// h.write_u8(15); - -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for Vec<u32> { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(16); -// for item in self { -// h.write(&item.to_le_bytes()); -// } -// h.write_u8(17); - -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for Vec<i32> { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(16); -// for item in self { -// h.write(&item.to_le_bytes()); -// } -// h.write_u8(17); - -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for Vec<u64> { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(18); -// for item in self { -// h.write(&item.to_le_bytes()); -// } -// h.write_u8(19); - -// HashCode(h.finish() as u32) -// } -// } -// impl AzaleaChecksum for Vec<i64> { -// fn azalea_checksum(&self) -> HashCode { -// let mut h = Crc32cHasher::default(); -// h.write_u8(18); -// for item in self { -// h.write(&item.to_le_bytes()); -// } -// h.write_u8(19); - -// HashCode(h.finish() as u32) -// } -// } diff --git a/azalea-protocol/src/packets/game/c_system_chat.rs b/azalea-protocol/src/packets/game/c_system_chat.rs index 356a31ff..b47fc5bd 100644 --- a/azalea-protocol/src/packets/game/c_system_chat.rs +++ b/azalea-protocol/src/packets/game/c_system_chat.rs @@ -67,7 +67,21 @@ mod tests { assert_eq!( packet.content.to_ansi(), - "\u{1b}[38;2;85;255;85mmatscan\u{1b}[m\u{1b}[38;2;255;255;255m: meow\u{1b}[m" + "\u{1b}[38;2;85;255;85mmatscan\u{1b}[38;2;255;255;255m: meow\u{1b}[m" ) } + + #[test] + fn test_ticket_message() { + #[rustfmt::skip] + let bytes = [ + 10, 9, 0, 5, 101, 120, 116, 114, 97, 10, 0, 0, 0, 11, 8, 0, 5, 99, 111, 108, 111, 114, 0, 6, 121, 101, 108, 108, 111, 119, 1, 0, 10, 117, 110, 100, 101, 114, 108, 105, 110, 101, 100, 0, 8, 0, 4, 116, 101, 120, 116, 0, 1, 91, 1, 0, 4, 98, 111, 108, 100, 0, 1, 0, 13, 115, 116, 114, 105, 107, 101, 116, 104, 114, 111, 117, 103, 104, 0, 1, 0, 10, 111, 98, 102, 117, 115, 99, 97, 116, 101, 100, 0, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 5, 103, 114, 101, 101, 110, 8, 0, 4, 116, 101, 120, 116, 0, 2, 83, 66, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 4, 97, 113, 117, 97, 8, 0, 4, 116, 101, 120, 116, 0, 6, 82, 97, 102, 102, 108, 101, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 6, 121, 101, 108, 108, 111, 119, 8, 0, 4, 116, 101, 120, 116, 0, 1, 93, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 4, 116, 101, 120, 116, 0, 1, 32, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 5, 119, 104, 105, 116, 101, 8, 0, 4, 116, 101, 120, 116, 0, 1, 91, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 6, 121, 101, 108, 108, 111, 119, 8, 0, 4, 116, 101, 120, 116, 0, 10, 72, 105, 103, 104, 114, 111, 108, 108, 101, 114, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 5, 119, 104, 105, 116, 101, 8, 0, 4, 116, 101, 120, 116, 0, 1, 93, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 5, 99, 111, 108, 111, 114, 0, 6, 121, 101, 108, 108, 111, 119, 8, 0, 4, 116, 101, 120, 116, 0, 10, 32, 115, 107, 121, 108, 97, 110, 100, 105, 97, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 4, 116, 101, 120, 116, 0, 1, 32, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 4, 116, 101, 120, 116, 0, 21, 106, 117, 115, 116, 32, 98, 111, 117, 103, 104, 116, 32, 49, 32, 116, 105, 99, 107, 101, 116, 33, 1, 0, 6, 105, 116, 97, 108, 105, 99, 0, 0, 8, 0, 4, 116, 101, 120, 116, 0, 0, 0, 0 + ]; + let packet = ClientboundSystemChat::azalea_read(&mut Cursor::new(&bytes)).unwrap(); + + assert_eq!( + packet.content.to_ansi(), + "\u{1b}[38;2;255;255;85m[\u{1b}[38;2;85;255;85mSB\u{1b}[38;2;85;255;255mRaffle\u{1b}[38;2;255;255;85m]\u{1b}[38;2;255;255;255m [\u{1b}[38;2;255;255;85mHighroller\u{1b}[38;2;255;255;255m]\u{1b}[38;2;255;255;85m skylandia\u{1b}[38;2;255;255;255m just bought 1 ticket!\u{1b}[m" + ); + } } |
