Reverse Engineering Process
How we went from a T64 tape image to complete game reconstruction, step by step.
Contents
Source Material
| File | RIGELREV.T64 (105,620 bytes) |
| Format | T64 tape container |
| Contents | Two PRG files (Part 1 and Part 2), each ~38KB |
| Platform | Commodore 64 (6502/6510 CPU, 64KB RAM) |
| Additional | ZX 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:
part_1_game.bin(38,911 bytes), loads at$0801part_2_game.bin(38,911 bytes), loads at$0801
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:
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):
- 29 NOPs (
$0830-$084C) as a buffer/signature SEIat$084D, followed by decompressor setup- Decompressor code copied to
$0100(stack page) and$0334 - Source data via zero-page pointer
$FE/$FF - Output via zero-page pointer
$2D/$2E - Exit:
LDA #$37 / STA $01 / CLI / JMP $8B60
Step 5: Validation with VICE
We validated our decompression against a VICE emulator memory dump:
- 80.8% byte-for-byte match with our
part1_final.bin - Differences are in I/O registers (
$D000-$DFFF), screen RAM, and runtime state - All engine tables (character table, dictionary, digrams) match exactly
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
- Found the text print loop at
$9782which calls the handler at$880E - Traced the
EOR #$FFinstruction at$881F(XOR with $FF) - Identified the three dispatch ranges: single char, digram, dictionary
- Located the character table at
$8604(96 entries) - Found the digram boundary table at
$86D8and character list at$86CC - Decoded the dictionary at
$8684(16 words) - Found the "SMART EGG!" watermark at
$9B2E
Compression Tables Extracted
| Table | Address | Size | Purpose |
|---|---|---|---|
| Character table | $8604 | 96 entries | Maps indices to PETSCII codes |
| Dictionary offsets | $8663 | 16 bytes | Start offset of each dictionary word |
| Dictionary lengths | $8673 | 16 bytes | Length-1 of each word |
| Dictionary data | $8684 | ~70 bytes | Encoded word characters |
| Digram chars | $86CC | 12 bytes | 12 most frequent characters |
| Digram bounds | $86D8 | 12 bytes | Bucket boundaries for the 12x12 grid |
Step 7: Text Extraction
Text is organized in pointer tables:
| Table | Address | Entries | Content |
|---|---|---|---|
| Room/System | $7983 | 35 | Room descriptions, parser responses, system messages |
| Action/Description | $75F1 | 255 | Narrative text, dialogues, object descriptions |
| System Messages | $9928 | 7 | Error 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
| Script | Purpose |
|---|---|
extract_t64.py | Extracts PRG files from T64 container |
decrunch_v2.py | Stage 1 decompression (6502 CPU emulator running Madsquad cruncher) |
decrunch_stage2.py | Stage 2 decompression (6502 CPU emulator running LZ77 decompressor) |
trace_text_engine.py | SMART EGG engine analysis and 6502 disassembly |
decode_smart_egg.py | Complete text decoder with all compression tables |
decode_all_text.py | Alternative text scanner for brute-force extraction |
extract_graphics.py | Graphics extractor for C64 and ZX Spectrum screens |