From: Lucian Mogosanu Date: Mon, 29 Jun 2026 17:30:46 +0000 (+0300) Subject: Initial commit, AI-genned X-Git-Url: https://git.mogosanu.ro/?a=commitdiff_plain;h=6962808125647353d5c1a74f7d5272aa42830094;p=openpt.git Initial commit, AI-genned --- 6962808125647353d5c1a74f7d5272aa42830094 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31a24b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/artifacts/* +/PIZZA diff --git a/CHECKLIST.md b/CHECKLIST.md new file mode 100644 index 0000000..c57ac64 --- /dev/null +++ b/CHECKLIST.md @@ -0,0 +1,44 @@ +# openpt — reverse-engineering checklist + +Living map of work across sessions. Status: `[ ]` todo · `[~]` in progress · +`[x]` done · `[?]` open question / blocked. Newest detail in +[JOURNAL.md](JOURNAL.md). + +## Phase 0 — scaffolding +- [x] Directory layout (`spec/`, `tools/`, `artifacts/`) +- [x] Pure-stdlib tooling baseline (no third-party deps; PNM not PNG) +- [x] Journal + checklist for cross-session continuity +- [x] One-command "regenerate all artifacts" entry point (`python3 -m openpt.build`) + +## Phase 1 — asset/data formats (no disassembly needed) +- [x] **VGA image** container + shared RLE codec — `spec/formats/vga-image.md`, + `conventions.md`; verified 145/145 files +- [?] VGA **palette selection**: which of the 71 `PALETTE.E` palettes per image +- [?] VGA **transparency** index (colorkey) value + semantics +- [ ] `.PC` character graphics (`CHR/*.PC`) — different header (`00 68 ...`), runs + of `0xfc`; format unknown +- [ ] `.PC` structured data (`DAT/CITY*.PC`) — city records (contains "Paris") +- [ ] `.BOB` sprite collections (count + offset table; e.g. `BOB/GAESTE.BOB`) +- [ ] `.DAT` maps/districts (`DAT/BEZ*.DAT`, `DAT/MAP*.DAT`) — tile grids +- [ ] `.FNT` bitmap fonts (`FONTS/*.FNT`) +- [ ] `.MIS` mission definitions (`M/*.MIS`) +- [ ] `.PIZ` hiscores (`D/HISCORES.PIZ`) +- [ ] `T/*.E` text/dialog strings → structured extraction +- [ ] Confirm whether `.PC`/`.BOB` reuse the VGA RLE codec + +## Phase 2 — DOS environment spec (`spec/environment/`) +- [ ] LE/Watcom/DOS4GW executable & memory model +- [ ] VGA mode (resolution, palette, page flipping) +- [ ] sound stack, input + +## Phase 3 — game mechanics (needs disassembly; DEFERRED decision) +- [?] Disassembly approach: own LE loader vs. Capstone/Ghidra (decide when here) +- [ ] main loop & daily simulation tick +- [ ] economy, customers, recipes/popularity, staff/managers, mafia missions + +## Phase 4 — consolidation +- [ ] `glossary.md` (German↔English domain terms) +- [ ] top-level spec overview tying the layers together + +## Reimplementation (JS) — deferred until spec matures +- [ ] (intentionally not started; spec-first) diff --git a/JOURNAL.md b/JOURNAL.md new file mode 100644 index 0000000..403ade5 --- /dev/null +++ b/JOURNAL.md @@ -0,0 +1,50 @@ +# openpt — work journal + +Chronological log of findings and decisions. Append newest at the bottom. The +high-level status map is in [CHECKLIST.md](CHECKLIST.md). + +--- + +## 2026-06-29 — Session 1: project shape + VGA image format + +**Decisions (with the user):** +- Goal: reverse-engineer Pizza Tycoon into a **language-neutral, human-readable + spec first**; reimplementation (JS or otherwise) deferred. +- Spec = knowledge, not artifacts. Format docs are format-neutral prose using the + **trio**: layout table + pseudocode + test vector. Each claim tagged with + provenance + confidence. +- Tooling is first-class and documented: **pure Python stdlib, zero third-party + deps**. Output uses **PNM (PGM/PPM), not PNG**. Avoid DOSBox if we can (soft). +- Notation: prose now, formalize (e.g. Kaitai) later only if worth it. +- Disassembly approach for `PT.EXE` is the one big DEFERRED decision; not needed + for the asset/data phases. + +**Recon of `PIZZA/`:** +- `PT.EXE` (470K) = LE executable, Watcom C/C++32 + DOS/4GW (flat 32-bit). +- Engine loads external data by filename templates baked in `PT.EXE` + (`chr\c%de.pc`, `bob\typ%de.bob`, `dat\city%d.pc`, `%s.mis`, …; `e` = English). +- Custom formats inventoried: `.VGA`/`.PC` images, `.BOB` sprites, `.DAT` maps, + `.FNT` fonts, `.MIS` missions, `.PIZ` hiscores, `T/*.E` plain-ASCII strings. + +**VGA image format — DONE (high confidence):** +- Container: `u16 width, u16 height, u32 size`, then `size` RLE bytes + (`size == filesize - 8`). Decompresses to exactly `width*height` palette + indices, row-major. Verified header on all files. +- RLE codec (shared, see `conventions.md`): control byte `c`; top bit clear = + literal run of `(c&0x7f)+1` bytes; top bit set = repeat next byte + `(c&0x7f)+1` times. **Both counts biased by 1.** +- Established the bias values **empirically** via `openpt.verify`, probing all 4 + bias combos against all 145 `.VGA` files: only `lit+1, run+1` gives + 145/145 exact `width*height`. (Hand-analysis had guessed the repeat count + wrong — worth noting the empirical loop caught it.) +- Visually confirmed: `GFX/BACK1.VGA` (a pizza on a table), `Z/ZB1.VGA` (a + competition medallion), `GFX/BACK10.VGA` (restaurant interior). Geometry exact. +- Palette: `GFX/PALETTE.E` = 71 palettes × 256 RGB, **6-bit DAC** (0..63), + scale ×255/63 to 8-bit. + +**Open questions raised (see CHECKLIST):** which palette pairs with which image +(not positional); the transparency/colorkey index; whether `.PC`/`.BOB` reuse +this RLE codec. + +**Artifacts:** `artifacts/*.pgm` (index-as-gray) and `*.ppm` (color). Tools: +`tools/openpt/{rle,vga,palette,pnm,decode_vga,verify}.py`. diff --git a/prompt.md b/prompt.md new file mode 100644 index 0000000..3d7af97 --- /dev/null +++ b/prompt.md @@ -0,0 +1,29 @@ +# AI prompt: reverse Pizza Tycoon and reimplement in JS + +I want to do an experiment, the kind of which tends to head in different +directions at the same time. Here are some points of view which describe what I +want to do: + +- examine technological artifacts from the DOS era, and their technical + limitations +- understand how a DOS game was coded +- reverse engineer the spec of a game from its binary form +- use the spec to reimplement the game with a relative level of fidelity versus + the original +- use javascript as the target technology; discern how much of the fidelity is + owed to (partially) emulating aspects of the original DOS environment and how + much to JS +- explore, see where this leads + +although Pizza Tycoon (found in the `PIZZA` folder) is a relatively small game +(~7MB in size), it has a certain level of complexity that I expect would make +it unfeasible for this sort of task to be achieved in a single shot. so for +this prompt, I want you to scope this complexity by exploring the game, and +devise a plan for a. reverse engineering the game, which should result in a +human-readable spec that anyone could use to implement the game; and b. +reimplementing the game in JavaScript -- I'm choosing JavaScript only because +it's a popular technology nowadays, but in principle someone should be able to +reimplement the game in, say, Python, or a whole different technology in the +future, if they wished so. + +are you able to help me with that? diff --git a/spec/formats/conventions.md b/spec/formats/conventions.md new file mode 100644 index 0000000..76a9f74 --- /dev/null +++ b/spec/formats/conventions.md @@ -0,0 +1,87 @@ +# Format conventions + +Shared notation and primitives used by all `spec/formats/*` documents. + +## Reading this spec + +Each format is described as a **trio**: + +1. **Layout** — an offset/type table of the byte structure. +2. **Algorithm** — any non-trivial transform (e.g. decompression) as pseudocode. +3. **Test vector** — a real byte snippet with its expected decoded result, so an + implementation in any language can self-check. + +Every non-trivial claim carries **provenance** and **confidence**: + +- provenance: `observed` (decoded bytes / watched the game), `disassembled` + (read it in `PT.EXE`), `inferred` (deduced from README/data/context). +- confidence: `high` / `medium` / `low`. + +The prose here is normative and language-neutral. The Python under `tools/` is a +*reference implementation* of this prose — one way to realize it, kept runnable +so the spec stays verifiable — not a second source of truth. + +## Primitives + +- **Endianness:** little-endian throughout (Watcom C/C++32, x86). *(disassembled + toolchain + observed in every multi-byte field; confidence high.)* +- `u8` = unsigned byte; `u16` = 2-byte LE unsigned; `u32` = 4-byte LE unsigned. +- Pixel data is **row-major**, top row first, one `u8` palette index per pixel. +- Colors are stored as **6-bit VGA DAC** values (0..63); see + [vga-image.md](vga-image.md) and the palette discussion there. + +## The RLE codec *(provenance: observed; confidence: high)* + +A single byte-oriented run-length scheme is shared by the VGA image files (and, +pending confirmation, likely other image containers). It interleaves two kinds +of run, distinguished by the top bit of a **control byte** `c`: + +| `c & 0x80` | meaning | bytes consumed after `c` | output | +|-----------:|---------|--------------------------|--------| +| `0` (clear) | **literal** run | `(c & 0x7f) + 1` | those bytes, verbatim | +| `1` (set) | **repeat** run | `1` | that byte, repeated `(c & 0x7f) + 1` times | + +Both counts are stored **biased by 1** (encoded value = count − 1), so one +control byte expresses a run length of 1..128. + +### Algorithm (pseudocode) + +``` +decode(input) -> output: + i = 0 + while i < len(input): + c = input[i]; i += 1 + n = (c & 0x7f) + 1 + if c & 0x80: # repeat run + v = input[i]; i += 1 + output.append(v repeated n times) + else: # literal run + output.append(input[i : i+n]); i += n + return output +``` + +Consumers that know the expected pixel count (`width*height`) may stop once it +is reached; trailing bytes, if any, are padding. + +### Test vector + +Input (first payload bytes of `Z/ZB1.VGA`): + +``` +0e 40 41 41 42 43 43 44 45 45 46 46 47 47 48 48 82 4a 08 4b 4c 4c 4d 4d 4e 4e 4f 4f +``` + +Decodes to 27 bytes: + +``` +40 41 41 42 43 43 44 45 45 46 46 47 47 48 48 4a 4a 4a 4b 4c 4c 4d 4d 4e 4e 4f 4f +``` + +Walk-through: `0e`→literal of 15 (`40..48`); `82`→repeat `4a` ×3; `08`→literal +of 9 (`4b..4f`). + +### Verification + +`python3 -m openpt.verify PIZZA` decodes all **145** shipped `.VGA` files; with +the bias values above, every file decodes to exactly `width*height` pixels +(145/145 exact, 0 short, 0 over). No other bias combination matches any file. diff --git a/spec/formats/vga-image.md b/spec/formats/vga-image.md new file mode 100644 index 0000000..22ac8db --- /dev/null +++ b/spec/formats/vga-image.md @@ -0,0 +1,74 @@ +# VGA image container (`*.VGA`) + +RLE-compressed, 8-bit palettized still images. Used for backgrounds, panels and +scene art. Shipped under `GFX/*.VGA` (e.g. `BACK*.VGA`) and `Z/*.VGA` (e.g. +`ZB*.VGA`) — 145 files total. + +*(Note: the `.PC` extension is **not** this format; it is overloaded by the game +for both character graphics and structured data — see future `pc-*.md`. `.VGA` +is the format documented here.)* + +## Layout *(provenance: observed; confidence: high)* + +| offset | type | field | notes | +|-------:|------|-------|-------| +| 0 | u16 | `width` | pixels | +| 2 | u16 | `height` | pixels | +| 4 | u32 | `size` | length of the RLE payload; always `filesize - 8` | +| 8 | u8[`size`] | `payload` | RLE stream (see [conventions.md](conventions.md#the-rle-codec)) | + +The payload decompresses to exactly `width * height` bytes, one palette index +per pixel, row-major from the top-left. The `size` field is redundant with the +file size in every shipped file, but is the authoritative payload length. + +### Algorithm + +``` +parse(file) -> image: + width, height, size = read u16, u16, u32 # offsets 0,2,4 + payload = file[8 : 8 + size] + pixels = rle_decode(payload) # see conventions.md + assert len(pixels) == width * height + image = { width, height, pixels } # pixels are palette indices +``` + +### Test vector + +`Z/ZB1.VGA`: header `64 00 30 00 3b 12 00 00` → `width=100`, `height=48`, +`size=0x123b=4667` (file is 4675 bytes = 4667 + 8). Payload decodes to 4800 +bytes = 100×48. (First 27 decoded bytes given in +[conventions.md](conventions.md#test-vector).) + +## Palette *(provenance: observed + inferred; confidence: medium)* + +Pixels are indices into a 256-color VGA palette. The game ships a palette bank +at `GFX/PALETTE.E`: + +- 54528 bytes = **71 palettes** × 768 bytes; each palette is 256 RGB triples. +- Values are **6-bit** (0..63), i.e. VGA DAC range. Scale to 8-bit for display: + `v8 = round(v6 * 255 / 63)`. + +**Open question — which palette goes with which image.** Rendering `GFX/BACK1.VGA` +under palette 0 yields a recognizable but mis-tinted picture, so the +image→palette association is *not* implicit/positional. It is presumably chosen +by the game per screen/scene (candidate sources: a table in `PT.EXE`, or a +per-scene load). Tracked in [CHECKLIST](../../CHECKLIST.md). Until resolved, the +reference tools can emit a palette-independent grayscale PGM (gray = raw index) +to verify geometry, or a PPM under a caller-chosen palette index. + +## Open questions + +- **Transparency:** scene images show a uniform "background" index (rendered as + cyan/orange under palette 0) that is almost certainly a transparent/colorkey + index rather than a real color. Value and handling TBD. *(inferred; low.)* +- **Palette selection** per image (above). +- Whether other extensions (`.PC` character art, `BOB` frames) reuse this exact + RLE codec (they appear to share the scheme; headers differ). *(inferred; low.)* + +## Reference tooling + +- `tools/openpt/vga.py` — parser (this layout). +- `tools/openpt/rle.py` — the shared codec. +- `tools/openpt/palette.py` — `GFX/PALETTE.E` bank loader. +- `tools/openpt/decode_vga.py` — CLI: `.VGA` → PGM/PPM. +- `tools/openpt/verify.py` — conformance check over all shipped `.VGA` files. diff --git a/tools/openpt/__init__.py b/tools/openpt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/openpt/build.py b/tools/openpt/build.py new file mode 100644 index 0000000..82e7eaf --- /dev/null +++ b/tools/openpt/build.py @@ -0,0 +1,42 @@ +"""Regenerate all derived artifacts from the original PIZZA/ data. + +Single entry point so artifacts are reproducible and need not be committed. +Currently: decode every .VGA image to a palette-independent grayscale PGM +(gray = raw palette index) under artifacts/vga/. As more formats are reversed, +add their regeneration here. + + python3 -m openpt.build [PIZZA_DIR] [ARTIFACTS_DIR] +""" + +import glob +import os +import sys + +from . import pnm, vga + + +def build_vga(pizza, artifacts): + out_dir = os.path.join(artifacts, "vga") + os.makedirs(out_dir, exist_ok=True) + files = sorted(glob.glob(os.path.join(pizza, "GFX", "*.VGA")) + + glob.glob(os.path.join(pizza, "Z", "*.VGA"))) + ok = 0 + for f in files: + img = vga.VgaImage.parse(open(f, "rb").read()) + if len(img.pixels) != img.width * img.height: + print(" WARN size mismatch:", f) + continue + name = os.path.splitext(os.path.basename(f))[0] + pnm.write_pgm(os.path.join(out_dir, name + ".pgm"), img.width, img.height, img.pixels) + ok += 1 + print("vga: wrote %d/%d PGM files to %s" % (ok, len(files), out_dir)) + + +def main(argv): + pizza = argv[1] if len(argv) > 1 else "PIZZA" + artifacts = argv[2] if len(argv) > 2 else "artifacts" + build_vga(pizza, artifacts) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/tools/openpt/decode_vga.py b/tools/openpt/decode_vga.py new file mode 100644 index 0000000..1f8ffa5 --- /dev/null +++ b/tools/openpt/decode_vga.py @@ -0,0 +1,42 @@ +"""CLI: decode a .VGA image to PNM. + + python3 -m openpt.decode_vga [--pgm] [--palette F.E] [--pal N] + +Default output is PPM (color) using palette N from gfx/palette.e. With --pgm, +or when no palette is given, output a PGM where the gray value is the raw +palette index (useful for verifying geometry independently of any palette). +""" + +import sys + +from . import pnm, palette as palmod, vga + + +def main(argv): + args = argv[1:] + if len(args) < 2: + print(__doc__) + return 2 + src, out = args[0], args[1] + want_pgm = "--pgm" in args + pal_path = None + pal_idx = 0 + if "--palette" in args: + pal_path = args[args.index("--palette") + 1] + if "--pal" in args: + pal_idx = int(args[args.index("--pal") + 1]) + + img = vga.VgaImage.parse(open(src, "rb").read()) + print("%s: %dx%d, %d pixels decoded" % (src, img.width, img.height, len(img.pixels))) + + if want_pgm or not pal_path: + pnm.write_pgm(out, img.width, img.height, img.pixels) + else: + bank = palmod.load_bank(open(pal_path, "rb").read()) + rgb = pnm.indices_to_rgb(img.pixels, bank[pal_idx]) + pnm.write_ppm(out, img.width, img.height, rgb) + print("wrote", out) + + +if __name__ == "__main__": + sys.exit(main(sys.argv) or 0) diff --git a/tools/openpt/palette.py b/tools/openpt/palette.py new file mode 100644 index 0000000..f951802 --- /dev/null +++ b/tools/openpt/palette.py @@ -0,0 +1,27 @@ +"""Parser for gfx/palette.e -- Pizza Tycoon's VGA palette bank. + +The file is a flat array of 6-bit VGA DAC palettes (values 0..63), each palette +being 256 RGB triples (768 bytes). The shipped file holds 71 such palettes. + +6-bit DAC values are scaled to 8-bit for display: v8 = round(v6 * 255 / 63). +""" + +PALETTE_BYTES = 256 * 3 + + +def _scale6to8(v): + return (v * 255 + 31) // 63 + + +def load_bank(blob): + """Return a list of palettes; each palette is a list of 256 (r,g,b) 8-bit.""" + count = len(blob) // PALETTE_BYTES + banks = [] + for p in range(count): + base = p * PALETTE_BYTES + pal = [] + for c in range(256): + o = base + c * 3 + pal.append((_scale6to8(blob[o]), _scale6to8(blob[o + 1]), _scale6to8(blob[o + 2]))) + banks.append(pal) + return banks diff --git a/tools/openpt/pnm.py b/tools/openpt/pnm.py new file mode 100644 index 0000000..2b4650f --- /dev/null +++ b/tools/openpt/pnm.py @@ -0,0 +1,32 @@ +"""Minimal PNM (Netpbm) writers -- pure stdlib, no PNG, no third-party deps. + +PGM (P5) = 8-bit grayscale; PPM (P6) = 8-bit RGB. Both are trivially readable +binary formats that most image viewers and `file` understand. We deliberately +avoid PNG to keep the toolchain dependency-free and the output format simple. +""" + + +def write_pgm(path, width, height, gray_bytes): + assert len(gray_bytes) == width * height, (len(gray_bytes), width * height) + with open(path, "wb") as f: + f.write(b"P5\n%d %d\n255\n" % (width, height)) + f.write(bytes(gray_bytes)) + + +def write_ppm(path, width, height, rgb_bytes): + assert len(rgb_bytes) == width * height * 3 + with open(path, "wb") as f: + f.write(b"P6\n%d %d\n255\n" % (width, height)) + f.write(bytes(rgb_bytes)) + + +def indices_to_rgb(pixels, palette): + """Map palette indices -> flat RGB bytes using palette[i] = (r,g,b).""" + out = bytearray(len(pixels) * 3) + for k, idx in enumerate(pixels): + r, g, b = palette[idx] + o = k * 3 + out[o] = r + out[o + 1] = g + out[o + 2] = b + return bytes(out) diff --git a/tools/openpt/rle.py b/tools/openpt/rle.py new file mode 100644 index 0000000..3d07e7b --- /dev/null +++ b/tools/openpt/rle.py @@ -0,0 +1,46 @@ +"""RLE codec used by Pizza Tycoon's VGA image files (gfx/*.vga, z/*.vga). + +This is the *reference implementation* of the algorithm documented in +spec/formats/conventions.md. It is intentionally pure-stdlib and small so it +reads like the pseudocode in the spec. It is one possible implementation, not +"the" implementation -- the normative description lives in the spec. + +The scheme (verified empirically, see tools/openpt/verify.py): + + control byte c: + c & 0x80 == 0 -> LITERAL run: copy the next (c & 0x7f) + 1 bytes verbatim + c & 0x80 != 0 -> REPEAT run: output the next 1 byte (c & 0x7f) + 1 times + + i.e. both counts are stored biased by 1 (encoded value = count - 1), so a + single control byte expresses a run of 1..128. Verified to decode all 145 + shipped .VGA files to exactly width*height (see verify.py). +""" + + +def decode(data, *, lit_bias=1, run_bias=1, stop_at=None): + """Decode an RLE byte stream into raw bytes. + + lit_bias / run_bias exist only so verify.py can probe the exact count + semantics; the verified production values are lit_bias=1, run_bias=0. + stop_at, if given, stops once that many output bytes are produced (used to + decode files that carry trailing padding after the image). + """ + out = bytearray() + i = 0 + n = len(data) + while i < n: + if stop_at is not None and len(out) >= stop_at: + break + ctrl = data[i] + i += 1 + if ctrl & 0x80: + count = (ctrl & 0x7F) + run_bias + if i >= n: + break + out += bytes([data[i]]) * count + i += 1 + else: + count = (ctrl & 0x7F) + lit_bias + out += data[i:i + count] + i += count + return bytes(out) diff --git a/tools/openpt/verify.py b/tools/openpt/verify.py new file mode 100644 index 0000000..db4ce66 --- /dev/null +++ b/tools/openpt/verify.py @@ -0,0 +1,63 @@ +"""Empirically verify the VGA/RLE format against every shipped file. + +For each gfx/*.vga and z/*.vga: read header (w,h,size), confirm size == filesize-8, +decode the payload, and check the decoded length reaches width*height. We probe +the four plausible count-bias combinations so the *data* tells us the exact +semantics rather than us guessing. Run: python3 -m openpt.verify +""" + +import glob +import os +import struct +import sys + +from . import rle + +HEADER = struct.Struct(" 1 else "PIZZA" + files = vga_files(pizza) + print("found %d .VGA files" % len(files)) + + # 1) header self-consistency: declared size == filesize - 8 + bad_hdr = 0 + for f in files: + blob = open(f, "rb").read() + w, h, size = HEADER.unpack_from(blob, 0) + if size != len(blob) - 8: + bad_hdr += 1 + print(" HDR MISMATCH %s: size=%d filesize-8=%d" % (f, size, len(blob) - 8)) + print("header size field == filesize-8 for %d/%d files" % (len(files) - bad_hdr, len(files))) + + # 2) which (lit_bias, run_bias) decodes ALL files to exactly width*height? + print("\nprobing count-bias variants (decoded length vs width*height):") + for lit_bias in (0, 1): + for run_bias in (0, 1): + exact = short = over = 0 + for f in files: + blob = open(f, "rb").read() + w, h, size = HEADER.unpack_from(blob, 0) + want = w * h + got = len(rle.decode(blob[8:8 + size], lit_bias=lit_bias, run_bias=run_bias)) + if got == want: + exact += 1 + elif got < want: + short += 1 + else: + over += 1 + print(" lit_bias=%d run_bias=%d -> exact=%-3d short=%-3d over=%-3d" + % (lit_bias, run_bias, exact, short, over)) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/tools/openpt/vga.py b/tools/openpt/vga.py new file mode 100644 index 0000000..973433e --- /dev/null +++ b/tools/openpt/vga.py @@ -0,0 +1,30 @@ +"""Parser for Pizza Tycoon's VGA image container (gfx/*.vga, z/*.vga). + +Layout (little-endian), documented normatively in spec/formats/vga-image.md: + + offset 0 u16 width (pixels) + offset 2 u16 height (pixels) + offset 4 u32 size (bytes of RLE payload that follow; == filesize - 8) + offset 8 ... RLE-compressed 8-bit palette indices, row-major, + width*height pixels after decompression +""" + +import struct + +from . import rle + +HEADER = struct.Struct("