vintage_schematics/
lib.rs

1// SPDX-FileCopyrightText: 2026 Lynnesbian <lynne@bune.city>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! A library for converting Minecraft schematics to Vintage Story [WorldEdit](https://worldedit.enginehub.org/)
5//! schematics.
6//!
7//! See [the `formats` module](formats) for the set of supported formats.
8
9use std::{
10	fs::File,
11	io::{BufReader, Read},
12	path::Path,
13};
14
15use cfg_if::cfg_if;
16use color_eyre::{
17	eyre,
18	eyre::{Context, bail},
19};
20use formats::{common::Xyz, litematic::Litematic, world_edit::WorldEdit};
21
22use crate::formats::{
23	Loadable, Saveable, Settings, ToInternal,
24	internal::{BlockCodes, Internal},
25	schematic::Schematic,
26	sponge::Sponge,
27	vanilla::Vanilla,
28};
29
30cfg_if! {
31	if #[cfg(feature = "regex-full")] {
32		pub(crate) use regex::Regex;
33	} else if #[cfg(feature = "regex-lite")] {
34		pub(crate) use regex_lite::Regex;
35	} else {
36		compile_error!("no regex backend! please enable the `regex-full` or `regex-lite` feature.");
37	}
38}
39
40pub mod ascii85;
41pub mod convert;
42pub mod entity;
43pub mod formats;
44#[cfg(test)]
45mod tests;
46
47cfg_if! {
48	if #[cfg(feature = "hashmap")] {
49		/// Alias for [`std::collections::HashMap`]. To use `BTreeMap` instead, disable the `hashmap` feature.
50		pub type Map<K, V> = std::collections::HashMap<K, V>;
51		/// Alias for [`std::collections::HashSet`]. To use `BTreeSet` instead, disable the `hashmap` feature.
52		pub type Set<V> = std::collections::HashSet<V>;
53	} else {
54		/// Alias for [`std::collections::BTreeMap`]. To use `HashMap` instead, enable the `hashmap` feature.
55		pub type Map<K, V> = std::collections::BTreeMap<K, V>;
56		/// Alias for [`std::collections::BTreeSet`]. To use `HashMap` instead, enable the `hashmap` feature.
57		pub type Set<V> = std::collections::BTreeSet<V>;
58	}
59}
60
61/// Converts a GZIP-compressed schematic file to a [`WorldEdit`] schematic.
62/// Infers the schematic format from the file extension.
63/// Use [`convert()`] to specify the format explicitly.
64///
65/// # Errors
66///
67/// Returns an error if [`Loadable::load`] fails, if an IO error occurs, or if the file extension is not one of
68/// `litematic` or `nbt`.
69pub fn convert_file(path: &Path, settings: &Settings) -> eyre::Result<WorldEdit> {
70	let file = BufReader::new(File::open(path).context("failed to open schematic file")?);
71	let ext = path.extension().map(|e| e.to_string_lossy().to_ascii_lowercase());
72	let function = match ext.as_deref() {
73		Some("litematic") => convert::<Litematic>,
74		Some("nbt") => convert::<Vanilla>,
75		Some("schematic") => convert::<Schematic>,
76		Some("schem") => convert::<Sponge>,
77
78		Some(ext) => bail!("unrecognised file extension '{ext}'"),
79		None => bail!("no file extension"),
80	};
81
82	function(file, settings)
83}
84
85/// Converts a GZIP-compressed schematic reader to a [`WorldEdit`] schematic.
86/// If you have an uncompressed schematic reader, use [`convert_uncompressed`] instead.
87///
88/// # Errors
89///
90/// Returns an error if [`Loadable::load`] or [`ToInternal::to_internal`] fails.
91pub fn convert<T>(reader: impl Read, settings: &Settings) -> eyre::Result<WorldEdit>
92where
93	T: Loadable + ToInternal,
94{
95	let schematic = T::load(reader, true).context("failed to load schematic file")?;
96	let mut internal = schematic.to_internal().context("failed to convert schematic to internal format")?;
97
98	// shitty_renderer(&internal);
99
100	// export
101	Ok(WorldEdit::save(&mut internal, settings))
102}
103
104/// Converts an uncompressed schematic reader to a [`WorldEdit`] schematic.
105/// If you have a GZIP-compressed schematic reader, use [`convert`](fn@convert) instead.
106///
107/// # Errors
108///
109/// Returns an error if [`Loadable::load`] or [`ToInternal::to_internal`] fails.
110pub fn convert_uncompressed<T>(reader: impl Read, settings: &Settings) -> eyre::Result<WorldEdit>
111where
112	T: Loadable + ToInternal,
113{
114	let schematic = T::load(reader, false).context("failed to load schematic file")?;
115	let mut internal = schematic.to_internal().context("failed to convert schematic to internal format")?;
116
117	// export
118	Ok(WorldEdit::save(&mut internal, settings))
119}
120
121#[allow(dead_code)]
122fn shitty_renderer(internal: &Internal) {
123	let Xyz {
124		x: width,
125		y: layers,
126		z: height,
127	} = internal.size;
128
129	let BlockCodes::Minecraft(block_codes) = &internal.block_codes else {
130		println!("can only render minecraft format!");
131		return;
132	};
133
134	for y in 0..layers {
135		println!(" === BEGIN LAYER {y} ===");
136		for z in 0..height {
137			for x in 0..width {
138				let Some(block) = internal.blocks.iter().find(|b| b.position.x == x && b.position.y == y && b.position.z == z)
139				else {
140					print!(" ");
141					continue;
142				};
143
144				let symbol = block_codes.get(block.id).map_or('!', |code| match code.name.as_str() {
145					"minecraft:air" => ' ',
146					"minecraft:red_mushroom_block" => 'M',
147					"minecraft:mushroom_stem" => 'S',
148					"minecraft:short_grass" => 'g',
149					"minecraft:grass_block" => 'G',
150					"minecraft:cobblestone" => 'C',
151					"minecraft:stone_bricks" => 'B',
152					"minecraft:oak_log" => 'O',
153					"minecraft:spruce_log" => '$',
154					"minecraft:dirt" => 'D',
155					"minecraft:farmland" => 'F',
156					"minecraft:water" => 'W',
157					_ => '?',
158				});
159				print!("{symbol}");
160			}
161			println!();
162		}
163		println!(" ===  END  LAYER {y} ===");
164		println!();
165	}
166}