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
22/// Vintage Story block properties.
23pub type EntityMap = Map<String, Value>;
24
25/// Internal representation of a schematic.
26///
27/// Can be converted to a Vintage Story-compatible format with the [`Internal::convert_to_vintage_story`] method.
28#[derive(Debug, Clone, Serialize)]
29pub struct Internal {
30	/// The "palette" of block codes and their properties.
31	pub block_codes: BlockCodes,
32
33	/// Individual blocks.
34	pub blocks: Vec<Block>,
35
36	/// Minecraft tile entities.
37	pub tile_entities: Vec<MinecraftBlockEntity>,
38
39	/// The size of this schematic in blocks.
40	pub size: Xyz,
41}
42
43/// Block codes and their properties.
44#[derive(Debug, Clone, Serialize)]
45pub enum BlockCodes {
46	VintageStory {
47		codes: Vec<VintageStoryBlockCode>,
48		properties: Vec<EntityMap>,
49	},
50	Minecraft(Vec<MinecraftBlockCode>),
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
54#[serde(rename_all = "PascalCase")]
55pub struct MinecraftBlockCode {
56	pub name: String,
57	#[serde(default)]
58	pub properties: PropertyMap,
59}
60
61#[derive(Debug, Clone, Serialize)]
62pub struct VintageStoryBlockCode {
63	/// This block's code.
64	///
65	/// <https://wiki.vintagestory.at/Block_codes>
66	pub name: String,
67
68	/// Index into the `properties` vector of `BlockCodes::VintageStory`.
69	pub properties: Option<usize>,
70}
71
72#[derive(Debug, Clone, Serialize)]
73pub struct Block {
74	pub id: usize,
75	pub position: Xyz,
76}
77
78impl Internal {
79	/// Converts this schematic to the Vintage Story format.
80	pub fn convert_to_vintage_story(&mut self, settings: &Settings) {
81		let (new_blocks, mut new_properties) = if let BlockCodes::Minecraft(block_codes) = &self.block_codes {
82			let mut new_blocks = Vec::with_capacity(block_codes.len());
83			let mut new_properties = Vec::new();
84			let mut extra_blocks = Vec::new();
85
86			for block in block_codes {
87				// check if the block is a flower pot, and if so:
88				// - extract the flower it's supposed to be potting
89				// - add that flower to the block codes set if it's not already present
90				// - ???
91				let (code, properties) = match convert_block(block, settings) {
92					Some(result) => result,
93					None if settings.replace_missing => (String::from("game:leaves-placed-birch"), None),
94					None => (String::from("game:air"), None), // TODO: can i just continue?
95				};
96
97				#[allow(clippy::option_if_let_else)]
98				let properties = if let Some(mut properties) = properties {
99					// check if these properties already exist
100					let existing = new_properties.iter().position(|p| *p == properties);
101
102					Some(
103						if let Some(existing) = existing
104							&& !properties.contains_key("unique!")
105						{
106							// reuse existing index
107							existing
108						} else {
109							if code == VS_FLOWER_POT_CODE
110								&& let Some(Value::String(flower)) = properties.remove("flower")
111							{
112								// special case: potted flower
113								// we need to update the properties to reference the flower's ID.
114								// this would be doable from inside `convert_block`, if the flower already exists in the block_code
115								// set, but we also need to handle the case where it isn't (e.g. the schematic contains a potted
116								// dandelion, but no "raw" dandelion).
117								let flower_id =
118									if let Some(id) = new_blocks.iter().position(|b: &VintageStoryBlockCode| b.name == flower) {
119										id
120									} else if let Some(id) = extra_blocks.iter().position(|e| e == &flower) {
121										// we've already added a potted flower with this code.
122										// extra_blocks get added to the end of new_blocks at the end of the loop.
123										// before adding the extra_blocks in, new_blocks will be the same length as block_codes.
124										// therefore, the position of this flower in new_blocks will be block_codes.len() + id.
125										block_codes.len() + id
126									} else {
127										// add the flower to the extra blocks set and return its ID using the logic outlined above.
128										// it's as shrimple as that.
129										extra_blocks.push(flower);
130										block_codes.len() + extra_blocks.len() - 1
131									};
132
133								// generate flower pot properties
134								let item_stack = ItemStack {
135									class: ItemClass::Block,
136									id: i32::try_from(flower_id).unwrap_or_default(),
137									stack_size: 1,
138									stack_attributes: Map::new(),
139								};
140
141								properties.insert(String::from("meshAngle"), Value::Float(0.0));
142								properties.insert(
143									String::from("inventory"),
144									Value::Tree(Map::from([
145										(String::from("qslots"), Value::Integer(1)),
146										(
147											String::from("slots"),
148											Value::Tree(Map::from([(String::from("0"), Value::ItemStack(Some(item_stack)))])),
149										),
150									])),
151								);
152							}
153							// insert new properties and return index
154							new_properties.push(properties);
155							new_properties.len() - 1
156						},
157					)
158				} else {
159					None
160				};
161
162				let block = VintageStoryBlockCode { name: code, properties };
163				new_blocks.push(block);
164			}
165
166			new_blocks.extend(extra_blocks.into_iter().map(|name| VintageStoryBlockCode { name, properties: None }));
167			(new_blocks, new_properties)
168		} else {
169			// already in vintage story format
170			return;
171		};
172
173		// add minecraft sign properties to VS signs
174		for tile_entity in &self.tile_entities {
175			// tee hee
176			if let Ok(sign) = Sign::try_from(tile_entity)
177				&& let Some(block) = self.blocks.iter().find(|block| block.position == tile_entity.position())
178				&& let Some(block) = new_blocks.get(block.id)
179				&& let Some(properties) = block.properties
180				&& let Some(properties) = new_properties.get_mut(properties)
181			{
182				let (r, g, b) = sign.colour();
183				// stored as ARGB
184				// https://github.com/anegostudios/vsapi/blob/f0d94e1/Math/ColorUtil.cs
185				let colour = i32::from_be_bytes([0xFF, r, g, b]);
186
187				properties.extend([
188					(String::from("color"), Value::Integer(colour)),
189					(String::from("fontSize"), Value::Float(16.0)),
190					(String::from("text"), Value::String(sign.text())),
191				]);
192				properties.remove("unique!");
193			}
194		}
195
196		self.block_codes = BlockCodes::VintageStory {
197			codes: new_blocks,
198			properties: new_properties,
199		};
200	}
201}