vintage_schematics/convert/
entities.rs

1//! [Block entity](https://minecraft.wiki/w/Block_entity_format) conversion logic.
2
3use std::sync::LazyLock;
4
5use color_eyre::{
6	eyre,
7	eyre::{bail, eyre},
8};
9
10use crate::{Regex, formats::common::MinecraftBlockEntity};
11
12/// Regex pattern for old (pre-1.21) Minecraft sign text values.
13/// Used in [`Sign::try_from`].
14static OLD_SIGN_TEXT_PATTERN: LazyLock<Regex> =
15	LazyLock::new(|| Regex::new(r#"(?i)^\{\s*"?text"?\s*:"(?P<text>.+)"\s*}\s*$"#).unwrap());
16
17/// A Minecraft sign.
18pub struct Sign<'a> {
19	/// Each line of the sign's front text, ordered from top to bottom.
20	pub messages: Vec<&'a str>,
21
22	/// The colour of the sign's text.
23	pub colour: Option<&'a str>,
24}
25
26impl Sign<'_> {
27	/// The text of the sign, formatted for Vintage Story.
28	#[must_use]
29	pub fn text(&self) -> String {
30		// leading newline because VS signs are taller than MC signs
31		String::from("\n") + &*self.messages.join("\n")
32	}
33
34	/// The colour of the sign's text as (`R`, `G`, `B`).
35	/// Based on Vintage Story's built-in pigments.
36	#[must_use]
37	pub fn colour(&self) -> (u8, u8, u8) {
38		// colours taken from VS assets/survival/itemtypes/resource:
39		// - charcoal.json: black
40		// - ore-ungraded.json (lapis lazuli, cinnabar): blue, red
41		// - nugget.json (malachite): green
42		// - stone.json (chalk, limestone): white, light grey
43		// - coke.json: not used
44
45		// to find these colours yourself, go to the resource directory and run:
46		// `rg -UP '^( |\t+)pigment(ByType)?:(?s:.)+?\n\1\}'`
47		// - ripgrep: https://github.com/BurntSushi/ripgrep
48		// - `-U` for multiline matches
49		// - `-P` for PCRE2
50		// - matches:
51		//   - leading indentation
52		//   - `pigment` or `pigmentByType`, followed by a colon
53		//   - the colour data
54		//   - newline, then trailing indentation of the same length, then a closing brace
55
56		match self.colour {
57			Some("white") => (237, 237, 237),
58			Some("light_gray" | "gray") => (221, 216, 199),
59			Some("green" | "lime" | "yellow") => (112, 154, 108),
60			Some("blue" | "light_blue" | "cyan") => (74, 113, 176),
61			Some("red" | "purple" | "pink") => (192, 66, 49),
62			_ => (25, 24, 22),
63		}
64	}
65}
66
67impl<'a> TryFrom<&'a MinecraftBlockEntity> for Sign<'a> {
68	type Error = eyre::Error;
69
70	fn try_from(value: &'a MinecraftBlockEntity) -> Result<Self, Self::Error> {
71		if value.id != "minecraft:sign" {
72			bail!("invalid sign: not a sign block")
73		}
74
75		if let Some(mininbt::Value::Compound(front_text)) = value.components.get("front_text") {
76			// new format: "front_text" with "messages" list
77			let messages = if let Some(mininbt::Value::List(messages)) = front_text.get("messages") {
78				messages
79					.iter()
80					.map(|message| message.as_str().unwrap_or_default().trim_matches('"'))
81					.collect::<Vec<_>>()
82			} else {
83				bail!("invalid sign: front_text.messages is missing or invalid");
84			};
85
86			Ok(Self {
87				messages,
88				colour: front_text.get("color").and_then(|c| c.as_str()),
89			})
90		} else {
91			// old format: Text1, Text2, etc
92			let messages = (1..=4)
93				.map(|i| {
94					value
95						.components
96						.get(&format!("Text{i}"))
97						.and_then(|v| v.as_str())
98						.and_then(|v| OLD_SIGN_TEXT_PATTERN.captures(v))
99						.and_then(|c| c.name("text").map(|m| m.as_str()))
100				})
101				.collect::<Option<Vec<_>>>()
102				.ok_or_else(|| eyre!("invalid sign: no front_text or Text1"))?;
103
104			Ok(Self {
105				messages,
106				colour: value.components.get("Color").and_then(|v| v.as_str()),
107			})
108		}
109	}
110}