diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c9847e..aabf1f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## [Unreleased] - ReleaseDate +### Fixed + +- Fix text colors for signs modified using dye +- Fix text colors specified using `#rrggbb` CSS syntax in JSON text + +Only named colors specified via JSON text were working as intended. Dyed signs use different +color names. + +The mapping of color names to values is now handled by the generator. Both the generator and the +viewer must be updated for sign text colors to work. + ## [2.3.0] - 2025-01-02 ### Added diff --git a/Cargo.lock b/Cargo.lock index c18929f..90a9b7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,6 +584,7 @@ dependencies = [ "minedmap-types", "num-integer", "num_cpus", + "phf", "rayon", "regex", "rustc-hash", @@ -717,6 +718,48 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.15" @@ -766,6 +809,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rayon" version = "1.10.0" @@ -923,6 +981,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index e2f36b6..287ac4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ minedmap-resource = { version = "0.5.0", path = "crates/resource" } minedmap-types = { version = "0.1.2", path = "crates/types" } num-integer = "0.1.45" num_cpus = "1.16.0" +phf = { version = "0.11.2", features = ["macros"] } rayon = "1.7.0" regex = "1.10.2" rustc-hash = "2.0.0" diff --git a/src/core/common.rs b/src/core/common.rs index 3dd01cf..be6d28a 100644 --- a/src/core/common.rs +++ b/src/core/common.rs @@ -46,7 +46,7 @@ pub const MIPMAP_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0); /// MinedMap processed entity data version number /// /// Increase when entity collection changes bacause of code changes. -pub const ENTITIES_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(0); +pub const ENTITIES_FILE_META_VERSION: FileMetaVersion = FileMetaVersion(1); /// Coordinate pair of a generated tile /// diff --git a/src/world/json_text.rs b/src/world/json_text.rs index a153179..fa18527 100644 --- a/src/world/json_text.rs +++ b/src/world/json_text.rs @@ -1,7 +1,8 @@ //! Newtype and helper methods for handling Minecraft Raw JSON Text -use std::{collections::VecDeque, fmt::Display, sync::Arc}; +use std::{collections::VecDeque, fmt::Display}; +use minedmap_resource::Color; use serde::{Deserialize, Serialize}; /// A span of formatted text @@ -17,8 +18,8 @@ pub struct FormattedText { /// Text content pub text: String, /// Text color - #[serde(skip_serializing_if = "Option::is_none")] - pub color: Option>, + #[serde(skip_serializing_if = "Option::is_none", with = "json_color")] + pub color: Option, /// Bold formatting #[serde(skip_serializing_if = "Option::is_none")] pub bold: Option, @@ -41,7 +42,7 @@ impl FormattedText { pub fn inherit(self, parent: &Self) -> Self { FormattedText { text: self.text, - color: self.color.or_else(|| parent.color.clone()), + color: self.color.or(parent.color), bold: self.bold.or(parent.bold), italic: self.italic.or(parent.italic), underlined: self.underlined.or(parent.underlined), @@ -175,3 +176,83 @@ impl JSONText { serde_json::from_str(&self.0).unwrap_or_default() } } + +mod json_color { + //! Helpers for serializing and deserializing [FormattedText](super::FormattedText) colors + + use minedmap_resource::Color; + use serde::{ + de::{self, Visitor}, + ser::Error as _, + Deserializer, Serializer, + }; + + /// Named JSON text colors + static COLORS: phf::Map<&'static str, Color> = phf::phf_map! { + "black" => Color([0x00, 0x00, 0x00]), + "dark_blue" => Color([0x00, 0x00, 0xAA]), + "dark_green" => Color([0x00, 0xAA, 0x00]), + "dark_aqua" => Color([0x00, 0xAA, 0xAA]), + "dark_red" => Color([0xAA, 0x00, 0x00]), + "dark_purple" => Color([0xAA, 0x00, 0xAA]), + "gold" => Color([0xFF, 0xAA, 0x00]), + "gray" => Color([0xAA, 0xAA, 0xAA]), + "dark_gray" => Color([0x55, 0x55, 0x55]), + "blue" => Color([0x55, 0x55, 0xFF]), + "green" => Color([0x55, 0xFF, 0x55]), + "aqua" => Color([0x55, 0xFF, 0xFF]), + "red" => Color([0xFF, 0x55, 0x55]), + "light_purple" => Color([0xFF, 0x55, 0xFF]), + "yellow" => Color([0xFF, 0xFF, 0x55]), + "white" => Color([0xFF, 0xFF, 0xFF]), + }; + + /// serde serialize function for [FormattedText::color](super::FormattedText::color) + pub fn serialize(color: &Option, serializer: S) -> Result + where + S: Serializer, + { + let &Some(color) = color else { + return Err(S::Error::custom("serialize called for None sign color")); + }; + + let text = format!("#{:02x}{:02x}{:02x}", color.0[0], color.0[1], color.0[2]); + serializer.serialize_str(&text) + } + + /// serde [Visitor] for use by [deserialize] + struct ColorVisitor; + + impl Visitor<'_> for ColorVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing a color") + } + + fn visit_str(self, color: &str) -> Result + where + E: de::Error, + { + if let Some(hex) = color.strip_prefix("#") { + if let Ok(value) = u32::from_str_radix(hex, 16) { + return Ok(Some(Color([ + (value >> 16) as u8, + (value >> 8) as u8, + value as u8, + ]))); + } + } + + Ok(COLORS.get(color).copied()) + } + } + + /// serde deserialize function for [FormattedText::color](super::FormattedText::color) + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(ColorVisitor) + } +} diff --git a/src/world/sign.rs b/src/world/sign.rs index 57b741a..eff319f 100644 --- a/src/world/sign.rs +++ b/src/world/sign.rs @@ -1,7 +1,8 @@ //! Processing of sign text -use std::{fmt::Display, sync::Arc}; +use std::fmt::Display; +use minedmap_resource::Color; use serde::{Deserialize, Serialize}; use super::{ @@ -23,10 +24,34 @@ pub struct RawSignText<'a> { pub color: Option<&'a str>, } +/// The color to use for signs without a color attribute ("black") +const DEFAULT_COLOR: Color = Color([0, 0, 0]); + +/// Map of text colors associated with dyes (except for black) +static DYE_COLORS: phf::Map<&'static str, Color> = phf::phf_map! { + "white" => Color([255, 255, 255]), + "orange" => Color([255, 104, 31]), + "magenta" => Color([255, 0, 255]), + "light_blue" => Color([154, 192, 205]), + "yellow" => Color([255, 255, 0]), + "lime" => Color([191, 255, 0]), + "pink" => Color([255, 105, 180]), + "gray" => Color([128, 128, 128]), + "light_gray" => Color([211, 211, 211]), + "cyan" => Color([0, 255, 255]), + "purple" => Color([160, 32, 240]), + "blue" => Color([0, 0, 255]), + "brown" => Color([139, 69, 19]), + "green" => Color([0, 255, 0]), + "red" => Color([255, 0, 0]), +}; + impl RawSignText<'_> { /// Decodes the [RawSignText] into a [SignText] pub fn decode(&self) -> SignText { - let color = self.color.map(|c| Arc::new(c.to_owned())); + let color = self + .color + .map(|c| DYE_COLORS.get(c).copied().unwrap_or(DEFAULT_COLOR)); let parent = FormattedText { color, ..Default::default() diff --git a/viewer/MinedMap.js b/viewer/MinedMap.js index e784eec..cfcccf1 100644 --- a/viewer/MinedMap.js +++ b/viewer/MinedMap.js @@ -153,25 +153,6 @@ const parseHash = function () { return args; } -const colors = { - black: '#000000', - dark_blue: '#0000AA', - dark_green: '#00AA00', - dark_aqua: '#00AAAA', - dark_red: '#AA0000', - dark_purple: '#AA00AA', - gold: '#FFAA00', - gray: '#AAAAAA', - dark_gray: '#555555', - blue: '#5555FF', - green: '#55FF55', - aqua: '#55FFFF', - red: '#FF5555', - light_purple: '#FF55FF', - yellow: '#FFFF55', - white: '#FFFFFF', -}; - function formatSignLine(line) { const el = document.createElement('span'); el.style.whiteSpace = 'pre'; @@ -180,7 +161,9 @@ function formatSignLine(line) { const child = document.createElement('span'); child.textContent = span.text; - const color = colors[span.color ?? 'black'] || colors['black']; + let color = span.color ?? ''; + if (color[0] !== '#') + color = '#000000'; if (span.bold) child.style.fontWeight = 'bold';