## Contents

1. Key for data types
2. Character Set (.CHR)
3. Palette Formats

1. Palette (.PAL)
2. Palette Indices (.PALIDX)

4. World and Save Format (all versions)

1. World Format Header and Magic
2. Save Format Header and Magic

5. World and Save Format (1.00)

1. Global Data
2. Board Names, Sizes, and Offsets
3. Boards and Board RLE Planes
4. Board Parameters
5. Board Objects
6. Robots
7. Scrolls
8. Sensors

6. World and Save Format (2.00 through 2.84X)

1. Global Data

1. World Block 1
2. Save Block 1 (.SAV only)
3. World Block 2
4. Save Block 2 (.SAV only)
5. Counters List (.SAV only)
6. Strings List (2.80X through 2.84X) (.SAV only)
7. Sprites List (2.65 through 2.84X) (.SAV only)
8. Strings List (2.68 through 2.70) (.SAV only)
9. Math and File IO (2.68 through 2.84X) (.SAV only)
10. SMZX Data and Commands (2.69 through 2.84X) (.SAV only)
11. Vlayer (2.69c through 2.84X) (.SAV only)

2. Custom SFX Table
3. Board Names, Sizes, and Offsets
4. Boards
5. Board RLE Planes
6. Board Parameters (2.00 through 2.82X)
7. Board Parameters (2.83 and 2.84X)
8. Board Objects
9. Robots
10. Scrolls
11. Sensors

7. World and Save Format (2.90 onward)

1. Files List
2. Properties Format
3. World Properties

1. Status Counter Properties

4. Custom SFX Table
5. Character Sets
6. Palettes, Palette Indices, and Palette Intensities

1. Prior to 2.93

7. Vlayer Planes
8. Sprite Properties (.SAV only)
9. Counter and String Lists (.SAV only)
10. Board Properties
11. Board Planes
12. Robot Properties
13. Scroll Properties
14. Sensor Properties

8. Board File (.MZB)
9. Image File (.MZM)

1. MZM3 (current)
2. MZM2
3. MZMX

10. Counters File
11. Custom SFX Table File (.SFX)
12. Robotic Bytecode (.BC)
13. License

## Key for data types

The data types used to describe the MZX file formats are as follows:

| Data Type | Description | Min. | Max. | |-----------|-----------------------------------------|-------------|------| | b | byte (8-bit) | 0 | 255 | bs | byte (8-bit, signed) | -128 | 127 | w | word (16-bit, little endian) | 0 | 65535 | ws | word (16-bit, little endian, signed) | -32768 | 32767 | d | dword (32-bit, little endian) | 0 | 4294967295 | ds | dword (32-bit, little endian, signed) | -2147483648 | 2147483647 | s# | string of fixed length # (i.e. # bytes) | | s... | string of variable length |

## Character Set (.CHR)

This file format describes one or more MZX graphical characters. A CHR file contains raw graphical data only. Each character in the CHR file consists of 14 bytes representing each graphical row of the character.

Within the byte, each pixel of the row is encoded either as a single bit (MZX) or as two bits (SMZX). The first byte of the next character immediately follows the 14th byte of the previous character.

A typical character set file contains 256 chars (for a total size of 3584). However, partial character sets are possible, as are character sets with more than 256 chars. Complete and partial character sets can be loaded from files or from MZX strings with the `load char set` command. The bytes representing a character in a character set are also the same as the values provided to the `char edit` command or to the `CHAR_BYTE` counter.

### MZX

The most significant bit of the byte represents the leftmost pixel in the row and the least significant bit of the byte represents the rightmost pixel.

Example: setting the third, fifth, and seventh pixels.

| 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | |-----|-----|-----|-----|-----|-----|-----|-----| | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
| | | |---------|-----| | Binary | `00101010` | Hex | `2A` | Decimal | `(128 * 0)
+ (64 * 0)
+ (32 * 1)
+ (16 * 0)
+ (8 * 1)
+ (4 * 0)
+ (2 * 1)
+ (1 * 0) = 42`

### SMZX

The most significant two bits of the byte represent the leftmost pixel in the row and the least significant two bits represent the rightmost pixel. The four colors are encoded as the following bit pairs and quaternary digits:

| Color | Binary | Quaternary | |-------|--------|------------| | 1 | `00` | `0` | 2 | `01` | `1` | 3 | `10` | `2` | 4 | `11` | `3`

Example: setting the first and second pixels to color 2, third to color 4, and fourth to color 1.

| 64 | 16 | 4 | 1 | |-----|-----|-----|-----| | `1` | `1` | `3` | `0` |
| | | |------------|------| | Quaternary | `1130` | Binary | `01011100` | Hex | `5C` | Decimal | `(64 * 1)
+ (16 * 1)
+ (4 * 3)
+ (1 * 0) = 92`

## Palette Formats

### Palette (.PAL)

This file format describes an MZX palette. A PAL file contains raw palette data only. Each color in the PAL file consists of 3 bytes representing the red, green, and blue components of the color. Each RGB component of a color is a value from 0 to 63 (inclusive).

A typical MZX or SMZX mode 1 PAL file contains 16 colors (48 bytes) and a typical SMZX mode 2 or mode 3 pal file contains 256 colors (768 bytes).

The PAL file does not encode any SMZX 3 color index information (see PALIDX).

#### Examples

| Color | R | G | B | Hex in-file: | |---------------|-----|-----|-----|--------------| | Black | 0 | 0 | 0 | `00 00 00` | Dark Blue | 0 | 0 | 42 | `00 00 2A` | Dark Green | 0 | 42 | 0 | `00 2A 00` | Dark Cyan | 0 | 42 | 42 | `00 2A 2A` | Dark Red | 42 | 0 | 0 | `2A 00 00` | Dark Magenta | 42 | 0 | 42 | `2A 00 2A` | Brown | 42 | 21 | 0 | `2A 15 00` | Light Grey | 42 | 42 | 42 | `2A 2A 2A` | Dark Grey | 21 | 21 | 21 | `15 15 15` | Light Blue | 21 | 21 | 63 | `15 15 3F` | Light Green | 21 | 63 | 21 | `15 3F 15` | Light Cyan | 21 | 63 | 63 | `15 3F 3F` | Light Red | 63 | 21 | 21 | `3F 15 15` | Light Magenta | 63 | 21 | 63 | `3F 15 3F` | Yellow | 63 | 63 | 21 | `3F 3F 15` | White | 63 | 63 | 63 | `3F 3F 3F`

### Palette Indices (.PALIDX)

This file format describes SMZX mode 3 subpalette indices. A PALIDX file contains 256 sets of indices (one for each subpalette). Each set of indices is a sequence of four bytes (for a total of 1024 bytes per file); the first indicates color 1 of the subpalette, the second color 2, and so on.

## World and Save Format (all versions)

This section documents the portions of the world and save formats that are consistent between all MZX versions.

### World Format Header and Magic

Most world files begin with a 29-byte header. The only exceptions are locked worlds from MegaZeux 1.xx, encrypted worlds from MegaZeux 2.00 through 2.6, and rearchived MegaZeux 2.90 worlds (see the 2.90+ world format).

| Pos. | Size | Description | |------|------|-------------| | 0 | s25 | World title (1) (2) | 25 | b | Protection method (always zero) | 26 | s3 | Magic

#### MegaZeux 1.xx locked world header:

| Pos. | Size | Description | |------|------|-------------| | 0 | s25 | World title | 25 | b | Protection method (always between 1 and 3) | 26 | b | Length of password (0 to 15) | 27 | s15 | Password (encrypted) (4) | 42 | s3 | Magic (not encrypted)

#### MegaZeux 2.00 through 2.6 encrypted world header:

| Pos. | Size | Description | |------|------|-------------| | 0 | s25 | World title | 25 | b | Protection method (always between 1 and 3) | 26 | s15 | Password (encrypted) (4) | 41 | s3 | Magic (not encrypted)

The following magic values are valid:

| Magic | World/File format version | |-------------|---------------------------| | `MZX` | 1.00 | `MZ2` | 2.00 through 2.51 | `MZA` | 2.51s1 | `M\x02\x09` | 2.51s2 through 2.61 (including MZXAk, SMZX 1.00a) | `M\x02\x11` | Used for decrypted worlds by some port MZX versions.
Likely derived from misreading 2.51s2 magic `M\002\011` (octal) as hex. | `M\x02\x32` | 2.62 and 2.62b (from octal `M\002\062`) | `M\x02\x41` | 2.65 (from decimal `65`) | `M\x02\x44` | 2.68 (from decimal `68`) (3) | `M\x02\x45` | 2.69 (from decimal `69`) | `M\x02\x46` | 2.69b | `M\x02\x48` | 2.69c | `M\x02\x49` | 2.70 | `M\x02\x50` | 2.80X (from decimal `80`) (first port releases) | `M\x02\x51` | 2.81X (and so on...) | `M\x02\x52` | 2.82X | `M\x02\x53` | 2.83 | `M\x02\x54` | 2.84X | `M\x02\x5A` | 2.90X | `M\x02\x5B` | 2.91X | `M\x02\x5C` | 2.92X | `M\x02\x5D` | 2.93X

The internal MZX version value is derived from the world magic by 1) treating the last two bytes as a big-endian word if the magic is from 2.51s2 or later or 2) using `0x0100`, `0x0205`, and `0x0208` for 1.00, 2.00, and 2.51s1 worlds (respectively).

In all world files with a password, the password can be decrypted with the following algorithm:

`/** * This code is copied from the MZX source code, which is GPL 2+ licensed. */ char magic_code[16] = "\xE6\x52\xEB\xF2\x6D\x4D\x4A\xB7\x87\xB2\x92\x88\xDE\x91\x24"; for(i = 0; i < 15; i++) { password[i] ^= magic_code[i]; password[i] -= 0x12 - protection_method; password[i] ^= 0x8D; }`

In MegaZeux 2.00 through 2.6 encrypted worlds, the rest of the MZX file is XORed with a byte calculated from the password. The bundled MegaZeux utility "checkres" can output the unencrypted password and XOR value when given the -v option, if needed.

#### Notes:

1. The world title is a duplicate of the name of the title board.
2. The world title is null terminated within its fixed buffer.
3. The MZX 2.68 magic was erroneously never saved with world files, meaning worlds from MZX 2.68 relying on MZX 2.68 features won't work (as they are versioned for MZX 2.65). Resaving these worlds in a newer version (such as MZX 2.69) usually fixes this issue.
4. The password field is null terminated if it is 14 or fewer characters long. If the password is 15 characters long, it is NOT null terminated, and the null terminator must be added by the decryption algorithm.

### Save Format Header and Magic

Save files use a different header from world files, but the idea is generally the same.

| Pos. | Size | Description | |------|------|-------------| | 0 | s5 | Magic | 5 | w | World version (separate from file format version) | 7 | b | Current board number

#### Format prior to MegaZeux 2.82

| Pos. | Size | Description | |------|------|-------------| | 0 | s5 | Magic | 5 | b | Current board number
| Magic | File Format Version | |---------------|---------------------| | `MZSAV` | 1.00 | `MZSV2` | 2.00 through 2.51 | `MZXSA` | 2.51s1 | `MZS\x02\x09` | 2.51s2 through 2.61 (including MZXAk, SMZX 1.00a) | `MZS\x02\x32` | 2.62 and 2.62b | `MZS\x02\x41` | 2.65 | `MZS\x02\x44` | 2.68 | `MZS\x02\x45` | 2.69 | `MZS\x02\x46` | 2.69b | `MZS\x02\x48` | 2.69c | `MZS\x02\x49` | 2.70 | `MZS\x02\x50` | 2.80X (first port releases) | `MZS\x02\x51` | 2.81X | `MZS\x02\x52` | 2.82X | `MZS\x02\x53` | 2.83 | `MZS\x02\x54` | 2.84X | `MZS\x02\x5A` | 2.90X | `MZS\x02\x5B` | 2.91X | `MZS\x02\x5C` | 2.92X | `MZS\x02\x5D` | 2.93X

The internal MZX file format version is derived from the save magic the same way it is derived from the world magic, and is always the same as the MZX version that produced the save file.

The world version field (saved separately here) is always the same as the world version derived from the world magic when the world was initially loaded. Notably, this value is saved as a little endian word (whereas it is represented as a big endian word in the world magic string). In versions where this field is not present, the world version should be treated as the same as the file format version.

## World and Save Format (1.00)

MegaZeux 1.00 world (.MZX) and save (.SAV) files share very similar formats, so they are documented together here. This format is sufficiently different enough from the 2.00 format that it has been given its own section.

### Global Data

World data immediately follows the header.

#### World Block 1

Total block length is 3890 bytes.

| Pos. | Size | Description | |-------|----------|-------------| | 0 | b * 3584 | Character set (see Character Sets) | 3584 | b * 128 | ID Chars block 1 (thing chars) (1) | 3712 | b * 178 | ID Chars block 2 (animations and colors) (2)

##### Notes

1. Worlds in this format do not contain ID chars block 3 (damages), so the 1.x default damages should be substituted for these instead. Likewise, goop (ID 34) didn't exist in 1.x, so char 176 should should be patched in for its character.
2. This is the first 178 bytes of the 2.00 ID chars block 2. The missing values are bullet chars (306-317), player chars (318-321), player color (322), missile color (323), and bullet colors (324-326). Equivalents to these values are stored separately later in the file.

#### Save Block 1 (.SAV only)

This block is present in save files only. Total block length is 16.

| Pos. | Size | Description | |-------|-------|-------------| | 0 | b * 16| Keys

#### World Block 2

Total block length is 72.

| Pos. | Size | Description | |-------|----------|-------------| | 0 | s15 * 4 | Status counter names (null terminated) | 60 | b * 4 | Bullet chars | 64 | b | Player char | 65 | b | Saved player X position | 66 | b | Saved player Y position | 67 | b | Saved player board number | 68 | b | Viewport edge color | 69 | b | Player color | 70 | b | Bullet color | 71 | b | Missile color

#### Save Block 2 (.SAV only)

This block is present in save files only. Total block length is 9.

| Pos. | Size | Description | |-------|-------|-------------| | 0 | b | Scroll base color | 1 | b | Scroll corner color | 2 | b | Scroll pointer color | 3 | b | Scroll title color | 4 | b | Scroll arrow color | 5 | d | Score

#### World Block 3

Total block length is 18.

| Pos. | Size | Description | |-------|----------|-------------| | 0 | b | First board number | 1 | b | Endgame board number (>=128: no endgame board) | 2 | b | Death board number (>=129: same position, 128: restart board) | 3 | b | Endgame board teleport X | 4 | b | Endgame board teleport Y | 5 | b | Death board teleport X | 6 | b | Death board teleport Y | 7 | ws | Starting lives | 9 | ws | Lives limit | 11 | ws | Starting health | 13 | ws | Health limit | 15 | b | Enemies' bullets hurt other enemies | 16 | b | Game over SFX enabled (1) or disabled (0) | 17 | b | Unused

#### Counters List (.SAV only)

This block is only present in save files and has an variable length. The counters list begins with the number of counters.

| Pos. | Size | Description | |------|---------|-------------| | 0 | b | Number of counters = N

A list of `N` counters follows, each in the following format:

| Pos. | Size | Description | |------|------|-------------| | 0 | s15 | Counter name (null terminated) | 15 | w | Counter value (unsigned)

### Board Names, Sizes, and Offsets

Following the board count byte is the board names list and the board sizes and offsets table.

For each board:

| Pos. | Size | Description | |------|------|-------------| | 0 | s25 | Board names (null terminated)

For each board:

| Pos. | Size | Description | |------|------|-------------| | 0 | d | Size of board data within the file | 4 | d | Offset of board data within the file

Typically, board data immediately follows these tables.

### Boards and Board RLE Planes

MegaZeux 1.xx does not support overlays or board modes; these should be treated as disabled and 100x100, respectively. Boards begin immediately with the board RLE plane data. Board contents are compressed with run length encoding, which is different from its MegaZeux 2.xx counterpart. Each board always has exactly 6 planes in the following order:

| name | description | |-------------------|-------------| | level_id | Board thing IDs | level_color | Board thing colors | level_param | Board thing parameters | level_under_id | Board thing IDs for floors beneath non-floor things | level_under_color | Board thing colors for floors beneath non-floor things | level_under_param | Board thing parameters for floors beneath non-floor things

Each compressed plane starts with the following fields:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Width (1) | 1 | b | Height

The RLE data follows after, and consists entirely of pairs of run lengths and byte values. The RLE plane can be decompressed with the following algorithm:

`/** * This code fragment is a simplified version of the RLE unpacking code from * MegaZeux's source code, and is therefore GPL 2+ licensed. */ int size = width * height; int i = 0; while(i < size) { runsize = *(stream++); current_char = *(stream++); if(runsize > size - i) runsize = size - i; // MegaZeux emits an error if this occurs. for(int j = 0; j < runsize; j++) plane[i++] = current_char; }`

The RLE stream ends when `(board width * board height)` bytes have been expanded from the stream, and the next compressed plane or the board parameters block immediately follows.

##### Notes

