use {
core::{
convert::{
TryInto,
},
},
std::{
io::{
self,
Read,
BufReader,
},
string,
},
thiserror::{
Error,
},
regenboog::{
RgbaU8,
},
};
use crate::{
MAGIC_NUMBER,
VERSION,
};
#[derive(Debug, Error)]
pub enum Error {
#[error("IO error: {0:}")]
IoError(#[from] io::Error),
#[error("UTF8 decode error: {0:}")]
FromUtf8Error(#[from] string::FromUtf8Error),
#[error("Invalid color id: {0:}")]
InvalidColorId(u8),
#[error("Invalid orientation: {0:}")]
InvalidOrientation(u8),
#[error("Invalid magic number at the start of the file: {0:?}")]
InvalidMagicNumber([u8; 3]),
#[error("Unsupported version: {0:}")]
UnsupportedVersion(u8),
#[error("Invalid block type: {0:}")]
InvalidBlockType(u8),
#[error("Invalid brick type: {0:}")]
InvalidBrickTypeId(u16),
}
enum State {
Header,
Palette {
palette_current: u8,
},
BlockHeader,
BrickTypeBlock {
block_entries: u8,
block_current: u8,
},
BrickBlock {
block_entries: u8,
block_current: u8,
},
End,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event<'a> {
Meta {
build_name: &'a str,
build_description: &'a str,
},
Color {
id: u8,
color: RgbaU8,
},
Brick {
brick_type: &'a str,
x: i32,
y: i32,
z: i32,
orientation: u8,
color_id: u8,
},
}
pub struct Reader<R: Read> {
state: State,
build_name: String,
build_description: String,
brick_types: Vec<String>,
palette_size: u8,
buf: Box<[u8; 65536]>,
inner: BufReader<R>,
}
impl<R: Read> Reader<R> {
pub fn new(reader: R) -> Self {
Self {
state: State::Header,
build_name: String::new(),
build_description: String::new(),
brick_types: Vec::new(),
inner: BufReader::new(reader),
palette_size: 0,
buf: Box::new([0; 65536]),
}
}
pub fn next_event<'a>(&'a mut self) -> Result<Option<Event<'a>>, Error> {
loop {
match self.state {
State::Header => {
self.inner.read_exact(&mut self.buf[..4])?;
if &self.buf[..3] != MAGIC_NUMBER {
break Err(Error::InvalidMagicNumber(self.buf[..3].try_into().unwrap()));
}
if self.buf[3] != VERSION {
break Err(Error::UnsupportedVersion(self.buf[3]));
}
self.inner.read_exact(&mut self.buf[..1])?;
let build_name_len = self.buf[0] as usize;
self.inner.read_exact(&mut self.buf[..build_name_len])?;
self.build_name = String::from_utf8(self.buf[..build_name_len].to_vec())?;
self.inner.read_exact(&mut self.buf[..2])?;
let build_description_len = u16::from_be_bytes(self.buf[..2].try_into().unwrap()) as usize;
self.inner.read_exact(&mut self.buf[..build_description_len])?;
self.build_description = String::from_utf8(self.buf[..build_description_len].to_vec())?;
self.inner.read_exact(&mut self.buf[..1])?;
self.palette_size = self.buf[0];
let palette_current = 0;
self.state = State::Palette { palette_current };
break Ok(Some(Event::Meta {
build_name: &self.build_name,
build_description: &self.build_description,
}));
},
State::Palette { palette_current } => if palette_current < self.palette_size {
let id = palette_current;
self.state = State::Palette {
palette_current: palette_current + 1,
};
self.inner.read_exact(&mut self.buf[..4])?;
break Ok(Some(Event::Color {
id,
color: RgbaU8::rgba(self.buf[0], self.buf[1], self.buf[2], self.buf[3]),
}));
} else {
self.state = State::BlockHeader;
},
State::BlockHeader => match self.inner.read_exact(&mut self.buf[..1]) {
Ok(_) => {
let block_type = self.buf[0];
self.inner.read_exact(&mut self.buf[..1])?;
let block_entries = self.buf[0];
match block_type {
1 => {
self.state = State::BrickTypeBlock {
block_entries,
block_current: 0,
};
},
2 => {
self.state = State::BrickBlock {
block_entries,
block_current: 0,
};
},
ty => break Err(Error::InvalidBlockType(ty)),
}
},
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
self.state = State::End;
},
Err(err) => {
break Err(err.into());
},
},
State::BrickTypeBlock { block_entries, block_current } => if block_current < block_entries {
self.inner.read_exact(&mut self.buf[..1])?;
let str_len = self.buf[0] as usize;
self.inner.read_exact(&mut self.buf[..str_len])?;
self.brick_types.push(String::from_utf8(self.buf[..str_len].to_vec())?);
self.state = State::BrickTypeBlock {
block_entries,
block_current: block_current + 1,
};
} else {
self.state = State::BlockHeader;
},
State::BrickBlock { block_entries, block_current } => if block_current < block_entries {
self.inner.read_exact(&mut self.buf[..16])?;
let brick_type_id = u16::from_be_bytes(self.buf[..2].try_into().unwrap());
let brick_position_x = i32::from_be_bytes(self.buf[2..6].try_into().unwrap());
let brick_position_y = i32::from_be_bytes(self.buf[6..10].try_into().unwrap());
let brick_position_z = i32::from_be_bytes(self.buf[10..14].try_into().unwrap());
let brick_orientation = self.buf[14];
let brick_color_id = self.buf[15];
if brick_type_id as usize >= self.brick_types.len() {
break Err(Error::InvalidBrickTypeId(brick_type_id));
}
self.state = State::BrickBlock {
block_entries,
block_current: block_current + 1,
};
break Ok(Some(Event::Brick {
brick_type: &self.brick_types[brick_type_id as usize],
x: brick_position_x,
y: brick_position_y,
z: brick_position_z,
orientation: brick_orientation,
color_id: brick_color_id,
}));
} else {
self.state = State::BlockHeader;
},
State::End => {
break Ok(None);
},
}
}
}
}
#[cfg(test)]
mod tests {
use {
std::{
io::{
Cursor,
},
},
};
use super::*;
#[test]
fn ttb_reader_works() {
let buf = [
b't', b't', b'b',
1,
4,
b't', b'e', b's', b't',
0, 14,
b't', b'h', b'i', b's', b' ', b'i', b's', b' ', b'a', b' ', b't', b'e', b's', b't',
3,
255, 0, 0, 255,
0, 255, 0, 255,
0, 0, 255, 255,
1, 2,
9,
b'1', b'x', b'1', b' ', b'P', b'l', b'a', b't', b'e',
15,
b'3', b'2', b'x', b'3', b'2', b' ', b'B', b'a', b's', b'e', b'p', b'l', b'a', b't', b'e',
2, 3,
0, 1,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0,
0,
0, 0,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 3,
0,
1,
0, 0,
0, 0, 0, 0,
0, 0, 0, 1,
0, 0, 0, 0,
1,
1,
];
let cursor = Cursor::new(&buf[..]);
let mut reader = Reader::new(cursor);
assert_eq!(reader.next_event().unwrap(), Some(Event::Meta {
build_name: "test",
build_description: "this is a test",
}));
assert_eq!(reader.next_event().unwrap(), Some(Event::Color { id: 0, color: RgbaU8::RED }));
assert_eq!(reader.next_event().unwrap(), Some(Event::Color { id: 1, color: RgbaU8::GREEN }));
assert_eq!(reader.next_event().unwrap(), Some(Event::Color { id: 2, color: RgbaU8::BLUE }));
assert_eq!(reader.next_event().unwrap(), Some(Event::Brick {
brick_type: "32x32 Baseplate",
x: 0,
y: 0,
z: 0,
orientation: 0,
color_id: 0,
}));
assert_eq!(reader.next_event().unwrap(), Some(Event::Brick {
brick_type: "1x1 Plate",
x: 1,
y: 1,
z: 3,
orientation: 0,
color_id: 1,
}));
assert_eq!(reader.next_event().unwrap(), Some(Event::Brick {
brick_type: "1x1 Plate",
x: 0,
y: 1,
z: 0,
orientation: 1,
color_id: 1,
}));
assert_eq!(reader.next_event().unwrap(), None);
}
}