Skip to content

Liberation Technical

Daniel Nylander edited this page Jul 29, 2026 · 11 revisions

Liberation: Captive II technical notes

Current boundary

OpenCaptive currently verifies and opens the known CD32 data track, reads its ISO9660 filesystem by content hash, and provides a separate city/interior runtime loop. It does not yet reproduce the original CityGen/PlotGen game logic. The verified payloads are preserved as a reverse-engineering boundary, not silently substituted for original behaviour.

CD32 data track

The supported track is a raw MODE1/2352 image containing ISO9660 with CDTV extensions. ISO sectors are 2352 bytes in raw storage; user data starts after the MODE1 framing. The ISO reader validates directory-record extents and file sizes before returning a buffer.

The root-track and all current runtime-analysis resources are selected by their SHA-256 bytes. See Data identity and verification for the manifest.

Resource roles

The verified resource set includes an executable payload, city generator, plot generator, plot text, city text and dialogue text. At present:

  1. liberation_data_open() opens the hash-identified raw track.
  2. Each required resource is looked up by a hash scan of the ISO directory.
  3. Verification fails closed if any required resource is absent or mismatched.
  4. liberation_data_read() exposes byte buffers to future parsers without filename coupling.

No original payload is bundled or emitted by tooling.

liberation_inventory is the discovery entry point for every remaining CD32 resource. It recursively walks the verified ISO and emits only SHA-256 digest, byte length and container class (IFF/ILBM, IFF/ANIM, RNC1, Amiga-HUNK, AMOS sprite/icon bank or raw). It intentionally neither displays nor accepts filenames:

./build/liberation_inventory /path/to/media

Any future graphics decoder must record the selected digest in code and tests before it is wired into the renderer.

Sprite-bank observation log

The hash-identified resource 07cca53c7efaac9e2880d50524039b0f9cb2a403e0cfbfb0b5f6ce408594d2d1 has the AmSp signature and a 42-entry declaration. Its initial eight entries are conventional four-colour-bitplane images followed by a fifth, one-bit transparency mask plane. The observed record size is therefore 10 + words × height × (depth + 1) × 2; this makes every boundary through the eighth entry exact. amos_sprite_dump decodes this verified, unflagged prefix to a PPM inspection image using a resource hash and an entry index.

The high bit of the width word is not compression. The original 68000 loader doubles the low 15 bits and subtracts one when that bit is set, so it represents an odd number of bytes per row. The same planar-plus-mask decoder handles both even and odd row widths. Records are word aligned: an odd total payload is followed by one padding byte before the next header. The full declared 42-entry stream, including the final entry, is now structurally decodable. The decoder remains an analysis tool until the runtime maps particular sprite hashes to particular game entities.

The loader also tests the X-hotspot sign bit. A negative X hotspot adds one transparency-mask plane after the colour planes; a non-negative hotspot leaves the image unmasked. Both variants are now accepted by the decoder.

The separately hash-identified resource d6bb0dd9c578beb8e84ddf9f458f0be43ec158b2b261491d023e972d2812c2d2 contains one unmasked 320×109 AMOS scene. It remains available to the analysis tools only. The runtime does not place it into an inferred city or building, because that would falsely assign original pixels to an unverified game state.

RNC method 2 in the verified CD32 data is the old Amiga backwards stream: it has a 12-byte header, reads compressed bytes from the packed end and writes the decoded bytes backwards. OpenCaptive verifies its output byte-for-byte against a separately decoded local reference before using it for container analysis.

Verified FORM/ANIM city raster

The presentation bundle is selected from the verified ISO by SHA-256 1d3a335d254c0eae919a712dd73bd41b24ed897bf145ed118ccf2277baa7a35f. Its byte offset 386824 is an old-RNC2 stream which expands to a FORM/ANIM container. The first PACK record declares a 320×167, six-bitplane image (40 × 167 × 6 = 40,080 bytes) and is paired with the container's 32-colour PALL palette. Runtime selection is exclusively the bundle digest plus this offset; the ISO member name is neither queried nor used as an identity.

liberation_anim_decode_first_frame() handles this verified record and liberation_anim_blit() renders planar pixels without interpolation. The native capture's 320×167 city region has been directly compared against an independent decoding of the same source bytes: both RGB buffers have SHA-256 c546bebd107928a5721cd1a33d7e458098b134d4cd86c48b6547f8b615abbdae. This proves that particular original raster is rendered exactly. It does not yet prove parity for animated overlays, the in-game HUD, city state, or plot logic.

The PACK stream is not a sequence of independently decodable full frames. The verified player first expands one bounded work area: in the city resource that area is 40,094 bytes (14-byte descriptor plus 40,080 planar bytes). The following bytes in the PACK chunk are not yet assigned a standalone record meaning. liberation_pack_decode_workspace() exposes only the bounded area; the stricter full-stream diagnostic intentionally reports the trailing format data as undecoded. This keeps the exact first raster available without misrepresenting unknown animation layers as image data.

