1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00

correct minecraft-chat 🎉

This commit is contained in:
mat 2021-12-11 15:17:42 -06:00
parent 6026c74430
commit ba911a8a20
14 changed files with 395 additions and 302 deletions

16
Cargo.lock generated
View file

@ -289,6 +289,7 @@ name = "minecraft-chat"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"serde",
"serde_json", "serde_json",
] ]
@ -308,6 +309,7 @@ dependencies = [
"byteorder", "byteorder",
"bytes", "bytes",
"minecraft-chat", "minecraft-chat",
"serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"tokio", "tokio",
@ -505,6 +507,20 @@ name = "serde"
version = "1.0.130" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" 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]] [[package]]
name = "serde_json" name = "serde_json"

View file

@ -1,4 +1,3 @@
use minecraft_client;
use minecraft_protocol::ServerAddress; use minecraft_protocol::ServerAddress;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;

View file

@ -7,4 +7,5 @@ version = "0.1.0"
[dependencies] [dependencies]
lazy_static = "1.4.0" lazy_static = "1.4.0"
serde = "^1.0.130"
serde_json = "^1.0.72" serde_json = "^1.0.72"

View file

@ -1,8 +1,11 @@
use serde_json; use serde::{
de::{self, Error},
Deserialize, Deserializer,
};
use crate::{ use crate::{
base_component::BaseComponent, base_component::BaseComponent,
style::Style, style::{ChatFormatting, Style},
text_component::TextComponent, text_component::TextComponent,
translatable_component::{StringOrComponent, TranslatableComponent}, translatable_component::{StringOrComponent, TranslatableComponent},
}; };
@ -13,141 +16,15 @@ pub enum Component {
Translatable(TranslatableComponent), Translatable(TranslatableComponent),
} }
lazy_static! {
pub static ref DEFAULT_STYLE: Style = Style {
color: Some(ChatFormatting::WHITE.try_into().unwrap()),
..Style::default()
};
}
/// A chat component /// A chat component
impl Component { impl Component {
pub fn new(json: &serde_json::Value) -> Result<Component, String> {
// we create a component that we might add siblings to
let mut component: Component;
// if it's primitive, make it a text component
if !json.is_array() && !json.is_object() {
return Ok(Component::Text(TextComponent::new(
json.as_str().unwrap_or("").to_string(),
)));
}
// if it's an object, do things with { text } and stuff
else if json.is_object() {
if json.get("text").is_some() {
let text = json.get("text").unwrap().as_str().unwrap_or("").to_string();
component = Component::Text(TextComponent::new(text));
} else if json.get("translate").is_some() {
let translate = json.get("translate").unwrap().to_string();
if json.get("with").is_some() {
let with = json.get("with").unwrap().as_array().unwrap();
let mut with_array = Vec::with_capacity(with.len());
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])?;
if let Component::Text(text_component) = c {
if text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
{
with_array.push(StringOrComponent::String(text_component.text));
break;
}
}
with_array.push(StringOrComponent::Component(Component::new(&with[i])?));
}
component =
Component::Translatable(TranslatableComponent::new(translate, with_array));
} else {
// if it doesn't have a "with", just have the with_array be empty
component =
Component::Translatable(TranslatableComponent::new(translate, Vec::new()));
}
} else if json.get("score").is_some() {
// object = GsonHelper.getAsJsonObject(jsonObject, "score");
let score_json = json.get("score").unwrap();
// 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()
);
}
// TODO
return Err("score text components aren't yet supported".to_string());
// 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());
// } 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());
} 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));
}
// 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 _interpret = match json.get("interpret") {
Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
None => false,
};
// 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());
// 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);
// } else {
// if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component");
// NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional);
// }
// }
}
// if (jsonObject.has("extra")) {
// object = GsonHelper.getAsJsonArray(jsonObject, "extra");
// if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components");
// for (int i = 0; i < object.size(); ++i) {
// var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext));
// }
// }
// var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class));
// return var5_17;
// }
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()),
};
if extra.is_empty() {
return Err("Unexpected empty array of components".to_string());
}
for extra_component in extra {
component.append(Component::new(extra_component)?);
}
}
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));
}
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])?;
for i in 1..json_array.len() {
component.append(Component::new(json_array.get(i).unwrap())?);
}
Ok(component)
}
// TODO: is it possible to use a macro so this doesn't have to be duplicated? // 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 { pub fn get_base_mut(&mut self) -> &mut BaseComponent {
@ -170,15 +47,20 @@ impl Component {
} }
/// Get the "separator" component from the json /// Get the "separator" component from the json
fn parse_separator(json: &serde_json::Value) -> Result<Option<Component>, String> { fn parse_separator(json: &serde_json::Value) -> Result<Option<Component>, serde_json::Error> {
if json.get("separator").is_some() { if json.get("separator").is_some() {
return Ok(Some(Component::new(json.get("separator").unwrap())?)); return Ok(Some(Component::deserialize(
json.get("separator").unwrap(),
)?));
} }
Ok(None) Ok(None)
} }
/// Convert this component into an ansi string /// Convert this component into an ansi string
pub fn to_ansi(&self) -> 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 // this contains the final string will all the ansi escape codes
let mut built_string = String::new(); let mut built_string = String::new();
// this style will update as we visit components // this style will update as we visit components
@ -191,7 +73,7 @@ impl Component {
}; };
let component_style = &component.get_base().style; let component_style = &component.get_base().style;
let ansi_text = running_style.compare_ansi(component_style); let ansi_text = running_style.compare_ansi(component_style, default_style);
built_string.push_str(&ansi_text); built_string.push_str(&ansi_text);
built_string.push_str(component_text); built_string.push_str(component_text);
@ -223,3 +105,163 @@ impl IntoIterator for Component {
type Item = Component; type Item = Component;
type IntoIter = std::vec::IntoIter<Self::Item>; 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;
// if it's primitive, make it a text component
if !json.is_array() && !json.is_object() {
return Ok(Component::Text(TextComponent::new(
json.as_str().unwrap_or("").to_string(),
)));
}
// if it's an object, do things with { text } and stuff
else if json.is_object() {
if json.get("text").is_some() {
let text = json.get("text").unwrap().as_str().unwrap_or("").to_string();
component = Component::Text(TextComponent::new(text));
} else if json.get("translate").is_some() {
let translate = json.get("translate").unwrap().to_string();
if json.get("with").is_some() {
let with = json.get("with").unwrap().as_array().unwrap();
let mut with_array = Vec::with_capacity(with.len());
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::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()
{
with_array.push(StringOrComponent::String(text_component.text));
break;
}
}
with_array.push(StringOrComponent::Component(
Component::deserialize(&with[i]).map_err(de::Error::custom)?,
));
}
component =
Component::Translatable(TranslatableComponent::new(translate, with_array));
} else {
// if it doesn't have a "with", just have the with_array be empty
component =
Component::Translatable(TranslatableComponent::new(translate, Vec::new()));
}
} else if json.get("score").is_some() {
// object = GsonHelper.getAsJsonObject(jsonObject, "score");
let score_json = json.get("score").unwrap();
// 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(de::Error::missing_field(
"A score component needs at least a name and an objective",
));
}
// TODO
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(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(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(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).map_err(de::Error::custom)?;
let _interpret = match json.get("interpret") {
Some(v) => v.as_bool().ok_or(Some(false)).unwrap(),
None => false,
};
// boolean bl = GsonHelper.getAsBoolean(jsonObject, "interpret", false);
// if (jsonObject.has("block")) {
if json.get("block").is_some() {}
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);
// } else {
// if (!jsonObject.has("storage")) throw new JsonParseException("Don't know how to turn " + jsonElement + " into a Component");
// NbtComponent.StorageNbtComponent storageNbtComponent = new NbtComponent.StorageNbtComponent((String)object, bl, new ResourceLocation(GsonHelper.getAsString(jsonObject, "storage")), optional);
// }
// }
}
// if (jsonObject.has("extra")) {
// object = GsonHelper.getAsJsonArray(jsonObject, "extra");
// if (object.size() <= 0) throw new JsonParseException("Unexpected empty array of components");
// for (int i = 0; i < object.size(); ++i) {
// var5_17.append(this.deserialize(object.get(i), type, jsonDeserializationContext));
// }
// }
// var5_17.setStyle((Style)jsonDeserializationContext.deserialize(jsonElement, Style.class));
// return var5_17;
// }
if json.get("extra").is_some() {
let extra = match json.get("extra").unwrap().as_array() {
Some(r) => r,
None => return Err(de::Error::custom("Extra isn't an array")),
};
if extra.is_empty() {
return Err(de::Error::custom("Unexpected empty array of components"));
}
for extra_component in extra {
let sibling =
Component::deserialize(extra_component).map_err(de::Error::custom)?;
component.append(sibling);
}
}
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(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::deserialize(&json_array[0]).map_err(de::Error::custom)?;
for i in 1..json_array.len() {
component.append(
Component::deserialize(json_array.get(i).unwrap()).map_err(de::Error::custom)?,
);
}
Ok(component)
}
}

