Compare commits
1 Commits
v0.1.0
..
452fad3302
| Author | SHA1 | Date | |
|---|---|---|---|
| 452fad3302 |
Generated
+138
-6
@@ -91,6 +91,28 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
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]]
|
[[package]]
|
||||||
name = "insta"
|
name = "insta"
|
||||||
version = "1.43.1"
|
version = "1.43.1"
|
||||||
@@ -134,13 +156,12 @@ name = "marzipan"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"codepage-437",
|
|
||||||
"compact_str",
|
"compact_str",
|
||||||
"insta",
|
"insta",
|
||||||
"lexopt",
|
"lexopt",
|
||||||
"mzp-peg-macro",
|
"mzp-peg-macro",
|
||||||
"nom",
|
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"zztff",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -168,12 +189,43 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.95"
|
||||||
@@ -212,18 +264,27 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.219"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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 = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.219"
|
version = "1.0.228"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -253,6 +314,56 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@@ -331,3 +442,24 @@ name = "windows_x86_64_msvc"
|
|||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
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",
|
||||||
|
]
|
||||||
|
|||||||
+1
-2
@@ -12,9 +12,8 @@ repository = "https://github.com/cmounce/marzipan"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
codepage-437 = "0.1.0"
|
|
||||||
mzp-peg-macro = { path = "./mzp_peg_macro", version = "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"
|
compact_str = "0.9.0"
|
||||||
rustc-hash = "2.1.1"
|
rustc-hash = "2.1.1"
|
||||||
lexopt = "0.3.1"
|
lexopt = "0.3.1"
|
||||||
|
|||||||
-109
@@ -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<Vec<u8>> {
|
|
||||||
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<Vec<u8>> {
|
|
||||||
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<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}""###);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+9
-5
@@ -1,6 +1,6 @@
|
|||||||
use std::{cell::RefCell, collections::VecDeque, error::Error, fmt::Display, ops::Range};
|
use std::{cell::RefCell, collections::VecDeque, error::Error, fmt::Display, ops::Range};
|
||||||
|
|
||||||
use crate::world::World;
|
use zztff::{Program, World};
|
||||||
|
|
||||||
pub enum Context<'a> {
|
pub enum Context<'a> {
|
||||||
Base(Box<RefCell<Vec<CompileMessage>>>),
|
Base(Box<RefCell<Vec<CompileMessage>>>),
|
||||||
@@ -148,20 +148,24 @@ impl CompileMessage {
|
|||||||
}
|
}
|
||||||
let board = location.board.map(|i| &world.boards[i]);
|
let board = location.board.map(|i| &world.boards[i]);
|
||||||
if let Some(board) = board {
|
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 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 {
|
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 name = first_line.filter(|x| x.starts_with("@")).unwrap_or("stat");
|
||||||
let (x, y) = (stat.x, stat.y);
|
let (x, y) = (stat.x, stat.y);
|
||||||
breadcrumbs.push(format!("{name} ({x},{y})"));
|
breadcrumbs.push(format!("{name} ({x},{y})"));
|
||||||
}
|
}
|
||||||
let span = stat.and_then(|stat| {
|
let span = code.and_then(|code| {
|
||||||
location
|
location
|
||||||
.span
|
.span
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|span| RichSpan::new(&span, &stat.code))
|
.map(|span| RichSpan::new(&span, code))
|
||||||
});
|
});
|
||||||
if let Some(ref span) = span {
|
if let Some(ref span) = span {
|
||||||
let line = span.line_number;
|
let line = span.line_number;
|
||||||
|
|||||||
+6
-2
@@ -3,7 +3,8 @@ use std::ops::Range;
|
|||||||
use compact_str::CompactString;
|
use compact_str::CompactString;
|
||||||
use grammar::Tag;
|
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<Chunk>;
|
pub type ParsedStat = Vec<Chunk>;
|
||||||
|
|
||||||
@@ -26,7 +27,10 @@ pub struct LabelName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_stat_labels(stat: &Stat, ctx: &Context) -> ParsedStat {
|
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);
|
let mut parser = ParseState::new(code);
|
||||||
assert!(
|
assert!(
|
||||||
grammar::program(&mut parser),
|
grammar::program(&mut parser),
|
||||||
|
|||||||
+33
-29
@@ -1,7 +1,8 @@
|
|||||||
use compact_str::CompactString;
|
use compact_str::CompactString;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{error::Context, world::Board};
|
use crate::error::Context;
|
||||||
|
use zztff::{Board, Program};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
parse::{Chunk, ParsedStat, parse_stat_labels},
|
parse::{Chunk, ParsedStat, parse_stat_labels},
|
||||||
@@ -36,18 +37,25 @@ pub fn process_labels(board: &Board, ctx: &Context) -> Option<Board> {
|
|||||||
|
|
||||||
// Join chunks together and replace old stats' code
|
// Join chunks together and replace old stats' code
|
||||||
for (old_stat, parsed_stat) in board.stats.iter_mut().zip(stats.into_iter()) {
|
for (old_stat, parsed_stat) in board.stats.iter_mut().zip(stats.into_iter()) {
|
||||||
let new_code = parsed_stat
|
// Only update the stat's code string if it had a code string to begin with.
|
||||||
.into_iter()
|
//
|
||||||
.map(|chunk| match chunk {
|
// This ensures we don't accidentally un-bind a stat by overwriting its binding with an
|
||||||
Chunk::Verbatim(s) => s,
|
// empty code string. It would probably be cleaner if we refactored the code to use
|
||||||
Chunk::Label {
|
// Option<ParsedStat> or similar, so that bound stats would never lead to an empty string
|
||||||
is_ref: _,
|
// existing in the first place.
|
||||||
is_anon: _,
|
if let Program::Own(code) = &mut old_stat.program {
|
||||||
name,
|
*code = parsed_stat
|
||||||
} => name.name.into(),
|
.into_iter()
|
||||||
})
|
.map(|chunk| match chunk {
|
||||||
.collect();
|
Chunk::Verbatim(s) => s,
|
||||||
old_stat.code = new_code;
|
Chunk::Label {
|
||||||
|
is_ref: _,
|
||||||
|
is_anon: _,
|
||||||
|
name,
|
||||||
|
} => name.name.into(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(!ctx.any_errors()).then_some(board)
|
(!ctx.any_errors()).then_some(board)
|
||||||
@@ -269,10 +277,8 @@ mod test {
|
|||||||
|
|
||||||
use insta::assert_snapshot;
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
use crate::{
|
use crate::error::Context;
|
||||||
error::Context,
|
use zztff::{Board, Program, Stat, World};
|
||||||
world::{Board, Stat, World},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::process_labels;
|
use super::process_labels;
|
||||||
|
|
||||||
@@ -286,19 +292,10 @@ mod test {
|
|||||||
.map(|code| Stat {
|
.map(|code| Stat {
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 1,
|
y: 1,
|
||||||
x_step: 0,
|
|
||||||
y_step: 0,
|
|
||||||
cycle: 3,
|
cycle: 3,
|
||||||
p1: 2,
|
p1: 2,
|
||||||
p2: 0,
|
program: Program::Own(code),
|
||||||
p3: 0,
|
..Stat::default()
|
||||||
follower: -1,
|
|
||||||
leader: -1,
|
|
||||||
under_element: 0,
|
|
||||||
under_color: 0,
|
|
||||||
instruction_pointer: 0,
|
|
||||||
bind_index: 0,
|
|
||||||
code,
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
board
|
board
|
||||||
@@ -311,7 +308,14 @@ mod test {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn board_to_text(board: Board) -> String {
|
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")
|
codes.join("---\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,9 +1,7 @@
|
|||||||
mod encoding;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod labels;
|
mod labels;
|
||||||
mod peg;
|
mod peg;
|
||||||
mod preprocess;
|
mod preprocess;
|
||||||
mod world;
|
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use error::Context as ErrContext;
|
use error::Context as ErrContext;
|
||||||
@@ -15,7 +13,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::exit,
|
process::exit,
|
||||||
};
|
};
|
||||||
use world::World;
|
use zztff::{Program, World};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let mut input_file = None;
|
let mut input_file = None;
|
||||||
@@ -75,7 +73,9 @@ fn main() -> Result<()> {
|
|||||||
// Codegen: Evaluate all macros
|
// Codegen: Evaluate all macros
|
||||||
for board in &mut world.boards {
|
for board in &mut world.boards {
|
||||||
for stat in &mut board.stats {
|
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)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-430
@@ -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<I> ParseError<I> 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<Err<LoadError>> for LoadError {
|
|
||||||
fn from(value: Err<LoadError>) -> 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<u8>,
|
|
||||||
pub flags: [Vec<u8>; 10],
|
|
||||||
pub time: i16,
|
|
||||||
pub time_ticks: i16,
|
|
||||||
pub locked: bool,
|
|
||||||
pub boards: Vec<Board>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<u8>,
|
|
||||||
pub enter_x: u8,
|
|
||||||
pub enter_y: u8,
|
|
||||||
pub time_limit: i16,
|
|
||||||
pub stats: Vec<Stat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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<World, LoadError> {
|
|
||||||
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<Vec<Board>, 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<Vec<u8>, Box<dyn Error>> {
|
|
||||||
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<Board, LoadError> {
|
|
||||||
// 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<Vec<u8>, &'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<u8> {
|
|
||||||
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<u8>, LoadError> {
|
|
||||||
move |input: &[u8]| -> IResult<&[u8], Vec<u8>, 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<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: &[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user