Skip to main content

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::{SIMPLE_CACHE, 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			if let Ok(mut lock) = SIMPLE_CACHE.lock() {
103				lock.clear();
104			}
105
106			for block in block_codes {
107				let (code, properties) = match convert_block(block, settings) {
108					Some(result) => result,
109					None if settings.replace_missing => (String::from("game:leaves-placed-birch"), None),
110					None => (String::from("game:air"), None), // TODO: can i just continue?
111				};
112
113				#[allow(clippy::option_if_let_else)]
114				let properties = if let Some(mut properties) = properties {
115					// check if these properties already exist
116					let existing = new_properties.iter().position(|p| *p == properties);
117
118					Some(
119						if let Some(existing) = existing
120							&& !properties.contains_key("unique!")
121						{
122							// reuse existing index
123							existing
124						} else {
125							if code == VS_FLOWER_POT_CODE
126								&& let Some(Value::String(flower)) = properties.remove("flower")
127							{
128								Self::convert_potted_flower(block_codes, &new_blocks, &mut extra_blocks, &mut properties, flower);
129							}
130							// insert new properties and return index
131							new_properties.push(properties);
132							new_properties.len() - 1
133						},
134					)
135				} else {
136					None
137				};
138
139				let properties = properties.map(|p| vec![p]).unwrap_or_default();
140				let block = VintageStoryBlockCode { name: code, properties };
141				new_blocks.push(block);
142			}
143
144			new_blocks.extend(extra_blocks.into_iter().map(|name| VintageStoryBlockCode {
145				name,
146				properties: Vec::new(),
147			}));
148			(new_blocks, new_properties)
149		} else {
150			// already in vintage story format
151			return;
152		};
153
154		self.convert_tile_entities(&mut new_blocks, &mut new_properties);
155
156		self.block_codes = BlockCodes::VintageStory {
157			codes: new_blocks,
158			properties: new_properties,
159		};
160	}
161
162	fn convert_potted_flower(
163		block_codes: &[MinecraftBlockCode],
164		new_blocks: &[VintageStoryBlockCode],
165		extra_blocks: &mut Vec<String>,
166		properties: &mut EntityMap,
167		flower: String,
168	) {
169		// special case: potted flower
170		// we need to update the properties to reference the flower's ID.
171		// this would be doable from inside `convert_block`, if the flower already exists in the block_code
172		// set, but we also need to handle the case where it isn't (e.g. the schematic contains a potted
173		// dandelion, but no "raw" dandelion).
174		#[allow(clippy::option_if_let_else)]
175		let flower_id = if let Some(id) = new_blocks.iter().position(|b: &VintageStoryBlockCode| b.name == flower) {
176			id
177		} else if let Some(id) = extra_blocks.iter().position(|e| e == &flower) {
178			// we've already added a potted flower with this code.
179			// extra_blocks get added to the end of new_blocks at the end of the loop.
180			// before adding the extra_blocks in, new_blocks will be the same length as block_codes.
181			// therefore, the position of this flower in new_blocks will be block_codes.len() + id.
182			block_codes.len() + id
183		} else {
184			// add the flower to the extra blocks set and return its ID using the logic outlined above.
185			// it's as shrimple as that.
186			extra_blocks.push(flower);
187			block_codes.len() + extra_blocks.len() - 1
188		};
189
190		// generate flower pot properties
191		let item_stack = ItemStack {
192			class: ItemClass::Block,
193			id: i32::try_from(flower_id).unwrap_or_default(),
194			stack_size: 1,
195			stack_attributes: Map::new(),
196		};
197
198		properties.insert(String::from("meshAngle"), Value::Float(0.0));
199		properties.insert(
200			String::from("inventory"),
201			Value::Tree(Map::from([
202				(String::from("qslots"), Value::Integer(1)),
203				(
204					String::from("slots"),
205					Value::Tree(Map::from([(String::from("0"), Value::ItemStack(Some(item_stack)))])),
206				),
207			])),
208		);
209	}
210
211	fn convert_tile_entities(&self, new_blocks: &mut [VintageStoryBlockCode], new_properties: &mut Vec<EntityMap>) {
212		// add minecraft sign properties to VS signs
213		for tile_entity in &self.tile_entities {
214			// tee hee
215			if let Ok(sign) = Sign::try_from(tile_entity)
216				&& let Some(block) = self.blocks.iter().find(|block| block.position == tile_entity.position())
217				&& let Some(block) = new_blocks.get_mut(block.id)
218				&& let Some(properties) = block.properties.first()
219				&& let Some(properties) = new_properties.get_mut(*properties)
220			{
221				let (r, g, b) = sign.colour();
222				// stored as ARGB
223				// https://github.com/anegostudios/vssurvivalmod/blob/75ece46/BlockEntityRenderer/SignRenderer.cs#L174
224				// https://github.com/anegostudios/vssurvivalmod/blob/75ece46/BlockEntity/BESign.cs#L123
225				let colour = i32::from_be_bytes([0xFF, r, g, b]);
226
227				let properties = if properties.contains_key("unique!") {
228					properties
229				} else {
230					// create a new set of sign properties
231					// if we don't do this, all signs will share the same set of properties :(
232					let properties = properties.clone();
233					block.properties.push(new_properties.len());
234					new_properties.push(properties);
235					let Some(properties) = new_properties.last_mut() else {
236						continue; // impossible
237					};
238					properties
239				};
240
241				properties.extend([
242					(String::from("color"), Value::Integer(colour)),
243					(String::from("fontSize"), Value::Float(16.0)),
244					(String::from("text"), Value::String(sign.text())),
245					// specify position now so that WorldEdit::save knows the sign properties belong to specific blocks (as
246					// opposed to being shared between all sign blocks)
247					(String::from("posx"), Value::Integer(tile_entity.x)),
248					(String::from("posy"), Value::Integer(tile_entity.y)),
249					(String::from("posz"), Value::Integer(tile_entity.z)),
250				]);
251				properties.remove("unique!");
252			}
253		}
254	}
255}