vintage_schematics/formats/
sponge.rs

1//! Sponge schematic format support.
2
3use std::io::Read;
4
5use color_eyre::{
6	Help, SectionExt, eyre,
7	eyre::{Context, OptionExt},
8};
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11use tracing::warn;
12
13use crate::{
14	Map,
15	entity::parse_seven_bit_int,
16	formats::{
17		Loadable, ToInternal,
18		common::{FLATTENING_VERSION, MODERNISED_BLOCKS, MinecraftBlockData, MinecraftBlockEntity, Size},
19		internal::{Block, BlockCodes, Internal, MinecraftBlockCode},
20		read, should_reject,
21	},
22};
23
24/// Sponge schematic format.
25///
26/// <https://github.com/SpongePowered/Schematic-Specification/blob/master/versions/schematic-3.md>
27// we're ignoring a lot of fields here:
28// - metadata: contains author name, creation date, etc. we don't care
29// - data version: the version of minecraft this schematic was created from. this might end up being relevant later!
30// - offset: [x, y, z] ints to add/subtract from the "paste" location. vintage story has no equivalent concept afaict
31// - biomes: don't care
32// - entities: mobs and such. don't care
33#[derive(Debug, Clone, Deserialize, Serialize)]
34#[serde(rename_all = "PascalCase")]
35pub struct Sponge {
36	/// Sponge schematic version.
37	/// Only version 3 is currently supported.
38	version: i32,
39
40	/// Schematic size.
41	#[serde(flatten)]
42	size: Size,
43
44	/// Block data.
45	blocks: SpongeBlocks,
46
47	/// The version of Minecraft this schematic was created from.
48	data_version: i32,
49}
50
51/// Sponge schematic block data.
52#[derive(Debug, Clone, Deserialize, Serialize)]
53#[serde(rename_all = "PascalCase")]
54pub struct SpongeBlocks {
55	/// Palette mapping of block IDs and properties to indices.
56	///
57	/// Block properties are encoded as a square-bracketed list of comma-separated `key=value` pairs.
58	/// For example, a wheat block with an `age` of `3` is represented as `minecraft:wheat[age=3]`.
59	palette: IndexMap<String, i32>,
60
61	/// Indices into the palette encoded in the
62	/// ["varint" format](https://minecraft.wiki/w/Java_Edition_protocol/VarInt_and_VarLong) (aka
63	/// [LEB128](https://en.wikipedia.org/wiki/LEB128)).
64	///
65	/// Entries are indexed by `x + z * Width + y * Width * Length`.
66	data: Vec<u8>,
67
68	/// Sponge block entities.
69	#[serde(default)]
70	entities: Vec<SpongeBlockEntity>,
71}
72
73#[derive(Debug, Clone, Deserialize, Serialize)]
74#[serde(rename_all = "PascalCase")]
75pub struct SpongeBlockEntity {
76	// we don't care about the ID because it's duplicated in the `data` field
77	// id: String,
78	/// This tile entity's position.
79	pos: (i32, i32, i32),
80
81	/// Associated entity data.
82	data: MinecraftBlockData,
83}
84
85/// Wrapper for the Sponge schematic format.
86///
87/// Sponge files are saved as a [compound](mininbt::Compound) with a single field named `Schematic`.
88#[derive(Debug, Clone, Deserialize)]
89#[serde(rename_all = "PascalCase")]
90pub struct SpongeWrapper {
91	/// The actual schematic data.
92	schematic: Sponge,
93}
94
95impl Loadable for Sponge {
96	fn load(reader: impl Read, compressed: bool) -> eyre::Result<Self>
97	where
98		Self: Sized,
99	{
100		let schematic: SpongeWrapper = read(reader, compressed).context("failed to load schematic")?;
101		let mut schematic = schematic.schematic;
102
103		if schematic.version != 3 {
104			warn!("unsupported schematic version: {}", schematic.version);
105		}
106
107		schematic.modernise();
108
109		Ok(schematic)
110	}
111
112	fn modernise(&mut self) {
113		let replace = |current: &String| {
114			if let Some((current, properties)) = current.split_once('[') {
115				if let Some((_, new)) = MODERNISED_BLOCKS.iter().find(|(old, _)| *old == current) {
116					Some((String::from(current), format!("{new}[{properties}")))
117				} else {
118					None
119				}
120			} else if let Some((_, new)) = MODERNISED_BLOCKS.iter().find(|(old, _)| *old == current) {
121				Some((current.clone(), new.to_string()))
122			} else {
123				None
124			}
125		};
126
127		if self.data_version < FLATTENING_VERSION {
128			let replacements: Vec<_> = self.blocks.palette.keys().filter_map(replace).collect();
129			for (old, new) in replacements {
130				if let Some(position) = self.blocks.palette.get_index_of(&old) {
131					let id = self.blocks.palette.shift_remove(&old).expect("replacement should exist");
132					self.blocks.palette.shift_insert(position, new, id);
133				}
134			}
135		}
136	}
137}
138
139impl ToInternal for Sponge {
140	fn to_internal(self) -> eyre::Result<Internal> {
141		// iterator over encoded block IDs
142		let mut block_bytes = self.blocks.data.into_iter();
143
144		// - new block IDs, where `None` means the block should be rejected
145		// - converted block codes
146		let (ids, block_codes): (Vec<Option<u16>>, BlockCodes) = {
147			let mut ids = Vec::with_capacity(self.blocks.palette.len());
148			let mut block_codes = Vec::with_capacity(self.blocks.palette.len());
149
150			let mut current_id = 0;
151			for (name, _) in self.blocks.palette {
152				let code = parse_sponge_name(&name).context("failed to parse block code")?;
153
154				if should_reject(&code) {
155					ids.push(None);
156				} else {
157					ids.push(Some(current_id));
158					block_codes.push(code);
159					current_id += 1;
160				}
161			}
162
163			(ids, BlockCodes::Minecraft(block_codes))
164		};
165
166		// size of the schematic in blocks
167		let size = self.size.blocks();
168
169		// internal blocks
170		#[allow(clippy::cast_sign_loss)]
171		let mut blocks = Vec::with_capacity(size as usize);
172
173		for i in 0..size {
174			// TODO: ensure we haven't run out of bytes?
175
176			// get "old" block id
177			#[allow(clippy::cast_sign_loss)]
178			let block = parse_seven_bit_int(&mut block_bytes)? as usize;
179
180			// look up the new ID, continue if the block was rejected
181			let Some(id) = ids[block] else { continue };
182			let id = id as usize;
183
184			let position = self.size.index_position(i);
185
186			blocks.push(Block { id, position });
187		}
188
189		// convert tile entities
190		let tile_entities = self
191			.blocks
192			.entities
193			.into_iter()
194			.map(|e| MinecraftBlockEntity {
195				id: e.data.id,
196				x: e.pos.0,
197				y: e.pos.1,
198				z: e.pos.2,
199				components: e.data.components,
200			})
201			.collect();
202
203		Ok(Internal {
204			block_codes,
205			blocks,
206			tile_entities,
207			size: self.size.into(),
208		})
209	}
210}
211
212fn parse_sponge_name(name: &str) -> eyre::Result<MinecraftBlockCode> {
213	let mut new_properties = Map::new();
214
215	let name = if let Some((name, properties)) = name.split_once('[') {
216		// `minecraft:block[key=value]`
217		let properties = properties
218			.strip_suffix(']')
219			.ok_or_eyre("malformed block properties: missing terminating bracket")
220			.with_section(|| properties.to_string().header("Properties:"))?;
221
222		// add properties to map
223		for pair in properties.split(',') {
224			let (key, value) = pair
225				.split_once('=')
226				.ok_or_eyre("malformed block properties: missing equals delimiter")
227				.with_section(|| pair.to_string().header("Property:"))?;
228			new_properties.insert(key.to_string(), value.to_string());
229		}
230
231		name
232	} else {
233		// plain old `minecraft:block`
234		name
235	};
236
237	Ok(MinecraftBlockCode {
238		name: name.to_string(),
239		properties: new_properties,
240	})
241}