-
Notifications
You must be signed in to change notification settings - Fork 0
Captive Technical
Documentation baseline: v1.1.126. Runtime parity claims remain scoped to the verified boundaries described below.
For the repeatable daily emulator procedure, see Captive DOSBox-X startup. It records the normal launch command, the required VGA/focus step, mouse capture handling, and the authentic ORBIT-to-LAND rule.
Captive's recovered presentation currently consists of the original 320×200
HUD shell, the documented visibility rules, and a source-backed compatibility
viewport. Legacy DungeonLevel and gameplay structures remain in the source
tree as reverse-engineering notes; they are not decoded original map or save
state.
For an authentic runtime capture, run the real CAPPO.EXE in DOSBox-X and
open its debugger with Alt+Pause. At the debugger prompt, use
MEMDUMPBIN 0 0 100000 and copy the resulting MEMDUMP.BIN without changing
its contents. This is the emulator-memory capture consumed by the OpenCaptive
runtime bridge; it must be captured after the desired real keypad action, not
replaced by a generated map or a hand-written state file.
Use tools/run_captive_dosbox_x.sh DATA_DIR for a clean Mission 0001 emulator
profile. It launches the real CAPTIVE.BAT 1 chain unless a different command
is explicitly supplied. It
forces VGA-only hardware, surface output, integer normal2x scaling,
disabled aspect correction, and disables DOSBox-X's [video] memory io optimization 1. The latter is important for CAPPO's planar VGA writes: with
the optimization enabled DOSBox-X emits the characteristic repeated-glyph and
arrow corruption instead of the real game pixels. The profile also avoids
inheriting unrelated global settings such as svga_s3, stretched output or
the debugger terminal as the game surface. On macOS the helper selects
/opt/homebrew/bin/dosbox-x when present, or the path in DOSBOX_X_BIN, and
prints the selected version. This prevents an older PATH installation from
silently being used. The helper rejects direct CAPPO.EXE launches because
they skip the original video-mode initialization and are not valid parity
runs.
The built OpenCaptive binary exposes the same source-faithful path directly:
./build/opencaptive --data-dir DATA_DIR --captive-authenticThis starts the original CAPTIVE.BAT 1 through DOSBox-X. CAPPO keeps control
of input, rendering, timer, audio, landing and dungeon state; OpenCaptive does
not manufacture a replacement map or roster. The graphical OpenCaptive start
menu launches this normal DOSBox-X process and exits its launcher process so
there is no debugger-backed or native gameplay surface competing for input.
The isolated profile is copied beside development binaries and inside the
macOS app bundle's Resources;
DOSBOX_X_BIN can select another verified DOSBox-X executable.
The same handoff occurs automatically when a new DOS Captive game is selected
from the graphical start menu and CAPTIVE.BAT is present. If the original
DOS runtime is unavailable, the launch is rejected with the existing localized
missing-data error; it never falls back to the native compatibility shell or
invented gameplay state.
The direct command-line path follows the same rule: --game captive hands off
to the authentic DOSBox-X runtime whenever no explicit --captive-dos-dump or
--capture-frame analysis operation was requested. Those two flags remain
intentionally headless/native because they consume a caller-supplied real
checkpoint; an ordinary launch must never silently open the incomplete native
compatibility shell or a generated dungeon.
The startup harness can reproduce the original INTRO selections without inventing input files:
tools/captive_dosbox_intro.expect DATA_DIRFor the already verified Mission 0001 segment, the live sequence harness sends
raw make/break scan bytes through DOSBox-X's emulated AT keyboard controller.
CAPPO's actual IRQ1 handler reads those bytes from port 60h and builds the
runtime keyboard matrix; the harness does not write CAPPO's private matrix:
tools/captive_dosbox_queue.expect DATA_DIR 47This is an emulator/disassembly probe only, and it never synthesizes a map,
save, droid roster or dungeon state. Direct SM writes to guessed CAPPO input
slots are not input proof and are not used by the live sequence.
The normal OpenCaptive game path hands control to the original CAPPO window. The original timer, VGA surface, keyboard, mouse and audio remain authoritative; no target, planet, landing point or dungeon state is synthesized by OpenCaptive.
The optional FIFO bridge consumes the documented DEL continuation and
exposes complete original dumps while CAPPO remains the state owner. It is a
debugger-backed observation tool only; it is not the interactive game path and
does not claim mouse, audio, orbit or landing parity by itself.
For a complete DOSBox-X memory dump, the A000:0000 VGA surface is copied
directly to the native 320×200 framebuffer, including an all-black surface.
The runtime renderer does not fall back to a descriptor draw-list or map
reconstruction when those pixels are present. A malformed dump fails closed;
this keeps a bad emulator handoff visible as a failure instead of presenting
synthetic-looking viewport graphics.
The diagnostic MOUSE_DX:n, MOUSE_DY:n, MOUSE_DOWN, and MOUSE_UP
commands are transport messages to the original CAPPO session. The movement
encoding follows DOSBox-X's integration device: the normalized relative value
is stored in the high word of register 0x804200 and decoded as
(value / 256) - 1. Negative movement is therefore sent as repeated -1
steps; no absolute pointer coordinate or CAPPO state is written. These
commands are useful for debugger experiments, but are not a substitute for
testing the normal DOSBox-X mouse path.
The bundled DOSBox-X profile uses mouse emulation=locked and the original
INT 33 range. DOSBox-X's integration mode only emulates mouse motion while
the host pointer is not locked; that stops CAPPO's own cursor after the first
click. The locked mode keeps the authentic CAPPO pointer fed while the
DOSBox-X window owns input. The resulting VGA frame remains the parity
authority.
The multi-step probe writes to an explicit output directory and never treats
printable strings as proof of the active state. CAPPO keeps its message table
resident in memory, so strings would produce false positives. The harness
proves the original startup handoff and the verifier compares the complete
decoded 320×200 VGA surface:
tools/captive_dosbox_sequence.expect DATA_DIR 47 480 /tmp/cappo-sequence 8The command can therefore produce a useful negative result: a dump whose
decoded VGA surface is not a complete original frame is rejected by the normal
tools/verify_captive_dos_dump.sh byte-exact comparison. Message strings are
diagnostic only and are never used to enter Orbit, landing, or dungeon state.
The real-data-only tools/verify_captive_target_route.sh gate drives the
original Mission 0001 controls (4D ten times, 48 ten times, then 47),
verifies the green target in the resulting VGA dump, and stops at the
authentic FLIGHT PATH SET boundary. It does not claim that boundary is
Orbit or LAND; those transitions still require an original-runtime proof.
Earlier diagnostic notes treated CS:IP=0824:CF87 after scan 47 as a
semantic CAPPO dispatch boundary. Exact byte matching against the unpacked
original shows that address corresponds to source offset 0xD387, a rendering
helper, not a proven Orbit/arrival handler. It is therefore not used as parity
evidence. No debugger address or resident message string is sufficient to
advance native state; only an original-runtime VGA/state transition can do so.
The unpacked DOS executable installs its keyboard IRQ1 handler at relative
offset 0x065c. In the verified Mission 0001 DOSBox-X memory image the
relocated IRQ/source-bank segment is 0824, so the handler queue is observable
at 0824:004e..0057; this is not CAPPO's active code/text segment. CAPPO's
active relocated code/text image is identified independently by its known
strings: FLIGHT PATH SET, ARRIVED AT DESTINATION, and LANDING SUCCESSFUL
all resolve with relocation base 0x7E40 in the current DOSBox-X dump. Thus
the CAPPO dispatcher at file offset 0x6A33 is reached at 07E4:6A33;
the original game loop explicitly
restores DS=0x0E3F at CAPPO.EXE offset 0xC252 before processing game state;
that disassembly evidence outranks transient debugger register values sampled
inside another routine. 0x1663 is not used as the live game-state segment.
The source bank is passed separately to descriptor decoding.
The handler reads raw XT/AT
scan bytes from the emulated AT controller at port 60h. The relocated IRQ1
handler at CAPPO file offset 0x065c reads the controller byte, translates it
through the original keyboard table, and updates CAPPO's runtime matrix before
the game loop consumes the action. This is why a generic BIOS-buffer injector,
or a direct write to a guessed CAPPO memory slot, cannot verify keypad
navigation.
The verified keypad mapping is therefore raw scan code 0x47 for keypad 7
(ORBIT) and 0x49 for keypad 9 (LAND), with the original 0x01 escape code.
These findings are a disassembly reference for the live emulator bridge; no
input state is fabricated in the game runtime. A live post-landing dump is
still required before claiming full viewport parity.
The authentic startup handoff has also been verified in DOSBox-X. INTRO.EXE
asks the four original choices (1, 4, 3, 3) and transfers to
FILEPLAY.EXE. FILEPLAY installs its own IRQ1 handler and stores the latest
make scan at CS:0027; it does not consume the BIOS keyboard ring. The
space-bar make scan is 0x39. Supplying that byte at the verified runtime
IRQ1 segment (CS=0824) lets FILEPLAY complete its original animation sequence and
load CAPPO.EXE. CAPPO's first continuation is its documented DEL/left-mouse
action, represented by raw scan 0x53 in the relocated IRQ1 queue; this must be
consumed before testing keypad navigation. The proof is a caller-supplied 1 MiB
DOSBox-X dump containing CAPPO's own runtime strings; a dump that still contains
only FILEPLAY is rejected by the authenticity gate.
The optional final argument to the sequence harness controls only the number of original DOSBox-X timer frames advanced after each delivered make/break scan pair. It does not create a map, marker, landing point, or viewport; it makes the timing of an authentic CAPPO surface reproducible for comparison.
The harness allows a deliberately long response window for each DOSBox-X observation. This is needed on slower hosts during real transit frames; it is only a debugger-response timeout and never advances CAPPO state by itself.
The application bridge has also been exercised end-to-end with:
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy ./build/opencaptive \
--data-dir /Users/bosse/.opencaptive/captivedebug/captive \
--captive-dos-dump capture/original-captive/captive/MEMDUMP.BIN \
--capture-frame /tmp/cappo-live.ppmThe resulting native frame matched the standalone DOS-VGA extraction byte for byte. This validates the bridge, but it is still a single captured emulator state rather than full mission playthrough evidence.
For a complete local input/viewport gate, use:
tools/verify_captive_navigation.sh /path/to/original/captive [BUILD_DIR]This runs the original CAPPO input sequence and checks the authentic VGA and
memory surfaces. The current verified sequence reaches CAPPO's real
FLIGHT PATH SET state; it does not claim arrival or landing merely because a
coordinate is displayed. Arrival and landing remain gated on a later live
CAPPO handoff. The gate is intentionally local-only because original game
media is player-supplied and is never committed or synthesized.
The normal Captive start-menu action starts the original CAPTIVE.BAT 1
runtime in a standalone DOSBox-X window and closes the launcher. CAPPO remains
authoritative for timer, VGA, keyboard, mouse, audio, orbit, landing, and
dungeon state; the debugger-backed live bridge is reserved for diagnostics.
For a longer local replay that verifies each accepted command, including repeated commands, use:
tools/verify_captive_live_replay.sh /path/to/original/captive [BUILD_DIR]This sends the original Orbit, Land, Forward, turn, and Forward scan codes through the live CAPPO IRQ1 queue. It waits for a complete dump after each observation, then requires the dump, DOS-VGA frame, and runtime frame comparison to pass. The gate also requires multiple authentic frame changes. It is local-only because the original media is supplied by the player; no map, position, graphics, or text is generated by the helper. A new dump is not treated as a semantic CAPPO acknowledgement: orbit, arrival, landing, and dungeon entry still require their own authentic runtime evidence.
For timing diagnostics, the optional live FIFO also accepts WAIT:<ticks>.
This advances only the original CAPPO timer (1–2000 ticks) and injects no
keyboard scan. It is useful for separating CAPPO's FLIGHT PATH SET phase
from ARRIVED AT DESTINATION; a changed coordinate or VGA surface alone is
not an arrival proof.
When CAPPO_TRACE_DISPATCH=1 is set for the diagnostic harness, DOSBox-X also
records one complete CS IP DS AX SI DI snapshot at the real CAPPO dispatcher
after each live scan. This proves that the original input reached CAPPO's
dispatcher. It does not prove that the selected scan reached its indirect
handler, that transit completed, or that the ship is in orbit; those states
still require an authentic original-runtime frame or state transition.
Direct debugger queue injection is not used as parity evidence: it is not
equivalent to a real keyboard event. The verified path stops before proving
ARRIVED AT DESTINATION, NOW IN ORBIT, or LANDING SUCCESSFUL.
The diagnostic harness starts the original INTRO → FILEPLAY → CAPPO chain,
waits for CAPPO's relocated IRQ1 queue, sends the original XT make scan
followed by its break scan for each arrow or numpad action, and reloads the
resulting DOSBox-X VGA dump. If DOSBox-X, expect, the FIFO session, or a complete dump is
unavailable, the Captive session fails closed; it does not switch to the old
procedural dungeon or invent a replacement frame.
The same check is reproducible with
tools/verify_captive_dos_dump.sh MEMDUMP.BIN DATA_DIR [BUILD_DIR]. The script
rejects any dump that is not exactly 1 MiB or lacks CAPPO's own runtime-state
strings before it compares decoded frames; this prevents an early boot/debugger
buffer from being mistaken for gameplay evidence. It never creates or modifies
game data.
The Mission 0001 reference target is measured from the verified marker in
holamap-target.png: the green component is at frame pixels
(103,110), (104,110), (103,111), and (104,111), directly beneath
CAPPO's six-pixel magenta cursor. This is a selection reference only. Live
CAPPO testing shows that its displayed flight coordinate can change while the
status remains FLIGHT PATH SET; coordinate equality is therefore not an
Orbit/arrival proof.
The live DOSBox-X check starts from the authentic Mission 0001 frame with the
cursor already over the green marker, then sends keypad 7. CAPPO responds with
its own FLIGHT PATH SET message. In the currently verified session, keypad 9 still reports
SWAN NOT YET IN ORBIT; no post-landing surface is accepted until the original
runtime proves arrival. ORBIT therefore starts transit only: LAND remains
disabled until CAPPO returns the authenticated in-orbit VGA frame. A subsequent keypad-8 Forward command is likewise
kept behind that authentic handoff.
The native compatibility path does not convert the landing transition into
STATE_GAME after a fixed delay. The production live path changes mode only
after a real DOSBox-X/CAPPO VGA handoff; a captured dungeon frame is accepted
as runtime evidence only when the original dump passes the authenticity gate.
In native reference mode, the space-navigation cluster is no longer a dead
end: keypad 8/2/4/6 and the corresponding on-screen arrows emit CAPPO's
matching original scans and move the logged
cursor, and keypad 7 records the original FLIGHT PATH SET action when that
cursor is on the real green target. A second explicit ORBIT command is not
treated as arrival without a live CAPPO handoff; keypad 9 before that point
remains the original SWAN NOT YET IN ORBIT condition. This is an input-driven
reference state, not a timer, procedural arrival, or generated planet/dungeon.
The separate DOSBox-X dump loader still requires an exact VGA match before it
accepts a live runtime handoff.
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. The complete relocated descriptor table contains
959 records copied from CAPPO.EXE file records 3..961 at unpacked file offset
0x216b0 and is declared in include/captive_cappo_descriptors.h and defined in
src/data/captive_cappo_descriptors.c. DOSBox-X
memory dumps confirm that file record 3 is runtime DS:00c0 record 0. The
focused 112-entry viewport subset is in include/captive_viewport_descriptors.h.
Each descriptor is 8 bytes:
source_offset(u16), destination_offset(u16), width_bytes(u8), height(u8),
flags(u8), source_bank(u8). The descriptor_blit() helper handles mirror and
mask-zero flags. It is covered as a decoder/validation helper; the live
viewport still uses the compatibility renderer until descriptor selection and
original draw-order integration are recovered. A test verifies all entries
have valid dimensions and destinations within the 160×112 viewport.
The LZEXE-expanded program has SHA-256
fa7d5ca76d26f614476ed41f27cf737084942e9216b20b4605734df9ede9aee4.
Offsets in this subsection are relative to that expanded load module, not a
DOS segment address. The view path dispatches a sampled cell at 0x1fd1.
Its handlers select a graphic ID, then enter the range-aware helpers at
0x1ee4 and 0x1ef3; those apply the original cell-depth adjustment before
calling the projection helper at 0x2bd7. That helper is distinct from the
static graphic-descriptor entry.
The static entry at 0x2f55 computes 0x00c0 + graphic_id × 8 in the
original runtime's descriptor table. The descriptor supplies a source pointer,
destination position, dimensions and blitter flags. The common path selects
one of the planar copy routines at 0x37dd, 0x393f, 0x3c5f, 0x3c62 or
0x3c65.
This is direct evidence that the projection is a table-driven sequence of
masked panel copies, rather than texture mapping. The complete descriptor table
has been recovered and verified — all 112 entries are extracted from the
expanded CAPPO.EXE binary at file offset 0x21698, with source coordinates,
destination positions, dimensions, mirror/mask flags, and source bank indices
matching the original DOS runtime.
The original one-megabyte DOS memory fixture has SHA-256
9003c4a8818cb97f8299ac90cfe51e90e535ab9a725545526fe75f14ddb8dd7e.
It captures a running DOS renderer, not an archive extraction. In that state
the source-bank selector table is present at segment 0x0824; this is a
relocated data-area selector, not the CPU's active code segment. This older
descriptor fixture uses offline selector segment 0x2942, not the live
post-handoff DS. MZ relocation changes the unexpanded table selector in the
code to segment 0x2942. The descriptor
array begins at 0x2942:0x00c0 and has eight bytes per graphic ID. The common
draw entry reads, in order, a little-endian source offset, a little-endian
destination offset, width, height, flags and source-bank index.
For example, graphic IDs 0x004–0x009 occupy source offsets near 0x6660,
have heights of 49 bytes and route through bank zero. The original code then
resolves that bank through its relocated segment selector table before choosing
the planar copy routine. This fixture makes the record layout, runtime
relocation and source-bank indirection reproducible. It is an analysis oracle
only: no dump bytes are embedded in OpenCaptive or used as a game-data
substitute.
The static entry also proves a missing operand in a table-only reconstruction.
It loads the descriptor destination word, then adds the caller-provided DI
base before entering the blitter. Thus the destination word is a relative
panel position, not a complete screen coordinate. One view-dispatch path at
0x1e7f sets DI to 0x5000 before jumping to the entry; another recovered
path uses 0x53a0. The selected base depends on the cell, range and
orientation. A single completed VGA frame can confirm source coverage, but
cannot reveal every per-call base or the draw order.
The expanded CAPPO executable also establishes the original dungeon-map
addressing without requiring generated data. At 0x4936, CAPPO accesses its
map byte array at DS:0x7CB3; the helper at 0x4949 computes the byte index as
(y << 6) + x and rejects coordinates above x=63 or y=31. The active
coordinates are read from DS:0x5E80 and DS:0x5E82. This proves the original
64×32 map layout and the live coordinate fields. The byte values are CAPPO's
cell/object codes, not OpenCaptive's native CellType enum, so no partial
interpretation is used until each code and its render path is source-verified.
The fixture and the 320×200 VGA frame with SHA-256
9003c4a8818cb97f8299ac90cfe51e90e535ab9a725545526fe75f14ddb8dd7e
also make individual panel commands testable without treating the dump as
runtime input. Source pixels are expanded with the original PL5 packing,
the transparent-write and horizontal-flip flag bits are applied, then the
opaque pixels are compared with the captured viewport.
| Graphic ID | Source | Flags | Captured viewport position | Evidence |
|---|---|---|---|---|
0x010 |
bank 0, offset 0x1a04, 16×98 pixels |
0x02 |
(32,64) |
1 511 of 1 568 source pixels agree; the rest are covered by later panels. |
0x01a |
bank 0, offset 0x1a0e, 16×98 pixels |
0x07 |
(48,64) |
1 356 of 1 356 opaque pixels agree exactly. |
These two commands establish the left-side 32-pixel panel pair for this specific captured view. They do not yet identify the original cell-to-graphic selection table, the full draw order, or a general dynamic renderer; those remain necessary before claiming viewport parity.
The captured selector table resolves the first seven source-bank slots to these DOS PL5 content hashes:
| Bank | Hash |
|---|---|
| 0 | 7edb8ee856a91e835ea86dda00af49fda3dae730d694bd7234b7fa96d711e296 |
| 1 | dec7143f063c98459ab2f267ed135204cdee1b521eda9810b219e8c10e05c7e8 |
| 2 | 70e0b9bfbaa5dfd12643b50cbe10d0b664de2fb1106d8ff0f2fde1ce6f443bbe |
| 3 | ce00ba2bc78f160b934486fe101a90264163356e02e7acbea2a41cf5d125b017 |
| 4 | fed16e510697e17123d474c08687de548076b26a55f08f1d00fd17e3fcdf9410 |
| 5 | 21db7daf64cff3b0cae19c3e7eb2057762df9110055e7253175024ecb146fb6b |
| 6 | 63ffa6901b59d463b050088065503d386ca2f3813ed91d8e0833320f9df2fe11 |
This relation was proven by comparing each relocated source segment to the corresponding full 40,000-byte hash-identified payload. It is a fixture for this verified renderer state, not a substitute for the original loader's asset-set selection.
The descriptor's destination word is not an offset into a 200-byte-wide
PL5 source sheet, nor a complete displayed coordinate. The static entry at
0x2f55 first adds the caller's DI base. In the native 0x3d42 copy path,
one five-byte source group is expanded to eight indexed output bytes and the
destination row advance is 0x140 (320 bytes). The alternative paths preserve
destination pixels for transparent source values. The low flag bit selects the
mirrored variant.
Consequently, a descriptor cannot be reproduced by decoding a source crop and
placing it at destination / 200. That tempting shortcut yields plausible
wall fragments but at incorrect positions and is not used by OpenCaptive. The
caller base and per-cell descriptor order must be recovered before native view
rendering can claim visual parity.
The renderer samples a 19-cell trapezoid rather than a full 5×5 view: five
cells at ranges four and three, three cells at ranges two and one, then the
left/current/right cells at range zero. It rotates that sample from the party
orientation into a fixed forward-facing order, removes cells hidden behind
walls, and draws back to front. captive_view_window_build() now reproduces
the verified 19-cell sampling order and the ordered original wall-occlusion
pass while retaining the earlier 5×5 work area for analysis. A cleared cell
does not take part in later wall tests, matching the original copied-map
behaviour. Panel projection and resource selection are still separate recovery
work, not inferred rendering.
The sampling footprint, ordered cleanup conditions and back-to-front boundary are documented independently in The Ultimate Captive Guide: View Rendering. The implementation intentionally limits itself to those published rules until the DOS panel tables are also recovered from verified executable/media bytes.
The five relevant DOS PL5 resources are selected only by SHA-256:
47ad15b4a593c37880d0306b6a0f51b7a9f20615cf6a188f23716d5b48315524
43833e4a8df622f84d53698a76c6d18f910c1cca79c6b89cbfacc563f695356c
8b7301fc6c302fd673a81d23e7a99d715aa02d5b404c1e1edea19ceccccc9681
519d3ef4494f0e868479a90c8a47249b840598e382c7ba3272f417ce3daf5936
7edb8ee856a91e835ea86dda00af49fda3dae730d694bd7234b7fa96d711e296
The first digest decodes to the published fed7-A interior reference image.
This establishes that the source data is authentic, but also rules out the old
64×64 tile interpretation: each 320×200 sheet contains irregular, overlapping
preprojected panels. A parity renderer must recover the original panel source
rectangles, destinations, transparency convention and per-cell state table;
sampling fixed-size tiles cannot reproduce the reference viewport.
The Captive presentation path is source-backed only. If an original ANM,
holomap, landing, or dungeon frame is missing or invalid, the renderer clears
the frame or returns to an authenticated frame; it does not paint replacement
status text, procedural planets, generated rosters, or a compatibility map.
The native fallback holds the verified CAPPO flight and landing transitions
and never advances them on a timer. A complete live handoff still requires a
DOSBox-X dump recorded after the real orbit or landing action, with the
original CAPPO VGA surface populated at A000:0000. When that dump is
reloaded, OpenCaptive compares the complete 320×200 VGA surface against the
corresponding real checkpoint. Only an exact orbit match enters orbit, and
only an exact landed match enters the landed view; any other dump remains in
the current transition.
The original-mode runtime now executes the recovered descriptor bands through the same 160-byte work-row layout used by CAPPO. The compositor is wired after the verified GAME SCRN shell and before any optional enhancement layer. It copies source pixels from the content-addressed PL5 sheets, preserves index zero transparency, and exposes the first 144 pixels of each row at the disassembly-verified viewport origin. The runtime table alignment was checked against a real DOSBox-X memory dump; the first three records in the unpacked file image are not runtime descriptor records.
This is a real-data rendering path, not a generated replacement scene. It is deliberately not described as complete parity yet: the original runtime still selects graphic IDs and destination bases from per-cell operands that are not fully recovered. Until those operands and the original map/runtime records are decoded, the compositor is a verified panel-draw boundary and the remaining compatibility renderer stays available for isolated tests.
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 DOS generator entry at 0x1C3C in the unpacked CAPPO.EXE operates on a bitmask buffer (20 bytes per row, stride 0xA0 in viewport mode). Key stages:
-
Random rectangle carving (0x1C4B): Decrements iteration counter
[0x8D7F], generates random position (AND 0x7F, max 0x6F) and size (AND 0x7, INC → 1-8), writes wall data via bitmask OR/AND. - Boundary cleanup (0x1D1C): Scans 3×3 neighborhoods around each cell comparing against threshold 0x18. Fills border cells with 0x20 (floor).
- Cell type assignment (0x1E50): Converts bitmask to cell records (8 bytes each). Values: 0x00=wall, 0x20=floor, 0x44=door. Checks grid boundaries at CX=0x40 (width 64) and BX=0x20 (height 32).
The map-generation PRNG at 0x8878 is the DOS variant: it computes
state = state * 0x5E5 + 0x29, performs three ROR AX,1 operations, then
executes XOR AH,0x08; the low word is stored at [0x92F8]. CAPPO also has a
second unrotated helper at 0x889A; it is not the map-generation entry.
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 verified unpacked executable (144,556 bytes) contains the complete game data tables. See Captive Game Data for the full extraction:
- 10 droid material grades (SHIT through TITANIUX)
- 10 combat skills (Brawling through Energy Weapon)
- ~40 items with hex classification bytes
- 48 name-generation syllables (8 consonants x 6 vowels)
- 14 music categories x 11 variations (154 track slots)
- 4 sound drivers (AdLib, Roland, PC Speaker, Sound Blaster)
- 16 ANM animation files and multiple PL5 graphics sheets
The executable structure, dispatch tables and descriptor format are documented above. The disassembly data provides ground truth for reimplementing item tables, shop pricing, combat formulas, and name generation without guessing.
src/render/viewport.c contains an experimental compatibility renderer based
on the 19-cell trapezoid from captive_view_window_build() and hash-verified
PL5 source sheets. It remains active so the game is playable, but its placement
and scaling rules are an approximation, not the original descriptor sequence.
Displaying it must not be described as pixel-identical DOS parity.
The active Captive path draws the verified original GAME SCRN shell and, when
available, an authenticated landed checkpoint. It does not draw the
compatibility viewport: the original DOS renderer is a back-to-front sequence
of descriptor-driven planar copies with caller-specific destination bases,
mask behaviour and per-cell ordering. Re-enabling a live viewport therefore
requires those commands to be recovered from the original runtime, then
compared against DOS-VGA captures. Until then, generated perspective, floor,
ceiling, door, creature and object pixels are not shown as Captive content.
captive_map_dump MEMDUMP.BIN [DS-segment-hex] [source-bank-segment-hex] is an
analysis-only tool. It
reads a caller-supplied, complete 1 MiB DOSBox-X memory dump and prints the
original 64×32 byte map at DS:7CB3, the active coordinates at DS:5E80 and
DS:5E82, and the orientation field at DS:5E84. It deliberately preserves
CAPPO's raw cell bytes and performs no conversion to OpenCaptive cell types.
The default segment is 0x0E3F, matching CAPPO's original game-loop DS reset.
No dump bytes are bundled and the tool is never used as runtime
game data.
The same boundary now reproduces CAPPO's 5×5 copied neighbourhood from
DS:7CB3 using the four orientation branches at 0x1818. Outside-map cells
are reported separately instead of being replaced with a guessed wall or
floor. This makes the raw viewport input testable without claiming that the
later cell-code-to-descriptor dispatch is complete.
CAPPO's draw-order input is also exposed as 38 raw eight-byte records at
DS:5B82..5CB2, the list iterated by 0x2D09. Each record is linked to its
DS:12F1 window index and routed through the correct normal or overlay
handler. captive_map_dump prints this sequence from a supplied DOSBox-X
dump; the fields remain raw offsets until the handler operands are verified.
The first raw dispatch gate is also recorded without semantic renaming. CAPPO
has two branches: the normal branch at 0x1AC0 and the overlay/object branch
entered when DL & 0x08 is nonzero at 0x1ABB:
raw & 0x7F routes recognized values to CAPPO handlers at 0x1D72, 0x1DFC,
0x1E13, 0x1E35, 0x201C, 0x2065, 0x206A, 0x20C7, 0x2103,
0x212F, 0x2171, 0x218C, 0x21A0, 0x21A9, 0x26F8, 0x2701,
0x272E or 0x445A. The route API exposes these as handler identifiers;
descriptor IDs and visual meanings are deliberately left unresolved until
the corresponding original table operands are verified. The normal branch is
exposed separately for raw 0x00..0x1B and 0x3E, including its 0x1C90,
0x1D17 and 0x1DF2 entry points; a raw byte is not assigned one universal
visual meaning across both branches.
The dump tool also prints a 5×5 matrix of these handler addresses, so an
emulator dump can be compared directly with the disassembly before any
viewport pixels are composed.
When a recovered handler operand exists, it additionally decodes the real
eight-byte descriptor record and reports its source offset, destination,
dimensions, flags, source bank and whether the record is drawable or a zero
dimension sentinel. The default source-bank segment 0x0824 is the segment
observed in the supplied runtime fixture and can be overridden for another
real dump.
The next analysis boundary recovers descriptor operands for handlers whose
immediate formulas are now complete. For 0x1D17, the operand is the
orientation-selected word at DS:5CC2 plus record byte 6. For 0x1E35,
record byte 6 indexes the real DS:1276 table; CAPPO adds 0x17E/0x18B
when flag bit 2 is set, or 0x192/0x199 on the ordinary path. 0x1DFC
and 0x1E13 expose their corresponding +0x1ED and +0x157 operands.
The API and dump tool emit these only after the recovered handler-local table,
orientation and state checks pass, and label them handler-preconditions-pass.
The IDs are the actual operands from the supplied dump; descriptor-table
sentinel validation and the remaining overlay bands are still separate work.
For the captured navigation frame this produces real operand sequences such
as 01DC 01E3 (the DS:1276[3] path) and 0406/0405 (the DS:5CC2
path). No descriptor ID is synthesized when a source table marks a route as
inactive.
Overlay handler 0x2103 is also decoded. CAPPO selects base 0x2E6 or
0x2EF from the relocated 0x8CFD mode and the real planet-coordinate
parity, then applies 0x20E4's BP range rule (0, >10 and 4 reject;
5..10 decrement). The resulting descriptor operand is reported only when
those exact conditions are satisfied. This remains an analysis path; it does
not substitute generated object or sprite data for an unresolved overlay
band.
Overlay handlers 0x2171 and 0x218C now expose their two real descriptor
operands each. 0x2171 calls 0x20E4 for bases 0x349 and 0x352; 0x218C
calls 0x20F3 for bases 0x35B and 0x367. The BP acceptance and decrement
rules are applied exactly as in CAPPO, and rejected values produce no operand.
These are still source-locked operand paths only: no pixels or replacement
object data are invented while the remaining overlay handlers are unresolved.
The same 0x20E4 path is now decoded for 0x206A (0x6B / 0x74) and
0x20C7 (0x86 / 0x7D). Their preceding 0x2768 calls are renderer-side
effects and do not supply descriptor IDs, so the analysis exposes only the two
actual operands from each handler and keeps the original rejection rule.
The shared 0x2089 state-table path is decoded for 0x2065 and 0x21A0.
0x26F8 and 0x2701 additionally expose their 0x2715 operand before the
same two 0x2089 operands. 0x212F exposes its 0x2715 operand plus the two
or four 0x20E4 operands selected by DS:5EF4 bit 0. The state table remains
caller-owned (DS:0E3F:93AE); an absent state match yields no fabricated ID.
For 0x21A9, the fixed odd-direction 0x21D1 branch is also exposed when
its exact BP=0x0C precondition holds. CAPPO selects 0x0D0, 0x119/0x11A
or 0x2AD/0x2AE from AX & 7, then always calls the shared 0x2089 path.
The other 0x21D1 branches depend on additional runtime state and remain
unresolved rather than being filled with guessed descriptors.
The compatibility viewport no longer fills missing object/lock sprites with
procedural colored rectangles. It draws only decoded pixels from the verified
OBJECTS.PL5/door sheets; absent real source data leaves the corresponding
area untouched. This prevents synthetic content from being mistaken for
Captive artwork while the raw CAPPO compositor is still being completed.
captive_dos_runtime_render() is now the first executable bridge for a
complete DOSBox-X memory image. It consumes CAPPO's live DS map, copied 5×5
window, 38 draw-order records, descriptor table and packed source banks, then
executes those original descriptor copies into the 160-byte work buffer. It
does not convert cells to GameState, use map_gen, or add a replacement
background. Unknown handler paths simply draw nothing, matching the current
evidence boundary.
The captive_runtime_render tool exercises this bridge against a caller-owned
MEMDUMP.BIN. The dump is deliberately not shipped or scanned as game data;
it is only an emulator observation. The next parity gate is a fresh DOSBox-X
dump taken after each real movement input, followed by an exact 144×112
viewport comparison. Until that gate passes, the production Captive path does
not claim live-dungeon parity.
The dump tool now also evaluates the shared 0x1C90 helper. It preserves
CAPPO's three-byte row padding: a neighbour read landing in that padding, or
outside the 5×5 copied window, is reported as unknown, never as a fake cell.
For direction values 1, 3 and 2 it reproduces the original [di+9],
[di+1], [di+7] and [di-1] comparisons against 0x1A, including the
special raw-code bypasses (0x1A, 0x1C, 0x22, 0x24). The result is a
helper outcome, not a universal handler gate: 0x1D17 and 0x1E35 continue
after calling 0x1C90. This separates its possible special-case descriptor
from the handler's own descriptor operands and is the next input needed before
enabling the active compositor.
The native OpenCaptive compatibility tick is disabled for Captive. It must not invent droid names, hit points, energy regeneration, combat events, messages or sound effects while the original CAPPO runtime is not driving the state. A Captive game frame is therefore accepted only from a real DOSBox-X memory dump or from a hash-verified original reference frame. Missing runtime bytes leave the unsupported portion empty; they are never replaced by a generated map, landing point, dungeon, roster or story sequence.
10 game-level SFX types mapped to CAP_A.BIN AdLib sequences via INT 61h disassembly:
| Game SFX | Index | Sequence offset | Description |
|---|---|---|---|
| SFX_GENERATOR | 8 | 0x56B6 | Generator hum |
| SFX_HIT | 13 | 0x5763 | Melee hit |
| SFX_LEVEL_UP | 15 | 0x56AC | Level up fanfare |
| SFX_DEATH | 17 | 0x578E | Droid death |
| SFX_BUTTON | 18 | 0xA48C | Button press |
| SFX_PICKUP | 20 | 0x50B5 | Item pickup |
| SFX_SHOOT | 22 | 0x92FE | Ranged weapon fire |
| SFX_DOOR_LOCKED | 23 | 0x82CC | Locked door rattle |
| SFX_DOOR_OPEN | 24 | 0x8285 | Door opening |
| SFX_STEP | 26 | 0x67D0 | Footstep |
Driver remapping formula: game index < 4 = silent, 4-14 = direct pass-through, 15+ = index - 11. This maps the 10 game-level SFX indices into the 49-entry sequence table in CAP_A.BIN.
Creature HP, category, speed and sprite routing are recovered from the DOS
tables documented below. Creature attack damage is not yet source-verified.
The code around unpacked CAPPO offset 0x5380/the corresponding attack
bytecode constructs a weapon damage word; it is not proof of the enemy attack
record or of a category/level creature formula.
OpenCaptive therefore keeps a bounded category/level compatibility formula in the runtime, clearly separated from the recovered data. It must not be described as original-game parity until the enemy attack record and caller are identified.
9 type code prefixes classify all items in the CAPPO.EXE item database:
| Code | Class | Examples |
|---|---|---|
| 0x00 | Melee weapon | DEV-SCAPE, KNUCLE-DUSTER, MINE, DIE, BALL, MAP |
| 0x08 | Ranged weapon | BATTERY |
| 0x10 | Ammo | CARTRIDGES, SHELLS, LASER PACK, SONIC PACK |
| 0x20 | Armor (chip) | DROID CHIP |
| 0x21 | Shield (equipment) | OPTIC, CAMERA, BATTLE-GLOVE, WAR-BLADE, PISTOL |
| 0x27 | Battery (body part) | HEAD, CHEST, LEG, FOOT, HAND, GOLD |
| 0x30 | Key (ranged weapon) | COLT, MAGNUM, RIFLE, automatics, energy, heavy |
| 0x60 | Body part (explosive) | EXPLOSIVES |
| 0x65 | Special (body variant) | ARM |
Defense values per body part slot, applied when computing damage reduction:
| Slot | Defense |
|---|---|
| HEAD | 10 |
| CHEST | 15 |
| ARM | 8 |
| LEG | 8 |
| FOOT | 5 |
| HAND | 5 |
These are base values scaled by the droid's material grade.
The DOS release includes 63 standard MIDI files (format 1) organized into 14 categories matching the music_categories table from the executable:
| Category | Variants | Use |
|---|---|---|
| MAIN2 | 1 | Title screen |
| GENBASE | 1 | Generator base exploration |
| BATT | 11 | Combat encounters |
| SHOPKEEP | 1 | Shop interaction |
| HOLOMAP | 1 | Holamap screen |
| ESCAPED | 1 | Escape sequence |
| FINAL2 | 1 | Final mission |
| TRAPPED | 1 | Trapped state |
| FCBASE | 11 | FC base exploration |
| VCBASE | 11 | VC base exploration |
| LONGNT | 11 | Long night ambient |
| W | 11 | Walking/exploration |
| COMPROOM | 1 | Computer room |
| RUNNING | 1 | Running/chase |
Categories with multiple variants are selected randomly using the game PRNG.
All 63 files are identified by SHA-256 content hash. The MIDI player renders
through a software synthesizer at the start menu's selected output rate
(22,050, 44,100 or 48,000 Hz; 22,050 Hz remains the compatibility fallback).
The optional --hq-midi path applies a short output filter after synthesis;
it does not replace the verified Captive instrument patches.
Sound effects are generated via AdLib OPL2 FM synthesis, not stored as PCM
samples. The CAP_A.BIN file (5,426 bytes) is a loadable x86 driver containing:
- x86 driver code (0x000–0x3BF): AdLib I/O port programming and SFX interpreter loop
- 9 channel descriptors (0x3C0–0x46F): OPL2 channel-to-register mapping
- Amplitude decay table (0x470–0x4FF): 128 descending values for envelope
- OPL2 F-number table (0x530–0x5CF): 128 frequency values for note pitch
-
49 SFX sequences (0x0A6A–0x138F): bytecode programs that write OPL2
registers to produce sound effects, each terminated by
0xFF+pend - 26 instrument patches (0x1390–0x1532): 16 bytes each containing the 11 OPL2 register values per voice (AM/VIB/EG/KSR/MULT, KSL/TL, AR/DR, SL/RR, waveform, feedback/connection)
The SFX sequences, instrument patches, and frequency table have been extracted
to adlib_data.c with a verification test suite.
The interpreter loop at offset 0x734 in CAP_A.BIN runs 4 voices simultaneously at ~70 Hz (DOS timer tick). Voice state is a 0x1C-byte struct with PC, delay counter, note offset, loop table pointer, subroutine return address, and PRNG.
| Opcode | Size | Action |
|---|---|---|
| 0x80 | 3 | Key on: note = operand1 + note_offset, delay = operand2 |
| 0x81 | 2 | Set delay counter |
| 0x82 | 2 | Set OPL2 volume register |
| 0x83 | 2 | Set note offset (added to all subsequent notes) |
| 0x84 | 2 | Load instrument patch by index |
| 0x85 | 2 | Set delay (variant, jumps to delay path) |
| 0x86 | 3 | Call subroutine (saves return address) |
| 0x87 | 1 | Return from subroutine |
| 0x88 | 2 | Key on with PRNG-generated note, delay = operand |
| 0x89 | 2 | Set delay to (PRNG & operand) + 1 |
| 0x8A | 3 | Jump to absolute address |
| 0xC8 | 1 | Key off |
| 0xFF | - | End/loop: decrement loop counter, advance loop table pointer |
Opcodes 0x88 and 0x89 use a per-voice xorshift PRNG for randomized pitch and timing effects (explosion rumble, ambient noise). Opcode 0x86/0x87 implement single-level subroutine calls for sequence reuse within SFX programs.
The CTV files (SB15.CTV, SB20.CTV, SBPRO.CTV) are CT-VOICE driver files
for Sound Blaster DSP, not Creative Voice (VOC) audio samples. They contain
executable code for DMA-based PCM playback but no sound data.
Item records are stored in the unpacked CAPPO.EXE at offset 0x1a090. The first item (HEAD) has a unique 16-byte prefix containing body part armor values for all six material grades. Subsequent records follow the format:
00 type_code [grade_byte] NAME 0x20
Type codes recovered from the binary:
| Code | Meaning | Examples |
|---|---|---|
| 0x00 | Misc / no class | DEV-SCAPE, KNUCLE-DUSTER, MINE, DIE, BALL, MAP |
| 0x08 | Consumable | BATTERY |
| 0x10 | Ammo | CARTRIDGES, SHELLS, LASER PACK, SONIC PACK |
| 0x20 | Chip | DROID CHIP |
| 0x21 | Equipment | OPTIC, CAMERA, BATTLE-GLOVE, WAR-BLADE, PISTOL |
| 0x27 | Body part (standard) | HEAD, CHEST, LEG, FOOT, HAND, GOLD |
| 0x30 | Ranged weapon | COLT, MAGNUM, RIFLE, all automatics/energy/heavy |
| 0x60 | Explosive | EXPLOSIVES |
| 0x65 | Body part (variant) | ARM |
Body parts carry a grade byte (always 0x05 in the base table). POISON, ACID,
and FLAMBOS use 04 27 as a terminator instead of 0x20, marking them as
special ammo sub-types (grenade-like items that deal body-part damage).
Ammo items (type 0x10) include caliber price entries: CARTRIDGES with prices
20/45/50 (using % and & delimiters for two price tiers), A51 MISSILES,
SHELLS, LASER PACK, SONIC PACK, and the three grenade types.
The weapon variant section at 0x1a220 contains 23 upgrade tier records in the
format 00 type_code 04 2c PRICE_STRING 2d where the price string is ASCII
decimal. Suffix letters (A/B/C) distinguish same-price items. Gold cap is 200
([0x8D81] capped at 0xC8 in bar renderer at 0x1AE7).
| Tier | Type | Price | Notes |
|---|---|---|---|
| 0 | 0x30 ranged | 1.9 | Base ranged |
| 1-5 | 0x21 melee | 2, 3, 5, 7, 14 | Melee progression |
| 6-19 | 0x30 ranged | 14, 27, 23, 33, 56, 78, 99, 111, 141, 165, 180, 200, 211, 231 | Ranged progression |
| 20-22 | 0x30 ranged | A12, L22, X42 | Named variant prefixes |
Item stats (damage, range, ammo capacity, price, weight) are not stored as lookup tables. The original game computes them procedurally from the type code and material grade. The stat computation formulas remain to be recovered from the code section of the executable.
Captive uses multiple PRNG variants, all based on the same linear congruential
core (state = state * 0x5E5 + 0x29) but with different post-processing:
| Location | Rotation | XOR | Usage |
|---|---|---|---|
| 0x8A8E | ror 3 | xor ah,0x08 | Main game PRNG |
| 0x9815 | ror 2 | none | Combat randomness |
| 0x9FBA | ror 3 | and 0x0F (mask) | Name/map generation |
| 0xDAB6 | none | xchg al,ah | Map seed chaining (3 iterations) |
The main PRNG state is stored at [0x9308] (combat) and [0x92F8] (general).
All operate on 16-bit values.
Combat is processed at 0x8D66 in the unpacked CAPPO.EXE. The system iterates over 4 droid slots (struct size 0x10E bytes, base 0x8DC7) from the droid array at 0x5E38:
-
Hit check (0x97D9): Looks up attacker's target list in a creature table indexed by CH. Walks the list comparing each entry to AL (target ID). Increments hit counter (DL) on match.
-
Damage calculation (0x9BF4):
damage = lo_byte([di+6]) * hi_byte([di+6]). The two bytes at offset 6 in the combat struct encode base damage and multiplier. -
Damage scaling (0x9BFC): Shifts damage left up to 3 times (×2, ×4, ×8), checking for signed overflow at each step. Returns 0xFFFD (-3) as an overflow sentinel.
-
Damage application (0x8DAE):
[di+6]damage value is added to the relevant accumulator for the attack record. This section describes the recovered attack-bytecode path; it does not identify the enemy creature record or prove that every creature uses the same fields.
XP is a 32-bit accumulator at droid struct offset 0x22 (low word) and 0x24 (high word), capped at 0xF8FFFFFF.
XP award (0x9621): on creature kill, each living droid receives:
xp_base = creature_xp_value + 3 + min(difficulty, 100)
xp_gain = droid_skill_byte * xp_base * 12
where droid_skill_byte is [di+0x0B] in the droid struct.
Display level (0xB142): level = xp >> 10. So 1024 XP = level 1.
XP threshold per skill (0xAB92): each of 10 skills has a base XP value from a table at DS:0xB708 (file 0x19EF8):
| Skill | Base | Growth | Max level |
|---|---|---|---|
| ROBOTICS | 10 | +12.5% + 8 per level | 66 |
| BRAWLING | 8 | +6.25% (rounded) | 24 |
| SWORDS | 30 | +6.25% (rounded) | 24 |
| HANDGUNS | 72 | +6.25% (rounded) | 24 |
| RIFLES | 216 | +6.25% (rounded) | 24 |
| AUTOMATICS | 648 | +6.25% (rounded) | 24 |
| LASERS | 1944 | +6.25% (rounded) | 24 |
| CANNONS | 3888 | +6.25% (rounded) | 24 |
| SPAYGUNS | 7776 | +6.25% (rounded) | 24 |
| EXPERIENCE | 7960 | +6.25% (rounded) | 24 |
All skills cap at threshold 0x8A47 (35,399) at their respective max level.
XP overflow protection (0x87EC): if high word ≥ 0xF8FF, no more XP is
added. The or cl, 0x01 ensures at least 1 XP per award.
Weapon damage encoding recovered from file offset 0x1A006.
Melee weapons (18 entries, 2 bytes each: lo, hi):
| # | lo | hi | Damage (lo×hi) | Notes |
|---|---|---|---|---|
| 0-3 | 0x04 | 0x21-0x3C | 132-240 | Starter weapon, 4 grades |
| 4-7 | 0x4C | 0x21-0x3C | 2508-4560 | Heavy melee, 4 grades |
| 8-12 | varies | 0x21-0x45 | 693-1311 | Medium melee, 5 grades |
| 13-17 | varies | 0x21-0x45 | 2970-6141 | Elite melee, 5 grades |
Grade progression uses hi bytes: 0x21(33), 0x2A(42), 0x33(51), 0x3C(60), 0x45(69) — increments of 9.
Ranged weapons (20 entries, 4 bytes each: base, modifier, flag, 0x00):
5 tiers × 4 levels. Base values: 72, 88, 104, 120 (increment 16). Modifier values: 45, 35, 25, 15, 5 (decrement 10 per tier). Flag: 1 for upgradeable, 0 for max-tier or base tier.
The combat PRNG at 0x9812 uses the weaker ror-2 variant without XOR, making combat sequences more predictable than general game randomness.
Creature stats recovered from CAPPO.EXE DS:0xA1BF (file offset 0x189AF).
Each creature type has a 4-byte entry: min_hp (word), max_hp (word).
25 creature types total, grouped into 8 categories (3 types each, plus 1 extra).
HP formula (0x9B12):
base = min + ((max - min) * difficulty) / 8
hp = ((base * 2 * creature_modifier) >> 8) + 6
Where difficulty = dungeon level capped at 0-8, creature_modifier = byte
from creature spawn data at [di+9].
| Type | HP min | HP max | Cat | Speed | Sprite |
|---|---|---|---|---|---|
| 1 | 10 | 150 | 0 | 0 | 0x67 |
| 2 | 200 | 400 | 0 | 5 | 0x67 |
| 3 | 400 | 600 | 0 | 7 | 0x67 |
| 4 | 600 | 800 | 1 | 10 | 0x67 |
| 5 | 800 | 1000 | 1 | 16 | 0x67 |
| 6 | 1000 | 1500 | 1 | 20 | 0x67 |
| 7 | 1000 | 1700 | 2 | 30 | 0x6A |
| 8 | 1700 | 2400 | 2 | 20 | 0x6A |
| 9 | 2400 | 3000 | 2 | 22 | 0x6A |
| 10 | 3000 | 3700 | 3 | 24 | 0x6A |
| 11 | 3700 | 4400 | 3 | 24 | 0x6A |
| 12 | 4000 | 4900 | 3 | 40 | 0x6A |
| 13 | 3000 | 3700 | 4 | 32 | 0x6A |
| 14 | 3700 | 4400 | 4 | 36 | 0x6A |
| 15 | 4400 | 5700 | 4 | 46 | 0x6A |
| 16 | 9000 | 10000 | 5 | 56 | 0x6B |
| 17 | 10000 | 12000 | 5 | 20 | 0x6B |
| 18 | 12000 | 15000 | 5 | 24 | 0x6B |
| 19 | 15000 | 17000 | 6 | 26 | 0x6C |
| 20 | 17000 | 19000 | 6 | 25 | 0x6B |
| 21 | 19000 | 22000 | 6 | 30 | 0x6C |
| 22 | 1000 | 3000 | 7 | 40 | 0x6D |
| 23 | 3000 | 5000 | 7 | 25 | 0x6D |
| 24 | 5000 | 7000 | 7 | 30 | 0x6E |
| 25 | 1000 | 1100 | 0 | 30 | 0x60 |
Category table at DS:0x9A43 (file 0x18233; the preceding byte at 0x18232 is not a creature entry): groups creature types into 8 categories (0-7), 3 types per category. Speed values at DS:0xA1A4 (file 0x18994). Sprite assignments at DS:0xA16E (file 0x1895E): graphic_id maps to ALIEN PL5 sheets, and frame_index selects the animation variant in the 10x5 32x40 frame grid. The active viewport uses both fields.
DS segment base = 0x0E3F (file offset = 0xE7F0 + DS_offset).
The 32-color VGA palette is set via INT handler at 0x9F2, loading from
DS=0x0E3F:0x7DE (file offset 0xEFCE). Each entry is 3 bytes of 6-bit VGA
values (0-63). The palette has been verified to match the existing
pl5_default_palette in the codebase. Notable entries:
- Colors 0-5: black to gray ramp (monochrome shading)
- Colors 6-7: green tones (vegetation, status)
- Colors 8-9: red tones (damage, alerts)
- Colors 10-15: yellow, pink, magenta, orange (effects, UI)
- Color 16: black (duplicate of 0, used as separate layer)
- Color 17: white
- Colors 19-23: brown ramp (wood, earth textures)
- Color 24: bright red
- Colors 27-29: blue ramp (sky, water, energy)
- Colors 30-31: green tones (different from 6-7)
The following blit rectangles were recovered from the VGA copy routines in
CAPPO.EXE. Each is defined by a linear VGA offset (mov di), line count
(mov bp), and bytes-per-line (mov cx or rep movsw count):
| File offset | Screen (x,y) | Height | Description |
|---|---|---|---|
| 0x485F | (32, 55) | 112 | 3D viewport (144×112, confirmed) |
| 0x4839 | (32, 170) | 28 | Message/text area below viewport |
| 0x0615 | (28, 3) | 32 | Top-left panel (droid status) |
| 0x0DCA | (220, 4) | 37 | Top-right panel |
| 0x3AD8 | (190, 15) | 30 | Right status panel |
| 0xD41D | (210, 157) | 56 | Bottom-right panel |
| 0xDC92 | (80, 120) | 49 | Center-bottom panel |
| 0xDD47 | (0, 119) | 99 | Left panel (inventory) |
| 0x1F48 | (293, 76) | 99 | Right edge panel |
The viewport blit at 0x485F copies 72 words (144 bytes) per line with a source skip of 16 bytes and destination stride of 176 (320−144), confirming the viewport fills exactly 144×112 pixels.
Captive's source-backed path currently accepts navigation-screen selection and
renders only authenticated CAPPO media or a caller-supplied DOSBox-X memory
image. It does not forward dungeon keys into the provisional GameState,
because doing so would change synthetic state behind an unchanged original
VGA frame. A live CAPPO input transport is therefore still required for
movement, rotation, interaction, inventory, terminal and save parity.
The F10 menu is not evidence of original-game parity. Its compatibility cheats remain available only to isolated non-Captive/test paths; the source-backed Captive path never applies them to generated state.
Recovered from CAPPO.EXE at 0x9987–0x9AA7. The spawn system places creatures into dungeon cells based on creature type, party direction, and cell position.
- Difficulty is read from the spawn record
[di+1], capped at 8 - Category index selects creature type via
spawn_select_type() - The recovered selector validates the candidate type against the difficulty offset and category data; the complete caller/selection contract is still under review. OpenCaptive's uniform three-entry category selection is therefore a compatibility behavior, not a claimed source-parity result.
- HP is computed:
base = min + (range * difficulty / 8); hp = (base * 2 * modifier >> 8) + 6; cap 255
| Type range | Placement | Count |
|---|---|---|
| < 0x0A | Single place()
|
1 |
| 0x0A–0x0B | Single place_flagged() (subcell OR 0x20) |
1 |
| 0x0C | Two place_flagged() with increment |
2 |
| 0x0D–0x0E | Three place() with increments |
3 |
| 0x0F | One place(); following calls derive its direction/subcell |
1 |
| 0x15 |
place() + place_flagged()
|
2 |
| other | Single place()
|
1 |
Direction-based subcell lookup uses two 16-byte tables at DS:0x9BD8 and DS:0x9BE8:
- Direction 0–1:
table1[(direction << 2) + (position & 3)] - Direction 2–3:
table2[position]
Table 1 (DS:0x9BD8): 00 06 08 02 02 00 06 08 06 08 02 00 08 02 00 06
Table 2 (DS:0x9BE8): 01 03 07 05 00 01 03 04 01 02 04 05 03 04 06 07
The placement writer at 0x53D1 does not convert this value into a second
map-cell coordinate. It packs the low direction bits and the selected
subcell into the creature record's byte at offset +5; the record is eight
bytes wide (+0 cell pointer/index, +2 source type, +4 count/flags,
+5 packed placement, +6 HP/state word). The current compatibility runtime
stores only cell coordinates, so applying subcell as an x/y offset would
be incorrect. A future parity implementation must decode this packed record
at the viewport/compositor boundary.
- Opposite:
direction XOR 2 - Perpendicular:
NOT direction AND 1
The verified CAPPO image contains a 25-byte mapping indexed by the runtime
creature candidate. It starts at file offset 0x18232 (DS:0x9A42) and is
not an 8×3 category-to-creature table:
0D 00 00 00 01 01 01 02 02 02 03 03 03 04 04 04 05 05 05 06 06 06 07 07
The preceding 0D is part of the same indexed table. The later runtime
selection/retry path around 0x97B3 consumes this mapping together with the
spawn record and the difficulty-offset table. OpenCaptive's reverse grouped
spawn_categories array remains a compatibility representation until that
caller contract is recovered; it must not be described as the original table.
The verified v1.06 CAPPO image contains 25 contiguous modifier bytes, indexed
by the 1-based creature type used by the runtime (entry 0 is reserved):
2, 4, 12, 30, 23, 60, 110, 90, 16, 18, 16, 12, 10, 12, 4, 0, 4, 8, 20, 8, 4, 2, 12, 4, 6.
The first 16 values are the original table commonly transcribed in older
notes; the remaining nine values are part of the same source data region and
are required for creature types 16–24. The OpenCaptive table is hash-checked
against the unpacked CAPPO image (fa7d5ca7…).
16 entries: 7, 0, 8, 16, 0, 8, 16, 0, 8, 16, 0, 8, 16, 0, 8, 16
Recovered from CAPPO.EXE at 0x3933–0x3FB3 and related functions.
The map buffer lives at segment 0x1BAA. Each cell is 5 bytes of wall/floor bitmask flags. The map is 10 cells wide × 56 rows tall, with a row stride of 10×5 + 150 = 200 bytes. The buffer is initialized (zeroed) at 0x3EB4.
MapGen uses the DOS variant PRNG at 0x3D54: state = state * 0x5E5 + 0x29
(no ROR, no XOR). State stored at [0x12DE]. This is distinct from the main
game PRNG at 0x8E78 (which uses ROR 3 + XOR 0x800).
Generates 32-bit bitmask patterns from PRNG output, rotated by varying amounts (low nibble, high nibble of PRNG output). Creates 8 pattern rows stored at segment 0x1BAA:0x4600+.
Reads two pattern bytes per cell and applies wall connectivity via 8-bit bitmask tests. Each of 8 bit positions controls a wall segment direction.
4 cellular automaton rule sets selected by cl:
| cl | Address | Description |
|---|---|---|
| 0 | 0x3AAA | Maze — wall connectivity propagation |
| 1 | 0x3B67 | Rooms — open area generation |
| 2 | 0x3C21 | Open — wide corridor layout |
| 3+ | 0x39CC | Mixed — combined wall/room rules |
4 rectangular sub-region templates at DS:0x5CD6, 0x5CE4, 0x5CF2, 0x5D00
(14 bytes each). Applied after pattern generation via 0x3981. Each template
has a random style byte (PRNG & 0xC0) + 0x0C.
After map generation loop, generators are placed:
- Count =
(PRNG & 7) + 1(1–8 generators per level) - Position: random x (0–111, capped at 63), random width (1–8)
- Marker byte
0x1Awritten to map buffer cells - Level counter
[0x8D7F]decremented per level
Each 5-byte cell maps to an 8-pixel-wide column in the viewport. The renderer writes to VGA offsets [di+3] through [di+7], with row stride 0xA0 (160 bytes).
| Byte | Bit | Wall segment |
|---|---|---|
| 0 | 0x10 | Right wall (N/S) |
| 1 | 0x10 | Right-center |
| 2 | 0x08 | Center (door position) |
| 3 | 0x80 | Left-center (ornament) |
| 4 | 0x10 | Left wall |
CA rule output values control wall thickness:
-
0x10: standard 1-pixel wall -
0x18: wide 2-pixel wall -
0x80: perpendicular cross wall -
0xC0: thick 3-pixel wall
Post-mapgen feature placement calls:
- 0x1D1C — position calculation
- 0x2025 — processing stage
- 0x33D7 — feature placement (called 2+ times with different parameters)
- Loop over 8-byte structures at DS:0x5B82–0x5CB2 (6 entries)
BuildingGen is an Amiga HUNK executable on Liberation Disk 3:
- HUNK_CODE: 23,252 bytes (one hunk)
- HUNK_BSS: 3,956 bytes
- HUNK_RELOC32 for code→BSS fixups
Same formula as Captive MapGen:
state = (state * 0x5E5 + 0x29) & 0xFFFF
State stored at a5+0x20.
Computed from seed and level number:
| Parameter | Formula | Range |
|---|---|---|
| density | level × 5 + 15 + (PRNG_roll & 7) | 15–100 |
| num_columns | (level >> 4) + 6 + (seed_roll & 3) − 2 | 4–15 |
| num_roads | 2 + (seed_ror & 3) − (level >> 3); min 1; if level=0 then 5 | 1–5 |
| num_cross_roads | (seed_ror & 1) + (level >> 3) + 1 | 1–5 |
Derived: road_buildings = cross_roads/2, side_buildings = min(roads×2, 5), buildings_per_segment = columns/2.
| Offset | Size | Field |
|---|---|---|
| 0 | 1 | Building type (0–8, PRNG % 9) |
| 1 | 1 | Sequential ID |
| 2 | 1 | Name seed |
| 3 | 1 | Flags: bits 0–1 = connection count, bits 2–5 = category<<2, bit 6 = dead end, bit 7 = disabled |
| 4–5 | 2 | Link word |
| 6–7 | 2 | Connection 0 marker (0xAAAA=forward, 0xBBBB=backward) |
| 10–11 | 2 | Connection 0 target building index |
| 12–19 | 8 | Connection 1 (same layout) |
| 20–27 | 8 | Connection 2 (same layout) |
| Index | Type | Name source |
|---|---|---|
| 0 | Shop | last_name + shop_type (9 types) |
| 1 | Bar | bar_type (12 types) |
| 2 | Business | last_name + business_type (11 types) |
| 3 | Industrial | industrial_type (12 types) |
| 4 | Residence | "Private Residence" |
| 5 | Library | "Library" |
| 6 | Police | "Police Station" |
| 7 | Records | "City Records Office" |
| 8 | Special | first_name + last_name |
German syllable pairs (32 syllables: gold, grun, braun, wein, eisen, …, platz) concatenated, capitalized, followed by Greek letter suffix based on level (Alpha, Beta, Gamma, Delta, Epsilon, Zeta, Eta, Theta, Kappa).
| Offset | Content |
|---|---|
| 0x2EE8 | Newspaper names ($-delimited, 16 entries) |
| 0x2F6F | TV station names ($-delimited, 9 entries) |
| 0x2FD7 | NPC titles ($-delimited, 8 entries) |
| 0x302F | Greek letter suffixes (null-terminated, 9 entries) |
| 0x1A09 | Character set (62 chars + $) |
| 0x288A | Building type names (Library, Police Station, Private Residence, City Records Office) |
CityGen is a separate Amiga HUNK executable on Liberation Disk 3:
- File: CityGen (10,896 bytes file, 10,824 bytes code, 4,888 bytes BSS)
- Version: "CityGen 1.12 (CaptiveII : Monday 03-Jan-94 02:17:04)"
-
PRNG:
state = state * 0x5E5 + 0x29(identical to BuildingGen and Captive MapGen)
The city is a 64×64 grid with 3 planes (12,288 bytes total):
| Plane | Purpose |
|---|---|
| 0 | Cell type (wall=0x00, road=0x0D, border=0xFF, building types) |
| 1 | Road/feature ID |
| 2 | Building ID (bit 7 = origin marker) |
Before the 64×64 grid is generated, an 8×8 meta-grid is constructed. Each meta-cell stores a 4-bit direction bitmask:
| Bit | Direction | dx | dy | Grid offset |
|---|---|---|---|---|
| 0 | North | 0 | -1 | -64 |
| 1 | East | +1 | 0 | +1 |
| 2 | South | 0 | +1 | +64 |
| 3 | West | -1 | 0 | -1 |
Four entry points connect roads from the grid edges:
| Direction | Corner (x,y) |
|---|---|
| North | (3, 0) |
| East | (6, 3) |
| South | (3, 6) |
| West | (0, 3) |
Road availability is determined by seed_lo via a 36-byte lookup table at 0x2694.
The generation is gated by difficulty level (0x2C8 initializes to 127):
| Phase | Difficulty | Subroutine | Description |
|---|---|---|---|
| Count roads | always | 0x1B40 | Count available road directions from seed |
| Generate roads | always | 0x1CE8 | Walk roads on meta-grid with PRNG-biased turns |
| Extra connections | always | 0x1B6C | Connect isolated road segments |
| Expand to grid | always | 0x1DE4 | 8×8 meta → 64×64 using 4×4 tile templates |
| Set borders | ≥ 0 | 0x1BE8 | Mark edges as walls (0xFF), save to plane1 |
| Place features | ≥ 1 | 0x1F1A | Feature placement with retry |
| Place feature blocks | ≥ 2 | 0x2E4 | Template B blocks (7 cells) near roads |
| Place road blocks | ≥ 3 | 0x330 | Template A blocks (6 cells) near roads |
| Building shapes | ≥ 3 | 0x7D2+ | Various building/structure subroutines |
| Advanced features | ≥ 4 | 0xA80+ | Higher-difficulty features |
| Finalize | always | 0x24B8 | Grid post-processing |
Two template sets are used for building placement:
Template A (0x28B4): 4 rotations × 6 cells + 2 adjacency checks = 16 bytes per entry. Represents a 2×3 building footprint.
Template B (0x28F8): 4 rotations × 7 cells + 2 adjacency checks = 20 bytes per entry. Represents a 3×3 building footprint.
Placement requires all template cells to be empty (0x00) and at least one adjacency cell to contain a road-type value (types 18-21 after masking with 0x3F).
13 tile templates of 4×4 bytes each control how meta-grid cells expand to the 64×64 grid. The template index is derived from the meta-cell's direction bitmask value.
| Offset | Size | Description |
|---|---|---|
| 0x2694 | 36 bytes | Road availability per seed (4 bytes × 9 entries) |
| 0x26B8 | 8 bytes | Road corner positions (x,y pairs × 4) |
| 0x26C0 | 16 bytes | Road direction deltas (dx,dy words × 4) |
| 0x2830 | 16 bytes | Grid direction table (dx,dy,offset × 4) |
| 0x2890 | 9 bytes | Road count per seed level |
| 0x2899 | 9 bytes | Block count per seed level |
| 0x28B4 | 68 bytes | Block template A (4 × 16 bytes + header) |
| 0x28F8 | 84 bytes | Block template B (4 × 20 bytes + header) |
| 0x294C | 238 bytes | BLOC structure (tile/building data) |
| 0x2958 | 208 bytes | 13 tile templates (4×4 bytes each) |
| 0x2478 | 18 bytes | PRNG subroutine |