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