# 03 — `.gmd` Binary Format Specification

`.gmd` (GameMaker Design) = canonical editable source format for GM 4.x and 5.x. Monolithic binary, **strictly sequential** serialization. Byte offset of each resource depends on size of all preceding ones — no random access without a parse pass.

5.3a `.gmd` is **transparent** relative to later `.gm6`/`.gmk` formats which add secondary structural encryption.

## Top-level layout (in order)

| # | Block | Purpose | Notes |
|---|---|---|---|
| 1 | **Magic Header** | Format version ID | Critical — byte alignment shifts between 4.3 / 5.0 / 5.3a |
| 2 | **Game Settings** | Globals, window res, color depth, loading screen, exec flags | Includes unencrypted ASCII title + author metadata |
| 3 | **Sounds** | Audio assets | Either relative file paths or fully embedded byte buffers (WAV/MIDI/MP3) |
| 4 | **Sprites** | Bounding boxes, origin pairs, collision masks | `.gmspr`-like internal format; ZLIB-compressed pixel data + sub-image count + collision matrix |
| 5 | **Backgrounds** | Tileable images + dimensions | ZLIB-compressed pixel arrays |
| 6 | **Paths** | Waypoint coords + speed multiplier nodes | Vector pathfinding logic |
| 7 | **Scripts** | GML source | Plaintext or lightly tokenized — recovery yields readable code |
| 8 | **Data Files** | Embedded arbitrary binary/text | New in 5.x — config files inside the `.gmd` blob |
| 9 | **Fonts** | Glyph metrics, render bounds | |
| 10 | **Timelines** | Step-indexed action sequences | Replaces alarm cascades from 4.3; integer-timer → script ref |
| 11 | **Objects** | Inheritance + Event arrays | Bind sprite ↔ behavioral logic; Events contain DnD nodes |
| 12 | **Rooms** | Instance placement coords | Includes background layer config, tile mappings, view/camera bounds |

## Parsing rules

- Strict sequential read — must fully parse block N to find block N+1's offset.
- ZLIB blocks present in Sprites + Backgrounds (and possibly Sounds for embedded audio).
- DnD action blocks live inside Object events — their own sub-format. See [04-dnd-serialization](04-dnd-serialization.md).
- Strings: usually null-terminated ASCII (game title, author, script names, GML source).
- Numbers: doubles stored as **8-byte IEEE 754** to preserve float precision.
- Booleans: 1 byte.
- IDs / counts: Int32 (little-endian, Delphi convention).

## Resource-ID conventions

Within DnD `Applies_To` and similar fields:

- `-1` = self
- `-2` = other
- `>= 0` = explicit internal Object ID

## Implementation reference

LateralGM's `LibReader.java` is the canonical open-source parser — maps the sequential byte stream into a navigable object model. Read it before writing your own. See [11-tool-lateralgm](11-tool-lateralgm.md).

## See also

- [04-dnd-serialization](04-dnd-serialization.md) — node-level format inside Object events
- [11-tool-lateralgm](11-tool-lateralgm.md) — reference parser
- [12-tool-gmksplitter](12-tool-gmksplitter.md) — explode monolithic blob to VCS tree
- [14-gb1-backups](14-gb1-backups.md) — `.gb1` is byte-identical to `.gmd`

---

## Errata (validated 2026-05-02 plan 01-07)

The following corrections were discovered while porting LateralGM 1.8.234's
`GmFileReader` to TypeScript and validated against the real
`BN Online Client 5-8.gmd` (4 MB, 854 sprites, 320 objects) and `Master 5-4.gmd`
(1.1 MB, 61 sprites, 58 objects). Each fix is preserved in the extractor source
and committed in plan 07. If you find this page contradicting the
`tools/extract-gmd/src/reader/*.ts` source, the SOURCE is canonical.

**Bug 1 — v530 header preamble is 24 bytes, not 8.**
Between the version int and the settings sub-block sver, v530 carries:
`[4 bytes reserved (0)] + [4 bytes gameId int32] + [16 bytes DPlay GUID]`.
Older docs that say "header = magic + version" are wrong for v530.

**Bug 2 — Booleans are 4-byte int32, not 1 byte.**
LateralGM `GmStreamDecoder.readBool()` calls `read4()`. The "Booleans: 1 byte"
line in earlier wiki revisions is errata. Confirmed against
`IsmAvatar/LateralGM/master/org/lateralgm/file/GmStreamDecoder.java` line 143-145.

**Bug 3 — Major blocks have a leading `block_sver = read4()` BEFORE the count.**
Sounds, sprites, backgrounds, paths, scripts, fonts, datafiles, timelines, objects, and
rooms ALL prepend a sub-block version int32 ahead of the per-block count. Older wiki
revisions described `[count][resources...]` for several blocks; the correct shape is
`[block_sver][count][resources...]`.

