vintage_schematics/formats/
litematic.rs1use 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#[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 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 #[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 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 cfg_if! {
106 if #[cfg(target_pointer_width = "64")] {
107 let bits = region.block_states.view_bits::<Lsb0>();
108 } else {
109 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 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 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 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 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 let i = (entity.y * height * width) + (entity.z * width) + entity.x;
177
178 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 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}