Reverse Engineering Process

How we went from a T64 tape image to complete game reconstruction, step by step.

Contents

  1. Source Material
  2. Step 1: Extract PRG Files from T64
  3. Step 2: Stage 1 Decompression (Madsquad)
  4. Step 3: Code Relocation
  5. Step 4: Stage 2 Decompression (LZ77)
  6. Step 5: Validation with VICE
  7. Step 6: SMART EGG Engine Tracing
  8. Step 7: Text Extraction
  9. Part 2: The Protected Merge
  10. Tools Created

Source Material

FileRIGELREV.T64 (105,620 bytes)
FormatT64 tape container
ContentsTwo PRG files (Part 1 and Part 2), each ~38KB
PlatformCommodore 64 (6502/6510 CPU, 64KB RAM)
AdditionalZX Spectrum .z80 snapshots and .tzx tape images

Step 1: Extract PRG Files from T64

The T64 container format stores one or more C64 programs with metadata. We extracted two PRG files:

Each PRG starts with a 2-byte load address ($01 $08 = $0801) followed by a BASIC stub that uses SYS to jump to machine code.

Step 2: Stage 1 Decompression (Madsquad Cruncher)

Both parts are packed with the "Madsquad/XPLODING" cruncher. We wrote a complete 6502 CPU emulator (decrunch_v2.py) to run the decompressor natively:

477K Part 1 CPU Cycles
3.5M Part 2 CPU Cycles

Part 1 exits via JMP $1900. Part 2 exits via JMP $8B60. The output is a 64KB memory dump representing the C64's RAM after decompression.

Step 3: Code Relocation (Part 1 only)

Part 1's Stage 1 exits to $1900, which sets up a copy routine at $0334:

Copy $2200-$FFFF -> $0800-$E5FF (offset: -$1A00)
Then JMP $0830

This relocates the game code from its packed position to its runtime addresses. The copy offset of -$1A00 means the decompressed data was loaded 6,656 bytes higher than its final runtime location.

Step 4: Stage 2 Decompression (LZ77)

At $0830, there's a second decompressor (LZ77/LZSS variant):

2.6M Part 1 LZ77 Cycles
1.0M Part 2 LZ77 Cycles

Step 5: Validation with VICE

We validated our decompression against a VICE emulator memory dump:

This confirmed our 6502 emulator was producing correct results.

Step 6: SMART EGG Engine Tracing

The text engine was fully traced by disassembling the 6502 code at $880E:

The Discovery

  1. Found the text print loop at $9782 which calls the handler at $880E
  2. Traced the EOR #$FF instruction at $881F (XOR with $FF)
  3. Identified the three dispatch ranges: single char, digram, dictionary
  4. Located the character table at $8604 (96 entries)
  5. Found the digram boundary table at $86D8 and character list at $86CC
  6. Decoded the dictionary at $8684 (16 words)
  7. Found the "SMART EGG!" watermark at $9B2E

Compression Tables Extracted

TableAddressSizePurpose
Character table$860496 entriesMaps indices to PETSCII codes
Dictionary offsets$866316 bytesStart offset of each dictionary word
Dictionary lengths$867316 bytesLength-1 of each word
Dictionary data$8684~70 bytesEncoded word characters
Digram chars$86CC12 bytes12 most frequent characters
Digram bounds$86D812 bytesBucket boundaries for the 12x12 grid

Step 7: Text Extraction

Text is organized in pointer tables:

TableAddressEntriesContent
Room/System$798335Room descriptions, parser responses, system messages
Action/Description$75F1255Narrative text, dialogues, object descriptions
System Messages$99287Error messages (OK, I/O Error, etc.)

Total extraction: 257 non-empty texts, 16,145 decoded characters from 10,279 compressed bytes.

Part 2: The Protected Merge

Part 2 presented a unique challenge. The VICE memory dump (part2_runtime.bin) was captured while the BASIC loading screen was still showing, before the game engine initialized. The engine tables at $8604 and $880E contained zeros instead of game data.

The Solution: Protected Merge

We created a merged memory image by taking Part 1's runtime (validated engine) as the base and overlaying Part 2's decompressed data, while protecting engine code ranges:

Protected ranges (preserved from Part 1):
  $8604-$86E5  (compression tables)
  $880E-$8891  (text decompression handler)
  $9782-$97A3  (print loop)
  $9AE1-$9BFC  (screen output)
  $8B60-$8BA0  (entry point)
  $8E3D-$8E60  (initialization)
  $9928-$9977  (system messages)

However, the pointer tables at $75F1 and $7983 were overwritten by Part 2 data. Instead of relying on pointer tables, we used a brute-force approach: scanning the $2D00-$4A10 range for SMART EGG compressed text blocks with quality filters (>=10 chars, >50% alphabetic, >=2 spaces).

Result

140 text segments successfully extracted: 36 objects, 56 room descriptions, 47 action/event texts, 1 misc (title screen).

Tools Created

ScriptPurpose
extract_t64.pyExtracts PRG files from T64 container
decrunch_v2.pyStage 1 decompression (6502 CPU emulator running Madsquad cruncher)
decrunch_stage2.pyStage 2 decompression (6502 CPU emulator running LZ77 decompressor)
trace_text_engine.pySMART EGG engine analysis and 6502 disassembly
decode_smart_egg.pyComplete text decoder with all compression tables
decode_all_text.pyAlternative text scanner for brute-force extraction
extract_graphics.pyGraphics extractor for C64 and ZX Spectrum screens