Skip to main content

mindus/data/
schematic.rs

1//! schematic parsing
2use fimg::DynImage;
3use std::collections::HashMap;
4use std::collections::hash_map::Entry;
5use std::fmt::{self, Write};
6use thiserror::Error;
7
8use crate::block::ratios::{Io, IoBuilder};
9use crate::block::{self, BLOCK_REGISTRY, Block, Rotation, State};
10use crate::data::base64;
11use crate::data::dynamic::{self, DynData};
12use crate::data::renderer::*;
13use crate::data::{self, DataRead, DataWrite, GridPos, Serializable};
14use crate::item::storage::ItemStorage;
15use crate::utils::Cow;
16use crate::utils::array::Array2D;
17
18/// biggest schematic
19pub const MAX_DIMENSION: usize = 1024;
20/// most possible blocks
21pub const MAX_BLOCKS: u32 = 1024 * 1024;
22
23/// a placement in a schematic
24#[derive(Clone, Hash)]
25pub struct Placement {
26    pub block: &'static Block,
27    pub rot: Rotation,
28    state: Option<State>,
29}
30
31impl PartialEq for Placement {
32    fn eq(&self, rhs: &Placement) -> bool {
33        self.block == rhs.block && self.rot == rhs.rot
34    }
35}
36
37impl fmt::Debug for Placement {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
39        write!(f, "P<{}[*{}]>", self.block.name(), self.rot.ch())
40    }
41}
42
43impl Placement {
44    /// make a placement from a block
45    #[must_use]
46    pub const fn new(block: &'static Block) -> Self {
47        Self {
48            block,
49            rot: Rotation::Up,
50            state: None,
51        }
52    }
53
54    /// gets the current state of this placement. you can cast it with `placement.block::get_state(placement.get_state()?)?`
55    #[must_use]
56    pub const fn get_state(&self) -> Option<&State> {
57        self.state.as_ref()
58    }
59
60    /// get mutable state.
61    pub fn get_state_mut(&mut self) -> Option<&mut State> {
62        self.state.as_mut()
63    }
64
65    /// draws this placement in particular
66    #[must_use]
67    pub fn image(
68        &self,
69        context: Option<&RenderingContext>,
70        rot: Rotation,
71        s: Scale,
72    ) -> DynImage<Cow> {
73        self.block.image(self.get_state(), context, rot, s)
74    }
75
76    /// set the state
77    pub fn set_state(&mut self, data: DynData) -> Result<Option<State>, block::DeserializeError> {
78        let state = self.block.deserialize_state(data)?;
79        Ok(std::mem::replace(&mut self.state, state))
80    }
81
82    /// rotate this
83    pub fn set_rotation(&mut self, rot: Rotation) -> Rotation {
84        std::mem::replace(&mut self.rot, rot)
85    }
86}
87
88impl BlockState for Placement {
89    fn get_block(&self) -> Option<&'static Block> {
90        Some(self.block)
91    }
92}
93
94impl RotationState for Placement {
95    fn get_rotation(&self) -> Option<Rotation> {
96        Some(self.rot)
97    }
98}
99
100impl BlockState for Option<Placement> {
101    fn get_block(&self) -> Option<&'static Block> {
102        Some(self.as_ref()?.block)
103    }
104}
105
106impl RotationState for Option<Placement> {
107    fn get_rotation(&self) -> Option<Rotation> {
108        Some(self.as_ref()?.rot)
109    }
110}
111
112#[derive(Clone, Debug)]
113/// a schematic.
114pub struct Schematic {
115    pub width: usize,
116    pub height: usize,
117    pub tags: HashMap<String, String>,
118    /// schems can have holes, so [Option] is used.
119    pub blocks: Array2D<Option<Placement>>,
120}
121impl core::hash::Hash for Schematic {
122    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
123        self.width.hash(state);
124        self.height.hash(state);
125        self.blocks.hash(state);
126    }
127}
128
129impl PartialEq for Schematic {
130    fn eq(&self, rhs: &Schematic) -> bool {
131        self.width == rhs.width
132            && self.height == rhs.height
133            && self.blocks == rhs.blocks
134            && self.tags == rhs.tags
135    }
136}
137
138impl Schematic {
139    /// the area around a point
140    pub(crate) fn cross(&self, c: &PositionContext) -> (Cross, Corners) {
141        let get = |x, y| {
142            let b = self.get(x?, y?).ok()??;
143            Some((b.get_block()?, b.get_rotation()?))
144        };
145        macro_rules! s {
146            ($x:expr) => {
147                Some($x)
148            };
149            ($a:expr => $b:expr) => {
150                if $a < $b { None } else { Some($a - $b) }
151            };
152        }
153        (
154            [
155                get(s!(c.position.0), s!(c.position.1 + 1)),
156                get(s!(c.position.0 + 1), s!(c.position.1)),
157                get(s!(c.position.0), s!(c.position.1 => 1)),
158                get(s!(c.position.0 => 1), s!(c.position.1)),
159            ],
160            [
161                get(s!(c.position.0 => 1), s!(c.position.1 => 1)),
162                get(s!(c.position.0 => 1), s!(c.position.1 + 1)),
163                get(s!(c.position.0 + 1), s!(c.position.1 => 1)),
164                get(s!(c.position.0 + 1), s!(c.position.1 + 1)),
165            ],
166        )
167    }
168
169    /// Ratios of this schematic.
170    /// ```
171    /// # #![feature(const_trait_impl)]
172    /// # use mindus::Schematic;
173    /// # use mindus::block::ratios::ratios;
174    /// assert_eq!(Schematic::deserialize_base64("bXNjaAF4nEWMSw7CMAxEh9REVSqx5hKcCLFISxaR0o9Sg8rtSTpFePPkmWfDwTWQyY8B5z75VdE9wzrkuGicJwA2+T6kFeb+aNDtym2MW8i4LJ/sNWo4dje8ksa31zmXuyv+Y4BTgRD2iIi9M+xM7WrUgnoNhYpQESpCxfKLrUo9FsISLX6vKgwhhCVK+wX5/BtM").unwrap().ratios(),
175    ///     ratios![[Coal: 5.25, Lead: 10.5, Sand: 10.5, Water: 180] => [BlastCompound: 5.25, SporePod: 0.75]]);
176    /// ```
177    pub fn ratios(&self) -> Io {
178        let mut io = IoBuilder::default();
179        for p in self.blocks.iter().filter_map(|o| o.as_ref()) {
180            io += p.block.io(p.state.as_ref());
181        }
182        io.into()
183    }
184
185    /// create a new schematic
186    /// ```
187    /// # use mindus::Schematic;
188    /// let s = Schematic::new(5, 5);
189    /// ```
190    pub fn new(width: usize, height: usize) -> Self {
191        let mut tags = HashMap::<String, String>::new();
192        tags.insert("name".to_string(), String::new());
193        tags.insert("description".to_string(), String::new());
194        tags.insert("labels".to_string(), "[]".to_string());
195        Self {
196            width,
197            height,
198            tags,
199            blocks: Array2D::new(None, width, height),
200        }
201    }
202
203    // #[must_use]
204    // /// check if a rect is empty
205    // /// ```
206    // /// # use mindus::Schematic;
207    // /// # use mindus::block::distribution::ROUTER;
208    // /// let mut s = Schematic::new(5, 5);
209    // /// s.put(0, 0, &ROUTER);
210    // /// assert!(s.is_region_empty(1, 1, 4, 4));
211    // /// s.put(2, 2, &ROUTER);
212    // /// assert!(s.is_region_empty(1, 1, 4, 4) == false);
213    // /// // out of bounds is empty
214    // /// assert!(s.is_region_empty(25, 25, 0, 0));
215    // /// ```
216    // pub fn is_region_empty(&self, x: usize, y: usize, w: usize, h: usize) -> bool {
217    //     if x >= self.width || y >= self.height || w == 0 || h == 0 {
218    //         return true;
219    //     }
220    //     if w > 1 || h > 1 {
221    //         for y in y..(y + h).min(self.height) {
222    //             for x in x..(x + w).min(self.width) {
223    //                 if self.get(x, y).unwrap().is_some() {
224    //                     return false;
225    //                 }
226    //             }
227    //         }
228    //         true
229    //     } else {
230    //         self.get(x, y).unwrap().is_none()
231    //     }
232    // }
233
234    /// gets a block
235    /// ```
236    /// # use mindus::Schematic;
237    /// # use mindus::block::Rotation;
238    /// # use mindus::block::DUO;
239    ///
240    /// let mut s = Schematic::new(5, 5);
241    /// assert!(s.get(0, 0).unwrap().is_none());
242    /// s.put(0, 0, &DUO);
243    /// assert!(s.get(0, 0).unwrap().is_some());
244    /// ```
245    pub fn get(&self, x: usize, y: usize) -> Result<Option<&Placement>, PosError> {
246        if x >= self.width || y >= self.height {
247            return Err(PosError {
248                x,
249                y,
250                w: self.width,
251                h: self.height,
252            });
253        }
254        Ok(self.blocks[x][y].as_ref())
255    }
256
257    /// gets a block, mutably
258    pub fn get_mut(&mut self, x: usize, y: usize) -> Result<Option<&mut Placement>, PosError> {
259        if x >= self.width || y >= self.height {
260            return Err(PosError {
261                x,
262                y,
263                w: self.width,
264                h: self.height,
265            });
266        }
267        Ok(self.blocks[x][y].as_mut())
268    }
269
270    /// put a block in (same as [`Schematic::set`], but less arguments and builder-ness). panics!!!
271    /// ```
272    /// # use mindus::Schematic;
273    /// # use mindus::block::ROUTER;
274    ///
275    /// let mut s = Schematic::new(5, 5);
276    /// s.put(0, 0, &ROUTER);
277    /// assert!(s.get(0, 0).unwrap().is_some() == true);
278    /// ```
279    pub fn put(&mut self, x: usize, y: usize, block: &'static Block) -> &mut Self {
280        self.set(x, y, block, DynData::Empty, Rotation::Up).unwrap();
281        self
282    }
283
284    /// set a block
285    /// ```
286    /// # use mindus::Schematic;
287    /// # use mindus::data::dynamic::DynData;
288    /// # use mindus::block::Rotation;
289    /// # use mindus::block::ROUTER;
290    ///
291    /// let mut s = Schematic::new(5, 5);
292    /// s.set(0, 0, &ROUTER, DynData::Empty, Rotation::Right);
293    /// assert!(s.get(0, 0).unwrap().is_some() == true);
294    /// ```
295    pub fn set(
296        &mut self,
297        x: usize,
298        y: usize,
299        block: &'static Block,
300        data: DynData,
301        rot: Rotation,
302    ) -> Result<(), PlaceError> {
303        let sz = usize::from(block.get_size());
304        let off = (sz - 1) / 2;
305        if x < off || y < off {
306            return Err(PlaceError::Bounds {
307                x,
308                y,
309                sz: block.get_size(),
310                w: self.width,
311                h: self.height,
312            });
313        }
314        if self.width - x < sz - off || self.height - y < sz - off {
315            return Err(PlaceError::Bounds {
316                x,
317                y,
318                sz: block.get_size(),
319                w: self.width,
320                h: self.height,
321            });
322        }
323        let state = block.deserialize_state(data)?;
324        let p = Placement { block, rot, state };
325        self.blocks[x][y] = Some(p);
326        Ok(())
327    }
328
329    /// take out a block
330    /// ```
331    /// # use mindus::Schematic;
332    /// # use mindus::block::DUO;
333    /// # use mindus::block::Rotation;
334    ///
335    /// let mut s = Schematic::new(5, 5);
336    /// s.put(0, 0, &DUO);
337    /// assert!(s.get(0, 0).unwrap().is_some() == true);
338    /// assert!(s.take(0, 0).unwrap().is_some() == true);
339    /// assert!(s.get(0, 0).unwrap().is_none() == true);
340    /// ```
341    pub fn take(&mut self, x: usize, y: usize) -> Result<Option<Placement>, PosError> {
342        if x >= self.width || y >= self.height {
343            return Err(PosError {
344                x,
345                y,
346                w: self.width,
347                h: self.height,
348            });
349        }
350        let b = self.blocks[x][y].take();
351        Ok(b)
352    }
353
354    /// iterate over all the blocks
355    pub fn block_iter(&self) -> impl Iterator<Item = (GridPos, &Placement)> {
356        self.blocks
357            .iter()
358            .enumerate()
359            .filter_map(|(i, p)| Some((GridPos(i / self.height, i % self.height), p.as_ref()?)))
360    }
361
362    #[must_use]
363    /// see how much this schematic costs.
364    /// returns (cost, `is_sandbox`)
365    /// ```
366    /// # use mindus::Schematic;
367    /// # use mindus::block::Rotation;
368    /// # use mindus::block::CYCLONE;
369    ///
370    /// let mut s = Schematic::new(5, 5);
371    /// s.put(1, 1, &CYCLONE);
372    /// assert_eq!(s.compute_total_cost().0.get_total(), 405);
373    /// ```
374    pub fn compute_total_cost(&self) -> (ItemStorage, bool) {
375        let mut cost = ItemStorage::new();
376        let mut sandbox = false;
377        for &Placement { block, .. } in self.blocks.iter().filter_map(|b| b.as_ref()) {
378            if let Some(curr) = block.get_build_cost() {
379                cost.add_all(&curr, u32::MAX);
380            } else {
381                sandbox = true;
382            }
383        }
384        (cost, sandbox)
385    }
386}
387
388/// error created by creating a new schematic
389#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
390pub enum NewError {
391    #[error("invalid schematic width ({0})")]
392    Width(usize),
393    #[error("invalid schematic height ({0})")]
394    Height(usize),
395}
396/// error created by doing stuff out of bounds
397#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
398#[error("position {x} / {y} out of bounds {w} / {h}")]
399pub struct PosError {
400    pub x: usize,
401    pub y: usize,
402    pub w: usize,
403    pub h: usize,
404}
405
406#[derive(Debug, Error)]
407pub enum PlaceError {
408    #[error("invalid block placement {x} / {y} (size {sz}) within {w} / {h}")]
409    Bounds {
410        x: usize,
411        y: usize,
412        sz: u8,
413        w: usize,
414        h: usize,
415    },
416    #[error("overlapping an existing block at {x} / {y}")]
417    Overlap { x: usize, y: usize },
418    #[error("block state deserialization failed")]
419    Deserialize(#[from] block::DeserializeError),
420}
421
422#[derive(Debug, Error)]
423pub enum ResizeError {
424    #[error("invalid target width ({0})")]
425    TargetWidth(u16),
426    #[error("invalid target height ({0})")]
427    TargetHeight(u16),
428    #[error("horizontal offset {dx} not in [-{new_w}, {old_w}]")]
429    XOffset { dx: i16, old_w: u16, new_w: u16 },
430    #[error("vertical offset {dy} not in [-{new_h}, {old_h}]")]
431    YOffset { dy: i16, old_h: u16, new_h: u16 },
432    #[error(transparent)]
433    Truncated(#[from] TruncatedError),
434}
435
436#[derive(Error, Debug)]
437pub struct TruncatedError {
438    right: u16,
439    top: u16,
440    left: u16,
441    bottom: u16,
442}
443
444impl fmt::Display for TruncatedError {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        macro_rules! fmt_dir {
447            ($f:ident, $first:ident, $name:expr, $value:expr) => {
448                if $value != 0 {
449                    if $first {
450                        f.write_str(" (")?;
451                        $first = false;
452                    } else {
453                        f.write_str(", ")?;
454                    }
455                    write!(f, "{}: {}", $name, $value)?;
456                }
457            };
458        }
459
460        f.write_str("truncated blocks")?;
461        let mut first = true;
462        fmt_dir!(f, first, "right", self.right);
463        fmt_dir!(f, first, "top", self.top);
464        fmt_dir!(f, first, "left", self.left);
465        fmt_dir!(f, first, "bottom", self.bottom);
466        if !first {
467            f.write_char(')')?;
468        }
469        Ok(())
470    }
471}
472
473const SCHEMATIC_HEADER: u32 =
474    ((b'm' as u32) << 24) | ((b's' as u32) << 16) | ((b'c' as u32) << 8) | (b'h' as u32);
475
476impl Serializable for Schematic {
477    type ReadError = ReadError;
478    type WriteError = WriteError;
479    fn deserialize(buff: &mut DataRead<'_>) -> Result<Schematic, Self::ReadError> {
480        let hdr = buff.read_u32()?;
481        if hdr != SCHEMATIC_HEADER {
482            return Err(ReadError::Header(hdr));
483        }
484        let version = buff.read_u8()?;
485        if version > 1 {
486            return Err(ReadError::Version(version));
487        }
488        let buff = buff.deflate()?;
489        let mut buff = DataRead::new(&buff);
490        let w = buff.read_i16()? as usize;
491        let h = buff.read_i16()? as usize;
492        if w > MAX_DIMENSION || h > MAX_DIMENSION {
493            return Err(ReadError::Dimensions(w, h));
494        }
495        let mut schematic = Schematic::new(w, h);
496        buff.read_map(&mut schematic.tags)?;
497        let num_table = buff.read_i8()?;
498        if num_table < 0 {
499            return Err(ReadError::TableSize(num_table));
500        }
501        let mut block_table = Vec::with_capacity(num_table as usize);
502        for _ in 0..num_table {
503            let name = buff.read_utf()?;
504            block_table.push(
505                BLOCK_REGISTRY
506                    .get(name)
507                    .copied()
508                    // wont get rendered
509                    .unwrap_or(&crate::block::METAL_FLOOR),
510            );
511        }
512        let num_blocks = buff.read_i32()?;
513        if num_blocks < 0 || num_blocks as u32 > MAX_BLOCKS {
514            return Err(ReadError::BlockCount(num_blocks));
515        }
516        for _ in 0..num_blocks {
517            let idx = buff.read_i8()?;
518            if idx < 0 || idx as usize >= block_table.len() {
519                return Err(ReadError::BlockIndex(idx, block_table.len()));
520            }
521            let pos = GridPos::from(buff.read_u32()?);
522            let block = block_table[idx as usize];
523            let config = if version < 1 {
524                block.data_from_i32(buff.read_i32()?, pos)?
525            } else {
526                DynData::deserialize(&mut buff)?
527            };
528            let rot = Rotation::from(buff.read_u8()?);
529            schematic.set(pos.0, pos.1, block, config, rot)?;
530        }
531        Ok(schematic)
532    }
533
534    fn serialize(&self, buff: &mut DataWrite<'_>) -> Result<(), Self::WriteError> {
535        // write the header first just in case
536        buff.write_u32(SCHEMATIC_HEADER)?;
537        buff.write_u8(1)?;
538
539        let mut rbuff = DataWrite::default();
540        // don't have to check dimensions because they're already limited to MAX_DIMENSION
541        rbuff.write_i16(self.width as i16)?;
542        rbuff.write_i16(self.height as i16)?;
543        if self.tags.len() > u8::MAX as usize {
544            return Err(WriteError::TagCount(self.tags.len()));
545        }
546        rbuff.write_u8(self.tags.len() as u8)?;
547        for (k, v) in &self.tags {
548            rbuff.write_utf(k)?;
549            rbuff.write_utf(v)?;
550        }
551        // use string keys here to avoid issues with different block refs with the same name
552        let mut block_map = HashMap::new();
553        let mut block_table = Vec::new();
554        let mut block_count = 0i32;
555        for curr in self.blocks.iter().filter_map(|b| b.as_ref()) {
556            block_count += 1;
557            if let Entry::Vacant(e) = block_map.entry(curr.block.name()) {
558                e.insert(block_table.len() as u32);
559                block_table.push(curr.block.name());
560            }
561        }
562        if block_table.len() > i8::MAX as usize {
563            return Err(WriteError::TableSize(block_table.len()));
564        }
565        // else: implies contents are also valid i8 (they're strictly less than the map length)
566        rbuff.write_i8(block_table.len() as i8)?;
567        for &name in &block_table {
568            rbuff.write_utf(name)?;
569        }
570        // don't have to check data.blocks.len() because dimensions don't allow exceeding MAX_BLOCKS
571        rbuff.write_i32(block_count)?;
572        for (pos, curr) in self.block_iter() {
573            rbuff.write_i8(block_map[curr.block.name()] as i8)?;
574            rbuff.write_u32(pos.into())?;
575            let data = match curr.state {
576                None => DynData::Empty,
577                Some(ref s) => curr.block.serialize_state(s)?,
578            };
579            data.serialize(&mut rbuff)?;
580            rbuff.write_u8(curr.rot.into())?;
581        }
582        rbuff.inflate(buff)?;
583        Ok(())
584    }
585}
586
587#[derive(Debug, Error)]
588pub enum ReadError {
589    #[error("failed to read from buffer")]
590    Read(#[from] data::ReadError),
591    #[error("incorrect header ({0:08X})")]
592    Header(u32),
593    #[error("unsupported version ({0})")]
594    Version(u8),
595    #[error("invalid schematic dimensions ({0} / {1})")]
596    Dimensions(usize, usize),
597    #[error("invalid block table size ({0})")]
598    TableSize(i8),
599    #[error("unknown block {0:?}")]
600    NoSuchBlock(String),
601    #[error("invalid total block count ({0})")]
602    BlockCount(i32),
603    #[error("invalid block index ({0} / {1})")]
604    BlockIndex(i8, usize),
605    #[error("block config conversion failed")]
606    BlockConfig(#[from] block::DataConvertError),
607    #[error("failed to read block data")]
608    ReadState(#[from] dynamic::ReadError),
609    #[error("deserialized block could not be placed")]
610    Placement(#[from] PlaceError),
611    #[error(transparent)]
612    Decompress(#[from] super::DecompressError),
613}
614
615#[derive(Debug, Error)]
616pub enum WriteError {
617    #[error("failed to write data to buffer")]
618    Write(#[from] data::WriteError),
619    #[error("tag list too long ({0})")]
620    TagCount(usize),
621    #[error("block table too long ({0})")]
622    TableSize(usize),
623    #[error(transparent)]
624    StateSerialize(#[from] block::SerializeError),
625    #[error("failed to write block data")]
626    WriteState(#[from] dynamic::WriteError),
627    #[error(transparent)]
628    Compress(#[from] super::CompressError),
629}
630
631impl Schematic {
632    /// deserializes a schematic from base64
633    /// ```
634    /// # use mindus::*;
635    /// let string = "bXNjaAF4nGNgZmBmZmDJS8xNZeBOyslPzlYAkwzcKanFyUWZBSWZ+XkMDAxsOYlJqTnFDEzRsYwMfAWJlTn5iSm6RfmlJalFQGlGEGJkZWSYxQAAcBkUPA==";
636    /// let s = Schematic::deserialize_base64(string).unwrap();
637    /// assert!(s.get(1, 1).unwrap().unwrap().block.name() == "payload-router");
638    /// ```
639    pub fn deserialize_base64(data: &str) -> Result<Schematic, R64Error> {
640        let mut buff = vec![0; data.len() / 4 * 3 + 1];
641        let n_out = base64::decode(data.as_bytes(), buff.as_mut())?;
642        Ok(Self::deserialize(&mut DataRead::new(&buff[..n_out]))?)
643    }
644
645    /// serialize a schematic to base64
646    pub fn serialize_base64(&self) -> Result<String, W64Error> {
647        let mut buff = DataWrite::default();
648        self.serialize(&mut buff)?;
649        let buff = buff.get_written();
650        // round up because of padding
651        let mut text = vec![0; 4 * (buff.len() / 3 + usize::from(buff.len() % 3 != 0))];
652        let n_out = base64::encode(buff, text.as_mut())?;
653        // trailing zeros are valid UTF8, but not valid base64
654        assert_eq!(n_out, text.len());
655        // SAFETY: base64 encoding outputs pure ASCII (hopefully)
656        Ok(unsafe { String::from_utf8_unchecked(text) })
657    }
658}
659
660#[derive(Debug, Error)]
661pub enum R64Error {
662    #[error("base-64 decoding failed")]
663    Base64(#[from] base64::DecodeError),
664    #[error(transparent)]
665    Content(#[from] ReadError),
666}
667
668#[derive(Debug, Error)]
669pub enum W64Error {
670    #[error("base-64 encoding failed")]
671    Base64(#[from] base64::EncodeError),
672    #[error(transparent)]
673    Content(#[from] WriteError),
674}
675
676#[cfg(test)]
677mod test {
678    use super::*;
679    #[track_caller]
680    fn unwrap_pretty<T, E: std::fmt::Display + std::error::Error>(r: Result<T, E>) -> T {
681        match r {
682            Ok(t) => t,
683            Err(e) => {
684                use std::error::Error;
685                eprintln!("{e}");
686                let mut err_ref = &e as &dyn Error;
687                loop {
688                    let Some(next) = err_ref.source() else {
689                        panic!();
690                    };
691                    eprintln!("\tFrom: {next}");
692                    err_ref = next;
693                }
694            }
695        }
696    }
697
698    macro_rules! test_schem {
699        ($name:ident, $($val:expr);+;) => {
700            #[test]
701            fn $name() {
702                $(
703                    let parsed = unwrap_pretty(Schematic::deserialize_base64($val));
704                    println!("\x1b[38;5;2mdeserialized\x1b[0m {}", parsed.tags.get("name").unwrap());
705                    let unparsed = unwrap_pretty(parsed.serialize_base64());
706                    println!("\x1b[38;5;2mserialized\x1b[0m {}", parsed.tags.get("name").unwrap());
707                    let parsed2 = unwrap_pretty(Schematic::deserialize_base64(&unparsed));
708                    println!("\x1b[38;5;2mredeserialized\x1b[0m {}", parsed.tags.get("name").unwrap());
709                    if parsed != parsed2 {
710                        #[cfg(feature = "bin")]
711                        parsed2.render().save("p2.png");
712                        #[cfg(feature = "bin")]
713                        parsed.render().save("p1.png");
714                        panic!("DIFFERENT! see `p1.png` != `p2.png`")
715                    }
716                )*
717            }
718        };
719    }
720
721    test_schem! {
722        ser_de,
723        "bXNjaAF4nCVNy07DMBCcvC1c4MBnoNz4G8TBSSxRycSRbVr646iHlmUc2/KOZ3dmFo9QDdrVfFkMb9Gsi5mgFxvncNzS0a8Aemcm6yLq948Bz2eTbBjtTwpmTj7gafs00Y6zX0/2Qt6dzLdLeNj8mbrVLxZ6ciamcQlH59BHH5iAYTKJeOGCF6AisFSoBxF55V+hJm1Lvwca8lpVIuzlS0eGLoMqTGUG6OLRJes3Mw40E5ijc2QedkPuU3DfLX0eHriDsgMapaScu9zkT26o5Uq8EmV/zS5vi4tr/wHvJE7M";
724        "bXNjaAF4nE2MzWrEMAyEJ7bjdOnPobDQvfUF8kSlhyTWFlOv3VWcQvv0lRwoawzSjL4ZHOAtXJ4uhEdi+oz8ek5bDCvuA60Lx68aSwbg0zRTWmHe3j2emWI+F14ojEvJYYsVD5RoqVzSzy8xDjNNlzGXQHi5gVO8SvnIZasCnW4uM8fwQf9tT9+Ua1OUV0GBI9ozHToY6IeDtaIACxkOnaoe1rVrg2RV1cP0CuycLA5+LxuUU+U055W0Yrb4sEcGNQ3u1NTh9iHmH6qaOTI=";
725        "bXNjaAF4nE2R226kQAxEzW1oYC5kopV23/IDfMw+R3ng0klaYehsD6w2+fqtamuiDILCLtvH9MgPaTLJl/5ipR3cy4MN9s2FB//PTVaayV7H4N5X5xcR2c39YOerpI9Pe/kVrFuefRjt1A3BTS+2G/0ybW6V41+7rDGyy9UGjNnGtQt+W78C7ZCcgVSD7S/d4kH8+W3q7P5sbrr1nb85N9DeznZcg58/PlFxx6V77tqNr/1lQOr0anuQ7eQCCn2QQ6Rvy+z7Cb7Ib9xSSJpICsGDV5bxoVJKxpSRLIdUKrVkBQoSkVxYJDuWq5SaNByboUEYJ5LgmFlZRhdejit6oDO5Uw/trDTqgWfgpCqFiiG91MVL7IJfLKck3NooyBDEZM4Gw+9jtJOEXgQZ7TQAJZSaM+POFd5TSWpIoVHEVsqrlUcX8xq+U2pi94wyCHZpICn625jAGdVy4DxGpdom2gXeKu2OIw+6w5E7UABnMgKO9CgxOukiHBGjmGz1dFp+VQO57cA7pUR4+wVvFd5q9x2aQT0r/Ew4k/FfPyvunjhGaPgPoVJdLw==";
726        "bXNjaAF4nD1TDUxTVxS+r6+vr30triCSVjLXiulKoAjMrJRkU8b4qSgLUAlIZ1rah7yt9L31h1LMMCNbRAQhYrKwOnEslhCcdmzJuohL1DjZT4FJtiVsoG5LFtzmGgGngHm790mam7x77ne+c945934HKIAcB2K3vYUGScXmWvM+TQ3jYhysG8idtNfhYTgfAw8ASFz2RtrlBaKG12VA6X1KMjg8fgfT6KLBJi7osfsYH21oYdpoD6A4NkB7DG7WSQOJl/X4IPYM426loeU0bABSv9vF2p3I1cI4PKyB87AO2gu9gGi1+10+kMTCiCYXGzActvtoWEY+ABhcIgzaOBCJ4EZICYDx6xAV86vCdx2IAS5QJJAEIRkQ4XAjAHSIIITBUCCGRwIuESCgheEIkwgYIpEAF4I3wSw9bWccTpvNmVkZy5raWT1p3r+vajJ2odyQb+HAW9HxvV556vfvpNy4oVUfDyq36Kyqe73xsdemprMyv52uAreYwcXzJaPU+aDp8fFM24nuzUvVqYo9yr7CjFT/aDDzUUz8S8W7g+X3VCpVnargblNubl4kI1q6J+cFPH2HS6VSF5xzZWhCyYCKO2FjqAEprB9WRsJbwNFFoLKhITRCQheBbByQCMAQQwow1I8M9oPJ2870npqvvq5RvvfFyYE3hjrLmst3TixrV0XSN08Uax/UrMSeHdmKDdj8uhh3Pef2Wa+qDljrj82pK+aM300sl0eTrC/rL3zzZKZhRWFMq+mLvvTZb0bbweGZL/85ywwnl4RLzR9MBdIGy0LJowOWHxoOY2EiaJ/7s7ZP0Tg2wjWb3y6Lm3IPRNonw/0yT/+lZsdFy/LmUEp2RojHl68B41zDx43WJ/qANkwdVOvGtxjzpgo/keUURn2XK6zerz9Km10w3Vb8Ww/t/UdmHyx7fXwEcPiP0w1Xx9f+/m/X/d13Wiees8yPnk69ePlS9Yuf9sQf1dvVB27mm68U+51Fj7emzS+mzw1jzwuvTKFXHoK30l9EXctVlozIiSPdpk5djrW965BmV1XW4qsp8kNXmtWztdklXXTa0u6lO0d1+GS3TV/Q95O+17+S23Hs5sIfP4e/uqvd9oo+p7u0cYiPb4+9f/L+Qn3PmuXDdDai/ev0ts69I9nuNTOXp9HfOmoy/a5Y9D2cYYsebq+cKgB1V9vXdYFfOz7vWiVCLNnUUVkLOGO9umVN0jl2KoIjYSINEzgUORoDBKAnJwSLTLikQOBSAoC0ABBAbMgDWYIuBBeFRE7CbBCXCAwxFBAJPRgCSAFADBlykokcZCKHFAkPbSRKRaFUUsRGUyZLTJksMWWyjSlDJKhfFALZmFAJdFPo1+gkQVKXw/EW8/zToeZ5fh0t/H+V6k8+";
727        "bXNjaAF4nGNgZ2BjZmDJS8xNZRByrkzOyc9LdctJLEpVT1F4v3AaA3dKanFyUWZBSWZ+HgMDA1tOYlJqTjEDU3QsFwN/eWJJapFuakVJUWJySX4RA3tSYglQpJKBPRliEgNXQX45UElefkoqA39SUWZKeqpucn5eWWolSDmQlVKaWcIgkpyfm1RaDLJDNz01L7UoEWQaf3JRZX5aTmlmim5uZkUqUCA3M7koX7egKD85tbgYqIIlIzEzB+gqQQYwYGYEEkwgxMjAAuQCKSYOZgam//8ZWP7/+/+HgZGZBSTPDlEGVMEKVssAooAMNqAWBpA0SCdQKTMDMzvEZAZGRhCXBZXLyv7///8cIC4AKgZaCOKGAHEh0DBWBpAKIAW0hQNkAR9Q16+KOXtDbhfNNhBQneu5XNV+o/0FSYFCtbrHC+dm3v/MnK3TnKUYGCv0+X3uygksNwzr3jbr755T/O3NuiOxk+7YX7lSoNb3oW3CUq7vKxq4bn1rUKqJsqldfsLX2KkoICQ679L8bW8fCLaY3K+LfGLIe6hoXlaW3iMvrsUq7Hc9Mq1W/OrydlRf+VK9+Ov1iSmsK1deCPKVPF69dG+I5RQ3qSf2PLmlH2bkLwgT4Q3V5+3qnBDPqcG1dNuqZfETim+6G0UqT9b5bGsUznqqb7GZxoxc5eUMT/JvvC79UdlruPvxJis9XhbeTZXLN+68WFB41+DkNRv7uZEGOr/2rvPPu8ZfyXRLI+zoUnmLXRu3+nz0EnP1Omq39TLX3c23cleZ8F62FSnMVCviO2H6tWXB7z2LpA02vleTOXiJK20BT+ADsencfQd0tlqrfQuoWut5dHaX1OP/KwIY5r66Zp4T9+2p241L0XvPfu5w/Zu3bNX77V89kt42zOLf9jJ9vk+msV1vy/Knlywv7Lh4NEY7fvHay0t3Sxo+2q918+je/O23P+50/qEWe45XqGzaR1vh0h1idRwBYZter2DKPJv559VDhbXSHzgin2x8PeXIKsvmtRIVB5V5b/1zcBP+f7VpfuP1OLcJKiePP7n8paroYrul0uF88dp5619+MN8Z7WT0p7DTUqftYOt3XqN51hGf+IVDH0UwcDKwAZMFMwCWiVNe";
728        "bXNjaAF4nGNgZGBkZmDJS8xNZeBMrShIzSvOLEtl4E5JLU4uyiwoyczPY2BgYMtJTErNKWZgio5lZODPzUwuytctKMpPTi0uzi8CyjMygAAfA4PQ+Yo5by9u9GxmZGB9GME502nTzKW+Aht/FJq1ez+o8nzYGn5n+wHR70VVf23t9tutu58/Xbm+qr5t/v+PAa93zIn+L1BpFbXfY17fNf1Jyxd/7X7yMuOv0qjQqNCo0KjQqNCo0KjQqNCo0KjQqNCo0KjQqNCo0KjQqNCoEJWFHp987V9uXjv/9y4GAOhu6pc=";
729        "bXNjaAF4nGNgY2BjZmDJS8xNZWBLTswrSyxm4E5JLU4uyiwoyczPY2BgYMtJTErNKWZgio5lhKthYOBkAAE+IDZjIB8wUWoAC2UGMFHqBSaoF1QYGTycJjFMUFHxVPBkmpQyiYXhpAonQ4OnEAPDJBVWBhXPW0wek7bkTlRhvLXNk4khdzYLQ8M2sAEUeoGFUi+wUBoLLJR5AQDzuCAp";
730        "bXNjaAF4nEWNQRLCIAxFf5O0LhxdewlP5LighQUzCIyl97chVmHx8nmZDyYIQ7J7BUgqruLsw7q8Y22xZABTcnNIK+jxZJyWkv0WGy51S2u4H/Fak2vB/zJww/8MIAVZYh2Gw+jtCx2s+O7pE6nZB0V3bD1sTqtITe8Uc2JOzIm50RpH/U9Bht19AOy5Ge4=";
731        "bXNjaAF4nBXKPQ6AIAwG0I+fuLjrKTyRcUDo0ASKKdXzq8kbHwJCQJTUCLGxEOZCIytfxl0ATDWdVAf8fjgsqRQ2fmhTyl2G6Z2t69fcz62I/gVp0BSJ";
732    }
733
734    #[test]
735    fn block_iter() {
736        macro_rules! test_iter {
737            ($it:ident, $($val:expr;)*) => {
738                $(assert_eq!($it.next().map(|(pos, p)| (pos, p.block)), $val);)*
739            };
740        }
741        macro_rules! pair {
742            ($x:literal,$y:literal,$v:expr) => {
743                Some((GridPos($x, $y), &$v))
744            };
745            () => {
746                None
747            };
748        }
749        use crate::block::*;
750        let mut s = Schematic::new(3, 3);
751        s.put(0, 0, &DISTRIBUTOR)
752            .put(0, 1, &JUNCTION)
753            .put(1, 1, &PHASE_CONVEYOR)
754            .put(2, 0, &ROUTER)
755            .put(1, 1, &CONVEYOR);
756        let mut it = s.block_iter();
757        test_iter![
758            it,
759            pair!(0, 0, DISTRIBUTOR);
760            pair!(0, 1, JUNCTION);
761            pair!(1, 1, CONVEYOR);
762            pair!(2, 0, ROUTER);
763            pair!( );
764        ];
765        let s = Schematic::deserialize_base64("bXNjaAF4nDXKywqAIBQA0fFRBH1itDC7C8E01IT+vgia1VkMFmOwyR3C0N0VG/Mu1ZdwtpATMEa3SazoZdVMPqcudy7/DJovpV4peAAt0xF6").unwrap();
766        let mut it = s.block_iter();
767        test_iter![it,
768            pair!(0, 0, CONVEYOR);
769            pair!(2, 1, VAULT);
770            pair!();
771        ];
772    }
773}