Reverse-engineering notes on The Apprentice (The Vision Factory / Philips Interactive Media International, 1994) — the CD-i's best-regarded action game, and a disc that turns out to be a very different object from the four CD-i titles documented before it.
This repository documents the European pressing: a CD-i Ready disc where the entire game lives inside the pregap of track 1 and the other 74 % of the pressing is 22 tracks of Red Book audio. It ships the linker's symbol table for the shipping build, a 68000 disassembler with an NVRAM switch to turn it on, and a default high-score table naming twenty people where the credits name three.
Documentation only. No game assets, no extracted images, no audio, no
executable code is committed here. The tools in tools/ reproduce
every figure, table and claim in these pages from your own legally obtained
copy.
| Doc | Contents |
|---|---|
| 00-overview.md | The release, the disc, and where 62 minutes of CD go |
| 01-disc-image.md | CD-i Ready, a dump that is entirely scrambled, and two identical filler regions |
| 02-filesystem.md | Green Book descriptor, 93 entries, and the CD-DA attribute bit that was wrong |
| 03-executables.md | Nine OS-9 modules, four applications, and the loader that chains them |
| 04-symbol-table.md | cdi_app.stb — 521 named symbols, shipped by accident |
| 05-dat-containers.md | The .dat format, proved by chaining 45 files to the byte |
| 06-graphics.md | CLUT7 and CLUT8, two line pitches, palettes, tile maps |
| 07-audio-and-music.md | The soundtrack shipped twice, and the 132-byte table that maps one onto the other |
| 08-game-architecture.md | What 521 symbols say the game is |
| 09-leftovers.md | A debugger, a switch to turn it on, twenty names, and five empty maps |
| 10-cross-disc.md | 714 KB byte-identical to another studio's disc, and a third filler recording |
| 11-open-questions.md | What is measured, what is inferred, what did not resolve |
| notes/file-inventory.md | All 93 entries, the volume descriptor, the sector budget |
| notes/symbol-inventory.md | All 521 symbols in address order, by section |
| notes/chunk-inventory.md | All 155 .dat chunks, and the eight that are shared |
| notes/music-map.md | 22 CD-DA tracks against 22 ADPCM channels |
| notes/string-inventory.md | Every readable string in all six modules |
| cdi-platformnotes-doc | Platform checklist — what to look for on any CD-i disc. Shared by every CD-i pipeline; this repo links it rather than forking it |
It is a CD-i Ready disc, and the dump is entirely scrambled.
There is no data track. All 22 tracks in the TOC are AUDIO, and track 1 has a
15:22 pregap — 69,150 sectors — which is where the whole CD-i volume lives,
hidden from any audio CD player that starts at INDEX 01. Every ripper therefore
reads the game as audio and hands back raw channel data: correct 12-byte sync
marks and nothing else recoverable. Grep the image for CD-RTOS and you get
nothing. The fix is one XOR, and the proof is four bytes:
LBA 0 stored 01 82 00 62
want 00 02 00 02 (MSF 00:02:00, mode 2, BCD)
ECMA-130 scrambler
01 80 00 60
xor 01 82 00 62 <-- matches
Seven probes across four orders of magnitude of LBA, seven exact matches.
python tools/cdiready.py extract writes an ordinary Mode 2 image.
The soundtrack ships twice, and the disc carries the table.
22 Red Book tracks (45.85 min, 73.9 % of the pressing) and 22 Level C stereo
ADPCM channels (46.0 min) across three real-time files, one to one. The mapping
is a 132-byte global the symbol file calls music_table, and comparing the two
sides closes:
python tools/cdimusic.py check
21 of 22 durations agree to within 0.25 s
The interleave is timed to the drive: a channel gets one sector in eight at
0.1067 s each, so its span in sectors equals its length as a CD-DA track.
music.rtf channel 3 spans 21,449 sectors and CD-DA track 7 is 21,449 sectors.
Track 15 is the one that does not agree — 66.61 s as CD-DA, 93.44 s as ADPCM, a single record either way. One of the two masters was cut.
The Microware linker's symbol file is on the retail disc.
/CMDS/cdi_app.stb, 10,584 bytes, is an OS-9 Data module holding 521 symbols
with their source-level names, mastered fourteen seconds after the executable
it describes. Four independent checks settle the address model:
btext -> 0x00000000 bname -> 0x00000048 = M$Name
_cstart -> 0x00000050 etext -> 0x00005ac4 = M$Size, 23,236
and one further constant, file_offset = addr + 48296, makes the initialised
value of every global readable — which is how the next two findings were
recovered.
A 68000 disassembler shipped, with a switch on the player to turn it on.
line_disassemble is 3,434 bytes — 14.8 % of the game module — and it is real:
the full mnemonic table (ADD AND BCD CHG CHK CLR … TRAPV TST UNLK) and the
condition-code table (T F HILSCCCSNEEQVCVSPLMIGELTGTLE) are both in the
binary, next to a register dumper and thirteen named exception vectors. And:
/nvr/CATCHER_ON
"catcher %sinstalled\r"
"Exception Catcher %s " with %s = "0.52"
/nvr is the player's non-volatile RAM. Create a file called CATCHER_ON
there and the retail game installs its debugger. One of the exception names is
Privilage violation.
Twenty names in the default high-score table, where the credits name three.
nvr_buffer is the factory contents of the save file, 244 bytes, twenty records
of rank + score + six-character name, scores descending by exactly 10,000:
01 200,000 LUKE 06 150,000 JOOST 11 100,000 DENNIS
02 190,000 TIM 07 140,000 JORIS 12 90,000 OLU
03 180,000 ARJEN 08 130,000 JOOP 13 80,000 SIBBY
04 170,000 STEFAN 09 120,000 JSB 14 70,000 DANION
05 160,000 LUC 10 110,000 ROB ... ... SAM MARC DAVE ELLIS RIK ERIC
The volume descriptor's data-preparer field reads Tim, Luke, Luc and Arjen — four names, and four of the top five scores. Sixth is Joost Egmond,
the credited composer. The credit roll names three people; this table names
twenty, and fifteen of them appear nowhere else on the disc.
714 KB of the disc is byte-identical to Link: The Faces of Evil.
/CMDS/philips.rtf and Link's bumper stream are the same file — 121 RL7 video
sectors, 149 Level B stereo audio sectors, 40 DYUV sectors, all identical,
across two studios, two continents and eighteen months. Only the one descriptor
sector differs, and only in the 37 bytes after its first 2,048. The DYUV picture
is 92,160 bytes = 384 × 240 exactly, and the container's magic is
0xBABEFACE with the field names cluts, count, filenum, frame_sizes.
A third filler recording, and the first disc to write it twice.
The 2,250 sectors in front of the file system are the standard CD-i filler
block, with the standard 00 00 20 00 subheader and zeroed EDC — but the audio
is neither the 1993–95 recording shared by Link, Origami and Merlin nor
Soccer's 1997 one. And it is written a second time behind the volume:
head 18-2267 5,229,000 B md5 4e61f608e1f1455d9ad5b2a0615dbbd3
tail 57868-60117 5,229,000 B md5 4e61f608e1f1455d9ad5b2a0615dbbd3
head == tail: True
Because this dump was never descrambled, the filler audio reads in the clear — which confirms from the opposite side the mechanism the other pipelines had to infer, and the boundary-jump test still gives the same 28 bytes destroyed per sector.
The .dat container closes 45 times out of 45.
A 2,048-byte directory — u16 count, then one u32 size per chunk — followed
by chunks each padded to a sector boundary. Chain them and the last one ends
exactly on the file size, on every container from 106 KB to 776 KB:
python tools/cdidat.py check
45 containers close exactly, 0 do not, 8 are not containers
Chunk 0 of every level and screen file is a BRA.W jump table — 68000 code
overlays, which is why the game module is only 23,236 bytes. All eighteen map
files share one 768-byte palette; all six level files share one 6,720-byte
320 × 21 status panel.
Five empty maps for a seventh level. map7_1.dat–map7_5.dat, written at
14:56 on the last day, 16 KB each where every other map is 116 KB, 82 % empty,
with no level7.dat, no intro7.dat and no l7_em.dat — and five of them
where every other level has three. The binary names them.
And the disc is otherwise immaculate. No all-zero files at 19.9 % capacity,
no abandoned localisation, no unreferenced assets — twenty filename templates in
the binaries expand to exactly the 56 .dat and .rtf files on the volume,
nothing left over in either direction — no MPEG, and no personal data beyond
twenty first names.
Platform-general, carried between CD-i pipelines:
| tool | what it does |
|---|---|
cdilib.py |
sector, subheader and directory primitives (CD-DA attribute bit corrected) |
cdifs.py |
list / tree / map / extract the Green Book file system |
os9mod.py |
OS-9/68000 module headers, parity and CRC |
cdistrings.py |
filtered strings out of a module |
cdiaudio.py |
Green Book ADPCM, mono and stereo decode |
cdistb.py |
parse a Microware .stb symbol module — stats, list, groups |
Written for this disc:
| tool | what it does |
|---|---|
cdiready.py |
start here — probe, extract, tracks: recover the CD-i data track from a CD-i Ready image |
cdihead.py |
the two filler regions — map, check, hash, stats, wav |
cdidat.py |
the .dat container — list, check, census, extract |
cdirtf.py |
real-time census — census, records, sizes, wav |
cdimusic.py |
music_table — table, and check it against the CD-DA tracks |
cdinvr.py |
the default high-score table — show, cross |
cdiready.py and cdistb.py should work unmodified on any CD-i Ready disc and
any Microware .stb; cdidat.py, cdimusic.py and cdinvr.py carry this
title's offsets and are worth reading rather than running elsewhere.
Point them at your own dump:
chdman extractcd -i "Apprentice The (Europe).chd" -o _work/disc.cue -ob _work/disc.bin
python tools/cdiready.py probe
python tools/cdiready.py extract # -> _work/data.bin
python tools/cdifs.py list
python tools/cdifs.py extract _work/files
python tools/cdistb.py list
python tools/cdidat.py check
python tools/cdimusic.py check
python tools/cdinvr.py cross
python tools/cdihead.py hash
CDI_RAW overrides the path to the raw image and CDI_BIN the descrambled
data track; they default to _work/disc.bin and _work/data.bin.
- cdi-platformnotes-doc — the shared CD-i checklist
- cdi-merlinsapprentice-doc — Merlin's Apprentice, a different game with a confusingly similar name
- cdi-linkthefacesofevil-doc — which shares 714 KB of bytes with this disc
- cdi-origami-doc
- cdi-ultracdisoccer-doc