mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 06:16:04 +00:00
correct minecraft-chat 🎉
This commit is contained in:
parent
6026c74430
commit
ba911a8a20
14 changed files with 395 additions and 302 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use minecraft_client;
|
||||
use minecraft_protocol::ServerAddress;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
|
|
|
@ -7,4 +7,5 @@ version = "0.1.0"
|
|||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
serde = "^1.0.130"
|
||||
serde_json = "^1.0.72"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(())
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue