ttb 0.1.0

A crate for saving and loading of tinytown build files
Documentation
use {
    core::{
        convert::{
            TryInto,
        },
    },
    std::{
        io::{
            self,
            Read,
            BufReader,
        },
        string,
    },
    thiserror::{
        Error,
    },
    regenboog::{
        RgbaU8,
    },
};

use crate::{
    MAGIC_NUMBER,
    VERSION,
};

/// An error which may be returned when reading from a `Reader`.
#[derive(Debug, Error)]
pub enum Error {
    /// An IO error has occured in the underlying `Read`.
    #[error("IO error: {0:}")]
    IoError(#[from] io::Error),
    /// A string was encountered that isn't valid utf-8.
    #[error("UTF8 decode error: {0:}")]
    FromUtf8Error(#[from] string::FromUtf8Error),
    /// A color id has been encountered that isn't in the palette.
    #[error("Invalid color id: {0:}")]
    InvalidColorId(u8),
    /// An orientation that is outside of the 0..=3 range has been encountered.
    #[error("Invalid orientation: {0:}")]
    InvalidOrientation(u8),
    /// This `Read` doesn't start with `b"ttb"`.
    #[error("Invalid magic number at the start of the file: {0:?}")]
    InvalidMagicNumber([u8; 3]),
    /// This version is not supported by this crate.
    #[error("Unsupported version: {0:}")]
    UnsupportedVersion(u8),
    /// An invalid block type has been encountered.
    #[error("Invalid block type: {0:}")]
    InvalidBlockType(u8),
    /// A brick has been encountered that has a brick type that isn't in the brick type table.
    #[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,
}

/// An event returned by `Reader::next_event`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event<'a> {
    /// The metadata of a build has been encountered.
    Meta {
        /// The name of the build
        build_name: &'a str,
        /// The description of the build
        build_description: &'a str,
    },
    /// A color in the palette has been encountered.
    Color {
        /// The id of the color
        id: u8,
        /// The color
        color: RgbaU8,
    },
    /// A brick has been encountered.
    Brick {
        /// The name of the type of brick
        brick_type: &'a str,
        /// The x coordinate of the minimum point of this brick
        x: i32,
        /// The y coordinate of the minimum point of this brick
        y: i32,
        /// The z coordinate of the minimum point of this brick
        z: i32,
        /// The orientation of this brick, this is guaranteed to be in the 0..=3 range.
        orientation: u8,
        /// The color id of a brick, this is guaranteed to be within the palette.
        color_id: u8,
    },
}

/// A wrapper for a type implementing `Read`, for reading TTB data
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> {
    /// Create a new `Reader` by wrapping a type implementing `Read`.
    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]),
        }
    }

    /// Try to fetch the next event from the reader.
    ///
    /// # Returns
    ///
    ///  - `Err(_)` on error
    ///  - `Ok(None)` on stream end
    ///  - `Ok(Some(_))` on new event
    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', // magic number
            1, // version
            4, // build name length
            b't', b'e', b's', b't', // build name: "test"
            0, 14, // build description length
            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', // build description: "this is a test"
            3, // palette size
            255, 0, 0, 255, // color 1: red
            0, 255, 0, 255, // color 2: green
            0, 0, 255, 255, // color 3: blue
            1, 2, // block header: type=brick_type entries=2
            9, // brick type 0: name size: 9 bytes
            b'1', b'x', b'1', b' ', b'P', b'l', b'a', b't', b'e', // brick type 0: name: "1x1 Plate"
            15, // brick type 1: name size: 15 bytes
            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', // brick type 1: name: "32x32 Baseplate"
            2, 3, // block header: type=brick entries=3
            0, 1, // brick 0: brick type: 1
            0, 0, 0, 0, //brick 0: brick x position: 0
            0, 0, 0, 0, //brick 0: brick y position: 0
            0, 0, 0, 0, //brick 0: brick z position: 0
            0, // brick 0: brick orientation: 0
            0, // brick 0: color id: 0
            0, 0, // brick 1: brick type: 0
            0, 0, 0, 1, //brick 1: brick x position: 1
            0, 0, 0, 1, //brick 1: brick y position: 1
            0, 0, 0, 3, //brick 1: brick z position: 3
            0, // brick 1: brick orientation: 0
            1, // brick 1: color id: 1
            0, 0, // brick 2: brick type: 0
            0, 0, 0, 0, //brick 2: brick x position: 0
            0, 0, 0, 1, //brick 2: brick y position: 1
            0, 0, 0, 0, //brick 2: brick z position: 0
            1, // brick 2: brick orientation: 1
            1, // brick 2: color id: 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);
    }
}