1. There MAY be an extra run of length 0 padding between any two planes; if the width byte read is 0, skip the following byte and read the width and height from the next two bytes. (It's not clear why old versions of MegaZeux emit these runs, but this is exactly how VER1TO2 handles these. I have never seen this in practice. -Lachesis)

### Board Parameters

The board parameters block follows the board RLE planes.

| Pos. | Size | Description | |------|------|-------------| | 0 | s13 | Mod playing (null terminated) | 13 | b | Viewport X | 14 | b | Viewport Y | 15 | b | Viewport width | 16 | b | Viewport height | 17 | b | Can shoot? | 18 | b | Can bomb? | 19 | b | Fire burns brown? | 20 | b | Fire burns space? | 21 | b | Fire burns fakes? | 22 | b | Fire burns trees? | 23 | b | Explosions leave (0: space, 1: ash, 2: fire) | 24 | b | Save mode (0: enabled, 1: disabled, 2: sensor only) | 25 | b | Forest to floor? | 26 | b | Collect bombs? | 27 | b | Fire burns forever? | 28 | b | Board # to north | 29 | b | Board # to south | 30 | b | Board # to east | 31 | b | Board # to west | 32 | b | Restart if zapped? | 33 | w | Time limit | 35 | b | Last key pressed | 36 | ws | Input (numeric value) | 38 | b | Inputsize | 39 | b | Volume | 40 | b | Player locked NS? | 41 | b | Player locked EW? | 42 | b | Player attack locked? | 43 | s81 | Input (string value) (null terminated) | 124 | b | Blind duration (1) | 125 | b | Firewalker duration | 126 | b | Unused | 127 | b | Freeze time duration | 128 | b | Slow time duration | 129 | b | Unused | 130 | b | Unused | 131 | b | Unused | 132 | b | Wind duration | 133 | b | Last player direction | 134 | s81 | Bottom message (null terminated) | 215 | b | Bottom message timer | 216 | b | Lazerwall timer | 217 | b | Bottom message row | 218 | b | Bottom message column (-1: center) (2) | 219 | bs | Scroll relative X offset | 220 | bs | Scroll relative Y offset

##### Notes

1. All 5 status durations are board variables in MegaZeux 1.x. This can be observed most easily in the "Flame Cavern" board of Caverns.
2. The default behavior is centered, but 1.x world files contain 0 here by default. A value of 0 needs to be corrected to -1 when loading worlds.

### Board Objects

Following the board parameters are the robot, scroll, and sensor lists. Each list is stored as a single byte `N` indicating the number of robots, scrolls, or sensors on the board (0 to 255), followed by `N` stored objects of that type. Each object in its respective list immediately follows the prior, and the next list starts immediately following the last object in the previous list. The board data ends after all three lists and their objects have been read.

The board robots/scrolls/sensors in the list count from ID 1 upward, as ID 0 is the global robot and is invalid for scrolls and sensors. Since objects in the file are sequential, robot/scroll/sensor IDs may have to be reassigned when saving worlds or saves in these versions to optimize out gaps in the list.

### Robots

Each robot is 37 bytes plus the length of the robot program long.

| Pos. | Size | Default | Description | |------|------|---------|-------------| | 0 | d | | Program length in bytes. FILEFORM.TXT claims signed... | 4 | d | | unused (1) | 8 | s15 | | Robot name (null-terminated) | 23 | b | 2 | Robot char | 24 | d | 1 | Current position in program | 28 | b | 0 | Position within current line (for `WAIT`/`GO`/etc) | 29 | b | 1 | Robot cycle | 30 | b | 0 | Cycle counter | 31 | b | 1 | Bullet type (0: player, 1: neutral, 2: enemy) | 32 | b | 0 | Locked status (0: unlocked, 1: locked) | 33 | b | 0 | Can lavawalk (0: no, 1: yes) | 34 | b | 0 | Walk direction (0: idle) | 35 | b | 0 | Last touch direction | 36 | b | 0 | Last shot direction | 37 | s... | | Robot program

#### Notes

1. DOS MZX versions wrote robot, scroll, and sensor structs directly to world or save files. The unused 4-byte field at offset 4 probably corresponds to the program memory pointer in these versions, if MegaZeux 2.x is any indication. Scrolls likewise have an unused 4 byte field.

### Scrolls

Each scroll block is 10 bytes plus the length of the scroll text long.

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Number of lines | 2 | d | unused | 6 | d | Scroll text length in bytes | 10 | s... | Scroll text (1)

#### Notes

1. Scrolls in MegaZeux 1.x have a unique form of color coding that is not available in versions after. If the first character of a line is '!', the byte immediately following it will be used to apply the palette corresponding to that byte to the entire line. For example, if a line begins with `33 8e` hex, the line will use color 8 as the background color and color 14 as the foreground color. Unlike 2.x scrolls, the scroll text does not begin with `01`.

### Sensors

Each sensor block is 31 bytes long.

| Pos. | Size | Description | |------|------|-------------| | 0 | s15 | Sensor name (null-terminated) | 15 | b | Sensor char | 16 | s15 | Robot to message (null-terminated)

## World and Save Format (2.00 through 2.84X)

MegaZeux 2.00 world (.MZX) and save (.SAV) files share very similar formats, so they are documented together here. This section also tries to note changes in these formats and when they occurred, but save format modifications between versions are somewhat of a mess, so please report any inaccuracies in this document.

### Global Data

World data immediately follows the header. For ease of readability, it has been broken down into several blocks. Each block starts immediately after the previous block ends.

#### World Block 1

Total block length is 4129 bytes.

| Pos. | Size | Description | |-------|----------|-------------| | 0 | b * 3584 | Character set (see Character Sets) | 3584 | b * 128 | ID Chars block 1 (thing chars) | 3712 | b * 195 | ID Chars block 2 (animations and colors) | 3907 | b | ID Chars missile color (block 2 no. 323) | 3908 | b * 3 | ID Chars bullet colors (block 2 no. 324 through 326) | 3911 | b * 128 | ID Chars block 3 (damage) | 4039 | s15 * 6 | Status counter names (null terminated)

#### Save Block 1 (.SAV only)

This block is only present in save files. Total block length is 73 bytes plus the length of the current playing mod name.

| Pos. | Size | Description | |-------|-------|-------------| | 0 | b * 16| Keys | 16 | b | Blind duration | 17 | b | Firewalker duration | 18 | b | Freeze time duration | 19 | b | Slow time duration | 20 | b | Wind duration | 21 | w * 8 | Saved player X positions | 37 | w * 8 | Saved player Y positions | 53 | b * 8 | Saved player board numbers (3) | 61 | b | Player color | 62 | b * 3 | Under player ID/color/param (1) | 65 | b | Message edges enabled (1) or disabled (0) | 66 | b | Scroll base color | 67 | b | Scroll corner color | 68 | b | Scroll pointer color | 69 | b | Scroll title color | 70 | b | Scroll arrow color | 71 | w | Mod playing length (2) | 73 | s... | Mod playing (NOT null terminated) (2)

##### Notes

1. The under player fields are used as an extra "layer" beneath where the player is standing. This is so e.g. the player can be placed on top of something that is on top of a floor and the floor won't be erased when the thing immediately beneath the player is pushed to the under layer. These fields are effectively unused though, because there is a duplicate in the second save block that is always loaded over these.
2. Prior to MZX 2.83, the mod playing length and mod playing fields were a single 13 byte null terminated buffer (total block length 84 bytes). Prior to MZX 2.51s2, the current playing mod was not saved at all (as it was always the same as the current board mod) (total block length 71 bytes).
3. Due to a bug, MegaZeux 2.80 through 2.84c instead save the board numbers of the first two saved positions as endian-dependent dwords (4 bytes) and ignore the last six.

#### World Block 2

Total block length is 72.

| Pos. | Size | Description | |-------|------------|-------------| | 0 | b | Viewport edge color | 1 | b | First board number | 2 | b | Endgame board number (255: no endgame board) | 3 | b | Death board number (254: same position, 255: restart board) | 4 | w | Endgame board teleport X | 6 | w | Endgame board teleport Y | 8 | b | Game over SFX enabled (1) or disabled (0) | 9 | w | Death board teleport X | 11 | w | Death board teleport Y | 13 | ws | Starting lives | 15 | ws | Lives limit | 17 | ws | Starting health | 19 | ws | Health limit | 21 | b | Enemies' bullets hurt other enemies | 22 | b | Clear messages and projectiles on exit | 23 | b | Can only play world from a `SWAP WORLD` | 24 | b * 3 * 16 | Palette (see Palettes)

#### Save Block 2 (.SAV only)

This block is only present in save files has a length of 24 bytes.

| Pos. | Size | Description | |------|---------|-------------| | 0 | b * 16 | Palette intensities (1) | 16 | b | Faded state (1: faded out, 0: faded in) | 17 | w | Player restart X | 19 | w | Player restart Y | 21 | b * 3 | Under player ID/color/param (2)

##### Notes

1. Only the first 16 palette intensities were ever saved for SMZX modes 2 and 3.
2. Yes, this is duplicate info from the first save block, and no, I don't know why MZX saved this info twice. This copy is always loaded over the copy in save block 1.

#### Counters List (.SAV only)

WARNING: The format of the save blocks following this frequently changed from version to version. If there are any inaccuracies in the legacy world format documentation, they're probably below. MZX versions prior to 2.90 regularly dropped support for saves from older MZX versions due to the format changes in this part of the format being too messy to support. Difficulty in supporting changes to the world/save format are one of the main reasons the world format was replaced in MZX 2.90.

This block is only present in save files and has an variable length. The counters list begins with the number of counters.

| Pos. | Size | Description | |------|---------|-------------| | 0 | (1) | Number of counters = N

A list of `N` counters follows, each in the following format in MZX versions 2.00 through 2.80X:

| Pos. | Size | Description | |------|------|-------------| | 0 | s15 | Counter name (null terminated) | 15 | ws | Counter value (2.80X saves this as a `ds` instead)

In MZX versions 2.81X and up, the counters are instead stored in the following format:

| Pos. | Size | Description | |------|------|-------------| | 0 | ds | Counter value | 4 | d | Name length | 8 | s... | Counter name (NOT null terminated)

##### Notes

1. In MZX 2.51 and below, the maximum number of counters saved was 59, and the number of counters was saved as a byte. In DOS versions afterward, the maximum was 1020, and the number of counters was saved as a word (2 bytes). In MZX 2.80 to 2.84X, the maximum number of counters was lifted, and the number of counters was saved as a dword (4 bytes).
2. Save files created by MZX 2.84c will save two additional "counters" for `MZX_SPEED` and the `MZX_SPEED` locked status. These are named "mzx_speed" and "_____lock_speed". This vile hack was made unnecessary in MZX 2.90 by the new world format.

#### Strings List (2.80X through 2.84X) (.SAV only)

The strings block in versions 2.80X through 2.84X follows the counters block and is stored in a similar way.

| Pos. | Size | Description | |------|---------|-------------| | 0 | d | Number of strings = N

A list of `N` string definitions follows. In 2.80X the strings were saved in the following format:

| Pos. | Size | Description | |------|---------|-------------| | 0 | s15 | String name (null terminated) | 15 | s64 | String value (null terminated)

In versions 2.81b and onward (1), the following structure is used instead:

| Pos. | Size | Description | |-------|---------|-------------| | 0 | d | String name length = X | 4 | d | String value length = Y | 8 | b * X | String name (NOT null terminated) | 8 + X | b * Y | String value (NOT null terminated)

##### Notes

1. MegaZeux 2.81 tries to load strings in the format provided above but saves them in a different format unique to this release. Combined with no format validation at the time, the result was that 2.81 saves with strings always crashed on load.

#### Sprites List (2.65 through 2.84X) (.SAV only)

In versions that support sprites, the sprites list begins with `N` sprite definitions, where `N` = 64 for MZX 2.65 through 2.69b and `N` = 256 for MZX 2.69c and all releases afterward.

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Sprite X | 2 | w | Sprite Y | 4 | w | Sprite reference X | 6 | w | Sprite reference Y | 8 | b | Sprite color | 9 | b | Sprite flags (1) | 10 | b | Sprite width | 11 | b | Sprite height | 12 | bs | Sprite collision X | 13 | bs | Sprite collision Y | 14 | b | Sprite collision width | 15 | b | Sprite collision height

The sprite data is followed by the following global sprite variables:

| Pos. | Size | Description | |------|----------|-------------| | 0 | b | Total number of active sprites | 1 | b | Sprite Y order enabled (1) or disabled (0) | 2 | w | Current number of sprite collisions

The sprite collision list follows. In MZX 2.65 through 2.69b it is stored as follows:

| Pos. | Size | Description | |------|----------|-------------| | 4 | bs * N | Sprite collision list (2.65: N=32, 2.68 through 2.69b: N=64)

From MZX 2.69c onward, the sprite collision list is saved as words and is always 256 in length:

| Pos. | Size | Description | |------|----------|-------------| | 4 | ws * 256 | Sprite collision list

##### Notes

1. Sprite flags:
   | Flag | Description | |--------|-------------| | `0x01` | Sprite is initialized | `0x02` | Sprite ccheck 1 is enabled | `0x04` | Sprite is drawn over the overlay | `0x08` | Sprite uses reference colors | `0x10` | Sprite is static relative to the viewport | `0x20` | Sprite ccheck 2 is enabled | `0x40` | Sprite references the vlayer

#### Strings List (2.68 through 2.70) (.SAV only)

Strings in DOS MZX versions do not have names and are instead numbered from 1 to 16. They are saved as fixed length buffers after the sprites list (rather than before it as in port versions). The length of this block is always 16*64=1024 (these weren't saved in versions with 16 char strings).

| Pos. | Size | Description | |------|-------------|-------------| | 0 | b * 16 * 64 | Strings data (16 strings, fixed length null terminated values)

#### Math and File IO (2.68 through 2.84X) (.SAV only)

MZX versions 2.68 through 2.70 save the math and file IO vars as follows:

| Pos. | Size | Description | |------|------|-------------| | 0 | ws | Multiplier | 2 | ws | Divider | 4 | ws | Circle divisions | 6 | s12 | Input filename (null terminated) | 18 | d | Input file position | 22 | s12 | Output filename (null terminated) | 24 | d | Output file position

MZX versions 2.80X through 2.82X save the following:

| Pos. | Size | Description | |------|------|-------------| | 0 | w(1) | Multiplier | 2 | w(1) | Divider | 4 | w(1) | Circle divisions | 6 | b | Built-in message status (0: disabled, 1: enabled) | 7 | s12 | Input filename (null terminated) | 19 | d | Input file position | 21 | s12 | Output filename (null terminated) | 25 | d | Output file position

MZX 2.83 saves the following:

| Pos. | Size | Description | |------------|------|-------------| | 0 | w(1) | Multiplier | 2 | w(1) | Divider | 4 | w(1) | Circle divisions | 6 | b | Built-in message status (0: disabled, 1: enabled) | 7 | w | Input filename length = X | 9 | s... | Input filename (NOT null terminated) | 9 + X | d | Input file position | 13 + X | w | Output filename length = Y | 15 + X | s... | Output filename (NOT null terminated) | 15 + X + Y | d | Output file position

MZX 2.84X saves the following:

| Pos. | Size | Description | |------------|------|-------------| | 0 | w(1) | Multiplier | 2 | w(1) | Divider | 4 | w(1) | Circle divisions | 6 | ws | Input file delimiter | 8 | ws | Output file delimiter | 10 | b | Built-in shooting status (`SPACELOCK`) (0: disabled, 1: enabled) | 11 | b | Built-in message status (0: disabled, 1: enabled) | 12 | w | Input filename length = X | 14 | s... | Input filename (NOT null terminated) | 14 + X | d | Input file position | 18 + X | w | Output filename length = Y | 20 + X | s... | Output filename (NOT null terminated) | 20 + X + Y | d | Output file position

##### Notes

1. These variables were still saved as a word from 2.80X through 2.84X despite the internal variable being expanded to a signed dword, meaning these were truncated and then loaded as unsigned words. This wasn't corrected until MZX 2.90. OOPS!

#### SMZX Data and Commands (2.69 through 2.84X) (.SAV only)

MZX versions 2.69 and up save the SMZX mode here.

| Pos. | Size | Description | |------|------|-------------| | 0 | w | SMZX mode (0: disabled)

If SMZX mode is 2 or 3 (2.81+), the SMZX palette (but NOT intensities, which were unsaved prior to 2.90) follows:

| Pos. | Size | Description | |------|-------------|-------------| | 0 | b * 3 * 768 | SMZX palette (see Palettes)

The commands value follows. MZX 2.69 through MZX 2.83 saved it as a word (despite the internal value being expanded to a dword in 2.80):

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Commands

MZX 2.84X finally corrected this to a dword:

| Pos. | Size | Description | |------|------|-------------| | 0 | d | Commands

#### Vlayer (2.69c through 2.84X) (.SAV only)

The vlayer is saved as follows in 2.69c and 2.70:

| Pos. | Size | Description | |-------|-----------|-------------| | 0 | w | Vlayer width | 2 | w | Vlayer height | 4 | b * 32768 | Vlayer chars | 32772 | b * 32768 | Vlayer colors

MegaZeux 2.80X saves this instead:

| Pos. | Size | Description | |-------------|-----------|-------------| | 0 | w | Vlayer width = X | 2 | w | Vlayer height = Y | 4 | b * X * Y | Vlayer chars | 4 + (X * Y) | b * X * Y | Vlayer colors

This was changed to the following in 2.81X:

| Pos. | Size | Description | |-------|-------|-------------| | 0 | d | Vlayer size = X | 4 | w | Vlayer width | 6 | w | Vlayer height | 8 | b * X | Vlayer chars | 8 + X | b * X | Vlayer colors

#### Global Robot Offset and Board Count

In an unencrypted world file this will always be at offset 4230.

| Pos. | Size | Description | |------|------|-------------| | 0 | d | Offset of global robot within the file | 4 | b | Number of boards (1-250) or 0 for SFX table

### Custom SFX Table

If the board count byte is 0, a custom SFX table is present. If there is not a custom SFX table in the world file, the world will use the MZX default SFX instead. The SFX table begins with:

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Length of the SFX table in bytes

Then, for each SFX (50 total):

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Length of SFX string, including null terminator (69 max) | 1 | s... | SFX string

Finally:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Number of boards in the world (1-250)

### Board Names, Sizes, and Offsets

Following the board count byte is the board names list and the board sizes and offsets table.

For each board:

| Pos. | Size | Description | |------|------|-------------| | 0 | s25 | Board names (null terminated)

For each board:

| Pos. | Size | Description | |------|------|-------------| | 0 | d | Size of board data within the file | 4 | d | Offset of board data within the file

Typically, board data immediately follows these tables, and the global robot is placed at the very end of the world file.

### Boards

Each board begins with the following fields:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Board mode (1) | 1 | b | Varies (2)

#### Notes

1. Boards in DOS MZX had a fixed buffer size of `10000` for each board plane. DOS MZX used the board mode field to determine what the maximum dimensions of the board were within the available space. MegaZeux 2.80+ ignores this field and uses the plane dimensions to to determine the board size.
   | Board Mode | Width | Height | |------------|-------|--------| | `0` | 60 | 166 | | `1` | 80 | 125 | | `2` | 100 | 100 | | `3` | 200 | 50 | | `4` | 400 | 25 |
2. If the second byte of the board non-zero, it is the lower byte of the level_id plane's width field. If this byte is `00`, the board has an overlay and has the following extra field. As a consequence of this, saving a world with overlay disabled and a board width of 256 (which results in a lower byte of `00`) in DOS MZX versions will generate a corrupt world and crash MZX. Port versions avoid this by incrementing the board width to a non-multiple of 256 when setting the board size in the editor.

| Pos. | Size | Description | |------|------|-------------| | 2 | b | Overlay mode (1: enabled, 2: static, 3: transparent)

### Board RLE Planes

Board and overlay contents are compressed with run length encoding. Each board has either 6 or 8 planes (depending on whether the overlay is enabled), which immediately follow the mode fields and are stored in the following order:

| name | description | |-------------------|-------------| | overlay_char | Overlay char data (only if overlay is enabled) | overlay_color | Overlay color data (only if overlay is enabled) | level_id | Board thing IDs | level_color | Board thing colors | level_param | Board thing parameters | level_under_id | Board thing IDs for floors beneath non-floor things | level_under_color | Board thing colors for floors beneath non-floor things | level_under_param | Board thing parameters for floors beneath non-floor things

Each compressed plane starts with the following fields:

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Width | 2 | w | Height

The RLE data follows after. Values 0 through 127 represent a single literal value in the uncompressed data. If bit 7 is set, instead a run of length `(byte & 127)` should be filled with the next byte in the stream. The RLE plane can be decompressed with the following algorithm:

`/** * This code fragment is a simplified version of the RLE2 unpacking code from * MegaZeux's source code, and is therefore GPL 2+ licensed. */ int size = width * height; int i = 0; while(i < size) { current_char = *(stream++); if(!(current_char & 0x80)) { plane[i++] = current_char; } else { int runsize = current_char & 0x7F; current_char = *(stream++); if(runsize > size - i) runsize = size - i; // MegaZeux emits an error if this occurs. for(int j = 0; j < runsize; j++) plane[i++] = current_char; } }`

The RLE stream ends when `(board width * board height)` bytes have been expanded from the stream, and the next compressed plane or the board parameters block immediately follows.

### Board Parameters (2.00 through 2.82X)

The board parameters block follows the board RLE planes. The following is the board parameters format for MZX versions prior to 2.83.

| Pos. | Size | Description | |------|------|-------------| | 0 | s13 | Mod playing (null terminated) | 13 | b | Viewport X | 14 | b | Viewport Y | 15 | b | Viewport width | 16 | b | Viewport height | 17 | b | Can shoot? | 18 | b | Can bomb? | 19 | b | Fire burns brown? | 20 | b | Fire burns space? | 21 | b | Fire burns fakes? | 22 | b | Fire burns trees? | 23 | b | Explosions leave (0: space, 1: ash, 2: fire) | 24 | b | Save mode (0: enabled, 1: disabled, 2: sensor only) | 25 | b | Forest to floor? | 26 | b | Collect bombs? | 27 | b | Fire burns forever? | 28 | b | Board # to north | 29 | b | Board # to south | 30 | b | Board # to east | 31 | b | Board # to west | 32 | b | Restart if zapped? | 33 | w | Time limit | 35 | b | Last key pressed | 36 | ws | Input (numeric value) | 38 | b | Inputsize | 39 | s81 | Input (string value) (null terminated) | 120 | b | Last player direction | 121 | s81 | Bottom message (null terminated) | 202 | b | Bottom message timer | 203 | b | Lazerwall timer | 204 | b | Bottom message row | 205 | bs | Bottom message column (-1: center) | 206 | ws | Scroll relative X offset | 208 | ws | Scroll relative Y offset | 210 | ws | Scroll locked X (-1: no lock) | 212 | ws | Scroll locked Y (-1: no lock) | 214 | b | Player locked NS? | 215 | b | Player locked EW? | 216 | b | Player attack locked? | 217 | b | Volume | 218 | b | Volume increment | 219 | b | Volume target

### Board Parameters (2.83 and 2.84X)

The board parameters block follows the board RLE planes. The following is the board format for MZX versions 2.83 and 2.84X.

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Mod playing length | 2 | s... | Mod playing (NOT null terminated)

This block immediately follows the playing mod:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Viewport X | 1 | b | Viewport Y | 2 | b | Viewport width | 3 | b | Viewport height | 4 | b | Can shoot? | 5 | b | Can bomb? | 6 | b | Fire burns brown? | 7 | b | Fire burns space? | 8 | b | Fire burns fakes? | 9 | b | Fire burns trees? | 10 | b | Explosions leave (0: space, 1: ash, 2: fire) | 11 | b | Save mode (0: enabled, 1: disabled, 2: sensor only) | 12 | b | Forest to floor? | 13 | b | Collect bombs? | 14 | b | Fire burns forever? | 15 | b | Board # to north | 16 | b | Board # to south | 17 | b | Board # to east | 18 | b | Board # to west | 19 | b | Restart if zapped? | 20 | w | Time limit

The following block is only present in save files.

| Pos. | Size | Description | |---------|------|-------------| | 0 | b | Last key pressed | 1 | w | Input (numeric value) | 3 | w | Inputsize | 5 | w | Input string value length = x | 7 | s... | Input (string value) (NOT null terminated) | 7+x | b | Last player direction | 8+x | w | Bottom message length = y | 10+x | s... | Bottom message (NOT null terminated) | 10+x+y | b | Bottom message timer | 11+x+y | b | Lazerwall timer | 12+x+y | b | Bottom message row | 13+x+y | bs | Bottom message column (-1: center) | 14+x+y | ws | Scroll relative X offset | 16+x+y | ws | Scroll relative Y offset | 18+x+y | ws | Scroll locked X (-1: no lock) | 20+x+y | ws | Scroll locked Y (-1: no lock)

The following block is in both world files and save files.

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Player locked NS? | 1 | b | Player locked EW? | 2 | b | Player attack locked?

The final block is only present in save files.

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Volume | 1 | b | Volume increment | 2 | b | Volume target

#### Notes

1. Some MZX worlds created with an early alpha version of MZX 2.83 have boards parameters in the older format. Since these were saved with the MZX 2.83 world magic, they can't be loaded in newer versions of MZX, but they can be fixed by hex editing the magic from `M\x02\x53` to `M\x02\x52`.

### Board Objects

Following the board parameters are the robot, scroll, and sensor lists. Each list is stored as a single byte `N` indicating the number of robots, scrolls, or sensors on the board (0 to 255), followed by `N` stored objects of that type. Each object in its respective list immediately follows the prior, and the next list starts immediately following the last object in the previous list. The board data ends after all three lists and their objects have been read.

The board robots/scrolls/sensors in the list count from ID 1 upward, as ID 0 is the global robot and is invalid for scrolls and sensors. Since objects in the file are sequential, robot/scroll/sensor IDs may have to be reassigned when saving worlds or saves in these versions to optimize out gaps in the list.

### Robots

Robots are stored in world/save files in the following format. Several of these fields are only used during runtime and thus initial values used when saving worlds are also listed.

| Pos. | Size | Default | Description | |------|------|---------|-------------| | 0 | w | | Program length in bytes | 2 | w | | unused (1) | 4 | s15 | | Robot name (null-terminated) | 19 | b | 2 | Robot char | 20 | w | 1 | Current position in program | 22 | b | 0 | Position within current line (for `WAIT`/`GO`/etc) | 23 | b | 1 | Robot cycle | 24 | b | 0 | Cycle counter | 25 | b | 1 | Bullet type | 26 | b | 0 | Locked status (0: unlocked, 1: locked) (2) | 27 | b | 0 | Can lavawalk (0: no, 1: yes) | 28 | b | 0 | Walk direction (0: idle) (2) | 29 | b | 0 | Last touch direction (3) | 30 | b | 0 | Last shot direction (3) | 31 | ws | note 4 | X position | 33 | ws | note 4 | Y position | 35 | b | 0 | Status for the current board cycle (5) | 36 | ws | 0 | Local (6) | 38 | b | 1 | Used (1) or unused (0) | 39 | ws | 0 | Loopcount (7)

#### Notes

1. DOS MZX versions wrote robot, scroll, and sensor structs directly to world or save files. The unused 2-byte field at offset 2 corresponds to the program memory pointer in these versions.
2. In DOS MZX versions from 2.69b to 2.70, the `LOCAL2` counter was stored by using the robot walk direction as the upper byte and the robot locked status as the lower byte. Setting `LOCAL2` had the side effects you would expect from modifying those variables.
3. In DOS MZX versions from 2.69b to 2.70, the `LOCAL3` counter was stored by using the robot last touch direction as the upper byte and the robot last shot direction as the lower byte. Setting `LOCAL3` had the side effects you would expect from modifying those variables.
4. The X/Y position variables are generally initialized to 0 in world files from DOS versions. Versions 2.80+ typically save the actual X and Y position in these variables in world files. The global robot is saved with the coordinates (-1,-1).
5. Robot statuses:
   | Status | Description | |--------|-------------| | 0 | Normal | 1 | Did not execute this cycle or only executed `end`, `wait` | 2 | Same as 1 but has also been sent a label
   When a robot's status is equal to 2, it will execute the label it was sent on reverse board scan. Every robot's status is reset to 0 at the start of each board cycle.
6. The local field in this block is completely ignored in versions 2.80+ and is saved in the robot save block instead. In versions prior to 2.51s1, this field existed but was not used.
7. The loopcount field in this block is completely ignored in 2.84X save files and is saved in the robot save block instead.

In save files from MegaZeux 2.80 and onward, an extra data block follows:

| Pos. | Size | Description | |------|---------|-------------| | 41 | ds | Loopcount (8) | 45 | ds * 32 | Locals | 173 | d | Stack length = X | 177 | d | Current position in stack | 181 | d * X | Robot stack

#### Notes

1. The loopcount field in this block was not present prior to 2.84 (i.e. the block started with the local counters instead and was 4 bytes shorter).

In both world and save files, the robot program immediately follows the above block(s). The size of the program in file is the same as the program length field in the robot data.

In older worlds, robots marked unused may have uninitialized memory saved where their program would usually go (the global robot from Slave Pit is an example). In this situation, the Robotic program should not be validated and should instead be ignored or replaced with `FF 00`.

### Scrolls

Each scroll block is 7 bytes plus the length of the scroll text long.

| Pos. | Size | Description | |------|------|-------------| | 0 | w | Number of lines | 2 | w | unused (1) | 4 | w | Scroll text length in bytes | 6 | b | Used (1) or unused (0) | 7 | s... | Scroll text (2)

#### Notes

1. The unused 2-byte field corresponds to the scroll text memory pointer in DOS versions of MZX.
2. The scroll text must begin with a `01` hex byte, must contain at least one line break (`0A` hex), and must be null terminated (i.e. the smallest valid scroll is `01 0A 00` with number of lines=1 and length=3).

### Sensors

Each sensor block is 32 bytes long.

| Pos. | Size | Description | |------|------|-------------| | 0 | s15 | Sensor name (null-terminated) | 15 | b | Sensor char | 16 | s15 | Robot to message (null-terminated) | 31 | b | Used (1) or unused (0)

## World and Save Format (2.90 onward)

The legacy world format was replaced starting from MegaZeux 2.90 with a ZIP-based format. The old world format was replaced for the following reasons:

- A large number of problems with the save format were already documented, such as a lack of SMZX intensities in the save file, the math variables being saved with the wrong data type, an awkward counter list hack being used to save `MZX_SPEED`, and others, so the format needed to be changed anyway. There were also numerous MZX features queued for addition that required world format changes, including improved SMZX support and vlayer editing.
- The legacy format was hard to maintain and modify. The logic required to support even simple changes to the binary world format (such as the changes to the board parameters format in MZX 2.83) tended to get ugly fast.
  The ZIP-based format addresses this problem in two ways: first, it allows for cleanly partitioning the world format into palatable chunks, and second, it replaces the proprietary binary blobs of data with slightly-less proprietary extensible fields lists in the properties format. These changes make it much easier to, for a few examples, add new data structures or fields or change the size of a field or to move formerly save-only data to world files without breaking the world format. ZIP-based save files from older versions can also be supported with fewer headaches.
- The legacy format mostly lacked anything resembling internal consistency checks, infamously leading to broken games like Brotherhood: The Legacy of Drake outwardly appearing to be fine but causing MZX instability and crashes at certain points in the game. The ZIP format provides a much more strict file structure, internal redundancy in its headers, and CRC-32 checksums out of the box. While these aren't foolproof, they're better than nothing, and combined with the increased internal modularization of world files, means corruption at the very least may corrupt less world data.
- Creating a new format provided an opportunity to clean up some artifacts of DOS MZX's saving code and other confusing relics that had managed to survive from the days of MZX 2.51.
- The old format notably was lacking compression for certain things that could generate very large .SAV files, and adding compression to these things would have added additional loader compatibility checks. Furthermore, the board plane RLE compression was very bad at handling values >=128 and could easily be manipulated to produce planes larger in file than they were in memory. With the ZIP format, however, any compression is an aspect of the container and not the data itself, meaning a data structure can be compressed with little-to-no consequence for the parsing of the data structure. DEFLATE also tends to compress board planes much better than the RLE did.

#### World and Save Headers

ZIP-based worlds begin with the same 29-byte header that unencrypted legacy worlds do, and ZIP-based saves begin with the same 8-byte header that legacy save files do. This is possible as ZIP archives parse starting from the end of the file, so ZIP archives with prefixed data are still valid ZIP archives. The header data is stored redundantly in the world properties file so ZIP worlds can be extracted and rearchived by an external program and still work even without the header.

### Files List

World and save files split their data into multiple internal files for modularization and to allow for compression of specific parts of the world data. The files supported by the world format and the order they are loaded in are as follows:

| Filename | Description | Notes | Compressed | |-----------|-------------------------------------------------------------|-----------------------------|------------| | `world` | World Properties | | Never | `gr` | Global Robot | | `sfx` | Custom SFX Table | Only if custom SFX enabled | As of 2.91c | `chars` | Character Sets | | Always | `pal` | Palette | | `palsmzx` | Palette (SMZX) | SMZX 2 and 3 only | `palidx` | Palette Indices | Save-only before 2.91, SMZX 3 only | `vco` | Vlayer Colors Plane | Save-only before 2.91 | Always | `vch` | Vlayer Chars Plane | Save-only before 2.91 | Always | `palint` | Palette Intensities | Save-only | `palints` | Palette Intensities (SMZX) | Save-only, SMZX 2 and 3 only | `spr` | Sprite Properties | Save-only | Always | `counter` | Counters List | Save-only | As of 2.93 | `string` | Strings List | Save-only | As of 2.93 | `b##` | Board Properties | See below. | `b##bid` | Board level_id Plane | | Always | `b##bpr` | Board level_param Plane | | Always | `b##bco` | Board level_color Plane | | Always | `b##uid` | Board level_under_id Plane | | Always | `b##upr` | Board level_under_param Plane | | Always | `b##uco` | Board level_under_color Plane | | Always | `b##och` | Board overlay_char Plane | Only if overlay enabled | Always | `b##oco` | Board overlay_color Plane | Only if overlay enabled | Always | `b##r##` | Board Robot Properties | Only if robot exists | `b##sc##` | Board Scroll Properties | Only if scroll exists | `b##se##` | Board Sensor Properties | Only if sensor exists

The board files (prefixed with `b##`) corresponding to a single board are all loaded before loading any files corresponding to the next board. The first `##` in the names of these files is the board number in hex. Examples: `b00` for the title board, `b0A` for board 10, `bF9` for board 249 (the highest possible regular board number), and `bFF` for the special temporary board that may sometimes exist in save files. This number must be exactly two hex digits long.

Extra storage data for robots, scrolls, and sensors is stored in separate files. The file `bXXrYY` corresponds to the robot on board `XX` (hex) with the robot ID `YY` (also hex). Values from `01` (for robot 1) to `FF` (for robot 255) are valid. The same applies for scrolls and sensors. The robot/scroll/sensor must exist on the board for its corresponding properties file to be valid. These numbers must be exactly two hex digits long.

Filenames are case-insensitive. Prior to 2.93, they were case-sensitive and had to be all lowercase (aside from board/object numbers).

### Properties Format

World, board, sprite, and object data are all stored in a simple extensible format created for MZX files similar to RIFF or a (very) simplified EBML. All internal files labeled "properties" use this format.

These files consist of an unspecified number of blocks of "properties" in the format:

| Pos. | Size | Description | |------|-------|-------------| | 0 | w | Property ID | 2 | d | Property length = X | 6 | b * X | Property data

Property data contents vary between different properties, so in the property list specifications the expected contents are explicitly defined. Most properties expect an `int` value and accept either 1 byte (equivalent to `b`), 2 bytes (`w`), or 4 bytes (`ds`) (expected size and signedness are explicitly noted). The next most common stored type is a variable length `string` with no null terminator (equivalent to `s...`). `string`s that are stored with null terminators are marked `(with \0)`.

The file ends immediately when a property ID of `0x0000` is encountered indicating the end of the file. This value is the same for all different properties files. Unrecognized properties IDs are usually skipped unless noted otherwise. The EOF ID should always be present at the end of the file and there should be no data after the EOF ID.

It is possible to nest properties files to create a file with a more complex structure (though MZX currently does not do this).

### World Properties

Global data is stored in a properties file for both world and save files. This is a common pattern in the new world format; save data is typically implemented as either extra properties only present in save files or (occasionally) as save-exclusive raw data files.

Unlike every other properties file in the format, the world properties file is very strict: every property expected for a particular file version must exist and must be in the order listed below. Also unlike other properties files in the world format, unrecognized properties in this file will usually generate an error. An integrity check is performed to enforce this early in the load process to determine whether or not the provided file is actually a world/save.

Additionally, compressing this file should be avoided, as it may be useful in the future for MZX to be able to peek at the contents of this file prior to initializing ZIP archive data structures.

| ID | Property | Data Type | Notes | |----------|--------------------------------------|-------------------------|-------| | `0x0001` | World name | string | (1) | `0x0002` | World version | int(w) | | `0x0003` | File version | int(w) | | `0x0004` | Save start board # | int(b) | 255: temporary board | `0x0005` | Save has temporary board? | int(b) | | `0x0008` | Number of boards in world | int(b) | | `0x0010` | ID Chars blocks 1 and 2 | array(b * 323) | | `0x0011` | ID Chars missile color | int(b) | | `0x0012` | ID Chars bullet colors | array(b * 3) | | `0x0013` | ID Chars block 3 (damage) | array(b * 128) | | `0x0018` | Status counters | Properties (2.93+)
array(s15 with \0 * 6) | | `0x0020` | Edge color | int(b) | `0x0021` | First board # | int(b) | `0x0022` | Endgame board # | int(b) | 255: no endgame board | `0x0023` | Death board # | int(b) | 254: same position
255: restart board | `0x0024` | Endgame board teleport X | int(w) | `0x0025` | Endgame board teleport Y | int(w) | `0x0026` | Game over SFX enabled? | int(b) | `0x0027` | Death board teleport X | int(w) | `0x0028` | Death board teleport Y | int(w) | `0x0029` | Starting lives | int(w) | `0x002A` | Lives limit | int(w) | `0x002B` | Starting health | int(w) | `0x002C` | Health limit | int(w) | `0x002D` | Enemies' bullets hurt other enemies | int(b) | `0x002E` | Clear messages/projectiles on exit | int(b) | `0x002F` | Can only play world from `SWAP WORLD`| int(b) | `0x8030` | SMZX mode (0: disabled) | int(b) | Save-only before 2.91 | `0x8031` | Vlayer width | int(w) | Save-only before 2.91 | `0x8032` | Vlayer height | int(w) | Save-only before 2.91 | `0x8033` | Vlayer size | int(d) | Save-only before 2.91 | `0x8040` | Mod playing | string | Save-only | `0x8041` | MZX speed | int(b) | Save-only | `0x8042` | MZX speed is locked? | int(b) | Save-only | `0x8043` | Commands per cycle | int(ds) | Save-only | `0x8044` | Commands per cycle breakpoint limit | int(ds) | Save-only | `0x8048` | Saved positions | array((w + w + b) * 8) | Save-only | `0x8049` | Under player ID/param/color | array(b * 3) | Save-only | `0x804A` | Player restart X | int(w) | Save-only | `0x804B` | Player restart Y | int(w) | Save-only | `0x804C` | Player color | int(b) | Save-only | `0x804D` | Keys | array(b * 16) | Save-only | `0x8050` | Blind duration | int(d) | Save-only | `0x8051` | Firewalker duration | int(d) | Save-only | `0x8052` | Freeze time duration | int(d) | Save-only | `0x8053` | Slow time duration | int(d) | Save-only | `0x8054` | Wind duration | int(d) | Save-only | `0x8058` | Scroll base color | int(b) | Save-only | `0x8059` | Scroll corner color | int(b) | Save-only | `0x805A` | Scroll pointer color | int(b) | Save-only | `0x805B` | Scroll title color | int(b) | Save-only | `0x805C` | Scroll arrow color | int(b) | Save-only | `0x8060` | Message edges enabled? | int(b) | Save-only | `0x8061` | Built-in shooting enabled? | int(b) | Save-only | `0x8062` | Built-in messages enabled? | int(b) | Save-only | `0x8063` | Faded state (1: faded out) | int(b) | Save-only | `0x8070` | Input filename | string | Save-only | `0x8074` | Input file position | int(d) | Save-only | `0x8075` | Input file delimiter | int(ds) | Save-only | `0x8078` | Output filename | string | Save-only | `0x807C` | Output file position | int(d) | Save-only | `0x807D` | Output file delimiter | int(ds) | Save-only | `0x807E` | Output file mode | int(b) | Save-only, 2.93+ (2) | `0x8080` | Multiplier | int(ds) | Save-only | `0x8081` | Divider | int(ds) | Save-only | `0x8082` | Circle divisions | int(ds) | Save-only | `0x8090` | Maximum simultaneous sound effects | int(ds) | Save-only, 2.91+ | `0x8091` | SMZX message enabled in SMZX mode? | int(b) | Save-only, 2.91+ | `0x8092` | Joystick presses simulate keypresses?| int(b) | Save-only, 2.92+

1. World name maximum length is currently 24 characters; extra data will be ignored. Versions prior to 2.92d expect this field to be null terminated, and may leave parts of previous world names in the loaded name if it is not.
2. This field may be omitted. Valid output file mode values:
   | Value | Mode | |-------|--------------| | 0 | none/unknown | 1 | `w+b` | 2 | `r+b` | 3 | `a+b`
   Files opened in mode `w+b` should be reopened in mode `r+b`. The output file is currently write-only, so `wb` and `ab` are used in Robotic instead of `w+b` and `a+b` for now.

#### Status Counter Properties

As of 2.93, the status counters are a nested properties file. Status counters not present in the properties file are unused/blank.
| ID | Property | Data Type | Notes | |----------|--------------------------------------|-------------------------|-------| | `0x0001` | Set current status counter ID | int(b) | Counters >=6 are ignored | `0x0002` | Status counter name | string | Max. length 14
Prior to 2.93, the status counters were a fixed size array of 6 15-byte ASCIIZ strings (total size 90 bytes). In 2.93+ worlds, if the status counters are 90 bytes long and are not a valid properties file, they will be loaded as the old format.

### Custom SFX Table

If this file is present, custom SFX will be enabled for the world. If this file is absent, custom SFX will be disabled.

In versions 2.93 and up, the sound effects are stored as a properties file with a 8-byte header:

| Pos. | Size | Description | |------|-------|-------------| | 0 | s6 | `MZFX\x1a\0` | 6 | w(BE) | MegaZeux version in big endian, same as last two bytes of world magic.
Versions less than 2.93 (`025Dh`) are invalid. The properties follow immediately:
| ID | Property | Data Type | Notes | |----------|--------------------------------------|-------------------------|-------| | `0x0001` | Set current sound effect ID | int(b) | SFX >= 256 are ignored | `0x0002` | Sound effect string | string | Max. length 255 | `0x0003` | Sound effect name | string | Max. length 9
Prior to 2.93, the custom SFX table is saved as a raw array of `(NUM_BUILTIN_SFX * LEGACY_SFX_SIZE` bytes, where `NUM_BUILTIN_SFX = 50` and `LEGACY_SFX_SIZE = 69` (for a total of `3450` bytes prior to compression). Each individual sound effect must be null terminated. This format is still allowed in 2.93+ version world files. If loaded into a 2.93+ world, all custom sound effects after the first 50 and all names will be cleared.

This file is identical in format to the exported custom SFX file format, and files in 2.90+ world and save files can be used interchangeably with files created by SFX export.

### Character Sets

The world character sets are saved as a single raw charset file. In 2.90+ saves and 2.91+ worlds, all 15 user charsets will be saved for a total size of `14*15*256 = 53760` bytes prior to compression. Worlds from MZX 2.90 will only save the main charset (for a total size of `3840` bytes).

### Palettes, Palette Indices, and Palette Intensities

The world palettes are saved as raw palette files. In 2.93 and up, the file `pal` always contains the 16 color palette corresponding to MZX mode and SMZX mode 1. If SMZX modes 2 or 3 are active, an additional 256 color palette will be saved to `palsmzx`. If SMZX mode 3 is enabled, the palette indices will also be saved as a raw palette indices file in 2.90+ save files and 2.91+ world files.

In save files, the palette intensities are also saved. The file `palint` contains a raw array of 16 unsigned little endian dwords representing the MZX and SMZX mode 1 palette intensities. If SMZX modes 2 or 3 are active, an additional file `palints` containing the SMZX modes 2 and 3 will be saved. This file contains 256 unsigned little endian dwords.

#### Prior to 2.93

Only one `pal` and `palint` file were saved. These files always contained 256 entries, and always represented the current active screen mode. The palette intensities were stored as bytes, which corrupted larger intensity values.

In SMZX modes 2 and 3, the MZX palette and palette intensities were NOT saved. For worlds/saves from these versions with SMZX modes 2 or 3 active, the MZX palette should be derived from the first 16 entries of the SMZX palette (same as 2.84X and prior) and the MZX intensities should default to 100.

In 2.90X save files, the palette intensities file was stored using the internal indices order, and was incompatible with palette indices files. For each color, the two middle indices are in reverse order.

### Vlayer Planes

The vlayer chars and colors planes are saved as raw data in two separate files in saves (2.90 and up) and world files (2.91 and up). These files are expected to be `(vlayer_size)` bytes long, where `vlayer_size` is specified in the world properties. If one of these files is missing, an error will be displayed and the plane will be zero-initialized.

### Sprite Properties (.SAV only)

Sprite data is stored in a single properties file for all sprites with the following properties:

| ID | Property | Data Type | Notes | |----------|----------------------------|---------------|-------| | `0x0001` | Set current sprite ID | int(b) | Each sprite(1) | `0x0002` | Sprite X | int(ds) | Each sprite | `0x0003` | Sprite Y | int(ds) | Each sprite | `0x0004` | Sprite reference X | int(ds) | Each sprite | `0x0005` | Sprite reference Y | int(ds) | Each sprite | `0x0006` | Sprite color | int(b) | Each sprite | `0x0007` | Sprite flags | int(d) | Each sprite(2) | `0x0008` | Sprite width | int(d) | Each sprite | `0x0009` | Sprite height | int(d) | Each sprite | `0x000A` | Sprite collision X | int(ds) | Each sprite | `0x000B` | Sprite collision Y | int(ds) | Each sprite | `0x000C` | Sprite collision width | int(d) | Each sprite | `0x000D` | Sprite collision height | int(d) | Each sprite | `0x000E` | Sprite transparent color | int(ds) | Each sprite | `0x000F` | Sprite character offset | int(ds) | Each sprite | `0x0010` | Sprite Z | int(ds) | Each sprite, 2.92+ | `0x8000` | Number of active sprites | int(d) | Only once | `0x8001` | Sprite Y order enabled | int(ds) | Only once | `0x8002` | Sprite collision count = N | int(d) | Only once | `0x8003` | Sprite collision list | array(ds * N) | Only once | `0x8004` | `SPR_NUM` | int(ds) | Only once

1. This property sets the sprite ID that all following individual sprite properties will be loaded to. This must be present before the properties of a given sprite for the sprite to be correctly loaded. All 256 sprites and their properties will typically be saved regardless of whether they have been used or not, which is why this file is compressed.
2. Sprite flags:
   | Flag | Description | |--------|-------------| | `0x01` | Sprite is initialized | `0x02` | Sprite ccheck 1 is enabled | `0x04` | Sprite is drawn over the overlay | `0x08` | Sprite uses reference colors | `0x10` | Sprite is static relative to the viewport | `0x20` | Sprite ccheck 2 is enabled | `0x40` | Sprite references the vlayer | `0x80` | Sprite is unbound | `0x100`| Sprite off-on-exit is enabled
   For unbound sprites, if both the ccheck 1 and ccheck 2 flags are set, sprite ccheck 3 is enabled instead.

### Counter and String Lists (.SAV only)

The counter and string lists are packed in the same binary format they were in prior versions, each in their own file.

#### Counters List

The counter list file starts with the number of counters:

| Pos. | Size | Description | |------|---------|-------------| | 0 | d | Number of counters = N

A list of `N` counter definitions follows:

| Pos. | Size | Description | |------|-------|-------------| | 0 | ds | Counter value | 4 | d | Name length = X | 8 | b * X | Counter name (NOT null terminated)

#### Strings List

The string list file starts with the number of strings:

| Pos. | Size | Description | |------|---------|-------------| | 0 | d | Number of strings = N

A list of `N` string definitions follows:

| Pos. | Size | Description | |-------|---------|-------------| | 0 | d | String name length = X | 4 | d | String value length = Y | 8 | b * X | String name (NOT null terminated) | 8 + X | b * Y | String value (NOT null terminated)

### Board Properties

Board data is stored in a properties file with the following properties:

| ID | Property | Data Type | Notes | |----------|------------------------------------|-----------|-------| | `0x0001` | Board name | string | Strict(1)(2) | `0x0002` | Board width | int(w) | Strict | `0x0003` | Board height | int(w) | Strict | `0x0004` | Overlay mode | int(b) | Strict | `0x0005` | Robot count | int(b) | Strict | `0x0006` | Scroll count | int(b) | Strict | `0x0007` | Sensor count | int(b) | Strict | `0x0008` | File version | int(w) | Strict(3) | `0x0010` | Mod playing | string | `0x0011` | Viewport X | int(b) | `0x0012` | Viewport Y | int(b) | `0x0013` | Viewport width | int(b) | `0x0014` | Viewport height | int(b) | `0x0015` | Can shoot? | int(b) | `0x0016` | Can bomb? | int(b) | `0x0017` | Fire burns brown? | int(b) | `0x0018` | Fire burns space? | int(b) | `0x0019` | Fire burns fakes? | int(b) | `0x001A` | Fire burns trees? | int(b) | `0x001B` | Explosions leave | int(b) | `0x001C` | Save mode | int(b) | `0x001D` | Forest to floor? | int(b) | `0x001E` | Collect bombs? | int(b) | `0x001F` | Fire burns forever? | int(b) | `0x0020` | Board # to north | int(b) | `0x0021` | Board # to south | int(b) | `0x0022` | Board # to east | int(b) | `0x0023` | Board # to west | int(b) | `0x0024` | Restart if zapped? | int(b) | `0x0025` | Time limit | int(w) | `0x0026` | Player locked NS? | int(b) | `0x0027` | Player locked EW? | int(b) | `0x0028` | Player attack locked? | int(b) | `0x0029` | Reset board on entry? | int(b) | `0x002A` | Board charset path | string | `0x002B` | Board palette path | string | `0x002C` | Reset/load on entry if same board? | int(b) | Since 2.93 | `0x002D` | Dragons move randomly | int(b) | Since 2.93 | `0x0100` | Scroll relative X offset | int(ws) | Save-only | `0x0101` | Scroll relative Y offset | int(ws) | Save-only | `0x0102` | Scroll locked X | int(ws) | Save-only (-1: no lock) | `0x0103` | Scroll locked Y | int(ws) | Save-only (-1: no lock) | `0x0104` | Last player direction | int(b) | Save-only | `0x010A` | Lazerwall timer | int(b) | Save-only | `0x010B` | Last key pressed | int(b) | Save-only | `0x010C` | Input (numeric value) | int(ds) | Save-only | `0x010D` | Inputsize | int(ds) | Save-only | `0x010E` | Input (string value) | string | Save-only | `0x0110` | Bottom message | string | Save-only | `0x0111` | Bottom message timer | int(b) | Save-only | `0x0112` | Bottom message row | int(b) | Save-only | `0x0113` | Bottom message column | int(bs) | Save-only (-1: center) | `0x0114` | Volume | int(b) | Save-only | `0x0115` | Volume increment | int(b) | Save-only | `0x0116` | Volume target | int(b) | Save-only | `0x0117` | Blind duration | int(b) | Save-only, 1.x only | `0x0118` | Firewalker duration | int(b) | Save-only, 1.x only | `0x0119` | Freeze time duration | int(b) | Save-only, 1.x only | `0x011a` | Slow time duration | int(b) | Save-only, 1.x only | `0x011b` | Wind duration | int(b) | Save-only, 1.x only

1. Properties marked "strict" must be present and in the order specified in the table for the board data to be considered valid. If the board properties file is invalid, the entire board contents will be dummied out. Properties marked "save-only" are saved to save files only, but will be respected if loaded from a world file. Properties marked "1.x only" are saved/loaded only if the world compatibility version is 1.x.
2. Board name maximum length is currently 24 characters; extra data will be ignored. Versions prior to 2.93 expect this field to be null terminated and may append garbage to the name if it is not.
3. This field exists mainly to provide redundancy for the .MZB file header. In world and save files, this field should be redundant with the file version field derived from the world or save magic.

### Board Planes

Each board has 6 or 8 planes (depending on if overlay is enabled) saved in separate files as raw data of size `(board width * board height)` bytes each. If any of the expected planes are missing when loading a board, an error will display and the plane will be zero-initialized.

### Robot Properties

Robot data is stored in a properties file with the following properties:

| ID | Property | Data Type | Notes | |----------|------------------------------|-----------------------|-------| | `0x0001` | Robot name | string | Strict(1)(2) | `0x0002` | Robot char | int(b) | Strict | `0x0003` | X position | int(ws) | Strict | `0x0004` | Y position | int(ws) | Strict | `0x00FF` | Program bytecode | array(b * N) | Strict | `0x0100` | Current position in program | int(d) | Save-only | `0x0101` | Position within current line | int(d) | Save-only | `0x0102` | Robot cycle | int(b) | Save-only | `0x0103` | Cycle counter | int(b) | Save-only | `0x0104` | Bullet type | int(b) | Save-only | `0x0105` | Locked status | int(b) | Save-only | `0x0106` | Can lavawalk | int(b) | Save-only | `0x0107` | Walk direction | int(b) | Save-only | `0x0108` | Last touch direction | int(b) | Save-only | `0x0109` | Last shot direction | int(b) | Save-only | `0x010A` | Status | int(b) | Save-only | `0x010B` | Loopcount | int(ds) | Save-only | `0x010C` | Locals | array(ds * 32) | Save-only | `0x0110` | Current position in stack | int(d) | Save-only | `0x0111` | Robot stack | array(d * N) | Save-only | `0x0120` | Can goopwalk | int(b) | Save-only

1. Properties marked "strict" must be present and in the order specified in the table for the robot data to be considered valid. Properties marked "save-only" are saved to save files only, but will be respected if loaded from a world file.
2. Robot name maximum length is currently 14 characters; extra data will be ignored. Versions prior to 2.92d expect this field to be exactly 15 bytes long and to contain a null terminator, and may behave unexpectedly if it does not.

### Scroll Properties

Scroll data is stored in a properties file with the following properties:

| ID | Property | Data Type | |----------|-----------------|-----------| | `0x0001` | Number of lines | int(w) | `0x0002` | Scroll text | string (with \0)

### Sensor Properties

Sensor data is stored in a properties file with the following properties:

| ID | Property | Data Type | Notes | |----------|------------------|-----------|-------| | `0x0001` | Sensor name | string | (1) | `0x0002` | Sensor char | int(b) | `0x0003` | Robot to message | string | (1)

1. Robot/sensor name maximum length is currently 14 characters; extra data will be ignored. Versions prior to 2.92d expect these fields to be exactly 15 bytes long and to contain a null terminator, and may behave unexpectedly if they do not.

## Board File (.MZB)

A MegaZeux board file begins with a 4-byte header:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Always hex `FF` | 1 | s3 | Magic (`MB2` for 2.00 through 2.51s1, same as world magic after)

### Board Files for MegaZeux 1.x

A MegaZeux 1.x board file contains the raw data for a single board in the world format (including robots, scrolls, and sensors as-needed). These board files actually do NOT contain the above header; they begin exactly at the first RLE plane. See the world format documentation for boards for more info. A 25-byte null terminated board name follows the board data.

### Board Files for 2.00 through 2.84X

The header is immediately followed by the raw data for a single board in the world format (including robots, scrolls, and sensors as-needed). See the world format documentation for boards for more info. A 25-byte null terminated board name follows the board data.

### Board Files for 2.90 onward

The header is immediately followed by a ZIP archive containing the board properties, board planes, and object properties files for a single board. The following filenames are used:

| Filename | Description | |-----------|-------------| | `b00` | Board properties | `b00bid` | Board level_id plane | `b00bpr` | Board level_param plane | `b00bco` | Board level_color plane | `b00uid` | Board level_under_id plane | `b00upr` | Board level_under_param plane | `b00uco` | Board level_under_color plane | `b00och` | Board overlay_char plane (if overlay is enabled) | `b00oco` | Board overlay_color plane (if overlay is enabled) | `b00r##` | Robot properties (up to 255) | `b00sc##` | Scroll properties (up to 255) | `b00se##` | Sensor properties (up to 255)

## Image File (.MZM)

Raw blocks of board and overlay/vlayer data can be saved to and loaded from MZMs (or MZX image files). Three variants of the MZM format exist. The current MZM format is MZM3, which was introduced in MegaZeux 2.84.

### MZM3

An MZM3 file begins with the following 20-byte header:

| Pos. | Size | Description | Values | |------|------|-----------------------------------------------|--------| | 0 | s4 | Magic | `MZM3` | 4 | w | Width | 6 | w | Height | 8 | d | Location in file of robot data | 0 if not present | 12 | b | Number of robots in data | 0 if not present | 13 | b | Storage mode | 0: board, 1: layer | 14 | b | "Savegame" MZM? (includes runtime robot data) | 0 if false, 1 if true | 15 | w | World version | See below. | 17 | b | reserved | 18 | b | reserved | 19 | b | reserved

The world version is stored as a 2-byte little endian value where the upper byte is the major version number and the lower byte is the minor version number (like the world version field in the save format header). MZM3 files are forward compatible unless they contain robot information, in which case the robots in the MZM are replaced with customblocks.

The header is immediately followed by a board data block or a layer data block depending on the storage mode value indicated in the header.

#### Storage Mode 0 (Board)

The MZM data is stored in the following format if the "board" storage mode is selected. The data is composed of `(width * height)` blocks, where each block is 6 bytes and contains the following:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | ID | 1 | b | Param | 2 | b | Color | 3 | b | Under ID | 4 | b | Under param | 5 | b | Under color

Signs, scrolls, sensors, players, and IDs >=128 will be replaced with spaces when an MZM is loaded. Robots in a board MZM require extra storage information.

When a board MZM is created from the overlay or vlayer, the ID is typically set to 5, the param to the char, the color to the color, and all other values are set to 0.

#### Storage Mode 1 (Layer)

The MZM data is stored in the following format if the "layer" storage mode is selected. The data is composed of `(width * height)` blocks, where each block is 2 bytes and contains the following:

| Pos. | Size | Description | |------|------|-------------| | 0 | b | Char | 1 | b | Color

#### Robot Data

MZMs using the "board" storage mode may additionally include a robot data block if the MZM board data contains robots. This block will always be located after the board data, and the format of this block is based on the MZX world format corresponding to the world version indicated in the header.

MZMs created in MZX 2.84X will contain `N` robot blocks as described in the 2.00 through 2.84X world format documentation. If this is a savegame MZM, the robots will be in the save format instead of the world format.

MZMs created in MZX 2.90 and onward will contain a ZIP archive after the board data containing `N` robot properties files named in the format `r##`, where `##` is a hexadecimal number between 1 and 255 corresponding to a robot ID in the board data. If this is a savegame MZM, the robots will contain savegame properties.

### MZM2

MZM2 was introduced in MegaZeux 2.80 and is essentially the same as MZM3 without the version field in the header. The MZM2 header is 16 bytes long:

| Pos. | Size | Description | |------|------|-------------| | 0 | s4 | `MZM2` | 4 | w | Width | 6 | w | Height | 8 | d | Location in file of robot table (0 for no robots) | 12 | b | Number of robots (0 for no robots) | 13 | b | Storage mode (0: board, 1: layer) | 14 | b | "Savegame" MZM? (includes runtime robot data) (0 if false, 1 if true) | 15 | b | unused

The board data and robot data follows exactly as MZM3 with the exception that robots are always use the MZX 2.80 through 2.83 robot format rather than newer formats.

### MZMX

MZMX is the original MZM format that was introduced in MZX 2.68. It is much more limited than newer MZM formats and is generally only supported for compatibility. MZMX only supports blocks with dimensions of 255 or smaller, does not save robots, and only supports storage mode 0. The MZMX header is 16 bytes long:

| Pos. | Size | Description | |------|--------|-------------| | 0 | s4 | `MZMX` | 4 | b | Width | 5 | b | Height | 6 | b * 10 | unused

The board data follows as described by MZM3's Storage Mode 0.

## Counters File

The counters file format is a light wrapper around the MZX 2.90+ save format counter list and string list files. Counters files are created and loaded by the `SAVE_COUNTERS` and `LOAD_COUNTERS` pseudo-commands and currently have no standard filename extension. A counters file beings with this header:

| Pos. | Size | Description | |------|------|-------------| | 0 | s8 | Magic (`COUNTERS`)

This header is followed by a ZIP archive containing only the `counter` and `string` files.

## Custom SFX Table File (.SFX)

The custom SFX table file format is exactly the same as the custom SFX table described in the 2.90+ world format. MegaZeux 2.93 and up can save sound effects in either the new format or in the old raw array format. All prior versions of MegaZeux that support sound effects export (2.00 through 2.92f) use the raw array format.

## Robotic Bytecode (.BC)

This section describes the general structure of Robotic bytecode. The specifics are out of the scope of this document. More info can be found in the file `info_mzx.txt` distributed with MegaZeux source packages.

- Valid Robotic programs begin with the byte `FF`.
- A sequence of commands in the following format follows: `LL XX ... LL`, where `LL` is the length of the bytecode command (0-255) and `XX` is the command number. The length does not include the length bytes themselves, only the command and parameters between them.
- Between the command number and terminating length byte, a number of parameters follow in one of two formats: `00 XX XX`, where `XX XX` is a little-endian word, or `LL [string] 00`, where `LL` is the length of the null-terminated param string that follows (including the null terminator). The number of required params varies between commands.
- The next command's prefixed length byte immediately follows the previous command's trailing length byte.
- A final byte of `00` (indicating a command of length zero) terminates the program.
- The shortest valid Robotic program is `FF 00` with a length of 2.

## License

Copyright © 2020-2024 Lachesis — https://github.com/AliceLR/megazeux/

Permission to use, copy, modify, and distribute this document for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE DOCUMENT IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS DOCUMENT INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS DOCUMENT.