1#![warn(
2 clippy::complexity,
3 clippy::correctness,
4 clippy::perf,
5 clippy::nursery,
6 clippy::suspicious,
7 clippy::style,
8)]
9#![allow(
10 clippy::semicolon_inside_block,
11 clippy::just_underscores_and_digits,
12)]
13
14use std::collections::{BTreeMap, HashMap};
15use std::io;
16use std::fmt;
17use std::str::FromStr;
18use quartz_nbt as nbt;
19
20pub mod data_version;
21pub mod utils;
22
23#[derive(Debug, Clone)]
25pub struct Schematic {
26 data_version: i32,
27
28 blocks: Vec<Block>,
29 block_entities: HashMap<[u16; 3], BlockEntity>,
30 size_x: u16,
31 size_y: u16,
32 size_z: u16,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Block {
38 id: String,
39 properties: BTreeMap<String, String>,
40}
41
42#[non_exhaustive]
44#[derive(Debug, Clone)]
45pub enum BlockEntity {
46 Barrel {
48 items: Vec<ItemSlot>,
49 },
50 SignPre1D20 {
55 glowing: bool,
56 color: String,
57 line_1: String,
58 line_2: String,
59 line_3: String,
60 line_4: String,
61 },
62}
63
64#[derive(Debug, Clone)]
66pub struct ItemSlot {
67 pub id: String,
68 pub extra: nbt::NbtCompound,
69 pub count: i8,
70 pub slot: i8,
71}
72
73impl FromStr for Block {
74 type Err = ();
75 fn from_str(block: &str) -> Result<Self, ()> {
76 let (id, properties) = block
77 .split_once('[')
78 .map_or_else(
79 || (block, None),
80 |(a, b)| (a, Some(b))
81 );
82
83 let mut prop = BTreeMap::new();
84 if let Some(properties) = properties {
85 if !matches!(properties.chars().last(), Some(']')) {
86 return Err(());
87 }
88
89 let properties = &properties[..properties.len()-1];
90
91 for property in properties.split(',') {
92 let (k, v) = property.split_once('=').ok_or(())?;
93 prop.insert(k.to_string(), v.to_string());
94 }
95 }
96
97 Ok(Self {
98 id: id.to_string(), properties: prop
99 })
100 }
101}
102
103impl fmt::Display for Block {
104 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105 self.id.fmt(f)?;
106
107 if !self.properties.is_empty() {
108 write!(
109 f,
110 "[{}]",
111 self.properties
112 .iter()
113 .map(|(k, v)| format!("{k}={v}"))
114 .collect::<Vec<String>>()
115 .join(",")
116 )?;
117 }
118
119 Ok(())
120 }
121}
122
123impl Schematic {
124 pub fn new(data_version: i32, size_x: u16, size_y: u16, size_z: u16) -> Self {
126 Self {
127 data_version,
128
129 blocks: vec![
130 Block::from_str("minecraft:air").unwrap();
131 (size_x * size_y * size_z) as usize
132 ],
133 block_entities: HashMap::new(),
134 size_x, size_y, size_z
135 }
136 }
137
138 pub fn set_block(&mut self, x: usize, y: usize, z: usize, block: Block) {
140 if x >= self.size_x as usize || y >= self.size_y as usize || z >= self.size_z as usize {
141 panic!("Set block to ({x}, {y}, {z}) which is out of bound");
142 }
143
144 self.blocks[
145 y * (self.size_x * self.size_z) as usize
146 + z * self.size_x as usize
147 + x
148 ] = block;
149 }
150
151 pub fn set_block_entity(
153 &mut self,
154 x: usize, y: usize, z: usize,
155 block: Block, be: BlockEntity
156 ) {
157 if x >= self.size_x as usize || y >= self.size_y as usize || z >= self.size_z as usize {
158 panic!("Set block to ({x}, {y}, {z}) which is out of bound");
159 }
160
161 self.blocks[
162 y * (self.size_x * self.size_z) as usize
163 + z * self.size_x as usize
164 + x
165 ] = block;
166
167 self.block_entities.insert([x as u16, y as u16, z as u16], be);
168 }
169 pub fn export<W: io::Write>(&self, writer: &mut W) -> Result<(), quartz_nbt::io::NbtIoError> {
171 let mut palette = Vec::new();
172 let mut block_data = Vec::new();
173 for block in self.blocks.iter() {
174 if !palette.contains(block) {
175 palette.push(block.clone());
176 }
177
178 let mut id = palette.iter().position(|v| v == block).unwrap();
179
180 while id & 0x80 != 0 {
181 block_data.push(id as u8 as i8 & 0x7F | 0x80_u8 as i8);
182 id >>= 7;
183 }
184 block_data.push(id as u8 as i8);
185 }
186
187 let mut palette_nbt = nbt::NbtCompound::new();
188 for (bi, b) in palette.iter().enumerate() {
189 palette_nbt.insert(format!("{b}"), nbt::NbtTag::Int(bi as i32));
190 }
191
192 let mut block_entities = vec![];
193 for (p, e) in self.block_entities.iter() {
194 let mut compound = nbt::compound! {
195 "Pos": [I; p[0] as i32, p[1] as i32, p[2] as i32],
196 "Id": e.id()
197 };
198 e.add_data(&mut compound);
199 block_entities.push(compound);
200 }
201
202 let schem = nbt::compound! {
203 "Version": 2_i32,
204 "DataVersion": self.data_version,
205 "Metadata": nbt::compound! {
206 },
213 "Width": self.size_x as i16,
214 "Height": self.size_y as i16,
215 "Length": self.size_z as i16,
216 "PaletteMax": palette.len() as i32,
217 "Palette": palette_nbt,
218 "BlockData": nbt::NbtTag::ByteArray(block_data),
219 "BlockEntities": nbt::NbtList::from(block_entities),
220 };
221
222 println!("{schem:#?}");
223
224 nbt::io::write_nbt(writer, Some("Schematic"), &schem, nbt::io::Flavor::GzCompressed)
225 }
226}
227
228impl BlockEntity {
229 fn id(&self) -> &'static str {
230 match self {
231 Self::Barrel { .. } => "minecraft:barrel",
232 Self::SignPre1D20 { .. } => "minecraft:sign",
233 }
234 }
235
236 fn add_data(&self, compound: &mut nbt::NbtCompound) {
237 match self {
238 Self::Barrel { items } => {
239 let mut items_nbt = Vec::with_capacity(items.len());
240
241 for i in items.iter() {
242 items_nbt.push(i.to_compound());
243 }
244
245 compound.insert("Items", nbt::NbtList::from(items_nbt));
246 },
247 Self::SignPre1D20 { glowing, color, line_1, line_2, line_3, line_4 } => {
251 compound.insert("GlowingText", *glowing as i8);
252 compound.insert("Color", color.clone());
253 compound.insert("Text1", line_1.clone());
254 compound.insert("Text2", line_2.clone());
255 compound.insert("Text3", line_3.clone());
256 compound.insert("Text4", line_4.clone());
257 },
258 }
259 }
260}
261
262impl ItemSlot {
263 fn to_compound(&self) -> nbt::NbtCompound {
264 nbt::compound! {
265 "Count": self.count,
266 "Slot": self.slot,
267 "id": self.id.clone(),
268 "tag": self.extra.clone()
269 }
270 }
271}