View file

@ -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)] #[derive(Clone, Debug)]
pub struct Style { 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 // these are options instead of just bools because None is different than false in this case
pub color: Option<TextColor>, pub color: Option<TextColor>,
pub bold: Option<bool>, pub bold: Option<bool>,
@ -218,17 +210,24 @@ pub struct Style {
pub underlined: Option<bool>, pub underlined: Option<bool>,
pub strikethrough: Option<bool>, pub strikethrough: Option<bool>,
pub obfuscated: Option<bool>, pub obfuscated: Option<bool>,
/// Whether it should reset the formatting before applying these styles
pub reset: bool,
} }
impl Style { impl Style {
pub fn default() -> Style { pub fn default() -> Self {
Style { Self::empty()
}
pub fn empty() -> Self {
Self {
color: None, color: None,
bold: None, bold: None,
italic: None, italic: None,
underlined: None, underlined: None,
strikethrough: None, strikethrough: None,
obfuscated: None, obfuscated: None,
reset: false,
} }
} }
@ -251,6 +250,7 @@ impl Style {
underlined, underlined,
strikethrough, strikethrough,
obfuscated, obfuscated,
..Style::default()
} }
} else { } else {
Style::default() Style::default()
@ -268,42 +268,35 @@ impl Style {
} }
/// find the necessary ansi code to get from this style to another /// find the necessary ansi code to get from this style to another
pub fn compare_ansi(&self, after: &Style) -> String { pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String {
let should_reset = { let should_reset = after.reset ||
// if it used to be bold and now it's not, reset // if it used to be bold and now it's not, reset
if self.bold.unwrap_or(false) && !after.bold.unwrap_or(true) { (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
true
}
// if it used to be italic and now it's not, reset // if it used to be italic and now it's not, reset
else if self.italic.unwrap_or(false) && !after.italic.unwrap_or(true) { (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
true
// if it used to be underlined and now it's not, reset // if it used to be underlined and now it's not, reset
} else if self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true) { (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
true
// if it used to be strikethrough and now it's not, reset // if it used to be strikethrough and now it's not, reset
} else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) { (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
true
// if it used to be obfuscated and now it's not, reset // 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 mut ansi_codes = String::new();
let (before, after) = if should_reset { let empty_style = Style::empty();
// 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();
updated_after.apply(after);
(Style::default(), updated_after)
} else {
(self.clone(), after.clone())
};
println!("should_reset {:?}", should_reset); let (before, after) = if should_reset {
ansi_codes.push_str(Ansi::RESET);
let mut updated_after = if after.reset {
default_style.clone()
} else {
self.clone()
};
updated_after.apply(after);
(&empty_style, updated_after)
} else {
(self, after.clone())
};
// if bold used to be false/default and now it's true, set bold // 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) { if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) {
@ -331,7 +324,7 @@ impl Style {
if before.color.is_none() && after.color.is_some() { if before.color.is_none() && after.color.is_some() {
true true
} else if before.color.is_some() && after.color.is_some() { } 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 { } else {
false false
} }
@ -369,21 +362,14 @@ impl Style {
/// Apply a ChatFormatting to this style /// Apply a ChatFormatting to this style
pub fn apply_formatting(&mut self, formatting: &ChatFormatting) { pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
match formatting { match *formatting {
&ChatFormatting::BOLD => self.bold = Some(true), ChatFormatting::BOLD => self.bold = Some(true),
&ChatFormatting::ITALIC => self.italic = Some(true), ChatFormatting::ITALIC => self.italic = Some(true),
&ChatFormatting::UNDERLINE => self.underlined = Some(true), ChatFormatting::UNDERLINE => self.underlined = Some(true),
&ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true), ChatFormatting::STRIKETHROUGH => self.strikethrough = Some(true),
&ChatFormatting::OBFUSCATED => self.obfuscated = Some(true), ChatFormatting::OBFUSCATED => self.obfuscated = Some(true),
&ChatFormatting::RESET => { ChatFormatting::RESET => self.reset = true,
self.color = None; ChatFormatting {
self.bold = None;
self.italic = None;
self.underlined = None;
self.strikethrough = None;
self.obfuscated = None;
}
&ChatFormatting {
name: _, name: _,
code: _, code: _,
is_format: _, is_format: _,
@ -401,6 +387,8 @@ impl Style {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::component::DEFAULT_STYLE;
use super::*; use super::*;
#[test] #[test]
@ -418,22 +406,15 @@ mod tests {
#[test] #[test]
fn ansi_difference_should_reset() { fn ansi_difference_should_reset() {
let style_a = Style { let style_a = Style {
color: None,
bold: Some(true), bold: Some(true),
italic: Some(true), italic: Some(true),
underlined: None, ..Style::default()
strikethrough: None,
obfuscated: None,
}; };
let style_b = Style { let style_b = Style {
color: None,
bold: Some(false), bold: Some(false),
italic: None, ..Style::default()
underlined: None,
strikethrough: None,
obfuscated: None,
}; };
let ansi_difference = style_a.compare_ansi(&style_b); let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
assert_eq!( assert_eq!(
ansi_difference, ansi_difference,
format!( format!(
@ -446,25 +427,40 @@ mod tests {
#[test] #[test]
fn ansi_difference_shouldnt_reset() { fn ansi_difference_shouldnt_reset() {
let style_a = Style { let style_a = Style {
color: None,
bold: Some(true), bold: Some(true),
italic: None, ..Style::default()
underlined: None,
strikethrough: None,
obfuscated: None,
}; };
let style_b = Style { let style_b = Style {
color: None,
bold: None,
italic: Some(true), italic: Some(true),
underlined: None, ..Style::default()
strikethrough: None,
obfuscated: None,
}; };
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) 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] #[test]
fn test_from_code() { fn test_from_code() {
assert_eq!( assert_eq!(

View file

@ -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)] #[derive(Clone, Debug)]
pub struct TextComponent { pub struct TextComponent {
@ -10,7 +16,7 @@ const LEGACY_FORMATTING_CODE_SYMBOL: char = '§';
/// Convert a legacy color code string into a Component /// 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 /// 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); 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 // 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 // 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 { if legacy_color_code.chars().nth(i).unwrap() == LEGACY_FORMATTING_CODE_SYMBOL {
let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap(); let formatting_code = legacy_color_code.chars().nth(i + 1).unwrap();
if let Ok(formatter) = ChatFormatting::from_code(formatting_code) { 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())); components.push(TextComponent::new("".to_string()));
} }
println!(
"applying formatter {:?} {:?}", let style = &mut components.last_mut().unwrap().base.style;
components.last_mut().unwrap().base.style, // if the formatter is a reset, then we need to reset the style to the default
formatter style.apply_formatting(formatter);
);
components
.last_mut()
.unwrap()
.base
.style
.apply_formatting(formatter);
println!(
"applied formatter {:?}",
components.last_mut().unwrap().base.style
);
} }
i += 1; i += 1;
} else { } 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.base.siblings.push(component.get());
} }
final_component.get() final_component
} }
impl<'a> TextComponent { impl<'a> TextComponent {
pub fn new(text: String) -> Self { pub fn new(text: String) -> Self {
Self { // if it contains a LEGACY_FORMATTING_CODE_SYMBOL, format it
base: BaseComponent::new(), if text.contains(LEGACY_FORMATTING_CODE_SYMBOL) {
text, 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 { fn get(self) -> Component {
Component::Text(self) Component::Text(self)
} }
} }
impl fmt::Display for TextComponent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.text.clone())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::style::Ansi;
use super::*; use super::*;
#[test] #[test]
fn test_legacy_color_code_to_component() { fn test_hypixel_motd() {
let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d"); let component =
TextComponent::new("§aHypixel Network §c[1.8-1.18]\n§b§lHAPPY HOLIDAYS".to_string())
.get();
assert_eq!( assert_eq!(
component.to_ansi(), component.to_ansi(None),
"\u{1b}[38;2;170;0;170mHello world\u{1b}[m" 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 = TextComponent::new("§lHello §r§1w§2o§3r§4l§5d".to_string()).get();
assert_eq!(
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())
)
); );
} }
} }

View file

@ -2,6 +2,7 @@ use minecraft_chat::{
component::Component, component::Component,
style::{Ansi, ChatFormatting, TextColor}, style::{Ansi, ChatFormatting, TextColor},
}; };
use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
#[test] #[test]
@ -14,9 +15,9 @@ fn basic_ansi_test() {
}"#, }"#,
) )
.unwrap(); .unwrap();
let component = Component::new(&j).unwrap(); let component = Component::deserialize(&j).unwrap();
assert_eq!( assert_eq!(
component.to_ansi(), component.to_ansi(None),
"\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m" "\u{1b}[1m\u{1b}[38;2;255;85;85mhello\u{1b}[m"
); );
} }
@ -50,9 +51,9 @@ fn complex_ansi_test() {
]"##, ]"##,
) )
.unwrap(); .unwrap();
let component = Component::new(&j).unwrap(); let component = Component::deserialize(&j).unwrap();
assert_eq!( assert_eq!(
component.to_ansi(), component.to_ansi(None),
format!( format!(
"{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}", "{bold}{italic}{underlined}{red}hello{reset}{bold}{italic}{red} {reset}{italic}{strikethrough}{abcdef}world{reset}{abcdef} asdf{bold}!{reset}",
bold = Ansi::BOLD, bold = Ansi::BOLD,
@ -69,6 +70,6 @@ fn complex_ansi_test() {
#[test] #[test]
fn component_from_string() { fn component_from_string() {
let j: Value = serde_json::from_str("\"foo\"").unwrap(); let j: Value = serde_json::from_str("\"foo\"").unwrap();
let component = Component::new(&j).unwrap(); let component = Component::deserialize(&j).unwrap();
assert_eq!(component.to_ansi(), "foo"); assert_eq!(component.to_ansi(None), "foo");
} }

View file

@ -7,12 +7,13 @@ version = "0.1.0"
[dependencies] [dependencies]
async-recursion = "^0.3.2" async-recursion = "^0.3.2"
async-trait = "0.1.51"
byteorder = "^1.4.3" byteorder = "^1.4.3"
bytes = "^1.1.0" 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" thiserror = "^1.0.30"
tokio = {version = "^1.14.0", features = ["io-util", "net", "macros"]} tokio = {version = "^1.14.0", features = ["io-util", "net", "macros"]}
tokio-util = "^0.6.9" tokio-util = "^0.6.9"
trust-dns-resolver = "^0.20.3" trust-dns-resolver = "^0.20.3"
async-trait = "0.1.51"
minecraft-chat = { path = "../minecraft-chat" }
serde_json = "^1.0.72"

View file

@ -45,7 +45,7 @@ impl Connection {
self.state = state; 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: // what this does:
// 1. reads the first 5 bytes, probably only some of this will be used to get the packet length // 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 // 2. how much we should read = packet length - 5
@ -69,9 +69,7 @@ impl Connection {
) )
.await?; .await?;
println!("packet: {:?}", packet); Ok(packet)
Ok(())
} }
/// Write a packet to the server /// Write a packet to the server

View file

@ -23,7 +23,7 @@ pub struct ServerIpAddress {
impl ServerAddress { impl ServerAddress {
/// Convert a Minecraft server address (host:port, the port is optional) to a 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() { if string.is_empty() {
return Err("Empty string".to_string()); return Err("Empty string".to_string());
} }

View file

@ -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) { pub fn write_varint(buf: &mut Vec<u8>, mut value: i32) {
let mut buffer = [0]; let mut buffer = [0];
if value == 0 { if value == 0 {
buf.write(&buffer).unwrap(); buf.write_all(&buffer).unwrap();
} }
while value != 0 { while value != 0 {
buffer[0] = (value & 0b0111_1111) as u8; 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 { if value != 0 {
buffer[0] |= 0b1000_0000; 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) 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 { if string.len() > len {
panic!( panic!(
"String too big (was {} bytes encoded, max {})", "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 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()); write_utf_with_len(buf, string, MAX_STRING_LENGTH.into());
} }

View file

@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use minecraft_chat::component::Component; use minecraft_chat::component::Component;
use serde::{Deserialize, Deserializer};
use serde_json::Value; use serde_json::Value;
use tokio::io::BufReader; use tokio::io::BufReader;
@ -8,29 +9,29 @@ use crate::{
packets::{Packet, PacketTrait}, packets::{Packet, PacketTrait},
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize)]
struct Version { pub struct Version {
name: String, pub name: String,
protocol: u32, pub protocol: u32,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize)]
struct SamplePlayer { pub struct SamplePlayer {
id: String, pub id: String,
name: String, pub name: String,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize)]
struct Players { pub struct Players {
max: u32, pub max: u32,
online: u32, pub online: u32,
sample: Vec<SamplePlayer>, pub sample: Vec<SamplePlayer>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug, Deserialize)]
pub struct ClientboundStatusResponsePacket { pub struct ClientboundStatusResponsePacket {
// version: Version, pub version: Version,
description: Component, pub description: Component,
} }
#[async_trait] #[async_trait]
@ -47,13 +48,9 @@ impl PacketTrait for ClientboundStatusResponsePacket {
let status_string = mc_buf::read_utf(buf).await?; let status_string = mc_buf::read_utf(buf).await?;
let status_json: Value = let status_json: Value =
serde_json::from_str(status_string.as_str()).expect("Server status isn't valid JSON"); 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::deserialize(status_json)
Ok(ClientboundStatusResponsePacket { .map_err(|e| e.to_string())?
// version: status_json.get("version"), .get())
description: Component::new(description_string)?,
}
.get())
} }
} }

View file

@ -29,8 +29,7 @@ pub async fn resolve_address(address: &ServerAddress) -> Result<ServerIpAddress,
.await; .await;
// if it resolves that means it's a redirect so we call resolve_address again with the new host // 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() { if let Ok(redirect_result) = srv_redirect_result {
let redirect_result = srv_redirect_result.unwrap();
let redirect_srv = redirect_result let redirect_srv = redirect_result
.iter() .iter()
.next() .next()

View file

@ -3,7 +3,7 @@ use crate::{
packets::{ packets::{
handshake::client_intention_packet::ClientIntentionPacket, handshake::client_intention_packet::ClientIntentionPacket,
status::serverbound_status_request_packet::ServerboundStatusRequestPacket, status::serverbound_status_request_packet::ServerboundStatusRequestPacket,
ConnectionProtocol, PacketTrait, ConnectionProtocol, Packet, PacketTrait,
}, },
resolver, ServerAddress, resolver, ServerAddress,
}; };
@ -33,7 +33,17 @@ pub async fn ping_server(address: &ServerAddress) -> Result<(), String> {
conn.send_packet(ServerboundStatusRequestPacket {}.get()) conn.send_packet(ServerboundStatusRequestPacket {}.get())
.await; .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(()) Ok(())