use std::any::Any;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{self, Write};
use std::iter::FusedIterator;
use std::slice::Iter;
use flate2::{
Compress, CompressError, Compression, Decompress, DecompressError, FlushCompress,
FlushDecompress, Status,
};
use image::RgbaImage;
use crate::block::{self, Block, BlockRegistry, Rotation};
use crate::data::base64;
use crate::data::dynamic::{self, DynData, DynSerializer};
use crate::data::{self, DataRead, DataWrite, GridPos, Serializer};
use crate::item::storage::Storage as ItemStorage;
use crate::registry::RegistryEntry;
pub const MAX_DIMENSION: u16 = 128;
pub const MAX_BLOCKS: u32 = 128 * 128;
pub struct Placement<'l> {
pub pos: GridPos,
pub block: &'l Block,
state: Option<Box<dyn Any>>,
rot: Rotation,
}
impl PartialEq for Placement<'_> {
fn eq(&self, rhs: &Placement<'_>) -> bool {
self.pos == rhs.pos && self.block == rhs.block && self.rot == rhs.rot
}
}
impl<'l> Placement<'l> {
#[must_use]
pub fn get_state(&self) -> Option<&dyn Any> {
match self.state {
None => None,
Some(ref b) => Some(b.as_ref()),
}
}
pub fn get_state_mut(&mut self) -> Option<&mut dyn Any> {
match self.state {
None => None,
Some(ref mut b) => Some(b.as_mut()),
}
}
pub fn image(&self) -> RgbaImage {
self.block.image(self.get_state())
}
pub fn set_state(
&mut self,
data: DynData,
) -> Result<Option<Box<dyn Any>>, block::DeserializeError> {
let state = self.block.deserialize_state(data)?;
Ok(std::mem::replace(&mut self.state, state))
}
pub fn set_rotation(&mut self, rot: Rotation) -> Rotation {
std::mem::replace(&mut self.rot, 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,
Some(ref s) => Some(self.block.clone_state(s)),
},
rot: self.rot,
}
}
}
#[derive(Clone)]
pub struct Schematic<'l> {
pub width: u16,
pub height: u16,
pub tags: HashMap<String, String>,
pub blocks: Vec<Placement<'l>>,
lookup: Vec<Option<usize>>,
}
impl<'l> PartialEq for Schematic<'l> {
fn eq(&self, rhs: &Schematic<'l>) -> bool {
self.width == rhs.width
&& self.height == rhs.height
&& self.blocks == rhs.blocks
&& self.tags == rhs.tags
}
}
impl<'l> Schematic<'l> {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
match Self::try_new(width, height) {
Ok(s) => s,
Err(NewError::Width(w)) => panic!("invalid schematic width ({w})"),
Err(NewError::Height(h)) => panic!("invalid schematic height ({h})"),
}
}
pub fn try_new(width: u16, height: u16) -> Result<Self, NewError> {
if width > MAX_DIMENSION {
return Err(NewError::Width(width));
}
if height > MAX_DIMENSION {
return Err(NewError::Height(height));
}
let mut tags = HashMap::<String, String>::new();
tags.insert("name".to_string(), String::new());
tags.insert("description".to_string(), String::new());
tags.insert("labels".to_string(), "[]".to_string());
Ok(Self {
width,
height,
tags,
blocks: Vec::new(),
lookup: Vec::new(),
})
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn get_block_count(&self) -> usize {
self.blocks.len()
}
#[must_use]
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()
}
}
pub fn get(&self, x: u16, y: u16) -> Result<Option<&Placement<'l>>, PosError> {
if x >= self.width || y >= self.height {
return Err(PosError {
x,
y,
w: self.width,
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])),
}
}
pub fn get_mut(&mut self, x: u16, y: u16) -> Result<Option<&mut Placement<'l>>, PosError> {
if x >= self.width || y >= self.height {
return Err(PosError {
x,
y,
w: self.width,
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])),
}
}
fn swap_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;
}
}
pub fn set(
&mut self,
x: u16,
y: u16,
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());
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 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.swap_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 })
}
}
}
}
pub fn take(&mut self, x: u16, y: u16) -> Result<Option<Placement<'l>>, PosError> {
if x >= self.width || y >= self.height {
return Err(PosError {
x,
y,
w: self.width,
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.swap_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);
}
}
}
}
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.as_mut(), horizontally, vertically);
}
}
self.rebuild_lookup();
}
}
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.as_mut(), clockwise);
}
}
self.rebuild_lookup();
}
}
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(ResizeError::Truncated {
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(())
}
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,
}
}
pub fn block_iter<'s>(&'s self) -> Iter<'s, Placement<'l>> {
self.blocks.iter()
}
#[must_use]
pub fn compute_total_cost(&self) -> (ItemStorage, bool) {
let mut cost = ItemStorage::new();
let mut sandbox = false;
for &Placement { block, .. } in &self.blocks {
if let Some(curr) = block.get_build_cost() {
cost.add_all(&curr, u32::MAX);
} else {
sandbox = true;
}
}
(cost, sandbox)
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum NewError {
Width(u16),
Height(u16),
}
impl fmt::Display for NewError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Width(w) => write!(f, "invalid schematic width ({w})"),
Self::Height(h) => write!(f, "invalid schematic height ({h})"),
}
}
}
impl Error for NewError {}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct PosError {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl fmt::Display for PosError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"position {x} / {y} out of bounds {w} / {h}",
x = self.x,
y = self.y,
w = self.w,
h = self.h
)
}
}
impl Error for PosError {}
#[derive(Debug)]
pub enum PlaceError {
Bounds {
x: u16,
y: u16,
sz: u8,
w: u16,
h: u16,
},
Overlap {
x: u16,
y: u16,
},
Deserialize(block::DeserializeError),
}
impl From<block::DeserializeError> for PlaceError {
fn from(value: block::DeserializeError) -> Self {
PlaceError::Deserialize(value)
}
}
impl fmt::Display for PlaceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bounds { x, y, sz, w, h } => write!(
f,
"invalid block placement {x} / {y} (size {sz}) within {w} / {h}"
),
Self::Overlap { x, y } => write!(f, "overlapping an existing block at {x} / {y}"),
Self::Deserialize(..) => f.write_str("block state deserialization failed"),
}
}
}
impl Error for PlaceError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
PlaceError::Deserialize(e) => Some(e),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ResizeError {
TargetWidth(u16),
TargetHeight(u16),
XOffset {
dx: i16,
old_w: u16,
new_w: u16,
},
YOffset {
dy: i16,
old_h: u16,
new_h: u16,
},
Truncated {
right: u16,
top: u16,
left: u16,
bottom: u16,
},
}
impl fmt::Display for ResizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TargetWidth(w) => write!(f, "invalid target width ({w})"),
Self::TargetHeight(w) => write!(f, "invalid target height ({w})"),
Self::XOffset { dx, old_w, new_w } => {
write!(f, "horizontal offset {dx} not in [-{new_w}, {old_w}]")
}
Self::YOffset { dy, old_h, new_h } => {
write!(f, "vertical offset {dy} not in [-{new_h}, {old_h}]")
}
Self::Truncated {
right,
top,
left,
bottom,
} => {
macro_rules! fmt_dir {
($f:ident, $first:ident, $name:expr, $value:expr) => {
if $value != 0 {
if $first {
f.write_str(" (")?;
$first = false;
} else {
f.write_str(", ")?;
}
write!(f, "{}: {}", $name, $value)?;
}
};
}
f.write_str("truncated blocks")?;
let mut first = true;
fmt_dir!(f, first, "right", *right);
fmt_dir!(f, first, "top", *top);
fmt_dir!(f, first, "left", *left);
fmt_dir!(f, first, "bottom", *bottom);
if !first {
f.write_char(')')?;
}
Ok(())
}
}
}
}
impl Error for ResizeError {}
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);
pub struct SchematicSerializer<'l>(pub &'l BlockRegistry<'l>);
impl<'l> Serializer<Schematic<'l>> for SchematicSerializer<'l> {
type ReadError = ReadError;
type WriteError = WriteError;
fn deserialize(&mut self, buff: &mut DataRead<'_>) -> Result<Schematic<'l>, Self::ReadError> {
let hdr = buff.read_u32()?;
if hdr != SCHEMATIC_HEADER {
return Err(ReadError::Header(hdr));
}
let version = buff.read_u8()?;
if version > 1 {
return Err(ReadError::Version(version));
}
let mut dec = Decompress::new(true);
let mut raw = Vec::<u8>::new();
raw.reserve(1024);
loop {
let t_in = dec.total_in();
let t_out = dec.total_out();
let res = dec.decompress_vec(buff.data, &mut raw, FlushDecompress::Finish)?;
if dec.total_in() > t_in {
// we have to advance input every time, decompress_vec only knows the output position
buff.data = &buff.data[(dec.total_in() - t_in) as usize..];
}
match res {
// there's no more input (and the flush mode says so), we need to reserve additional space
Status::Ok | Status::BufError => (),
// input was already at the end, so this is referring to the output
Status::StreamEnd => break,
}
if dec.total_in() == t_in && dec.total_out() == t_out {
// protect against looping forever
return Err(ReadError::DecompressStall);
}
raw.reserve(1024);
}
assert_eq!(dec.total_out() as usize, raw.len());
let mut rbuff = DataRead::new(&raw);
let w = rbuff.read_i16()?;
let h = rbuff.read_i16()?;
if w < 0 || h < 0 || w as u16 > MAX_DIMENSION || h as u16 > MAX_DIMENSION {
return Err(ReadError::Dimensions(w, h));
}
let mut schematic = Schematic::new(w as u16, h as u16);
for _ in 0..rbuff.read_u8()? {
let key = rbuff.read_utf()?;
let value = rbuff.read_utf()?;
schematic.tags.insert(key.to_owned(), value.to_owned());
}
let num_table = rbuff.read_i8()?;
if num_table < 0 {
return Err(ReadError::TableSize(num_table));
}
let mut block_table = Vec::<&'l Block>::new();
block_table.reserve(num_table as usize);
for _ in 0..num_table {
let name = rbuff.read_utf()?;
match self.0.get(name) {
None => return Err(ReadError::NoSuchBlock(name.to_owned())),
Some(b) => block_table.push(b),
}
}
let num_blocks = rbuff.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 = rbuff.read_i8()?;
if idx < 0 || idx as usize >= block_table.len() {
return Err(ReadError::BlockIndex(idx, block_table.len()));
}
let pos = GridPos::from(rbuff.read_u32()?);
let block = block_table[idx as usize];
let config = if version < 1 {
block.data_from_i32(rbuff.read_i32()?, pos)?
} else {
DynSerializer.deserialize(&mut rbuff)?
};
let rot = Rotation::from(rbuff.read_u8()?);
schematic.set(pos.0, pos.1, block, config, rot)?;
}
Ok(schematic)
}
fn serialize(
&mut self,
buff: &mut DataWrite<'_>,
data: &Schematic,
) -> Result<(), Self::WriteError> {
// write the header first just in case
buff.write_u32(SCHEMATIC_HEADER)?;
buff.write_u8(1)?;
let mut rbuff = DataWrite::default();
// don't have to check dimensions because they're already limited to MAX_DIMENSION
rbuff.write_i16(data.width as i16)?;
rbuff.write_i16(data.height as i16)?;
if data.tags.len() > u8::MAX as usize {
return Err(WriteError::TagCount(data.tags.len()));
}
rbuff.write_u8(data.tags.len() as u8)?;
for (k, v) in &data.tags {
rbuff.write_utf(k)?;
rbuff.write_utf(v)?;
}
// use string keys here to avoid issues with different block refs with the same name
let mut block_map = HashMap::<&str, u32>::new();
let mut block_table = Vec::<&str>::new();
for curr in &data.blocks {
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());
}
}
if block_table.len() > i8::MAX as usize {
return Err(WriteError::TableSize(block_table.len()));
}
// else: implies contents are also valid i8 (they're strictly less than the map length)
rbuff.write_i8(block_table.len() as i8)?;
for &name in &block_table {
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_i8(block_map[curr.block.get_name()] as i8)?;
rbuff.write_u32(u32::from(curr.pos))?;
let data = match curr.state {
None => DynData::Empty,
Some(ref s) => curr.block.serialize_state(s.as_ref())?,
};
DynSerializer.serialize(&mut rbuff, &data)?;
rbuff.write_u8(curr.rot.into())?;
num += 1;
}
assert_eq!(num, data.blocks.len());
// compress into the provided buffer
let data::WriteBuff::Vec(raw) = rbuff.data else { unreachable!("write buffer not owned") };
let mut comp = Compress::new(Compression::default(), true);
// compress the immediate buffer into a temp buffer to copy it to buff? no thanks
match buff.data {
data::WriteBuff::Ref {
raw: ref mut dst,
ref mut pos,
} => {
match comp.compress(&raw, &mut dst[*pos..], FlushCompress::Finish)? {
// there's no more input (and the flush mode says so), but we can't resize the output
Status::Ok | Status::BufError => {
return Err(WriteError::CompressEof(
raw.len() - comp.total_in() as usize,
))
}
Status::StreamEnd => (),
}
}
data::WriteBuff::Vec(ref mut dst) => {
let mut input = raw.as_ref();
dst.reserve(1024);
loop {
let t_in = comp.total_in();
let t_out = comp.total_out();
let res = comp.compress_vec(input, dst, FlushCompress::Finish)?;
if comp.total_in() > t_in {
// we have to advance input every time, compress_vec only knows the output position
input = &input[(comp.total_in() - t_in) as usize..];
}
match res {
// there's no more input (and the flush mode says so), we need to reserve additional space
Status::Ok | Status::BufError => (),
// input was already at the end, so this is referring to the output
Status::StreamEnd => break,
}
if comp.total_in() == t_in && comp.total_out() == t_out {
// protect against looping forever
return Err(WriteError::CompressStall);
}
dst.reserve(1024);
}
}
}
assert_eq!(comp.total_in() as usize, raw.len());
Ok(())
}
}
#[derive(Debug)]
pub enum ReadError {
Read(data::ReadError),
Header(u32),
Version(u8),
Decompress(DecompressError),
DecompressStall,
Dimensions(i16, i16),
TableSize(i8),
NoSuchBlock(String),
BlockCount(i32),
BlockIndex(i8, usize),
BlockConfig(block::DataConvertError),
ReadState(dynamic::ReadError),
Placement(PlaceError),
}
impl From<data::ReadError> for ReadError {
fn from(value: data::ReadError) -> Self {
Self::Read(value)
}
}
impl From<DecompressError> for ReadError {
fn from(value: DecompressError) -> Self {
Self::Decompress(value)
}
}
impl From<dynamic::ReadError> for ReadError {
fn from(value: dynamic::ReadError) -> Self {
Self::ReadState(value)
}
}
impl From<block::DataConvertError> for ReadError {
fn from(value: block::DataConvertError) -> Self {
Self::BlockConfig(value)
}
}
impl From<PlaceError> for ReadError {
fn from(value: PlaceError) -> Self {
Self::Placement(value)
}
}
impl fmt::Display for ReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read(..) => f.write_str("failed to read from buffer"),
Self::Header(hdr) => write!(f, "incorrect header ({hdr:08X})"),
Self::Version(ver) => write!(f, "unsupported version ({ver})"),
Self::Decompress(..) => f.write_str("zlib decompression failed"),
Self::DecompressStall => f.write_str("decompressor stalled before completion"),
Self::Dimensions(w, h) => write!(f, "invalid schematic dimensions ({w} * {h})"),
Self::TableSize(cnt) => write!(f, "invalid block table size ({cnt})"),
Self::NoSuchBlock(name) => write!(f, "unknown block {name:?}"),
Self::BlockCount(cnt) => write!(f, "invalid total block count ({cnt})"),
Self::BlockIndex(idx, cnt) => write!(f, "invalid block index ({idx} / {cnt})"),
Self::BlockConfig(..) => f.write_str("block config conversion failed"),
Self::ReadState(..) => f.write_str("failed to read block data"),
Self::Placement(..) => f.write_str("deserialized block could not be placed"),
}
}
}
impl Error for ReadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Read(e) => Some(e),
Self::Decompress(e) => Some(e),
Self::BlockConfig(e) => Some(e),
Self::ReadState(e) => Some(e),
Self::Placement(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug)]
pub enum WriteError {
Write(data::WriteError),
TagCount(usize),
TableSize(usize),
StateSerialize(block::SerializeError),
WriteState(dynamic::WriteError),
Compress(CompressError),
CompressEof(usize),
CompressStall,
}
impl From<data::WriteError> for WriteError {
fn from(value: data::WriteError) -> Self {
Self::Write(value)
}
}
impl From<block::SerializeError> for WriteError {
fn from(value: block::SerializeError) -> Self {
Self::StateSerialize(value)
}
}
impl From<CompressError> for WriteError {
fn from(value: CompressError) -> Self {
Self::Compress(value)
}
}
impl From<dynamic::WriteError> for WriteError {
fn from(value: dynamic::WriteError) -> Self {
Self::WriteState(value)
}
}
impl fmt::Display for WriteError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Write(..) => f.write_str("failed to write data to buffer"),
Self::TagCount(len) => write!(f, "tag list too long ({len})"),
Self::TableSize(len) => write!(f, "block table too long ({len})"),
Self::StateSerialize(e) => e.fmt(f),
Self::WriteState(..) => f.write_str("failed to write block data"),
Self::Compress(..) => f.write_str("zlib compression failed"),
Self::CompressEof(remain) => write!(
f,
"compression overflow with {remain} bytes of input remaining"
),
Self::CompressStall => f.write_str("compressor stalled before completion"),
}
}
}
impl Error for WriteError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Write(e) => Some(e),
Self::StateSerialize(e) => e.source(),
Self::Compress(e) => Some(e),
_ => None,
}
}
}
impl<'l> SchematicSerializer<'l> {
pub fn deserialize_base64(&mut self, data: &str) -> Result<Schematic<'l>, R64Error> {
let mut buff = Vec::<u8>::new();
buff.resize(data.len() / 4 * 3 + 1, 0);
let n_out = base64::decode(data.as_bytes(), buff.as_mut())?;
Ok(self.deserialize(&mut DataRead::new(&buff[..n_out]))?)
}
pub fn serialize_base64(&mut self, data: &Schematic<'l>) -> Result<String, W64Error> {
let mut buff = DataWrite::default();
self.serialize(&mut buff, data)?;
let buff = buff.get_written();
// round up because of padding
let required = 4 * (buff.len() / 3 + usize::from(buff.len() % 3 != 0));
let mut text = Vec::<u8>::new();
text.resize(required, 0);
let n_out = base64::encode(buff, text.as_mut())?;
// trailing zeros are valid UTF8, but not valid base64
assert_eq!(n_out, text.len());
// SAFETY: base64 encoding outputs pure ASCII (see base64::CHARS)
Ok(unsafe { String::from_utf8_unchecked(text) })
}
}
#[derive(Debug)]
pub enum R64Error {
Base64(base64::DecodeError),
Content(ReadError),
}
impl From<base64::DecodeError> for R64Error {
fn from(value: base64::DecodeError) -> Self {
Self::Base64(value)
}
}
impl From<ReadError> for R64Error {
fn from(value: ReadError) -> Self {
Self::Content(value)
}
}
impl fmt::Display for R64Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Base64(..) => f.write_str("base-64 decoding failed"),
Self::Content(e) => e.fmt(f),
}
}
}
impl Error for R64Error {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Base64(e) => Some(e),
Self::Content(e) => e.source(),
}
}
}
#[derive(Debug)]
pub enum W64Error {
Base64(base64::EncodeError),
Content(WriteError),
}
impl From<base64::EncodeError> for W64Error {
fn from(value: base64::EncodeError) -> Self {
Self::Base64(value)
}
}
impl From<WriteError> for W64Error {
fn from(value: WriteError) -> Self {
Self::Content(value)
}
}
impl fmt::Display for W64Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Base64(..) => f.write_str("base-64 encoding failed"),
Self::Content(e) => e.fmt(f),
}
}
}
impl Error for W64Error {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Base64(e) => Some(e),
Self::Content(e) => e.source(),
}
}
}
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);
};
}
macro_rules! test_schem {
($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);
)*
}
};
}
test_schem! {
ser_de,
"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"
}
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
);
}