10 Commits

Author SHA1 Message Date
cmounce 4d41eaaf56 Bump version to v0.3.0 2026-04-29 14:23:13 -07:00
cmounce 5fffaf2582 Update dependencies 2026-04-29 14:18:41 -07:00
cmounce 1f3173a563 Tweak comment 2026-04-29 14:16:21 -07:00
cmounce e1a5c6528d Add doc comment about board size limits 2026-03-21 00:22:27 -07:00
cmounce cfb4518a53 Rename x/y step fields to step_x, step_y
This makes things more consistent with Board's `enter_x` and `enter_y`,
and it prevents autocomplete prefix collisions with Stat's `x` and `y`.
2026-03-20 23:07:39 -07:00
cmounce 24d20ae01f Fix up names, doc comments for some Board fields 2026-03-20 20:55:03 -07:00
cmounce 4ed4eb46b4 Clean up interface for standalone board parsing 2026-03-20 19:55:40 -07:00
cmounce ba1889d40d Rename a text element yet again 2026-03-20 19:46:11 -07:00
cmounce d82e05505c Update packages, bump version to 0.2.0 2026-03-18 17:23:39 -07:00
cmounce 3e8bedb0c9 Clean up docs for Program enum, text functions 2026-03-18 17:20:53 -07:00
8 changed files with 188 additions and 217 deletions
Generated
+50 -118
View File
@@ -10,9 +10,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -31,14 +31,13 @@ dependencies = [
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.11" version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
dependencies = [ dependencies = [
"encode_unicode", "encode_unicode",
"libc", "libc",
"once_cell", "windows-sys",
"windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -81,14 +80,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
@@ -120,9 +119,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -138,21 +137,21 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown 0.17.0",
"serde", "serde",
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.46.3" version = "1.47.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e"
dependencies = [ dependencies = [
"console", "console",
"once_cell", "once_cell",
@@ -162,9 +161,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
@@ -174,9 +173,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@@ -207,9 +206,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [ dependencies = [
"num_enum_derive", "num_enum_derive",
"rustversion", "rustversion",
@@ -217,9 +216,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum_derive" name = "num_enum_derive"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@@ -229,9 +228,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
@@ -286,7 +285,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@@ -303,9 +302,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]] [[package]]
name = "serde" name = "serde"
@@ -368,15 +367,15 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.26.0" version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom", "getrandom",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@@ -401,18 +400,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.4+spec-1.1.0" version = "0.25.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@@ -422,9 +421,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.9+spec-1.1.0" version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
@@ -443,11 +442,11 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.2+wasi-0.2.9" version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [ dependencies = [
"wit-bindgen", "wit-bindgen 0.57.1",
] ]
[[package]] [[package]]
@@ -456,7 +455,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [ dependencies = [
"wit-bindgen", "wit-bindgen 0.51.0",
] ]
[[package]] [[package]]
@@ -499,15 +498,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -517,75 +507,11 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -599,6 +525,12 @@ dependencies = [
"wit-bindgen-rust-macro", "wit-bindgen-rust-macro",
] ]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "wit-bindgen-core" name = "wit-bindgen-core"
version = "0.51.0" version = "0.51.0"
@@ -686,7 +618,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]] [[package]]
name = "zztff" name = "zztff"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"codepage-437", "codepage-437",
"insta", "insta",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "zztff" name = "zztff"
version = "0.1.0" version = "0.3.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
description = "Library for reading/writing ZZT worlds and boards" description = "Library for reading/writing ZZT worlds and boards"
+1 -1
View File
@@ -69,6 +69,6 @@ pub enum Element {
CyanText = 49, CyanText = 49,
RedText = 50, RedText = 50,
PurpleText = 51, PurpleText = 51,
BrownText = 52, YellowText = 52,
WhiteText = 53, WhiteText = 53,
} }
+1 -1
View File
@@ -34,7 +34,7 @@
//! ```rust,no_run //! ```rust,no_run
//! let bytes = std::fs::read("TOWN.ZZT")?; //! let bytes = std::fs::read("TOWN.ZZT")?;
//! let world = zztff::World::from_bytes(&bytes)?; //! let world = zztff::World::from_bytes(&bytes)?;
//! let first3: Vec<&str> = world.boards.iter().take(3).map(|b| b.name.as_str()).collect(); //! let first3: Vec<&str> = world.boards.iter().take(3).map(|b| b.title.as_str()).collect();
//! println!("Loaded world {:?}, {} boards.", world.name, world.boards.len()); //! println!("Loaded world {:?}, {} boards.", world.name, world.boards.len());
//! println!("First 3 boards: {:?}", first3); //! println!("First 3 boards: {:?}", first3);
//! # Ok::<(), Box<dyn std::error::Error>>(()) //! # Ok::<(), Box<dyn std::error::Error>>(())
@@ -1,6 +1,5 @@
--- ---
source: src/world.rs source: src/world.rs
assertion_line: 589
expression: world expression: world
--- ---
World { World {
@@ -39,7 +38,7 @@ World {
saved_game: true, saved_game: true,
boards: [ boards: [
Board { Board {
name: "Title screen", title: "Title screen",
tiles: [Tile; 1500], tiles: [Tile; 1500],
max_shots: 255, max_shots: 255,
is_dark: false, is_dark: false,
@@ -47,7 +46,7 @@ World {
exit_south: None, exit_south: None,
exit_west: None, exit_west: None,
exit_east: None, exit_east: None,
restart_on_zap: false, reenter_when_zapped: false,
message: "", message: "",
enter_x: 1, enter_x: 1,
enter_y: 1, enter_y: 1,
@@ -56,8 +55,8 @@ World {
Stat { Stat {
x: 1, x: 1,
y: 1, y: 1,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 1, cycle: 1,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -76,7 +75,7 @@ World {
], ],
}, },
Board { Board {
name: "First board (NW, defaults)", title: "First board (NW, defaults)",
tiles: [Tile; 1500], tiles: [Tile; 1500],
max_shots: 255, max_shots: 255,
is_dark: false, is_dark: false,
@@ -88,7 +87,7 @@ World {
exit_east: Some( exit_east: Some(
2, 2,
), ),
restart_on_zap: false, reenter_when_zapped: false,
message: "", message: "",
enter_x: 1, enter_x: 1,
enter_y: 1, enter_y: 1,
@@ -97,8 +96,8 @@ World {
Stat { Stat {
x: 1, x: 1,
y: 1, y: 1,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 1, cycle: 1,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -117,7 +116,7 @@ World {
], ],
}, },
Board { Board {
name: "Second board (NE, non-default settings)", title: "Second board (NE, non-default settings)",
tiles: [Tile; 1500], tiles: [Tile; 1500],
max_shots: 0, max_shots: 0,
is_dark: true, is_dark: true,
@@ -129,7 +128,7 @@ World {
1, 1,
), ),
exit_east: None, exit_east: None,
restart_on_zap: true, reenter_when_zapped: true,
message: "Hello, board message!", message: "Hello, board message!",
enter_x: 12, enter_x: 12,
enter_y: 34, enter_y: 34,
@@ -138,8 +137,8 @@ World {
Stat { Stat {
x: 1, x: 1,
y: 1, y: 1,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 1, cycle: 1,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -158,7 +157,7 @@ World {
], ],
}, },
Board { Board {
name: "Third board (SE, mostly object stats)", title: "Third board (SE, mostly object stats)",
tiles: [Tile; 1500], tiles: [Tile; 1500],
max_shots: 255, max_shots: 255,
is_dark: false, is_dark: false,
@@ -170,7 +169,7 @@ World {
4, 4,
), ),
exit_east: None, exit_east: None,
restart_on_zap: false, reenter_when_zapped: false,
message: "", message: "",
enter_x: 1, enter_x: 1,
enter_y: 1, enter_y: 1,
@@ -179,8 +178,8 @@ World {
Stat { Stat {
x: 1, x: 1,
y: 1, y: 1,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 1, cycle: 1,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -199,8 +198,8 @@ World {
Stat { Stat {
x: 3, x: 3,
y: 2, y: 2,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 3, cycle: 3,
p1: 51, p1: 51,
p2: 0, p2: 0,
@@ -219,8 +218,8 @@ World {
Stat { Stat {
x: 20, x: 20,
y: 4, y: 4,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 0, cycle: 0,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -239,8 +238,8 @@ World {
Stat { Stat {
x: 5, x: 5,
y: 2, y: 2,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 1, cycle: 1,
p1: 49, p1: 49,
p2: 1, p2: 1,
@@ -259,8 +258,8 @@ World {
Stat { Stat {
x: 7, x: 7,
y: 2, y: 2,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 3, cycle: 3,
p1: 64, p1: 64,
p2: 0, p2: 0,
@@ -279,8 +278,8 @@ World {
Stat { Stat {
x: 10, x: 10,
y: 2, y: 2,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 3, cycle: 3,
p1: 9, p1: 9,
p2: 0, p2: 0,
@@ -299,7 +298,7 @@ World {
], ],
}, },
Board { Board {
name: "Fourth board (SW, other stats)", title: "Fourth board (SW, other stats)",
tiles: [Tile; 1500], tiles: [Tile; 1500],
max_shots: 255, max_shots: 255,
is_dark: false, is_dark: false,
@@ -311,7 +310,7 @@ World {
exit_east: Some( exit_east: Some(
3, 3,
), ),
restart_on_zap: false, reenter_when_zapped: false,
message: "", message: "",
enter_x: 1, enter_x: 1,
enter_y: 1, enter_y: 1,
@@ -320,8 +319,8 @@ World {
Stat { Stat {
x: 1, x: 1,
y: 1, y: 1,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 1, cycle: 1,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -340,8 +339,8 @@ World {
Stat { Stat {
x: 3, x: 3,
y: 2, y: 2,
x_step: 0, step_x: 0,
y_step: -1, step_y: -1,
cycle: 4, cycle: 4,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -360,8 +359,8 @@ World {
Stat { Stat {
x: 5, x: 5,
y: 2, y: 2,
x_step: 0, step_x: 0,
y_step: 1, step_y: 1,
cycle: 4, cycle: 4,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -380,8 +379,8 @@ World {
Stat { Stat {
x: 7, x: 7,
y: 2, y: 2,
x_step: 1, step_x: 1,
y_step: 0, step_y: 0,
cycle: 4, cycle: 4,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -400,8 +399,8 @@ World {
Stat { Stat {
x: 9, x: 9,
y: 2, y: 2,
x_step: -1, step_x: -1,
y_step: 0, step_y: 0,
cycle: 4, cycle: 4,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -420,8 +419,8 @@ World {
Stat { Stat {
x: 3, x: 3,
y: 4, y: 4,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 2, cycle: 2,
p1: 4, p1: 4,
p2: 4, p2: 4,
@@ -440,8 +439,8 @@ World {
Stat { Stat {
x: 4, x: 4,
y: 4, y: 4,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 2, cycle: 2,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -460,8 +459,8 @@ World {
Stat { Stat {
x: 5, x: 5,
y: 4, y: 4,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 2, cycle: 2,
p1: 0, p1: 0,
p2: 0, p2: 0,
+10 -6
View File
@@ -3,7 +3,8 @@ use codepage_437::CP437_WINGDINGS;
use super::errors::EncodeError; use super::errors::EncodeError;
/// Serialize a multi-line code string for an object or a scroll. /// Serialize a multi-line code string for an object or a scroll.
/// Uses ZZT's convention of CR-terminated lines. ///
/// Characters that do not have a CP437 equivalent will result in an [`EncodeError::EncodingError`].
pub fn encode_multiline(input: &str) -> Result<Vec<u8>, EncodeError> { pub fn encode_multiline(input: &str) -> Result<Vec<u8>, EncodeError> {
input input
.chars() .chars()
@@ -33,8 +34,11 @@ pub fn decode_multiline(input: &[u8]) -> String {
.collect() .collect()
} }
/// Serialize a single-line string for a board title. /// Serialize a single-line string, such as a board title.
/// Newlines cannot be encoded because ZZT interprets them as dingbats (♪). ///
/// Newlines cannot be encoded because in single-line contexts, ZZT interprets that byte as a
/// dingbat, specifically the '♪' character. Passing a newline, or any non-CP437 character, will
/// result in an [`EncodeError::EncodingError`].
pub fn encode_oneline(input: &str) -> Result<Vec<u8>, EncodeError> { pub fn encode_oneline(input: &str) -> Result<Vec<u8>, EncodeError> {
input input
.chars() .chars()
@@ -46,7 +50,7 @@ pub fn encode_oneline(input: &str) -> Result<Vec<u8>, EncodeError> {
.collect() .collect()
} }
/// Deserialize a board title. /// Deserialize a single-line string, such as a board title.
pub fn decode_oneline(input: &[u8]) -> String { pub fn decode_oneline(input: &[u8]) -> String {
input.iter().map(|&x| CP437_WINGDINGS.decode(x)).collect() input.iter().map(|&x| CP437_WINGDINGS.decode(x)).collect()
} }
@@ -60,7 +64,7 @@ mod tests {
encode_multiline(input).expect("Error in test") encode_multiline(input).expect("Error in test")
} }
fn serialize_title(input: &str) -> Vec<u8> { fn serialize_line(input: &str) -> Vec<u8> {
encode_oneline(input).expect("Error in test") encode_oneline(input).expect("Error in test")
} }
@@ -73,7 +77,7 @@ mod tests {
#[test] #[test]
fn roundtrip_oneline() { fn roundtrip_oneline() {
let bytes: Vec<u8> = (0..=255).collect(); let bytes: Vec<u8> = (0..=255).collect();
assert_eq!(bytes, serialize_title(&decode_oneline(&bytes))) assert_eq!(bytes, serialize_line(&decode_oneline(&bytes)))
} }
#[test] #[test]
+81 -45
View File
@@ -125,7 +125,7 @@ pub struct Keys {
/// ///
/// Boards mainly exist within a containing [`World`]. But as part of a editing workflow, they can /// Boards mainly exist within a containing [`World`]. But as part of a editing workflow, they can
/// also be packaged as standalone .BRD files, which use the same format; use /// also be packaged as standalone .BRD files, which use the same format; use
/// [`Board::from_brd_bytes`] to parse one. /// [`Board::from_bytes`] to parse one.
#[derive(Clone)] #[derive(Clone)]
pub struct Board { pub struct Board {
/// The board's title. /// The board's title.
@@ -137,7 +137,7 @@ pub struct Board {
/// shorter limits: ZZT's internal editor artificially limits titles to 34 characters, and /// shorter limits: ZZT's internal editor artificially limits titles to 34 characters, and
/// titles longer than 42 characters may cause visual glitches when displayed in a /// titles longer than 42 characters may cause visual glitches when displayed in a
/// standard-width message box. /// standard-width message box.
pub name: String, pub title: String,
/// The 60x25 terrain grid, stored in order from left-to-right, top-to-bottom. /// The 60x25 terrain grid, stored in order from left-to-right, top-to-bottom.
/// ///
/// For convenience, [`Board::tile`] and [`Board::set_tile`] are available for coordinate-based /// For convenience, [`Board::tile`] and [`Board::set_tile`] are available for coordinate-based
@@ -159,15 +159,16 @@ pub struct Board {
pub exit_west: Option<NonZero<u8>>, pub exit_west: Option<NonZero<u8>>,
/// Board index of the eastern neighbor, or `None` for no exit. /// Board index of the eastern neighbor, or `None` for no exit.
pub exit_east: Option<NonZero<u8>>, pub exit_east: Option<NonZero<u8>>,
/// Whether the player restarts at the board entrance after taking damage. /// Whether the player, upon taking damage, is transported back to where they entered the board.
pub restart_on_zap: bool, pub reenter_when_zapped: bool,
/// The currently-flashing status bar message (max 58 characters). /// The currently-flashing status bar message (max 58 characters).
pub message: String, pub message: String,
/// X coordinate where the player entered the board (1-based). /// X coordinate where the player entered the board (1-based).
/// ///
/// The entrance coordinates are part of the "re-enter when zapped" feature. The values only /// The entry coordinates are part of the "re-enter when zapped" feature. These don't matter
/// really matter if the user starts out on this board; they will be overwritten with the /// most of the time; ZZT ignores the coordinates from the file, preferring to update these with
/// player's actual entry coordinates during play. /// the player's actual entry coordinates. The only time they are ever loaded is for the current
/// board when the user restores a save file.
pub enter_x: u8, pub enter_x: u8,
/// Y coordinate where the player entered the board (1-based). /// Y coordinate where the player entered the board (1-based).
pub enter_y: u8, pub enter_y: u8,
@@ -184,7 +185,7 @@ pub struct Board {
impl Default for Board { impl Default for Board {
fn default() -> Self { fn default() -> Self {
Board { Board {
name: String::new(), title: String::new(),
tiles: [Tile::default(); 1500], tiles: [Tile::default(); 1500],
max_shots: 255, max_shots: 255,
is_dark: false, is_dark: false,
@@ -192,8 +193,11 @@ impl Default for Board {
exit_south: None, exit_south: None,
exit_west: None, exit_west: None,
exit_east: None, exit_east: None,
restart_on_zap: false, reenter_when_zapped: false,
message: String::new(), message: String::new(),
// (1, 1) is an objectively better default for enter x/y than (0, 0). It is possible to
// get ZZT to consume this value by restoring a game, and in that case (0, 0) will take
// the player out of bounds, leading to a crash.
enter_x: 1, enter_x: 1,
enter_y: 1, enter_y: 1,
time_limit: 0, time_limit: 0,
@@ -213,7 +217,7 @@ impl Default for Board {
impl Debug for Board { impl Debug for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Board") f.debug_struct("Board")
.field("name", &self.name) .field("title", &self.title)
.field("tiles", &format_args!("[Tile; 1500]")) .field("tiles", &format_args!("[Tile; 1500]"))
.field("max_shots", &self.max_shots) .field("max_shots", &self.max_shots)
.field("is_dark", &self.is_dark) .field("is_dark", &self.is_dark)
@@ -221,7 +225,7 @@ impl Debug for Board {
.field("exit_south", &self.exit_south) .field("exit_south", &self.exit_south)
.field("exit_west", &self.exit_west) .field("exit_west", &self.exit_west)
.field("exit_east", &self.exit_east) .field("exit_east", &self.exit_east)
.field("restart_on_zap", &self.restart_on_zap) .field("reenter_when_zapped", &self.reenter_when_zapped)
.field("message", &self.message) .field("message", &self.message)
.field("enter_x", &self.enter_x) .field("enter_x", &self.enter_x)
.field("enter_y", &self.enter_y) .field("enter_y", &self.enter_y)
@@ -263,9 +267,9 @@ pub struct Stat {
/// are possible and are useful for advanced techniques, but care must be taken to ensure that /// are possible and are useful for advanced techniques, but care must be taken to ensure that
/// they are safe; ZZT's bounds checking code assumes step distances will never exceed 1, so /// they are safe; ZZT's bounds checking code assumes step distances will never exceed 1, so
/// steps larger than that may go off-board and cause memory corruption. /// steps larger than that may go off-board and cause memory corruption.
pub x_step: i16, pub step_x: i16,
/// Vertical step delta. /// Vertical step delta.
pub y_step: i16, pub step_y: i16,
/// How often this stat is updated, in game ticks. /// How often this stat is updated, in game ticks.
/// ///
/// Roughly speaking, stats update every 1/cycle game ticks. A cycle 1 object will run its code /// Roughly speaking, stats update every 1/cycle game ticks. A cycle 1 object will run its code
@@ -326,8 +330,8 @@ impl Default for Stat {
Stat { Stat {
x: 0, x: 0,
y: 0, y: 0,
x_step: 0, step_x: 0,
y_step: 0, step_y: 0,
cycle: 0, cycle: 0,
p1: 0, p1: 0,
p2: 0, p2: 0,
@@ -351,15 +355,36 @@ pub struct Tile {
pub color: u8, pub color: u8,
} }
/// A stat's ZZT-OOP program. /// A stat's ZZT-OOP program text.
/// ///
/// In ZZT, stats can either have their own code or bind to another stat's code. /// Stats must either have their own program or bind to another stat's program. This is either-or:
/// This enum prevents invalid states where both are set. /// in ZZT, it is impossible to represent a stat that has both (or neither). This enum models that
/// constraint.
///
/// Internally, this is because ZZT uses a single `i16` like an enum discriminant. Zero or more
/// indicates the length of the program text, i.e., how many bytes follow the stat on disk. Negative
/// values are interpreted as negated bind indexes: -123 means no program text follows, and that the
/// stat instead shares code with the 123rd stat on the board.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Program { pub enum Program {
/// The stat owns its own code. /// The stat has its own program code.
///
/// This is the typical default for most stats. For stats that don't have or need a program, an
/// empty `String` is used. The text has a maximum of 32767 characters, though in ZZT 3.2 you
/// will encounter other limits before that.
///
/// Despite the name, this has more to do with ZZT's behavior than with Rust ownership.
Own(String), Own(String),
/// The stat is bound to another stat (index into the stats list). /// The stat is bound to the code at the given stat index.
///
/// Bind indexes are 0-based, with the asterisk that zero is not representable due to how ZZT
/// encodes bind indexes. The smallest possible value is 1, which represents being bound to the
/// stat that comes immediately after the player.
///
/// The largest representable bind index is 32768. zztff will successfully serialize this value,
/// though it's only useful as a curiosity and will likely crash ZZT: the index will definitely
/// be out of bounds because it is impossible to serialize a stat list that long without hitting
/// other limits in the file format. Indexes larger than that will result in an [`EncodeError`].
Bound(NonZero<u16>), Bound(NonZero<u16>),
} }
@@ -484,13 +509,13 @@ impl Board {
self.tiles[(y - 1) * 60 + (x - 1)] = tile; self.tiles[(y - 1) * 60 + (x - 1)] = tile;
} }
/// Parse a board from bytes (including the 2-byte size header). /// Parse a ZZT board from bytes.
pub fn from_bytes(bytes: &[u8]) -> Result<Board, DecodeError> { pub fn from_bytes(bytes: &[u8]) -> Result<Board, DecodeError> {
// Ignore length bytes // Ignore length bytes
let (input, _) = le_u16.parse(bytes)?; let (input, _) = le_u16.parse(bytes)?;
// Read board name // Read board title
let (input, name) = pstring(50)(input)?; let (input, title) = pstring(50)(input)?;
// Read terrain // Read terrain
const NUM_TILES: usize = 60 * 25; const NUM_TILES: usize = 60 * 25;
@@ -518,7 +543,7 @@ impl Board {
let exit_south = NonZero::new(exit_s); let exit_south = NonZero::new(exit_s);
let exit_west = NonZero::new(exit_w); let exit_west = NonZero::new(exit_w);
let exit_east = NonZero::new(exit_e); let exit_east = NonZero::new(exit_e);
let (input, (restart_on_zap, message)) = (bool_u8, pstring(58)).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, (enter_x, enter_y, time_limit)) = (le_u8, le_u8, le_i16).parse(input)?;
let (input, _) = take(16usize)(input)?; let (input, _) = take(16usize)(input)?;
@@ -531,7 +556,7 @@ impl Board {
let (_input, stats) = count(Stat::parse, num_stats as usize).parse(input)?; let (_input, stats) = count(Stat::parse, num_stats as usize).parse(input)?;
Ok(Board { Ok(Board {
name, title,
tiles, tiles,
max_shots, max_shots,
is_dark, is_dark,
@@ -539,7 +564,7 @@ impl Board {
exit_south, exit_south,
exit_east, exit_east,
exit_west, exit_west,
restart_on_zap, reenter_when_zapped,
message, message,
enter_x, enter_x,
enter_y, enter_y,
@@ -548,16 +573,16 @@ impl Board {
}) })
} }
/// Parse a standalone .brd file (same format, no world header).
pub fn from_brd_bytes(bytes: &[u8]) -> Result<Board, DecodeError> {
Board::from_bytes(bytes)
}
/// Serialize this board to bytes. /// Serialize this board to bytes.
///
/// Returns [`EncodeError::BoardTooLarge`] if the board is over 64 KiB when serialized. The file
/// format allows up to this size, but be aware that boards larger than 20 kB will cause memory
/// corruption in ZZT 3.2. Tools may want to warn the user if this function returns more than
/// 20,002 bytes (2 byte size header + 20 kB content).
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodeError> { pub fn to_bytes(&self) -> Result<Vec<u8>, EncodeError> {
let mut result = vec![]; let mut result = vec![];
result.push_padding(2); // reserve space for board size result.push_padding(2); // reserve space for board size
result.push_string(50, &self.name)?; result.push_string(50, &self.title)?;
// Encode terrain // Encode terrain
let mut iter = self.tiles.iter().peekable(); let mut iter = self.tiles.iter().peekable();
@@ -579,7 +604,7 @@ impl Board {
result.push(self.exit_south.map_or(0, |n| n.get())); result.push(self.exit_south.map_or(0, |n| n.get()));
result.push(self.exit_west.map_or(0, |n| n.get())); result.push(self.exit_west.map_or(0, |n| n.get()));
result.push(self.exit_east.map_or(0, |n| n.get())); result.push(self.exit_east.map_or(0, |n| n.get()));
result.push_bool(self.restart_on_zap); result.push_bool(self.reenter_when_zapped);
result.push_string(58, &self.message)?; result.push_string(58, &self.message)?;
result.push(self.enter_x); result.push(self.enter_x);
result.push(self.enter_y); result.push(self.enter_y);
@@ -605,7 +630,7 @@ impl Board {
impl Stat { impl Stat {
fn parse(input: &[u8]) -> IResult<&[u8], Self, DecodeError> { fn parse(input: &[u8]) -> IResult<&[u8], Self, DecodeError> {
let (input, (x, y, x_step, y_step)) = (le_u8, le_u8, le_i16, le_i16).parse(input)?; let (input, (x, y, step_x, step_y)) = (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, (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, (follower, leader)) = (le_i16, le_i16).parse(input)?;
let (input, (under_element, under_color)) = (le_u8, le_u8).parse(input)?; let (input, (under_element, under_color)) = (le_u8, le_u8).parse(input)?;
@@ -630,8 +655,8 @@ impl Stat {
Stat { Stat {
x, x,
y, y,
x_step, step_x,
y_step, step_y,
follower, follower,
leader, leader,
cycle, cycle,
@@ -652,8 +677,8 @@ impl Stat {
let mut result = vec![]; let mut result = vec![];
result.push(self.x); result.push(self.x);
result.push(self.y); result.push(self.y);
result.push_i16(self.x_step); result.push_i16(self.step_x);
result.push_i16(self.y_step); result.push_i16(self.step_y);
result.push_i16(self.cycle); result.push_i16(self.cycle);
result.push(self.p1); result.push(self.p1);
result.push(self.p2); result.push(self.p2);
@@ -743,22 +768,33 @@ mod tests {
use super::*; use super::*;
use insta::assert_debug_snapshot; use insta::assert_debug_snapshot;
const BYTES: &[u8] = include_bytes!("../tests/fixtures/all.zzt"); const WORLD: &[u8] = include_bytes!("../tests/fixtures/all.zzt");
const BOARD: &[u8] = include_bytes!("../tests/fixtures/title.brd");
#[test] #[test]
fn roundtrip() { fn roundtrip() {
let world = World::from_bytes(BYTES).unwrap(); let world = World::from_bytes(WORLD).unwrap();
let encoded = world.to_bytes().unwrap(); let encoded = world.to_bytes().unwrap();
assert_eq!(BYTES, &encoded[..]); assert_eq!(WORLD, &encoded[..]);
} }
#[test] #[test]
fn decode_snapshot() { fn decode_snapshot() {
let world = World::from_bytes(BYTES).unwrap(); let world = World::from_bytes(WORLD).unwrap();
assert_debug_snapshot!(world); assert_debug_snapshot!(world);
} }
/// Format board terrain as two hex grids (elements, then colors). #[test]
fn brd_roundtrip() {
// We don't bother with a snapshot test for the individual board case, because the world
// snapshot already exercises the contents of boards. This test is just to make sure it's
// *possible* to read/write standalone board files.
let board = Board::from_bytes(BOARD).unwrap();
let encoded = board.to_bytes().unwrap();
assert_eq!(BOARD, &encoded[..]);
}
/// Test helper: format board terrain as two hex grids (elements, then colors).
fn format_tiles(board: &Board) -> String { fn format_tiles(board: &Board) -> String {
let mut out = String::new(); let mut out = String::new();
for y in 1..=25 { for y in 1..=25 {
@@ -780,10 +816,10 @@ mod tests {
#[test] #[test]
fn tiles_snapshot() { fn tiles_snapshot() {
let world = World::from_bytes(BYTES).unwrap(); let world = World::from_bytes(WORLD).unwrap();
let mut all_tiles = String::new(); let mut all_tiles = String::new();
for (i, board) in world.boards.iter().enumerate() { for (i, board) in world.boards.iter().enumerate() {
all_tiles.push_str(&format!("// board {}: {}\n", i, board.name)); all_tiles.push_str(&format!("// board {}: {}\n", i, board.title));
all_tiles.push_str(&format_tiles(board)); all_tiles.push_str(&format_tiles(board));
} }
insta::assert_snapshot!(all_tiles); insta::assert_snapshot!(all_tiles);
BIN
View File
Binary file not shown.