Skip to main content

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