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)) } }