**Bug 4 — ZLIB-image payload inflates to a complete BMP file, NOT raw RGBA.**
LateralGM `GmStreamDecoder.readZlibImage` decompresses to a Windows DIB/BMP
file (magic `0x42 0x4D = "BM"`), then hands it to `ImageIO.read` for decode.
v530-era sprites and backgrounds carry full BMP bytes inside the deflate stream.
**Plan 07 Option A pivot:** Phase 1 carries these bytes opaquely (`SpriteFrame.imageBytes`,
`BackgroundImage.imageBytes`); BMP→PNG conversion is deferred to the asset-pipeline
(Phase 6/7 AST-01) per CLAUDE.md hard rule #6 (Extract → document → rewrite).

**Bug 5 — Sprite bbox component order is `L/R/B/T`, NOT `L/R/T/B`.**
LateralGM passes properties in order `BB_LEFT, BB_RIGHT, BB_BOTTOM, BB_TOP`
(line 632 of `GmFileReader.readSprites`). Older wiki revisions transposed B and T.

**Bug 6 — Background `useAsTileset` bool comes BEFORE the tile-geometry int32 group.**
Per LateralGM line 712-714 the order is `[smoothEdges][preload][useAsTileset]
[tileWidth..tileVSep]`. Older wiki revisions placed `useAsTileset` after the tile
geometry; that drift cascades into background image misframes.

**Bug 7 — Block 8 (datafiles) and Block 9 (fonts) share ONE dispatch byte.**
The orchestrator reads `int rver = in.read4()` BETWEEN scripts and what was
historically called the "fonts block":
- `rver == 440` → datafiles branch (v530-era "Includes")
- `rver == 540 || 800` → fonts branch
This is NOT two independent blocks with their own sver prefixes. Treating them
as two separate blocks (the previous extractor approach) double-consumes 4 bytes
and mis-frames every downstream block. Fix: `readFontsOrDataFiles(rver)` is a
single dispatcher.

**Bug 8 — Object event-type count: `noEvents = read4() + 1` (not hardcoded 12).**
LateralGM line 962. The on-wire value is `noEvents - 1`. Iterating a fixed 0..11
range mis-frames objects whose actual event-type count differs.

**Bug 9 — `drawBgColor` + `viewsClear` are bit-decoded from a single int32.**
Per LateralGM line 1031-1037: `int backgroundViewClear = read4();
drawBgColor = (bvc & 1) != 0; viewsClear = (bvc & 0b10) == 0`. Treating
drawBgColor as a 4-byte readBool steals 4 bytes from the next field.

**Bug 10 — Room snap component order is `Y, X` (not `X, Y`).**
LateralGM line 1027 passes `PRoom.WIDTH, PRoom.HEIGHT, PRoom.SNAP_Y, PRoom.SNAP_X`.

**Bug 11 — Room view `portW/portH` are only present when room sub-version > 520.**
For ver2==520 they default to viewW/viewH respectively (port_size == view_size).
For BNO (ver2==541), portW/portH ARE on the wire.

**Bug 12 — DnD per-action layout: `argCount` and `argKindCount` are SEPARATE wire fields.**
Per LateralGM line 1377-1382: `Argument[] args = new Argument[in.read4()];
byte[] argkinds = new byte[in.read4()]; for argkinds.length: argkinds[x] = read4();`.
Both lengths are read; `argKindCount` is typically 8 (MAX_ARGUMENTS), `argCount`
the actual allocation. When `actualNoArgs > argCount`, excess args are skipped
via `skip(read4-strLen)`. There is also a TRAILING `isNot` bool (4 bytes) per action
(LateralGM line 1467 `act.setNot(in.readBool())`), OUTSIDE the `actualnoargs`
inner loop but INSIDE the per-action loop.

**Bug 13 — ZLIB-image wire layout: `[int32 marker][int32 compressedLen][bytes ZLIB]`.**
The first int32 is a presence FLAG (10 == present, -1 == sentinel "no data").
`readZlibImage` THEN reads its own `[compressedLen][bytes]` pair. Treating the
first int32 as the compressed length (the obvious-but-wrong reading) directly
hands non-ZLIB bytes to `inflate` and yields `incorrect header check` errors.

(Bugs 1–13 are cumulative; together they make the difference between "tiny synthetic
fixtures pass" and "real BN Online .gmd parses end-to-end." All canonical references
point to `IsmAvatar/LateralGM v1.8.234`'s `org.lateralgm.file.GmFileReader` and
`GmStreamDecoder`.)
