From 61d456846aee3beaad5a77fb0f9fbf5da351eeab Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Sat, 25 Nov 2023 12:08:52 +0100 Subject: [PATCH] world: implement decoding raw JSON text into a linear list of formatted strings --- src/world/json_text.rs | 150 ++++++++++++++++++++++++++++++++++++++++- src/world/mod.rs | 1 + src/world/sign.rs | 88 ++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 src/world/sign.rs diff --git a/src/world/json_text.rs b/src/world/json_text.rs index 561725f..8e53ead 100644 --- a/src/world/json_text.rs +++ b/src/world/json_text.rs @@ -1,7 +1,155 @@ //! Newtype and helper methods for handling Minecraft Raw JSON Text +use std::{collections::VecDeque, sync::Arc}; + use serde::Deserialize; +/// A span of formatted text +/// +/// A [JSONText] consists of a tree of [FormattedText] nodes (canonically +/// represented as a [FormattedTextTree], but other kinds are possible with +/// is handled by [DeserializedText]. +/// +/// Formatting that is not set in a node is inherited from the parent. +#[derive(Debug, Deserialize, Default)] +pub struct FormattedText { + #[serde(default)] + /// Text content + pub text: String, + /// Text color + pub color: Option>, + /// Bold formatting + pub bold: Option, + /// Italic formatting + pub italic: Option, + /// Underlines formatting + pub underlined: Option, + /// Strikethrough formatting + pub strikethrough: Option, + /// Obfuscated formatting + pub obfuscated: Option, +} + +impl FormattedText { + /// Fills in unset formatting fields from a parent node + pub fn inherit(self, parent: &Self) -> Self { + FormattedText { + text: self.text, + color: self.color.or_else(|| parent.color.clone()), + bold: self.bold.or(parent.bold), + italic: self.italic.or(parent.italic), + underlined: self.underlined.or(parent.underlined), + strikethrough: self.strikethrough.or(parent.strikethrough), + obfuscated: self.obfuscated.or(parent.obfuscated), + } + } +} + +/// A tree of [FormattedText] nodes +/// +/// Each node including the root has a `text` and a list of children (`extra`). +#[derive(Debug, Deserialize, Default)] +pub struct FormattedTextTree { + /// Root node content + #[serde(flatten)] + text: FormattedText, + /// List of child trees + #[serde(default)] + extra: VecDeque, +} + +impl From for FormattedTextTree { + fn from(value: String) -> Self { + FormattedTextTree { + text: FormattedText { + text: value, + ..Default::default() + }, + extra: VecDeque::new(), + } + } +} + +/// List of [FormattedText] +#[derive(Debug)] +pub struct FormattedTextList(pub Vec); + +impl FormattedTextList { + /// Returns `true` when [FormattedTextList] does not contain any text + pub fn is_empty(&self) -> bool { + self.0.iter().all(|text| text.text.is_empty()) + } +} + +/// Raw deserialized [JSONText] +/// +/// A [JSONText] can contain various different JSON types. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum DeserializedText { + /// Unformatted string + String(String), + /// Unformatted number (will be converted to a string) + Number(f32), + /// Unformatted boolean (will be converted to a string) + Boolean(bool), + /// List of [DeserializedText] + /// + /// The tail elements are appended as children of the head element. + List(VecDeque), + /// The canonical [FormattedTextTree] structure + Object(FormattedTextTree), +} + +impl DeserializedText { + /// Converts a [DeserializedText] into the regular [FormattedTextTree] format + /// + /// Most variants are simply converted to strings. A list is handled by + /// appending all tail elements to the `extra` field of the head. + pub fn canonicalize(self) -> FormattedTextTree { + match self { + DeserializedText::Object(obj) => obj, + DeserializedText::String(s) => FormattedTextTree::from(s), + DeserializedText::Number(n) => FormattedTextTree::from(n.to_string()), + DeserializedText::Boolean(b) => FormattedTextTree::from(b.to_string()), + DeserializedText::List(mut list) => { + let mut obj = list + .pop_front() + .map(|t| t.canonicalize()) + .unwrap_or_default(); + obj.extra.append(&mut list); + obj + } + } + } + + /// Converts the tree of [FormattedText] nodes into a linear list by + /// copying formatting flags into each node. + pub fn linearize(self, parent: &FormattedText) -> FormattedTextList { + let obj = self.canonicalize(); + let mut ret = vec![obj.text.inherit(parent)]; + + for extra in obj.extra { + ret.append(&mut extra.linearize(&ret[0]).0); + } + + FormattedTextList(ret) + } +} + +impl Default for DeserializedText { + fn default() -> Self { + DeserializedText::Object(FormattedTextTree::from(String::new())) + } +} + /// Minecraft Raw JSON Text #[derive(Debug, Deserialize)] -pub struct JSONText(String); +pub struct JSONText(pub String); + +impl JSONText { + /// Deserializes a [JSONText] into a [DeserializedText] + pub fn deserialize(&self) -> DeserializedText { + serde_json::from_str(&self.0).unwrap_or_default() + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs index e3c52ba..38d5174 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -5,3 +5,4 @@ pub mod de; pub mod json_text; pub mod layer; pub mod section; +pub mod sign; diff --git a/src/world/sign.rs b/src/world/sign.rs new file mode 100644 index 0000000..c13eebe --- /dev/null +++ b/src/world/sign.rs @@ -0,0 +1,88 @@ +//! Processing of sign text + +use std::sync::Arc; + +use super::{ + de, + json_text::{FormattedText, FormattedTextList, JSONText}, +}; + +/// Version-independent reference to (front or back) sign text +#[derive(Debug, Default)] +pub struct RawSignText<'a> { + /// Lines of sign text + /// + /// A regular sign always has 4 lines of text. The back of pre-1.20 + /// signs is represented as a [SignText] without any `messages`. + pub messages: Vec<&'a JSONText>, + /// Sign color + /// + /// Defaults to "black". + pub color: Option<&'a str>, +} + +impl<'a> RawSignText<'a> { + /// Decodes the [RawSignText] into a [SignText] + pub fn decode(&self) -> SignText { + let color = self.color.map(|c| Arc::new(c.to_owned())); + let parent = FormattedText { + color, + ..Default::default() + }; + SignText( + self.messages + .iter() + .map(|message| message.deserialize().linearize(&parent)) + .collect(), + ) + } +} + +impl<'a> From<&'a de::BlockEntitySignV1_20Text> for RawSignText<'a> { + fn from(value: &'a de::BlockEntitySignV1_20Text) -> Self { + RawSignText { + messages: value.messages.iter().collect(), + color: value.color.as_deref(), + } + } +} + +/// Helper methods for [de::BlockEntitySign] +pub trait BlockEntitySignExt { + /// Returns the front and back text of a sign in a version-indepentent format + fn text(&self) -> (RawSignText, RawSignText); +} + +impl BlockEntitySignExt for de::BlockEntitySign { + fn text(&self) -> (RawSignText, RawSignText) { + match self { + de::BlockEntitySign::V0 { + text1, + text2, + text3, + text4, + color, + } => ( + RawSignText { + messages: vec![text1, text2, text3, text4], + color: color.as_deref(), + }, + Default::default(), + ), + de::BlockEntitySign::V1_20 { + front_text, + back_text, + } => (front_text.into(), back_text.into()), + } + } +} + +/// Deserialized and linearized sign text +pub struct SignText(pub Vec); + +impl SignText { + /// Checks if all lines of the sign text are empty + pub fn is_empty(&self) -> bool { + self.0.iter().all(|line| line.is_empty()) + } +}