From 620aac3d8dc14d45346434492a402b0192d0abf6 Mon Sep 17 00:00:00 2001 From: Vladislav Golub Date: Mon, 23 Dec 2019 22:36:03 +0300 Subject: [PATCH] Add chat --- Cargo.toml | 3 - src/chat.rs | 630 +++++++++++++++++++++++++++ src/lib.rs | 2 + test/chat/click_change_page.json | 1 + test/chat/click_open_url.json | 1 + test/chat/click_run_command.json | 1 + test/chat/click_suggest_command.json | 1 + test/chat/hover_show_entity.json | 1 + test/chat/hover_show_item.json | 1 + test/chat/hover_show_text.json | 1 + test/chat/keybind_jump.json | 1 + test/chat/text_hello_world.json | 1 + test/chat/translate_opped_steve.json | 1 + 13 files changed, 642 insertions(+), 3 deletions(-) create mode 100644 src/chat.rs create mode 100644 test/chat/click_change_page.json create mode 100644 test/chat/click_open_url.json create mode 100644 test/chat/click_run_command.json create mode 100644 test/chat/click_suggest_command.json create mode 100644 test/chat/hover_show_entity.json create mode 100644 test/chat/hover_show_item.json create mode 100644 test/chat/hover_show_text.json create mode 100644 test/chat/keybind_jump.json create mode 100644 test/chat/text_hello_world.json create mode 100644 test/chat/translate_opped_steve.json diff --git a/Cargo.toml b/Cargo.toml index e55fd89..394058f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,6 @@ repository = "https://github.com/eihwaz/minecraft-protocol" keywords = ["minecraft", "protocol", "packet", "io"] readme = "README.md" -[lib] -name = "mcpl" - [dependencies] byteorder = "1" serde = { version = "1.0", features = ["derive"] } diff --git a/src/chat.rs b/src/chat.rs new file mode 100644 index 0000000..415ec0c --- /dev/null +++ b/src/chat.rs @@ -0,0 +1,630 @@ +//! Minecraft chat are represented as json object. It's used in different packets. +//! Information about format can be found at https://wiki.vg/Chat. +//! +//! # Example +//! +//! ## Serialize +//! +//! ``` +//! use minecraft_protocol::chat::{Payload, Color, MessageBuilder}; +//! +//! let message = MessageBuilder::builder(Payload::text("Hello")) +//! .color(Color::Yellow) +//! .bold(true) +//! .then(Payload::text("world")) +//! .color(Color::Green) +//! .bold(true) +//! .italic(true) +//! .then(Payload::text("!")) +//! .color(Color::Blue) +//! .build(); +//! +//! println!("{}", message.to_json().unwrap()); +//! ``` +//! +//! ## Deserialize +//! +//! ``` +//! use minecraft_protocol::chat::{MessageBuilder, Color, Payload, Message}; +//! +//! let json = r#" +//! { +//! "bold":true, +//! "color":"yellow", +//! "text":"Hello", +//! "extra":[ +//! { +//! "bold":true, +//! "italic":true, +//! "color":"green", +//! "text":"world" +//! }, +//! { +//! "color":"blue", +//! "text":"!" +//! } +//! ] +//! } +//! "#; +//! +//! let expected_message = MessageBuilder::builder(Payload::text("Hello")) +//! .color(Color::Yellow) +//! .bold(true) +//! .then(Payload::text("world")) +//! .color(Color::Green) +//! .bold(true) +//! .italic(true) +//! .then(Payload::text("!")) +//! .color(Color::Blue) +//! .build(); +//! +//! assert_eq!(expected_message, Message::from_json(json).unwrap()); +//! ``` + +use serde::{Deserialize, Serialize}; +use serde_json::Error; + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Color { + Black, + DarkBlue, + DarkGreen, + DarkAqua, + DarkRed, + DarkPurple, + Gold, + Gray, + DarkGray, + Blue, + Green, + Aqua, + Red, + LightPurple, + Yellow, + White, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ClickAction { + OpenUrl, + RunCommand, + SuggestCommand, + ChangePage, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct ClickEvent { + pub action: ClickAction, + pub value: String, +} + +impl ClickEvent { + pub fn new(action: ClickAction, value: &str) -> Self { + ClickEvent { + action, + value: value.to_owned(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum HoverAction { + ShowText, + ShowItem, + ShowEntity, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct HoverEvent { + pub action: HoverAction, + pub value: String, +} + +impl HoverEvent { + pub fn new(action: HoverAction, value: &str) -> Self { + HoverEvent { + action, + value: value.to_owned(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Payload { + Text { + text: String, + }, + Translation { + translate: String, + with: Vec, + }, + Keybind { + keybind: String, + }, + Score { + name: String, + objective: String, + value: String, + }, + Selector { + selector: String, + }, +} + +impl Payload { + pub fn text(text: &str) -> Self { + Payload::Text { + text: text.to_owned(), + } + } + + pub fn translation(translate: &str, with: Vec) -> Self { + Payload::Translation { + translate: translate.to_owned(), + with, + } + } + + pub fn keybind(keybind: &str) -> Self { + Payload::Keybind { + keybind: keybind.to_owned(), + } + } + + pub fn score(name: &str, objective: &str, value: &str) -> Self { + Payload::Score { + name: name.to_owned(), + objective: objective.to_owned(), + value: value.to_owned(), + } + } + + pub fn selector(selector: &str) -> Self { + Payload::Selector { + selector: selector.to_owned(), + } + } +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Message { + #[serde(skip_serializing_if = "Option::is_none")] + pub bold: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub italic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub underlined: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub strikethrough: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub obfuscated: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub insertion: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub click_event: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hover_event: Option, + #[serde(flatten)] + pub payload: Payload, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub extra: Vec, +} + +impl Message { + pub fn new(payload: Payload) -> Self { + Message { + bold: None, + italic: None, + underlined: None, + strikethrough: None, + obfuscated: None, + color: None, + insertion: None, + click_event: None, + hover_event: None, + payload, + extra: vec![], + } + } + + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + } + + pub fn to_json(&self) -> Result { + serde_json::to_string(&self) + } +} + +pub struct MessageBuilder { + current: Message, + root: Option, +} + +macro_rules! create_builder_style_method ( + ($style: ident) => ( + pub fn $style(mut self, value: bool) -> Self { + self.current.$style = Some(value); + self + } + ); +); + +macro_rules! create_builder_click_event_method ( + ($method_name: ident, $event: ident) => ( + pub fn $method_name(mut self, value: &str) -> Self { + let click_event = ClickEvent::new(ClickAction::$event, value); + self.current.click_event = Some(click_event); + self + } + ); +); + +macro_rules! create_builder_hover_event_method ( + ($method_name: ident, $event: ident) => ( + pub fn $method_name(mut self, value: &str) -> Self { + let hover_event = HoverEvent::new(HoverAction::$event, value); + self.current.hover_event = Some(hover_event); + self + } + ); +); + +impl MessageBuilder { + pub fn builder(payload: Payload) -> Self { + let current = Message::new(payload); + + MessageBuilder { + current, + root: None, + } + } + + pub fn color(mut self, color: Color) -> Self { + self.current.color = Some(color); + self + } + + pub fn insertion(mut self, insertion: &str) -> Self { + self.current.insertion = Some(insertion.to_owned()); + self + } + + create_builder_style_method!(bold); + create_builder_style_method!(italic); + create_builder_style_method!(underlined); + create_builder_style_method!(strikethrough); + create_builder_style_method!(obfuscated); + + create_builder_click_event_method!(click_open_url, OpenUrl); + create_builder_click_event_method!(click_run_command, RunCommand); + create_builder_click_event_method!(click_suggest_command, SuggestCommand); + create_builder_click_event_method!(click_change_page, ChangePage); + + create_builder_hover_event_method!(hover_show_text, ShowText); + create_builder_hover_event_method!(hover_show_item, ShowItem); + create_builder_hover_event_method!(hover_show_entity, ShowEntity); + + pub fn then(mut self, payload: Payload) -> Self { + match self.root.as_mut() { + Some(root) => { + root.extra.push(self.current); + } + None => { + self.root = Some(self.current); + } + } + + self.current = Message::new(payload); + self + } + + pub fn build(self) -> Message { + match self.root { + Some(mut root) => { + root.extra.push(self.current); + root + } + None => self.current, + } + } +} + +#[test] +fn test_serialize_text_hello_world() { + let message = MessageBuilder::builder(Payload::text("Hello")) + .color(Color::Yellow) + .bold(true) + .then(Payload::text("world")) + .color(Color::Green) + .bold(true) + .italic(true) + .then(Payload::text("!")) + .color(Color::Blue) + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/text_hello_world.json") + ); +} + +#[test] +fn test_deserialize_text_hello_world() { + let expected_message = MessageBuilder::builder(Payload::text("Hello")) + .color(Color::Yellow) + .bold(true) + .then(Payload::text("world")) + .color(Color::Green) + .bold(true) + .italic(true) + .then(Payload::text("!")) + .color(Color::Blue) + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/text_hello_world.json")).unwrap() + ); +} + +#[test] +fn test_serialize_translate_opped_steve() { + let with = vec![Message::new(Payload::text("Steve"))]; + let message = Message::new(Payload::translation("Opped %s", with)); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/translate_opped_steve.json") + ); +} + +#[test] +fn test_deserialize_translate_opped_steve() { + let with = vec![Message::new(Payload::text("Steve"))]; + let expected_message = Message::new(Payload::translation("Opped %s", with)); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/translate_opped_steve.json")).unwrap() + ); +} + +#[test] +fn test_serialize_keybind_jump() { + let message = MessageBuilder::builder(Payload::text("Press \"")) + .color(Color::Yellow) + .bold(true) + .then(Payload::keybind("key.jump")) + .color(Color::Blue) + .bold(false) + .underlined(true) + .then(Payload::text("\" to jump!")) + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/keybind_jump.json") + ); +} + +#[test] +fn test_deserialize_keybind_jump() { + let expected_message = MessageBuilder::builder(Payload::text("Press \"")) + .color(Color::Yellow) + .bold(true) + .then(Payload::keybind("key.jump")) + .color(Color::Blue) + .bold(false) + .underlined(true) + .then(Payload::text("\" to jump!")) + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/keybind_jump.json")).unwrap() + ); +} + +#[test] +fn test_serialize_click_open_url() { + let message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::Yellow) + .bold(true) + .click_open_url("http://minecraft.net") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/click_open_url.json") + ); +} + +#[test] +fn test_deserialize_click_open_url() { + let expected_message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::Yellow) + .bold(true) + .click_open_url("http://minecraft.net") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/click_open_url.json")).unwrap() + ); +} + +#[test] +fn test_serialize_click_run_command() { + let message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::LightPurple) + .italic(true) + .click_run_command("/help") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/click_run_command.json") + ); +} + +#[test] +fn test_deserialize_click_run_command() { + let expected_message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::LightPurple) + .italic(true) + .click_run_command("/help") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/click_run_command.json")).unwrap() + ); +} + +#[test] +fn test_serialize_click_suggest_command() { + let message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::Blue) + .obfuscated(true) + .click_suggest_command("/help") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/click_suggest_command.json") + ); +} + +#[test] +fn test_deserialize_click_suggest_command() { + let expected_message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::Blue) + .obfuscated(true) + .click_suggest_command("/help") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/click_suggest_command.json")).unwrap() + ); +} + +#[test] +fn test_serialize_click_change_page() { + let message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::DarkGray) + .underlined(true) + .click_change_page("2") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/click_change_page.json") + ); +} + +#[test] +fn test_deserialize_click_change_page() { + let expected_message = MessageBuilder::builder(Payload::text("click me")) + .color(Color::DarkGray) + .underlined(true) + .click_change_page("2") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/click_change_page.json")).unwrap() + ); +} + +#[test] +fn test_serialize_hover_show_text() { + let message = MessageBuilder::builder(Payload::text("hover at me")) + .color(Color::DarkPurple) + .bold(true) + .hover_show_text("Herobrine behind you!") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/hover_show_text.json") + ); +} + +#[test] +fn test_deserialize_hover_show_text() { + let expected_message = MessageBuilder::builder(Payload::text("hover at me")) + .color(Color::DarkPurple) + .bold(true) + .hover_show_text("Herobrine behind you!") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/hover_show_text.json")).unwrap() + ); +} + +#[test] +fn test_serialize_hover_show_item() { + let message = MessageBuilder::builder(Payload::text("hover at me")) + .color(Color::DarkRed) + .italic(true) + .hover_show_item("{\"id\":\"stone\",\"Count\":1}") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/hover_show_item.json") + ); +} + +#[test] +fn test_deserialize_hover_show_item() { + let expected_message = MessageBuilder::builder(Payload::text("hover at me")) + .color(Color::DarkRed) + .italic(true) + .hover_show_item("{\"id\":\"stone\",\"Count\":1}") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/hover_show_item.json")).unwrap() + ); +} + +#[test] +fn test_serialize_hover_show_entity() { + let message = MessageBuilder::builder(Payload::text("hover at me")) + .color(Color::DarkAqua) + .obfuscated(true) + .hover_show_entity("{\"id\":\"7e4a61cc-83fa-4441-a299-bf69786e610a\",\"type\":\"minecraft:zombie\",\"name\":\"Zombie}\"") + .build(); + + assert_eq!( + message.to_json().unwrap(), + include_str!("../test/chat/hover_show_entity.json") + ); +} + +#[test] +fn test_deserialize_hover_show_entity() { + let expected_message = MessageBuilder::builder(Payload::text("hover at me")) + .color(Color::DarkAqua) + .obfuscated(true) + .hover_show_entity("{\"id\":\"7e4a61cc-83fa-4441-a299-bf69786e610a\",\"type\":\"minecraft:zombie\",\"name\":\"Zombie}\"") + .build(); + + assert_eq!( + expected_message, + Message::from_json(include_str!("../test/chat/hover_show_entity.json")).unwrap() + ); +} diff --git a/src/lib.rs b/src/lib.rs index 4820de9..b9f2810 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ use serde_json::error::Error as JsonError; use std::io; use std::io::{Read, Write}; use std::string::FromUtf8Error; + +pub mod chat; pub mod status; /// Current supported protocol version. diff --git a/test/chat/click_change_page.json b/test/chat/click_change_page.json new file mode 100644 index 0000000..5713ef1 --- /dev/null +++ b/test/chat/click_change_page.json @@ -0,0 +1 @@ +{"underlined":true,"color":"dark_gray","clickEvent":{"action":"change_page","value":"2"},"text":"click me"} \ No newline at end of file diff --git a/test/chat/click_open_url.json b/test/chat/click_open_url.json new file mode 100644 index 0000000..e6a2e34 --- /dev/null +++ b/test/chat/click_open_url.json @@ -0,0 +1 @@ +{"bold":true,"color":"yellow","clickEvent":{"action":"open_url","value":"http://minecraft.net"},"text":"click me"} \ No newline at end of file diff --git a/test/chat/click_run_command.json b/test/chat/click_run_command.json new file mode 100644 index 0000000..b54faa4 --- /dev/null +++ b/test/chat/click_run_command.json @@ -0,0 +1 @@ +{"italic":true,"color":"light_purple","clickEvent":{"action":"run_command","value":"/help"},"text":"click me"} \ No newline at end of file diff --git a/test/chat/click_suggest_command.json b/test/chat/click_suggest_command.json new file mode 100644 index 0000000..4060914 --- /dev/null +++ b/test/chat/click_suggest_command.json @@ -0,0 +1 @@ +{"obfuscated":true,"color":"blue","clickEvent":{"action":"suggest_command","value":"/help"},"text":"click me"} \ No newline at end of file diff --git a/test/chat/hover_show_entity.json b/test/chat/hover_show_entity.json new file mode 100644 index 0000000..d6dedff --- /dev/null +++ b/test/chat/hover_show_entity.json @@ -0,0 +1 @@ +{"obfuscated":true,"color":"dark_aqua","hoverEvent":{"action":"show_entity","value":"{\"id\":\"7e4a61cc-83fa-4441-a299-bf69786e610a\",\"type\":\"minecraft:zombie\",\"name\":\"Zombie}\""},"text":"hover at me"} \ No newline at end of file diff --git a/test/chat/hover_show_item.json b/test/chat/hover_show_item.json new file mode 100644 index 0000000..45585a1 --- /dev/null +++ b/test/chat/hover_show_item.json @@ -0,0 +1 @@ +{"italic":true,"color":"dark_red","hoverEvent":{"action":"show_item","value":"{\"id\":\"stone\",\"Count\":1}"},"text":"hover at me"} \ No newline at end of file diff --git a/test/chat/hover_show_text.json b/test/chat/hover_show_text.json new file mode 100644 index 0000000..6ab5a5d --- /dev/null +++ b/test/chat/hover_show_text.json @@ -0,0 +1 @@ +{"bold":true,"color":"dark_purple","hoverEvent":{"action":"show_text","value":"Herobrine behind you!"},"text":"hover at me"} \ No newline at end of file diff --git a/test/chat/keybind_jump.json b/test/chat/keybind_jump.json new file mode 100644 index 0000000..7d37f1a --- /dev/null +++ b/test/chat/keybind_jump.json @@ -0,0 +1 @@ +{"bold":true,"color":"yellow","text":"Press \"","extra":[{"bold":false,"underlined":true,"color":"blue","keybind":"key.jump"},{"text":"\" to jump!"}]} \ No newline at end of file diff --git a/test/chat/text_hello_world.json b/test/chat/text_hello_world.json new file mode 100644 index 0000000..9687c0b --- /dev/null +++ b/test/chat/text_hello_world.json @@ -0,0 +1 @@ +{"bold":true,"color":"yellow","text":"Hello","extra":[{"bold":true,"italic":true,"color":"green","text":"world"},{"color":"blue","text":"!"}]} \ No newline at end of file diff --git a/test/chat/translate_opped_steve.json b/test/chat/translate_opped_steve.json new file mode 100644 index 0000000..1bebc04 --- /dev/null +++ b/test/chat/translate_opped_steve.json @@ -0,0 +1 @@ +{"translate":"Opped %s","with":[{"text":"Steve"}]} \ No newline at end of file