Skip to main content

vintage_schematics/formats/
litematic.rs

1//! Litematica format support.
2
3use std::io::Read;
4
5use bitvec::{field::BitField, prelude::*};
6use cfg_if::cfg_if;
7use eyre::{Context, OptionExt};
8use formats::should_reject;
9use serde::{Deserialize, Serialize};
10use tracing::warn;
11
12use crate::{
13	Map, formats,
14	formats::{
15		Loadable, ToInternal,
16		common::{FLATTENING_VERSION, MODERNISED_BLOCKS, MinecraftBlockEntity, Xyz},
17		internal,
18		internal::{BlockCodes, Internal, MinecraftBlockCode},
19		read,
20	},
21};
22
23/// Litematica schematic format.
24///
25/// <https://modrinth.com/mod/litematica>
26#[derive(Debug, Clone, Deserialize, Serialize)]
27#[serde(rename_all = "PascalCase")]
28pub struct Litematic {
29	pub regions: Map<String, Region>,
30	pub version: i32,
31	#[serde(default)]
32	pub sub_version: i32,
33	pub minecraft_data_version: i32,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize)]
37#[serde(rename_all = "PascalCase")]
38pub struct Region {
39	pub block_state_palette: Vec<MinecraftBlockCode>,
40	pub block_states: Vec<u64>,
41	pub size: Xyz,
42	pub tile_entities: Vec<LitematicBlockEntity>,
43}
44
45#[derive(Debug, Clone, Deserialize, Serialize)]
46pub struct LitematicBlockEntity {
47	// some files are missing this field :(
48	pub id: Option<String>,
49	pub x: i32,
50	pub y: i32,
51	pub z: i32,
52	#[serde(flatten, skip_serializing)]
53	pub components: Map<String, mininbt::Value>,
54}
55
56impl Region {
57	/// The minimum number of bits required to represent all block IDs in the palette.
58	#[allow(clippy::cast_possible_truncation)]
59	#[must_use]
60	pub fn block_bit_width(&self) -> u32 { 2.max((self.block_state_palette.len() as u32 - 1).bit_width()) }
61}
62
63impl Loadable for Litematic {
64	fn load(reader: impl Read, compressed: bool) -> eyre::Result<Self> {
65		let mut schematic: Self = read(reader, compressed).context("failed to load schematic")?;
66
67		if schematic.version != 7 && schematic.version != 6 {
68			warn!("unsupported schematic version: {}", schematic.version);
69		}
70
71		schematic.modernise();
72
73		Ok(schematic)
74	}
75
76	fn modernise(&mut self) {
77		if self.minecraft_data_version < FLATTENING_VERSION {
78			for region in &mut self.regions.values_mut() {
79				for block in &mut region.block_state_palette {
80					if let Some((_, new)) = MODERNISED_BLOCKS.iter().find(|(old, _)| old == &block.name) {
81						block.name = new.to_string();
82					}
83				}
84			}
85		}
86	}
87}
88
89impl ToInternal for Litematic {
90	#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
91	fn to_internal(self) -> eyre::Result<Internal> {
92		// TODO: multiple regions
93		if self.regions.len() > 1 {
94			warn!("multiple regions in schematic, only using one region");
95		}
96		let (_name, region) = self.regions.into_iter().next().ok_or_eyre("schematic has no regions")?;
97		let bit_width = region.block_bit_width();
98
99		// block IDs are stored as arbitrary bit width unsigned little endian integers, saved as an NBT long array
100		cfg_if! {
101			if #[cfg(target_pointer_width = "64")] {
102				let bits = region.block_states.view_bits::<Lsb0>();
103			} else {
104				// `u64` doesn't implement `BitStore` on 32-bit platforms:
105				// https://github.com/ferrilab/bitvec/blob/5fb855073acc2ed045094ed89d8daf8c765f0135/src/store.rs#L194
106				// unfortunately, this is much slower :(
107				let bytes: Vec<u8> = region.block_states.into_iter().flat_map(u64::to_le_bytes).collect();
108				let bits = bytes.view_bits::<Lsb0>();
109			}
110		}
111		let chunks = bits.chunks_exact(bit_width as usize);
112
113		// negative size coords indicate where the region's "corners" are, something we don't care about
114		let height = region.size.z.abs();
115		let width = region.size.x.abs();
116		let layers = region.size.y.abs();
117
118		let palette_size = region.block_state_palette.len();
119		let mut blocks = Vec::with_capacity(chunks.len());
120
121		// build new block ID list and convert non-rejected palette entries to `MinecraftBlockCode`
122		let (ids, block_codes): (Vec<Option<u32>>, Vec<MinecraftBlockCode>) = {
123			let mut current_id = 0;
124			let mut ids = Vec::with_capacity(palette_size);
125			let mut block_codes = Vec::with_capacity(palette_size);
126			for code in region.block_state_palette {
127				if should_reject(&code) {
128					ids.push(None);
129				} else {
130					ids.push(Some(current_id));
131					block_codes.push(code);
132					current_id += 1;
133				}
134			}
135			(ids, block_codes)
136		};
137
138		// coordinate system:
139		// ^ y (UP)
140		// |
141		// |
142		// |-------> x (EAST)
143		//  \_
144		//    \_
145		//      V z (SOUTH)
146
147		for (i, index) in (0..=i32::MAX).zip(chunks.map(BitField::load_le::<u32>)) {
148			let index = index as usize;
149			let Some(id) = ids[index] else {
150				continue;
151			};
152			let id = id as usize;
153
154			let (width_quotient, width_remainder) = (i / width, i % width);
155			let position = Xyz {
156				x: width_remainder,
157				y: width_quotient / height,
158				z: width_quotient % height,
159			};
160
161			blocks.push(internal::Block { id, position });
162		}
163
164		// fix tile entities with missing IDs
165		let mut tile_entities = region.tile_entities;
166		let bit_width = bit_width as usize;
167
168		for entity in &mut tile_entities {
169			if entity.id.is_none() {
170				// determine this entity's position in the packed block bits
171				let i = (entity.y * height * width) + (entity.z * width) + entity.x;
172
173				// if this is the third block, and the bit width is five, the bits for the block can be found at:
174				// (3 * 5)..(3 * 5) + 5
175				let start_bit = i as usize * bit_width;
176				let index: u32 = bits[start_bit..start_bit + bit_width].load_le();
177
178				let index = index as usize;
179				let Some(id) = ids[index] else {
180					// rejected
181					continue;
182				};
183				let id = id as usize;
184
185				entity.id = Some(block_codes[id].name.clone());
186			}
187		}
188
189		let tile_entities = tile_entities
190			.into_iter()
191			.filter(|e| e.id.is_some())
192			.map(|e| MinecraftBlockEntity {
193				id: e.id.expect("missing IDs have been filtered out"),
194				x: e.x,
195				y: e.y,
196				z: e.z,
197				components: e.components,
198			})
199			.collect();
200
201		Ok(Internal {
202			block_codes: BlockCodes::Minecraft(block_codes),
203			blocks,
204			tile_entities,
205			size: Xyz {
206				x: width,
207				y: layers,
208				z: height,
209			},
210		})
211	}
212}