aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <git@matdoes.dev>2025-08-12 17:39:05 +1200
committermat <git@matdoes.dev>2025-08-12 17:39:05 +1200
commit12aeae07d2667cd188acf88190ed5c3f7983926a (patch)
tree4f4549b8e37d76f098d4164398517813bb0dfcc6
parentfa1050d6eedf80bbc20e91163b99c0306414e627 (diff)
downloadazalea-drasl-12aeae07d2667cd188acf88190ed5c3f7983926a.tar.xz
fix wrong chat styling sometimes when 'extra' field is used
-rw-r--r--CHANGELOG.md3
-rw-r--r--azalea-chat/src/base_component.rs1
-rw-r--r--azalea-chat/src/click_event.rs31
-rw-r--r--azalea-chat/src/component.rs161
-rw-r--r--azalea-chat/src/style.rs202
-rw-r--r--azalea-chat/src/text_component.rs47
-rw-r--r--azalea-chat/tests/integration_test.rs8
-rw-r--r--azalea-core/src/checksum.rs157
-rw-r--r--azalea-protocol/src/packets/game/c_system_chat.rs16
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"
+ );
+ }
}