Skip to main content

vintage_schematics/formats/
sponge.rs

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