From 11339ef6ae0752ece1cebbf96ef99f8db24fa8ff Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Tue, 17 Mar 2026 17:22:26 -0700 Subject: [PATCH] Add docs on treatment of strings, World fields --- src/lib.rs | 20 ++++++++++++++++++ src/text.rs | 9 ++++++++ src/world.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 76913e0..8552ad7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,26 @@ //! 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 diff --git a/src/text.rs b/src/text.rs index ceef8c6..a76b99d 100644 --- a/src/text.rs +++ b/src/text.rs @@ -76,6 +76,15 @@ mod tests { assert_eq!(bytes, serialize_title(&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 = (0..=255).collect(); + assert_eq!(256, decode_oneline(&bytes).chars().count()); + assert_eq!(256, decode_multiline(&bytes).chars().count()); + } + #[test] fn newlines_to_cr() { assert_debug_snapshot!(serialize_code("ABC\nDEF"), @r###" diff --git a/src/world.rs b/src/world.rs index 3e0b243..ea93578 100644 --- a/src/world.rs +++ b/src/world.rs @@ -11,23 +11,79 @@ use super::elements::Element; use super::errors::{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)] pub struct World { + /// Value of the `ammo` counter. pub ammo: i16, + /// Value of the `gems` counter. pub gems: i16, + /// Which keys the player is carrying. pub keys: Keys, + /// Value of the `health` counter. 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, + /// Value of the `torches` counter. pub torches: i16, + /// Remaining cycles of torch light (0 = torch not active). pub torch_cycles: i16, + /// Remaining cycles of energizer effect (0 = not active). pub energizer_cycles: i16, + /// Value of the `score` counter. 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, + /// 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], + /// Value of the `time` counter. 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, + /// 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, + /// 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, }