mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'src/data/schematic.rs')
| -rw-r--r-- | src/data/schematic.rs | 990 |
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(®); /// 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(®); $( - 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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"; + "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(®); + + 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 - ); } |