vintage_schematics/formats/
litematic.rs1use 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#[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 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 #[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 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 cfg_if! {
101 if #[cfg(target_pointer_width = "64")] {
102 let bits = region.block_states.view_bits::<Lsb0>();
103 } else {
104 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 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 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 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 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 let i = (entity.y * height * width) + (entity.z * width) + entity.x;
172
173 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 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}