Time Commando and the Adeline libraries
June 4, 2026 · View on GitHub
A field guide to Time Commando (Adeline Software International, 1996) as seen through this repo's LBA2 tooling. Time Commando shipped a year before LBA2 and shares the same engine lineage, so most of its on-disk formats are the same Adeline libraries we already have source for here. Reading its data is a clean way to see how those libraries worked before LBA2 layered its own game on top.
Nothing here is committed by default — the explorer scripts live in
scripts/dev/ and the extracted samples under .tc-scratch/ (gitignored / untracked).
What's shared, what's not
| Layer | Time Commando (1996) | LBA2 (1997) | Our code |
|---|---|---|---|
| Resource container | HQR | HQR | LIB386/SYSTEM/HQFILE.CPP, identical |
| Compression | stored / LZSS / LZMIT | same | LIB386/SYSTEM/LZ.CPP ExpandLZ |
| Cinematics | XCF (Adeline tile codec) | Smacker (libsmacker) | SOURCES/DEC_XCF.* (XCF) vs SOURCES/PLAYACF.CPP (Smacker) |
| 3D / world | Adeline 3D libs | Adeline 3D libs (LIB386/3D) | shared lineage |
The headline finding: SOURCES/PLAYACF.CPP does not decode Time Commando's
.ACF files. By LBA2 the cinematic pipeline had been swapped to Smacker
(smk_open_memory), and "PlayAcf" is just a legacy name. The original Adeline
cinematic decoder is still in the tree though — SOURCES/DEC_XCF.{ASM,CPP} and
SOURCES/DEC.{ASM,CPP} — unused by the shipping LBA2 binary, but exactly what
plays Time Commando's videos.
How the disc is packaged (GOG)
The GOG release ships the CD as a raw image plus a cue sheet wearing a .DAT
extension:
GAME.GOG— rawMODE1/2352image (2352 bytes/sector: 12 sync + 4 header + 2048 user data + 288 EDC/ECC). 260383 sectors.GAME.DAT— a CUE: oneMODE1/2352data track (the ISO9660 filesystem) plus twoAUDIOtracks (Red Book CD music).RESSOURC.HQR,TIMECO.EXE, etc. are also dropped loose in the install dir.
scripts/dev/iso_bin.py reads the ISO9660 fs straight out of the 2352-byte
sectors (logical block lba → byte offset lba*2352 + 16, take 2048 bytes), so
there's no need to convert to a plain .iso first. (Same reader works on any
Adeline CD dump — LBA1 LBA.DOT, LBA2 LBA2.GOG, TC GAME.GOG.)
python3 scripts/dev/iso_bin.py GAME.GOG --tree
python3 scripts/dev/iso_bin.py GAME.GOG --extract /STAGE00/RUN0/SCENE.HQR out.hqr
Disc layout (volume TIMECOMM):
/RESSOURC.HQR shared resources (palettes, fonts, texture pages)
/CREDITS.HQR
/SEQUENCE/*.ACF cutscenes; BIG*=hi-res, LOW*=lo-res variants
/STAGE00../STAGE0A/ 12 time-period levels
RUN0/ RUN1/ "runs" through the level (forward / back)
SCENE.HQR level data (models, collision, scripts)
SCENE.ACF the pre-rendered scrolling background animation
That SCENE.HQR + SCENE.ACF pairing is the core of the game: a real-time 3D
character is composited over a pre-rendered animated background. The background
leaves a hole for the playfield — see Recouvre below.
HQR — the resource container
Byte-identical to LBA's. scripts/dev/hqr_inspect.py (a port of HQFILE.CPP +
LZ.CPP) reads every Time Commando HQR with no changes:
- File starts with a
U32offset table;table[0]/4= slot count; each slot is a byte offset to that entry's header (0 = empty). - Entry header:
{ u32 SizeFile; u32 CompressedSizeFile; s16 CompressMethod }, then data. Method0= stored,1= LZSS,2= LZMIT. - LZSS/LZMIT are the same
ExpandLZscheme: a flag byte whose 8 bits select literal-vs-match; matches are(length = low nibble + method + 1, offset = (hi<<4)|(lo>>4))back-references. Decompressing all ofRESSOURC.HQR(114 entries),SCENE.HQR, andCREDITS.HQRyields exact sizes, zero failures.
XCF — the cinematic codec (.ACF files)
Internal signature, literally in the bytes: XCF File Format / Adeline Software. The container is a flat list of chunks; each is
char tag[8] // ASCII, space-padded
u32 size // little-endian payload length
u8 data[size]
contiguous (next tag immediately follows the payload). scripts/dev/acf_inspect.py
walks them. Tags seen:
| tag | meaning |
|---|---|
FrameLen | u32 frameCount, u32 biggestFrame, then a per-frame duration byte each |
Format | the FORMAT struct below |
| `Palette $ | 768-\text{byte} \text{VGA} \text{palette} (256 \times \text{RGB}, 0–255) |
| $KeyFrame` | intra-coded frame |
DltFrame | delta (inter-coded) frame |
Camera | 32 bytes: x,y,z, target_x,y,z, roll, fov (the scripted rail camera) |
Recouvre | overlay-mask region — where the live 3D playfield is punched in |
SoundBuf | audio pre-roll buffer |
SoundFrm | per-frame audio |
SoundEnd / End | terminators |
NulChunk | padding to align the next frame chunk |
FORMAT (SOURCES/DEC_XCF.H): struct_size, DeltaX(width), DeltaY(height), FrameSize, KeySize, KeyRate, PlayRate, SamplingRate, SampleType, SampleFlags, Compressor (0=ACF, 1=XCF). Observed: 320×200 or 320×240, 15 fps. Audio is
22050 Hz, 8-bit unsigned, stereo (SampleType=2); 2940 B/frame × 15 fps = 44100 B/s = 22050 × 2ch.
The frame codec (the interesting part)
A frame is a grid of 8×8 tiles. Each tile gets a 6-bit opcode (0–63) that selects one of 64 decode routines. The frame payload splits into three regions:
payload[0:4] = colour_offset (u32)
payload[4 : 4+(H/8)*30] = opcode stream (6 bits/tile, 4 packed per 3 bytes)
payload[4+(H/8)*30 : co] = "aligned" stream
payload[colour_offset:] = "unaligned" stream
(H/8)*30 because each tile-row is W/8 = 40 tiles × 6 bits = 240 bits = 30
bytes. DEC.H derives the same number: Dec_CodeLen = (H/8)*(320/8)*6/8 + 4.
The two data streams interleave per opcode — aligned carries bitmasks and
packed palette indices, unaligned carries literal colour bytes and motion
vectors. Opcodes are read 4 at a time: op = (read3 | 0xff000000), take
op & 63, op >>= 6; the forced top byte makes op == -1 after exactly four
tiles, signalling the next 3-byte read.
The 64 opcodes (from the original TabFunc jump table in DEC_XCF.ASM) fall
into families. Most motion/fill ops come in four flavours: plain, then +update4 / +update8 / +update16 residual passes that repaint a few pixels on top.
| opcodes | routine | what it does |
|---|---|---|
| 0 | Raw | 64 literal palette bytes straight into the tile |
| 1–4 | ZeroMotion | copy tile from previous frame, same position |
| 5–8 | `SMotion8$ | \text{short} \text{motion}, \text{one} \text{signed} 4+4 \text{nibble} \text{vector}, 8 \times 8 |
| 9–12 | \text{absolute} \text{offset} \text{into} \text{previous} \text{frame}, 8 \times 8 | |
| 13–16 | \text{short} \text{motion}, \text{four} 4 \times 4 \text{sub}-\text{blocks} | |
| 17–20 | \text{absolute} \text{motion}, \text{four} 4 \times 4 \text{sub}-\text{blocks} | |
| 21–24 | $SingleFill` | one colour over the whole tile |
| 25–28 | `FourFill$ | \text{one} \text{colour} \text{per} 4 \times 4 \text{quadrant} |
| 29–32 | $Bit1..Bit4` | 1/2/3/4-bit packed indices into a small per-tile palette |
| 33–35 | `Split1..3$ | \text{same} \text{idea}, \text{but} \text{per} 4 \times 4 \text{sub}-\text{tile} |
| 36 | 4 \text{base} \text{colours} + \text{a} 4 \times 4 \text{correspondence} \text{map} | |
| 37 | $Prime` | one prime colour + a bitmask of literal exceptions |
| 38–39 | Bank1/Bank2 | 4-bit / 5-bit indices relative to one or two colour banks |
| 40–43 | Block | run-fill in horizontal / vertical / two diagonal scan orders |
| 44–47 | BlockBank1 | same runs, nibble-packed within a bank |
| 48–51 | `ROMotion8$ | \text{relative}-\text{offset} \text{motion} (\text{signed} 16-\text{bit}), 8 \times 8 |
| 52–55 | \text{relative} \text{motion} \text{with} \text{separate} \text{dx}/\text{dy} \text{bytes}, 8 \times 8 | |
| 56–63 | \text{the} 4 \times 4 \text{versions} |
$update4reads four(x,y)positions (3 packed bytes, 6 bits each) from the unaligned stream and four colours from the aligned stream;update16` reads an
8×8 bitmask and literal pixels. These let a motion-copied tile carry a small
residual without a full re-encode — a hand-rolled equivalent of motion
compensation + sparse residual, in 1996, on a 486.
scripts/dev/acf_decode.py is a Python port of DEC_XCF.CPP (cross-checked
against ../timeco/src/acf.c). It decodes keyframes and delta frames with full
double-buffering:
python3 scripts/dev/acf_decode.py SCENE.ACF --frame 0 --out frame0.png
python3 scripts/dev/acf_decode.py SCENE.ACF --all out_dir/ # every frame
Verified by rendering: scene keyframes (the Adeline boxing-ring intro, a fiery cavern), and an Activision-logo delta frame 45 — reconstructed cleanly from the keyframe through 44 motion-compensated deltas, no drift.
Recouvre — the playfield hole
Recouvre ("covers/overlay" in French) marks the region the pre-rendered
background leaves empty so the engine can composite the real-time 3D character.
In a decoded scene frame it shows up as a black, white-rimmed cut-out. This is
the architectural heart of the game: animated pre-rendered backdrop + live 3D
actor masked into a window, with a scripted Camera rail driving both.
What this teaches about the Adeline libraries
- One resource layer, reused for years. HQR + the
ExpandLZLZSS/LZMIT pair carried from Time Commando through LBA1/LBA2 unchanged. Learn it once. - Codec built for the hardware, not the format. XCF's split aligned/unaligned streams, 6-bit opcodes packed 4-per-3-bytes, and bank-relative nibble indices are all about cheap 486-era reads and small palettes, not elegance. The 64-way jump table is a per-tile cost model: pick the cheapest representation for each 8×8 block.
- The same studio iterated the pipeline. Time Commando = pre-rendered XCF
backdrops + 3D actor. LBA2 kept the 3D and HQR libraries but moved cutscenes
to licensed Smacker — which is why
DEC_XCFsits unused in this tree whilePLAYACF.CPPcalls libsmacker.
Tools (this repo, scripts/dev/)
| script | purpose |
|---|---|
iso_bin.py | read ISO9660 out of a raw MODE1/2352 CD image (.gog/.bin/.dot) |
hqr_inspect.py | list / decompress HQR entries (LZSS / LZMIT / stored) |
acf_inspect.py | walk the XCF/ACF chunk container |
acf_decode.py | decode XCF key/delta frames to PNG (port of DEC_XCF.CPP) |
References
- Original Adeline source in this repo:
SOURCES/DEC.{ASM,CPP},SOURCES/DEC_XCF.{ASM,CPP,H},LIB386/SYSTEM/HQFILE.CPP,LIB386/SYSTEM/LZ.CPP. - TimeCo reimplementation (LBALab):
../timeco/src/acf.c— clean C port of the decoder. - Defence-Force "Time Commando" articles +
ACF2PCX.cppreverse-engineering notes (linked from../timeco/src/acf.c).