-
Notifications
You must be signed in to change notification settings - Fork 0
Captive Technical
Captive is implemented as a grid-based first-person crawler. The engine keeps
logical DungeonLevel arrays at 64×32 cells. A cell has a type, per-face wall
textures, floor/ceiling textures, ornaments and small object fields. The
viewport renders the current level while HUD, inventory, terminal, shop and
puzzle systems operate on the same GameState.
A PL5 image is 40,000 bytes: 320×200 pixels at five bits per pixel. Every five
input bytes encode eight output pixels using a bespoke bit layout, not a normal
linear bitstream. pl5_decode() expands it to indexed pixels; palette values
are converted to ARGB for renderer textures.
The decoder has a dedicated regression test. Bounds checks reject a truncated payload instead of reading beyond it.
The verified DOS executable has SHA-256
71bcf404103f1ac2920800a8bc166939bb49a1204cf51bebce8aca7dd5faafde before
LZEXE unpacking. Static analysis of its unpacked 16-bit code shows a 320×200
VGA presentation path and two distinct PL5 blitters. One copies decoded pixels
unconditionally; the other preserves destination pixels for transparent source
values. Both write four VGA planes when planar output is active. The executable
also contains the original game-screen, roof, wall and door asset references.
These observations validate the resource-to-framebuffer path and are useful for checking the PL5 decoder. They do not yet identify the first-person projection tables, draw ordering, wall/door state encoding or creature placement. OpenCaptive therefore keeps the default dungeon viewport unpainted until those parts can be reproduced from original behaviour rather than an invented perspective approximation.
The renderer's preparation path also reads map cells with a 64-byte row stride, then rotates the sampling direction from the party orientation. It builds a near-field 5×5 cell window in a work buffer with an eight-byte row stride before dispatching draw operations. That establishes the original traversal window and map row width; it does not yet reveal the screen-space projection or the resource table selected for each cell type.
ANM starts with a 768-byte VGA palette of 6-bit RGB triples. A little-endian word at offset 768 marks the command end. Frame records are read backwards from end of file; each ends in a little-endian total-size word. Frame deltas use:
non-zero byte => XOR that byte into the current output position
zero byte => next byte is a skip count
zero + zero => end of frame
The target is a 64,000-byte 320×200 chunky frame buffer. Reconstructing each frame from XOR deltas preserves the original incremental animation behaviour.
Atari ST and Amiga resources may begin with RNC\x01. The header contains
big-endian unpacked and packed lengths plus CRC16 fields. Method 1 uses three
Huffman tables per sub-block. The decoder validates lengths before allocating
or copying and is kept separate from container readers.
- Atari ST: FAT12 disk image; the BIOS parameter block begins at offset 11.
- Amiga: 80 tracks × 2 sides × 11 sectors × 512 bytes (880 KiB), with OFS or FFS filesystem structures.
- DOS-style asset sets: may be extracted or ZIP-packed; the VFS identifies required content by SHA-256.
The original map model is a single 2,048-byte, 64×32 allocation divided into
sixteen 16×8 physical sections. Logical floors are assigned across those
sections; changing level changes the logical floor offset, not the allocation
shape. The modern engine exposes those regions as levels[], which is an API
adaptation rather than proof of byte-for-byte MapGen parity.
The documented mission/base seed is:
seed = ((mission - 1) × 11) + base
The first base is seed zero. Architect uses the sparse physical sections 2, 6
and 10 (one-based numbering) and starts the player at (30,0). OpenCaptive
implements and tests that special case. For maps 1–4, the documented usable
section sets progressively expand: row one plus 6–7; rows one/two plus 10–11;
the first three rows; then the first three rows plus 14–15.
The original generator has 30 ordered stages. The documented order includes
floor layout, offsets, root position, unconditional walls, elevators,
no-touch zones, digging/rooms, root directions, dead-end smoothing, fire,
generators, doors, puzzles, traps, encounters, decorations and exterior
generation. Current map_generate_base() preserves the allocation dimensions,
early masks, root special case and deterministic API, but it is not yet a
byte-identical implementation of those 30 stages. In particular, the
current PRNG arithmetic and feature placement must not be treated as recovered
original code until they are validated against original MapGen output.
The technical reference used for this boundary is the documented MapGen introduction and its linked stage pages. New MapGen work must add an original-output fixture or an independently reproducible reference before claiming parity.
The mission objective is generator destruction, not elapsed time or creature count. Completing every generator advances to the next mission; mission ten sets victory. Combat uses grid line-of-sight: walls and closed doors block both droid and enemy attacks. Normal doors require interaction to change to floor.