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"
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"

View file

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

View file

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

View file

@ -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,141 +16,15 @@ 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> {
// 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?
pub fn get_base_mut(&mut self) -> &mut BaseComponent {
@ -170,15 +47,20 @@ impl Component {
}
/// 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() {
return Ok(Some(Component::new(json.get("separator").unwrap())?));
return Ok(Some(Component::deserialize(
json.get("separator").unwrap(),
)?));
}
Ok(None)
}
/// 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
let mut built_string = String::new();
// this style will update as we visit components
@ -191,7 +73,7 @@ impl Component {
};
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(component_text);
@ -223,3 +105,163 @@ impl IntoIterator for Component {
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;
// 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)]
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,42 +268,35 @@ 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 (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();
updated_after.apply(after);
(Style::default(), updated_after)
} else {
(self.clone(), after.clone())
};
let empty_style = Style::empty();
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 !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() {
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,25 +427,40 @@ 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!(

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)]
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_legacy_color_code_to_component() {
let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d");
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(),
"\u{1b}[38;2;170;0;170mHello world\u{1b}[m"
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 = 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,
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");
}

View file

@ -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"

View file

@ -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

View file

@ -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());
}

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) {
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());
}

View file

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

View file

@ -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()

View file

@ -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(())