aboutsummaryrefslogtreecommitdiff
path: root/minecraft-chat/src
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2021-12-11 11:38:12 -0600
committermat <github@matdoes.dev>2021-12-11 11:38:12 -0600
commit6026c74430f311c9217b77e7ac07d183efde5bce (patch)
tree1d20fc2794e69e4ebb56aa1e9dd43503c39a4144 /minecraft-chat/src
parent5039f9668f3512240af22ac6bb49140012885509 (diff)
downloadazalea-drasl-6026c74430f311c9217b77e7ac07d183efde5bce.tar.xz
add legacy color codes
Diffstat (limited to 'minecraft-chat/src')
-rw-r--r--minecraft-chat/src/base_component.rs2
-rw-r--r--minecraft-chat/src/component.rs76
-rw-r--r--minecraft-chat/src/style.rs80
-rw-r--r--minecraft-chat/src/text_component.rs77
4 files changed, 184 insertions, 51 deletions
diff --git a/minecraft-chat/src/base_component.rs b/minecraft-chat/src/base_component.rs
index 40fb3909..b07e08e7 100644
--- a/minecraft-chat/src/base_component.rs
+++ b/minecraft-chat/src/base_component.rs
@@ -11,7 +11,7 @@ impl BaseComponent {
pub fn new() -> Self {
Self {
siblings: Vec::new(),
- style: Style::new(),
+ style: Style::default(),
}
}
}
diff --git a/minecraft-chat/src/component.rs b/minecraft-chat/src/component.rs
index c59a5d5d..300a1228 100644
--- a/minecraft-chat/src/component.rs
+++ b/minecraft-chat/src/component.rs
@@ -1,5 +1,3 @@
-
-
use serde_json;
use crate::{
@@ -11,8 +9,8 @@ use crate::{
#[derive(Clone, Debug)]
pub enum Component {
- TextComponent(TextComponent),
- TranslatableComponent(TranslatableComponent),
+ Text(TextComponent),
+ Translatable(TranslatableComponent),
}
/// A chat component
@@ -23,7 +21,7 @@ impl Component {
// if it's primitive, make it a text component
if !json.is_array() && !json.is_object() {
- return Ok(Component::TextComponent(TextComponent::new(
+ return Ok(Component::Text(TextComponent::new(
json.as_str().unwrap_or("").to_string(),
)));
}
@@ -31,7 +29,7 @@ impl Component {
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::TextComponent(TextComponent::new(text));
+ 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() {
@@ -41,7 +39,7 @@ impl Component {
// 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::TextComponent(text_component) = c {
+ if let Component::Text(text_component) = c {
if text_component.base.siblings.is_empty()
&& text_component.base.style.is_empty()
{
@@ -51,15 +49,12 @@ impl Component {
}
with_array.push(StringOrComponent::Component(Component::new(&with[i])?));
}
- component = Component::TranslatableComponent(TranslatableComponent::new(
- translate, with_array,
- ));
+ 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::TranslatableComponent(TranslatableComponent::new(
- translate,
- Vec::new(),
- ));
+ component =
+ Component::Translatable(TranslatableComponent::new(translate, Vec::new()));
}
} else if json.get("score").is_some() {
// object = GsonHelper.getAsJsonObject(jsonObject, "score");
@@ -157,15 +152,15 @@ impl Component {
pub fn get_base_mut(&mut self) -> &mut BaseComponent {
match self {
- Self::TextComponent(c) => &mut c.base,
- Self::TranslatableComponent(c) => &mut c.base,
+ Self::Text(c) => &mut c.base,
+ Self::Translatable(c) => &mut c.base,
}
}
pub fn get_base(&self) -> &BaseComponent {
match self {
- Self::TextComponent(c) => &c.base,
- Self::TranslatableComponent(c) => &c.base,
+ Self::Text(c) => &c.base,
+ Self::Translatable(c) => &c.base,
}
}
@@ -182,30 +177,17 @@ impl Component {
Ok(None)
}
- /// Recursively call the function for every component in this component
- pub fn visit<F>(&self, f: &mut F)
- where
- // The closure takes an `i32` and returns an `i32`.
- F: FnMut(&Component),
- {
- f(self);
- self.get_base()
- .siblings
- .iter()
- .for_each(|s| Component::visit(s, f));
- }
-
/// Convert this component into an ansi string
pub fn to_ansi(&self) -> String {
// this contains the final string will all the ansi escape codes
let mut built_string = String::new();
// this style will update as we visit components
- let mut running_style = Style::new();
+ let mut running_style = Style::default();
- self.visit(&mut |component| {
- let component_text = match component {
- Self::TextComponent(c) => &c.text,
- Self::TranslatableComponent(c) => &c.key,
+ for component in self.clone().into_iter() {
+ let component_text = match &component {
+ Self::Text(c) => &c.text,
+ Self::Translatable(c) => &c.key,
};
let component_style = &component.get_base().style;
@@ -214,12 +196,30 @@ impl Component {
built_string.push_str(component_text);
running_style.apply(component_style);
- });
+ }
if !running_style.is_empty() {
- built_string.push_str("\x1b[m");
+ built_string.push_str("\u{1b}[m");
}
built_string
}
}
+
+impl IntoIterator for Component {
+ /// Recursively call the function for every component in this component
+ fn into_iter(self) -> Self::IntoIter {
+ let base = self.get_base();
+ let siblings = base.siblings.clone();
+ let mut v: Vec<Component> = Vec::with_capacity(siblings.len() + 1);
+ v.push(self);
+ for sibling in siblings {
+ v.extend(sibling.into_iter());
+ }
+
+ v.into_iter()
+ }
+
+ type Item = Component;
+ type IntoIter = std::vec::IntoIter<Self::Item>;
+}
diff --git a/minecraft-chat/src/style.rs b/minecraft-chat/src/style.rs
index ecde7c9e..14fabec5 100644
--- a/minecraft-chat/src/style.rs
+++ b/minecraft-chat/src/style.rs
@@ -66,16 +66,16 @@ pub struct ChatFormatting<'a> {
pub struct Ansi {}
impl Ansi {
- pub const BOLD: &'static str = "\x1b[1m";
- pub const ITALIC: &'static str = "\x1b[3m";
- pub const UNDERLINED: &'static str = "\x1b[4m";
- pub const STRIKETHROUGH: &'static str = "\x1b[9m";
- pub const OBFUSCATED: &'static str = "\x1b[8m";
- pub const RESET: &'static str = "\x1b[m";
+ pub const BOLD: &'static str = "\u{1b}[1m";
+ pub const ITALIC: &'static str = "\u{1b}[3m";
+ pub const UNDERLINED: &'static str = "\u{1b}[4m";
+ pub const STRIKETHROUGH: &'static str = "\u{1b}[9m";
+ pub const OBFUSCATED: &'static str = "\u{1b}[8m";
+ pub const RESET: &'static str = "\u{1b}[m";
pub fn rgb(value: u32) -> String {
format!(
- "\x1b[38;2;{};{};{}m",
+ "\u{1b}[38;2;{};{};{}m",
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF
@@ -159,6 +159,15 @@ impl<'a> ChatFormatting<'a> {
color,
}
}
+
+ pub fn from_code(code: char) -> Result<&'static ChatFormatting<'static>, String> {
+ for formatter in &ChatFormatting::FORMATTERS {
+ if formatter.code == code {
+ return Ok(formatter);
+ }
+ }
+ Err(format!("Invalid formatting code {}", code))
+ }
}
impl TextColor {
@@ -212,7 +221,7 @@ pub struct Style {
}
impl Style {
- pub fn new() -> Style {
+ pub fn default() -> Style {
Style {
color: None,
bold: None,
@@ -244,7 +253,7 @@ impl Style {
obfuscated,
}
} else {
- Style::new()
+ Style::default()
};
}
@@ -275,7 +284,9 @@ impl Style {
} else if self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true) {
true
// if it used to be obfuscated and now it's not, reset
- } else { self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true) }
+ } else {
+ self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true)
+ }
};
let mut ansi_codes = String::new();
@@ -287,7 +298,7 @@ impl Style {
ansi_codes.push_str(Ansi::RESET);
let mut updated_after = self.clone();
updated_after.apply(after);
- (Style::new(), updated_after)
+ (Style::default(), updated_after)
} else {
(self.clone(), after.clone())
};
@@ -355,6 +366,37 @@ impl Style {
self.obfuscated = Some(*obfuscated);
}
}
+
+ /// 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 {
+ name: _,
+ code: _,
+ is_format: _,
+ id: _,
+ color,
+ } => {
+ // if it's a color, set it
+ if let Some(color) = color {
+ self.color = Some(TextColor::from_rgb(color));
+ }
+ }
+ }
+ }
}
#[cfg(test)]
@@ -422,4 +464,20 @@ mod tests {
let ansi_difference = style_a.compare_ansi(&style_b);
assert_eq!(ansi_difference, Ansi::ITALIC)
}
+
+ #[test]
+ fn test_from_code() {
+ assert_eq!(
+ ChatFormatting::from_code('a').unwrap(),
+ &ChatFormatting::GREEN
+ );
+ }
+
+ #[test]
+ fn test_apply_formatting() {
+ let mut style = Style::default();
+ style.apply_formatting(&ChatFormatting::BOLD);
+ style.apply_formatting(&ChatFormatting::RED);
+ assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
+ }
}
diff --git a/minecraft-chat/src/text_component.rs b/minecraft-chat/src/text_component.rs
index 9bca1fa0..66bde690 100644
--- a/minecraft-chat/src/text_component.rs
+++ b/minecraft-chat/src/text_component.rs
@@ -1,4 +1,4 @@
-use crate::base_component::BaseComponent;
+use crate::{base_component::BaseComponent, component::Component, style::ChatFormatting};
#[derive(Clone, Debug)]
pub struct TextComponent {
@@ -6,6 +6,63 @@ pub struct TextComponent {
pub text: String,
}
+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 {
+ 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
+
+ // we don't use a normal for loop since we need to be able to skip after reading the formatter code symbol
+ let mut i = 0;
+ while i < legacy_color_code.chars().count() {
+ 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() {
+ 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
+ );
+ }
+ i += 1;
+ } else {
+ if components.is_empty() {
+ components.push(TextComponent::new("".to_string()));
+ }
+ components
+ .last_mut()
+ .unwrap()
+ .text
+ .push(legacy_color_code.chars().nth(i).unwrap());
+ };
+ i += 1;
+ }
+
+ // create the final component by using the first one as the base, and then adding the rest as siblings
+ let mut final_component = components.remove(0);
+ for component in components {
+ final_component.base.siblings.push(component.get());
+ }
+
+ final_component.get()
+}
+
impl<'a> TextComponent {
pub fn new(text: String) -> Self {
Self {
@@ -17,4 +74,22 @@ impl<'a> TextComponent {
pub fn to_string(&self) -> String {
self.text.clone()
}
+
+ fn get(self) -> Component {
+ Component::Text(self)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_legacy_color_code_to_component() {
+ let component = legacy_color_code_to_component("§lHello §r§1w§2o§3r§4l§5d");
+ assert_eq!(
+ component.to_ansi(),
+ "\u{1b}[38;2;170;0;170mHello world\u{1b}[m"
+ );
+ }
}