Adapt ZZT world/board parsing from Marzipan
This commit is contained in:
@@ -0,0 +1,110 @@
|
|||||||
|
use codepage_437::CP437_WINGDINGS;
|
||||||
|
|
||||||
|
use super::error::ParseError;
|
||||||
|
|
||||||
|
/// Serialize a multi-line code string for an object or a scroll.
|
||||||
|
/// Uses ZZT's convention of CR-terminated lines.
|
||||||
|
pub fn encode_multiline(input: &str) -> Result<Vec<u8>, ParseError> {
|
||||||
|
input
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
if c == '\n' {
|
||||||
|
Ok(b'\r')
|
||||||
|
} else {
|
||||||
|
CP437_WINGDINGS
|
||||||
|
.encode(c)
|
||||||
|
.ok_or_else(|| ParseError::EncodingError(c))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a multi-line code string.
|
||||||
|
pub fn decode_multiline(input: &[u8]) -> String {
|
||||||
|
input
|
||||||
|
.iter()
|
||||||
|
.map(|&x| {
|
||||||
|
if x == b'\r' {
|
||||||
|
'\n'
|
||||||
|
} else {
|
||||||
|
CP437_WINGDINGS.decode(x)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a single-line string for a board title.
|
||||||
|
/// Newlines cannot be encoded because ZZT interprets them as dingbats (♪).
|
||||||
|
pub fn encode_oneline(input: &str) -> Result<Vec<u8>, ParseError> {
|
||||||
|
input
|
||||||
|
.chars()
|
||||||
|
.map(|c| {
|
||||||
|
CP437_WINGDINGS
|
||||||
|
.encode(c)
|
||||||
|
.ok_or_else(|| ParseError::EncodingError(c))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a board title.
|
||||||
|
pub fn decode_oneline(input: &[u8]) -> String {
|
||||||
|
input.iter().map(|&x| CP437_WINGDINGS.decode(x)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use insta::assert_debug_snapshot;
|
||||||
|
|
||||||
|
fn serialize_code(input: &str) -> Vec<u8> {
|
||||||
|
encode_multiline(input).expect("Error in test")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_title(input: &str) -> Vec<u8> {
|
||||||
|
encode_oneline(input).expect("Error in test")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_multiline() {
|
||||||
|
let bytes: Vec<u8> = (0..=255).collect();
|
||||||
|
assert_eq!(bytes, serialize_code(&decode_multiline(&bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_oneline() {
|
||||||
|
let bytes: Vec<u8> = (0..=255).collect();
|
||||||
|
assert_eq!(bytes, serialize_title(&decode_oneline(&bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn newlines_to_cr() {
|
||||||
|
assert_debug_snapshot!(serialize_code("ABC\nDEF"), @r###"
|
||||||
|
[
|
||||||
|
65,
|
||||||
|
66,
|
||||||
|
67,
|
||||||
|
13,
|
||||||
|
68,
|
||||||
|
69,
|
||||||
|
70,
|
||||||
|
]
|
||||||
|
"###)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn byte_13_to_dingbat() {
|
||||||
|
// Code is allowed to have newlines
|
||||||
|
let bytes = serialize_code("ABC\nDEF");
|
||||||
|
assert!(bytes.contains(&13));
|
||||||
|
|
||||||
|
// But in a board title, that same byte is a dingbat
|
||||||
|
assert_debug_snapshot!(decode_oneline(&bytes), @r#""ABC♪DEF""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wingdings() {
|
||||||
|
assert_debug_snapshot!(decode_multiline(&(0..32).collect::<Vec<_>>()), @r###""\0☺☻♥♦♣♠•◘○◙♂♀\n♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼""###);
|
||||||
|
assert_debug_snapshot!(decode_multiline(&(112..144).collect::<Vec<_>>()), @r###""pqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅ""###);
|
||||||
|
assert_debug_snapshot!(decode_multiline(&(224..=255).collect::<Vec<_>>()), @r###""αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}""###);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use nom::error::{ErrorKind, ParseError as NomParseError};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("invalid ZZT file magic")]
|
||||||
|
InvalidMagic,
|
||||||
|
|
||||||
|
#[error("invalid tile count: {0}")]
|
||||||
|
InvalidTileCount(usize),
|
||||||
|
|
||||||
|
#[error("negative stat count")]
|
||||||
|
NegativeStatCount,
|
||||||
|
|
||||||
|
#[error("string too long")]
|
||||||
|
StringTooLong { max: u8 },
|
||||||
|
|
||||||
|
#[error("cannot encode character: {0}")]
|
||||||
|
EncodingError(char),
|
||||||
|
|
||||||
|
#[error("board data too large")]
|
||||||
|
BoardTooLarge,
|
||||||
|
|
||||||
|
#[error("parse error: {0}")]
|
||||||
|
ParseError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> NomParseError<I> for ParseError {
|
||||||
|
fn from_error_kind(_input: I, kind: ErrorKind) -> Self {
|
||||||
|
Self::ParseError(kind.description().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append(_input: I, kind: ErrorKind, other: Self) -> Self {
|
||||||
|
Self::ParseError(format!("{}: {:?}", other, kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<nom::Err<ParseError>> for ParseError {
|
||||||
|
fn from(value: nom::Err<ParseError>) -> Self {
|
||||||
|
match value {
|
||||||
|
nom::Err::Error(e) | nom::Err::Failure(e) => e,
|
||||||
|
nom::Err::Incomplete(needed) => Self::ParseError(format!("incomplete: {:?}", needed)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
mod encoding;
|
||||||
|
mod error;
|
||||||
|
mod parse;
|
||||||
|
|
||||||
|
pub use encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||||
|
pub use error::ParseError;
|
||||||
|
pub use parse::{Board, Program, Stat, Tile, World};
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
use nom::{
|
||||||
|
IResult, Parser,
|
||||||
|
bytes::complete::{tag, take},
|
||||||
|
multi::count,
|
||||||
|
number::complete::{le_i16, le_u8, le_u16},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||||
|
use super::error::ParseError;
|
||||||
|
|
||||||
|
/// A single tile on a ZZT board.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Tile {
|
||||||
|
pub element: u8,
|
||||||
|
pub color: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A stat's ZZT-OOP program.
|
||||||
|
///
|
||||||
|
/// In ZZT, stats can either have their own code or bind to another stat's code.
|
||||||
|
/// This enum prevents invalid states where both are set.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Program {
|
||||||
|
/// The stat owns its own code.
|
||||||
|
Own(String),
|
||||||
|
/// The stat is bound to another stat (index into the stats list).
|
||||||
|
Bound(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Program {
|
||||||
|
fn default() -> Self {
|
||||||
|
Program::Own(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A status element on a ZZT board.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Stat {
|
||||||
|
pub x: u8,
|
||||||
|
pub y: u8,
|
||||||
|
pub x_step: i16,
|
||||||
|
pub y_step: i16,
|
||||||
|
pub cycle: i16,
|
||||||
|
pub p1: u8,
|
||||||
|
pub p2: u8,
|
||||||
|
pub p3: u8,
|
||||||
|
pub follower: i16,
|
||||||
|
pub leader: i16,
|
||||||
|
pub under: Tile,
|
||||||
|
pub instruction_pointer: i16,
|
||||||
|
pub program: Program,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A ZZT board.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Board {
|
||||||
|
pub name: String,
|
||||||
|
pub tiles: Vec<Tile>,
|
||||||
|
pub max_shots: u8,
|
||||||
|
pub is_dark: bool,
|
||||||
|
pub exit_north: u8,
|
||||||
|
pub exit_south: u8,
|
||||||
|
pub exit_west: u8,
|
||||||
|
pub exit_east: u8,
|
||||||
|
pub restart_on_zap: bool,
|
||||||
|
pub message: String,
|
||||||
|
pub enter_x: u8,
|
||||||
|
pub enter_y: u8,
|
||||||
|
pub time_limit: i16,
|
||||||
|
pub stats: Vec<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A ZZT world file.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct World {
|
||||||
|
pub ammo: i16,
|
||||||
|
pub gems: i16,
|
||||||
|
pub keys: [bool; 7],
|
||||||
|
pub health: i16,
|
||||||
|
pub starting_board: i16,
|
||||||
|
pub torches: i16,
|
||||||
|
pub torch_cycles: i16,
|
||||||
|
pub energizer_cycles: i16,
|
||||||
|
pub score: i16,
|
||||||
|
pub name: String,
|
||||||
|
pub flags: [String; 10],
|
||||||
|
pub time: i16,
|
||||||
|
pub time_ticks: i16,
|
||||||
|
pub locked: bool,
|
||||||
|
pub boards: Vec<Board>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsing helpers
|
||||||
|
|
||||||
|
fn bool_u8(input: &[u8]) -> IResult<&[u8], bool, ParseError> {
|
||||||
|
let (input, byte) = le_u8(input)?;
|
||||||
|
Ok((input, byte != 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], String, ParseError> {
|
||||||
|
move |input: &[u8]| -> IResult<&[u8], String, ParseError> {
|
||||||
|
let (input, len) = le_u8(input)?;
|
||||||
|
let actual_len = len.min(cap);
|
||||||
|
let (input, data) = take(actual_len)(input)?;
|
||||||
|
let (input, _) = take(cap - actual_len)(input)?;
|
||||||
|
Ok((input, decode_oneline(data)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], ParseError> {
|
||||||
|
let (_, size) = le_u16.parse(bytes)?;
|
||||||
|
take(size as usize + 2).parse(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialization helpers
|
||||||
|
|
||||||
|
trait SerializationHelpers {
|
||||||
|
fn push_bool(&mut self, value: bool);
|
||||||
|
fn push_i16(&mut self, value: i16);
|
||||||
|
fn push_string(&mut self, cap: u8, value: &str) -> Result<(), ParseError>;
|
||||||
|
fn push_padding(&mut self, size: usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializationHelpers for Vec<u8> {
|
||||||
|
fn push_bool(&mut self, value: bool) {
|
||||||
|
self.push(if value { 1 } else { 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_i16(&mut self, value: i16) {
|
||||||
|
self.extend(value.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_string(&mut self, cap: u8, value: &str) -> Result<(), ParseError> {
|
||||||
|
let bytes = encode_oneline(value)?;
|
||||||
|
if bytes.len() > cap as usize {
|
||||||
|
return Err(ParseError::StringTooLong { max: cap });
|
||||||
|
}
|
||||||
|
self.push(bytes.len() as u8);
|
||||||
|
self.extend_from_slice(&bytes);
|
||||||
|
self.push_padding(cap as usize - bytes.len());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_padding(&mut self, size: usize) {
|
||||||
|
self.resize(self.len() + size, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
/// Parse a ZZT world from bytes.
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<World, ParseError> {
|
||||||
|
let (input, _) = tag(&[0xff, 0xff][..])
|
||||||
|
.parse(bytes)
|
||||||
|
.map_err(|_: nom::Err<ParseError>| ParseError::InvalidMagic)?;
|
||||||
|
let (input, num_boards) = le_i16.parse(input)?;
|
||||||
|
let (input, (ammo, gems, keys)) = (le_i16, le_i16, count(bool_u8, 7)).parse(input)?;
|
||||||
|
let (input, (health, starting_board, torches, torch_cycles, energizer_cycles)) =
|
||||||
|
(le_i16, le_i16, le_i16, le_i16, le_i16).parse(input)?;
|
||||||
|
let (input, (_, score, name)) = (take(2usize), le_i16, pstring(20)).parse(input)?;
|
||||||
|
let (input, flags) = count(pstring(20), 10).parse(input)?;
|
||||||
|
let (_input, (time, time_ticks, locked)) = (le_i16, le_i16, bool_u8).parse(input)?;
|
||||||
|
|
||||||
|
// Rest of header is padding; fast-forward starting from original input
|
||||||
|
let (input, _) = take(512usize).parse(bytes)?;
|
||||||
|
|
||||||
|
// Load boards
|
||||||
|
let num_boards = num_boards as usize + 1;
|
||||||
|
let (_input, chunks) = count(board_slice, num_boards).parse(input)?;
|
||||||
|
let boards: Result<Vec<Board>, ParseError> = chunks
|
||||||
|
.iter()
|
||||||
|
.map(|bytes: &&[u8]| Board::from_bytes(bytes))
|
||||||
|
.collect();
|
||||||
|
let boards = boards?;
|
||||||
|
|
||||||
|
Ok(World {
|
||||||
|
ammo,
|
||||||
|
gems,
|
||||||
|
keys: keys.try_into().unwrap(),
|
||||||
|
health,
|
||||||
|
starting_board,
|
||||||
|
torches,
|
||||||
|
torch_cycles,
|
||||||
|
energizer_cycles,
|
||||||
|
score,
|
||||||
|
name,
|
||||||
|
flags: flags.try_into().unwrap(),
|
||||||
|
time,
|
||||||
|
time_ticks,
|
||||||
|
locked,
|
||||||
|
boards,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize this world to bytes.
|
||||||
|
pub fn to_bytes(&self) -> Result<Vec<u8>, ParseError> {
|
||||||
|
let mut result = Vec::with_capacity(512);
|
||||||
|
result.push_i16(-1); // file magic: ZZT world
|
||||||
|
result.push_i16(self.boards.len() as i16 - 1);
|
||||||
|
result.push_i16(self.ammo);
|
||||||
|
result.push_i16(self.gems);
|
||||||
|
for key in self.keys {
|
||||||
|
result.push_bool(key);
|
||||||
|
}
|
||||||
|
result.push_i16(self.health);
|
||||||
|
result.push_i16(self.starting_board);
|
||||||
|
result.push_i16(self.torches);
|
||||||
|
result.push_i16(self.torch_cycles);
|
||||||
|
result.push_i16(self.energizer_cycles);
|
||||||
|
result.push_padding(2);
|
||||||
|
result.push_i16(self.score);
|
||||||
|
result.push_string(20, &self.name)?;
|
||||||
|
for flag in &self.flags {
|
||||||
|
result.push_string(20, flag)?;
|
||||||
|
}
|
||||||
|
result.push_i16(self.time);
|
||||||
|
result.push_i16(self.time_ticks);
|
||||||
|
result.push_bool(self.locked);
|
||||||
|
result.push_padding(512 - result.len());
|
||||||
|
|
||||||
|
for board in &self.boards {
|
||||||
|
result.extend_from_slice(&board.to_bytes()?);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
/// Parse a board from bytes (including the 2-byte size header).
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<Board, ParseError> {
|
||||||
|
// Ignore length bytes
|
||||||
|
let (input, _) = le_u16.parse(bytes)?;
|
||||||
|
|
||||||
|
// Read board name
|
||||||
|
let (input, name) = pstring(50)(input)?;
|
||||||
|
|
||||||
|
// Read terrain
|
||||||
|
const NUM_TILES: usize = 60 * 25;
|
||||||
|
let mut input = input;
|
||||||
|
let mut tiles = Vec::with_capacity(NUM_TILES);
|
||||||
|
while tiles.len() < NUM_TILES {
|
||||||
|
let (next_input, (count, element, color)) = (le_u8, le_u8, le_u8).parse(input)?;
|
||||||
|
input = next_input;
|
||||||
|
let count: usize = if count == 0 { 256 } else { count.into() };
|
||||||
|
for _ in 0..count {
|
||||||
|
tiles.push(Tile { element, color });
|
||||||
|
if tiles.len() > NUM_TILES {
|
||||||
|
return Err(ParseError::InvalidTileCount(tiles.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read board info
|
||||||
|
let (input, (max_shots, is_dark)) = (le_u8, bool_u8).parse(input)?;
|
||||||
|
let (input, (exit_north, exit_south, exit_west, exit_east)) =
|
||||||
|
(le_u8, le_u8, le_u8, le_u8).parse(input)?;
|
||||||
|
let (input, (restart_on_zap, message)) = (bool_u8, pstring(58)).parse(input)?;
|
||||||
|
let (input, (enter_x, enter_y, time_limit)) = (le_u8, le_u8, le_i16).parse(input)?;
|
||||||
|
let (input, _) = take(16usize)(input)?;
|
||||||
|
|
||||||
|
// Read stats
|
||||||
|
let (input, num_stats) = le_i16(input)?;
|
||||||
|
let num_stats = num_stats + 1;
|
||||||
|
if num_stats < 0 {
|
||||||
|
return Err(ParseError::NegativeStatCount);
|
||||||
|
}
|
||||||
|
let (_input, stats) = count(Stat::parse, num_stats as usize).parse(input)?;
|
||||||
|
|
||||||
|
Ok(Board {
|
||||||
|
name,
|
||||||
|
tiles,
|
||||||
|
max_shots,
|
||||||
|
is_dark,
|
||||||
|
exit_north,
|
||||||
|
exit_south,
|
||||||
|
exit_east,
|
||||||
|
exit_west,
|
||||||
|
restart_on_zap,
|
||||||
|
message,
|
||||||
|
enter_x,
|
||||||
|
enter_y,
|
||||||
|
time_limit,
|
||||||
|
stats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a standalone .brd file (same format, no world header).
|
||||||
|
pub fn from_brd_bytes(bytes: &[u8]) -> Result<Board, ParseError> {
|
||||||
|
Board::from_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize this board to bytes.
|
||||||
|
pub fn to_bytes(&self) -> Result<Vec<u8>, ParseError> {
|
||||||
|
let mut result = vec![];
|
||||||
|
result.push_padding(2); // reserve space for board size
|
||||||
|
result.push_string(50, &self.name)?;
|
||||||
|
|
||||||
|
// Encode terrain
|
||||||
|
if self.tiles.len() != 1500 {
|
||||||
|
return Err(ParseError::InvalidTileCount(self.tiles.len()));
|
||||||
|
}
|
||||||
|
let mut iter = self.tiles.iter().peekable();
|
||||||
|
while let Some(tile) = iter.next() {
|
||||||
|
let mut count: u8 = 1;
|
||||||
|
while count < 255 && iter.peek().map_or(false, |&next_tile| next_tile == tile) {
|
||||||
|
count += 1;
|
||||||
|
iter.next();
|
||||||
|
}
|
||||||
|
result.push(count);
|
||||||
|
result.push(tile.element);
|
||||||
|
result.push(tile.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Board info
|
||||||
|
result.push(self.max_shots);
|
||||||
|
result.push_bool(self.is_dark);
|
||||||
|
result.push(self.exit_north);
|
||||||
|
result.push(self.exit_south);
|
||||||
|
result.push(self.exit_west);
|
||||||
|
result.push(self.exit_east);
|
||||||
|
result.push_bool(self.restart_on_zap);
|
||||||
|
result.push_string(58, &self.message)?;
|
||||||
|
result.push(self.enter_x);
|
||||||
|
result.push(self.enter_y);
|
||||||
|
result.push_i16(self.time_limit);
|
||||||
|
result.push_padding(16);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
let num_stats: i16 = (self.stats.len() as i16) - 1;
|
||||||
|
result.push_i16(num_stats);
|
||||||
|
for stat in &self.stats {
|
||||||
|
result.extend_from_slice(&stat.to_bytes()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix up board size
|
||||||
|
let size: u16 = (result.len() - 2)
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| ParseError::BoardTooLarge)?;
|
||||||
|
result.splice(0..2, size.to_le_bytes());
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stat {
|
||||||
|
fn parse(input: &[u8]) -> IResult<&[u8], Self, ParseError> {
|
||||||
|
let (input, (x, y, x_step, y_step)) = (le_u8, le_u8, le_i16, le_i16).parse(input)?;
|
||||||
|
let (input, (cycle, p1, p2, p3)) = (le_i16, le_u8, le_u8, le_u8).parse(input)?;
|
||||||
|
let (input, (follower, leader)) = (le_i16, le_i16).parse(input)?;
|
||||||
|
let (input, (under_element, under_color)) = (le_u8, le_u8).parse(input)?;
|
||||||
|
let (input, _) = take(4usize)(input)?; // unused pointer
|
||||||
|
let (input, (instruction_pointer, length)) = (le_i16, le_i16).parse(input)?;
|
||||||
|
let (input, _) = take(8usize)(input)?; // padding
|
||||||
|
|
||||||
|
let (input, program) = if length < 0 {
|
||||||
|
// Negative length means bound to another stat
|
||||||
|
(input, Program::Bound((-length) as u16))
|
||||||
|
} else {
|
||||||
|
// Positive length means own code
|
||||||
|
let (input, code_bytes) = take(length as usize)(input)?;
|
||||||
|
(input, Program::Own(decode_multiline(code_bytes)))
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Stat {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
x_step,
|
||||||
|
y_step,
|
||||||
|
follower,
|
||||||
|
leader,
|
||||||
|
cycle,
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
p3,
|
||||||
|
under: Tile {
|
||||||
|
element: under_element,
|
||||||
|
color: under_color,
|
||||||
|
},
|
||||||
|
instruction_pointer,
|
||||||
|
program,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_bytes(&self) -> Result<Vec<u8>, ParseError> {
|
||||||
|
let mut result = vec![];
|
||||||
|
result.push(self.x);
|
||||||
|
result.push(self.y);
|
||||||
|
result.push_i16(self.x_step);
|
||||||
|
result.push_i16(self.y_step);
|
||||||
|
result.push_i16(self.cycle);
|
||||||
|
result.push(self.p1);
|
||||||
|
result.push(self.p2);
|
||||||
|
result.push(self.p3);
|
||||||
|
result.push_i16(self.follower);
|
||||||
|
result.push_i16(self.leader);
|
||||||
|
result.push(self.under.element);
|
||||||
|
result.push(self.under.color);
|
||||||
|
result.push_padding(4); // unused pointer
|
||||||
|
result.push_i16(self.instruction_pointer);
|
||||||
|
|
||||||
|
match &self.program {
|
||||||
|
Program::Bound(index) => {
|
||||||
|
result.push_i16(-(*index as i16));
|
||||||
|
result.push_padding(8);
|
||||||
|
}
|
||||||
|
Program::Own(code) => {
|
||||||
|
let code_bytes = encode_multiline(code)?;
|
||||||
|
result.push_i16(code_bytes.len() as i16);
|
||||||
|
result.push_padding(8);
|
||||||
|
result.extend_from_slice(&code_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user