diff --git a/pumpkin-protocol/src/text.rs b/pumpkin-protocol/src/text.rs index 78ebbfcaa..58940cc42 100644 --- a/pumpkin-protocol/src/text.rs +++ b/pumpkin-protocol/src/text.rs @@ -1,13 +1,47 @@ use core::str; use fastnbt::SerOpts; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Text(Box); // Fepresents a Text component // Reference: https://wiki.vg/Text_formatting#Text_components -#[derive(Clone, PartialEq, Default, Debug, Deserialize)] +#[derive(Clone, Default, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct TextComponent { - pub text: String, + /// The actual text + #[serde(flatten)] + pub content: TextContent, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(flatten)] + /// Changes the color to render the content + pub color: Option, + /// Whether to render the content in bold. + /// Keep in mind that booleans are representet as bytes in nbt + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bold: Option, + /// Whether to render the content in italic. + /// Keep in mind that booleans are representet as bytes in nbt + #[serde(default, skip_serializing_if = "Option::is_none")] + pub italic: Option, + /// Whether to render the content in underlined. + /// Keep in mind that booleans are representet as bytes in nbt + #[serde(default, skip_serializing_if = "Option::is_none")] + pub underlined: Option, + /// Whether to render the content in strikethrough. + /// Keep in mind that booleans are representet as bytes in nbt + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strikethrough: Option, + /// Whether to render the content in obfuscated. + /// Keep in mind that booleans are representet as bytes in nbt + #[serde(default, skip_serializing_if = "Option::is_none")] + pub obfuscated: Option, + /// When the text is shift-clicked by a player, this string is inserted in their chat input. It does not overwrite any existing text the player was writing. This only works in chat messages + #[serde(default, skip_serializing_if = "Option::is_none")] + pub insertion: Option, } impl serde::Serialize for TextComponent { @@ -20,33 +54,152 @@ impl serde::Serialize for TextComponent { } impl TextComponent { + pub fn color(mut self, color: Color) -> Self { + self.color = Some(color); + self + } + + pub fn color_named(mut self, color: NamedColor) -> Self { + self.color = Some(Color::Named(color)); + self + } + + /// Makes the text bold + pub fn bold(mut self) -> Self { + self.bold = Some(1); + self + } + + /// Makes the text italic + pub fn italic(mut self) -> Self { + self.italic = Some(1); + self + } + + /// Makes the text underlined + pub fn underlined(mut self) -> Self { + self.underlined = Some(1); + self + } + + /// Makes the text strikethrough + pub fn strikethrough(mut self) -> Self { + self.strikethrough = Some(1); + self + } + + /// Makes the text obfuscated + pub fn obfuscated(mut self) -> Self { + self.obfuscated = Some(1); + self + } + + /// When the text is shift-clicked by a player, this string is inserted in their chat input. It does not overwrite any existing text the player was writing. This only works in chat messages + pub fn insertion(mut self, text: String) -> Self { + self.insertion = Some(text); + self + } + pub fn encode(&self) -> Vec { - #[derive(serde::Serialize)] + // TODO: Somehow fix this ugly mess + #[derive(serde::Serialize, Debug)] + #[serde(rename_all = "camelCase")] struct TempStruct<'a> { - text: &'a String, + #[serde(flatten)] + text: &'a TextContent, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub color: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub bold: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub italic: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub underlined: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub strikethrough: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub obfuscated: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub insertion: &'a Option, } - - fastnbt::to_bytes_with_opts(&TempStruct { text: &self.text }, SerOpts::network_nbt()) - .unwrap() + let astruct = TempStruct { + text: &self.content, + color: &self.color, + bold: &self.bold, + italic: &self.italic, + underlined: &self.underlined, + strikethrough: &self.strikethrough, + obfuscated: &self.obfuscated, + insertion: &self.insertion, + }; + let nbt = fastnbt::to_bytes_with_opts(&astruct, SerOpts::network_nbt()).unwrap(); + nbt } } impl From for TextComponent { fn from(value: String) -> Self { - Self { text: value } + Self { + content: TextContent::Text { text: value }, + color: None, + bold: None, + italic: None, + underlined: None, + strikethrough: None, + obfuscated: None, + insertion: None, + } } } impl From<&str> for TextComponent { fn from(value: &str) -> Self { Self { - text: value.to_string(), + content: TextContent::Text { + text: value.to_string(), + }, + color: None, + bold: None, + italic: None, + underlined: None, + strikethrough: None, + obfuscated: None, + insertion: None, } } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TextContent { + /// Raw Text + Text { text: String }, + /// Translated text + Translate { + translate: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + with: Vec, + }, + /// Displays the name of one or more entities found by a selector. + EntityNames { + selector: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + separator: Option, + }, + /// A keybind identifier + /// https://minecraft.fandom.com/wiki/Controls#Configurable_controls + Keybind { keybind: String }, +} + +impl Default for TextContent { + fn default() -> Self { + Self::Text { text: "".into() } + } +} + /// Text color -#[derive(Default, Debug, Clone, Copy)] +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(untagged)] pub enum Color { /// The default color for the text will be used, which varies by context /// (in some cases, it's white; in others, it's black; in still others, it @@ -58,22 +211,23 @@ pub enum Color { } /// Named Minecraft color -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum NamedColor { Black = 0, DarkBlue, DarkGreen, - DarkCyan, + DarkAqua, DarkRed, - Purple, + DarkPurple, Gold, Gray, DarkGray, Blue, - BrightGreen, - Cyan, + Green, + Aqua, Red, - Pink, + LightPurple, Yellow, White, } diff --git a/pumpkin/src/client/mod.rs b/pumpkin/src/client/mod.rs index daae01a85..82e1bbf2f 100644 --- a/pumpkin/src/client/mod.rs +++ b/pumpkin/src/client/mod.rs @@ -343,10 +343,8 @@ impl Client { .unwrap_or_else(|_| self.close()); } ConnectionState::Play => { - self.try_send_packet(CPlayDisconnect::new(TextComponent { - text: reason.to_string(), - })) - .unwrap_or_else(|_| self.close()); + self.try_send_packet(CPlayDisconnect::new(TextComponent::from(reason))) + .unwrap_or_else(|_| self.close()); } _ => { log::warn!("Cant't kick in {:?} State", self.connection_state) diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index 33cb4c819..56650391e 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -31,7 +31,7 @@ impl<'a> CommandSender<'a> { pub fn send_message(&mut self, text: TextComponent) { match self { // TODO: add color and stuff to console - CommandSender::Console => log::info!("{}", text.text), + CommandSender::Console => log::info!("{:?}", text.content), CommandSender::Player(c) => c.send_system_message(text), } } diff --git a/pumpkin/src/commands/pumpkin.rs b/pumpkin/src/commands/pumpkin.rs index 69d560192..f946a461a 100644 --- a/pumpkin/src/commands/pumpkin.rs +++ b/pumpkin/src/commands/pumpkin.rs @@ -20,6 +20,92 @@ impl<'a> Command<'a> for PumpkinCommand { fn on_execute(sender: &mut super::CommandSender<'a>, _command: String) { let version = env!("CARGO_PKG_VERSION"); let description = env!("CARGO_PKG_DESCRIPTION"); - sender.send_message(TextComponent::from(format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})"))) + // sender.send_message(TextComponent::from(format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")).color_named(pumpkin_protocol::text::NamedColor::Green)) + + // test + sender.send_message( + TextComponent::from("Pumpkin") + .color_named(pumpkin_protocol::text::NamedColor::Black) + .bold(), + ); + sender.send_message( + TextComponent::from("is") + .color_named(pumpkin_protocol::text::NamedColor::DarkBlue) + .italic(), + ); + sender.send_message( + TextComponent::from("such") + .color_named(pumpkin_protocol::text::NamedColor::DarkGreen) + .underlined(), + ); + sender.send_message( + TextComponent::from("a") + .color_named(pumpkin_protocol::text::NamedColor::DarkAqua) + .strikethrough(), + ); + sender.send_message( + TextComponent::from("super") + .color_named(pumpkin_protocol::text::NamedColor::DarkRed) + .obfuscated(), + ); + sender.send_message( + TextComponent::from("mega") + .color_named(pumpkin_protocol::text::NamedColor::DarkPurple) + .bold(), + ); + sender.send_message( + TextComponent::from("great") + .color_named(pumpkin_protocol::text::NamedColor::Gold) + .italic(), + ); + sender.send_message( + TextComponent::from("project") + .color_named(pumpkin_protocol::text::NamedColor::Gray) + .underlined(), + ); + sender.send_message(TextComponent::from("")); + sender.send_message( + TextComponent::from("no") + .color_named(pumpkin_protocol::text::NamedColor::DarkGray) + .bold(), + ); + sender.send_message( + TextComponent::from("worries") + .color_named(pumpkin_protocol::text::NamedColor::Blue) + .bold(), + ); + sender.send_message( + TextComponent::from("there") + .color_named(pumpkin_protocol::text::NamedColor::Green) + .underlined(), + ); + sender.send_message( + TextComponent::from("will") + .color_named(pumpkin_protocol::text::NamedColor::Aqua) + .italic(), + ); + sender.send_message( + TextComponent::from("be") + .color_named(pumpkin_protocol::text::NamedColor::Red) + .obfuscated(), + ); + sender.send_message( + TextComponent::from("chunk") + .color_named(pumpkin_protocol::text::NamedColor::LightPurple) + .bold(), + ); + sender.send_message( + TextComponent::from("loading") + .color_named(pumpkin_protocol::text::NamedColor::Yellow) + .strikethrough(), + ); + sender.send_message( + TextComponent::from("soon") + .color_named(pumpkin_protocol::text::NamedColor::White) + .bold(), + ); + sender.send_message( + TextComponent::from("soon").color_named(pumpkin_protocol::text::NamedColor::White), + ); } }