230 lines
16 KiB
Markdown
230 lines
16 KiB
Markdown
# Modem Wars (C64, Electronic Arts / Ozark Softscape 1988) - disk and memory overview
|
|
|
|
Source image: `Modem_Wars_1988_Electronic_Arts_b4.d64` (35 tracks, 683 blocks, disk name
|
|
`SPORT OF WAR`, id `OZ`, DOS `2A`). "Sport of War" was the game's working title; "OZ" = Ozark
|
|
Softscape (Dan Bunten).
|
|
|
|
Everything in this document was derived by: extracting the two directory files with `c1541`,
|
|
disassembling the boot loader and the 1541 drive code, decrypting every sector in Python with the
|
|
cipher recovered from the drive code, and confirming the resulting memory image against a VICE
|
|
(NTSC, true drive emulation) RAM snapshot taken at the game entry point `$0800`.
|
|
|
|
## 1. Disk layout
|
|
|
|
| Track(s) | Encrypted? | Contents |
|
|
|-----------------|-----------:|----------|
|
|
| 1 s0 | no | C128 auto-boot sector ("CBM" header, message "AN ELECTRONIC ARTS PRODUCTION"); copies a CBM80 pseudo-cartridge to `$8000` and calls GO64 |
|
|
| 1 s1-10 | no | Plain ASCII letter from Dan Bunten to would-be pirates (`docs/track1_DanBuntenLetter.txt`) |
|
|
| 1 s11-15 | - | random filler |
|
|
| 1 s16 | - | filler that happens to contain fragments of assembler source text from the mastering system |
|
|
| 1 s17 | no | 1541 bootstrap sector, executed in the drive with `B-E 2 0 1 17`; also holds a hidden 1988 EA message |
|
|
| 1 s18-20 | no | 1541 fast-loader code, runs at `$0300-$05FF` in the drive |
|
|
| 2-17 | - | every byte `$01` (unused filler; 12-17 are also marked free in the BAM) |
|
|
| 18 | no | directory track: s0 BAM, s1 directory, s7 first page of the modem driver, s8-14 unit start-position templates, s15-16 terrain tile rules + start-up code, s17-18 save slots |
|
|
| 19-21 | no | CBM file chains of the directory files `ea` (1 block) and `load` (45 blocks) |
|
|
| 22-27 s0-17 | yes | main program -> `$0800-$6EFF` (the last four sectors also supply `$0400-$07FF`) |
|
|
| 28 s0-12 | yes | data -> `$9300-$9FFF` |
|
|
| 28 s13-17 | - | random filler |
|
|
| 29 s0-3 | yes | solo trainer setup: picks the opening play -> `$EC00-$EFFF` |
|
|
| 29 s4-5 | yes | solo trainer drone-flight AI -> `$EE00-$EFFF` |
|
|
| 29 s6-8 | yes | solo trainer playbook (five canned opening plays) -> `$0200-$04FF` |
|
|
| 29 s9-10 | yes | modem status message strings -> `$8800-$89FF` (reused as AI work RAM by the trainer) |
|
|
| 29 s11-15 | yes | high-memory resident code -> loaded at `$FA00`, then moved to `$FBB8-$FFD1` |
|
|
| 30 s0-15 | yes | text/glyph engine, message system, unit pictures -> `$C000-$CFFF` (over the boot loader) |
|
|
| 31 s0-16 + 32 s0-7 | yes | random map generator and setup phase -> `$6F00-$87FF` |
|
|
| 33 s0-16 + 32 s8-15 | yes | comcen missile and drone screens -> `$6F00-$87FF` (what the boot loader installs) |
|
|
| 34 s1-15 (+18 s7) | yes | **modem driver** (software UART, Hayes control, packet protocol) -> `$E000-$EFFF` |
|
|
| 35 s0-15 | yes | **solo trainer** (the computer opponent) -> `$E000-$EFFF` |
|
|
| other last sectors (29 s16-17, 30 s16-17, 31 s17, 32 s16, 33 s17?, 34 s0/s16, 35 s16) | - | random filler |
|
|
|
|
## 2. Boot sequence
|
|
|
|
1. `LOAD"EA",8,1` (or the C128 path) loads the 102-byte file `ea` to `$02A8-$030B`. Its last bytes
|
|
overwrite the six BASIC vectors at `$0300-$030B` with `$02B8`, so BASIC's warm start runs the
|
|
loader instead of printing READY.
|
|
2. `$02B8`: KERNAL `LOAD` of file `load` (`$9800-$C3FF`), then `JSR $C000` -> `$C145`, then `JMP $0800`.
|
|
3. `$C145` (boot loader):
|
|
* shows the title picture (bitmap `$A000`, screen `$9C00->$8C00`, colours `$9800->$D800`, VIC bank 2,
|
|
`$D011=$3B $D016=$D8 $D018=$38`);
|
|
* opens RS-232 (device 2, 300 baud) and sends the Hayes string `ATE0S0=0` to a modem, if any;
|
|
* `I0` initialise, then `B-E 2 0 1 17` - the drive executes the bootstrap sector, which reads
|
|
track 1 sectors 18-20 to `$0300-$05FF` and jumps there;
|
|
* switches to `$01=$35` (I/O visible, no ROMs), points NMI/IRQ at an RTI (`$C229`);
|
|
* pulls tracks 22-27 (18 sectors each) to `$0800+`, copies `$6F00-$72FF` to `$0400-$07FF`,
|
|
then track 33 s0-16 -> `$6F00`, track 32 s8-15 -> `$8000`, track 28 s0-12 -> `$9300`,
|
|
track 29 s11-15 -> `$FA00` followed by a block move up by `$1B8` (so the data ends at `$FFD1`);
|
|
* stores a SID noise sample as random seed at `$09C3`. Its final RTS does not return to `$02B8`: the stack
|
|
was rebuilt during the load so that it lands at `$0461`, a memory-checksum routine in the `$0400` block
|
|
that ends with `JMP $0800`.
|
|
4. `$0800` -> `$0A6C` is the game. Early on it loads the runtime overlays (section 5).
|
|
|
|
## 3. Fast-loader protocol (C64 `$C004/$C034`, private game copy `$0804/$085C`)
|
|
|
|
* `setDest` (`$C034` / `$085C`): X = destination low byte, Y = high byte (stored in self-modifying code).
|
|
* `read` (`$C004` / `$0804`): A = number of sectors, Y = track, X = first sector. Each sector is
|
|
requested with a 3-byte command `$80, track, sector` (command byte, then parameters), transferred
|
|
256 bytes at a time (command bytes go out LSB first, one bit per handshake; the drive answers each with a status bit)
|
|
with a 2-bit handshake on CIA2 port A (`$DD00` bits 4/5 out, 6/7 in) timed
|
|
against `$D012` to avoid bad lines, and followed by a status byte (0 = ok). Errors turn the
|
|
border red and retry forever. The destination advances one page per sector.
|
|
* `write` (`$0843`, game only): same registers, command `$60`; the drive encrypts and writes.
|
|
* command `$C0` resets the drive.
|
|
|
|
## 4. Sector cipher (drive code `$0531`)
|
|
|
|
Every sector except those on track 18 is XORed with a rolling key:
|
|
|
|
```
|
|
a = ((track | $C0) << 1) ; carry = 1 because bit 7 is set
|
|
a = a + sector + carry ; carry = overflow of this add
|
|
key = ROL(a) ; rotates in the carry above; this is key[0]
|
|
for y = 1..255: key = ROL(y ^ key) ; carry chains from the previous ROL
|
|
data[y] ^= key
|
|
```
|
|
|
|
`extracted/decryptDisk.py` implements it; `extracted/decrypted/` holds the plaintext sectors.
|
|
|
|
## 5. Memory map at run time
|
|
|
|
| Range | Source | Notes |
|
|
|-------|--------|-------|
|
|
| `$0000-$00FF` | - | zero page (see `disassembly/zeropage.inc`) |
|
|
| `$0200-$03FF` | T18 s15-16 | terrain tile rule table (`$0200`, 256 entries) and start-up code (`$0300`); later reused as a sprite decompression buffer, and replaced by the trainer playbook in a solo game |
|
|
| `$0400-$07FF` | copy of T27 s13-16 | manual-check answer table (`$0401`), the checksum routine the loader returns into (`$0461`), the status message strings (`$0500`..), and later the unit scan lists and AI threat maps |
|
|
| `$0800-$6EFF` | T22-27 | main program; `$0804` loader, `$0843` writer, `$09A6` manual look-up protection, `$0B4F-$10EE` overlay loading |
|
|
| `$6F00-$87FF` | T31+T32a / T33+T32b | map generator + setup phase, or the comcen missile/drone screens; the flag at `$87FF` says which is resident |
|
|
| `$8800-$89FF` | T29 s9-10 | modem status strings; the solo trainer overwrites the whole block with its own per-unit tables |
|
|
| `$8A00-$8BFF` | - | work RAM (`$8A00`, `$8A40`, `$8A80`, `$8AC0`, `$8B00` referenced) |
|
|
| `$8C00-$8FFF` | LOAD `$9C00` | title screen RAM during boot; later reused by the game |
|
|
| `$9000-$92FF` | - | work RAM and variables (`$92A1`, `$92C7`, `$92F0`, ...) |
|
|
| `$9300-$9FFF` | T28 s0-12 | tables / graphics |
|
|
| `$A000-$BFFF` | LOAD | title bitmap during boot; free for the game afterwards |
|
|
| `$C000-$CFFF` | T30 | text/glyph engine (`$C133` print string, `$C01A` print char), message queue, sound, unit pictures (the boot loader lived here first) |
|
|
| `$D000-$DFFF` | - | I/O (`$01 = $35`) |
|
|
| `$E000-$EFFF` | T35 or T18s7+T34 | **opponent module**: either the solo trainer AI or the modem driver. Both present the same jump table (`$E000` per-frame poll, `$E003` open/close link, `$E006` get byte, `$E009` send byte, `$E00C` per-frame, `$E015` get key) so the game cannot tell them apart. `$EC00-$EFFF` may be swapped for the trainer setup (T29 s0-3) or the drone AI (T29 s4-5) |
|
|
| `$F000-$F6FF` | T18 s8-14 | at run time `$F000-$F63F` is the 40x40 map (one byte per cell) and the 100-entry unit record arrays start at `$F640`, `$64` apart. What is *stored* on the disk is different: four 400-byte unit start-position templates (see section 10) |
|
|
| `$F700-$FBB7` | - | work RAM / buffers |
|
|
| `$FBB8-$FFD1` | T29 s11-15 (moved) | resident high code incl. the IRQ handler region `$FD5D-$FD6F`; `$FFDE-$FFE7` receives 10 bytes from the BAM (offset `$AB`) = disk serial used by the protocol |
|
|
| `$FFFA-$FFFF` | - | NMI/RESET/IRQ vectors (RAM), set by the loader to `$C229` and later by the game |
|
|
|
|
Mode flags: `$0BA5` bit 7 chooses the opponent - set means the solo trainer (track 35), clear means a
|
|
modem game (track 34). `$87FF` says which `$6F00` overlay is resident, `$0B9B` bit 7 marks film playback
|
|
and `$0BA3` holds the game type.
|
|
|
|
### Runtime overlay loads (all through `$0804` read / `$085C` setDest)
|
|
|
|
| Call site | Destination | Track/sectors |
|
|
|-----------|-------------|---------------|
|
|
| `$0F07` | `$C000` | T30 s0-15 |
|
|
| `$1047` | `$6F00` | T31 s0-16 then T32 s0-7 (map generator) |
|
|
| `$1086` | `$6F00` | T33 s0-16 then T32 s8-15 (comcen screens) |
|
|
| `$1060` | `$8800` | T29 s9-10 |
|
|
| `$1074` | `$EC00` | T34 s12-15 (modem driver tail) |
|
|
| `$0B4F` | `$EC00` | T35 s12-15 (solo trainer tail) |
|
|
| `$0F1A/$0F38` | `$E000` | T18 s7 + T34 s1-11 (modem driver) |
|
|
| `$0F1A/$0F22` | `$E000` | T35 s0-11 (solo trainer) |
|
|
| `$10A4` | `$F000` | T18 s8-14 (unit start-position templates) |
|
|
| `$10BB` | `$0200` | T18 s15-16 (terrain tile rules) |
|
|
| `$0FB1` | `$0200` | T18 s0 (BAM) - disk id check against `OZ` / `EA` |
|
|
| `$7B24` (map generator) | `$EC00` | T29 s0-3 (trainer setup) |
|
|
| `$E0B4` (trainer) | `$EE00` | T35 s14-15 (restore its own tail) |
|
|
| `$E0D6` (trainer) | `$EE00` | T29 s4-5 (drone flight AI) |
|
|
| `$7E85`, `$8194` (map generator) | write | save game film / setup to track 18 |
|
|
| `$ECAA` (trainer setup) | `$0200` | T29 s6-8 (the trainer's playbook) |
|
|
|
|
## 6. Copy protection
|
|
|
|
`$09A6`: picks an entry of the 32 x 3-byte table at `$0401`, asks the player to "IDENTIFY THIS MAP
|
|
(SEE MANUAL)" (string at `$0A48`) and compares the three answer bytes at `$5A-$5C`. The answers are map
|
|
names, and a map name is simply the game's 24-bit map seed written in base 32 using the 32-character
|
|
alphabet at `$890C` - the same name the two players compare before a modem game to be sure they are
|
|
playing the same battlefield. So the manual's map list and the map generator are two views of one number. After three
|
|
failures it wipes all of memory (`$0A1E` loop) and hangs. The disk itself is not copy protected
|
|
(standard sectors); Dan Bunten's letter on track 1 explains why.
|
|
|
|
## 7. Files in this repository
|
|
|
|
* `extracted/files/` - the two directory files (`ea.prg`, `load.prg`)
|
|
* `extracted/sectors/` - every raw sector, `extracted/decrypted/` - every decrypted sector
|
|
* `extracted/bootImage.bin` - 64 KB memory image exactly as at the first execution of `$0800`
|
|
* `extracted/runtimeImage_A.bin`, `_B.bin` - with the runtime overlays applied (two variants)
|
|
* `extracted/vice/` - VICE automation used to validate the image (`ram_at_0800.bin` = real snapshot)
|
|
* `disassembly/` - the annotated, reassemblable ca65 sources (`verify.sh` rebuilds and compares)
|
|
* `tools/` - the Python disassembler / build pipeline
|
|
* `docs/` - this file, the title picture, the hidden texts
|
|
|
|
## 8. Game concepts recovered from the code and its text
|
|
|
|
* **Unit types** (`unitTypeTable` = `$F76C+unit`, names at `$C964`): 0 GRUNT, 1 RIDER, 2 BOOMER,
|
|
3 SPY, 4 COMCEN. Each side owns 50 units: indices 0-49 are player 0, 50-99 player 1, and the
|
|
last unit of each side (49, 99) is that side's COMCEN (command centre).
|
|
* **Unit records** are parallel 100-byte arrays that follow the map, spaced `$64` apart from
|
|
`$F640`: column, row, flags/facing, type, destination column/row, secondary target, cloak,
|
|
blitz, stun/dug-in, energy (0-50, shown as a percentage), move state, draw flags, the terrain
|
|
hidden under the unit, and the group id.
|
|
* **Groups** (names at `$C92C`, 7 x 8 bytes): ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER.
|
|
* **Game types** (`$0BA3` bits 0-2, names at `$CCC8`): SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF,
|
|
SLUGGERS, FULL WAR, DEFENDER; `$CC11` selects STANDARD or CUSTOM rules. All seven names match the
|
|
manual, except that the shipped game spells the first one SCRIMAGE with a single M.
|
|
* **Options** offered on the first screen: compete with modem opponent, practice with solo trainer,
|
|
watch game film, save game film, load game film. Custom-game switches include HANDICAP,
|
|
TERRAIN PTS, DRONES and COMCEN SPEED (`$EF52`..).
|
|
* **Console panels**: STATS, REPAIR and MISC tabs, reached with the joystick and the fire button;
|
|
unit status words (READY, RELOAD, FIRED, REPAIR, MOVE, PINNED, DIGGIN) live at `$C98A`..
|
|
* **Screen**: multicolour bitmap in VIC bank 2 - bitmap `$A000`, screen RAM `$8C00`, colour RAM
|
|
`$D800`. The left 22x22 character frame shows the whole 40x40 battlefield at 4x4 pixels per
|
|
cell; the right frame shows a 7x5 cell tactical view at 16x16 pixels per cell, with the message
|
|
and status panel below it. "Text" is drawn into the bitmap with the game's own 8x8 font at
|
|
`$9426` (114 glyphs), not with the VIC's character mode.
|
|
* **Commands** to the engine and to the opponent are single bytes `$80`-`$A6` followed by 0-3
|
|
argument bytes, queued into a ring buffer and dispatched through `commandHandlerTable` at
|
|
`$4D2A`; the same stream is what gets recorded and replayed as a "game film".
|
|
|
|
## 9. The two opponent modules at `$E000`
|
|
|
|
Everything the game knows about "the other player" goes through one jump table at `$E000`, and exactly
|
|
one of two completely different modules is loaded behind it:
|
|
|
|
| | `game/modemDriverE000` (track 34) | `game/trainerAiE000` (track 35) |
|
|
|---|---|---|
|
|
| chosen when | "COMPETE WITH MODEM OPPONENT" (`$0BA5` bit 7 clear) | "PRACTICE WITH SOLO TRAINER" (`$0BA5` bit 7 set) |
|
|
| what it is | a complete software modem | the computer opponent |
|
|
| `$E006` get byte | pops the receive ring filled by the UART | always reports "nothing received" |
|
|
| `$E009` send byte | queues it for transmission | throws it away |
|
|
|
|
**The modem driver** is three layers. A bit-banged RS-232 UART on the user port, driven from the CIA2
|
|
NMI, with the CIA timers latched for exact 300 or 1200 baud; a Hayes AT layer that dials, answers and
|
|
watches the result codes (so it can notice a `CONNECT 1200` and switch speed), with Commodore-key hot
|
|
keys for answer/originate/baud/DTR; and a packet layer whose header byte carries the payload length,
|
|
with bit 7 distinguishing chat text from game commands.
|
|
|
|
**The solo trainer** presents the same interface but never touches a CIA. Once per lock-step exchange
|
|
it builds the packet the game is about to "receive", so its orders enter the engine through exactly the
|
|
same command interpreter a human opponent's would. It plays side 1 (units 50-99), keeps its per-unit
|
|
tables in the `$8800` block (the modem status strings, which are dead once a solo game starts), picks an
|
|
opening play from a canned playbook loaded to `$0200-$04FF`, and evaluates moves against an influence
|
|
map built from the enemy units' reach and weight. While its drone is airborne it swaps a drone-flight
|
|
AI into `$EE00-$EFFF` and swaps its own tail back afterwards.
|
|
|
|
That symmetry is the neat part of the design: the battle engine, the recorded "game film" and the
|
|
command interpreter are identical whether the opponent is a person on a phone line or the trainer.
|
|
|
|
## 10. Unit start-position templates (track 18 sectors 8-14)
|
|
|
|
The seven sectors the loader puts at `$F000-$F6FF` are not a battlefield. They hold four 400-byte
|
|
templates, at `$F000`, `$F190`, `$F320` and `$F4B0`. Each template is four parallel 100-entry arrays:
|
|
|
|
| offset | array |
|
|
|---|---|
|
|
| +$000 | starting column of each unit |
|
|
| +$064 | starting row |
|
|
| +$0C8 | flags - facing in bits 0-1, side in bit 5, "in a group" in bit 6, group id in bits 2-4 |
|
|
| +$12C | unit type (0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN) |
|
|
|
|
One template is chosen according to the game type and copied into the live unit arrays, which occupy
|
|
the same addresses from `$F640` upwards once play begins. The standard roster each template describes
|
|
is 28 GRUNTs, 12 RIDERs, 6 BOOMERs, 3 SPYs and 1 COMCEN per side - 50 units each.
|
|
|
|
The last 192 bytes of the block, which land at `$F640-$F6FF`, are not used by the game at all: they are
|
|
a fragment of an older build of the game's own options-list printer that the development system happened
|
|
to leave in the sector. It is overwritten by the unit arrays as soon as a game starts.
|