The accompanying SCPT IFF chunk is now copied verbatim into LiberationAnimScript after the same FORM boundary checks. Both the intro and city presentation report decoded/SCPT through --verify-data liberation. This makes the original scene bytecode available to the runtime without pretending that its instruction semantics have already been recovered.

The verified CD32 player traverses a script by reading a big-endian 16-bit record size at each record boundary, advancing by that full size, and stopping at a zero size. OpenCaptive validates this first sequence and exposes its records through liberation_anim_script_record_at(). All 21 decoded presentation resources terminate cleanly under that rule. This proves the container-level record boundaries only: the payload opcodes, timing units and draw operations are still deliberately opaque.

One payload field is also directly evidenced by the player: byte 2 of a record is multiplied by the current timing base, shifted right by four and added to the next update countdown. It is exposed as timing_multiplier; the absolute clock unit and all remaining payload fields are not yet assigned semantics.

CityGen observation log

The city-generator payload selected by e54540c3bf8dfaf569380a135ac039f1438e9efb85cf6d5e3e487e25d4c7c13e is 10,896 bytes and is recognised as an AmigaOS loadseg() executable. Its embedded release string identifies it as CityGen 1.12, built 1994-01-03.

It uses the Amiga HUNK container layout. Initial disassembly shows an exported entry path that receives a caller-owned parameter block, clears a 12,288-byte work area and records 64×64 dimensions in its output state before invoking its generation routines. These are observations from the verified bytes, not a claim that OpenCaptive already reproduces CityGen output.

amiga_hunk_parse() now validates the HUNK header, allocation table, code, data, BSS, RELOC32, symbol and end records without loading or executing the payload. It is deliberately a structural parser: relocations are counted and bounds-checked, but no original instruction stream is interpreted as gameplay logic.

Against the verified CityGen digest, the parser reports two hunks, one code block of 10,824 bytes at byte offset 36, one BSS block and one RELOC32 entry. The structural inspection command is hash-driven:

./build/liberation_hunk_info /path/to/media \
  e54540c3bf8dfaf569380a135ac039f1438e9efb85cf6d5e3e487e25d4c7c13e

The verified PlotGen digest bc9c922801661eb66024d0bcf822c03e38ffea7f3576693e0512692ccf6d6705 has the same two-hunk shape, with one 12,388-byte code block at offset 36, one BSS block and three RELOC32 entries. This confirms that CityGen and PlotGen are separate relocatable executable payloads, not data tables that can be substituted with the current procedural interior generator.

Text-resource observation log

The hash-identified city-text payload 99f7bd75794a7b4f3e94eeef9c61b756da938d862bb83339b140c18d02eb79c5 is 17,809 bytes. The hash-identified dialogue payload e154d250c1acdbed66835bb356a699efdb6f9f8b5e6d586ca07080414610a94c is 14,136 bytes. Both contain readable English prose interleaved with compact control bytes and expression-like tokens, so neither is a flat NUL-terminated string table.

The city-text payload has a readable location-description section whose records begin with a numeric selector followed by prose and branch comments. The surrounding control stream contains conditional expressions and variable references. The dialogue payload similarly contains response alternatives, quoted text and scripted branches. This is enough to establish that text is data-driven, but not enough to assign opcode semantics or wire original plot decisions into the runtime. A future parser must retain byte offsets and control boundaries, rather than stripping printable strings and treating them as independent records.

liberation_extract is the supported inspection entry point:

./build/liberation_extract /path/to/media \
  e54540c3bf8dfaf569380a135ac039f1438e9efb85cf6d5e3e487e25d4c7c13e \
  /tmp/citygen.bin

The command first verifies the enclosing CD32 track, then performs an ISO lookup by the resource digest. Its output must stay outside version control.

Current city runtime

The current LibState is deliberately separate from Captive's dungeon and mission loop. It contains a 32×32 city grid, building footprints, up to four interior floors, a city/building mode and player coordinates. Its deterministic generator uses a seed, produces streets and building blocks, and supports entering a building, walking interiors and using elevators.

This separation matters: Captive combat ticks and Captive generator-completion rules must not advance or end a Liberation session. The main loop explicitly branches before applying either Captive-only rule. The former inferred city and interior rasterizers have been removed; no procedural drawing is used as a fallback when original presentation data is available.

Reverse-engineering plan

The next parity work is data-driven rather than visual:

  1. Decode executable container/relocation records without distributing code.
  2. Identify CityGen inputs, PRNG, output layout and persistent city state.
  3. Identify PlotGen’s building/interior format and plot progression state.
  4. Decode text table offsets, encoding and dialogue references.
  5. Add small independently testable parsers, golden hashes and structural invariants before wiring them into the live city loop.

Until those steps are complete, do not describe the procedural city as a faithful reproduction of Liberation’s original story, population or plot logic.

Clone this wiki locally