9 Commits

Author SHA1 Message Date
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
cmounce 76149f0a69 Add documentation for Stat struct 2026-03-18 17:20:53 -07:00
cmounce 15bfe7712b Document the Board struct a little more 2026-03-18 17:20:51 -07:00
cmounce 11339ef6ae Add docs on treatment of strings, World fields 2026-03-18 02:53:52 -07:00
cmounce a1b54b6172 Add crate-level documentation, examples 2026-03-16 02:57:07 -07:00
cmounce c649933afa Rename a few files for clarity 2026-03-15 16:53:38 -07:00
cmounce cb9c0fd68e Update variant names of Element enum
These had been based on the "ZZT file format" wiki article, but Weave
came up with some nicer unofficial names for the elements that ZZT 3.2
left unnamed. The variants are still named in CamelCase; this commit
just makes them a little closer to what they're called in practice.

Also in this commit: capitalize letters when they stand for directions
in names, like `SliderEW` (instead of `SliderEw`, which sounds like
expressing disgust over slider puzzles).
2026-03-15 16:27:44 -07:00
cmounce ba3321fb4f Re-export Keys struct 2026-03-15 14:40:55 -07:00
9 changed files with 338 additions and 54 deletions
Generated
+17 -17
View File
@@ -207,9 +207,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 +217,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 +229,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"
@@ -368,9 +368,9 @@ 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",
@@ -401,18 +401,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.0.0+spec-1.1.0" version = "1.0.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 = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
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.5+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 = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@@ -422,9 +422,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.9+spec-1.1.0" version = "1.0.10+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 = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
@@ -583,9 +583,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" 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 = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -686,7 +686,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]] [[package]]
name = "zztff" name = "zztff"
version = "0.1.0" version = "0.2.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.2.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"
+12 -12
View File
@@ -18,7 +18,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
#[repr(u8)] #[repr(u8)]
pub enum Element { pub enum Element {
Empty = 0, Empty = 0,
BoardEdge = 1, Edge = 1,
Messenger = 2, Messenger = 2,
Monitor = 3, Monitor = 3,
Player = 4, Player = 4,
@@ -42,15 +42,15 @@ pub enum Element {
Normal = 22, Normal = 22,
Breakable = 23, Breakable = 23,
Boulder = 24, Boulder = 24,
SliderNs = 25, SliderNS = 25,
SliderEw = 26, SliderEW = 26,
Fake = 27, Fake = 27,
Invisible = 28, Invisible = 28,
BlinkWall = 29, BlinkWall = 29,
Transporter = 30, Transporter = 30,
Line = 31, Line = 31,
Ricochet = 32, Ricochet = 32,
BlinkRayH = 33, BlinkEW = 33,
Bear = 34, Bear = 34,
Ruffian = 35, Ruffian = 35,
Object = 36, Object = 36,
@@ -60,15 +60,15 @@ pub enum Element {
Pusher = 40, Pusher = 40,
Lion = 41, Lion = 41,
Tiger = 42, Tiger = 42,
BlinkRayV = 43, BlinkNS = 43,
Head = 44, Head = 44,
Segment = 45, Segment = 45,
// 46 is unused // 46 is unused
TextBlue = 47, BlueText = 47,
TextGreen = 48, GreenText = 48,
TextCyan = 49, CyanText = 49,
TextRed = 50, RedText = 50,
TextPurple = 51, PurpleText = 51,
TextBrown = 52, BrownText = 52,
TextBlack = 53, WhiteText = 53,
} }
View File
+85 -6
View File
@@ -1,9 +1,88 @@
//! # Tools for reading/writing ZZT's file formats
//!
//! zztff is a library for working with [ZZT](https://en.wikipedia.org/wiki/ZZT) world and board files.
//! It is somewhat low-level; it allows control over every data field used by ZZT,
//! but creating new files requires some familiarity with the file format.
//!
//! zztff currently only supports the format used by ZZT 3.2, the last official release.
//! Super ZZT (the sequel) is not currently supported.
//!
//! ## String encoding
//!
//! zztff decodes all text data from extended ASCII into Unicode `String`s, because those are usually more convenient
//! for examining text and performing string manipuation. "Extended ASCII" is ambiguous as an encoding, so zztff assumes
//! [CP437](https://en.wikipedia.org/wiki/Code_page_437) for all conversions.
//! This is the de facto standard for most ZZT worlds and is what you want most of the time.
//!
//! If you have text that is not CP437, zztff will still read and write it just fine, and it is guaranteed to round-trip
//! back to disk unchanged. However, depending on the author's original encoding, foreign characters and dingbats
//! may display incorrectly.
//! (This is a difficult problem to solve in general, because ZZT formats do not specify their encodings, and if the
//! ZZT world was using a custom font, it can have characters without any Unicode equivalent.)
//!
//! Serialization will fail with [`EncodeError::EncodingError`] if you add a character to a String that doesn't have a
//! CP437 equivalent.
//! Serialization can also fail with [`EncodeError::StringTooLong`] if a String would exceed the maximum length of the
//! given text field in the file format.
//! (You can check the encoded length of a String by counting its codepoints, e.g., `s.chars().count()`;
//! zztff guarantees a 1:1 correspondence between codepoints in the String and bytes in the file format.)
//!
//! ## Examples
//!
//! ### Reading an existing .ZZT file
//!
//! ```rust,no_run
//! let bytes = std::fs::read("TOWN.ZZT")?;
//! let world = zztff::World::from_bytes(&bytes)?;
//! let first3: Vec<&str> = world.boards.iter().take(3).map(|b| b.name.as_str()).collect();
//! println!("Loaded world {:?}, {} boards.", world.name, world.boards.len());
//! println!("First 3 boards: {:?}", first3);
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! Assuming you have TOWN.ZZT, this loads the file and prints a brief summary:
//!
//! ```text
//! Loaded world "TOWN", 34 boards.
//! First 3 boards: ["Introduction Screen", "Room One", "Armory"]
//! ```
//!
//! ### Creating a new .ZZT file
//!
//! Creating a valid world file from scratch is more involved.
//! Worlds must contain at least one board, and we must populate that board with a player.
//! And to create a functioning player, we must create a corresponding stat to allow the player tile to move around.
//!
//! ```rust,no_run
//! use zztff::{World, Board, Tile, Stat, Element};
//!
//! let mut world = World::default();
//! world.name = "HELLO".into(); // should match stem of filename
//!
//! let mut board = Board::default();
//! board.set_tile(30, 12, Tile { // place a player tile in the center, at (30, 12)
//! element: Element::Player as u8,
//! color: 0x1f,
//! });
//! board.stats.push(Stat {
//! x: 30, // associate a stat with the player tile at (30, 12)
//! y: 12,
//! cycle: 1,
//! ..Stat::default()
//! });
//! world.boards.push(board);
//!
//! let bytes = world.to_bytes()?;
//! std::fs::write("HELLO.ZZT", &bytes)?;
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
mod elements; mod elements;
mod encoding; mod errors;
mod error; mod text;
mod parse; mod world;
pub use elements::Element; pub use elements::Element;
pub use encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline}; pub use errors::{DecodeError, EncodeError};
pub use error::{DecodeError, EncodeError}; pub use text::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
pub use parse::{Board, Program, Stat, Tile, World}; pub use world::{Board, Keys, Program, Stat, Tile, World};
@@ -1,5 +1,6 @@
--- ---
source: src/parse.rs source: src/world.rs
assertion_line: 589
expression: world expression: world
--- ---
World { World {
@@ -1,5 +1,6 @@
--- ---
source: src/parse.rs source: src/world.rs
assertion_line: 620
expression: all_tiles expression: all_tiles
--- ---
// board 0: Title screen // board 0: Title screen
+20 -7
View File
@@ -1,9 +1,10 @@
use codepage_437::CP437_WINGDINGS; use codepage_437::CP437_WINGDINGS;
use super::error::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,16 @@ 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]
fn one_codepoint_per_byte() {
// This check guarantees that length checks on Unicode Strings will tell you something useful
// about how much headroom your text will have once serialized into a field in the file format.
let bytes: Vec<u8> = (0..=255).collect();
assert_eq!(256, decode_oneline(&bytes).chars().count());
assert_eq!(256, decode_multiline(&bytes).chars().count());
} }
#[test] #[test]
+199 -9
View File
@@ -8,26 +8,82 @@ use nom::{
}; };
use super::elements::Element; use super::elements::Element;
use super::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline}; use super::errors::{DecodeError, EncodeError};
use super::error::{DecodeError, EncodeError}; use super::text::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
/// A ZZT world file. /// A data structure representing a ZZT world.
///
/// ZZT uses the same format for both worlds proper (.ZZT files) as well as their saved games (.SAV); saved games are
/// just snapshots of the world state after it's been played in. This `World` struct is for working with either type of
/// file.
///
/// Some of the fields are more esoteric than others due to this need of being able to represent the entire world state,
/// not just the starting conditions. If you just want to create a new world, it's safe to call `::default()` and leave
/// most of the fields alone. The only one that needs a non-default value is [`World::name`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct World { pub struct World {
/// Value of the `ammo` counter.
pub ammo: i16, pub ammo: i16,
/// Value of the `gems` counter.
pub gems: i16, pub gems: i16,
/// Which keys the player is carrying.
pub keys: Keys, pub keys: Keys,
/// Value of the `health` counter.
pub health: i16, pub health: i16,
/// Index of the board the player starts on.
///
/// If this is a saved game, this field holds the index of the board that the player is _currently_ on, which is
/// where they will start play upon restoring their save.
pub starting_board: i16, pub starting_board: i16,
/// Value of the `torches` counter.
pub torches: i16, pub torches: i16,
/// Remaining cycles of torch light (0 = torch not active).
pub torch_cycles: i16, pub torch_cycles: i16,
/// Remaining cycles of energizer effect (0 = not active).
pub energizer_cycles: i16, pub energizer_cycles: i16,
/// Value of the `score` counter.
pub score: i16, pub score: i16,
/// Stem of the world's filename, e.g., "TOWN" for a world named TOWN.ZZT (max 20 characters).
///
/// This field should typically match the filename of the .ZZT file you're writing to disk. If it differs, ZZT may
/// have trouble loading your file.
///
/// For saved games (.SAV files), this field should match the filename of the .ZZT file that was being played.
/// played. ZZT uses this field to associate the saved game with the world file it came from.
pub name: String, pub name: String,
/// Named flags set by ZZT-OOP `#set` commands (max 20 characters each).
///
/// Empty strings represent unused slots. The slot order is mostly invisible to ZZT-OOP, and ZZT will fill up the
/// slots from lowest to highest. However, order does matter once the array is full: trying to `#set` an eleventh
/// flag overwrites the tenth slot.
///
/// The file format supports arbitrary names, but ZZT-OOP only recognizes uppercase flags. Additionally, ZZT-OOP's
/// quirky parsing rules mean that many special characters are not recognized; see [this wiki
/// article](https://wiki.zzt.org/wiki/Set) for details.
pub flags: [String; 10], pub flags: [String; 10],
/// Value of the `time` counter.
pub time: i16, pub time: i16,
/// Sub-second state for the `time` countdown.
///
/// When on a board with a time limit, ZZT decrements the `time` counter approximately every second. This field
/// tracks partial seconds that have elapsed, so that ZZT can decide between decrementing `time` during this tick
/// versus waiting another tick.
pub time_ticks: i16, pub time_ticks: i16,
/// Whether this file is a saved game (.SAV) rather than a world file (.ZZT).
///
/// ZZT 3.2 uses this as an anti-cheat mechanism: the built-in editor refuses to open any files that have it set to
/// true. Otherwise, people could temporarily rename their save to have a .ZZT extension, then edit their way out of
/// a sticky situation.
///
/// (This was never very secure, and is even less of a deterrent nowadays. But it's still part of the file format.)
pub saved_game: bool, pub saved_game: bool,
/// The boards in this world.
///
/// A valid world must have at least 1 board (the title screen). Trying to serialize an empty world will result in
/// an encoding error.
///
/// ZZT 3.2 supports a maximum of 101 boards per world. The file format (and zztff) can handle larger worlds, but
/// trying to load them in ZZT may cause it to crash.
pub boards: Vec<Board>, pub boards: Vec<Board>,
} }
@@ -65,22 +121,63 @@ pub struct Keys {
pub white: bool, pub white: bool,
} }
/// A ZZT board. /// An individual ZZT board.
///
/// 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
/// [`Board::from_brd_bytes`] to parse one.
#[derive(Clone)] #[derive(Clone)]
pub struct Board { pub struct Board {
/// The board's title.
///
/// Board titles are not shown during gameplay; they exist for organizational purposes while
/// editing.
///
/// The file format supports a maximum length of 50 characters. But editors usually impose
/// 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
/// standard-width message box.
pub name: String, pub name: String,
/// 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
/// access, but in some cases if you are updating lots of tiles, it may be more efficient to
/// directly mutate this array.
pub tiles: [Tile; 1500], pub tiles: [Tile; 1500],
/// Maximum number of bullets the player can have on screen at once on this board.
pub max_shots: u8, pub max_shots: u8,
/// Whether the board is dark.
pub is_dark: bool, pub is_dark: bool,
/// Board index of the northern neighbor, or `None` for no exit.
///
/// Internally, ZZT uses zero to indicate "no exit". For this reason, it is impossible for a
/// board edge to link to the title screen, because board index 0 is unrepresentable.
pub exit_north: Option<NonZero<u8>>, pub exit_north: Option<NonZero<u8>>,
/// Board index of the southern neighbor, or `None` for no exit.
pub exit_south: Option<NonZero<u8>>, pub exit_south: Option<NonZero<u8>>,
/// Board index of the western neighbor, or `None` for no exit.
pub exit_west: Option<NonZero<u8>>, pub exit_west: Option<NonZero<u8>>,
/// 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.
pub restart_on_zap: bool, pub restart_on_zap: bool,
/// 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).
///
/// The entrance coordinates are part of the "re-enter when zapped" feature. The values only
/// really matter if the user starts out on this board; they will be overwritten with the
/// player's actual entry coordinates during play.
pub enter_x: u8, pub enter_x: u8,
/// Y coordinate where the player entered the board (1-based).
pub enter_y: u8, pub enter_y: u8,
/// Time limit for the board in seconds (0 = no limit).
pub time_limit: i16, pub time_limit: i16,
/// Status elements on this board (objects, creatures, the player, etc.).
///
/// ZZT 3.2 supports a maximum of 151 stats per board. The file format can handle more than
/// that, and zztff will happily read/write more than 151 stats, but ZZT may crash if this limit
/// is exceeded.
pub stats: Vec<Stat>, pub stats: Vec<Stat>,
} }
@@ -135,20 +232,92 @@ impl Debug for Board {
} }
/// A status element on a ZZT board. /// A status element on a ZZT board.
///
/// Stats provide additional data for specific tiles on the board. The majority of tiles will be
/// static, such as most of the board terrain (walls, water, empties, etc). But many things that
/// move, or any tile that needs state or code, will have an associated stat.
///
/// A stat's x/y coordinates are what link them to their associated tiles. Typically this is a 1:1
/// relationship: if there is an object tile at (12, 34), the board's stat list will usually have
/// exactly one entry that points to (12, 34). However, there are advanced use cases for having
/// multiple stats (or no stats!) in specific situations. So zztff does not enforce a 1:1
/// relationship---that is the responsibility of editing software that wants to be user friendly.
///
/// ZZT has a wide variety of tile types, called elements. The precise meanings of some of the
/// fields (particularly `p1`, `p2`, and `p3`) will vary depending on which tile the stat is pointed
/// at. You may want to consult a reference source, such as [the wiki articles on
/// elements](https://wiki.zzt.org/wiki/Element), for more info.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Stat { pub struct Stat {
/// X coordinate of the stat's tile (1-based).
pub x: u8, pub x: u8,
/// Y coordinate of the stat's tile (1-based).
pub y: u8, pub y: u8,
/// Horizontal step delta.
///
/// This is usually -1, 0, or 1. Paired with `y_step`, the step deltas are used to encode
/// direction or movement. For example, for transporters, this encodes which direction the
/// transporter is facing, whereas for pushers, it encodes the direction of movement.
///
/// Step values of (0, 0) or the four cardinal directions are the most common. Other step values
/// 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
/// steps larger than that may go off-board and cause memory corruption.
pub x_step: i16, pub x_step: i16,
/// Vertical step delta.
pub y_step: i16, pub y_step: i16,
/// 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
/// on every tick, a cycle 2 object runs every other tick, etc.
pub cycle: i16, pub cycle: i16,
/// Element-specific parameter 1.
pub p1: u8, pub p1: u8,
/// Element-specific parameter 2.
pub p2: u8, pub p2: u8,
/// Element-specific parameter 3.
pub p3: u8, pub p3: u8,
/// Index of the following stat in a centipede chain, or -1 if none.
///
/// Leader and follower do not need to be explicitly set if you are creating a world from
/// scratch; if there are no existing links, ZZT will automatically link adjacent segments into
/// a chain when the board first loads. These pointers are used mainly for preserving the state
/// of saved games---but a world author could still use them if they needed a specific centipede
/// layout for some reason.
pub follower: i16, pub follower: i16,
/// Index of the leading stat in a centipede chain, or -1 if none.
pub leader: i16, pub leader: i16,
/// The tile that is underneath this stat.
///
/// This is often an empty tile, but it may be something else if the stat was placed on top of
/// another terrain element (such as a fake wall/floor). ZZT uses this field to fill in the gap
/// if this stat ever moves; tracking the `under` type allows stats to move over terrain without
/// erasing it.
///
/// Note that this field only holds a tile, i.e., not a stat. In general, ZZT assumes that a
/// given coordinate can only be occupied by one stat at a time. For example, bullets may pass
/// over terrain, but they cannot pass by each other; they destroy each other on collision.
///
/// (Stats can be manually made to occupy the same x/y coordinates, but although this technique
/// is known as "stat stacking", strictly speaking ZZT has no way to represent one stat being
/// above or below another. The combination of stats in this way does not behave like two
/// overlapping items.)
pub under: Tile, pub under: Tile,
/// Current position in an object's ZZT-OOP program.
///
/// This is a byte offset in the file format, which corresponds to a character offset in the
/// `String` representation. 0 starts from the top of the program and is the typical default.
///
/// ZZT uses -1 to indicate that execution has halted; this value can be set from within ZZT
/// using the `#end` command, but it can also be set directly in the file format to keep an
/// object from running its code when the board is first loaded.
///
/// ZZT has no call stack, so this is the main piece of execution state.
pub instruction_pointer: i16, pub instruction_pointer: i16,
/// The stat's ZZT-OOP program, if any.
///
/// If a stat doesn't have a program, this is set to the empty string. This field is only ever
/// used by scrolls and objects, so it's pretty common for this to be empty.
pub program: Program, pub program: Program,
} }
@@ -182,15 +351,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>),
} }