mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'src/data/schematic.rs')
-rw-r--r--src/data/schematic.rs990
1 files changed, 274 insertions, 716 deletions
diff --git a/src/data/schematic.rs b/src/data/schematic.rs
index 75378ba..2a71b3e 100644
--- a/src/data/schematic.rs
+++ b/src/data/schematic.rs
@@ -2,25 +2,24 @@
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt::{self, Write};
-use std::iter::FusedIterator;
-use std::slice::Iter;
use thiserror::Error;
use crate::block::{self, Block, BlockRegistry, Rotation, State};
use crate::data::base64;
use crate::data::dynamic::{self, DynData, DynSerializer};
+use crate::data::renderer::*;
use crate::data::{self, DataRead, DataWrite, GridPos, Serializer};
use crate::item::storage::ItemStorage;
use crate::registry::RegistryEntry;
+use crate::utils::array::Array2D;
/// biggest schematic
-pub const MAX_DIMENSION: u16 = 256;
+pub const MAX_DIMENSION: usize = 256;
/// most possible blocks
pub const MAX_BLOCKS: u32 = 256 * 256;
/// a placement in a schematic
pub struct Placement<'l> {
- pub pos: GridPos,
pub block: &'l Block,
pub rot: Rotation,
state: Option<State>,
@@ -28,11 +27,26 @@ pub struct Placement<'l> {
impl PartialEq for Placement<'_> {
fn eq(&self, rhs: &Placement<'_>) -> bool {
- self.pos == rhs.pos && self.block == rhs.block && self.rot == rhs.rot
+ self.block == rhs.block && self.rot == rhs.rot
+ }
+}
+
+impl fmt::Debug for Placement<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+ write!(f, "P<{}[*{}]>", self.block.name(), self.rot.ch())
}
}
impl<'l> Placement<'l> {
+ /// make a placement from a block
+ pub fn new(block: &'l Block) -> Self {
+ Self {
+ block,
+ rot: Rotation::Up,
+ state: None,
+ }
+ }
+
/// gets the current state of this placement. you can cast it with `placement.block::get_state(placement.get_state()?)?`
#[must_use]
pub fn get_state(&self) -> Option<&State> {
@@ -45,8 +59,8 @@ impl<'l> Placement<'l> {
}
/// draws this placement in particular
- pub fn image(&self) -> crate::data::renderer::ImageHolder {
- self.block.image(self.get_state())
+ pub fn image(&self, context: Option<&RenderingContext>) -> ImageHolder {
+ self.block.image(self.get_state(), context)
}
/// set the state
@@ -61,11 +75,40 @@ impl<'l> Placement<'l> {
}
}
+impl<'l> BlockState<'l> for Placement<'l> {
+ fn get_block(&self) -> Option<&'l Block> {
+ Some(self.block)
+ }
+}
+
+impl RotationState for Placement<'_> {
+ fn get_rotation(&self) -> Option<Rotation> {
+ Some(self.rot)
+ }
+}
+
+impl<'l> BlockState<'l> for Option<Placement<'l>> {
+ fn get_block(&self) -> Option<&'l Block> {
+ let Some(p) = self else {
+ return None;
+ };
+ Some(p.block)
+ }
+}
+
+impl RotationState for Option<Placement<'_>> {
+ fn get_rotation(&self) -> Option<Rotation> {
+ let Some(p) = self else {
+ return None;
+ };
+ Some(p.rot)
+ }
+}
+
// manual impl because trait objects cannot be cloned
impl<'l> Clone for Placement<'l> {
fn clone(&self) -> Self {
Self {
- pos: self.pos,
block: self.block,
state: match self.state {
None => None,
@@ -76,14 +119,14 @@ impl<'l> Clone for Placement<'l> {
}
}
-#[derive(Clone)]
+#[derive(Clone, Debug)]
/// a schematic.
pub struct Schematic<'l> {
- pub width: u16,
- pub height: u16,
+ pub width: usize,
+ pub height: usize,
pub tags: HashMap<String, String>,
- pub blocks: Vec<Placement<'l>>,
- lookup: Vec<Option<usize>>,
+ /// schems can have holes, so [Option] is used.
+ pub blocks: Array2D<Option<Placement<'l>>>,
}
impl<'l> PartialEq for Schematic<'l> {
@@ -102,7 +145,7 @@ impl<'l> Schematic<'l> {
/// # use mindus::Schematic;
/// let s = Schematic::new(5, 5);
/// ```
- pub fn new(width: u16, height: u16) -> Self {
+ pub fn new(width: usize, height: usize) -> Self {
match Self::try_new(width, height) {
Ok(s) => s,
Err(NewError::Width(w)) => panic!("invalid schematic width ({w})"),
@@ -110,12 +153,38 @@ impl<'l> Schematic<'l> {
}
}
+ /// the area around a point
+ pub(crate) fn cross(&self, c: &PositionContext) -> Cross {
+ let get = |x, y| {
+ let b = self.get(x?, y?).ok()??;
+ Some((b.get_block()?, b.get_rotation()?))
+ };
+ macro_rules! s {
+ ($x:expr) => {
+ Some($x)
+ };
+ ($a:expr => $b:expr) => {
+ if $a < $b {
+ None
+ } else {
+ Some($a - $b)
+ }
+ };
+ }
+ [
+ get(s!(c.position.0), s!(c.position.1 + 1)),
+ get(s!(c.position.0 + 1), s!(c.position.1)),
+ get(s!(c.position.0), s!(c.position.1 => 1)),
+ get(s!(c.position.0 => 1), s!(c.position.1)),
+ ]
+ }
+
/// create a new schematic, erroring if too big
/// ```
/// # use mindus::Schematic;
/// assert!(Schematic::try_new(500, 500).is_err() == true);
/// ```
- pub fn try_new(width: u16, height: u16) -> Result<Self, NewError> {
+ pub fn try_new(width: usize, height: usize) -> Result<Self, NewError> {
if width > MAX_DIMENSION {
return Err(NewError::Width(width));
}
@@ -130,66 +199,52 @@ impl<'l> Schematic<'l> {
width,
height,
tags,
- blocks: Vec::new(),
- lookup: Vec::new(),
+ blocks: Array2D::new(None, width, height),
})
}
- #[must_use]
- /// have blocks?
- pub fn is_empty(&self) -> bool {
- self.blocks.is_empty()
- }
+ // #[must_use]
+ // /// check if a rect is empty
+ // /// ```
+ // /// # use mindus::Schematic;
+ // /// # use mindus::block::distribution::ROUTER;
+ // /// let mut s = Schematic::new(5, 5);
+ // /// s.put(0, 0, &ROUTER);
+ // /// assert!(s.is_region_empty(1, 1, 4, 4));
+ // /// s.put(2, 2, &ROUTER);
+ // /// assert!(s.is_region_empty(1, 1, 4, 4) == false);
+ // /// // out of bounds is empty
+ // /// assert!(s.is_region_empty(25, 25, 0, 0));
+ // /// ```
+ // pub fn is_region_empty(&self, x: usize, y: usize, w: usize, h: usize) -> bool {
+ // if x >= self.width || y >= self.height || w == 0 || h == 0 {
+ // return true;
+ // }
+ // if w > 1 || h > 1 {
+ // for y in y..(y + h).min(self.height) {
+ // for x in x..(x + w).min(self.width) {
+ // if self.get(x, y).unwrap().is_some() {
+ // return false;
+ // }
+ // }
+ // }
+ // true
+ // } else {
+ // self.get(x, y).unwrap().is_none()
+ // }
+ // }
- #[must_use]
- /// count blocks
- pub fn get_block_count(&self) -> usize {
- self.blocks.len()
- }
-
- #[must_use]
- /// check if a rect is empty
+ /// gets a block
/// ```
/// # use mindus::Schematic;
- /// let s = Schematic::new(5, 5);
- /// assert!(s.is_region_empty(1, 1, 4, 4) == true);
+ /// # use mindus::block::Rotation;
+ ///
+ /// let mut s = Schematic::new(5, 5);
+ /// assert!(s.get(0, 0).unwrap().is_none());
+ /// s.put(0, 0, &mindus::block::turrets::DUO);
+ /// assert!(s.get(0, 0).unwrap().is_some());
/// ```
- pub fn is_region_empty(&self, x: u16, y: u16, w: u16, h: u16) -> bool {
- if self.blocks.is_empty() {
- return true;
- }
- if x >= self.width || y >= self.height || w == 0 || h == 0 {
- return true;
- }
- if w > 1 || h > 1 {
- let stride = self.width as usize;
- let x_end = if self.width - x > w {
- x + w
- } else {
- self.width
- } as usize;
- let y_end = if self.height - y > h {
- y + h
- } else {
- self.height
- } as usize;
- let x = x as usize;
- let y = y as usize;
- for cy in y..y_end {
- for cx in x..x_end {
- if self.lookup[cx + cy * stride].is_some() {
- return false;
- }
- }
- }
- true
- } else {
- self.lookup[(x as usize) + (y as usize) * (self.width as usize)].is_none()
- }
- }
-
- /// gets a block
- pub fn get(&self, x: u16, y: u16) -> Result<Option<&Placement<'l>>, PosError> {
+ pub fn get(&self, x: usize, y: usize) -> Result<Option<&Placement<'l>>, PosError> {
if x >= self.width || y >= self.height {
return Err(PosError {
x,
@@ -198,18 +253,15 @@ impl<'l> Schematic<'l> {
h: self.height,
});
}
- if self.blocks.is_empty() {
- return Ok(None);
- }
- let pos = (x as usize) + (y as usize) * (self.width as usize);
- match self.lookup[pos] {
- None => Ok(None),
- Some(idx) => Ok(Some(&self.blocks[idx])),
+ let b = &self.blocks[x][y];
+ match b {
+ Some(b) => Ok(Some(b)),
+ _ => Ok(None),
}
}
/// gets a block, mutably
- pub fn get_mut(&mut self, x: u16, y: u16) -> Result<Option<&mut Placement<'l>>, PosError> {
+ pub fn get_mut(&mut self, x: usize, y: usize) -> Result<Option<&mut Placement<'l>>, PosError> {
if x >= self.width || y >= self.height {
return Err(PosError {
x,
@@ -218,68 +270,24 @@ impl<'l> Schematic<'l> {
h: self.height,
});
}
- if self.blocks.is_empty() {
- return Ok(None);
- }
- let pos = (x as usize) + (y as usize) * (self.width as usize);
- match self.lookup[pos] {
- None => Ok(None),
- Some(idx) => Ok(Some(&mut self.blocks[idx])),
+ let b = &mut self.blocks[x][y];
+ match b {
+ Some(b) => Ok(Some(b)),
+ _ => Ok(None),
}
}
- fn remove(&mut self, idx: usize) -> Placement<'l> {
- // swap_remove not only avoids moves in self.blocks but also reduces the lookup changes we have to do
- let prev = self.blocks.swap_remove(idx);
- self.fill_lookup(
- prev.pos.0 as usize,
- prev.pos.1 as usize,
- prev.block.get_size() as usize,
- None,
- );
- if idx < self.blocks.len() {
- // fix the swapped block's lookup entries
- let swapped = &self.blocks[idx];
- self.fill_lookup(
- swapped.pos.0 as usize,
- swapped.pos.1 as usize,
- swapped.block.get_size() as usize,
- Some(idx),
- );
- }
- prev
- }
-
- fn fill_lookup(&mut self, x: usize, y: usize, sz: usize, val: Option<usize>) {
- if self.lookup.is_empty() {
- self.lookup
- .resize((self.width as usize) * (self.height as usize), None);
- }
- if sz > 1 {
- let off = (sz - 1) / 2;
- let (x0, y0) = (x - off, y - off);
- for dy in 0..sz {
- for dx in 0..sz {
- self.lookup[(x0 + dx) + (y0 + dy) * (self.width as usize)] = val;
- }
- }
- } else {
- self.lookup[x + y * (self.width as usize)] = val;
- }
- }
-
- /// put a block in (same as [Schematic::set], but less arguments)
+ /// put a block in (same as [Schematic::set], but less arguments and builder-ness). panics!!!
/// ```
/// # use mindus::Schematic;
- /// # use mindus::DynData;
- /// # use mindus::block::Rotation;
///
/// let mut s = Schematic::new(5, 5);
/// s.put(0, 0, &mindus::block::distribution::ROUTER);
/// assert!(s.get(0, 0).unwrap().is_some() == true);
/// ```
- pub fn put(&mut self, x: u16, y: u16, block: &'l Block) -> Result<&Placement<'l>, PlaceError> {
- self.set(x, y, block, DynData::Empty, Rotation::Up)
+ pub fn put(&mut self, x: usize, y: usize, block: &'l Block) -> &mut Self {
+ self.set(x, y, block, DynData::Empty, Rotation::Up).unwrap();
+ self
}
/// set a block
@@ -294,58 +302,17 @@ impl<'l> Schematic<'l> {
/// ```
pub fn set(
&mut self,
- x: u16,
- y: u16,
+ x: usize,
+ y: usize,
block: &'l Block,
data: DynData,
rot: Rotation,
- ) -> Result<&Placement<'l>, PlaceError> {
- let sz = u16::from(block.get_size());
- let off = (sz - 1) / 2;
- if x < off || y < off {
- return Err(PlaceError::Bounds {
- x,
- y,
- sz: block.get_size(),
- w: self.width,
- h: self.height,
- });
- }
- if self.width - x < sz - off || self.height - y < sz - off {
- return Err(PlaceError::Bounds {
- x,
- y,
- sz: block.get_size(),
- w: self.width,
- h: self.height,
- });
- }
- if self.is_region_empty(x - off, y - off, sz, sz) {
- let idx = self.blocks.len();
- let state = block.deserialize_state(data)?;
- self.blocks.push(Placement {
- pos: GridPos(x, y),
- block,
- state,
- rot,
- });
- self.fill_lookup(x as usize, y as usize, block.get_size() as usize, Some(idx));
- Ok(&self.blocks[idx])
- } else {
- Err(PlaceError::Overlap { x, y })
- }
- }
-
- pub fn replace(
- &mut self,
- x: u16,
- y: u16,
- block: &'l Block,
- data: DynData,
- rot: Rotation,
- collect: bool,
- ) -> Result<Option<Vec<Placement<'l>>>, PlaceError> {
- let sz = u16::from(block.get_size());
+ ) -> Result<(), PlaceError> {
+ println!(
+ "putting {block:?} at {x} / {y} ({}/{})",
+ self.width, self.height
+ );
+ let sz = usize::from(block.get_size());
let off = (sz - 1) / 2;
if x < off || y < off {
return Err(PlaceError::Bounds {
@@ -365,74 +332,17 @@ impl<'l> Schematic<'l> {
h: self.height,
});
}
- if sz > 1 {
- let mut result = if collect { Some(Vec::new()) } else { None };
- // remove all blocks in the region
- for dy in 0..(sz as usize) {
- for dx in 0..(sz as usize) {
- if let Some(idx) =
- self.lookup[(x as usize + dx) + (y as usize + dy) * (self.width as usize)]
- {
- let prev = self.remove(idx);
- if let Some(ref mut v) = result {
- v.push(prev);
- }
- }
- }
- }
- let idx = self.blocks.len();
- let state = block.deserialize_state(data)?;
- self.blocks.push(Placement {
- pos: GridPos(x, y),
- block,
- state,
- rot,
- });
- self.fill_lookup(x as usize, y as usize, sz as usize, Some(idx));
- Ok(result)
- } else {
- let pos = (x as usize) + (y as usize) * (self.width as usize);
- match self.lookup[pos] {
- None => {
- let idx = self.blocks.len();
- let state = block.deserialize_state(data)?;
- self.blocks.push(Placement {
- pos: GridPos(x, y),
- block,
- state,
- rot,
- });
- self.lookup[pos] = Some(idx);
- Ok(if collect { Some(Vec::new()) } else { None })
- }
- Some(idx) => {
- let state = block.deserialize_state(data)?;
- let prev = std::mem::replace(
- &mut self.blocks[idx],
- Placement {
- pos: GridPos(x, y),
- block,
- state,
- rot,
- },
- );
- self.fill_lookup(
- prev.pos.0 as usize,
- prev.pos.1 as usize,
- prev.block.get_size() as usize,
- None,
- );
- self.fill_lookup(x as usize, y as usize, sz as usize, Some(idx));
- Ok(if collect { Some(vec![prev]) } else { None })
- }
- }
- }
+ let state = block.deserialize_state(data)?;
+ let p = Placement { block, state, rot };
+ self.blocks[x][y] = Some(p);
+ Ok(())
}
/// take out a block
/// ```
/// # use mindus::Schematic;
/// # use mindus::DynData;
+
/// # use mindus::block::Rotation;
///
/// let mut s = Schematic::new(5, 5);
@@ -441,7 +351,7 @@ impl<'l> Schematic<'l> {
/// assert!(s.take(0, 0).unwrap().is_some() == true);
/// assert!(s.get(0, 0).unwrap().is_none() == true);
/// ```
- pub fn take(&mut self, x: u16, y: u16) -> Result<Option<Placement<'l>>, PosError> {
+ pub fn take(&mut self, x: usize, y: usize) -> Result<Option<Placement<'l>>, PosError> {
if x >= self.width || y >= self.height {
return Err(PosError {
x,
@@ -450,207 +360,36 @@ impl<'l> Schematic<'l> {
h: self.height,
});
}
- if self.blocks.is_empty() {
- Ok(None)
- } else {
- let pos = (x as usize) + (y as usize) * (self.width as usize);
- match self.lookup[pos] {
- None => Ok(None),
- Some(idx) => Ok(Some(self.remove(idx))),
- }
- }
- }
-
- fn rebuild_lookup(&mut self) {
- self.lookup.clear();
- if !self.blocks.is_empty() {
- self.lookup
- .resize((self.width as usize) * (self.height as usize), None);
- for (i, curr) in self.blocks.iter().enumerate() {
- let sz = curr.block.get_size() as usize;
- let x = curr.pos.0 as usize - (sz - 1) / 2;
- let y = curr.pos.1 as usize - (sz - 1) / 2;
- if sz > 1 {
- for dy in 0..sz {
- for dx in 0..sz {
- self.lookup[(x + dx) + (y + dy) * (self.width as usize)] = Some(i);
- }
- }
- } else {
- self.lookup[x + y * (self.width as usize)] = Some(i);
- }
- }
- }
- }
-
- /// flip it
- pub fn mirror(&mut self, horizontally: bool, vertically: bool) {
- if !self.blocks.is_empty() && (horizontally || vertically) {
- for curr in &mut self.blocks {
- // because position is the bottom left of the center (which changes during mirroring)
- let shift = (u16::from(curr.block.get_size()) - 1) % 2;
- if horizontally {
- curr.pos.0 = self.width - 1 - curr.pos.0 - shift;
- }
- if vertically {
- curr.pos.1 = self.height - 1 - curr.pos.1 - shift;
- }
- if !curr.block.is_symmetric() {
- curr.rot.mirror(horizontally, vertically);
- }
- if let Some(ref mut state) = curr.state {
- curr.block.mirror_state(state, horizontally, vertically);
- }
- }
- self.rebuild_lookup();
- }
- }
-
- /// turn
- /// ```
- /// # use mindus::Schematic;
- /// # use mindus::DynData;
- /// # use mindus::block::Rotation;
- ///
- /// let mut s = Schematic::new(5, 5);
- /// // 0, 0 == bottom left
- /// s.put(0, 0, &mindus::block::turrets::HAIL);
- /// s.rotate(true);
- /// assert!(s.get(0, 4).unwrap().is_some() == true);
- /// ```
- pub fn rotate(&mut self, clockwise: bool) {
- let w = self.width;
- let h = self.height;
- self.width = h;
- self.height = w;
- if !self.blocks.is_empty() {
- for curr in &mut self.blocks {
- let x = curr.pos.0;
- let y = curr.pos.1;
- // because position is the bottom left of the center (which changes during rotation)
- let shift = (u16::from(curr.block.get_size()) - 1) % 2;
- if clockwise {
- curr.pos.0 = y;
- curr.pos.1 = w - 1 - x - shift;
- } else {
- curr.pos.0 = h - 1 - y - shift;
- curr.pos.1 = x;
- }
- if !curr.block.is_symmetric() {
- curr.rot.rotate(clockwise);
- }
- if let Some(ref mut state) = curr.state {
- curr.block.rotate_state(state, clockwise);
- }
- }
- self.rebuild_lookup();
- }
- }
-
- /// resize this schematic
- pub fn resize(&mut self, dx: i16, dy: i16, w: u16, h: u16) -> Result<(), ResizeError> {
- if w > MAX_DIMENSION {
- return Err(ResizeError::TargetWidth(w));
- }
- if h > MAX_DIMENSION {
- return Err(ResizeError::TargetHeight(h));
- }
- if dx <= -(w as i16) || dx >= self.width as i16 {
- return Err(ResizeError::XOffset {
- dx,
- old_w: self.width,
- new_w: w,
- });
- }
- if dy <= -(h as i16) || dy >= self.height as i16 {
- return Err(ResizeError::YOffset {
- dy,
- old_h: self.height,
- new_h: h,
- });
- }
- // check that all blocks fit into the new bounds
- let mut right = 0u16;
- let mut top = 0u16;
- let mut left = 0u16;
- let mut bottom = 0u16;
- let right_bound = dx + w as i16 - 1;
- let top_bound = dy + h as i16 - 1;
- let left_bound = dx;
- let bottom_bound = dy;
- for Placement { pos, block, .. } in &self.blocks {
- let sz = u16::from(block.get_size());
- let (x0, y0, x1, y1) = (
- pos.0 - (sz - 1) / 2,
- pos.1 - (sz - 1) / 2,
- pos.0 + sz / 2,
- pos.1 + sz / 2,
- );
- if (x1 as i16) > right_bound && x1 - right_bound as u16 > right {
- right = x1 - right_bound as u16;
- }
- if (y1 as i16) > top_bound && y1 - top_bound as u16 > top {
- top = y1 - top_bound as u16;
- }
- if (x0 as i16) < left_bound && left_bound as u16 - x0 > left {
- left = left_bound as u16 - x0;
- }
- if (y0 as i16) < bottom_bound && bottom_bound as u16 - y0 > bottom {
- bottom = bottom_bound as u16 - y0;
- }
- }
- if left > 0 || top > 0 || right > 0 || bottom > 0 {
- return Err(TruncatedError {
- right,
- top,
- left,
- bottom,
- })?;
- }
- self.width = w;
- self.height = h;
- for Placement { pos, .. } in &mut self.blocks {
- pos.0 = (pos.0 as i16 + dx) as u16;
- pos.1 = (pos.1 as i16 + dy) as u16;
- }
- Ok(())
- }
-
- /// like rotate(), but 180
- pub fn rotate_180(&mut self) {
- self.mirror(true, true);
- }
-
- #[must_use]
- pub fn pos_iter(&self) -> PosIter {
- PosIter {
- x: 0,
- y: 0,
- w: self.width,
- h: self.height,
- }
+ let b = self.blocks[x][y].take();
+ Ok(b)
}
/// iterate over all the blocks
- pub fn block_iter<'s>(&'s self) -> Iter<'s, Placement<'l>> {
- self.blocks.iter()
+ pub fn block_iter(&self) -> impl Iterator<Item = (GridPos, &Placement<'_>)> {
+ self.blocks.iter().enumerate().filter_map(|(i, p)| {
+ let Some(p) = p else {
+ return None;
+ };
+ Some((GridPos(i / self.height, i % self.height), p))
+ })
}
#[must_use]
/// see how much this schematic costs.
+ /// returns (cost, is_sandbox)
/// ```
/// # use mindus::Schematic;
/// # use mindus::DynData;
/// # use mindus::block::Rotation;
///
/// let mut s = Schematic::new(5, 5);
- /// s.put(0, 0, &mindus::block::turrets::CYCLONE);
- /// // assert_eq!(s.compute_total_cost().0.get_total(), 405);
+ /// s.put(1, 1, &mindus::block::turrets::CYCLONE);
+ /// assert_eq!(s.compute_total_cost().0.get_total(), 405);
/// ```
pub fn compute_total_cost(&self) -> (ItemStorage, bool) {
let mut cost = ItemStorage::new();
let mut sandbox = false;
- for &Placement { block, .. } in &self.blocks {
+ for &Placement { block, .. } in self.blocks.iter().filter_map(|b| b.as_ref()) {
if let Some(curr) = block.get_build_cost() {
cost.add_all(&curr, u32::MAX);
} else {
@@ -665,32 +404,32 @@ impl<'l> Schematic<'l> {
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
pub enum NewError {
#[error("invalid schematic width ({0})")]
- Width(u16),
+ Width(usize),
#[error("invalid schematic height ({0})")]
- Height(u16),
+ Height(usize),
}
/// error created by doing stuff out of bounds
#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
#[error("position {x} / {y} out of bounds {w} / {h}")]
pub struct PosError {
- pub x: u16,
- pub y: u16,
- pub w: u16,
- pub h: u16,
+ pub x: usize,
+ pub y: usize,
+ pub w: usize,
+ pub h: usize,
}
#[derive(Debug, Error)]
pub enum PlaceError {
#[error("invalid block placement {x} / {y} (size {sz}) within {w} / {h}")]
Bounds {
- x: u16,
- y: u16,
+ x: usize,
+ y: usize,
sz: u8,
- w: u16,
- h: u16,
+ w: usize,
+ h: usize,
},
#[error("overlapping an existing block at {x} / {y}")]
- Overlap { x: u16, y: u16 },
+ Overlap { x: usize, y: usize },
#[error("block state deserialization failed")]
Deserialize(#[from] block::DeserializeError),
}
@@ -746,180 +485,6 @@ impl fmt::Display for TruncatedError {
}
}
-impl fmt::Debug for Schematic<'_> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- fmt::Display::fmt(self, f)
- }
-}
-
-impl<'l> fmt::Display for Schematic<'l> {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- /*
- Because characters are about twice as tall as they are wide, two are used to represent a single block.
- Each block has a single letter to describe what it is + an optional rotation.
- For size-1 blocks, that's "*]" for symmetric and "*>", "*^", "*<", "*v" for rotations.
- Larger blocks are formed using pipes, slashes and minuses to form a border, which is filled with spaces.
- Then, the letter is placed inside followed by the rotation (if any).
- */
-
- // find unique letters for each block, more common blocks pick first
- let mut name_cnt = HashMap::<&str, u16>::new();
- for p in &self.blocks {
- match name_cnt.entry(p.block.get_name()) {
- Entry::Occupied(mut e) => *e.get_mut() += 1,
- Entry::Vacant(e) => {
- e.insert(1);
- }
- }
- }
- // only needed the map for counting
- let mut name_cnt = Vec::from_iter(name_cnt);
- name_cnt.sort_by(|l, r| r.1.cmp(&l.1));
- // set for control characters, space, b'*', DEL and b">^<v]/|\\-"
- let mut used = [
- 0xFFu8, 0xFFu8, 0xFFu8, 0xFFu8, 0x01u8, 0xA4u8, 0x00u8, 0x50u8, 0x00u8, 0x00u8, 0x00u8,
- 0x70u8, 0x00u8, 0x00u8, 0x40u8, 0x90u8,
- ];
- let mut types = HashMap::<&str, char>::new();
- for &(name, _) in &name_cnt {
- let mut found = false;
- for c in name.chars() {
- if c > ' ' && c <= '~' {
- let upper = c.to_ascii_uppercase() as usize;
- let lower = c.to_ascii_lowercase() as usize;
- if used[upper >> 3] & (1 << (upper & 7)) == 0 {
- found = true;
- used[upper >> 3] |= 1 << (upper & 7);
- types.insert(name, unsafe { char::from_u32_unchecked(upper as u32) });
- break;
- }
- if lower != upper && used[lower >> 3] & (1 << (lower & 7)) == 0 {
- found = true;
- used[lower >> 3] |= 1 << (lower & 7);
- types.insert(name, unsafe { char::from_u32_unchecked(lower as u32) });
- break;
- }
- }
- }
- if !found {
- // just take whatever symbol's still free (avoids collisions with letters)
- match used.iter().enumerate().find(|(_, &v)| v != u8::MAX) {
- // there's no more free symbols... how? use b'*' instead for all of them (reserved)
- None => {
- types.insert(name, '*');
- }
- Some((i, v)) => {
- let idx = i + v.trailing_ones() as usize;
- used[idx >> 3] |= 1 << (idx & 7);
- types.insert(name, unsafe { char::from_u32_unchecked(idx as u32) });
- }
- }
- }
- }
-
- // coordinates start in the bottom left, so y starts at self.height - 1
- if self.blocks.is_empty() {
- write!(f, "<empty {} * {}>", self.width, self.height)?;
- } else {
- for y in (0..self.height as usize).rev() {
- let mut x = 0usize;
- while x < self.width as usize {
- if let Some(idx) = self.lookup[x + y * (self.width as usize)] {
- let Placement {
- pos,
- block,
- state: _,
- rot,
- } = self.blocks[idx];
- let c = *types.get(block.get_name()).unwrap();
- match block.get_size() as usize {
- 0 => unreachable!(),
- 1 => {
- f.write_char(c)?;
- match rot {
- _ if block.is_symmetric() => f.write_char(']')?,
- Rotation::Right => f.write_char('>')?,
- Rotation::Up => f.write_char('^')?,
- Rotation::Left => f.write_char('<')?,
- Rotation::Down => f.write_char('v')?,
- }
- }
- s => {
- let y0 = pos.1 as usize - (s - 1) / 2;
- if y == y0 + (s - 1) {
- // top row, which looks like /---[...]---\
- f.write_char('/')?;
- if s == 2 {
- // label & rotation are in this row
- f.write_char(c)?;
- match rot {
- _ if block.is_symmetric() => f.write_char('-')?,
- Rotation::Right => f.write_char('>')?,
- Rotation::Up => f.write_char('^')?,
- Rotation::Left => f.write_char('<')?,
- Rotation::Down => f.write_char('v')?,
- }
- } else {
- // label & rotation are not in this row
- for _ in 0..(2 * s - 2) {
- f.write_char('-')?;
- }
- }
- f.write_char('\\')?;
- } else if y == y0 {
- // bottom row, which looks like \---[...]---/
- f.write_char('\\')?;
- for _ in 0..(2 * s - 2) {
- f.write_char('-')?;
- }
- f.write_char('/')?;
- } else if s > 2 && y == y0 + s / 2 {
- // middle row with label
- f.write_char('|')?;
- for cx in 0..(2 * s - 2) {
- if cx == s - 2 {
- f.write_char(c)?;
- } else if cx == s - 1 {
- match rot {
- _ if block.is_symmetric() => f.write_char(' ')?,
- Rotation::Right => f.write_char('>')?,
- Rotation::Up => f.write_char('^')?,
- Rotation::Left => f.write_char('<')?,
- Rotation::Down => f.write_char('v')?,
- }
- } else {
- f.write_char(' ')?;
- }
- }
- f.write_char('|')?;
- } else {
- // middle row, which looks like | [...] |
- f.write_char('|')?;
- for _ in 0..(2 * s - 2) {
- f.write_char(' ')?;
- }
- f.write_char('|')?;
- }
- }
- }
- x += block.get_size() as usize;
- } else {
- f.write_str(" ")?;
- x += 1;
- }
- }
- writeln!(f)?;
- }
- // print the letters assigned to blocks
- for (k, _) in name_cnt {
- let v = *types.get(k).unwrap();
- write!(f, "\n({v}) {k}")?;
- }
- }
- Ok(())
- }
-}
-
const SCHEMATIC_HEADER: u32 =
((b'm' as u32) << 24) | ((b's' as u32) << 16) | ((b'c' as u32) << 8) | (b'h' as u32);
@@ -940,12 +505,12 @@ impl<'l> Serializer<Schematic<'l>> for SchematicSerializer<'l> {
}
let buff = buff.deflate()?;
let mut buff = DataRead::new(&buff);
- let w = buff.read_i16()?;
- let h = buff.read_i16()?;
- if w < 0 || h < 0 || w as u16 > MAX_DIMENSION || h as u16 > MAX_DIMENSION {
+ let w = buff.read_i16()? as usize;
+ let h = buff.read_i16()? as usize;
+ if w > MAX_DIMENSION || h > MAX_DIMENSION {
return Err(ReadError::Dimensions(w, h));
}
- let mut schematic = Schematic::new(w as u16, h as u16);
+ let mut schematic = Schematic::new(w, h);
buff.read_map(&mut schematic.tags)?;
let num_table = buff.read_i8()?;
if num_table < 0 {
@@ -953,23 +518,23 @@ impl<'l> Serializer<Schematic<'l>> for SchematicSerializer<'l> {
}
let mut block_table = Vec::new();
block_table.reserve(num_table as usize);
- for _ in 0..num_table {
+ for _ in 0..dbg!(num_table) {
let name = buff.read_utf()?;
match self.0.get(name) {
None => return Err(ReadError::NoSuchBlock(name.to_owned())),
Some(b) => block_table.push(b),
}
}
- let num_blocks = buff.read_i32()?;
+ let num_blocks = dbg!(buff.read_i32()?);
if num_blocks < 0 || num_blocks as u32 > MAX_BLOCKS {
return Err(ReadError::BlockCount(num_blocks));
}
for _ in 0..num_blocks {
- let idx = buff.read_i8()?;
+ let idx = dbg!(buff.read_i8()?);
if idx < 0 || idx as usize >= block_table.len() {
return Err(ReadError::BlockIndex(idx, block_table.len()));
}
- let pos = GridPos::from(buff.read_u32()?);
+ let pos = dbg!(GridPos::from(buff.read_u32()?));
let block = block_table[idx as usize];
let config = if version < 1 {
block.data_from_i32(buff.read_i32()?, pos)?
@@ -1006,7 +571,9 @@ impl<'l> Serializer<Schematic<'l>> for SchematicSerializer<'l> {
// use string keys here to avoid issues with different block refs with the same name
let mut block_map = HashMap::new();
let mut block_table = Vec::new();
- for curr in &data.blocks {
+ let mut block_count = 0i32;
+ for curr in data.blocks.iter().filter_map(|b| b.as_ref()) {
+ block_count += 1;
if let Entry::Vacant(e) = block_map.entry(curr.block.get_name()) {
e.insert(block_table.len() as u32);
block_table.push(curr.block.get_name());
@@ -1021,20 +588,17 @@ impl<'l> Serializer<Schematic<'l>> for SchematicSerializer<'l> {
rbuff.write_utf(name)?;
}
// don't have to check data.blocks.len() because dimensions don't allow exceeding MAX_BLOCKS
- rbuff.write_i32(data.blocks.len() as i32)?;
- let mut num = 0;
- for curr in &data.blocks {
+ rbuff.write_i32(block_count)?;
+ for (pos, curr) in data.block_iter() {
rbuff.write_i8(block_map[curr.block.get_name()] as i8)?;
- rbuff.write_u32(u32::from(curr.pos))?;
+ rbuff.write_u32(pos.into())?;
let data = match curr.state {
None => DynData::Empty,
Some(ref s) => curr.block.serialize_state(s)?,
};
DynSerializer.serialize(&mut rbuff, &data)?;
rbuff.write_u8(curr.rot.into())?;
- num += 1;
}
- assert_eq!(num, data.blocks.len());
rbuff.inflate(buff)?;
Ok(())
}
@@ -1048,8 +612,8 @@ pub enum ReadError {
Header(u32),
#[error("unsupported version ({0})")]
Version(u8),
- #[error("invalid schematic dimensions ({0} * {1})")]
- Dimensions(i16, i16),
+ #[error("invalid schematic dimensions ({0} / {1})")]
+ Dimensions(usize, usize),
#[error("invalid block table size ({0})")]
TableSize(i8),
#[error("unknown block {0:?}")]
@@ -1064,6 +628,8 @@ pub enum ReadError {
ReadState(#[from] dynamic::ReadError),
#[error("deserialized block could not be placed")]
Placement(#[from] PlaceError),
+ #[error(transparent)]
+ Decompress(#[from] super::DecompressError),
}
#[derive(Debug, Error)]
@@ -1078,6 +644,8 @@ pub enum WriteError {
StateSerialize(#[from] block::SerializeError),
#[error("failed to write block data")]
WriteState(#[from] dynamic::WriteError),
+ #[error(transparent)]
+ Compress(#[from] super::CompressError),
}
impl<'l> SchematicSerializer<'l> {
@@ -1088,7 +656,7 @@ impl<'l> SchematicSerializer<'l> {
/// let reg = build_registry();
/// let mut ss = SchematicSerializer(&reg);
/// let s = ss.deserialize_base64(string).unwrap();
- /// assert!(s.get(0, 0).unwrap().unwrap().block.name() == "payload-router");
+ /// assert!(s.get(1, 1).unwrap().unwrap().block.name() == "payload-router");
/// ```
pub fn deserialize_base64(&mut self, data: &str) -> Result<Schematic<'l>, R64Error> {
let mut buff = Vec::<u8>::new();
@@ -1130,88 +698,46 @@ pub enum W64Error {
Content(#[from] WriteError),
}
-pub struct PosIter {
- x: u16,
- y: u16,
- w: u16,
- h: u16,
-}
-
-impl Iterator for PosIter {
- type Item = GridPos;
-
- fn next(&mut self) -> Option<Self::Item> {
- if self.w > 0 && self.y < self.h {
- let p = GridPos(self.x, self.y);
- self.x += 1;
- if self.x == self.w {
- self.x = 0;
- self.y += 1;
- }
- Some(p)
- } else {
- None
- }
- }
-
- fn size_hint(&self) -> (usize, Option<usize>) {
- let pos = (self.x as usize) + (self.y as usize) * (self.w as usize);
- let end = (self.w as usize) * (self.h as usize);
- (end - pos, Some(end - pos))
- }
-
- fn count(self) -> usize {
- let pos = (self.x as usize) + (self.y as usize) * (self.w as usize);
- let end = (self.w as usize) * (self.h as usize);
- end - pos
- }
-
- fn last(self) -> Option<Self::Item> {
- // self.y < self.h implies self.h > 0
- if self.w > 0 && self.y < self.h {
- Some(GridPos(self.w - 1, self.h - 1))
- } else {
- None
- }
- }
-}
-
-impl FusedIterator for PosIter {}
-
#[cfg(test)]
mod test {
use super::*;
-
- macro_rules! test_iter {
- ($name:ident, $it:expr, $($val:expr),+) => {
- #[test]
- fn $name() {
- let mut it = $it;
- $(test_iter!(impl it, $val);)+
- }
- };
- (impl $it:ident, $val:literal) => {
- for _ in 0..$val {
- assert_ne!($it.next(), None, "iterator returned None too early");
- }
- };
- (impl $it:ident, $val:expr) => {
- assert_eq!($it.next(), $val);
- };
- }
+ fn unwrap_pretty<T, E: std::fmt::Display + std::error::Error>(r: Result<T, E>) -> T {
+ match r {
+ Ok(t) => t,
+ Err(e) => {
+ use std::error::Error;
+ eprintln!("{e}");
+ let mut err_ref = &e as &dyn Error;
+ loop {
+ let Some(next) = err_ref.source() else {
+ panic!();
+ };
+ eprintln!("\tFrom: {next}");
+ err_ref = next;
+ }
+ }
+ }
+ }
macro_rules! test_schem {
- ($name:ident, $($val:expr),+) => {
+ ($name:ident, $($val:expr);+;) => {
#[test]
fn $name() {
let reg = crate::block::build_registry();
let mut ser = SchematicSerializer(&reg);
$(
- let parsed = ser.deserialize_base64($val).unwrap();
- println!("{}", parsed.tags.get("name").unwrap());
- let unparsed = ser.serialize_base64(&parsed).unwrap();
- let parsed2 = ser.deserialize_base64(&unparsed).unwrap();
- assert_eq!(parsed, parsed2);
+ let parsed = unwrap_pretty(ser.deserialize_base64($val));
+ println!("\x1b[38;5;2mdeserialized\x1b[0m {}", parsed.tags.get("name").unwrap());
+ let unparsed = unwrap_pretty(ser.serialize_base64(&parsed));
+ println!("\x1b[38;5;2mserialized\x1b[0m {}", parsed.tags.get("name").unwrap());
+ let parsed2 = unwrap_pretty(ser.deserialize_base64(&unparsed));
+ println!("\x1b[38;5;2mredeserialized\x1b[0m {}", parsed.tags.get("name").unwrap());
+ if parsed != parsed2 {
+ // TODO serialization currently lossy? missing right side mostly
+ parsed2.render().save("p2.png").unwrap();
+ parsed.render().save("p1.png").unwrap();
+ panic!("DIFFERENT! see `p1.png` != `p2.png`")
+ }
)*
}
};
@@ -1219,22 +745,54 @@ mod test {
test_schem! {
ser_de,
- "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"
+ "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";
+ "bXNjaAF4nE2MzWrEMAyEJ7bjdOnPobDQvfUF8kSlhyTWFlOv3VWcQvv0lRwoawzSjL4ZHOAtXJ4uhEdi+oz8ek5bDCvuA60Lx68aSwbg0zRTWmHe3j2emWI+F14ojEvJYYsVD5RoqVzSzy8xDjNNlzGXQHi5gVO8SvnIZasCnW4uM8fwQf9tT9+Ua1OUV0GBI9ozHToY6IeDtaIACxkOnaoe1rVrg2RV1cP0CuycLA5+LxuUU+U055W0Yrb4sEcGNQ3u1NTh9iHmH6qaOTI=";
+ "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==";
+ "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+";
+ "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";
+ "bXNjaAF4nGNgZGBkZmDJS8xNZeBMrShIzSvOLEtl4E5JLU4uyiwoyczPY2BgYMtJTErNKWZgio5lZODPzUwuytctKMpPTi0uzi8CyjMygAAfA4PQ+Yo5by9u9GxmZGB9GME502nTzKW+Aht/FJq1ez+o8nzYGn5n+wHR70VVf23t9tutu58/Xbm+qr5t/v+PAa93zIn+L1BpFbXfY17fNf1Jyxd/7X7yMuOv0qjQqNCo0KjQqNCo0KjQqNCo0KjQqNCo0KjQqNCo0KjQqNCoEJWFHp987V9uXjv/9y4GAOhu6pc=";
+ }
+
+ #[test]
+ fn block_iter() {
+ macro_rules! test_iter {
+ ($it:ident, $($val:expr;)*) => {
+ $(assert_eq!($it.next().map(|(pos, p)| (pos, p.block)), $val);)*
+ };
+ }
+ macro_rules! pair {
+ ($x:literal,$y:literal,$v:expr) => {
+ Some((GridPos($x, $y), &$v))
+ };
+ () => {
+ None
+ };
+ }
+ use crate::block::all::*;
+ let mut s = Schematic::new(3, 3);
+ s.put(0, 0, &DISTRIBUTOR)
+ .put(0, 1, &JUNCTION)
+ .put(1, 1, &PHASE_CONVEYOR)
+ .put(2, 0, &ROUTER)
+ .put(1, 1, &CONVEYOR);
+ let mut it = s.block_iter();
+ test_iter![
+ it,
+ pair!(0, 0, DISTRIBUTOR);
+ pair!(0, 1, JUNCTION);
+ pair!(1, 1, CONVEYOR);
+ pair!(2, 0, ROUTER);
+ pair!( );
+ ];
+ let reg = crate::block::build_registry();
+ let mut s = SchematicSerializer(&reg);
+
+ let s = s.deserialize_base64("bXNjaAF4nDXKywqAIBQA0fFRBH1itDC7C8E01IT+vgia1VkMFmOwyR3C0N0VG/Mu1ZdwtpATMEa3SazoZdVMPqcudy7/DJovpV4peAAt0xF6").unwrap();
+ let mut it = s.block_iter();
+ test_iter![it,
+ pair!(0, 0, CONVEYOR);
+ pair!(2, 1, VAULT);
+ pair!();
+ ];
}
-
- test_iter!(
- block_iter,
- Schematic::new(3, 4).pos_iter(),
- Some(GridPos(0, 0)),
- Some(GridPos(1, 0)),
- Some(GridPos(2, 0)),
- Some(GridPos(0, 1)),
- 7,
- Some(GridPos(2, 3)),
- None
- );
}