diff --git a/Cargo.lock b/Cargo.lock index 76feaa2..5935fbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,28 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "insta" version = "1.43.1" @@ -134,13 +156,12 @@ name = "marzipan" version = "0.1.0" dependencies = [ "anyhow", - "codepage-437", "compact_str", "insta", "lexopt", "mzp-peg-macro", - "nom", "rustc-hash", + "zztff", ] [[package]] @@ -168,12 +189,43 @@ dependencies = [ "memchr", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -212,18 +264,27 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -253,6 +314,56 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.18" @@ -331,3 +442,24 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "zztff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81bebb421e202e22f22f88427f98582ab74dc846a05ffc6da1527cbe6cdae9d" +dependencies = [ + "codepage-437", + "nom", + "num_enum", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml index 5c752f4..cf075b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,8 @@ repository = "https://github.com/cmounce/marzipan" [dependencies] anyhow = "1.0.86" -codepage-437 = "0.1.0" mzp-peg-macro = { path = "./mzp_peg_macro", version = "0.1.0" } -nom = "8.0.0" +zztff = "0.3" compact_str = "0.9.0" rustc-hash = "2.1.1" lexopt = "0.3.1" diff --git a/src/encoding.rs b/src/encoding.rs deleted file mode 100644 index b9a0f66..0000000 --- a/src/encoding.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anyhow::{Result, anyhow}; -use codepage_437::CP437_WINGDINGS; - -/// 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> { - input - .chars() - .map(|c| { - if c == '\n' { - Ok('\r' as u8) - } else { - CP437_WINGDINGS - .encode(c) - .ok_or(anyhow!("Couldn't encode char: {}", c)) - } - }) - .collect() -} - -/// Deserialize a multi-line code string. -pub fn decode_multiline(input: &[u8]) -> String { - input - .iter() - .map(|&x| { - if x == '\r' as u8 { - '\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> { - input - .chars() - .map(|c| { - CP437_WINGDINGS - .encode(c) - .ok_or(anyhow!("Couldn't encode char: {}", 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 { - encode_multiline(input).expect("Error in test") - } - - fn serialize_title(input: &str) -> Vec { - encode_oneline(input).expect("Error in test") - } - - #[test] - fn roundtrip_multiline() { - let bytes: Vec = (0..=255).collect(); - assert_eq!(bytes, serialize_code(&decode_multiline(&bytes))) - } - - #[test] - fn roundtrip_oneline() { - let bytes: Vec = (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::>()), @r###""\0☺☻♥♦♣♠•◘○◙♂♀\n♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼""###); - assert_debug_snapshot!(decode_multiline(&(112..144).collect::>()), @r###""pqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅ""###); - assert_debug_snapshot!(decode_multiline(&(224..=255).collect::>()), @r###""αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u{a0}""###); - } -} diff --git a/src/error.rs b/src/error.rs index b1e310f..2666ef5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use std::{cell::RefCell, collections::VecDeque, error::Error, fmt::Display, ops::Range}; -use crate::world::World; +use zztff::{Program, World}; pub enum Context<'a> { Base(Box>>), @@ -148,20 +148,24 @@ impl CompileMessage { } let board = location.board.map(|i| &world.boards[i]); if let Some(board) = board { - breadcrumbs.push(board.name.clone()); + breadcrumbs.push(board.title.clone()); } let stat = board.and_then(|board| location.stat.map(|i| &board.stats[i])); + let code = stat.and_then(|stat| match &stat.program { + Program::Own(code) => Some(code.as_str()), + Program::Bound(_) => None, + }); if let Some(stat) = stat { - let first_line = stat.code.lines().next(); + let first_line = code.and_then(|x| x.lines().next()); let name = first_line.filter(|x| x.starts_with("@")).unwrap_or("stat"); let (x, y) = (stat.x, stat.y); breadcrumbs.push(format!("{name} ({x},{y})")); } - let span = stat.and_then(|stat| { + let span = code.and_then(|code| { location .span .as_ref() - .map(|span| RichSpan::new(&span, &stat.code)) + .map(|span| RichSpan::new(&span, code)) }); if let Some(ref span) = span { let line = span.line_number; diff --git a/src/labels/parse.rs b/src/labels/parse.rs index 05443c4..45c7af8 100644 --- a/src/labels/parse.rs +++ b/src/labels/parse.rs @@ -3,7 +3,8 @@ use std::ops::Range; use compact_str::CompactString; use grammar::Tag; -use crate::{error::Context, peg::ParseState, world::Stat}; +use crate::{error::Context, peg::ParseState}; +use zztff::{Program, Stat}; pub type ParsedStat = Vec; @@ -26,7 +27,10 @@ pub struct LabelName { } pub fn parse_stat_labels(stat: &Stat, ctx: &Context) -> ParsedStat { - let code = &stat.code; + let code = match &stat.program { + Program::Own(code) => code, + Program::Bound(_) => return Vec::new(), + }; let mut parser = ParseState::new(code); assert!( grammar::program(&mut parser), diff --git a/src/labels/process.rs b/src/labels/process.rs index 647e768..918de8a 100644 --- a/src/labels/process.rs +++ b/src/labels/process.rs @@ -1,7 +1,8 @@ use compact_str::CompactString; use rustc_hash::FxHashMap; -use crate::{error::Context, world::Board}; +use crate::error::Context; +use zztff::{Board, Program}; use super::{ parse::{Chunk, ParsedStat, parse_stat_labels}, @@ -36,18 +37,25 @@ pub fn process_labels(board: &Board, ctx: &Context) -> Option { // Join chunks together and replace old stats' code for (old_stat, parsed_stat) in board.stats.iter_mut().zip(stats.into_iter()) { - let new_code = parsed_stat - .into_iter() - .map(|chunk| match chunk { - Chunk::Verbatim(s) => s, - Chunk::Label { - is_ref: _, - is_anon: _, - name, - } => name.name.into(), - }) - .collect(); - old_stat.code = new_code; + // Only update the stat's code string if it had a code string to begin with. + // + // This ensures we don't accidentally un-bind a stat by overwriting its binding with an + // empty code string. It would probably be cleaner if we refactored the code to use + // Option or similar, so that bound stats would never lead to an empty string + // existing in the first place. + if let Program::Own(code) = &mut old_stat.program { + *code = parsed_stat + .into_iter() + .map(|chunk| match chunk { + Chunk::Verbatim(s) => s, + Chunk::Label { + is_ref: _, + is_anon: _, + name, + } => name.name.into(), + }) + .collect(); + } } (!ctx.any_errors()).then_some(board) @@ -269,10 +277,8 @@ mod test { use insta::assert_snapshot; - use crate::{ - error::Context, - world::{Board, Stat, World}, - }; + use crate::error::Context; + use zztff::{Board, Program, Stat, World}; use super::process_labels; @@ -286,19 +292,10 @@ mod test { .map(|code| Stat { x: 1, y: 1, - x_step: 0, - y_step: 0, cycle: 3, p1: 2, - p2: 0, - p3: 0, - follower: -1, - leader: -1, - under_element: 0, - under_color: 0, - instruction_pointer: 0, - bind_index: 0, - code, + program: Program::Own(code), + ..Stat::default() }) .collect(); board @@ -311,7 +308,14 @@ mod test { } fn board_to_text(board: Board) -> String { - let codes: Vec<_> = board.stats.into_iter().map(|stat| stat.code).collect(); + let codes: Vec<_> = board + .stats + .into_iter() + .filter_map(|stat| match stat.program { + Program::Own(code) => Some(code), + Program::Bound(_) => None, + }) + .collect(); codes.join("---\n") } diff --git a/src/main.rs b/src/main.rs index 822452f..ecb2458 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ -mod encoding; mod error; mod labels; mod peg; mod preprocess; -mod world; use anyhow::{Result, anyhow}; use error::Context as ErrContext; @@ -15,7 +13,7 @@ use std::{ path::{Path, PathBuf}, process::exit, }; -use world::World; +use zztff::{Program, World}; fn main() -> Result<()> { let mut input_file = None; @@ -75,7 +73,9 @@ fn main() -> Result<()> { // Codegen: Evaluate all macros for board in &mut world.boards { for stat in &mut board.stats { - stat.code = eval_context.eval_program(&stat.code)?; + if let Program::Own(code) = &mut stat.program { + *code = eval_context.eval_program(code)?; + } } } diff --git a/src/world.rs b/src/world.rs deleted file mode 100644 index f7de7d9..0000000 --- a/src/world.rs +++ /dev/null @@ -1,430 +0,0 @@ -use std::{error::Error, fmt::Display}; - -use nom::{ - Err, IResult, Parser, - bytes::complete::{tag, take}, - combinator::fail, - error::{ErrorKind, ParseError}, - multi::count, - number::complete::{le_i16, le_u8, le_u16}, -}; - -use crate::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline}; - -#[derive(Debug)] -pub struct LoadError { - message: String, -} - -impl ParseError for LoadError { - fn from_error_kind(_input: I, kind: ErrorKind) -> Self { - Self { - message: kind.description().into(), - } - } - - fn append(_input: I, kind: ErrorKind, other: Self) -> Self { - Self { - message: format!("{}: {:?}", other.message, kind), - } - } -} - -impl From> for LoadError { - fn from(value: Err) -> Self { - let message = match value { - Err::Error(e) => e.message, - Err::Incomplete(x) => format!("{:?}", x), - Err::Failure(e) => e.message, - }; - Self { message } - } -} - -impl From<&str> for LoadError { - fn from(value: &str) -> Self { - Self { - message: value.into(), - } - } -} - -impl Display for LoadError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.message.fmt(f) - } -} - -impl Error for LoadError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - None - } - - fn description(&self) -> &str { - "description() is deprecated; use Display" - } - - fn cause(&self) -> Option<&dyn Error> { - self.source() - } -} - -#[derive(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 world_name: Vec, - pub flags: [Vec; 10], - pub time: i16, - pub time_ticks: i16, - pub locked: bool, - pub boards: Vec, -} - -#[derive(Clone)] -pub struct Board { - pub name: String, - pub terrain: Vec<[u8; 2]>, - pub max_shots: u8, - pub is_dark: bool, - pub board_n: u8, - pub board_s: u8, - pub board_w: u8, - pub board_e: u8, - pub reenter_when_zapped: bool, - pub message: Vec, - pub enter_x: u8, - pub enter_y: u8, - pub time_limit: i16, - pub stats: Vec, -} - -#[derive(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_element: u8, - pub under_color: u8, - pub instruction_pointer: i16, - pub bind_index: i16, - pub code: String, -} - -impl World { - pub fn from_bytes(bytes: &[u8]) -> Result { - let (input, (_, num_boards)) = (tag(&[0xff, 0xff][..]), le_i16).parse(bytes)?; - 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, world_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, LoadError> = 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, - world_name, - flags: flags.try_into().unwrap(), - time, - time_ticks, - locked, - boards, - }) - } - - pub fn to_bytes(&self) -> Result, Box> { - 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.world_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 { - pub fn from_bytes(bytes: &[u8]) -> Result { - // Ignore length bytes - let (input, _) = le_u16.parse(bytes)?; - - // Read board name - let (input, name_bytes) = pstring(50)(input)?; - let name = decode_oneline(&name_bytes); - - // Read terrain - const NUM_TILES: usize = 60 * 25; - let mut input = input; - let mut terrain = Vec::with_capacity(NUM_TILES); - while terrain.len() < NUM_TILES { - let (next_input, (count, element, color)) = (le_u8, le_u8, le_u8).parse(input)?; - input = next_input; - let count: u32 = if count == 0 { 256 } else { count.into() }; - for _ in 0..count { - terrain.push([element, color]); - if terrain.len() > NUM_TILES { - return Err("too many tiles of board terrain".into()); - } - } - } - - // Read board info - let (input, (max_shots, is_dark)) = (le_u8, bool_u8).parse(input)?; - let (input, (board_n, board_s, board_w, board_e)) = - (le_u8, le_u8, le_u8, le_u8).parse(input)?; - let (input, (reenter_when_zapped, 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("cannot have a negative number of stats".into()); - } - let (_input, stats) = count(Stat::from_bytes, num_stats as usize).parse(input)?; - - Ok(Board { - name, - terrain, - max_shots, - is_dark, - board_n, - board_s, - board_e, - board_w, - reenter_when_zapped, - message, - enter_x, - enter_y, - time_limit, - stats, - }) - } - - pub fn to_bytes(&self) -> Result, &'static str> { - let mut result = vec![]; - result.push_padding(2); // reserve space for board size - let name_bytes = encode_oneline(&self.name).unwrap(); - result.push_string(50, &name_bytes)?; - - // Encode terrain - if self.terrain.len() != 1500 { - return Err("invalid number of tiles for board terrain"); - } - let mut iter = self.terrain.iter().peekable(); - while let Some(tile) = iter.next() { - let mut count = 1; - while count < 255 && iter.peek().map_or(false, |&next_tile| next_tile == tile) { - count += 1; - iter.next(); - } - result.push(count); - result.extend_from_slice(tile); - } - - // Board info - result.push(self.max_shots); - result.push_bool(self.is_dark); - result.push(self.board_n); - result.push(self.board_s); - result.push(self.board_w); - result.push(self.board_e); - result.push_bool(self.reenter_when_zapped); - 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() - 1) - .try_into() - .map_err(|_| "invalid length for stats")?; - 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(|_| "too many bytes of board data")?; - result.splice(0..2, size.to_le_bytes()); - - Ok(result) - } -} - -impl Stat { - pub fn from_bytes(input: &[u8]) -> IResult<&[u8], Self, LoadError> { - 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)?; - let (input, (instruction_pointer, length)) = (le_i16, le_i16).parse(input)?; - let (input, _) = take(8usize)(input)?; - let (input, code_bytes) = take(0.max(length) as usize)(input)?; - let code = decode_multiline(&code_bytes); - Ok(( - input, - Stat { - x, - y, - x_step, - y_step, - follower, - leader, - cycle, - p1, - p2, - p3, - under_element, - under_color, - instruction_pointer, - bind_index: 0.min(length), - code, - }, - )) - } - - pub fn to_bytes(&self) -> Vec { - 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); - result.push_i16(self.instruction_pointer); - // TODO: more safety around valid bind-indexes (positive? negative?) - let code_bytes = encode_multiline(&self.code).unwrap(); - result.push_i16(if self.bind_index < 0 { - self.bind_index - } else { - code_bytes.len() as i16 - }); - result.push_padding(8); - if self.bind_index >= 0 { - // TODO: more safety around bind-index XOR code - result.extend_from_slice(&code_bytes); - } - result - } -} - -fn bool_u8(input: &[u8]) -> IResult<&[u8], bool, LoadError> { - let (input, byte) = le_u8(input)?; - Ok((input, byte != 0)) -} - -fn pstring(cap: u8) -> impl Fn(&[u8]) -> IResult<&[u8], Vec, LoadError> { - move |input: &[u8]| -> IResult<&[u8], Vec, LoadError> { - let (input, len) = le_u8(input)?; - if len >= cap { - return fail().parse(input); - } - let (input, data) = take(len)(input)?; - let (input, _) = take(cap - len)(input)?; - Ok((input, data.to_vec())) - } -} - -fn board_slice(bytes: &[u8]) -> IResult<&[u8], &[u8], LoadError> { - let (_, size) = le_u16.parse(bytes)?; - take(size + 2).parse(bytes) -} - -trait SerializationHelpers { - fn push_bool(&mut self, value: bool); - fn push_i16(&mut self, value: i16); - fn push_string(&mut self, cap: u8, value: &[u8]) -> Result<(), &'static str>; - fn push_padding(&mut self, size: usize); -} - -impl SerializationHelpers for Vec { - 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: &[u8]) -> Result<(), &'static str> { - if value.len() > cap as usize { - return Err("string too long"); - } - self.push(value.len() as u8); - self.extend_from_slice(value); - self.push_padding(cap as usize - value.len()); - Ok(()) - } - - fn push_padding(&mut self, size: usize) { - self.resize(self.len() + size, 0); - } -}