From 5f3a4d34d14e6e059b873a4beffeb1db9988d333 Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Fri, 9 Aug 2024 00:48:19 +0200 Subject: [PATCH 01/17] Expand TextComponent --- pumpkin-protocol/src/text.rs | 192 ++++++++++++++++++++++++++++---- pumpkin/src/client/mod.rs | 6 +- pumpkin/src/commands/mod.rs | 2 +- pumpkin/src/commands/pumpkin.rs | 88 ++++++++++++++- 4 files changed, 263 insertions(+), 25 deletions(-) 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 encode(&self) -> Vec { - #[derive(serde::Serialize)] - struct TempStruct<'a> { - text: &'a String, - } + pub fn color(mut self, color: Color) -> Self { + self.color = Some(color); + self + } - fastnbt::to_bytes_with_opts(&TempStruct { text: &self.text }, SerOpts::network_nbt()) - .unwrap() + 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 { + // 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, + #[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, + } + 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), + ); } } From f122b47e50841ec4665ee064b7e0af698980aa37 Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Fri, 9 Aug 2024 10:41:10 +0200 Subject: [PATCH 02/17] Create pumpkin-text --- Cargo.lock | 12 ++- Cargo.toml | 2 +- pumpkin-protocol/Cargo.toml | 5 +- .../client/play/c_disguised_chat_message.rs | 3 +- .../src/client/play/c_play_disconnect.rs | 3 +- .../src/client/play/c_player_chat_message.rs | 3 +- .../src/client/play/c_system_chat_message.rs | 3 +- pumpkin-protocol/src/lib.rs | 1 - pumpkin-text/Cargo.toml | 9 ++ pumpkin-text/src/click.rs | 19 ++++ pumpkin-text/src/color.rs | 36 +++++++ pumpkin-text/src/hover.rs | 32 +++++++ .../src/text.rs => pumpkin-text/src/lib.rs | 75 +++++++-------- pumpkin/Cargo.toml | 1 + pumpkin/src/client/mod.rs | 2 +- pumpkin/src/client/player_packet.rs | 2 +- pumpkin/src/commands/mod.rs | 2 +- pumpkin/src/commands/pumpkin.rs | 93 +------------------ 18 files changed, 162 insertions(+), 141 deletions(-) create mode 100644 pumpkin-text/Cargo.toml create mode 100644 pumpkin-text/src/click.rs create mode 100644 pumpkin-text/src/color.rs create mode 100644 pumpkin-text/src/hover.rs rename pumpkin-protocol/src/text.rs => pumpkin-text/src/lib.rs (80%) diff --git a/Cargo.lock b/Cargo.lock index 768e1fcf9..b3c78926e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,6 +1155,7 @@ dependencies = [ "pumpkin-entity", "pumpkin-protocol", "pumpkin-registry", + "pumpkin-text", "pumpkin-world", "rand", "reqwest", @@ -1194,12 +1195,12 @@ dependencies = [ "aes", "bytes", "cfb8", - "fastnbt 2.5.0 (git+https://github.com/owengage/fastnbt.git)", "flate2", "log", "num-derive", "num-traits", "pumpkin-macros", + "pumpkin-text", "serde", "serde_json", "take_mut", @@ -1217,6 +1218,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pumpkin-text" +version = "0.1.0" +dependencies = [ + "fastnbt 2.5.0 (git+https://github.com/owengage/fastnbt.git)", + "serde", + "uuid", +] + [[package]] name = "pumpkin-world" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 158cf41d4..7d819891f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "pumpkin-entity", "pumpkin-macros/", "pumpkin-protocol/", "pumpkin-registry/", "pumpkin-world", "pumpkin/" ] +members = [ "pumpkin-entity", "pumpkin-macros/", "pumpkin-protocol/", "pumpkin-registry/", "pumpkin-text", "pumpkin-world", "pumpkin/" ] [workspace.package] version = "0.1.0" diff --git a/pumpkin-protocol/Cargo.toml b/pumpkin-protocol/Cargo.toml index 941bdfbfe..e63bfb2c1 100644 --- a/pumpkin-protocol/Cargo.toml +++ b/pumpkin-protocol/Cargo.toml @@ -4,8 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] - pumpkin-macros = { path = "../pumpkin-macros" } +pumpkin-text = { path = "../pumpkin-text" } bytes = "1.7" @@ -22,9 +22,6 @@ log = "0.4" num-traits = "0.2" num-derive = "0.4" -# for text component -fastnbt = { git = "https://github.com/owengage/fastnbt.git" } - # encryption aes = "0.8.4" cfb8 = "0.8.1" diff --git a/pumpkin-protocol/src/client/play/c_disguised_chat_message.rs b/pumpkin-protocol/src/client/play/c_disguised_chat_message.rs index beff080d7..fab48f175 100644 --- a/pumpkin-protocol/src/client/play/c_disguised_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_disguised_chat_message.rs @@ -1,6 +1,7 @@ use pumpkin_macros::packet; +use pumpkin_text::TextComponent; -use crate::{text::TextComponent, ClientPacket, VarInt}; +use crate::{ClientPacket, VarInt}; #[derive(Clone)] #[packet(0x1E)] diff --git a/pumpkin-protocol/src/client/play/c_play_disconnect.rs b/pumpkin-protocol/src/client/play/c_play_disconnect.rs index e54771f52..e5aa3f197 100644 --- a/pumpkin-protocol/src/client/play/c_play_disconnect.rs +++ b/pumpkin-protocol/src/client/play/c_play_disconnect.rs @@ -1,8 +1,7 @@ use pumpkin_macros::packet; +use pumpkin_text::TextComponent; use serde::Serialize; -use crate::text::TextComponent; - #[derive(Serialize)] #[packet(0x1D)] pub struct CPlayDisconnect { diff --git a/pumpkin-protocol/src/client/play/c_player_chat_message.rs b/pumpkin-protocol/src/client/play/c_player_chat_message.rs index 927e0ebbc..79b403dab 100644 --- a/pumpkin-protocol/src/client/play/c_player_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_player_chat_message.rs @@ -1,8 +1,9 @@ use num_derive::{FromPrimitive, ToPrimitive}; use pumpkin_macros::packet; +use pumpkin_text::TextComponent; use serde::Serialize; -use crate::{text::TextComponent, VarInt}; +use crate::{ VarInt}; #[derive(Serialize, Clone)] #[packet(0x39)] diff --git a/pumpkin-protocol/src/client/play/c_system_chat_message.rs b/pumpkin-protocol/src/client/play/c_system_chat_message.rs index 9c68b5860..17b2bedce 100644 --- a/pumpkin-protocol/src/client/play/c_system_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_system_chat_message.rs @@ -1,8 +1,7 @@ use pumpkin_macros::packet; +use pumpkin_text::TextComponent; use serde::Serialize; -use crate::text::TextComponent; - #[derive(Serialize, Clone)] #[packet(0x6C)] pub struct CSystemChatMessge { diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index c84279d16..72a061297 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -8,7 +8,6 @@ use thiserror::Error; pub mod bytebuf; pub mod client; pub mod server; -pub mod text; pub mod packet_decoder; pub mod packet_encoder; diff --git a/pumpkin-text/Cargo.toml b/pumpkin-text/Cargo.toml new file mode 100644 index 000000000..d687a00b6 --- /dev/null +++ b/pumpkin-text/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "pumpkin-text" +version.workspace = true +edition.workspace = true + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +fastnbt = { git = "https://github.com/owengage/fastnbt.git" } +uuid = "1.10" diff --git a/pumpkin-text/src/click.rs b/pumpkin-text/src/click.rs new file mode 100644 index 000000000..5bdc91694 --- /dev/null +++ b/pumpkin-text/src/click.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +/// Action to take on click of the text. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +#[serde(tag = "action", content = "value", rename_all = "snake_case")] +pub enum ClickEvent { + /// Opens an URL + OpenUrl(String), + /// Works in signs, but only on the root text component + RunCommand(String), + /// Replaces the contents of the chat box with the text, not necessarily a + /// command. + SuggestCommand(String), + /// Only usable within written books. Changes the page of the book. Indexing + /// starts at 1. + ChangePage(i32), + /// Copies the given text to system clipboard + CopyToClipboard(String), +} \ No newline at end of file diff --git a/pumpkin-text/src/color.rs b/pumpkin-text/src/color.rs new file mode 100644 index 000000000..f4afea427 --- /dev/null +++ b/pumpkin-text/src/color.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +/// Text color +#[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 + /// is a shade of gray that isn't normally used on text). + #[default] + Reset, + /// One of the 16 named Minecraft colors + Named(NamedColor), +} + +/// Named Minecraft color +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum NamedColor { + Black = 0, + DarkBlue, + DarkGreen, + DarkAqua, + DarkRed, + DarkPurple, + Gold, + Gray, + DarkGray, + Blue, + Green, + Aqua, + Red, + LightPurple, + Yellow, + White, +} \ No newline at end of file diff --git a/pumpkin-text/src/hover.rs b/pumpkin-text/src/hover.rs new file mode 100644 index 000000000..77f9b3622 --- /dev/null +++ b/pumpkin-text/src/hover.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +use crate::Text; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "action", content = "contents", rename_all = "snake_case")] +#[allow(clippy::enum_variant_names)] +pub enum HoverEvent { + /// Displays a tooltip with the given text. + ShowText(Text), + /// Shows an item. + ShowItem { + /// Resource identifier of the item + id: String, + /// Number of the items in the stack + count: Option, + /// NBT information about the item (sNBT format) + tag: String, + }, + /// Shows an entity. + ShowEntity { + /// The entity's UUID + id: uuid::Uuid, + /// Resource identifier of the entity + #[serde(rename = "type")] + #[serde(default, skip_serializing_if = "Option::is_none")] + kind: Option, + /// Optional custom name for the entity + #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, + }, +} diff --git a/pumpkin-protocol/src/text.rs b/pumpkin-text/src/lib.rs similarity index 80% rename from pumpkin-protocol/src/text.rs rename to pumpkin-text/src/lib.rs index 58940cc42..4913a8f4f 100644 --- a/pumpkin-protocol/src/text.rs +++ b/pumpkin-text/src/lib.rs @@ -1,11 +1,18 @@ use core::str; +use click::ClickEvent; +use color::Color; use fastnbt::SerOpts; +use hover::HoverEvent; use serde::{Deserialize, Serialize}; +pub mod color; +pub mod click; +pub mod hover; + #[derive(Clone, Default, Debug, Serialize, Deserialize)] #[serde(transparent)] -pub struct Text(Box); +pub struct Text(pub Box); // Fepresents a Text component // Reference: https://wiki.vg/Text_formatting#Text_components @@ -42,6 +49,12 @@ pub struct TextComponent { /// 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, + /// Allows for events to occur when the player clicks on text. Only work in chat. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub click_event: Option, + /// Allows for a tooltip to be displayed when the player hovers their mouse over text. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub hover_event: Option, } impl serde::Serialize for TextComponent { @@ -59,7 +72,7 @@ impl TextComponent { self } - pub fn color_named(mut self, color: NamedColor) -> Self { + pub fn color_named(mut self, color: color::NamedColor) -> Self { self.color = Some(Color::Named(color)); self } @@ -100,6 +113,18 @@ impl TextComponent { self } + /// Allows for events to occur when the player clicks on text. Only work in chat. + pub fn click_event(mut self, event: ClickEvent) -> Self { + self.click_event = Some(event); + self + } + + /// Allows for a tooltip to be displayed when the player hovers their mouse over text. + pub fn hover_event(mut self, event: HoverEvent) -> Self { + self.hover_event = Some(event); + self + } + pub fn encode(&self) -> Vec { // TODO: Somehow fix this ugly mess #[derive(serde::Serialize, Debug)] @@ -121,6 +146,10 @@ impl TextComponent { pub obfuscated: &'a Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub insertion: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub click_event: &'a Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub hover_event: &'a Option, } let astruct = TempStruct { text: &self.content, @@ -131,7 +160,10 @@ impl TextComponent { strikethrough: &self.strikethrough, obfuscated: &self.obfuscated, insertion: &self.insertion, + click_event: &self.click_event, + hover_event: &self.hover_event, }; + // dbg!(&serde_json::to_string(&astruct)); let nbt = fastnbt::to_bytes_with_opts(&astruct, SerOpts::network_nbt()).unwrap(); nbt } @@ -148,6 +180,8 @@ impl From for TextComponent { strikethrough: None, obfuscated: None, insertion: None, + click_event: None, + hover_event: None, } } } @@ -165,6 +199,8 @@ impl From<&str> for TextComponent { strikethrough: None, obfuscated: None, insertion: None, + click_event: None, + hover_event: None, } } } @@ -196,38 +232,3 @@ impl Default for TextContent { Self::Text { text: "".into() } } } - -/// Text color -#[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 - /// is a shade of gray that isn't normally used on text). - #[default] - Reset, - /// One of the 16 named Minecraft colors - Named(NamedColor), -} - -/// Named Minecraft color -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum NamedColor { - Black = 0, - DarkBlue, - DarkGreen, - DarkAqua, - DarkRed, - DarkPurple, - Gold, - Gray, - DarkGray, - Blue, - Green, - Aqua, - Red, - LightPurple, - Yellow, - White, -} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 6b599f81e..83738121d 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -5,6 +5,7 @@ description = "Empowering everyone to host fast and efficient Minecraft servers. edition = "2021" [dependencies] +pumpkin-text = { path = "../pumpkin-text" } pumpkin-world = { path = "../pumpkin-world"} pumpkin-entity = { path = "../pumpkin-entity"} pumpkin-protocol = { path = "../pumpkin-protocol"} diff --git a/pumpkin/src/client/mod.rs b/pumpkin/src/client/mod.rs index 82e1bbf2f..ac355d672 100644 --- a/pumpkin/src/client/mod.rs +++ b/pumpkin/src/client/mod.rs @@ -32,9 +32,9 @@ use pumpkin_protocol::{ }, status::{SPingRequest, SStatusRequest}, }, - text::TextComponent, ClientPacket, ConnectionState, PacketError, RawPacket, ServerPacket, }; +use pumpkin_text::TextComponent; use std::io::Read; use thiserror::Error; diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 1a07da9d8..ea6625cf1 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -8,8 +8,8 @@ use pumpkin_protocol::{ SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SSwingArm, }, - text::TextComponent, }; +use pumpkin_text::TextComponent; use crate::{ commands::{handle_command, CommandSender}, diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index 56650391e..ba095c710 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -1,6 +1,6 @@ use gamemode::GamemodeCommand; use pumpkin::PumpkinCommand; -use pumpkin_protocol::text::TextComponent; +use pumpkin_text::TextComponent; use crate::client::Client; diff --git a/pumpkin/src/commands/pumpkin.rs b/pumpkin/src/commands/pumpkin.rs index f946a461a..3cdbeb83d 100644 --- a/pumpkin/src/commands/pumpkin.rs +++ b/pumpkin/src/commands/pumpkin.rs @@ -1,4 +1,7 @@ -use pumpkin_protocol::{text::TextComponent, CURRENT_MC_PROTOCOL}; +use pumpkin_protocol::{ + CURRENT_MC_PROTOCOL, +}; +use pumpkin_text::{click::ClickEvent, color::NamedColor, hover::HoverEvent, TextComponent}; use crate::server::CURRENT_MC_VERSION; @@ -20,92 +23,6 @@ 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})")).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), - ); + sender.send_message(TextComponent::from(format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")).color_named(NamedColor::Green)) } } From e96f072951652f029d4aaaa8fe08ab99c23701b7 Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Fri, 9 Aug 2024 12:52:53 +0200 Subject: [PATCH 03/17] Fix: Clientbound remove entities --- pumpkin-protocol/src/client/play/c_remove_entities.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pumpkin-protocol/src/client/play/c_remove_entities.rs b/pumpkin-protocol/src/client/play/c_remove_entities.rs index e8dab896d..22477d744 100644 --- a/pumpkin-protocol/src/client/play/c_remove_entities.rs +++ b/pumpkin-protocol/src/client/play/c_remove_entities.rs @@ -6,11 +6,12 @@ use crate::VarInt; #[derive(Serialize, Clone)] #[packet(0x42)] pub struct CRemoveEntities { + count: VarInt, entitiy_ids: Vec, } impl CRemoveEntities { pub fn new(entitiy_ids: Vec) -> Self { - Self { entitiy_ids } + Self { count: VarInt(entitiy_ids.len() as i32), entitiy_ids } } } From 770d73f080e33ba7db71deca2a0826a84e54318c Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Fri, 9 Aug 2024 14:29:51 +0200 Subject: [PATCH 04/17] minior position/rotation improvements --- .../src/client/play/c_player_chat_message.rs | 2 +- .../src/client/play/c_remove_entities.rs | 5 ++- .../src/client/play/c_spawn_player.rs | 24 +++++------ pumpkin-text/src/click.rs | 2 +- pumpkin-text/src/color.rs | 2 +- pumpkin-text/src/lib.rs | 8 ++-- pumpkin/src/client/player_packet.rs | 40 +++++++++++++------ pumpkin/src/commands/pumpkin.rs | 6 +-- pumpkin/src/server.rs | 27 +++++++------ pumpkin/src/util/math.rs | 12 ++++++ pumpkin/src/util/mod.rs | 2 +- 11 files changed, 80 insertions(+), 50 deletions(-) create mode 100644 pumpkin/src/util/math.rs diff --git a/pumpkin-protocol/src/client/play/c_player_chat_message.rs b/pumpkin-protocol/src/client/play/c_player_chat_message.rs index 79b403dab..a08aaf99b 100644 --- a/pumpkin-protocol/src/client/play/c_player_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_player_chat_message.rs @@ -3,7 +3,7 @@ use pumpkin_macros::packet; use pumpkin_text::TextComponent; use serde::Serialize; -use crate::{ VarInt}; +use crate::VarInt; #[derive(Serialize, Clone)] #[packet(0x39)] diff --git a/pumpkin-protocol/src/client/play/c_remove_entities.rs b/pumpkin-protocol/src/client/play/c_remove_entities.rs index 22477d744..bf711da8b 100644 --- a/pumpkin-protocol/src/client/play/c_remove_entities.rs +++ b/pumpkin-protocol/src/client/play/c_remove_entities.rs @@ -12,6 +12,9 @@ pub struct CRemoveEntities { impl CRemoveEntities { pub fn new(entitiy_ids: Vec) -> Self { - Self { count: VarInt(entitiy_ids.len() as i32), entitiy_ids } + Self { + count: VarInt(entitiy_ids.len() as i32), + entitiy_ids, + } } } diff --git a/pumpkin-protocol/src/client/play/c_spawn_player.rs b/pumpkin-protocol/src/client/play/c_spawn_player.rs index 9bfa4a694..1efec8c5a 100644 --- a/pumpkin-protocol/src/client/play/c_spawn_player.rs +++ b/pumpkin-protocol/src/client/play/c_spawn_player.rs @@ -29,13 +29,13 @@ impl CSpawnEntity { x: f64, y: f64, z: f64, - pitch: u8, // angle - yaw: u8, // angle - head_yaw: u8, // angle + pitch: f32, // angle + yaw: f32, // angle + head_yaw: f32, // angle data: VarInt, - velocity_x: i16, - velocity_y: i16, - velocity_z: i16, + velocity_x: f32, + velocity_y: f32, + velocity_z: f32, ) -> Self { Self { entity_id, @@ -44,13 +44,13 @@ impl CSpawnEntity { x, y, z, - pitch, - yaw, - head_yaw, + pitch: (pitch * 256.0 / 360.0).floor() as u8, + yaw: (yaw * 256.0 / 360.0).floor() as u8, + head_yaw: (head_yaw * 256.0 / 360.0).floor() as u8, data, - velocity_x, - velocity_y, - velocity_z, + velocity_x: (velocity_x.clamp(-3.9, 3.9) * 8000.0) as i16, + velocity_y: (velocity_y.clamp(-3.9, 3.9) * 8000.0) as i16, + velocity_z: (velocity_z.clamp(-3.9, 3.9) * 8000.0) as i16, } } } diff --git a/pumpkin-text/src/click.rs b/pumpkin-text/src/click.rs index 5bdc91694..c1656e28b 100644 --- a/pumpkin-text/src/click.rs +++ b/pumpkin-text/src/click.rs @@ -16,4 +16,4 @@ pub enum ClickEvent { ChangePage(i32), /// Copies the given text to system clipboard CopyToClipboard(String), -} \ No newline at end of file +} diff --git a/pumpkin-text/src/color.rs b/pumpkin-text/src/color.rs index f4afea427..6fa25219b 100644 --- a/pumpkin-text/src/color.rs +++ b/pumpkin-text/src/color.rs @@ -33,4 +33,4 @@ pub enum NamedColor { LightPurple, Yellow, White, -} \ No newline at end of file +} diff --git a/pumpkin-text/src/lib.rs b/pumpkin-text/src/lib.rs index 4913a8f4f..f18c04188 100644 --- a/pumpkin-text/src/lib.rs +++ b/pumpkin-text/src/lib.rs @@ -6,8 +6,8 @@ use fastnbt::SerOpts; use hover::HoverEvent; use serde::{Deserialize, Serialize}; -pub mod color; pub mod click; +pub mod color; pub mod hover; #[derive(Clone, Default, Debug, Serialize, Deserialize)] @@ -163,9 +163,9 @@ impl TextComponent { click_event: &self.click_event, hover_event: &self.hover_event, }; - // dbg!(&serde_json::to_string(&astruct)); - let nbt = fastnbt::to_bytes_with_opts(&astruct, SerOpts::network_nbt()).unwrap(); - nbt + // dbg!(&serde_json::to_string(&astruct)); + + fastnbt::to_bytes_with_opts(&astruct, SerOpts::network_nbt()).unwrap() } } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index ea6625cf1..9163561a4 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -15,6 +15,7 @@ use crate::{ commands::{handle_command, CommandSender}, entity::player::Hand, server::Server, + util::math::wrap_degrees, }; use super::Client; @@ -38,18 +39,27 @@ impl Client { } } + fn clamp_horizontal(pos: f64) -> f64 { + pos.clamp(-3.0E7, 3.0E7) + } + + fn clamp_vertical(pos: f64) -> f64 { + pos.clamp(-2.0E7, 2.0E7) + } + pub fn handle_position(&mut self, server: &mut Server, position: SPlayerPosition) { if position.x.is_nan() || position.feet_y.is_nan() || position.z.is_nan() { self.kick("Invalid movement"); + return; } let player = self.player.as_mut().unwrap(); let entity = &mut player.entity; entity.lastx = entity.x; entity.lasty = entity.y; entity.lastz = entity.z; - entity.x = position.x; - entity.y = position.feet_y; - entity.z = position.z; + entity.x = Self::clamp_horizontal(position.x); + entity.y = Self::clamp_vertical(position.feet_y); + entity.z = Self::clamp_horizontal(position.z); // TODO: teleport when moving > 8 block // send new position to all other players @@ -81,18 +91,20 @@ impl Client { || position_rotation.z.is_nan() { self.kick("Invalid movement"); + return; } if !position_rotation.yaw.is_finite() || !position_rotation.pitch.is_finite() { self.kick("Invalid rotation"); + return; } let player = self.player.as_mut().unwrap(); let entity = &mut player.entity; - entity.x = position_rotation.x; - entity.y = position_rotation.feet_y; - entity.z = position_rotation.z; - entity.yaw = position_rotation.yaw % 360.0; - entity.pitch = position_rotation.pitch.clamp(-90.0, 90.0) % 360.0; + entity.x = Self::clamp_horizontal(position_rotation.x); + entity.y = Self::clamp_vertical(position_rotation.feet_y); + entity.z = Self::clamp_horizontal(position_rotation.z); + entity.yaw = wrap_degrees(position_rotation.yaw); + entity.pitch = wrap_degrees(position_rotation.pitch); // send new position to all other players let on_ground = player.on_ground; @@ -122,22 +134,24 @@ impl Client { pub fn handle_rotation(&mut self, server: &mut Server, rotation: SPlayerRotation) { if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() { self.kick("Invalid rotation"); + return; } let player = self.player.as_mut().unwrap(); let entity = &mut player.entity; - entity.yaw = rotation.yaw % 360.0; - entity.pitch = rotation.pitch.clamp(-90.0, 90.0) % 360.0; + entity.yaw = wrap_degrees(rotation.yaw); + entity.pitch = wrap_degrees(rotation.pitch); // send new position to all other players let on_ground = player.on_ground; let entity_id = entity.entity_id; - let yaw = entity.yaw; - let pitch = entity.pitch; + let yaw = (entity.yaw * 256.0 / 360.0).floor(); + let pitch = (entity.pitch * 256.0 / 360.0).floor(); + let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor(); server.broadcast_packet( self, CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, on_ground), ); - server.broadcast_packet(self, CHeadRot::new(entity_id.into(), yaw as u8)); + server.broadcast_packet(self, CHeadRot::new(entity_id.into(), head_yaw as u8)); } pub fn handle_chat_command(&mut self, _server: &mut Server, command: SChatCommand) { diff --git a/pumpkin/src/commands/pumpkin.rs b/pumpkin/src/commands/pumpkin.rs index 3cdbeb83d..0b6eabc76 100644 --- a/pumpkin/src/commands/pumpkin.rs +++ b/pumpkin/src/commands/pumpkin.rs @@ -1,7 +1,5 @@ -use pumpkin_protocol::{ - CURRENT_MC_PROTOCOL, -}; -use pumpkin_text::{click::ClickEvent, color::NamedColor, hover::HoverEvent, TextComponent}; +use pumpkin_protocol::CURRENT_MC_PROTOCOL; +use pumpkin_text::{color::NamedColor, TextComponent}; use crate::server::CURRENT_MC_VERSION; diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index 3cc11a6cd..afcf829a2 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -165,6 +165,8 @@ impl Server { let x = 10.0; let y = 500.0; let z = 10.0; + let yaw = 10.0; + let pitch = 10.0; client.teleport(x, y, z, 10.0, 10.0); let gameprofile = client.gameprofile.as_ref().unwrap(); // first send info update to our new player, So he can see his Skin @@ -208,6 +210,7 @@ impl Server { // spawn player for every client self.broadcast_packet_expect( client, + // TODO: add velo CSpawnEntity::new( entity_id.into(), gameprofile.id, @@ -215,13 +218,13 @@ impl Server { x, y, z, - 0, - 0, - 0, + pitch, + yaw, + yaw, 0.into(), - 0, - 0, - 0, + 0.0, + 0.0, + 0.0, ), ); // spawn players for our client @@ -238,13 +241,13 @@ impl Server { entity.x, entity.y, entity.z, - entity.yaw as u8, - entity.pitch as u8, - 0, + entity.yaw, + entity.pitch, + entity.pitch, 0.into(), - 0, - 0, - 0, + 0.0, + 0.0, + 0.0, )) } } diff --git a/pumpkin/src/util/math.rs b/pumpkin/src/util/math.rs new file mode 100644 index 000000000..2d90277d4 --- /dev/null +++ b/pumpkin/src/util/math.rs @@ -0,0 +1,12 @@ +pub fn wrap_degrees(var: f32) -> f32 { + let mut var1 = var % 360.0; + if var1 >= 180.0 { + var1 -= 360.0; + } + + if var1 < -180.0 { + var1 += 360.0; + } + + var1 +} diff --git a/pumpkin/src/util/mod.rs b/pumpkin/src/util/mod.rs index 8b1378917..b8135c3c3 100644 --- a/pumpkin/src/util/mod.rs +++ b/pumpkin/src/util/mod.rs @@ -1 +1 @@ - +pub mod math; From 76106ded18bc551e90a4fcc8e50401b3304cde4c Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Fri, 9 Aug 2024 22:14:41 +0200 Subject: [PATCH 05/17] Add WIP Resource Pack support --- Cargo.lock | 11 +++++ .../src/client/config/c_add_resource_pack.rs | 31 ++++++++++++++ pumpkin-protocol/src/client/config/mod.rs | 2 + pumpkin/Cargo.toml | 2 +- pumpkin/src/client/authentication.rs | 10 +++-- pumpkin/src/client/client_packet.rs | 22 +++++++++- pumpkin/src/config/auth_config.rs | 4 +- pumpkin/src/config/mod.rs | 28 +++++++------ pumpkin/src/config/resource_pack.rs | 40 +++++++++++++++++++ pumpkin/src/server.rs | 8 +++- 10 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 pumpkin-protocol/src/client/config/c_add_resource_pack.rs create mode 100644 pumpkin/src/config/resource_pack.rs diff --git a/Cargo.lock b/Cargo.lock index b3c78926e..9e11ea864 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -776,6 +776,16 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1984,6 +1994,7 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ + "md-5", "serde", ] diff --git a/pumpkin-protocol/src/client/config/c_add_resource_pack.rs b/pumpkin-protocol/src/client/config/c_add_resource_pack.rs new file mode 100644 index 000000000..2980d85d0 --- /dev/null +++ b/pumpkin-protocol/src/client/config/c_add_resource_pack.rs @@ -0,0 +1,31 @@ +use pumpkin_macros::packet; +use pumpkin_text::TextComponent; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(0x09)] +pub struct CConfigAddResourcePack { + uuid: uuid::Uuid, + url: String, + hash: String, + forced: bool, + prompt_message: Option, +} + +impl CConfigAddResourcePack { + pub fn new( + uuid: uuid::Uuid, + url: String, + hash: String, + forced: bool, + prompt_message: Option, + ) -> Self { + Self { + uuid, + url, + hash, + forced, + prompt_message, + } + } +} diff --git a/pumpkin-protocol/src/client/config/mod.rs b/pumpkin-protocol/src/client/config/mod.rs index 99128ab90..1e53e20d5 100644 --- a/pumpkin-protocol/src/client/config/mod.rs +++ b/pumpkin-protocol/src/client/config/mod.rs @@ -1,3 +1,4 @@ +mod c_add_resource_pack; mod c_config_disconnect; mod c_cookie_request; mod c_finish_config; @@ -5,6 +6,7 @@ mod c_known_packs; mod c_plugin_message; mod c_registry_data; +pub use c_add_resource_pack::*; pub use c_config_disconnect::*; pub use c_cookie_request::*; pub use c_finish_config::*; diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 83738121d..e4b726028 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -46,6 +46,6 @@ log = "0.4" mio = { version = "1.0.1", features = ["os-poll", "net"]} crossbeam-channel = "0.5.13" -uuid = { version = "1.10", features = ["serde"]} +uuid = { version = "1.10", features = ["serde", "v3"]} tokio.workspace = true \ No newline at end of file diff --git a/pumpkin/src/client/authentication.rs b/pumpkin/src/client/authentication.rs index c13388a8c..2de7a4b20 100644 --- a/pumpkin/src/client/authentication.rs +++ b/pumpkin/src/client/authentication.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, net::IpAddr}; +use std::{collections::HashMap, net::IpAddr, time::Duration}; use base64::{engine::general_purpose, Engine}; use num_bigint::BigInt; use pumpkin_protocol::Property; -use reqwest::{StatusCode, Url}; +use reqwest::{header::CONTENT_TYPE, StatusCode, Url}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; @@ -52,14 +52,16 @@ pub async fn authenticate( ip: &IpAddr, server: &mut Server, ) -> Result { - assert!(server.advanced_config.authentication.use_authentication); + assert!(server.advanced_config.authentication.enabled); assert!(server.auth_client.is_some()); let address = if server .advanced_config .authentication .prevent_proxy_connections { - format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}") + let test = format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}"); + dbg!(&test); + test } else { format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}") }; diff --git a/pumpkin/src/client/client_packet.rs b/pumpkin/src/client/client_packet.rs index 754103392..c8cefdf28 100644 --- a/pumpkin/src/client/client_packet.rs +++ b/pumpkin/src/client/client_packet.rs @@ -1,7 +1,9 @@ +use std::str::FromStr; + use num_traits::FromPrimitive; use pumpkin_protocol::{ client::{ - config::{CFinishConfig, CKnownPacks, CRegistryData}, + config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData}, login::{CEncryptionRequest, CLoginSuccess, CSetCompression}, status::{CPingResponse, CStatusResponse}, }, @@ -14,6 +16,7 @@ use pumpkin_protocol::{ ConnectionState, KnownPack, RawBytes, }; use pumpkin_registry::Registry; +use pumpkin_text::TextComponent; use rsa::Pkcs1v15Encrypt; use sha1::{Digest, Sha1}; @@ -167,6 +170,23 @@ impl Client { ) { self.connection_state = ConnectionState::Config; server.send_brand(self); + + let resource_config = &server.advanced_config.resource_pack; + if resource_config.enabled { + let prompt_message = if resource_config.prompt_message.is_empty() { + None + } else { + Some(TextComponent::from(resource_config.prompt_message.clone())) + }; + self.send_packet(CConfigAddResourcePack::new( + uuid::Uuid::from_str(&resource_config.resource_pack_url).unwrap(), + resource_config.resource_pack_url.clone(), + resource_config.resource_pack_sha1.clone(), + resource_config.force, + prompt_message, + )); + } + // known data packs self.send_packet(CKnownPacks::new(&[KnownPack { namespace: "minecraft", diff --git a/pumpkin/src/config/auth_config.rs b/pumpkin/src/config/auth_config.rs index 0fe373ca8..5768cd1fc 100644 --- a/pumpkin/src/config/auth_config.rs +++ b/pumpkin/src/config/auth_config.rs @@ -5,7 +5,7 @@ use crate::client::authentication::ProfileAction; #[derive(Deserialize, Serialize)] pub struct Authentication { /// Whether to use Mojang authentication. - pub use_authentication: bool, + pub enabled: bool, /// Prevent proxy connections. pub prevent_proxy_connections: bool, @@ -84,7 +84,7 @@ impl Default for TextureTypes { impl Default for Authentication { fn default() -> Self { Self { - use_authentication: true, + enabled: true, prevent_proxy_connections: true, player_profile: Default::default(), textures: Default::default(), diff --git a/pumpkin/src/config/mod.rs b/pumpkin/src/config/mod.rs index 02acb657a..98779ec4a 100644 --- a/pumpkin/src/config/mod.rs +++ b/pumpkin/src/config/mod.rs @@ -1,11 +1,13 @@ use std::path::Path; use auth_config::Authentication; +use resource_pack::ResourcePack; use serde::{Deserialize, Serialize}; use crate::{entity::player::GameMode, server::Difficulty}; pub mod auth_config; +pub mod resource_pack; /// Current Config version of the Base Config const CURRENT_BASE_VERSION: &str = "1.0.0"; @@ -18,6 +20,7 @@ pub struct AdvancedConfiguration { pub commands: Commands, pub authentication: Authentication, pub packet_compression: Compression, + pub resource_pack: ResourcePack, } #[derive(Deserialize, Serialize)] @@ -63,6 +66,7 @@ impl Default for AdvancedConfiguration { authentication: Authentication::default(), commands: Commands::default(), packet_compression: Compression::default(), + resource_pack: ResourcePack::default(), } } } @@ -83,10 +87,6 @@ pub struct BasicConfiguration { pub view_distance: u8, /// The maximum simulated view distance. pub simulation_distance: u8, - /// The path to the resource pack. - pub resource_pack: String, - /// The SHA1 hash of the resource pack. - pub resource_pack_sha1: String, /// The default game difficulty. pub default_difficulty: Difficulty, /// Whether the Nether dimension is enabled. @@ -113,8 +113,6 @@ impl Default for BasicConfiguration { max_players: 100000, view_distance: 10, simulation_distance: 10, - resource_pack: "".to_string(), - resource_pack_sha1: "".to_string(), default_difficulty: Difficulty::Normal, allow_nether: true, hardcore: false, @@ -130,21 +128,30 @@ impl AdvancedConfiguration { pub fn load>(path: P) -> AdvancedConfiguration { if path.as_ref().exists() { let toml = std::fs::read_to_string(path).expect("Couldn't read configuration"); - toml::from_str(toml.as_str()).expect("Couldn't parse, Proberbly old config") + let config: AdvancedConfiguration = + toml::from_str(toml.as_str()).expect("Couldn't parse features.toml, Proberbly old config, Replacing with a new one or just delete it"); + config.validate(); + config } else { let config = AdvancedConfiguration::default(); let toml = toml::to_string(&config).expect("Couldn't create toml!"); std::fs::write(path, toml).expect("Couldn't save configuration"); + config.validate(); config } } + pub fn validate(&self) { + self.resource_pack.validate() + } } impl BasicConfiguration { pub fn load>(path: P) -> BasicConfiguration { if path.as_ref().exists() { let toml = std::fs::read_to_string(path).expect("Couldn't read configuration"); - toml::from_str(toml.as_str()).expect("Couldn't parse") + let config: BasicConfiguration = toml::from_str(toml.as_str()).expect("Couldn't parse configuration.toml, Proberbly old config, Replacing with a new one or just delete it"); + config.validate(); + config } else { let config = BasicConfiguration::default(); let toml = toml::to_string(&config).expect("Couldn't create toml!"); @@ -170,10 +177,5 @@ impl BasicConfiguration { "When Online Mode is enabled, Encryption must be enabled" ) } - assert_eq!( - !self.resource_pack.is_empty(), - !self.resource_pack_sha1.is_empty(), - "Resource Pack path or Sha1 hash is missing" - ); } } diff --git a/pumpkin/src/config/resource_pack.rs b/pumpkin/src/config/resource_pack.rs new file mode 100644 index 000000000..25e97d759 --- /dev/null +++ b/pumpkin/src/config/resource_pack.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct ResourcePack { + pub enabled: bool, + /// The path to the resource pack. + pub resource_pack_url: String, + /// The SHA1 hash (40) of the resource pack. + pub resource_pack_sha1: String, + /// Custom propmt Text component, Leave blank for none + pub prompt_message: String, + /// Will force the Player to accept the resource pack + pub force: bool, +} + +impl ResourcePack { + pub fn validate(&self) { + assert_eq!( + !self.resource_pack_url.is_empty(), + !self.resource_pack_sha1.is_empty(), + "Resource Pack path or Sha1 hash is missing" + ); + assert!( + self.resource_pack_sha1.len() <= 40, + "Resource pack sha1 hash is too long (max. 40)" + ) + } +} + +impl Default for ResourcePack { + fn default() -> Self { + Self { + enabled: false, + resource_pack_url: "".into(), + resource_pack_sha1: "".into(), + force: false, + prompt_message: "".into(), + } + } +} diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index afcf829a2..e4ac7464e 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -4,6 +4,7 @@ use std::{ io::Cursor, rc::Rc, sync::atomic::{AtomicI32, Ordering}, + time::Duration, }; use base64::{engine::general_purpose, Engine}; @@ -77,7 +78,12 @@ impl Server { ) .into_boxed_slice(); let auth_client = if config.0.online_mode { - Some(reqwest::Client::new()) + Some( + reqwest::Client::builder() + .timeout(Duration::from_millis(5000)) + .build() + .expect("Failed to to make reqwest client"), + ) } else { None }; From 50dcbfeb6dc4f856d9145456a80f34ec0e9dabd1 Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Fri, 9 Aug 2024 22:38:00 +0200 Subject: [PATCH 06/17] Add inventory support --- Cargo.lock | 9 ++++ Cargo.toml | 2 +- README.md | 1 + pumpkin-inventory/Cargo.toml | 8 ++++ pumpkin-inventory/src/lib.rs | 41 +++++++++++++++++++ .../src/client/play/c_open_screen.rs | 23 +++++++++++ pumpkin-protocol/src/client/play/mod.rs | 2 + pumpkin-registry/src/chat_type.rs | 6 +++ pumpkin-text/src/lib.rs | 2 +- pumpkin/Cargo.toml | 1 + pumpkin/src/client/client_packet.rs | 1 + pumpkin/src/client/player_packet.rs | 15 +++++-- 12 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 pumpkin-inventory/Cargo.toml create mode 100644 pumpkin-inventory/src/lib.rs create mode 100644 pumpkin-protocol/src/client/play/c_open_screen.rs diff --git a/Cargo.lock b/Cargo.lock index 9e11ea864..65dfa29a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1163,6 +1163,7 @@ dependencies = [ "num-derive", "num-traits", "pumpkin-entity", + "pumpkin-inventory", "pumpkin-protocol", "pumpkin-registry", "pumpkin-text", @@ -1189,6 +1190,14 @@ dependencies = [ "num-traits", ] +[[package]] +name = "pumpkin-inventory" +version = "0.1.0" +dependencies = [ + "num-derive", + "num-traits", +] + [[package]] name = "pumpkin-macros" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7d819891f..c1b1034be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "pumpkin-entity", "pumpkin-macros/", "pumpkin-protocol/", "pumpkin-registry/", "pumpkin-text", "pumpkin-world", "pumpkin/" ] +members = [ "pumpkin-entity", "pumpkin-inventory", "pumpkin-macros/", "pumpkin-protocol/", "pumpkin-registry/", "pumpkin-world", "pumpkin/" ] [workspace.package] version = "0.1.0" diff --git a/README.md b/README.md index ba2b50a25..7ca197778 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Pumpkin is currently under heavy development. - [ ] Player Inventory - [ ] Player Attack - Server + - [x] Inventories - [x] Chat - [x] Commands diff --git a/pumpkin-inventory/Cargo.toml b/pumpkin-inventory/Cargo.toml new file mode 100644 index 000000000..f9f1b10d1 --- /dev/null +++ b/pumpkin-inventory/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "pumpkin-inventory" +version.workspace = true +edition.workspace = true + +[dependencies] +num-traits = "0.2" +num-derive = "0.4" \ No newline at end of file diff --git a/pumpkin-inventory/src/lib.rs b/pumpkin-inventory/src/lib.rs new file mode 100644 index 000000000..32a322a63 --- /dev/null +++ b/pumpkin-inventory/src/lib.rs @@ -0,0 +1,41 @@ +use num_derive::ToPrimitive; + +/// https://wiki.vg/Inventory +#[derive(Debug, ToPrimitive)] +pub enum WindowType { + // not used + Generic9x1, + // not used + Generic9x2, + // General-purpose 3-row inventory. Used by Chest, minecart with chest, ender chest, and barrel + Generic9x3, + // not used + Generic9x4, + // not used + Generic9x5, + // Used by large chests + Generic9x6, + // General-purpose 3-by-3 square inventory, used by Dispenser and Dropper + Generic3x3, + // General-purpose 3-by-3 square inventory, used by the Crafter + Craft3x3, + Anvil, + Beacon, + BlastFurance, + BrewingStand, + CraftingTable, + EnchantmentTable, + Furnace, + Grindstone, + // Hopper or minecart with hopper + Hopper, + Lectern, + Loom, + // Villager, Wandering Trader + Merchant, + ShulkerBox, + SmithingTable, + Smoker, + CartographyTable, + Stonecutter, +} diff --git a/pumpkin-protocol/src/client/play/c_open_screen.rs b/pumpkin-protocol/src/client/play/c_open_screen.rs new file mode 100644 index 000000000..0e0480584 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_open_screen.rs @@ -0,0 +1,23 @@ +use pumpkin_macros::packet; +use pumpkin_text::TextComponent; +use serde::Serialize; + +use crate::VarInt; + +#[derive(Serialize, Clone)] +#[packet(0x33)] +pub struct COpenScreen { + window_id: VarInt, + window_type: VarInt, + window_title: TextComponent, +} + +impl COpenScreen { + pub fn new(window_id: VarInt, window_type: VarInt, window_title: TextComponent) -> Self { + Self { + window_id, + window_type, + window_title, + } + } +} diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index c120926ee..664d22b40 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -6,6 +6,7 @@ mod c_entity_metadata; mod c_game_event; mod c_head_rot; mod c_login; +mod c_open_screen; mod c_play_disconnect; mod c_player_abilities; mod c_player_chat_message; @@ -28,6 +29,7 @@ pub use c_entity_metadata::*; pub use c_game_event::*; pub use c_head_rot::*; pub use c_login::*; +pub use c_open_screen::*; pub use c_play_disconnect::*; pub use c_player_abilities::*; pub use c_player_chat_message::*; diff --git a/pumpkin-registry/src/chat_type.rs b/pumpkin-registry/src/chat_type.rs index 8b1378917..aa70c24bb 100644 --- a/pumpkin-registry/src/chat_type.rs +++ b/pumpkin-registry/src/chat_type.rs @@ -1 +1,7 @@ +pub struct ChatType {} +pub struct Decoration { + translation_key: String, + // style: Option, + parameters: Vec, +} diff --git a/pumpkin-text/src/lib.rs b/pumpkin-text/src/lib.rs index f18c04188..1a9512bdc 100644 --- a/pumpkin-text/src/lib.rs +++ b/pumpkin-text/src/lib.rs @@ -231,4 +231,4 @@ impl Default for TextContent { fn default() -> Self { Self::Text { text: "".into() } } -} +} \ No newline at end of file diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index e4b726028..4288e8fef 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] pumpkin-text = { path = "../pumpkin-text" } +pumpkin-inventory = { path = "../pumpkin-inventory"} pumpkin-world = { path = "../pumpkin-world"} pumpkin-entity = { path = "../pumpkin-entity"} pumpkin-protocol = { path = "../pumpkin-protocol"} diff --git a/pumpkin/src/client/client_packet.rs b/pumpkin/src/client/client_packet.rs index c8cefdf28..fd87d5711 100644 --- a/pumpkin/src/client/client_packet.rs +++ b/pumpkin/src/client/client_packet.rs @@ -134,6 +134,7 @@ impl Client { } } for ele in self.gameprofile.as_ref().unwrap().properties.clone() { + // todo, use this unpack_textures(ele, &server.advanced_config.authentication.textures); } diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 9163561a4..95d429338 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -1,14 +1,15 @@ use num_traits::FromPrimitive; +use pumpkin_inventory::WindowType; use pumpkin_protocol::{ client::play::{ - Animation, CEntityAnimation, CHeadRot, CSystemChatMessge, CUpdateEntityPos, + Animation, CEntityAnimation, CHeadRot, COpenScreen, CSystemChatMessge, CUpdateEntityPos, CUpdateEntityPosRot, CUpdateEntityRot, }, server::play::{ SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SSwingArm, - }, -}; + }, VarInt, + }; use pumpkin_text::TextComponent; use crate::{ @@ -189,6 +190,14 @@ impl Client { pub fn handle_chat_message(&mut self, server: &mut Server, chat_message: SChatMessage) { let message = chat_message.message; + server.broadcast_packet( + self, + COpenScreen::new( + VarInt(0), + VarInt(WindowType::CraftingTable as i32), + TextComponent::from("Test Crafter"), + ), + ); // TODO: filter message & validation let gameprofile = self.gameprofile.as_ref().unwrap(); dbg!("got message"); From 414cf3190ccc36e4f66e3e4d3c395245ff141b5b Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Sat, 10 Aug 2024 23:53:32 +0200 Subject: [PATCH 07/17] Added RCON support --- Cargo.lock | 1 + README.md | 10 +- pumpkin-registry/src/chat_type.rs | 2 +- pumpkin-text/src/lib.rs | 2 +- pumpkin/Cargo.toml | 2 + pumpkin/src/client/authentication.rs | 4 +- pumpkin/src/client/player_packet.rs | 7 +- pumpkin/src/commands/mod.rs | 7 +- pumpkin/src/config/mod.rs | 23 ++- pumpkin/src/main.rs | 11 +- pumpkin/src/rcon/mod.rs | 220 +++++++++++++++++++++++++++ pumpkin/src/rcon/packet.rs | 113 ++++++++++++++ 12 files changed, 386 insertions(+), 16 deletions(-) create mode 100644 pumpkin/src/rcon/mod.rs create mode 100644 pumpkin/src/rcon/packet.rs diff --git a/Cargo.lock b/Cargo.lock index 65dfa29a1..2475436a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,6 +1154,7 @@ name = "pumpkin" version = "0.1.0-dev" dependencies = [ "base64", + "bytes", "crossbeam-channel", "digest 0.11.0-pre.9", "image 0.25.2", diff --git a/README.md b/README.md index 7ca197778..505e303bd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![CI](https://github.com/Snowiiii/Pumpkin/actions/workflows/rust.yml/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -![Current version)](https://img.shields.io/badge/current_version-1.21-blue) +![Current version)](https://img.shields.io/badge/current_version-1.21.1-blue) @@ -27,13 +27,10 @@ Pumpkin is currently under heavy development. ### Features (WIP) - [x] Configuration (toml) - [x] Server Status/Ping - - [x] Custom maximum player amout + - [x] Custom maximum player amount - [x] Custom Icon - [x] Custom Status (MOTD) -- Login - - [x] Authentication - - [x] Encryption - - [x] Packet Compression +- [x] Login - Player Configuration - [x] Registries (biome types, paintings, dimensions) - [x] Server Brand @@ -57,6 +54,7 @@ Pumpkin is currently under heavy development. - [ ] Player Inventory - [ ] Player Attack - Server + - [x] RCON - [x] Inventories - [x] Chat - [x] Commands diff --git a/pumpkin-registry/src/chat_type.rs b/pumpkin-registry/src/chat_type.rs index aa70c24bb..a66dc7647 100644 --- a/pumpkin-registry/src/chat_type.rs +++ b/pumpkin-registry/src/chat_type.rs @@ -2,6 +2,6 @@ pub struct ChatType {} pub struct Decoration { translation_key: String, - // style: Option, + // style: Option, parameters: Vec, } diff --git a/pumpkin-text/src/lib.rs b/pumpkin-text/src/lib.rs index 1a9512bdc..f18c04188 100644 --- a/pumpkin-text/src/lib.rs +++ b/pumpkin-text/src/lib.rs @@ -231,4 +231,4 @@ impl Default for TextContent { fn default() -> Self { Self::Text { text: "".into() } } -} \ No newline at end of file +} diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 4288e8fef..b4b956829 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -17,6 +17,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8.19" +bytes = "1.7" + rand = "0.8.5" num-traits = "0.2" diff --git a/pumpkin/src/client/authentication.rs b/pumpkin/src/client/authentication.rs index 2de7a4b20..e4aa33996 100644 --- a/pumpkin/src/client/authentication.rs +++ b/pumpkin/src/client/authentication.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, net::IpAddr, time::Duration}; +use std::{collections::HashMap, net::IpAddr}; use base64::{engine::general_purpose, Engine}; use num_bigint::BigInt; use pumpkin_protocol::Property; -use reqwest::{header::CONTENT_TYPE, StatusCode, Url}; +use reqwest::{StatusCode, Url}; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index 95d429338..b020b4d4c 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -8,8 +8,9 @@ use pumpkin_protocol::{ server::play::{ SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation, SSwingArm, - }, VarInt, - }; + }, + VarInt, +}; use pumpkin_text::TextComponent; use crate::{ @@ -156,7 +157,7 @@ impl Client { } pub fn handle_chat_command(&mut self, _server: &mut Server, command: SChatCommand) { - handle_command(&mut CommandSender::Player(self), command.command); + handle_command(&mut CommandSender::Player(self), &command.command); } pub fn handle_player_command(&mut self, _server: &mut Server, command: SPlayerCommand) { diff --git a/pumpkin/src/commands/mod.rs b/pumpkin/src/commands/mod.rs index ba095c710..3e75202a2 100644 --- a/pumpkin/src/commands/mod.rs +++ b/pumpkin/src/commands/mod.rs @@ -23,6 +23,7 @@ pub trait Command<'a> { } pub enum CommandSender<'a> { + Rcon(&'a mut Vec), Console, Player(&'a mut Client), } @@ -33,6 +34,7 @@ impl<'a> CommandSender<'a> { // TODO: add color and stuff to console CommandSender::Console => log::info!("{:?}", text.content), CommandSender::Player(c) => c.send_system_message(text), + CommandSender::Rcon(s) => s.push(format!("{:?}", text.content)), } } @@ -40,6 +42,7 @@ impl<'a> CommandSender<'a> { match self { CommandSender::Console => false, CommandSender::Player(_) => true, + CommandSender::Rcon(_) => false, } } @@ -47,16 +50,18 @@ impl<'a> CommandSender<'a> { match self { CommandSender::Console => true, CommandSender::Player(_) => false, + CommandSender::Rcon(_) => true, } } pub fn as_mut_player(&mut self) -> Option<&mut Client> { match self { CommandSender::Player(client) => Some(client), CommandSender::Console => None, + CommandSender::Rcon(_) => None, } } } -pub fn handle_command(sender: &mut CommandSender, command: String) { +pub fn handle_command(sender: &mut CommandSender, command: &str) { let command = command.to_lowercase(); // an ugly mess i know if command.starts_with(PumpkinCommand::NAME) { diff --git a/pumpkin/src/config/mod.rs b/pumpkin/src/config/mod.rs index 98779ec4a..ee84669e0 100644 --- a/pumpkin/src/config/mod.rs +++ b/pumpkin/src/config/mod.rs @@ -21,6 +21,26 @@ pub struct AdvancedConfiguration { pub authentication: Authentication, pub packet_compression: Compression, pub resource_pack: ResourcePack, + pub rcon: RCONConfig, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct RCONConfig { + pub enabled: bool, + pub ip: String, + pub port: u16, + pub password: String, +} + +impl Default for RCONConfig { + fn default() -> Self { + Self { + enabled: false, + ip: "0.0.0.0".to_string(), + port: 25575, + password: "".to_string(), + } + } } #[derive(Deserialize, Serialize)] @@ -67,6 +87,7 @@ impl Default for AdvancedConfiguration { commands: Commands::default(), packet_compression: Compression::default(), resource_pack: ResourcePack::default(), + rcon: RCONConfig::default(), } } } @@ -107,7 +128,7 @@ impl Default for BasicConfiguration { fn default() -> Self { Self { config_version: CURRENT_BASE_VERSION.to_string(), - server_address: "127.0.0.1".to_string(), + server_address: "0.0.0.0".to_string(), server_port: 25565, seed: "".to_string(), max_players: 100000, diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index 2746444a6..2eb6a1bc3 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -19,6 +19,7 @@ pub mod client; pub mod commands; pub mod config; pub mod entity; +pub mod rcon; pub mod server; pub mod util; @@ -27,6 +28,8 @@ pub mod util; async fn main() -> io::Result<()> { use std::{cell::RefCell, time::Instant}; + use rcon::RCONServer; + let time = Instant::now(); let basic_config = BasicConfiguration::load("configuration.toml"); @@ -58,6 +61,7 @@ async fn main() -> io::Result<()> { let mut unique_token = Token(SERVER.0 + 1); let use_console = advanced_configuration.commands.use_console; + let rcon = advanced_configuration.rcon.clone(); let mut connections: HashMap>> = HashMap::new(); @@ -73,10 +77,15 @@ async fn main() -> io::Result<()> { stdin .read_line(&mut out) .expect("Failed to read console line"); - handle_command(&mut commands::CommandSender::Console, out); + handle_command(&mut commands::CommandSender::Console, &out); } }); } + if rcon.enabled { + tokio::spawn(async move { + RCONServer::new(&rcon).await.unwrap(); + }); + } loop { if let Err(err) = poll.poll(&mut events, None) { if interrupted(&err) { diff --git a/pumpkin/src/rcon/mod.rs b/pumpkin/src/rcon/mod.rs new file mode 100644 index 000000000..c3606ca2e --- /dev/null +++ b/pumpkin/src/rcon/mod.rs @@ -0,0 +1,220 @@ +use std::{ + collections::HashMap, + io::{self, Read}, +}; + +use mio::{ + net::{TcpListener, TcpStream}, + Events, Interest, Poll, Token, +}; +use packet::{Packet, PacketError, PacketType}; +use thiserror::Error; + +use crate::{commands::handle_command, config::RCONConfig}; + +mod packet; + +#[derive(Debug, Error)] +pub enum RCONError { + #[error("authentication failed")] + Auth, + #[error("command exceeds the maximum length")] + CommandTooLong, + #[error("{}", _0)] + Io(io::Error), +} + +const SERVER: Token = Token(0); + +pub struct RCONServer {} + +impl RCONServer { + pub async fn new(config: &RCONConfig) -> Result { + assert!(config.enabled, "RCON is not enabled"); + let addr = format!("{}:{}", config.ip, config.port) + .parse() + .expect("Failed to parse RCON address"); + let mut poll = Poll::new().unwrap(); + let mut listener = TcpListener::bind(addr).unwrap(); + + poll.registry() + .register(&mut listener, SERVER, Interest::READABLE) + .unwrap(); + + let mut unique_token = Token(SERVER.0 + 1); + + let mut events = Events::with_capacity(128); + + let mut connections: HashMap = HashMap::new(); + + let password = config.password.clone(); + + loop { + poll.poll(&mut events, None).unwrap(); + + for event in events.iter() { + match event.token() { + SERVER => loop { + // Received an event for the TCP server socket, which + // indicates we can accept an connection. + let (mut connection, address) = match listener.accept() { + Ok((connection, address)) => (connection, address), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + // If we get a `WouldBlock` error we know our + // listener has no more incoming connections queued, + // so we can return to polling and wait for some + // more. + break; + } + Err(e) => { + // If it was any other kind of error, something went + // wrong and we terminate with an error. + return Err(e); + } + }; + log::info!("Accepted connection from: {}", address); + + let token = Self::next(&mut unique_token); + poll.registry() + .register( + &mut connection, + token, + Interest::READABLE.add(Interest::WRITABLE), + ) + .unwrap(); + connections.insert(token, RCONClient::new(connection)); + }, + + token => { + let done = if let Some(client) = connections.get_mut(&token) { + client.handle(&password).await + } else { + false + }; + if done { + if let Some(mut client) = connections.remove(&token) { + poll.registry().deregister(&mut client.connection)?; + } + } + } + } + } + } + } + + fn next(current: &mut Token) -> Token { + let next = current.0; + current.0 += 1; + Token(next) + } +} + +pub struct RCONClient { + connection: TcpStream, + logged_in: bool, + incoming: Vec, + closed: bool, +} + +impl RCONClient { + pub fn new(connection: TcpStream) -> Self { + Self { + connection, + logged_in: false, + incoming: Vec::new(), + closed: false, + } + } + + pub async fn handle(&mut self, password: &str) -> bool { + if !self.closed { + loop { + match self.read_bytes() { + // Stream closed, so we can't reply, so we just close everything. + Ok(true) => return true, + Ok(false) => {} + Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, + Err(e) => { + log::error!("could not read packet: {e}"); + return true; + } + } + } + // If we get a close here, we might have a reply, which we still want to write. + match self.poll(password).await { + Ok(()) => {} + Err(e) => { + log::error!("rcon error: {e}"); + self.closed = true; + } + } + } + self.closed + } + + async fn poll(&mut self, password: &str) -> Result<(), PacketError> { + loop { + let packet = match self.receive_packet().await? { + Some(p) => p, + None => return Ok(()), + }; + + match packet.get_type() { + PacketType::Auth => { + let body = packet.get_body(); + if !body.is_empty() && packet.get_body() == password { + self.send(&mut Packet::new( + packet.get_id(), + PacketType::AuthResponse, + "".into(), + )) + .await + .unwrap(); + log::info!("RCON Client logged in successfully"); + self.logged_in = true; + } else { + log::warn!("RCON Client has tried wrong password"); + self.send(&mut Packet::new(-1, PacketType::AuthResponse, "".into())) + .await + .unwrap(); + return Err(PacketError::WrongPassword); + } + } + PacketType::ExecCommand => { + if self.logged_in { + let mut output = Vec::new(); + handle_command( + &mut crate::commands::CommandSender::Rcon(&mut output), + packet.get_body(), + ); + for line in output { + self.send(&mut Packet::new(packet.get_id(), PacketType::Output, line)) + .await + .unwrap(); + } + } + } + PacketType::Output => todo!(), + PacketType::AuthResponse => unreachable!(), + } + } + } + + fn read_bytes(&mut self) -> io::Result { + let mut buf = [0; 1460]; + let n = self.connection.read(&mut buf)?; + if n == 0 { + return Ok(true); + } + self.incoming.extend_from_slice(&buf[..n]); + Ok(false) + } + + async fn send(&mut self, packet: &mut Packet) -> io::Result<()> { + packet.send_packet(&mut self.connection).await + } + + async fn receive_packet(&mut self) -> Result, PacketError> { + Packet::deserialize(&mut self.incoming).await + } +} diff --git a/pumpkin/src/rcon/packet.rs b/pumpkin/src/rcon/packet.rs new file mode 100644 index 000000000..5d1abd195 --- /dev/null +++ b/pumpkin/src/rcon/packet.rs @@ -0,0 +1,113 @@ +use std::io::{self, BufRead, Cursor, Write}; + +use bytes::{BufMut, BytesMut}; +use mio::net::TcpStream; +use thiserror::Error; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PacketType { + Auth, + AuthResponse, + ExecCommand, + Output, +} + +#[derive(Error, Debug)] +pub enum PacketError { + #[error("invalid length")] + InvalidLength, + #[error("expected terminating NUL byte")] + NoNullTermination, + #[error("wrong password")] + WrongPassword, +} + +impl PacketType { + fn to_i32(self) -> i32 { + match self { + PacketType::Auth => 3, + PacketType::AuthResponse => 2, + PacketType::ExecCommand => 2, + PacketType::Output => 0, + } + } + + pub fn from_i32(n: i32) -> PacketType { + match n { + 3 => PacketType::Auth, + 2 => PacketType::ExecCommand, + _ => PacketType::Output, + } + } +} + +#[derive(Debug)] +pub struct Packet { + id: i32, + ptype: PacketType, + body: String, +} + +impl Packet { + pub fn new(id: i32, ptype: PacketType, body: String) -> Packet { + Packet { id, ptype, body } + } + + pub async fn send_packet(&mut self, connection: &mut TcpStream) -> io::Result<()> { + // let len = outgoing.len() as u64; + let mut buf = BytesMut::new(); + // 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes. + buf.put_i32_le(10 + self.get_body().len() as i32); + buf.put_i32_le(self.id); + buf.put_i32_le(self.get_type().to_i32()); + let bytes = self.get_body().as_bytes(); + buf.put_slice(bytes); + buf.put_u8(0); + buf.put_u8(0); + connection.write(&buf).unwrap(); + Ok(()) + } + + pub async fn deserialize(incoming: &mut Vec) -> Result, PacketError> { + if incoming.len() < 4 { + return Ok(None); + } + let mut buf = Cursor::new(&incoming); + let len = buf.read_i32_le().await.unwrap() + 4; + if !(0..=1460).contains(&len) { + return Err(PacketError::InvalidLength); + } + let id = buf.read_i32_le().await.unwrap(); + let ty = buf.read_i32_le().await.unwrap(); + let mut payload = vec![]; + let _ = buf.read_until(b'\0', &mut payload).unwrap(); + payload.pop(); + if buf.read_u8().await.unwrap() != 0 { + return Err(PacketError::NoNullTermination); + } + if buf.position() != len as u64 { + return Err(PacketError::InvalidLength); + } + incoming.drain(0..len as usize); + + let packet = Packet { + id, + ptype: PacketType::from_i32(ty), + body: String::from_utf8(payload).unwrap(), + }; + + Ok(Some(packet)) + } + pub fn get_body(&self) -> &str { + &self.body + } + + pub fn get_type(&self) -> PacketType { + self.ptype + } + + pub fn get_id(&self) -> i32 { + self.id + } +} From fd8897ce1d0b04c1020aad56d79d6e45e3fea378 Mon Sep 17 00:00:00 2001 From: lidar <97009593+lidarbtc@users.noreply.github.com> Date: Sun, 11 Aug 2024 11:59:57 +0900 Subject: [PATCH 08/17] Add release profile optimiztion --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index c1b1034be..79cd76031 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ version = "0.1.0" edition = "2021" [profile.release] +strip = true lto = true +codegen-units = 1 [workspace.dependencies] tokio = { version = "1.39.2", features = ["net", "macros", "rt-multi-thread", "fs", "io-util"] } From af912f30880197d04b66bc95e89599aab6b1a59b Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 11 Aug 2024 10:31:47 +0200 Subject: [PATCH 09/17] Disable prevent_proxy_connections by default --- pumpkin/src/config/auth_config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pumpkin/src/config/auth_config.rs b/pumpkin/src/config/auth_config.rs index 5768cd1fc..936ed9dda 100644 --- a/pumpkin/src/config/auth_config.rs +++ b/pumpkin/src/config/auth_config.rs @@ -85,7 +85,7 @@ impl Default for Authentication { fn default() -> Self { Self { enabled: true, - prevent_proxy_connections: true, + prevent_proxy_connections: false, player_profile: Default::default(), textures: Default::default(), } From 41442c5b5dd230264b4c70d2dccf848ff46d9807 Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Sun, 11 Aug 2024 14:18:56 +0200 Subject: [PATCH 10/17] Add text Titles --- pumpkin-protocol/src/client/play/c_actionbar.rs | 15 +++++++++++++++ pumpkin-protocol/src/client/play/c_set_title.rs | 15 +++++++++++++++ pumpkin-protocol/src/client/play/c_subtitle.rs | 15 +++++++++++++++ pumpkin-protocol/src/client/play/mod.rs | 6 ++++++ pumpkin/src/commands/gamemode.rs | 6 ------ pumpkin/src/commands/pumpkin.rs | 6 ------ 6 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 pumpkin-protocol/src/client/play/c_actionbar.rs create mode 100644 pumpkin-protocol/src/client/play/c_set_title.rs create mode 100644 pumpkin-protocol/src/client/play/c_subtitle.rs diff --git a/pumpkin-protocol/src/client/play/c_actionbar.rs b/pumpkin-protocol/src/client/play/c_actionbar.rs new file mode 100644 index 000000000..d9046de06 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_actionbar.rs @@ -0,0 +1,15 @@ +use pumpkin_macros::packet; +use pumpkin_text::TextComponent; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(0x4C)] +pub struct CActionBar { + action_bar: TextComponent, +} + +impl CActionBar { + pub fn new(action_bar: TextComponent) -> Self { + Self { action_bar } + } +} diff --git a/pumpkin-protocol/src/client/play/c_set_title.rs b/pumpkin-protocol/src/client/play/c_set_title.rs new file mode 100644 index 000000000..7a549b904 --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_set_title.rs @@ -0,0 +1,15 @@ +use pumpkin_macros::packet; +use pumpkin_text::TextComponent; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(0x65)] +pub struct CTitleText { + title: TextComponent, +} + +impl CTitleText { + pub fn new(title: TextComponent) -> Self { + Self { title } + } +} diff --git a/pumpkin-protocol/src/client/play/c_subtitle.rs b/pumpkin-protocol/src/client/play/c_subtitle.rs new file mode 100644 index 000000000..35ebbe50b --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_subtitle.rs @@ -0,0 +1,15 @@ +use pumpkin_macros::packet; +use pumpkin_text::TextComponent; +use serde::Serialize; + +#[derive(Serialize)] +#[packet(0x63)] +pub struct CSubtitle { + subtitle: TextComponent, +} + +impl CSubtitle { + pub fn new(subtitle: TextComponent) -> Self { + Self { subtitle } + } +} diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 664d22b40..84b92e1d0 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -1,3 +1,4 @@ +mod c_actionbar; mod c_change_difficulty; mod c_chunk_data_update_light; mod c_disguised_chat_message; @@ -13,7 +14,9 @@ mod c_player_chat_message; mod c_player_info_update; mod c_remove_entities; mod c_set_held_item; +mod c_set_title; mod c_spawn_player; +mod c_subtitle; mod c_sync_player_position; mod c_system_chat_message; mod c_update_entitiy_pos_rot; @@ -21,6 +24,7 @@ mod c_update_entity_pos; mod c_update_entity_rot; mod player_action; +pub use c_actionbar::*; pub use c_change_difficulty::*; pub use c_chunk_data_update_light::*; pub use c_disguised_chat_message::*; @@ -36,7 +40,9 @@ pub use c_player_chat_message::*; pub use c_player_info_update::*; pub use c_remove_entities::*; pub use c_set_held_item::*; +pub use c_set_title::*; pub use c_spawn_player::*; +pub use c_subtitle::*; pub use c_sync_player_position::*; pub use c_system_chat_message::*; pub use c_update_entitiy_pos_rot::*; diff --git a/pumpkin/src/commands/gamemode.rs b/pumpkin/src/commands/gamemode.rs index f21345987..4533f06e9 100644 --- a/pumpkin/src/commands/gamemode.rs +++ b/pumpkin/src/commands/gamemode.rs @@ -2,12 +2,6 @@ use super::Command; pub struct GamemodeCommand {} -impl GamemodeCommand { - pub fn new() -> Self { - Self {} - } -} - impl<'a> Command<'a> for GamemodeCommand { const NAME: &'a str = "gamemode"; diff --git a/pumpkin/src/commands/pumpkin.rs b/pumpkin/src/commands/pumpkin.rs index 0b6eabc76..8a648dd50 100644 --- a/pumpkin/src/commands/pumpkin.rs +++ b/pumpkin/src/commands/pumpkin.rs @@ -7,12 +7,6 @@ use super::Command; pub struct PumpkinCommand {} -impl PumpkinCommand { - pub fn new() -> Self { - Self {} - } -} - impl<'a> Command<'a> for PumpkinCommand { const NAME: &'a str = "pumpkin"; From 5b636111e407a7cb96c1109378ec303d9da46c3e Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Sun, 11 Aug 2024 17:55:41 +0200 Subject: [PATCH 11/17] Prepare ChatType registry --- .../src/client/login/c_login_success.rs | 4 +- pumpkin-protocol/src/client/play/c_login.rs | 12 ++--- .../src/client/play/c_player_chat_message.rs | 7 ++- .../src/client/play/c_remove_entities.rs | 8 ++-- pumpkin-protocol/src/lib.rs | 2 +- pumpkin-registry/src/chat_type.rs | 30 ++++++++++++- pumpkin-registry/src/lib.rs | 20 ++++++++- pumpkin/src/client/client_packet.rs | 7 ++- pumpkin/src/client/player_packet.rs | 44 +++++++++---------- pumpkin/src/server.rs | 11 +++-- 10 files changed, 99 insertions(+), 46 deletions(-) diff --git a/pumpkin-protocol/src/client/login/c_login_success.rs b/pumpkin-protocol/src/client/login/c_login_success.rs index 065dc5e10..2493f1ced 100644 --- a/pumpkin-protocol/src/client/login/c_login_success.rs +++ b/pumpkin-protocol/src/client/login/c_login_success.rs @@ -5,7 +5,7 @@ use crate::{bytebuf::ByteBuffer, ClientPacket, Property}; #[packet(0x02)] pub struct CLoginSuccess<'a> { pub uuid: uuid::Uuid, - pub username: String, // 16 + pub username: &'a str, // 16 pub properties: &'a [Property], pub strict_error_handling: bool, } @@ -13,7 +13,7 @@ pub struct CLoginSuccess<'a> { impl<'a> CLoginSuccess<'a> { pub fn new( uuid: uuid::Uuid, - username: String, + username: &'a str, properties: &'a [Property], strict_error_handling: bool, ) -> Self { diff --git a/pumpkin-protocol/src/client/play/c_login.rs b/pumpkin-protocol/src/client/play/c_login.rs index 4ad679bbf..11f923347 100644 --- a/pumpkin-protocol/src/client/play/c_login.rs +++ b/pumpkin-protocol/src/client/play/c_login.rs @@ -5,11 +5,11 @@ use crate::VarInt; #[derive(Serialize)] #[packet(0x2B)] -pub struct CLogin { +pub struct CLogin<'a> { entity_id: i32, is_hardcore: bool, dimension_count: VarInt, - dimension_names: Vec, + dimension_names: &'a [&'a str], max_players: VarInt, view_distance: VarInt, simulated_distance: VarInt, @@ -17,7 +17,7 @@ pub struct CLogin { enabled_respawn_screen: bool, limited_crafting: bool, dimension_type: VarInt, - dimension_name: String, + dimension_name: &'a str, hashed_seed: i64, game_mode: u8, previous_gamemode: i8, @@ -28,12 +28,12 @@ pub struct CLogin { enforce_secure_chat: bool, } -impl CLogin { +impl<'a> CLogin<'a> { #[allow(clippy::too_many_arguments)] pub fn new( entity_id: i32, is_hardcore: bool, - dimension_names: Vec, + dimension_names: &'a [&'a str], max_players: VarInt, view_distance: VarInt, simulated_distance: VarInt, @@ -41,7 +41,7 @@ impl CLogin { enabled_respawn_screen: bool, limited_crafting: bool, dimension_type: VarInt, - dimension_name: String, + dimension_name: &'a str, hashed_seed: i64, game_mode: u8, previous_gamemode: i8, diff --git a/pumpkin-protocol/src/client/play/c_player_chat_message.rs b/pumpkin-protocol/src/client/play/c_player_chat_message.rs index a08aaf99b..e207575ba 100644 --- a/pumpkin-protocol/src/client/play/c_player_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_player_chat_message.rs @@ -3,7 +3,7 @@ use pumpkin_macros::packet; use pumpkin_text::TextComponent; use serde::Serialize; -use crate::VarInt; +use crate::{BitSet, VarInt}; #[derive(Serialize, Clone)] #[packet(0x39)] @@ -14,10 +14,13 @@ pub struct CPlayerChatMessage<'a> { message: String, timestamp: i64, salt: i64, + previous_messages_count: VarInt, previous_messages: &'a [PreviousMessage<'a>], // max 20 unsigned_content: Option, /// See `FilterType` filter_type: VarInt, + // TODO: THIS IS A HACK, We currently don't support writing or reading bitsets + filter_type_bits: bool, chat_type: VarInt, sender_name: TextComponent, target_name: Option, @@ -46,9 +49,11 @@ impl<'a> CPlayerChatMessage<'a> { message, timestamp, salt, + previous_messages_count: previous_messages.len().into(), previous_messages, unsigned_content, filter_type, + filter_type_bits: false, chat_type, sender_name, target_name, diff --git a/pumpkin-protocol/src/client/play/c_remove_entities.rs b/pumpkin-protocol/src/client/play/c_remove_entities.rs index bf711da8b..d110b138e 100644 --- a/pumpkin-protocol/src/client/play/c_remove_entities.rs +++ b/pumpkin-protocol/src/client/play/c_remove_entities.rs @@ -5,13 +5,13 @@ use crate::VarInt; #[derive(Serialize, Clone)] #[packet(0x42)] -pub struct CRemoveEntities { +pub struct CRemoveEntities<'a> { count: VarInt, - entitiy_ids: Vec, + entitiy_ids: &'a [VarInt], } -impl CRemoveEntities { - pub fn new(entitiy_ids: Vec) -> Self { +impl<'a> CRemoveEntities<'a> { + pub fn new(entitiy_ids: &'a [VarInt]) -> Self { Self { count: VarInt(entitiy_ids.len() as i32), entitiy_ids, diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index 72a061297..2fe1b7cd0 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -2,7 +2,7 @@ use std::io::{self, Write}; use bytebuf::{packet_id::Packet, ByteBuffer, DeserializerError}; use bytes::Buf; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer}; use thiserror::Error; pub mod bytebuf; diff --git a/pumpkin-registry/src/chat_type.rs b/pumpkin-registry/src/chat_type.rs index a66dc7647..0614c2b4b 100644 --- a/pumpkin-registry/src/chat_type.rs +++ b/pumpkin-registry/src/chat_type.rs @@ -1,7 +1,33 @@ -pub struct ChatType {} +use serde::Serialize; +#[derive(Debug, Clone, Serialize)] +pub struct ChatType { + chat: Decoration, + narration: Decoration, +} + +#[derive(Debug, Clone, Serialize)] pub struct Decoration { translation_key: String, - // style: Option, + style: u8, + // TODO + // style: Option, parameters: Vec, } + +impl Default for ChatType { + fn default() -> Self { + Self { + chat: Decoration { + style: 0, + parameters: vec!["sender".into(), "content".into()], + translation_key: "chat.type.text".into(), + }, + narration: Decoration { + style: 0, + parameters: vec!["sender".into(), "content".into()], + translation_key: "chat.type.text.narrate".into(), + }, + } + } +} diff --git a/pumpkin-registry/src/lib.rs b/pumpkin-registry/src/lib.rs index 4d177f84d..8bee85a1e 100644 --- a/pumpkin-registry/src/lib.rs +++ b/pumpkin-registry/src/lib.rs @@ -1,4 +1,5 @@ use biomes::Biome; +use chat_type::ChatType; use dimensions::Dimension; use fastnbt::SerOpts; use paint::Painting; @@ -50,6 +51,16 @@ impl Registry { .unwrap(), }], }; + + let _chat_types = Registry { + registry_id: "minecraft:chat_type".to_string(), + registry_entries: vec![RegistryEntry { + entry_id: "minecraft:chat", + data: fastnbt::to_bytes_with_opts(&ChatType::default(), SerOpts::network_nbt()) + .unwrap(), + }], + }; + let damage_types = Registry { registry_id: "minecraft:damage_type".to_string(), registry_entries: damage_type::entires(), @@ -62,6 +73,13 @@ impl Registry { .unwrap(), }], }; - vec![dimensions, damage_types, biomes, wolf_variants, paintings] + vec![ + dimensions, + damage_types, + biomes, + wolf_variants, + paintings, + // chat_types, + ] } } diff --git a/pumpkin/src/client/client_packet.rs b/pumpkin/src/client/client_packet.rs index fd87d5711..0dcd3c18b 100644 --- a/pumpkin/src/client/client_packet.rs +++ b/pumpkin/src/client/client_packet.rs @@ -15,7 +15,6 @@ use pumpkin_protocol::{ }, ConnectionState, KnownPack, RawBytes, }; -use pumpkin_registry::Registry; use pumpkin_text::TextComponent; use rsa::Pkcs1v15Encrypt; use sha1::{Digest, Sha1}; @@ -150,7 +149,7 @@ impl Client { } if let Some(profile) = self.gameprofile.as_ref().cloned() { - let packet = CLoginSuccess::new(profile.id, profile.name, &profile.properties, false); + let packet = CLoginSuccess::new(profile.id, &profile.name, &profile.properties, false); self.send_packet(packet); } else { self.kick("game profile is none"); @@ -225,8 +224,8 @@ impl Client { } } - pub fn handle_known_packs(&mut self, _server: &mut Server, _config_acknowledged: SKnownPacks) { - for registry in Registry::get_static() { + pub fn handle_known_packs(&mut self, server: &mut Server, _config_acknowledged: SKnownPacks) { + for registry in &server.cached_registry { self.send_packet(CRegistryData::new( ®istry.registry_id, ®istry.registry_entries, diff --git a/pumpkin/src/client/player_packet.rs b/pumpkin/src/client/player_packet.rs index b020b4d4c..1f1cbcb2d 100644 --- a/pumpkin/src/client/player_packet.rs +++ b/pumpkin/src/client/player_packet.rs @@ -2,8 +2,8 @@ use num_traits::FromPrimitive; use pumpkin_inventory::WindowType; use pumpkin_protocol::{ client::play::{ - Animation, CEntityAnimation, CHeadRot, COpenScreen, CSystemChatMessge, CUpdateEntityPos, - CUpdateEntityPosRot, CUpdateEntityRot, + Animation, CEntityAnimation, CHeadRot, COpenScreen, CPlayerChatMessage, CUpdateEntityPos, + CUpdateEntityPosRot, CUpdateEntityRot, FilterType, }, server::play::{ SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition, @@ -203,30 +203,30 @@ impl Client { let gameprofile = self.gameprofile.as_ref().unwrap(); dbg!("got message"); // yeah a "raw system message", the ugly way to do that, but it works + // server.broadcast_packet( + // self, + // CSystemChatMessge::new( + // TextComponent::from(format!("{}: {}", gameprofile.name, message)), + // false, + // ), + // ); server.broadcast_packet( self, - CSystemChatMessge::new( - TextComponent::from(format!("{}: {}", gameprofile.name, message)), - false, + CPlayerChatMessage::new( + gameprofile.id, + 0.into(), + None, + message.clone(), + chat_message.timestamp, + chat_message.salt, + &[], + Some(TextComponent::from(message.clone())), + pumpkin_protocol::VarInt(FilterType::PassThrough as i32), + 0.into(), + TextComponent::from(gameprofile.name.clone()), + None, ), ) - /*server.broadcast_packet( - self, - CPlayerChatMessage::new( - gameprofile.id, - 0.into(), - None, - message.clone(), - chat_message.timestamp, - chat_message.salt, - &[], - Some(TextComponent::from(message.clone())), - pumpkin_protocol::VarInt(FilterType::PassThrough as i32), - 0.into(), - TextComponent::from(gameprofile.name.clone()), - None, - ), - ) */ /* server.broadcast_packet( self, CDisguisedChatMessage::new( diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index e4ac7464e..fc2c4738a 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -22,6 +22,7 @@ use pumpkin_protocol::{ }, BitSet, ClientPacket, Players, Sample, StatusResponse, VarInt, Version, CURRENT_MC_PROTOCOL, }; +use pumpkin_registry::Registry; use pumpkin_world::chunk::TestChunk; use rsa::{traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey}; use serde::{Deserialize, Serialize}; @@ -50,6 +51,9 @@ pub struct Server { /// Cache the Server brand buffer so we don't have to rebuild them every time a player joins pub cached_server_brand: Vec, + /// Cache the registry so we don't have to parse it every time a player joins + pub cached_registry: Vec, + pub current_clients: HashMap, Rc>>, // TODO: replace with HashMap @@ -89,6 +93,7 @@ impl Server { }; Self { + cached_registry: Registry::get_static(), // 0 is invalid entity_id: 2.into(), // world: World::load(""), @@ -123,7 +128,7 @@ impl Server { // todo: put this into the entitiy struct if client.is_player() { let id = client.player.as_ref().unwrap().entity_id(); - self.broadcast_packet(&mut client, CRemoveEntities::new(vec![id.into()])) + self.broadcast_packet(&mut client, CRemoveEntities::new(&[id.into()])) } } @@ -140,7 +145,7 @@ impl Server { client.send_packet(CLogin::new( entity_id, self.base_config.hardcore, - vec!["minecraft:overworld".into()], + &["minecraft:overworld"], self.base_config.max_players.into(), self.base_config.view_distance.into(), // TODO: view distance self.base_config.simulation_distance.into(), // TODO: sim view dinstance @@ -148,7 +153,7 @@ impl Server { false, false, 0.into(), - "minecraft:overworld".into(), + "minecraft:overworld", 0, // seed match self.base_config.default_gamemode { GameMode::Undefined => GameMode::Survival, From 65c2207c0ae76024866f754389afd8b36ef67771 Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Mon, 12 Aug 2024 15:09:34 +0200 Subject: [PATCH 12/17] Add Player Tab list --- .../src/client/play/c_player_remove.rs | 20 ++++++++++++ pumpkin-protocol/src/client/play/mod.rs | 2 ++ pumpkin-protocol/src/lib.rs | 1 + pumpkin-protocol/src/uuid.rs | 12 +++++++ pumpkin/src/client/mod.rs | 4 +-- pumpkin/src/server.rs | 31 ++++++++++++------- 6 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 pumpkin-protocol/src/client/play/c_player_remove.rs create mode 100644 pumpkin-protocol/src/uuid.rs diff --git a/pumpkin-protocol/src/client/play/c_player_remove.rs b/pumpkin-protocol/src/client/play/c_player_remove.rs new file mode 100644 index 000000000..7cd5db84e --- /dev/null +++ b/pumpkin-protocol/src/client/play/c_player_remove.rs @@ -0,0 +1,20 @@ +use pumpkin_macros::packet; +use serde::Serialize; + +use crate::{uuid::UUID, VarInt}; + +#[derive(Serialize, Clone)] +#[packet(0x3D)] +pub struct CRemovePlayerInfo<'a> { + players_count: VarInt, + players: &'a [UUID], +} + +impl<'a> CRemovePlayerInfo<'a> { + pub fn new(players_count: VarInt, players: &'a [UUID]) -> Self { + Self { + players_count, + players, + } + } +} diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 84b92e1d0..c7eba0bbb 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -12,6 +12,7 @@ mod c_play_disconnect; mod c_player_abilities; mod c_player_chat_message; mod c_player_info_update; +mod c_player_remove; mod c_remove_entities; mod c_set_held_item; mod c_set_title; @@ -38,6 +39,7 @@ pub use c_play_disconnect::*; pub use c_player_abilities::*; pub use c_player_chat_message::*; pub use c_player_info_update::*; +pub use c_player_remove::*; pub use c_remove_entities::*; pub use c_set_held_item::*; pub use c_set_title::*; diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index 2fe1b7cd0..70af5148f 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -8,6 +8,7 @@ use thiserror::Error; pub mod bytebuf; pub mod client; pub mod server; +pub mod uuid; pub mod packet_decoder; pub mod packet_encoder; diff --git a/pumpkin-protocol/src/uuid.rs b/pumpkin-protocol/src/uuid.rs new file mode 100644 index 000000000..49691becd --- /dev/null +++ b/pumpkin-protocol/src/uuid.rs @@ -0,0 +1,12 @@ +use serde::Serialize; + +pub struct UUID(pub uuid::Uuid); + +impl Serialize for UUID { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(self.0.as_bytes()) + } +} diff --git a/pumpkin/src/client/mod.rs b/pumpkin/src/client/mod.rs index ac355d672..9f60a8933 100644 --- a/pumpkin/src/client/mod.rs +++ b/pumpkin/src/client/mod.rs @@ -294,9 +294,7 @@ impl Client { } Ok(n) => { bytes_read += n; - if bytes_read == received_data.len() { - received_data.resize(received_data.len() + 1024, 0); - } + received_data.extend(&vec![0; n]); } // Would block "errors" are the OS's way of saying that the // connection is not actually ready to perform this I/O operation. diff --git a/pumpkin/src/server.rs b/pumpkin/src/server.rs index fc2c4738a..26ceddcdf 100644 --- a/pumpkin/src/server.rs +++ b/pumpkin/src/server.rs @@ -17,9 +17,10 @@ use pumpkin_protocol::{ config::CPluginMessage, play::{ CChunkDataUpdateLight, CGameEvent, CLogin, CPlayerAbilities, CPlayerInfoUpdate, - CRemoveEntities, CSpawnEntity, PlayerAction, + CRemoveEntities, CRemovePlayerInfo, CSpawnEntity, PlayerAction, }, }, + uuid::UUID, BitSet, ClientPacket, Players, Sample, StatusResponse, VarInt, Version, CURRENT_MC_PROTOCOL, }; use pumpkin_registry::Registry; @@ -128,6 +129,8 @@ impl Server { // todo: put this into the entitiy struct if client.is_player() { let id = client.player.as_ref().unwrap().entity_id(); + let uuid = client.gameprofile.as_ref().unwrap().id; + self.broadcast_packet(&mut client, CRemovePlayerInfo::new(1.into(), &[UUID(uuid)])); self.broadcast_packet(&mut client, CRemoveEntities::new(&[id.into()])) } } @@ -185,13 +188,16 @@ impl Server { self.broadcast_packet( client, CPlayerInfoUpdate::new( - 0x01, + 0x01 | 0x08, &[pumpkin_protocol::client::play::Player { uuid: gameprofile.id, - actions: vec![PlayerAction::AddPlayer { - name: gameprofile.name.clone(), - properties: gameprofile.properties.clone(), - }], + actions: vec![ + PlayerAction::AddPlayer { + name: gameprofile.name.clone(), + properties: gameprofile.properties.clone(), + }, + PlayerAction::UpdateListed { listed: true }, + ], }], ), ); @@ -204,14 +210,17 @@ impl Server { let gameprofile = client.gameprofile.as_ref().unwrap(); entries.push(pumpkin_protocol::client::play::Player { uuid: gameprofile.id, - actions: vec![PlayerAction::AddPlayer { - name: gameprofile.name.clone(), - properties: gameprofile.properties.clone(), - }], + actions: vec![ + PlayerAction::AddPlayer { + name: gameprofile.name.clone(), + properties: gameprofile.properties.clone(), + }, + PlayerAction::UpdateListed { listed: true }, + ], }) } } - client.send_packet(CPlayerInfoUpdate::new(0x01, &entries)); + client.send_packet(CPlayerInfoUpdate::new(0x01 | 0x08, &entries)); // Start waiting for level chunks client.send_packet(CGameEvent::new(13, 0.0)); From 5cf19a4e98d755b3c7eb7f618c61dbb6433529ca Mon Sep 17 00:00:00 2001 From: Snowiiii Date: Mon, 12 Aug 2024 15:46:24 +0200 Subject: [PATCH 13/17] Add ChatType --- Cargo.lock | 1 + .../src/client/config/c_plugin_message.rs | 9 +- .../src/client/login/c_encryption_request.rs | 12 +- .../src/client/play/c_player_chat_message.rs | 6 +- .../src/client/play/c_spawn_player.rs | 27 +---- pumpkin-protocol/src/lib.rs | 10 -- pumpkin-protocol/src/uuid.rs | 1 + pumpkin-registry/Cargo.toml | 2 + pumpkin-registry/src/chat_type.rs | 10 +- pumpkin-registry/src/lib.rs | 4 +- pumpkin-text/src/lib.rs | 107 ++++-------------- pumpkin-text/src/style.rs | 100 ++++++++++++++++ pumpkin/src/client/client_packet.rs | 4 +- pumpkin/src/client/player_packet.rs | 10 +- pumpkin/src/commands/pumpkin.rs | 2 +- pumpkin/src/server.rs | 4 +- 16 files changed, 158 insertions(+), 151 deletions(-) create mode 100644 pumpkin-text/src/style.rs diff --git a/Cargo.lock b/Cargo.lock index 2475436a4..540f83e59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,6 +1235,7 @@ dependencies = [ "fastnbt 2.5.0 (git+https://github.com/owengage/fastnbt.git)", "fastsnbt", "pumpkin-protocol", + "pumpkin-text", "serde", ] diff --git a/pumpkin-protocol/src/client/config/c_plugin_message.rs b/pumpkin-protocol/src/client/config/c_plugin_message.rs index 9eb3d6757..bdb7692b4 100644 --- a/pumpkin-protocol/src/client/config/c_plugin_message.rs +++ b/pumpkin-protocol/src/client/config/c_plugin_message.rs @@ -1,7 +1,9 @@ use pumpkin_macros::packet; +use serde::Serialize; use crate::{bytebuf::ByteBuffer, ClientPacket}; +#[derive(Serialize)] #[packet(0x01)] pub struct CPluginMessage<'a> { channel: &'a str, @@ -13,10 +15,3 @@ impl<'a> CPluginMessage<'a> { Self { channel, data } } } - -impl<'a> ClientPacket for CPluginMessage<'a> { - fn write(&self, bytebuf: &mut ByteBuffer) { - bytebuf.put_string(self.channel); - bytebuf.put_slice(self.data); - } -} diff --git a/pumpkin-protocol/src/client/login/c_encryption_request.rs b/pumpkin-protocol/src/client/login/c_encryption_request.rs index a7f0a7f01..1ce55c020 100644 --- a/pumpkin-protocol/src/client/login/c_encryption_request.rs +++ b/pumpkin-protocol/src/client/login/c_encryption_request.rs @@ -8,24 +8,24 @@ use crate::{RawBytes, VarInt}; pub struct CEncryptionRequest<'a> { server_id: &'a str, // 20 public_key_length: VarInt, - public_key: RawBytes<'a>, + public_key: &'a [u8], verify_token_length: VarInt, - verify_token: RawBytes<'a>, + verify_token: &'a [u8], should_authenticate: bool, } impl<'a> CEncryptionRequest<'a> { pub fn new( server_id: &'a str, - public_key: RawBytes<'a>, - verify_token: RawBytes<'a>, + public_key: &'a [u8], + verify_token: &'a [u8], should_authenticate: bool, ) -> Self { Self { server_id, - public_key_length: public_key.0.len().into(), + public_key_length: public_key.len().into(), public_key, - verify_token_length: verify_token.0.len().into(), + verify_token_length: verify_token.len().into(), verify_token, should_authenticate, } diff --git a/pumpkin-protocol/src/client/play/c_player_chat_message.rs b/pumpkin-protocol/src/client/play/c_player_chat_message.rs index e207575ba..22da96a0c 100644 --- a/pumpkin-protocol/src/client/play/c_player_chat_message.rs +++ b/pumpkin-protocol/src/client/play/c_player_chat_message.rs @@ -3,12 +3,12 @@ use pumpkin_macros::packet; use pumpkin_text::TextComponent; use serde::Serialize; -use crate::{BitSet, VarInt}; +use crate::{uuid::UUID, BitSet, VarInt}; #[derive(Serialize, Clone)] #[packet(0x39)] pub struct CPlayerChatMessage<'a> { - sender: uuid::Uuid, + sender: UUID, index: VarInt, message_signature: Option<&'a [u8]>, message: String, @@ -29,7 +29,7 @@ pub struct CPlayerChatMessage<'a> { impl<'a> CPlayerChatMessage<'a> { #[allow(clippy::too_many_arguments)] pub fn new( - sender: uuid::Uuid, + sender: UUID, index: VarInt, message_signature: Option<&'a [u8]>, message: String, diff --git a/pumpkin-protocol/src/client/play/c_spawn_player.rs b/pumpkin-protocol/src/client/play/c_spawn_player.rs index 1efec8c5a..295860788 100644 --- a/pumpkin-protocol/src/client/play/c_spawn_player.rs +++ b/pumpkin-protocol/src/client/play/c_spawn_player.rs @@ -1,12 +1,13 @@ use pumpkin_macros::packet; +use serde::Serialize; -use crate::{ClientPacket, VarInt}; +use crate::{uuid::UUID, VarInt}; -#[derive(Clone)] +#[derive(Serialize, Clone)] #[packet(0x01)] pub struct CSpawnEntity { entity_id: VarInt, - entity_uuid: uuid::Uuid, + entity_uuid: UUID, typ: VarInt, x: f64, y: f64, @@ -24,7 +25,7 @@ impl CSpawnEntity { #[allow(clippy::too_many_arguments)] pub fn new( entity_id: VarInt, - entity_uuid: uuid::Uuid, + entity_uuid: UUID, typ: VarInt, x: f64, y: f64, @@ -54,21 +55,3 @@ impl CSpawnEntity { } } } - -impl ClientPacket for CSpawnEntity { - fn write(&self, bytebuf: &mut crate::bytebuf::ByteBuffer) { - bytebuf.put_var_int(&self.entity_id); - bytebuf.put_uuid(self.entity_uuid); - bytebuf.put_var_int(&self.typ); - bytebuf.put_f64(self.x); - bytebuf.put_f64(self.y); - bytebuf.put_f64(self.z); - bytebuf.put_u8(self.pitch); - bytebuf.put_u8(self.yaw); - bytebuf.put_u8(self.head_yaw); - bytebuf.put_var_int(&self.data); - bytebuf.put_i16(self.velocity_x); - bytebuf.put_i16(self.velocity_y); - bytebuf.put_i16(self.velocity_z); - } -} diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index 70af5148f..f64eca8f6 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -91,16 +91,6 @@ impl VarInt { } } -pub struct RawBytes<'a>(pub &'a [u8]); -impl<'a> Serialize for RawBytes<'a> { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_bytes(self.0) - } -} - impl From for VarInt { fn from(value: i32) -> Self { VarInt(value) diff --git a/pumpkin-protocol/src/uuid.rs b/pumpkin-protocol/src/uuid.rs index 49691becd..ab70ef83a 100644 --- a/pumpkin-protocol/src/uuid.rs +++ b/pumpkin-protocol/src/uuid.rs @@ -1,5 +1,6 @@ use serde::Serialize; +#[derive(Clone)] pub struct UUID(pub uuid::Uuid); impl Serialize for UUID { diff --git a/pumpkin-registry/Cargo.toml b/pumpkin-registry/Cargo.toml index e557bc927..3087a0ec3 100644 --- a/pumpkin-registry/Cargo.toml +++ b/pumpkin-registry/Cargo.toml @@ -5,6 +5,8 @@ edition.workspace = true [dependencies] pumpkin-protocol = { path = "../pumpkin-protocol"} +pumpkin-text = { path = "../pumpkin-text"} + # nbt fastnbt = { git = "https://github.com/owengage/fastnbt.git" } fastsnbt = "0.2" diff --git a/pumpkin-registry/src/chat_type.rs b/pumpkin-registry/src/chat_type.rs index 0614c2b4b..45f324c26 100644 --- a/pumpkin-registry/src/chat_type.rs +++ b/pumpkin-registry/src/chat_type.rs @@ -1,3 +1,4 @@ +use pumpkin_text::style::Style; use serde::Serialize; #[derive(Debug, Clone, Serialize)] @@ -9,9 +10,8 @@ pub struct ChatType { #[derive(Debug, Clone, Serialize)] pub struct Decoration { translation_key: String, - style: u8, - // TODO - // style: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + style: Option