vintage_schematics/formats/
internal.rs

1//! Internal format used as an intermediate step between the various Minecraft formats and Vintage Story's
2//! [`WorldEdit`](crate::formats::world_edit::WorldEdit) format.
3
4use serde::{Deserialize, Serialize};
5
6use crate::{
7	Map,
8	convert::{
9		block::{VS_FLOWER_POT_CODE, convert_block},
10		entities::Sign,
11	},
12	entity::{ItemClass, ItemStack, Value},
13	formats::{
14		Settings,
15		common::{MinecraftBlockEntity, Xyz},
16	},
17};
18
19/// Minecraft block properties.
20pub type PropertyMap = Map<String, String>;
21
22pub trait PropertyMapExt {
23	fn get_facing_or_north(&self) -> &str;
24
25	fn get_u8(&self, key: &str) -> Option<u8>;
26
27	fn is_true(&self, key: &str) -> bool;
28}
29
30impl PropertyMapExt for PropertyMap {
31	fn get_facing_or_north(&self) -> &str { self.get("facing").map_or("north", String::as_str) }
32
33	fn get_u8(&self, key: &str) -> Option<u8> { self.get(key).and_then(|v| v.parse().ok()) }
34
35	fn is_true(&self, key: &str) -> bool { self.get(key).is_some_and(|v| v == "true") }
36}
37
38/// Vintage Story block properties.
39pub type EntityMap = Map<String, Value>;
40
41/// Internal representation of a schematic.
42///
43/// Can be converted to a Vintage Story-compatible format with the [`Internal::convert_to_vintage_story`] method.
44#[derive(Debug, Clone, Serialize)]
45pub struct Internal {
46	/// The "palette" of block codes and their properties.
47	pub block_codes: BlockCodes,
48
49	/// Individual blocks.
50	pub blocks: Vec<Block>,
51
52	/// Minecraft tile entities.
53	pub tile_entities: Vec<MinecraftBlockEntity>,
54
55	/// The size of this schematic in blocks.
56	pub size: Xyz,
57}
58
59/// Block codes and their properties.
60#[derive(Debug, Clone, Serialize)]
61pub enum BlockCodes {
62	VintageStory {
63		codes: Vec<VintageStoryBlockCode>,
64		properties: Vec<EntityMap>,
65	},
66	Minecraft(Vec<MinecraftBlockCode>),
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70#[serde(rename_all = "PascalCase")]
71pub struct MinecraftBlockCode {
72	pub name: String,
73	#[serde(default)]
74	pub properties: PropertyMap,
75}
76
77#[derive(Debug, Clone, Serialize)]
78pub struct VintageStoryBlockCode {
79	/// This block's code.
80	///
81	/// <https://wiki.vintagestory.at/Block_codes>
82	pub name: String,
83
84	/// Indeces into the `properties` vector of `BlockCodes::VintageStory`.
85	pub properties: Vec<usize>,
86}
87
88#[derive(Debug, Clone, Serialize)]
89pub struct Block {
90	pub id: usize,
91	pub position: Xyz,
92}
93
94impl Internal {
95	/// Converts this schematic to the Vintage Story format.
96	pub fn convert_to_vintage_story(&mut self, settings: &Settings) {
97		let (mut new_blocks, mut new_properties) = if let BlockCodes::Minecraft(block_codes) = &self.block_codes {
98			let mut new_blocks = Vec::with_capacity(block_codes.len());
99			let mut new_properties = Vec::new();
100			let mut extra_blocks = Vec::new();
101
102			for block in block_codes {
103				let (code, properties) = match convert_block(block, settings) {
104					Some(result) => result,
105					None if settings.replace_missing => (String::from("game:leaves-placed-birch"), None),
106					None => (String::from("game:air"), None), // TODO: can i just continue?
107				};
108
109				#[allow(clippy::option_if_let_else)]
110				let properties = if let Some(mut properties) = properties {
111					// check if these properties already exist
112					let existing = new_properties.iter().position(|p| *p == properties);
113
114					Some(
115						if let Some(existing) = existing
116							&& !properties.contains_key("unique!")
117						{
118							// reuse existing index
119							existing
120						} else {
121							if code == VS_FLOWER_POT_CODE
122								&& let Some(Value::String(flower)) = properties.remove("flower")
123							{
124								Self::convert_potted_flower(block_codes, &new_blocks, &mut extra_blocks, &mut properties, flower);
125							}
126							// insert new properties and return index
127							new_properties.push(properties);
128							new_properties.len() - 1
129						},
130					)
131				} else {
132					None
133				};
134
135				let properties = properties.map(|p| vec![p]).unwrap_or_default();
136				let block = VintageStoryBlockCode { name: code, properties };
137				new_blocks.push(block);
138			}
139
140			new_blocks.extend(extra_blocks.into_iter().map(|name| VintageStoryBlockCode {
141				name,
142				properties: Vec::new(),
143			}));
144			(new_blocks, new_properties)
145		} else {
146			// already in vintage story format
147			return;
148		};
149
150		self.convert_tile_entities(&mut new_blocks, &mut new_properties);
151
152		self.block_codes = BlockCodes::VintageStory {
153			codes: new_blocks,
154			properties: new_properties,
155		};
156	}
157
158	fn convert_potted_flower(
159		block_codes: &[MinecraftBlockCode],
160		new_blocks: &[VintageStoryBlockCode],
161		extra_blocks: &mut Vec<String>,
162		properties: &mut EntityMap,
163		flower: String,
164	) {
165		// special case: potted flower
166		// we need to update the properties to reference the flower's ID.
167		// this would be doable from inside `convert_block`, if the flower already exists in the block_code
168		// set, but we also need to handle the case where it isn't (e.g. the schematic contains a potted
169		// dandelion, but no "raw" dandelion).
170		#[allow(clippy::option_if_let_else)]
171		let flower_id = if let Some(id) = new_blocks.iter().position(|b: &VintageStoryBlockCode| b.name == flower) {
172			id
173		} else if let Some(id) = extra_blocks.iter().position(|e| e == &flower) {
174			// we've already added a potted flower with this code.
175			// extra_blocks get added to the end of new_blocks at the end of the loop.
176			// before adding the extra_blocks in, new_blocks will be the same length as block_codes.
177			// therefore, the position of this flower in new_blocks will be block_codes.len() + id.
178			block_codes.len() + id
179		} else {
180			// add the flower to the extra blocks set and return its ID using the logic outlined above.
181			// it's as shrimple as that.
182			extra_blocks.push(flower);
183			block_codes.len() + extra_blocks.len() - 1
184		};
185
186		// generate flower pot properties
187		let item_stack = ItemStack {
188			class: ItemClass::Block,
189			id: i32::try_from(flower_id).unwrap_or_default(),
190			stack_size: 1,
191			stack_attributes: Map::new(),
192		};
193
194		properties.insert(String::from("meshAngle"), Value::Float(0.0));
195		properties.insert(
196			String::from("inventory"),
197			Value::Tree(Map::from([
198				(String::from("qslots"), Value::Integer(1)),
199				(
200					String::from("slots"),
201					Value::Tree(Map::from([(String::from("0"), Value::ItemStack(Some(item_stack)))])),
202				),
203			])),
204		);
205	}
206
207	fn convert_tile_entities(&self, new_blocks: &mut [VintageStoryBlockCode], new_properties: &mut Vec<EntityMap>) {
208		// add minecraft sign properties to VS signs
209		for tile_entity in &self.tile_entities {
210			// tee hee
211			if let Ok(sign) = Sign::try_from(tile_entity)
212				&& let Some(block) = self.blocks.iter().find(|block| block.position == tile_entity.position())
213				&& let Some(block) = new_blocks.get_mut(block.id)
214				&& let Some(properties) = block.properties.first()
215				&& let Some(properties) = new_properties.get_mut(*properties)
216			{
217				let (r, g, b) = sign.colour();
218				// stored as ARGB
219				// https://github.com/anegostudios/vssurvivalmod/blob/75ece46/BlockEntityRenderer/SignRenderer.cs#L174
220				// https://github.com/anegostudios/vssurvivalmod/blob/75ece46/BlockEntity/BESign.cs#L123
221				let colour = i32::from_be_bytes([0xFF, r, g, b]);
222
223				let properties = if properties.contains_key("unique!") {
224					properties
225				} else {
226					// create a new set of sign properties
227					// if we don't do this, all signs will share the same set of properties :(
228					let properties = properties.clone();
229					block.properties.push(new_properties.len());
230					new_properties.push(properties);
231					let Some(properties) = new_properties.last_mut() else {
232						continue; // impossible
233					};
234					properties
235				};
236
237				properties.extend([
238					(String::from("color"), Value::Integer(colour)),
239					(String::from("fontSize"), Value::Float(16.0)),
240					(String::from("text"), Value::String(sign.text())),
241					// specify position now so that WorldEdit::save knows the sign properties belong to specific blocks (as
242					// opposed to being shared between all sign blocks)
243					(String::from("posx"), Value::Integer(tile_entity.x)),
244					(String::from("posy"), Value::Integer(tile_entity.y)),
245					(String::from("posz"), Value::Integer(tile_entity.z)),
246				]);
247				properties.remove("unique!");
248			}
249		}
250	}
251}