Skip to main content

vintage_schematics/convert/
entities.rs

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