aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2021-12-11 15:17:42 -0600
committermat <github@matdoes.dev>2021-12-11 15:17:42 -0600
commitba911a8a207eb47df7a055410570767b2e33c2ae (patch)
tree4a53d384f08b7272ba287bdb326f3d7fcb09f289
parent6026c74430f311c9217b77e7ac07d183efde5bce (diff)
downloadazalea-drasl-ba911a8a207eb47df7a055410570767b2e33c2ae.tar.xz
correct minecraft-chat :tada:
-rw-r--r--Cargo.lock16
-rw-r--r--bot/src/main.rs1
-rw-r--r--minecraft-chat/Cargo.toml1
-rw-r--r--minecraft-chat/src/component.rs234
-rw-r--r--minecraft-chat/src/style.rs162
-rw-r--r--minecraft-chat/src/text_component.rs91
-rw-r--r--minecraft-chat/tests/integration_test.rs13
-rw-r--r--minecraft-protocol/Cargo.toml7
-rw-r--r--minecraft-protocol/src/connection.rs6
-rw-r--r--minecraft-protocol/src/lib.rs2
-rw-r--r--minecraft-protocol/src/mc_buf.rs8
-rw-r--r--minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs45
-rw-r--r--minecraft-protocol/src/resolver.rs3
-rw-r--r--minecraft-protocol/src/server_status_pinger.rs14
14 files changed, 348 insertions, 255 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f82d7659..3847548f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -289,6 +289,7 @@ name = "minecraft-chat"
version = "0.1.0"
dependencies = [
"lazy_static",
+ "serde",
"serde_json",
]
@@ -308,6 +309,7 @@ dependencies = [
"byteorder",
"bytes",
"minecraft-chat",
+ "serde",
"serde_json",
"thiserror",
"tokio",
@@ -505,6 +507,20 @@ name = "serde"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
name = "serde_json"
diff --git a/bot/src/main.rs b/bot/src/main.rs
index 68fd41bf..47af37b4 100644
--- a/bot/src/main.rs
+++ b/bot/src/main.rs
@@ -1,4 +1,3 @@
-use minecraft_client;
use minecraft_protocol::ServerAddress;
use tokio::runtime::Runtime;
diff --git a/minecraft-chat/Cargo.toml b/minecraft-chat/Cargo.toml
index 81f676af..aa803864 100644
--- a/minecraft-chat/Cargo.toml
+++ b/minecraft-chat/Cargo.toml
@@ -7,4 +7,5 @@ version = "0.1.0"
[dependencies]
lazy_static = "1.4.0"
+serde = "^1.0.130"
serde_json = "^1.0.72"
diff --git a/minecraft-chat/src/component.rs b/minecraft-chat/src/component.rs
index 300a1228..2ff6111a 100644
--- a/minecraft-chat/src/component.rs
+++ b/minecraft-chat/src/component.rs
@@ -1,8 +1,11 @@
-use serde_json;
+use serde::{
+ de::{self, Error},
+ Deserialize, Deserializer,
+};
use crate::{
base_component::BaseComponent,
- style::Style,
+ style::{ChatFormatting, Style},
text_component::TextComponent,
translatable_component::{StringOrComponent, TranslatableComponent},
};
@@ -13,9 +16,105 @@ pub enum Component {
Translatable(TranslatableComponent),
}
+lazy_static! {
+ pub static ref DEFAULT_STYLE: Style = Style {
+ color: Some(ChatFormatting::WHITE.try_into().unwrap()),
+ ..Style::default()
+ };
+}
+
/// A chat component
impl Component {
- pub fn new(json: &serde_json::Value) -> Result<Component, String> {
+ // TODO: is it possible to use a macro so this doesn't have to be duplicated?
+
+ pub fn get_base_mut(&mut self) -> &mut BaseComponent {
+ match self {
+ Self::Text(c) => &mut c.base,
+ Self::Translatable(c) => &mut c.base,
+ }
+ }
+
+ pub fn get_base(&self) -> &BaseComponent {
+ match self {
+ Self::Text(c) => &c.base,
+ Self::Translatable(c) => &c.base,
+ }
+ }
+
+ /// Add a component as a sibling of this one
+ fn append(&mut self, sibling: Component) {
+ self.get_base_mut().siblings.push(sibling);
+ }
+
+ /// Get the "separator" component from the json
+ fn parse_separator(json: &serde_json::Value) -> Result<Option<Component>, serde_json::Error> {
+ if json.get("separator").is_some() {
+ return Ok(Some(Component::deserialize(
+ json.get("separator").unwrap(),
+ )?));
+ }
+ Ok(None)
+ }
+
+ /// Convert this component into an ansi string
+ pub fn to_ansi(&self, default_style: Option<&Style>) -> String {
+ // default the default_style to white if it's not set
+ let default_style: &Style = default_style.unwrap_or_else(|| &DEFAULT_STYLE);
+
+ // this contains the final string will all the ansi escape codes
+ let mut built_string = String::new();
+ // this style will update as we visit components
+ let mut running_style = Style::default();
+
+ for component in self.clone().into_iter() {
+ let component_text = match &component {
+ Self::Text(c) => &c.text,
+ Self::Translatable(c) => &c.key,
+ };
+ let component_style = &component.get_base().style;
+
+ let ansi_text = running_style.compare_ansi(component_style, default_style);
+ built_string.push_str(&ansi_text);
+ built_string.push_str(component_text);
+
+ running_style.apply(component_style);
+ }
+
+ if !running_style.is_empty() {
+ built_string.push_str("\u{1b}[m");
+ }
+
+ built_string
+ }
+}
+
+impl IntoIterator for Component {
+ /// Recursively call the function for every component in this component
+ fn into_iter(self) -> Self::IntoIter {
+ let base = self.get_base();
+ let siblings = base.siblings.clone();
+ let mut v: Vec<Component> = Vec::with_capacity(siblings.len() + 1);
+ v.push(self);
+ for sibling in siblings {
+ v.extend(sibling.into_iter());
+ }
+
+ v.into_iter()
+ }
+
+ type Item = Component;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+}
+
+impl<'de> Deserialize<'de> for Component {
+ fn deserialize<D>(de: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ println!("deserializing component");
+ let json: serde_json::Value = serde::Deserialize::deserialize(de)?;
+ println!("made json");
+
// we create a component that we might add siblings to
let mut component: Component;
@@ -38,7 +137,7 @@ impl Component {
for i in 0..with.len() {
// if it's a string component with no styling and no siblings, just add a string to with_array
// otherwise add the component to the array
- let c = Component::new(&with[i])?;
+ let c = Component::deserialize(&with[i]).map_err(de::Error::custom)?;
if let Component::Text(text_component) = c {
if text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
@@ -47,7 +146,9 @@ impl Component {
break;
}
}
- with_array.push(StringOrComponent::Component(Component::new(&with[i])?));
+ with_array.push(StringOrComponent::Component(
+ Component::deserialize(&with[i]).map_err(de::Error::custom)?,
+ ));
}
component =
Component::Translatable(TranslatableComponent::new(translate, with_array));
@@ -62,33 +163,41 @@ impl Component {
// if (!object.has("name") || !object.has("objective")) throw new JsonParseException("A score component needs a least a name and an objective");
// ScoreComponent scoreComponent = new ScoreComponent(GsonHelper.getAsString((JsonObject)object, "name"), GsonHelper.getAsString((JsonObject)object, "objective"));
if score_json.get("name").is_none() || score_json.get("objective").is_none() {
- return Err(
- "A score component needs at least a name and an objective".to_string()
- );
+ return Err(de::Error::missing_field(
+ "A score component needs at least a name and an objective",
+ ));
}
// TODO
- return Err("score text components aren't yet supported".to_string());
+ return Err(de::Error::custom(
+ "score text components aren't yet supported",
+ ));
// component = ScoreComponent
} else if json.get("selector").is_some() {
// } else if (jsonObject.has("selector")) {
// object = this.parseSeparator(type, jsonDeserializationContext, jsonObject);
// SelectorComponent selectorComponent = new SelectorComponent(GsonHelper.getAsString(jsonObject, "selector"), (Optional<Component>)object);
- return Err("selector text components aren't yet supported".to_string());
+ return Err(de::Error::custom(
+ "selector text components aren't yet supported",
+ ));
// } else if (jsonObject.has("keybind")) {
// KeybindComponent keybindComponent = new KeybindComponent(GsonHelper.getAsString(jsonObject, "keybind"));
} else if json.get("keybind").is_some() {
- return Err("keybind text components aren't yet supported".to_string());
+ return Err(de::Error::custom(
+ "keybind text components aren't yet supported",
+ ));
} else {
// } else {
// if (!jsonObject.has("nbt")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component");
if json.get("nbt").is_none() {
- return Err(format!("Don't know how to turn {} into a Component", json));
+ return Err(de::Error::custom(
+ format!("Don't know how to turn {} into a Component", json).as_str(),
+ ));
}
// object = GsonHelper.getAsString(jsonObject, "nbt");
let _nbt = json.get("nbt").unwrap().to_string();
// Optional<Component> optional = this.parseSeparator(type, jsonDeserializationContext, jsonObject);
- let _separator = Component::parse_separator(json)?;
+ let _separator = Component::parse_separator(&json).map_err(de::Error::custom)?;
let _interpret = match json.get("interpret") {
Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
@@ -97,7 +206,9 @@ impl Component {
// boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false);
// if (jsonObject.has("block")) {
if json.get("block").is_some() {}
- return Err("nbt text components aren't yet supported".to_string());
+ return Err(de::Error::custom(
+ "nbt text components aren't yet supported",
+ ));
// NbtComponent.BlockNbtComponent blockNbtComponent = new NbtComponent.BlockNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "block"), optional);
// } else if (jsonObject.has("entity")) {
// NbtComponent.EntityNbtComponent entityNbtComponent = new NbtComponent.EntityNbtComponent((String)object, bl, GsonHelper.getAsString(jsonObject, "entity"), optional);
@@ -120,106 +231,37 @@ impl Component {
if json.get("extra").is_some() {
let extra = match json.get("extra").unwrap().as_array() {
Some(r) => r,
- None => return Err("Extra isn't an array".to_string()),
+ None => return Err(de::Error::custom("Extra isn't an array")),
};
if extra.is_empty() {
- return Err("Unexpected empty array of components".to_string());
+ return Err(de::Error::custom("Unexpected empty array of components"));
}
for extra_component in extra {
- component.append(Component::new(extra_component)?);
+ let sibling =
+ Component::deserialize(extra_component).map_err(de::Error::custom)?;
+ component.append(sibling);
}
}
- let style = Style::deserialize(json);
+ let style = Style::deserialize(&json);
component.get_base_mut().style = style;
return Ok(component);
}
// ok so it's not an object, if it's an array deserialize every item
else if !json.is_array() {
- return Err(format!("Don't know how to turn {} into a Component", json));
+ return Err(de::Error::custom(
+ format!("Don't know how to turn {} into a Component", json).as_str(),
+ ));
}
let json_array = json.as_array().unwrap();
// the first item in the array is the one that we're gonna return, the others are siblings
- let mut component = Component::new(&json_array[0])?;
+ let mut component = Component::deserialize(&json_array[0]).map_err(de::Error::custom)?;
for i in 1..json_array.len() {
- component.append(Component::new(json_array.get(i).unwrap())?);
+ component.append(
+ Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?,
+ );
}
Ok(component)
}
-
- // TODO: is it possible to use a macro so this doesn't have to be duplicated?
-
- pub fn get_base_mut(&mut self) -> &mut BaseComponent {
- match self {
- Self::Text(c) => &mut c.base,
- Self::Translatable(c) => &mut c.base,
- }
- }
-
- pub fn get_base(&self) -> &BaseComponent {
- match self {
- Self::Text(c) => &c.base,
- Self::Translatable(c) => &c.base,
- }
- }
-
- /// Add a component as a sibling of this one
- fn append(&mut self, sibling: Component) {
- self.get_base_mut().siblings.push(sibling);
- }
-
- /// Get the "separator" component from the json
- fn parse_separator(json: &serde_json::Value) -> Result<Option<Component>, String> {
- if json.get("separator").is_some() {
- return Ok(Some(Component::new(json.get("separator").unwrap())?));
- }
- Ok(None)
- }
-
- /// Convert this component into an ansi string
- pub fn to_ansi(&self) -> String {
- // this contains the final string will all the ansi escape codes
- let mut built_string = String::new();
- // this style will update as we visit components
- let mut running_style = Style::default();
-
- for component in self.clone().into_iter() {
- let component_text = match &component {
- Self::Text(c) => &c.text,
- Self::Translatable(c) => &c.key,
- };
- let component_style = &component.get_base().style;
-
- let ansi_text = running_style.compare_ansi(component_style);
- built_string.push_str(&ansi_text);
- built_string.push_str(component_text);
-
- running_style.apply(component_style);
- }
-
- if !running_style.is_empty() {
- built_string.push_str("\u{1b}[m");
- }
-
- built_string
- }
-}
-
-impl IntoIterator for Component {
- /// Recursively call the function for every component in this component
- fn into_iter(self) -> Self::IntoIter {
- let base = self.get_base();
- let siblings = base.siblings.clone();
- let mut v: Vec<Component> = Vec::with_capacity(siblings.len() + 1);
- v.push(self);
- for sibling in siblings {
- v.extend(sibling.into_iter());
- }
-
- v.into_iter()
- }
-
- type Item = Component;
- type IntoIter = std::vec::IntoIter<Self::Item>;
}
diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs
index 14fabec5..7b333e5f 100644
--- a/minecraft-chat/src/style.rs
+++ b/minecraft-chat/src/style.rs
@@ -188,29 +188,21 @@ impl TextColor {
}
}
+// from ChatFormatting to TextColor
+impl TryFrom<ChatFormatting<'_>> for TextColor {
+ type Error = String;
+
+ fn try_from(formatter: ChatFormatting<'_>) -> Result<Self, Self::Error> {
+ if formatter.is_format {
+ return Err(format!("{} is not a color", formatter.name));
+ }
+ let color = formatter.color.unwrap_or(0);
+ Ok(Self::new(color, Some(formatter.name.to_string())))
+ }
+}
+
#[derive(Clone, Debug)]
pub struct Style {
- // @Nullable
- // final TextColor color;
- // @Nullable
- // final Boolean bold;
- // @Nullable
- // final Boolean italic;
- // @Nullable
- // final Boolean underlined;
- // @Nullable
- // final Boolean strikethrough;
- // @Nullable
- // final Boolean obfuscated;
- // @Nullable
- // final ClickEvent clickEvent;
- // @Nullable
- // final HoverEvent hoverEvent;
- // @Nullable
- // final String insertion;
- // @Nullable
- // final ResourceLocation font;
-
// these are options instead of just bools because None is different than false in this case
pub color: Option<TextColor>,
pub bold: Option<bool>,
@@ -218,17 +210,24 @@ pub struct Style {
pub underlined: Option<bool>,
pub strikethrough: Option<bool>,
pub obfuscated: Option<bool>,
+ /// Whether it should reset the formatting before applying these styles
+ pub reset: bool,
}
impl Style {
- pub fn default() -> Style {
- Style {
+ pub fn default() -> Self {
+ Self::empty()
+ }
+
+ pub fn empty() -> Self {
+ Self {
color: None,
bold: None,
italic: None,
underlined: None,
strikethrough: None,
obfuscated: None,
+ reset: false,
}
}
@@ -251,6 +250,7 @@ impl Style {
underlined,
strikethrough,
obfuscated,
+ ..Style::default()
}
} else {
Style::default()
@@ -268,43 +268,36 @@ impl Style {
}
/// find the necessary ansi code to get from this style to another
- pub fn compare_ansi(&self, after: &Style) -> String {
- let should_reset = {
+ pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String {
+ let should_reset = after.reset ||
// if it used to be bold and now it's not, reset
- if self.bold.unwrap_or(false) && !after.bold.unwrap_or(true) {
- true
- }
+ (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
// if it used to be italic and now it's not, reset
- else if self.italic.unwrap_or(false) && !after.italic.unwrap_or(true) {
- true
+ (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
// if it used to be underlined and now it's not, reset
- } else if self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true) {
- true
+ (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
// if it used to be strikethrough and now it's not, reset
- } else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) {
- true
+ (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
// if it used to be obfuscated and now it's not, reset
- } else {
- self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)
- }
- };
+ (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true));
let mut ansi_codes = String::new();
+ let empty_style = Style::empty();
+
let (before, after) = if should_reset {
- // if it's true before and none after, make it true after
- // if it's false before and none after, make it false after
- // we should apply after into before and use that as after
ansi_codes.push_str(Ansi::RESET);
- let mut updated_after = self.clone();
+ let mut updated_after = if after.reset {
+ default_style.clone()
+ } else {
+ self.clone()
+ };
updated_after.apply(after);
- (Style::default(), updated_after)
+ (&empty_style, updated_after)
} else {
- (self.clone(), after.clone())
+ (self, after.clone())
};
- println!("should_reset {:?}", should_reset);
-
// if bold used to be false/default and now it's true, set bold
if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) {
ansi_codes.push_str(Ansi::BOLD);
@@ -331,7 +324,7 @@ impl Style {
if before.color.is_none() && after.color.is_some() {
true
} else if before.color.is_some() && after.color.is_some() {
- before.color.unwrap().value != after.color.as_ref().unwrap().value
+ before.color.clone().unwrap().value != after.color.as_ref().unwrap().value
} else {
false
}
@@ -369,21 +362,14 @@ impl Style {
/// Apply a ChatFormatting to this style
pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
- match formatting {
- &ChatFormatting::BOLD => self.bold = Some(true),
- &ChatFormatting::ITALIC => self.italic = Some(true),
- &ChatFormatting::UNDERLINE => self.underlined = Some(true),
- &ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true),
- &ChatFormatting::OBFUSCATED => self.obfuscated = Some(true),
- &ChatFormatting::RESET => {
- self.color = None;
- self.bold = None;
- self.italic = None;
- self.underlined = None;
- self.strikethrough = None;
- self.obfuscated = None;
- }
- &ChatFormatting {
+ match *formatting {
+ ChatFormatting::BOLD => self.bold = Some(true),
+ ChatFormatting::ITALIC => self.italic = Some(true),
+ ChatFormatting::UNDERLINE => self.underlined = Some(true),
+ ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true),
+ ChatFormatting::OBFUSCATED => self.obfuscated = Some(true),
+ ChatFormatting::RESET => self.reset = true,
+ ChatFormatting {
name: _,
code: _,
is_format: _,
@@ -401,6 +387,8 @@ impl Style {
#[cfg(test)]
mod tests {
+ use crate::component::DEFAULT_STYLE;
+
use super::*;
#[test]
@@ -418,22 +406,15 @@ mod tests {
#[test]
fn ansi_difference_should_reset() {
let style_a = Style {
- color: None,
bold: Some(true),
italic: Some(true),
- underlined: None,
- strikethrough: None,
- obfuscated: None,
+ ..Style::default()
};
let style_b = Style {
- color: None,
bold: Some(false),
- italic: None,
- underlined: None,
- strikethrough: None,
- obfuscated: None,
+ ..Style::default()
};
- let ansi_difference = style_a.compare_ansi(&style_b);
+ let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
assert_eq!(
ansi_difference,
format!(
@@ -446,26 +427,41 @@ mod tests {
#[test]
fn ansi_difference_shouldnt_reset() {
let style_a = Style {
- color: None,
bold: Some(true),
- italic: None,
- underlined: None,
- strikethrough: None,
- obfuscated: None,
+ ..Style::default()
};
let style_b = Style {
- color: None,
- bold: None,
italic: Some(true),
- underlined: None,
- strikethrough: None,
- obfuscated: None,
+ ..Style::default()
};
- let ansi_difference = style_a.compare_ansi(&style_b);
+ let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
assert_eq!(ansi_difference, Ansi::ITALIC)
}
#[test]
+ fn ansi_difference_explicit_reset() {
+ let style_a = Style {
+ bold: Some(true),
+ ..Style::empty()
+ };
+ let style_b = Style {
+ italic: Some(true),
+ reset: true,
+ ..Style::empty()
+ };
+ let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE);
+ assert_eq!(
+ ansi_difference,
+ format!(
+ "{reset}{italic}{white}",
+ reset = Ansi::RESET,
+ white = Ansi::rgb(ChatFormatting::WHITE.color.unwrap()),
+ italic = Ansi::ITALIC
+ )
+ )
+ }
+
+ #[test]
fn test_from_code() {
assert_eq!(
ChatFormatting::from_code('a').unwrap(),
diff --git a/minecraft-chat/src/text_component.rs b/minecraft-chat/src/text_component.rs
index 66bde690..a5030fa1 100644
--- a/minecraft-chat/src/text_component.rs
+++ b/minecraft-chat/src/text_component.rs
@@ -1,4 +1,10 @@
-use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting};
+use std::fmt;
+
+use crate::{
+ base_component::BaseComponent,
+ component::Component,
+ style::{ChatFormatting, Style, TextColor},
+};
#[derive(Clone, Debug)]
pub struct TextComponent {
@@ -10,7 +16,7 @@ const LEGACY_FORMATTING_CODE_SYMBOL: char = '§';
/// Convert a legacy color code string into a Component
/// Technically in Minecraft this is done when displaying the text, but AFAIK it's the same as just doing it in TextComponent
-pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component {
+pub fn legacy_color_code_to_text_component(legacy_color_code: &str) -> TextComponent {
let mut components: Vec<TextComponent> = Vec::with_capacity(1);
// iterate over legacy_color_code, if it starts with LEGACY_COLOR_CODE_SYMBOL then read the next character and get the style from that
// otherwise, add the character to the text
@@ -21,24 +27,15 @@ pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component {
if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL {
let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap();
if let Ok(formatter) = ChatFormatting::from_code(formatting_code) {
- if components.is_empty() || components.last().unwrap().text.is_empty() {
+ if components.is_empty() {
+ components.push(TextComponent::new("".to_string()));
+ } else if !components.last().unwrap().text.is_empty() {
components.push(TextComponent::new("".to_string()));
}
- println!(
- "applying formatter {:?} {:?}",
- components.last_mut().unwrap().base.style,
- formatter
- );
- components
- .last_mut()
- .unwrap()
- .base
- .style
- .apply_formatting(formatter);
- println!(
- "applied formatter {:?}",
- components.last_mut().unwrap().base.style
- );
+
+ let style = &mut components.last_mut().unwrap().base.style;
+ // if the formatter is a reset, then we need to reset the style to the default
+ style.apply_formatting(formatter);
}
i += 1;
} else {
@@ -60,36 +57,72 @@ pub fn legacy_color_code_to_component(legacy_color_code: &str) -> Component {
final_component.base.siblings.push(component.get());
}
- final_component.get()
+ final_component
}
impl<'a> TextComponent {
pub fn new(text: String) -> Self {
- Self {
- base: BaseComponent::new(),
- text,
+ // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it
+ if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) {
+ legacy_color_code_to_text_component(&text)
+ } else {
+ Self {
+ base: BaseComponent::new(),
+ text,
+ }
}
}
- pub fn to_string(&self) -> String {
- self.text.clone()
- }
-
fn get(self) -> Component {
Component::Text(self)
}
}
+impl fmt::Display for TextComponent {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.text.clone())
+ }
+}
+
#[cfg(test)]
mod tests {
+ use crate::style::Ansi;
+
use super::*;
#[test]
+ fn test_hypixel_motd() {
+ let component =
+ TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
+ .get();
+ assert_eq!(
+ component.to_ansi(None),
+ format!(
+ "{GREEN}Hypixel Network {RED}[1.8-1.18]\n{BOLD}{AQUA}HAPPY HOLIDAYS{RESET}",
+ GREEN = Ansi::rgb(ChatFormatting::GREEN.color.unwrap()),
+ RED = Ansi::rgb(ChatFormatting::RED.color.unwrap()),
+ AQUA = Ansi::rgb(ChatFormatting::AQUA.color.unwrap()),
+ BOLD = Ansi::BOLD,
+ RESET = Ansi::RESET
+ )
+ );
+ }
+
+ #[test]
fn test_legacy_color_code_to_component() {
- let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d");
+ let component = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get();
assert_eq!(
- component.to_ansi(),
- "\u{1b}[38;2;170;0;170mHello world\u{1b}[m"
+ component.to_ansi(None),
+ format!(
+ "{BOLD}Hello {RESET}{DARK_BLUE}w{DARK_GREEN}o{DARK_AQUA}r{DARK_RED}l{DARK_PURPLE}d{RESET}",
+ BOLD = Ansi::BOLD,
+ RESET = Ansi::RESET,
+ DARK_BLUE = Ansi::rgb(ChatFormatting::DARK_BLUE.color.unwrap()),
+ DARK_GREEN = Ansi::rgb(ChatFormatting::DARK_GREEN.color.unwrap()),
+ DARK_AQUA = Ansi::rgb(ChatFormatting::DARK_AQUA.color.unwrap()),
+ DARK_RED = Ansi::rgb(ChatFormatting::DARK_RED.color.unwrap()),
+ DARK_PURPLE = Ansi::rgb(ChatFormatting::DARK_PURPLE.color.unwrap())
+ )
);
}
}
diff --git a/minecraft-chat/tests/integration_test.rs b/minecraft-chat/tests/integration_test.rs
index 3feff1ed..aac12875 100644
--- a/minecraft-chat/tests/integration_test.rs
+++ b/minecraft-chat/tests/integration_test.rs
@@ -2,6 +2,7 @@ use minecraft_chat::{
component::Component,
style::{Ansi, ChatFormatting, TextColor},
};
+use serde::Deserialize;
use serde_json::Value;
#[test]
@@ -14,9 +15,9 @@ fn basic_ansi_test() {
}"#,
)
.unwrap();
- let component = Component::new(&j).unwrap();
+ let component = Component::deserialize(&j).unwrap();
assert_eq!(
- component.to_ansi(),
+ component.to_ansi(None),
"\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m"
);
}
@@ -50,9 +51,9 @@ fn complex_ansi_test() {
]"##,
)
.unwrap();
- let component = Component::new(&j).unwrap();
+ let component = Component::deserialize(&j).unwrap();
assert_eq!(
- component.to_ansi(),
+ component.to_ansi(None),
format!(
"{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}",
bold = Ansi::BOLD,
@@ -69,6 +70,6 @@ fn complex_ansi_test() {
#[test]
fn component_from_string() {
let j: Value = serde_json::from_str("\"foo\"").unwrap();
- let component = Component::new(&j).unwrap();
- assert_eq!(component.to_ansi(), "foo");
+ let component = Component::deserialize(&j).unwrap();
+ assert_eq!(component.to_ansi(None), "foo");
}
diff --git a/minecraft-protocol/Cargo.toml b/minecraft-protocol/Cargo.toml
index 7894ecfd..3cbf663b 100644
--- a/minecraft-protocol/Cargo.toml
+++ b/minecraft-protocol/Cargo.toml
@@ -7,12 +7,13 @@ version = "0.1.0"
[dependencies]
async-recursion = "^0.3.2"
+async-trait = "0.1.51"
byteorder = "^1.4.3"
bytes = "^1.1.0"
+minecraft-chat = {path = "../minecraft-chat"}
+serde = {version = "1.0.130", features = ["serde_derive"]}
+serde_json = "^1.0.72"
thiserror = "^1.0.30"
tokio = {version = "^1.14.0", features = ["io-util", "net", "macros"]}
tokio-util = "^0.6.9"
trust-dns-resolver = "^0.20.3"
-async-trait = "0.1.51"
-minecraft-chat = { path = "../minecraft-chat" }
-serde_json = "^1.0.72"
diff --git a/minecraft-protocol/src/connection.rs b/minecraft-protocol/src/connection.rs
index cfca403c..2fe03dfb 100644
--- a/minecraft-protocol/src/connection.rs
+++ b/minecraft-protocol/src/connection.rs
@@ -45,7 +45,7 @@ impl Connection {
self.state = state;
}
- pub async fn read_packet(&mut self) -> Result<(), String> {
+ pub async fn read_packet(&mut self) -> Result<Packet, String> {
// what this does:
// 1. reads the first 5 bytes, probably only some of this will be used to get the packet length
// 2. how much we should read = packet length - 5
@@ -69,9 +69,7 @@ impl Connection {
)
.await?;
- println!("packet: {:?}", packet);
-
- Ok(())
+ Ok(packet)
}
/// Write a packet to the server
diff --git a/minecraft-protocol/src/lib.rs b/minecraft-protocol/src/lib.rs
index 88b3603f..aaf3da50 100644
--- a/minecraft-protocol/src/lib.rs
+++ b/minecraft-protocol/src/lib.rs
@@ -23,7 +23,7 @@ pub struct ServerIpAddress {
impl ServerAddress {
/// Convert a Minecraft server address (host:port, the port is optional) to a ServerAddress
- pub fn parse(string: &String) -> Result<ServerAddress, String> {
+ pub fn parse(string: &str) -> Result<ServerAddress, String> {
if string.is_empty() {
return Err("Empty string".to_string());
}
diff --git a/minecraft-protocol/src/mc_buf.rs b/minecraft-protocol/src/mc_buf.rs
index a9ad1642..6c812058 100644
--- a/minecraft-protocol/src/mc_buf.rs
+++ b/minecraft-protocol/src/mc_buf.rs
@@ -49,7 +49,7 @@ pub async fn read_varint<T: AsyncRead + std::marker::Unpin>(
pub fn write_varint(buf: &mut Vec<u8>, mut value: i32) {
let mut buffer = [0];
if value == 0 {
- buf.write(&buffer).unwrap();
+ buf.write_all(&buffer).unwrap();
}
while value != 0 {
buffer[0] = (value & 0b0111_1111) as u8;
@@ -57,7 +57,7 @@ pub fn write_varint(buf: &mut Vec<u8>, mut value: i32) {
if value != 0 {
buffer[0] |= 0b1000_0000;
}
- buf.write(&buffer).unwrap();
+ buf.write_all(&buffer).unwrap();
}
}
@@ -134,7 +134,7 @@ pub async fn read_utf_with_len<T: AsyncRead + std::marker::Unpin>(
Ok(string)
}
-pub fn write_utf_with_len(buf: &mut Vec<u8>, string: &String, len: usize) {
+pub fn write_utf_with_len(buf: &mut Vec<u8>, string: &str, len: usize) {
if string.len() > len {
panic!(
"String too big (was {} bytes encoded, max {})",
@@ -152,7 +152,7 @@ pub async fn read_utf<T: AsyncRead + std::marker::Unpin>(
read_utf_with_len(buf, MAX_STRING_LENGTH.into()).await
}
-pub fn write_utf(buf: &mut Vec<u8>, string: &String) {
+pub fn write_utf(buf: &mut Vec<u8>, string: &str) {
write_utf_with_len(buf, string, MAX_STRING_LENGTH.into());
}
diff --git a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs b/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs
index 20db9fe1..0868a062 100644
--- a/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs
+++ b/minecraft-protocol/src/packets/status/clientbound_status_response_packet.rs
@@ -1,5 +1,6 @@
use async_trait::async_trait;
use minecraft_chat::component::Component;
+use serde::{Deserialize, Deserializer};
use serde_json::Value;
use tokio::io::BufReader;
@@ -8,29 +9,29 @@ use crate::{
packets::{Packet, PacketTrait},
};
-#[derive(Clone, Debug)]
-struct Version {
- name: String,
- protocol: u32,
+#[derive(Clone, Debug, Deserialize)]
+pub struct Version {
+ pub name: String,
+ pub protocol: u32,
}
-#[derive(Clone, Debug)]
-struct SamplePlayer {
- id: String,
- name: String,
+#[derive(Clone, Debug, Deserialize)]
+pub struct SamplePlayer {
+ pub id: String,
+ pub name: String,
}
-#[derive(Clone, Debug)]
-struct Players {
- max: u32,
- online: u32,
- sample: Vec<SamplePlayer>,
+#[derive(Clone, Debug, Deserialize)]
+pub struct Players {
+ pub max: u32,
+ pub online: u32,
+ pub sample: Vec<SamplePlayer>,
}
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Deserialize)]
pub struct ClientboundStatusResponsePacket {
- // version: Version,
- description: Component,
+ pub version: Version,
+ pub description: Component,
}
#[async_trait]
@@ -47,13 +48,9 @@ impl PacketTrait for ClientboundStatusResponsePacket {
let status_string = mc_buf::read_utf(buf).await?;
let status_json: Value =
serde_json::from_str(status_string.as_str()).expect("Server status isn't valid JSON");
- let description_string: &Value = status_json.get("description").unwrap();
-
- // this.status = GsonHelper.fromJson(GSON, friendlyByteBuf.readUtf(32767), ServerStatus.class);
- Ok(ClientboundStatusResponsePacket {
- // version: status_json.get("version"),
- description: Component::new(description_string)?,
- }
- .get())
+
+ Ok(ClientboundStatusResponsePacket::deserialize(status_json)
+ .map_err(|e| e.to_string())?
+ .get())
}
}
diff --git a/minecraft-protocol/src/resolver.rs b/minecraft-protocol/src/resolver.rs
index b751e05f..24687a6e 100644
--- a/minecraft-protocol/src/resolver.rs
+++ b/minecraft-protocol/src/resolver.rs
@@ -29,8 +29,7 @@ pub async fn resolve_address(address: &ServerAddress) -> Result<ServerIpAddress,
.await;
// if it resolves that means it's a redirect so we call resolve_address again with the new host
- if srv_redirect_result.is_ok() {
- let redirect_result = srv_redirect_result.unwrap();
+ if let Ok(redirect_result) = srv_redirect_result {
let redirect_srv = redirect_result
.iter()
.next()
diff --git a/minecraft-protocol/src/server_status_pinger.rs b/minecraft-protocol/src/server_status_pinger.rs
index 0e12a6a7..ae41ed51 100644
--- a/minecraft-protocol/src/server_status_pinger.rs
+++ b/minecraft-protocol/src/server_status_pinger.rs
@@ -3,7 +3,7 @@ use crate::{
packets::{
handshake::client_intention_packet::ClientIntentionPacket,
status::serverbound_status_request_packet::ServerboundStatusRequestPacket,
- ConnectionProtocol, PacketTrait,
+ ConnectionProtocol, Packet, PacketTrait,
},
resolver, ServerAddress,
};
@@ -33,7 +33,17 @@ pub async fn ping_server(address: &ServerAddress) -> Result<(), String> {
conn.send_packet(ServerboundStatusRequestPacket {}.get())
.await;
- conn.read_packet().await.unwrap();
+ let packet = conn.read_packet().await.unwrap();
+
+ match packet {
+ Packet::ClientboundStatusResponsePacket(p) => {
+ println!("{:?}", p);
+ println!("{}", p.description.to_ansi(None));
+ }
+ _ => {
+ println!("unexpected packet {:?}", packet);
+ }
+ }
Ok(())