Commodore 64 · 1982 · Brøderbund Software
Every byte of the C64's 64 KB, one pixel each, 512 to a row. Lit pixels are what this game is: its code, graphics, level data, sound, text, tables and variables, exactly where they sit in memory. Dim areas are screen and working memory; dark is unused.
| What | Bytes | Of 64 KB |
|---|---|---|
| Program | 23,661 | 36.1 % |
| Code | 12,306 | 18.8 % |
| Graphics | 4,620 | 7.0 % |
| Text | 166 | 0.3 % |
| Tables | 5,753 | 8.8 % |
| Variables | 816 | 1.2 % |
| Screen, bitmap, colour, stack, I/O | 22,400 | 34.2 % |
| ROM the game runs under | 8,192 | 12.5 % |
| Unused | 11,283 | 17.2 % |
| Tier | silver |
|---|---|
| Coverage | 100 % of the bytes the game uses have a description |
| Build analysed | cartridge image, CBM80 header at $8004, cold start $9593 |
| Copy | agent-draft |
| Tools | emulator: VICE x64sc 3.10 with vice-mcp, disassembler: regenerator2000 0.9.20 |
| Model | claude-opus-5 (Claude Code), with nine subagents on the same model |
| Kit version | 0.0.1 |
Read this before annotating code. What the game is documented to do, with verification status against the binary. Statuses: open (documented, not found yet), traced (in the code, could not be exercised; say what was tried), confirmed (in the code, consistent with the emulator), live (observed directly), differs (the code does something else). "Absent" is not a status.
Sources: the C64 manual scanned at https://archive.org/details/Choplifter_1982_Broderbund (text only; no images were taken from it), the C64 entry on c64-wiki (https://www.c64-wiki.com/wiki/Choplifter), the game's own title screen, and play in the emulator. Every screenshot in reference/ was taken from the emulator during this run.
| Feature | Status | Where |
|---|---|---|
| Helicopter flown with a joystick in control port 1 | live | read at $DC01 from $976A, $97AD, $A71A, $A741 |
| A short press of the fire button shoots | live | $9774 compares the hold counter $96 with 15; under 15 it calls $97D2 |
| A longer press turns the helicopter | live | 15 or more calls $97F1; the turn continues while the button is held |
| Direction of the turn follows the stick | differs | the stick is read only when $59 is 0 or ±1. At a full facing the hold simply flips the machine to the other side, whatever the stick does ($9816) |
| 64 hostages in total | confirmed | $0CEE + $0CEF is compared with #$40 at $9683 |
| Hostages are held in sheds that have to be blown open | live | $B652 sets $0CF4,x; the shed body switches from shape 71 to 72 |
| Eighteen hostages per barracks | differs | $BE52 puts 16 in each of sheds 1 to 3 and 8 in shed 4, which starts open |
| At most 16 hostages ride at once | confirmed | $9D18 refuses to board when $0CF1 holds 16 |
| Hostages walk to the helicopter and climb in | live | $9B7D; boarding at a signed distance of +8/+9 or −11/−10 |
| The helicopter can run its own people over | confirmed | a hostage at −9 to +7 with the machine down and still is killed ($9E14) |
| Status bar counts killed, aboard and delivered | live | three BCD counters written by $A978; $0CFC, $0CFE, $0CFD |
| Tanks attack from the far side of the border | live | actor type 3, handler $A0BB, five gun elevations |
| Jets appear after the first sortie | confirmed | the type-9 cap at $A3B6 is 0 at difficulty 0 and rises to 1 and 2 |
| Drones appear on the third sortie | traced | actor type 10, handler $B73D, released from sortie 2 on, one at a time, homing on the helicopter. The shared emulator stayed on sortie 0 for the whole watch, so this rests on the code and the shape, not on seeing one |
| Three helicopters per game | live | the life count is sortie_number ($0CFF). A sortie ends only when the helicopter is destroyed: the settle counter $62 runs to 6 ($A875), sets sortie_end_delay ($63), the main loop adds 3 a pass until it goes negative ($968E) and jumps to sortie_start, which increments $0CFF ($964B) after the message. $0CFF = 3 at $9621 is THE END. Live: a full-speed crash with $0CFF = 2 ran the whole chain and ended the game |
| No points are scored, only hostages counted | confirmed | there is no score variable; the only counters are the three on the bar |
| Sorties are announced as FIRST / SECOND / THIRD SORTIE | live | message table at $8AD7, drawn into a raster text window by $8A8B |
| THE END and MAGNIFICENT! end messages | confirmed | same table, shown from $96E4 and $96E8 |
| RUN/STOP pauses | live | $98BC reads the KERNAL scan code $3F from $C5 |
| Q ends the game | open | no read of the Q key was found. The game's only keyboard input is the RUN/STOP poll at $98BC, reached through $C5, and the only other keyboard code is the KERNAL scan the raster handler calls. Searched: every $DC00/$DC01/$C5/$CB/$0277 access in $8000-$BFFF, and every immediate comparison against the matrix code for Q ($3E) and its PETSCII ($51) |
CBM80 autostart header at $8000 and a loader bolted on the front.$1000-$1FFF, hidden under the character ROM's shadow, so the blitter never shifts at run time.rti that ends the raster interrupt handler.How to get from the contributor's own copy to the analysed state. Someone else must be able to follow this exactly.
work/choplifter.d64, a 174,848-byte disk image. The directory shows a disk named WWW.C64HQ.COM holding one program:
| Name | Type | Blocks |
|---|---|---|
CHOPLIFTER | prg | 66 |
The program loads at $0801 and ends at $4904. $0801 is a one-line BASIC program, SYS 2064, and $0810 is a 24-byte copy loop that moves $0900-$48FF up to $8000-$BFFF, writes $36 to $01 and jumps to $959C.
What it copies is a 16 KB cartridge image. $8000/$8001 and $8002/$8003 are the cartridge cold and warm start vectors, both $9593, and $8004-$8008 holds the CBM80 signature that makes a C64 start a cartridge on reset. The disk file is that cartridge with a loader bolted on the front, and the loader enters at $959C rather than at the cartridge's own $9593. There is no trainer, no cracktro and no decompression. Nothing identifies a cracking group.
The game's own title screen is the only build marker: BRODERBUND SOFTWARE PRESENTS, CHOPLIFTER!, DAN GORLIN, DANE BIGHAM, (C) 1982.
Getting fire into the machine is the awkward part of this game. The vice-mcp vice_joystick_set tool only drives CIA1 port A ($DC00, control port 2); called for the other port it reports success and changes nothing, and vice_keyboard_matrix did not move $DC01 either. Choplifter reads control port 1 at $DC01. The way in is to make CIA1 port B an output and write the port directly:
python3 - <<'PY' import sys, time sys.path.insert(0, 'kit/scripts') from vice import connect, stick_arm, stick, FIRE, UP, LEFT rpc = connect() stick_arm(rpc) # DDRB = $1F, so writes to $DC01 are what the game reads stick(rpc, FIRE); time.sleep(0.2); stick(rpc, 0) PY
stick_arm, stick and stick_release are in kit/scripts/vice.py. Bits are the standard ones, active low: 1 up, 2 down, 4 left, 8 right, 16 fire. Leaving bits 5-7 as inputs keeps the keyboard columns the game shares that read with.
| File | State |
|---|---|
work/play-inflight.vsf | first sortie, helicopter airborne over the base, nothing carried. This is the snapshot everything downstream is read from. |
Saved without ROMs. The 64 KB RAM image then starts at file offset 209. Checked against two bytes of the game's own code at $9760 and $8010 rather than against $0000/$0001 or the screen. $8000-$BFFF in the snapshot is byte for byte identical to the copy on the disk, so the program never modifies itself and the cartridge image can be read either way.
$01 is $36: BASIC ROM switched out so the game owns $A000-$BFFF, I/O visible, KERNAL ROM still in. The game calls the KERNAL's keyboard scan at $FF9F from its own interrupt handler.$0314 points at $99F2 and $0318 at $9AAB. The hardware vectors at $FFFA still hold their KERNAL values, so interrupts arrive through the ROM and out through the RAM vectors.$A79E. The handler at $99F2 reads $D012 and switches the display mode at raster $1E, $51, $81, $91, $A9 and $E0.$D015 and it reads 0 during play.$0400 and its bitmap at $2000; bank 1 has them at $4400 and $6000. $8D16 toggles bit 6 of $0F, which is added into the high byte of every bitmap row address, and $8D1F writes the matching bank into $DD00.| Range | What | |---|---| | $0000-$00FF | the game's zero page variables | | $0400-$07E7 | video matrix, bank 0 copy | | $0800-$08FF | the disk loader's BASIC line and copy loop, dead after the copy | | $0A00-$0B47 | bitmap row address tables, low bytes then high bytes | | $0B50-$0D02 | actor records and game state | | $2000-$3F3F | bitmap, bank 0 copy | | $4400-$47E7 | video matrix, bank 1 copy | | $5000-$57FF | character set for the top bar in bank 1 | | $6000-$7F3F | bitmap, bank 1 copy | | $8000-$8008 | cartridge header | | $8009-$809E | 75 shape pointers | | $80A1-$8A8A | 75 shape bitmaps | | $8A8B-$BFFE | the engine: code, tables and text |
Everything from $0900 to $48FF still holds the loader's copy of the cartridge when play begins, and the bitmap and the row tables are laid down on top of it. Nothing reads it again.
$0810 is the entire loader: a 16 KB lda ($FE),y / sta ($FC),y copy from $0900 to $8000, lda #$36 / sta $01 to swap the BASIC ROM out for the RAM the top half of the cartridge needs, and jmp $959C. It does not decompress, patch, or check anything, and it is not annotated further.
The symbol map for this game is symbols.json; the listing behind the Source tab is listing.json. Write-ups, facts and symbol maps are CC BY-SA 4.0. The game itself is not hosted here.