1use std::io::Read;
4
5use color_eyre::{
6 eyre,
7 eyre::{Context, bail, eyre},
8};
9use serde::{Deserialize, Serialize};
10use tracing::debug;
11
12use crate::{
13 Map,
14 convert::block::{FENCE_PATTERN, STAIR_PATTERN},
15 formats::{
16 Loadable, ToInternal,
17 common::{MODERNISED_BLOCKS, MinecraftBlockEntity, Size},
18 internal::{Block, BlockCodes, Internal, MinecraftBlockCode, PropertyMap},
19 read, should_reject,
20 },
21};
22
23const WOOD_VALUES: [&str; 6] = ["oak", "spruce", "birch", "jungle", "acacia", "dark_oak"];
24
25const COLOURS: [&str; 16] = [
26 "white",
27 "orange",
28 "magenta",
29 "light_blue",
30 "yellow",
31 "lime",
32 "pink",
33 "gray",
34 "light_gray",
35 "cyan",
36 "purple",
37 "blue",
38 "brown",
39 "green",
40 "red",
41 "black",
42];
43
44const DIRS_SWNE: [&str; 4] = ["south", "west", "north", "east"];
48
49const DIRS_NSWE: [&str; 4] = ["north", "south", "west", "east"];
53
54const DIRS_EWSN: [&str; 4] = ["east", "west", "south", "north"];
58
59#[derive(Debug, Clone, Deserialize, Serialize)]
61#[serde(rename_all = "PascalCase")]
62pub struct Schematic {
63 #[serde(flatten)]
65 pub size: Size,
66
67 pub blocks: Vec<u8>,
70
71 pub data: Vec<u8>,
73
74 pub tile_entities: Vec<MinecraftBlockEntity>,
76
77 pub schematica_mapping: Map<String, u16>,
80
81 pub materials: String,
84}
85
86#[derive(Clone, Copy, PartialEq, Eq, Debug)]
87enum CacheState {
88 Rejected,
89 Unseen,
90 Cached(u16),
91}
92
93impl Loadable for Schematic {
94 fn load(reader: impl Read, compressed: bool) -> eyre::Result<Self>
95 where
96 Self: Sized,
97 {
98 let mut schematic: Self = read(reader, compressed).context("failed to load schematic")?;
99
100 if schematic.materials != "Alpha" {
101 bail!("unsupported schematic material version: expected 'Alpha', found '{}'", schematic.materials);
102 }
103
104 if schematic.blocks.len() != schematic.data.len() {
105 bail!(
106 "blocks and data arrays have different lengths ({}, {})",
107 schematic.blocks.len(),
108 schematic.data.len()
109 );
110 }
111
112 schematic.modernise();
113
114 Ok(schematic)
115 }
116
117 fn modernise(&mut self) {
118 for (old, new) in MODERNISED_BLOCKS {
119 if let Some(value) = self.schematica_mapping.remove(*old) {
120 self.schematica_mapping.insert(new.to_string(), value);
121 }
122 }
123 }
124}
125
126impl ToInternal for Schematic {
127 fn to_internal(self) -> eyre::Result<Internal> {
128 let mut block_codes: Vec<MinecraftBlockCode> = Vec::with_capacity(self.schematica_mapping.len());
131
132 let mut names: Box<[Option<&str>; 256]> = Box::new([None; 256]);
135 for (name, &id) in &self.schematica_mapping {
136 if u8::try_from(id).is_ok() {
137 names[id as usize] = Some(name);
139 }
140 }
141
142 let mut cache = Box::new([CacheState::Unseen; 4096]);
146
147 let mut blocks = Vec::with_capacity(self.blocks.len());
149
150 let air = self.schematica_mapping.iter().find(|(k, _)| **k == "minecraft:air").map(|(_, v)| *v);
152
153 for (i, (block, data)) in (0..=i32::MAX).zip(self.blocks.into_iter().zip(self.data.into_iter())) {
154 if air.is_some_and(|a| a == u16::from(block)) {
155 continue;
156 }
157
158 let id = if data < 16 {
159 let cache_idx = ((block as usize) << 4) | data as usize;
160 let cached = cache[cache_idx];
161
162 match cached {
163 CacheState::Rejected => continue,
164 CacheState::Unseen => {
165 let name = names[block as usize].ok_or_else(|| eyre!("unknown block ID: {}", block))?;
167 let code = convert_block(name, data)?;
168
169 if should_reject(&code) {
170 cache[cache_idx] = CacheState::Rejected;
172 continue;
173 }
174
175 let id = block_codes.iter().position(|c| c == &code).unwrap_or_else(|| {
178 block_codes.push(code);
179 block_codes.len() - 1
180 });
181
182 #[allow(clippy::cast_possible_truncation)]
183 let cached = id as u16;
184 cache[cache_idx] = CacheState::Cached(cached);
185
186 id
187 }
188 CacheState::Cached(id) => id as usize,
189 }
190 } else {
191 bail!("data value for block {i} out of range: expected 0-15, got {}", data);
192 };
193
194 blocks.push(Block {
195 id,
196 position: self.size.index_position(i),
197 });
198 }
199
200 Ok(Internal {
201 block_codes: BlockCodes::Minecraft(block_codes),
202 blocks,
203 tile_entities: self.tile_entities,
204 size: self.size.into(),
205 })
206 }
207}
208
209fn convert_block(name: &str, data: u8) -> eyre::Result<MinecraftBlockCode> {
210 let (name, properties) = match name {
212 "minecraft:torch" | "minecraft:redstone_torch" => convert_torch(name, data)?,
213 "minecraft:leaves" => convert_leaves(data, false),
214 "minecraft:leaves2" => convert_leaves(data, true),
215 "minecraft:standing_sign" => convert_sign(data, false)?,
216 "minecraft:wall_sign" => convert_sign(data, true)?,
217 "minecraft:chest" => convert_chest(data)?,
218 "minecraft:log" => convert_log(data),
219
220 "minecraft:planks" => (format!("minecraft:{}_planks", WOOD_VALUES.get(data as usize).unwrap_or(&"oak")), None),
222
223 "minecraft:bed" => convert_bed(data)?,
224
225 "minecraft:wool" => convert_colourful("wool", data),
227 "minecraft:stained_hardened_clay" => convert_colourful("terracotta", data),
228 "minecraft:carpet" => convert_colourful("carpet", data),
229 "minecraft:stained_glass" => convert_colourful("stained_glass", data),
230 "minecraft:stained_glass_pane" => convert_colourful("stained_glass_pane", data),
231
232 _ => {
233 if name.ends_with("_gate") && FENCE_PATTERN.is_match(name) {
234 convert_gate(data, name)?
235 } else if name.ends_with("_stairs") && STAIR_PATTERN.is_match(name) {
236 convert_stairs(name, data)?
237 } else {
238 if tracing::enabled!(tracing::Level::DEBUG) {
239 debug!("passthrough: {name} with {data} ({data:#08b})");
240 }
241 (String::from(name), None)
242 }
243 }
244 };
245
246 let properties = properties.unwrap_or_default();
247 let code = MinecraftBlockCode { name, properties };
248 Ok(code)
249}
250
251fn convert_torch(name: &str, data: u8) -> eyre::Result<(String, Option<PropertyMap>)> {
252 let redstone = name == "minecraft:redstone_torch";
254
255 Ok(if data == 5 {
256 (String::from(name), None)
257 } else {
258 let facing = data_facing(data, DIRS_EWSN, 1)?;
259
260 let torch = if redstone {
261 "minecraft:redstone_wall_torch"
262 } else {
263 "minecraft:wall_torch"
264 };
265
266 (String::from(torch), Some(Map::from([(String::from("facing"), facing.to_string())])))
267 })
268}
269
270fn convert_leaves(data: u8, group_2: bool) -> (String, Option<PropertyMap>) {
271 let leaf_array = if group_2 {
273 &["acacia", "dark_oak", "oak", "oak"]
274 } else {
275 &WOOD_VALUES[0..4]
276 };
277
278 let leaf_type = leaf_array[(data & 0b11) as usize];
279 (format!("minecraft:{leaf_type}_leaves"), None)
280}
281
282fn convert_sign(data: u8, wall: bool) -> eyre::Result<(String, Option<PropertyMap>)> {
283 let properties = if wall {
284 (String::from("facing"), data_facing(data, DIRS_NSWE, 2)?.to_string())
285 } else {
286 (String::from("rotation"), data.to_string())
287 };
288
289 let name = if wall {
290 "minecraft:oak_wall_sign"
291 } else {
292 "minecraft:oak_sign"
293 };
294
295 Ok((String::from(name), Some(Map::from([properties]))))
296}
297
298fn convert_bed(data: u8) -> eyre::Result<(String, Option<PropertyMap>)> {
299 let facing = data_facing(data, DIRS_SWNE, 0)?;
301 let part = if data & 0b1000 != 0 { "head" } else { "foot" };
302
303 Ok((
304 String::from("minecraft:red_bed"),
305 Some(Map::from([(String::from("facing"), facing.to_string()), (String::from("part"), part.to_string())])),
306 ))
307}
308
309fn convert_chest(data: u8) -> eyre::Result<(String, Option<PropertyMap>)> {
310 Ok((
311 String::from("minecraft:chest"),
312 Some(Map::from([
313 (String::from("facing"), data_facing(data, DIRS_NSWE, 2)?.to_string()),
314 (String::from("type"), String::from("single")),
315 ])),
316 ))
317}
318
319fn convert_log(data: u8) -> (String, Option<PropertyMap>) {
320 let wood = WOOD_VALUES[(data & 0b11) as usize];
322
323 let axis = match data >> 2 {
324 0 => "y",
325 1 => "x",
326 2 => "z",
327
328 _ => return (format!("minecraft:{wood}_wood"), None),
330 };
331
332 (format!("minecraft:{wood}_log"), Some(Map::from([(String::from("axis"), axis.to_string())])))
333}
334
335fn convert_gate(data: u8, name: &str) -> eyre::Result<(String, Option<PropertyMap>)> {
336 let facing = data_facing(data, DIRS_SWNE, 0)?;
337
338 let opened = if data & 0b100 != 0 { "opened" } else { "closed" };
339
340 Ok((
341 String::from(name),
342 Some(Map::from([
343 (String::from("facing"), String::from(facing)),
344 (String::from("open"), String::from(opened)),
345 ])),
346 ))
347}
348
349fn convert_stairs(name: &str, data: u8) -> eyre::Result<(String, Option<PropertyMap>)> {
350 let facing = data_facing(data, DIRS_EWSN, 0)?;
353 let half = if data & 0b100 != 0 { "top" } else { "bottom" };
354
355 Ok((
356 name.to_string(),
357 Some(Map::from([
358 (String::from("facing"), String::from(facing)),
359 (String::from("half"), String::from(half)),
360 ])),
361 ))
362}
363
364fn convert_colourful(name: &str, data: u8) -> (String, Option<PropertyMap>) {
365 let colour = COLOURS[(data & 0b1111) as usize];
366 (format!("minecraft:{colour}_{name}"), None)
367}
368
369fn data_facing(data: u8, directions: [&str; 4], start: u8) -> eyre::Result<&str> {
373 directions.get(((data - start) & 0b11) as usize).copied().ok_or_else(|| {
374 eyre!(
375 "couldn't determine facing direction: expected data value between {start} and {}, got {data}",
376 start + 4
377 )
378 })
379}
380
381#[test]
382fn test_data_facing() {
383 let bed_facings = DIRS_SWNE;
385 assert_eq!(data_facing(0, bed_facings, 0).unwrap(), "south");
386 assert_eq!(data_facing(1, bed_facings, 0).unwrap(), "west");
387
388 let torch_facings = DIRS_EWSN;
390 assert_eq!(data_facing(1, torch_facings, 1).unwrap(), "east");
391 assert_eq!(data_facing(3, torch_facings, 1).unwrap(), "south");
392}