Compare commits
7 Commits
v0.1.0
...
76149f0a69
| Author | SHA1 | Date | |
|---|---|---|---|
| 76149f0a69 | |||
| 15bfe7712b | |||
| 11339ef6ae | |||
| a1b54b6172 | |||
| c649933afa | |||
| cb9c0fd68e | |||
| ba3321fb4f |
+12
-12
@@ -18,7 +18,7 @@ use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
#[repr(u8)]
|
||||
pub enum Element {
|
||||
Empty = 0,
|
||||
BoardEdge = 1,
|
||||
Edge = 1,
|
||||
Messenger = 2,
|
||||
Monitor = 3,
|
||||
Player = 4,
|
||||
@@ -42,15 +42,15 @@ pub enum Element {
|
||||
Normal = 22,
|
||||
Breakable = 23,
|
||||
Boulder = 24,
|
||||
SliderNs = 25,
|
||||
SliderEw = 26,
|
||||
SliderNS = 25,
|
||||
SliderEW = 26,
|
||||
Fake = 27,
|
||||
Invisible = 28,
|
||||
BlinkWall = 29,
|
||||
Transporter = 30,
|
||||
Line = 31,
|
||||
Ricochet = 32,
|
||||
BlinkRayH = 33,
|
||||
BlinkEW = 33,
|
||||
Bear = 34,
|
||||
Ruffian = 35,
|
||||
Object = 36,
|
||||
@@ -60,15 +60,15 @@ pub enum Element {
|
||||
Pusher = 40,
|
||||
Lion = 41,
|
||||
Tiger = 42,
|
||||
BlinkRayV = 43,
|
||||
BlinkNS = 43,
|
||||
Head = 44,
|
||||
Segment = 45,
|
||||
// 46 is unused
|
||||
TextBlue = 47,
|
||||
TextGreen = 48,
|
||||
TextCyan = 49,
|
||||
TextRed = 50,
|
||||
TextPurple = 51,
|
||||
TextBrown = 52,
|
||||
TextBlack = 53,
|
||||
BlueText = 47,
|
||||
GreenText = 48,
|
||||
CyanText = 49,
|
||||
RedText = 50,
|
||||
PurpleText = 51,
|
||||
BrownText = 52,
|
||||
WhiteText = 53,
|
||||
}
|
||||
|
||||
+85
-6
@@ -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 encoding;
|
||||
mod error;
|
||||
mod parse;
|
||||
mod errors;
|
||||
mod text;
|
||||
mod world;
|
||||
|
||||
pub use elements::Element;
|
||||
pub use encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||
pub use error::{DecodeError, EncodeError};
|
||||
pub use parse::{Board, Program, Stat, Tile, World};
|
||||
pub use errors::{DecodeError, EncodeError};
|
||||
pub use text::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||
pub use world::{Board, Keys, Program, Stat, Tile, World};
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/parse.rs
|
||||
source: src/world.rs
|
||||
assertion_line: 589
|
||||
expression: world
|
||||
---
|
||||
World {
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: src/parse.rs
|
||||
source: src/world.rs
|
||||
assertion_line: 620
|
||||
expression: all_tiles
|
||||
---
|
||||
// board 0: Title screen
|
||||
@@ -1,6 +1,6 @@
|
||||
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.
|
||||
/// Uses ZZT's convention of CR-terminated lines.
|
||||
@@ -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<u8> = (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###"
|
||||
+173
-4
@@ -8,26 +8,82 @@ use nom::{
|
||||
};
|
||||
|
||||
use super::elements::Element;
|
||||
use super::encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||
use super::error::{DecodeError, EncodeError};
|
||||
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<Board>,
|
||||
}
|
||||
|
||||
@@ -65,22 +121,63 @@ pub struct Keys {
|
||||
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)]
|
||||
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,
|
||||
/// 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],
|
||||
/// Maximum number of bullets the player can have on screen at once on this board.
|
||||
pub max_shots: u8,
|
||||
/// Whether the board is dark.
|
||||
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>>,
|
||||
/// Board index of the southern neighbor, or `None` for no exit.
|
||||
pub exit_south: Option<NonZero<u8>>,
|
||||
/// Board index of the western neighbor, or `None` for no exit.
|
||||
pub exit_west: Option<NonZero<u8>>,
|
||||
/// Board index of the eastern neighbor, or `None` for no exit.
|
||||
pub exit_east: Option<NonZero<u8>>,
|
||||
/// Whether the player restarts at the board entrance after taking damage.
|
||||
pub restart_on_zap: bool,
|
||||
/// The currently-flashing status bar message (max 58 characters).
|
||||
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,
|
||||
/// Y coordinate where the player entered the board (1-based).
|
||||
pub enter_y: u8,
|
||||
/// Time limit for the board in seconds (0 = no limit).
|
||||
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>,
|
||||
}
|
||||
|
||||
@@ -135,20 +232,92 @@ impl Debug for 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)]
|
||||
pub struct Stat {
|
||||
/// X coordinate of the stat's tile (1-based).
|
||||
pub x: u8,
|
||||
/// Y coordinate of the stat's tile (1-based).
|
||||
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,
|
||||
/// Vertical step delta.
|
||||
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,
|
||||
/// Element-specific parameter 1.
|
||||
pub p1: u8,
|
||||
/// Element-specific parameter 2.
|
||||
pub p2: u8,
|
||||
/// Element-specific parameter 3.
|
||||
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,
|
||||
/// Index of the leading stat in a centipede chain, or -1 if none.
|
||||
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,
|
||||
/// 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,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user