Expand TextComponent

This commit is contained in:
Snowiiii 2024-08-09 00:48:19 +02:00
parent b4abc6b297
commit 5f3a4d34d1
4 changed files with 263 additions and 25 deletions

View File

@ -1,13 +1,47 @@
use core::str; use core::str;
use fastnbt::SerOpts; use fastnbt::SerOpts;
use serde::Deserialize; use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Text(Box<TextComponent>);
// Fepresents a Text component // Fepresents a Text component
// Reference: https://wiki.vg/Text_formatting#Text_components // 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 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<Color>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// 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<u8>,
/// 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<String>,
} }
impl serde::Serialize for TextComponent { impl serde::Serialize for TextComponent {
@ -20,33 +54,152 @@ impl serde::Serialize for TextComponent {
} }
impl TextComponent { impl TextComponent {
pub fn encode(&self) -> Vec<u8> { pub fn color(mut self, color: Color) -> Self {
#[derive(serde::Serialize)] self.color = Some(color);
struct TempStruct<'a> { self
text: &'a String,
} }
fastnbt::to_bytes_with_opts(&TempStruct { text: &self.text }, SerOpts::network_nbt()) pub fn color_named(mut self, color: NamedColor) -> Self {
.unwrap() 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<u8> {
// TODO: Somehow fix this ugly mess
#[derive(serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TempStruct<'a> {
#[serde(flatten)]
text: &'a TextContent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub color: &'a Option<Color>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bold: &'a Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub italic: &'a Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub underlined: &'a Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strikethrough: &'a Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub obfuscated: &'a Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub insertion: &'a Option<String>,
}
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<String> for TextComponent { impl From<String> for TextComponent {
fn from(value: String) -> Self { 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 { impl From<&str> for TextComponent {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
Self { Self {
content: TextContent::Text {
text: value.to_string(), 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<Text>,
},
/// 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<Text>,
},
/// 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 /// Text color
#[derive(Default, Debug, Clone, Copy)] #[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Color { pub enum Color {
/// The default color for the text will be used, which varies by context /// 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 /// (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 /// 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 { pub enum NamedColor {
Black = 0, Black = 0,
DarkBlue, DarkBlue,
DarkGreen, DarkGreen,
DarkCyan, DarkAqua,
DarkRed, DarkRed,
Purple, DarkPurple,
Gold, Gold,
Gray, Gray,
DarkGray, DarkGray,
Blue, Blue,
BrightGreen, Green,
Cyan, Aqua,
Red, Red,
Pink, LightPurple,
Yellow, Yellow,
White, White,
} }

View File

@ -343,9 +343,7 @@ impl Client {
.unwrap_or_else(|_| self.close()); .unwrap_or_else(|_| self.close());
} }
ConnectionState::Play => { ConnectionState::Play => {
self.try_send_packet(CPlayDisconnect::new(TextComponent { self.try_send_packet(CPlayDisconnect::new(TextComponent::from(reason)))
text: reason.to_string(),
}))
.unwrap_or_else(|_| self.close()); .unwrap_or_else(|_| self.close());
} }
_ => { _ => {

View File

@ -31,7 +31,7 @@ impl<'a> CommandSender<'a> {
pub fn send_message(&mut self, text: TextComponent) { pub fn send_message(&mut self, text: TextComponent) {
match self { match self {
// TODO: add color and stuff to console // 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), CommandSender::Player(c) => c.send_system_message(text),
} }
} }

View File

@ -20,6 +20,92 @@ impl<'a> Command<'a> for PumpkinCommand {
fn on_execute(sender: &mut super::CommandSender<'a>, _command: String) { fn on_execute(sender: &mut super::CommandSender<'a>, _command: String) {
let version = env!("CARGO_PKG_VERSION"); let version = env!("CARGO_PKG_VERSION");
let description = env!("CARGO_PKG_DESCRIPTION"); 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),
);
} }
} }