ZZT Ultra File Formats
Through experimentation and reverse-engineering, dedicated individuals have
managed to crack virtually all of the original file format specifications used
for ZZT and Super ZZT world and board files. Because so much documentation exists
on these formats, these will not be discussed here.
The most comprehensive source for the older formats is this
Modding Wiki.
The new formats, as used by ZZT Ultra exclusively, are discussed here. The
overall goal of the new file formats is twofold: extensibility and compatibility.
GUI Definition
These files have extension ZZTGUI. A file is composed of a single
JSON dictionary containing all the GUI property
definitions. See the GUI Editor documentation
for more information about the properties.
When ZZT Ultra initializes, it loads the file guis/zzt_guis.txt relative
to the startup directory. This file contains the individual startup GUI specs
arranged in a parent JSON dictionary, with the dictionary key representing the
GUI name.
The Text property within this dictionary contains a string with text
characters of the GUI. Each row of the GUI is delimited with the text "\n".
There must be a "\n" before the first row and another "\n" after the last row.
Also note that because this is JSON, appropriate characters must be marked up
if required.
The Color property contains a JSON array with color attribute numbers
for each cell. The numbers alternate between color attributes and length counters
for the preceding attribute (a number between 1 and the maximum number of characters
for the GUI).
Both Text and Color must contain an appropriate length as
indicated by the properties GuiWidth (columns) and GuiHeight
(rows).
Object Type Definition
When ZZT Ultra initializes, it loads the file guis/zzt_objs.txt relative
to the startup directory. See
Type Definitions in ZZT Ultra for more information
about individual type definition specs.
ZZT Ultra does not establish much in the way of "hard-coded" object behaviors.
All of the original types and behaviors for ZZT and Super ZZT are recreated in
guis/zzt_objs.txt. It is relatively easy to fine-tune such behaviors by
tweaking the behavioral code.
The CODE object property defines an object's ZZT-OOP code. Generally speaking,
the object will ignore the first line if it is empty. Also note that JSON string
special characters (namely, double quotes) must be marked up if present.
Game World
The WAD file format is used to store game world data in ZZT Ultra.
This favors an overall better modular file format design, plus the possibility
of future editing and patching from third-party editors.
The two basic data structures of WAD are the following:
struct WAD_Header {
char id[4]; // "IWAD"
int numLumps; // Count of all lumps
int infoTableOfs; // Directory offset, byte
};
struct WAD_Directory_Entry {
int filePos; // Offset of lump, byte
int size; // Size of lump, bytes
char name[8]; // Name of lump
};
A WAD_Header always appears as the first 12 bytes of the file.
The directory offset points to the location within the file of the directory
entries. There are numLumps entries at this location, making the directory
(16 * numLumps) in length.
The following types of lumps are defined for a ZZT Ultra world. A lump
type is identified by the name in the directory entry. If the name is shorter
than 8 bytes, space characters are used to pad out the remainder of the name.
WORLDHDR
x1, required. This is a JSON-encoded dictionary containing the world properties.
GLOBALS
x1, required. This is a JSON-encoded dictionary of global variables.
TYPEMAP
x1, required. This is a 256-byte binary block, containing the type
look-up code for each original ZZT/SZT kind number. The nature of dynamic
compilation dictates that this will not necessarily be the same for each
world file, because the type look-up info is assigned linearly, while kind
numbers tend to jump. When loading a WAD, status elements of a specific type
will need to be translated to kind number using this table,
and possibly re-translated back to the new type number as compiled in
the new version.
PLAYBACK
x1, optional. This is a composited string of unprocessed sound channel
queues. If BGM was playing at the time of the save, the exact position
(minus latency considerations) should be restored.
EXTRATYP
x1, optional. This is a JSON-encoded dictionary containing extra (or
replaced) type definitions. When a world file is loaded, these types are
added to the preexisting type definitions, possibly replacing existing
types if there is name overlap.
EXTRAGUI
x1, optional. This is JSON-encoded dictionary containing GUI definitions
specific to the world. When a world file is loaded, these GUIs are added to
the preexisting GUI definitions, possibly replacing existing GUIs if there is
name overlap.
SOUNDFX
x1, optional. This is a JSON-encoded dictionary containing PLAY strings
for effects specific to the world. Each key is an effect name, and each value
is a string that would work with a PLAY statement. Note that it is
possible to replace the built-in effects in ZZT with customized effects defined
here if there is name overlap.
MASKS
x1, optional. This is a JSON-encoded dictionary containing masks specific
to the world. Each key is a mask name, and each value is an array of strings
containing "010101" mask info, with "0" indicating a "false" hit and
and "1" indicating a "true" hit. The number of rows in the mask is
implied from the length of the array, while the number of columns in
the mask is implied from the highest number of characters in any one
string.
CUSTCODE
x (number of objects with compiled code), optional. Composed of text
representing uncompiled object code. The lines are ZZT-OOP code lines.
These must be compiled again upon load. The order in which CUSTCODE
blocks appear is the same order as original compilation.
The first line of a CUSTCODE lump is the ZZT/SZT kind number indicating
the type associated with this code. ZZT Ultra needs this information when
reconstituting a composite rendition of the original type code and the
custom code. All subsequent lines of the CUSTCODE lump compose the actual
custom portion of the code.
BOARDHDR
x (number of boards), required. This is a JSON-encoded dictionary
containing the properties for a board. Boards are always stored in the
directory in the order they would have appeared in the original ZZT world
file, which is, zero (title screen) up to the last board.
BOARDRGN
x (number of boards), required. This is a JSON-encoded dictionary
containing the regions for a board. Boards are always stored in the
directory in the order they would have appeared in the original ZZT world
file, which is, zero (title screen) up to the last board.
STATELEM
x (number of boards), required. This is a JSON-encoded list of status
element objects for a board, in the same order they had appeared in the
statElem vector during run-time. These lumps are also stored in lexical
order like BOARDHDR. Note that default or otherwise unnecessary
values of some element attributes are not included as dictionary keys for
the status element, because these can be repopulated with defaults upon
load. The excluded defaults are UNDERID and UNDERCOLOR (if zero),
CYCLE (if matching the type default), and STEPX and STEPY (if matching
the type default).
BOARDRLE
x (number of boards), required. This is a binary block containing
three RLE-compressed data streams for the board. First is type stream,
followed by the color stream, followed by the lighting stream. These lumps
are also stored in lexical order like BOARDHDR.
Run-Length-Encoded Streams
BOARDRLE does not compress streams in the same way as the
original ZZT, because there were numerous inefficiencies in that format
that sometimes resulted in negative compression or otherwise poor compression.
The streams are instead compressed in a way that will consistently yield a
reduced size (unless the board is extremely irregular and "tooty-fruity" colored).
Type Stream
This stream is stored as the real-time look-up types. Because kind numbers
are not necessarily the same as types, one must use the TYPEMAP to translate
between types found in this stream and kind numbers.
The stream is composed of a series of one-two [L][B] combinations, with
L=Length byte and B=One or more bytes. If 0<L<128, [B] is one
byte, repeated. If -128<=L<0, [B] is a run of -L bytes.
This sequence proceeds until all grid space is exhausted. The minimum
positive value of L should be 4 in practice, because no compression advantage
is realized unless the repeated sequence is at least 4 spaces long.
The value of L=0 is special, indicating an end to the type stream (rest of
grid should be considered EMPTY).
Color Stream
This is actually two back-to-back substreams. The first substream
represents foreground colors, and the second substream represents
background. Both substreams are repeated until all grid space is exhausted.
The format for a substream is a series of bytes of [L.C], where L+1 is
the length the color is repeated (for foreground or background), and C is the
16-bit color. L is high bits (4-7) while C is low bits (0-3).
The system is designed to take advantage of the fact that color variation
is less frequent than type variation, and also the fact that background is
likely to remain the same even if foreground varies.
Lighting Stream
This was not stored in original ZZT format, but it is necessary for the
new format. "Lit" portions of the level are stored as lengths because
the lighting profile is boolean: either lit or unlit. The first byte,
[P], identifies if the stream exists. If 0, there is no lighting
stream. If 1, a lighting stream follows.
When a lighting stream follows, it alternates between "unlit range" and
"lit range" bytes: [UR][LR], allowing for the full range of the
unsigned byte for either lit or unlit ranges (0-255). The "lit range" code
of LR=0 is special, indicating an end to the lighting stream (rest of
grid should be considered unlit).
World File Patch
ZZT Ultra leaves open the possibilty of patching content on the basis of
dictionary-style storage for text-based properties, and the use of the PWAD
file format for overall binary-based content.
The way a PWAD typically works is that an equivalent lump in a
targeted IWAD file is replaced or otherwise modified wherever a lump by
the same name is found in the PWAD file.
For ZZT Ultra's world files, there are a few additional nuances to patching
that should be taken into consideration. These details are as follows.
Dictionary-based patching
Dictionary-based patching is a very simple concept. If a "patch" dictionary
must be applied to an existing dictionary, a key that is brand-new to the
dictionary is simply assigned to it, while an existing key is replaced by a key
of the same name. This applies to WORLDHDR, BOARDHDR, GLOBALS, EXTRATYP,
EXTRAGUI, SOUNDFX, MASKS, and STATELEM.
If for some reason it becomes necessary to remove an existing key
instead of replacing it with something, one can do so by having the "patch"
dictionary include this member:
"delete" : "ExistingKey"
The "delete" key in the above example would change the following
dictionary...
{
"oneKey" : 1,
"twoKey" : 2,
"ExistingKey" : 3
}
...into this...
{
"oneKey" : 1,
"twoKey" : 2
}
If a PWAD lists a lump containing a JSON dictionary, it should patch
the existing dictionary using this "add-replace-delete" system.
The STATELEM lump is organized as an array of structures, which makes
deletion of status elements follow a slightly amended model. The
"X" and "Y" keys of a status element identify a location. If the "TYPE" key is
set to "delete", an existing status element at (X, Y) should be removed.
Binary-based patching
For binary-based patching or text patching that does not make use of JSON
dictionaries, the ability to patch content is heavily dependent on the WAD lump
type itself. Most lumps are replaced in their entirety, but some need to
have a more sophisticated patching mechanism.
Of primary importance to the complex patching of binary lumps is BOARDRLE.
Patches, if they are necessary for a board, must break down the gridded
data in a highly piecemeal fashion.
ZZT Ultra supports the PATCH type, with number 255, to indicate that
the square should not be replaced in the patched board. This
"transparent" type compresses easily if there is only one small part of a board
that needs to be patched.
Color and lighting streams are the same format as for an IWAD. These
generally would also compress well for the PATCH type, because a uniform
zero byte can be set for PATCH, which ends up compressing just as well as
any other consistent color or lighting.
If a board within a world does not need to be patched at all, it can have a
lump length of zero, indicating that the board is acknowledged within the overall
order, but it should remain the same as before. The reason to do this is if only
the 5th board out of a 20-board world needs to be patched, the first four boards
can have empty lumps.
Legacy file patching
ZZT Ultra allows an interesting possibility when it comes to patching old ZZT
and SZT world files. Even though no "patch" file per se was supported by the
legacy formats, it is possible in ZZT Ultra to create such a PWAD patch
that will work for the world when loaded in ZZT Ultra.
As long as there is reasonable consistency in the board order, object
positions, etc. across the distributed version of the legacy world file, a
PWAD in ZZT Ultra can perform targeted fixes upon load.
|