mcschem/
lib.rs

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/// A struct holding infomation about a schematic
24#[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/// A block with ID and properties
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Block {
38    id: String,
39    properties: BTreeMap<String, String>,
40}
41
42/// A block entity
43#[non_exhaustive]
44#[derive(Debug, Clone)]
45pub enum BlockEntity {
46    /// Represents a barrel
47    Barrel {
48        items: Vec<ItemSlot>,
49    },
50    // /// A post-1.20 sign
51    // Sign {
52    // },
53    /// A pre-1.20 sign
54    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/// An item slot in a container
65#[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    /// Initialize a new schematic filled with `minecraft:air`
125    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    /// Sets a block in the schematic
139    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    /// Sets a block entity in the schematic
152    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    /// Export the schematic to a writer
170    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                // "WEOffsetX": 0_i32,
207                // "WEOffsetY": 0_i32,
208                // "WEOffsetZ": 0_i32,
209                // "MCSchematicMetadata": nbt::compound! {
210                //     "Generated": "Generated with rust crate `mcschem`"
211                // },
212            },
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::Sign { .. } | */ 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::Sign {  } => {
248            //     todo!();
249            // },
250            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}