aboutsummaryrefslogtreecommitdiff
path: root/azalea-chat/src
diff options
context:
space:
mode:
authormat <27899617+mat-1@users.noreply.github.com>2025-08-10 18:55:23 -0500
committerGitHub <noreply@github.com>2025-08-10 18:55:23 -0500
commit7120842f9d2c659a2f12d8922299c2a761bc5582 (patch)
tree0d7976ceec82d914e4c75f23adcdd5839f9960a4 /azalea-chat/src
parent3b659833c1ad4cca89b4cd553193edcb6d223163 (diff)
downloadazalea-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.rs12
-rw-r--r--azalea-chat/src/click_event.rs16
-rw-r--r--azalea-chat/src/component.rs6
-rw-r--r--azalea-chat/src/hover_event.rs20
-rw-r--r--azalea-chat/src/lib.rs2
-rw-r--r--azalea-chat/src/style.rs132
-rw-r--r--azalea-chat/src/text_component.rs18
-rw-r--r--azalea-chat/src/translatable_component.rs10
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(),
})