use {
std::{
io::{
self,
Write,
},
},
regenboog::{
RgbaU8,
},
};
use crate::{
MAGIC_NUMBER,
VERSION,
};
#[derive(Debug, PartialEq)]
enum State {
Header,
Blocks,
}
#[must_use = "the `finish` method must be called to correctly end the stream and write all data"]
pub struct Writer<W: Write> {
state: State,
palette_size: u8,
brick_types: Vec<String>,
pending_brick_types: Vec<u16>,
pending_bricks: Vec<(u16, i32, i32, i32, u8, u8)>,
inner: W,
}
impl<W: Write> Writer<W> {
pub fn new(writer: W) -> Self {
Self {
state: State::Header,
palette_size: 0,
brick_types: Vec::new(),
pending_brick_types: Vec::new(),
pending_bricks: Vec::new(),
inner: writer,
}
}
pub fn into_inner(self) -> W {
self.inner
}
pub fn write_header(&mut self, build_name: &str, build_description: &str, palette: &[RgbaU8]) -> io::Result<()> {
if self.state != State::Header {
panic!("invalid state: expecting state Header but am in state {:?}", self.state);
}
assert!(build_name.len() < 256);
assert!(build_description.len() < 65536);
self.inner.write(MAGIC_NUMBER)?;
self.inner.write(&[VERSION])?;
let build_name_len = build_name.len() as u8;
let build_description_len = build_description.len() as u16;
self.inner.write(&[build_name_len])?;
self.inner.write(build_name.as_bytes())?;
self.inner.write(&build_description_len.to_be_bytes())?;
self.inner.write(build_description.as_bytes())?;
let palette_size = palette.len() as u8;
self.inner.write(&[palette_size])?;
for &col in palette {
let col_arr: [u8; 4] = col.into();
self.inner.write(&col_arr)?;
}
self.palette_size = palette_size;
self.state = State::Blocks;
Ok(())
}
pub fn write_brick(&mut self, brick_type_name: &str, x: i32, y: i32, z: i32, orientation: u8, color: u8) -> io::Result<()> {
if self.state != State::Blocks {
panic!("invalid state: expecting state Blocks but am in state {:?}", self.state);
}
if color >= self.palette_size {
panic!("color overflows palette: {}", color);
}
if orientation >= 4 {
panic!("invalid brick orientation: {}", orientation);
}
assert!(brick_type_name.len() < 256);
let tyid = if let Some(tyid) = self.brick_types.iter().position(|x| x == brick_type_name) {
tyid as u16
} else {
let tyid = self.brick_types.len() as u16;
self.brick_types.push(brick_type_name.to_owned());
self.add_brick_type(tyid)?;
tyid
};
self.add_brick(tyid, x, y, z, orientation, color)?;
Ok(())
}
pub fn finish(mut self) -> io::Result<()> {
self.flush_brick_types()?;
self.flush_bricks()?;
Ok(())
}
fn add_brick_type(&mut self, tyid: u16) -> io::Result<()> {
if self.pending_brick_types.len() >= 255 {
self.flush_brick_types()?;
}
self.pending_brick_types.push(tyid);
Ok(())
}
fn add_brick(&mut self, tyid: u16, x: i32, y: i32, z: i32, orientation: u8, color: u8) -> io::Result<()> {
if self.pending_bricks.len() >= 255 {
self.flush_brick_types()?;
self.flush_bricks()?;
}
self.pending_bricks.push((tyid, x, y, z, orientation, color));
Ok(())
}
fn flush_brick_types(&mut self) -> io::Result<()> {
if !self.pending_brick_types.is_empty() {
let len = self.pending_brick_types.len();
self.inner.write_all(&[1, len as u8])?;
for idx in self.pending_brick_types.drain(..) {
let brick_type_name = &self.brick_types[idx as usize];
self.inner.write_all(&[brick_type_name.len() as u8])?;
self.inner.write_all(brick_type_name.as_bytes())?;
}
}
Ok(())
}
fn flush_bricks(&mut self) -> io::Result<()> {
if !self.pending_bricks.is_empty() {
let len = self.pending_bricks.len();
self.inner.write_all(&[2, len as u8])?;
for (tyid, x, y, z, orientation, color) in self.pending_bricks.drain(..) {
self.inner.write_all(&tyid.to_be_bytes())?;
self.inner.write_all(&x.to_be_bytes())?;
self.inner.write_all(&y.to_be_bytes())?;
self.inner.write_all(&z.to_be_bytes())?;
self.inner.write_all(&[orientation, color])?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use {
std::{
io::{
Cursor,
},
},
regenboog::{
RgbaU8,
},
};
use super::*;
#[test]
#[should_panic = "invalid state: expecting state Header but am in state Blocks"]
fn ttb_writer_header_doesnt_write_twice() {
let mut buf = [0; 2048];
let cursor = Cursor::new(&mut buf[..]);
let mut writer = Writer::new(cursor);
writer.write_header("test", "test", &[
RgbaU8::RED,
RgbaU8::GREEN,
RgbaU8::BLUE,
]).unwrap();
writer.write_header("test", "test", &[
RgbaU8::RED,
RgbaU8::GREEN,
RgbaU8::BLUE,
]).unwrap();
}
#[test]
#[should_panic = "invalid state: expecting state Blocks but am in state Header"]
fn ttb_writer_header_must_be_written() {
let mut buf = [0; 2048];
let cursor = Cursor::new(&mut buf[..]);
let mut writer = Writer::new(cursor);
writer.write_brick("1x1 Plate", 0, 0, 0, 0, 0).unwrap();
}
#[test]
#[should_panic = "color overflows palette: 10"]
fn ttb_writer_color_overflows_palette() {
let mut buf = [0; 2048];
let cursor = Cursor::new(&mut buf[..]);
let mut writer = Writer::new(cursor);
writer.write_header("test", "test", &[
RgbaU8::RED,
RgbaU8::GREEN,
RgbaU8::BLUE,
]).unwrap();
writer.write_brick("1x1 Plate", 0, 0, 0, 0, 10).unwrap();
}
#[test]
#[should_panic = "invalid brick orientation: 5"]
fn ttb_writer_invalid_orienbtation() {
let mut buf = [0; 2048];
let cursor = Cursor::new(&mut buf[..]);
let mut writer = Writer::new(cursor);
writer.write_header("test", "test", &[
RgbaU8::RED,
RgbaU8::GREEN,
RgbaU8::BLUE,
]).unwrap();
writer.write_brick("1x1 Plate", 0, 0, 0, 5, 2).unwrap();
}
}