Four ways to break the game, traced through the disassembly, and one section on everything that turned out to be watertight. None of the four cracks is in combat; they all live in navigation, the economy, and the save file.
Addresses are absolute Z80 addresses; CODE.COM loads at 0x0100 (addr = file_offset + 0x100). Disassembly via tools/zdis.py.
You can reach any system in the current galaxy in a single jump, regardless of distance, and be charged only the fuel cost of a legal short-range target.
The recipe:
H to start the countdown (10, 9, 8…).F (Find), then type the
name of any system in the galaxy, including one far out of fuel range.The range and fuel test runs exactly once, at the instant you press H, and never again.
H (handler 0x88ac, dispatch 48 ac 88 at 0x41e0):
88AC CALL 0x6dcf
88AF LD A,(0xe91b) : OR A : JP nz,0x16fe ; countdown already running? ignore key
...
88E5 CALL 0x5314 ; cursor -> target system 0xe72b
88E8 CALL 0x53fd ; target 0xe72b -> destination coords / cursor
88FA CALL 0x6141 ; DISTANCE(origin 0xeb67 -> target 0xe72b) -> DE
88FE LD HL,(0xf775) ; HL = current fuel (max 0x46 = 70 = 7.0 LY)
8901 XOR A : LD H,A
8903 SBC HL,DE ; fuel - distance
8905 JR nc,0x8910 ; fuel >= distance -> OK
8907 CALL 0x16fe
890A LD HL,0x8c4a ; "Beyond Range"
890D JP 0x1683 ; ...and abort, no countdown
8910 LD A,E
8911 LD (0xe87f),A ; CACHE this distance as the fuel to burn on arrival
8916 LD (0xe91c),A = 0x32 ; sub-tick reload (50)
891B LD (0xe91b),A = 10 ; start the 10-unit countdown
8925 RET
The only consequences of the SBC HL,DE at 0x8903 are the Beyond Range abort (message
0x8c4a, referenced nowhere else) and caching the distance to 0xe87f. Find is a
completely separate way to set the target (handler 0x55a3, dispatch 46 a3 55 = 'F' at
0x41d4) that never looks at the countdown:
55A3 LD A,(0xe902) : CP 4 : RET nz ; galactic chart only (no countdown check)
... <scan every system in the galaxy for a name match>
55F9 LD (0xe72b),A ; *** selected target := the found system ***
55FC CALL 0x53fd ; target -> destination coords / cursor
5604 JP 0x52ce ; redraw chart
Find rewrites 0xe72b and re-runs 0x53fd, but it never touches the cached distance
0xe87f, the countdown 0xe91b, or the fire flag 0xe920. The countdown keeps ticking in
the VDP interrupt at 0x7202, which runs every frame regardless of screen:
721F LD A,(0xe91b) : OR A : JR z,... ; countdown active?
7225 DEC (0xe91c) : JR nz,... ; per-second sub-tick (reload 0x32)
7232 DEC (0xe91b) : JR nz,... ; tick the seconds
7238 LD A,0xff : LD (0xe920),A ; hit 0 -> raise the "JUMP NOW" flag
The fire executor 0x894d, called unconditionally from the main loop at 0x1520, does no
range test at all, and arrival copies the target and debits the cached distance:
894D LD A,(0xe920) : OR A : RET z ; jump flag raised?
8952 CALL 0x8ac8 / 0x1a7d / 0x64a1 / 0x8b7b ; do the jump
...
8A6A LD A,(0xe72b) ; the (Find-tampered) target
8A6D LD (0xeb67),A ; *** current system := target, unconditionally ***
8A7F LD A,(0xf775) ; fuel
8A82 LD HL,0xe87f
8A85 SUB (HL) ; fuel -= CACHED distance (the original short target!)
8A86 LD (0xf775),A
Two consequences, precisely: unlimited range (any of the galaxy’s 256 systems in one
jump, 0xeb67 copied from 0xe72b with no comparison), and near-free travel (fuel
burned is the decoy’s cost cached at 0x8911; pick a 0.1 LY decoy and a 40 LY real
destination and you cross the galaxy for almost nothing). Because the original passed
SBC HL,DE, the SUB (HL) can never underflow.
The root cause: the design assumes target selection and range validation are the same
event, true when the only way to change the target is the cursor plus H. Find is a second
way to set 0xe72b while a countdown is live. The check guards the keypress, not the jump.
| addr | role |
|---|---|
0xe72b | selected hyperspace target (written by both H and Find) |
0xeb67 | current system, set from 0xe72b on arrival (0x8a6d) |
0xe87f | cached jump distance / fuel to burn, latched at countdown start (0x8911) |
0xe91b / 0xe91c | countdown seconds / sub-tick |
0xe920 | “jump now” flag, raised by the ISR |
0xf775 | fuel (tenths of a LY, max 0x46) |
0xe902 | screen mode (4 = galactic chart, gates Find) |
Every hyperspace arrival halves your bounty. A handful of jumps through empty systems washes a wanted commander back to Clean, no fine, no cost.
Legal status is not a stored label; it is classified live at 0x62fc from the single
bounty byte 0xeb65 (0 = Clean, 1 to 49 = Offender, >= 50 = Fugitive). The arrival
sequence unconditionally halves it:
8A7A LD HL,0xeb65
8A7D SRL (HL) ; bounty >>= 1, every jump
From the maximum 255 it takes at most eight jumps to reach 0 (255, 127, 63, 31, 15, 7, 3, 1, 0), and a bare Fugitive bounty of 50 clears in six. Nothing in the jump path can
raise it. This is not cosmetic: police Vipers do spawn, and the arrival ambush size is
derived from the top bits of the same byte:
8A32 LD A,(0xeb65)
8A35 RLCA : RLCA : AND 3 : INC A ; ambush party size = 1..4, scaled by bounty
The periodic hostile launcher at 0x3176 spawns the Viper (type 0x0c). So halving the
bounty both calls off pursuit (status -> Clean) and shrinks the ambushes.
The commander loader copies on-disk bytes straight into live variables with no checksum and no bounds check.
The load path 0xa377 checks only for a BDOS disk error:
A386 CALL 0xa4c6 ; random-read the file into buffer 0xd606
A389 JP c,0xa55c ; the ONLY guard: disk I/O error. No content validation.
A397 LDIR ... ; copy 128-byte payload to 0xf6c0
A3A2 CALL 0xa49d ; apply payload to live variables
The apply routine 0xa49d walks a field-descriptor table at 0xa599
([dest_lo, dest_hi, len], hi == 0 terminates) and LDIRs each field straight into the
live variable:
A4A3 LD C,(HL):INC HL ; dest lo
A4A4 LD A,(HL):INC HL ; dest hi
A4A7 OR A : JR z,end ; hi==0 -> end of table
A4AB LD A,(HL):INC HL ; length
A4AE EX DE,HL : LD D,B:LD E,C : LD C,A:LD B,0
A4B4 LDIR ; file buffer -> live var, no validation
The save side 0xa483 is the mirror image and appends no checksum. Security-relevant
fields reachable this way:
| var | len | grants |
|---|---|---|
0xeb16 | 3 | cash, 24-bit (up to ~16.7M cr) |
0xf775 | 20 | fuel, missiles, every equipment flag (large bay, ECM, escape capsule, docking computer, galactic hyperdrive, energy unit…) |
0xeb12 | 1 | combat rank 0–8 (8 = E-L-I-T-E) |
0xeb13 | 3 | kill tally |
0xeb65 | 1 | bounty / legal status (0 = Clean) |
Set the cash bytes and rank byte, flip the equipment flags, zero the bounty, and the game applies all of it on load.
Buying a cheap escape capsule and ejecting resets your bounty to zero, an instant
one-shot record wipe. The eject handler 0x19fc (gated on the capsule flag 0xf77c) on
a successful ejection:
1A5A XOR A
1A5B LD (0xf77c),A ; consume the capsule (must re-buy), correct
1A5E LD (0xf814),A
1A61 LD (0xeb65),A ; *** bounty / legal status := 0 = Clean ***
Where the per-jump launder in section 2 needs several hops, the capsule zeroes 0xeb65
outright. It does consume the capsule, so it is a paid one-shot, but a capsule costs far
less than the fines it erases.
The whole combat loop was audited for free kills, double-scoring, god-mode and resource cheats, and it is the most tightly guarded subsystem in the game. Every path re-validates on the operation that matters:
0x2d7e fire, 0x2db6 consume). Arming needs 0xf776 != 0 (0x2d58);
lock 1 -> 2 needs the crosshair on a real ship (0x477c); firing needs lock state 2 and
both clears the lock and DEC (0xf776). There is no path to fire at 0 missiles, so the
DEC cannot underflow. A destroyed target auto-unarms with no refund (0x30ea).0xf813). The handler bails at CP 0x30 : RET z (0x2ad5) before
setting the fire-pulse flag 0xf81b, and damage (0x28f6) is gated on that flag, so at
max heat there is no beam and no damage. Switching view never resets heat; only
launch/arrival (0x70ae) does.0xf817 max 0x60; shields 0xf80f/0x10 max 0x18). A full
refill happens only on launch/arrival (0x707d); there is no docking-menu or
screen-toggle refill path.0x32e7 runs once, only from the mark-destroyed tail
0x31d5. Exploding ships are routed away from the damage path (bit-3 check at 0x42a7)
so re-hits cannot re-score, and fleeing/hyperspacing enemies leave via 0x3100, which
never scores. Bounty writes to 0xeb65 are penalties (shooting the station 0x293a,
killing innocents 0x31e9), not farmable rewards.0x329e) consumes its one-shot flag 0xf77d and skips the station
(CP 0x13 at 0x32c4). ECM drains energy per tick (0x3259), auto-expires
(0x7246), and only destroys value-0 missiles. Fuel scoop caps at 7.0 LY
(CP 0x46 : RET z, 0x15bb).0xeb05 only
suppresses enemy aggression; it is not invulnerability.Trading was checked the same way: you sell your entire holding at once (count read from the
hold, so it cannot go negative), buys are 24-bit credit-checked before the deduction
(0x5c25), cost cannot 16-bit-overflow (stock masked to 63 by AND 0x3f at 0x4c08, and
255 x 4 x 63 = 64,260 < 65,536), and prices are deterministic with a fixed 95% sell
spread.
Two theoretical soft spots remain, neither practically exploitable. The energy-bomb ship
loop 0x32d5 omits the exploding-bit check the ECM loop has, so it could in principle
re-score a ship on the exact detonation frame, but the bomb is one-shot and visits each
slot once. And prolonged sun-skimming hull damage was never located (cabin temp 0xf811
only lights a warning at 0x16d4), so whether extended skimming is truly risk-free is
unconfirmed either way.