# Modem Wars - routine and data knowledge base (phase 1 survey, auto-generated)

Names are unit-scoped: overlay variants at the same address have different code.

## boot/c128BootSector

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 0B27 | c128BootEntry | Real entry point of the C128 auto-boot sector. The C128 KERNAL BOOT_CALL ($F890, checked in kernal-318020-05) reads track 1 sector 0 to $0B00, prints BOOTING + the message, parses the filename field, then calls the code with JSRFAR in bank 15. Because the byte after the message terminator is $A0 (the opcode of the LDY #$00 at $0B25), the KERNAL treats it as a one-character filename, tries to LOAD "0:"+$A0 (fails silently, result ignored) and sets the entry to $0B27 - verified in x128: the breakpoint at $0B25 never fires, $0B27 is hit with A=X=Y=0. The routine copies 256 bytes from $0B89 to $8000-$80FF (the CBM80 pseudo-cartridge, see boot/c64cart_8000.s), opens the drive command channel, sends "U0>M0" (1571 -> 1541 mode) and "U0>H0" (head/side 0), writes 0 to the MMU configuration register $FF00 (bank 15) and JMPs to the KERNAL GO64 entry $FF4D, which performs a C64-mode reset; the C64 KERNAL then finds CBM80 at $8004 and runs $8009. | Y must be 0 at entry for the full 256-byte copy. It is, but only because JSRFAR loads Y from the register image at $07, which is zero after a cold reset; the LDY #$00 at $0B25 is never executed. | $8000-$80FF <- $0B89-$0C88 (only $8000-$8076 are meaningful; the rest is C128 RS-232 buffer contents); logical file 15 opened to device 8 secondary 15; drive switched to 1541 mode; $FF00=0; never returns (C64 mode reset). | high |
| 0B45 | c128OpenCommandChannel | SETNAM with length 0, SETLFS lfn 15 / device 8 / secondary 15, then JMP KERNAL_OPEN: opens the disk command channel. Called from c128BootEntry ($0B30) and again from the C64-mode cartridge code at $8043, which also relies on the SETLFS values it leaves behind for its later KERNAL_LOAD of "0:EA". | none | Logical file 15 open to device 8 secondary 15; A/X/Y clobbered; C = KERNAL_OPEN error flag (ignored by callers). | high |
| 0B55 | c128SendDriveCommand | Sends one of three zero-terminated DOS command strings on the already open command channel. A is doubled to index the word table driveCmdPtrTable ($0B75); the pointer goes to $FB/$FC; CHKOUT lfn 15, CIOUT every byte up to (not including) the $00, then JMP KERNAL_CLRCHN. Index 0 = "U0>M0" (1571: 1541 mode), 1 = "U0>H0" (1571: head 0), 2 = "I" (initialise). Called with 0 and 1 by c128BootEntry and with 2 by the C64-mode cartridge code at $8048. | A = command index 0..2 (pre-doubled to a word-table offset in X); command channel 15 must be open. | $FB/$FC = pointer to the string; X/Y clobbered (Y = string length on exit); output channel restored to the screen by CLRCHN. | high |
| 0B68 | sendDriveCommandLoop | Loop label inside c128SendDriveCommand: LDA ($FB),Y / BEQ done / CIOUT / INY. Not a callable routine. | ($FB),Y = next string byte | Y advanced; exits to $0B72 at the $00 terminator | high |
| 0B72 | sendDriveCommandDone | Exit of c128SendDriveCommand: JMP KERNAL_CLRCHN, which unlistens the drive (terminating the command) and restores the default channels. | none | Default I/O channels restored | high |

Data:

- `$0B00` **c128BootHeader** (byteTable, 7 bytes): C128 boot-sector header: signature "CBM" ($0B00-$0B02), load address $0000 for additional blocks ($0B03/$0B04), bank 0 ($0B05), 0 additional blocks ($0B06). The KERNAL copies $0B03-$0B06 to $AC-$AF.
- `$0B07` **c128BootMessage** (text, 30 bytes): "AN ELECTRONIC ARTS PRODUCTION" plus $00 terminator at $0B24; the C128 prints BOOTING + this text + "...". At run time the KERNAL overwrites $0B23/$0B24 with the drive-number prefix "0:" for the filename it thinks follows (observed in x128: $0B20 = "TIO0:" $A0 $00).
- `$0B25` **c128BootFileName** (unknown, 2 bytes): Intended as the empty-filename terminator + first instruction LDY #$00 ($A0 $00), but the KERNAL parses the $A0 as a one-character filename and the $00 as its terminator. The two bytes are never executed; see misclassified.
- `$0B75` **driveCmdPtrTable** (addrTable, 6 bytes): Three little-endian pointers to the DOS command strings: $0B7B, $0B81, $0B87. Indexed by 2*A in c128SendDriveCommand (lo via $0B75,X, hi via $0B76,X - hence the disassembler's split labels D_0B75/D_0B76).
- `$0B7B` **driveCmdSet1541Mode** (text, 6 bytes): "U0>M0",$00 - 1571 command: switch to 1541 mode (the game's drive-resident fast loader and cipher code run at $0300-$05FF in 1541 mode). Harmless on a 1541 (error 31, never read).
- `$0B81` **driveCmdSelectHead0** (text, 6 bytes): "U0>H0",$00 - 1571 command: select head 0 (side 0 of the disk).
- `$0B87` **driveCmdInitialize** (text, 2 bytes): "I",$00 - DOS initialise; sent by the C64-mode cartridge code ($8046: LDA #2 / JSR $0B55), not by the C128-mode code.
- `$0B89` **c64CartImage** (unknown, 107 bytes): Bytes $89-$F3 of the sector = the CBM80 pseudo-cartridge ($8000-$806A after the copy): vectors $8009/$8009, "CBM80", then the C64-mode start-up code that IOINITs, clears low RAM, sets MEMTOP, opens the command channel via $0B45, sends "I" via $0B55, SETNAM "0:EA", LOADs it and JMPs to $02B8. Disassembled in boot/c64cart_8000.s; do not annotate as code here.
- `$0BF4` **sectorTailFiller** (unknown, 12 bytes): $00 then 8E 72 17 8E 73 17 8E 70 17 8E 71 (fragments of STX $1772/$1773/$1770/$1771): unused tail of the sector, leftover from the mastering buffer; lands at $806B-$8076 and is never referenced.

Insights:

- [boot_c128boot_T1S0_0B00] C128 boot path: KERNAL BOOT_CALL loads T1S0 to $0B00, prints "BOOTING AN ELECTRONIC ARTS PRODUCTION...", attempts (and fails) to LOAD a bogus one-character file because the boot sector lacks the empty-filename $00 byte, then JSRFARs to $0B27. The sector copies its bytes $89-$FF to $8000 as a CBM80 cartridge, puts a 1571 into 1541 mode (U0>M0, U0>H0), sets $FF00=0 and calls GO64 ($FF4D). The C64 KERNAL reset sees CBM80 and runs $8009 (boot/c64cart_8000.s), which reuses $0B45/$0B55 still sitting in RAM, loads "0:EA" and jumps to $02B8 (eaLoadGame).
- [boot_c128boot_T1S0_0B00] The entry bug is benign only because JSRFAR takes Y from the register image at $07, which the C128 reset leaves at 0; the copy loop then covers all 256 bytes. With any other Y the CBM80 signature would not be copied and the C128 would drop into C64 BASIC.
- [boot_c128boot_T1S0_0B00] The only zero-page locations used are $FB/$FC (DOS command string pointer); nothing in this sector persists into the game except the RAM copies at $0B45-$0B8A, which the C64-mode cartridge code calls before the fast loader overwrites $0800-$6EFF. After that, $0B45-$0BFF belongs to the main program (loadCommTailBuild1 at $0B4F etc.) and is unrelated.
- [boot_c128boot_T1S0_0B00] 1571 commands: "U0>M0" forces 1541 (1 MHz, single-sided) mode so the drive-side fast loader/cipher code on track 1 s17-20 behaves identically on 1541 and 1571; "U0>H0" selects head 0. A plain 1541 answers these with a syntax error that is never read.
- [boot_c128boot_T1S0_0B00] The copy loop takes 256 bytes starting at $0B89, so $0C00-$0C88 (C128 RS-232 buffer area) also lands at $8077-$80FF; only $8000-$8076 is meaningful and $806B-$80FF is never referenced by the cartridge code (the runtime overlay variants A/B later overwrite $8000-$87FF anyway).
- [boot_c128boot_T1S0_0B00] The KERNAL patches the boot buffer in place: $0B23/$0B24 become "0:" (drive number + colon) when it builds the filename, so a RAM dump taken after boot shows "AN ELECTRONIC ARTS PRODUCTIO0:" rather than the text on disk.
- [boot_c128boot_T1S0_0B00] Verification used ~/.local/share/vice/C128/kernal-318020-05.bin (BOOT_CALL at $F890: $9F filename length starts 0, $F94A BIT-skip loop counts filename chars, $F954 STX $04 sets the entry low byte, $F986 JSR $02CD = JSRFAR) plus an x128 run with -moncommands breakpoints; the driver script is in the session scratchpad (x128boot.py), not in the repository.

## boot/c64CartridgeStub8000

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 8009 | cartColdStart | Cold/warm start entry of the CBM80 pseudo-cartridge that the C128 boot sector copies to $8000 before GO64. Re-implements the parts of the C64 KERNAL reset that are skipped when a cartridge is present (IOINIT, clear $0002-$0101/$0200-$03FF, MEMTOP=$A000, MEMSTR=$0800, HIBASE=$0400, RESTOR, CINT), sets border/background to blue (6) and blanks the screen (VIC_CTRL1 bit 4 cleared), opens the drive command channel via $0B45, sends drive command index 2 = "I" via $0B55, closes lfn 15, then SETNAM "0:EA" and falls into cartLoadEaFile. It never returns; execution continues at $02B8 (eaLoadGame). | None (entered from the KERNAL reset via the $8000 cold-start vector). Relies on the C128 boot sector code still being present at $0B45/$0B55 (it is: the KERNAL reset skips RAMTAS when CBM80 is found and this routine only clears $0002-$0101 and $0200-$03FF). | Zero page $02-$FF and $0100/$0101, $0200-$03FF cleared; $0282=$08, $0288=$04; MEMTOP=$A000; KERNAL vectors restored; screen editor initialised; VIC_BORDER=VIC_BG0=6; display blanked; drive initialised; stack pointer = $F0; A=0 (LOAD, not verify) on entry to cartLoadEaFile. Does not return. | high |
| 800F | clearLowRamLoop | Loop body inside cartColdStart: with A=0 and Y=0..255 it stores to $0002+Y (absolute-indexed, so $0002-$0101), $0200+Y and $0300+Y. Equivalent to the RAMTAS clear but leaves $00/$01 (CPU port) untouched. Not a callable routine, just the loop label. | A=0, Y=0 | $0002-$0101, $0200-$03FF zeroed; Y=0 on exit | high |
| 8065 | cartLoadEaFile | Tail of cartColdStart (reached by JMP over the embedded "0:EA" filename): JSR KERNAL_LOAD then JMP $02B8. The LOAD uses the SETLFS left by $0B45 (lfn 15, device 8, secondary 15), so the file "ea" is loaded to its own address $02A8-$030B; control then goes straight to eaLoadGame at $02B8 rather than through the BASIC vector hijack used on the plain C64 path. The carry/error result of LOAD is ignored. | A=0 (load), filename set by SETNAM (4 bytes at $8061), logical file parameters from $0B45 | $02A8-$030B loaded from disk; jumps to $02B8, never returns | high |

Data:

- `$8000` **cartHeader** (addrTable, 9 bytes): Standard C64 cartridge header: cold-start vector $8009, warm-start vector $8009, signature "CBM80" ($C3 $C2 $CD $38 $30). Found by the KERNAL reset code, which then jumps to $8009.
- `$8061` **eaFileNameCart** (text, 4 bytes): Filename "0:EA" passed to SETNAM (X=$61, Y=$80, length 4) at $8050-$8056; plain PETSCII, no terminator (length-counted). Sits in the middle of the code and is skipped by the JMP at $805E.
- `$806B` **cartTailFiller** (unknown, 12 bytes): Never referenced: $00 followed by the byte pattern 8E 72 17 8E 73 17 8E 70 17 8E 71, i.e. fragments of STX $1772 / STX $1773 / STX $1770 / STX $1771. These are the last bytes ($F4-$FF) of track 1 sector 0 and look like leftovers from the mastering buffer (compare the assembler-source fragments on track 1 sector 16).

Insights:

- [boot_c64cart_8000_8000] This image is only live on the C128 boot path: C128 boot sector (track 1 sector 0, at $0B00) copies its bytes $89-$FF to $8000, switches a 1571 to 1541 mode, then GO64. The C64 KERNAL reset finds CBM80 at $8004, skips RAMTAS and jumps to $8009.
- [boot_c64cart_8000_8000] Because RAMTAS is skipped, the C128 boot sector's helper routines at $0B45 (open command channel 15,8,15) and $0B55 (send drive command string) survive in RAM and are reused by the cartridge code; the cartridge's own clear loop deliberately stops at $03FF. $0B45/$0B55 in the game's main program ($0B4F loadCommTailBuild1 etc.) are unrelated code that only exists after the fast loader has run.
- [boot_c64cart_8000_8000] The LOAD at $8065 relies on the secondary address 15 set by $0B45 (nonzero -> load to the file's own address), so 'ea' lands at $02A8-$030B. The cartridge then JMPs directly to $02B8 (eaLoadGame), which SETLFS 8,1 / loads 'load' to $9800-$C3FF / JSR $C000 / JMP $0800. The BASIC-vector hijack in 'ea' ($0300-$030B = $02B8) is only needed on the plain C64 LOAD"EA",8,1 path.
- [boot_c64cart_8000_8000] The cartridge mimics the standard power-on memory configuration without entering BASIC: MEMTOP $A000, MEMSTR $0800 ($0282), HIBASE $0400 ($0288), RESTOR + CINT. Screen is blanked (DEN bit cleared) with blue border/background; the $C000 boot loader later unblanks it for the title picture.
- [boot_c64cart_8000_8000] Stack pointer is forced to $F0 before the LOAD (the reset code would have used $FF; $F0 leaves room below for nothing in particular, most likely copied from EA's generic cartridge template).
- [boot_c64cart_8000_8000] At run time $8000-$87FF is overwritten by overlay variants A/B; the XREF entry L_806A (branch target from $8065 in game/ovl_6F00_B) belongs to the overlay, not to this cartridge image.
- [boot_c64cart_8000_8000] No zero-page variables of the game are used; the only zero-page access is the wholesale clear of $0002-$0101.

## boot/eaBootFile

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 02B8 | eaLoadGame | Stage-1 boot stub reached through the hijacked BASIC vectors (normally IMAIN at $0302 after BASIC prints READY. following LOAD"EA",8,1). Sets logical file 8 / device 8 / secondary address 1, names the file "LOAD" (4 chars at $02EC), clears the KERNAL message flag so no SEARCHING/LOADING text appears, then KERNAL-LOADs "load" to its own header address $9800-$C3FF. On success it JSRs the boot loader entry at $C000 (-> bootMain $C145, which shows the title picture, installs the drive fast loader and fills $0800+ with the game) and finally JMPs to gameEntry $0800. On a LOAD error (C=1) it skips the boot loader and jumps to $0800 anyway - the original error handler that followed was patched away (see $02D8). | none (entered by JMP via the BASIC vector table, BASIC stack in place); reads filename at $02EC | never returns; zp $9D (MSGFLG) = 0; memory $9800-$C3FF loaded; then $C000 boot path and JMP $0800 | high |
| 02DA | deadLoadErrorRetry | Unreachable remnant of the original load-error handler. Decodes as LDA #$12 / STA $0401 / STA $0402 / STA $0404 / LDA #$0F / STA $0403 / BNE $02B8, i.e. writes screen codes 'R','R','O','R' into the top-left screen row (the preceding LDA #$05 / STA $0400 = 'E' was overwritten by the JSR $C000 + JMP $0800 patch, leaving the orphaned operand bytes $00,$04 at $02D8-$02D9) and then loops back to retry the whole load. The BCS at $02D0 originally landed here; in this build it lands on JMP $0800 instead. | none | would write screen RAM $0400-$0404 and branch to $02B8; never executed in this build | medium |

Data:

- `$02A8` **strayBasicStub** (unknown, 16 bytes): Tokenised BASIC program text: link $080D, line 10, token $93 LOAD, "EA",8,1, $00, then $00,$00 end-of-program. Formatted as if it lived at $0801 (the link only matches a 12-byte line, e.g. LOAD"EA",8 without ,1, so it was probably edited by hand). Never executed - it merely pads the file so that the vector table lands on $0300. The seed note places the line at $02AA; it really begins at $02A8 with its link word.
- `$02D8` **leftoverErrorHandler** (unknown, 20 bytes): $02D8-$02D9 = orphaned operand ($0400) of an overwritten STA $0400; $02DA-$02EB = valid but unreachable 6502 code that writes 'ERROR' screen codes to $0400-$0404 and BNEs back to $02B8 (see routine deadLoadErrorRetry).
- `$02EC` **eaFileName** (text, 4 bytes): "LOAD" - PETSCII name of the 45-block second-stage file passed to SETNAM (length 4).
- `$02F0` **fileNamePadding** (unknown, 16 bytes): "00" ($30,$30) followed by 14 zero bytes; probably the tail of a longer name from the mastering template (e.g. LOAD00) plus padding up to the vector table. Unused.
- `$0300` **basicVectorHijack** (wordTable, 12 bytes): Six little-endian words, all $02B8, overwriting the BASIC vectors IERROR $0300, IMAIN $0302, ICRNCH $0304, IQPLOP $0306, IGONE $0308, IEVAL $030A. Whichever path BASIC takes after the LOAD (READY main loop, an error, tokenising, listing, executing, evaluating) ends up in eaLoadGame. The table extends to $030B, 4 bytes past the nominal chunk end, but is fully contained in lines 19-55 of the file.

Insights:

- [boot_ea_02A8] Boot chain, stage 1: LOAD"EA",8,1 puts this 100-byte image at $02A8-$030B. BASIC prints READY. and executes JMP ($0302) -> $02B8. eaLoadGame then loads "LOAD" ($9800-$C3FF, secondary address 1 so the file header supplies the address), JSR $C000 (bootEntry = JMP bootMain $C145), JMP $0800 (gameEntry -> mainStart $0A6C).
- [boot_ea_02A8] The boot loader at $C000 returns to $02D5, so $02B8-$02D7 must survive the fast load; it does because the loader only fills $0800+ and the high pages. The game itself later overwrites $0200-$03FF from track 18 s15-16 (sub_10BB at $0A84) and then calls the new $0300 code at $0A87 - every XREF hit on $02AB/$02D5/$0300-$0307 concerns that runtime block, not this file.
- [boot_ea_02A8] The load-error path is effectively broken: BCS $02D0 -> JMP $0800 with nothing loaded. Inferred (medium confidence): because the ROMs are still banked in and $0800 holds $00, the resulting BRK goes through the KERNAL BRK handler -> BASIC warm start -> JMP ($0302) -> $02B8, so in practice a failed load just retries; RUN/STOP-RESTORE during the KERNAL LOAD does the same since only the $0314+ KERNAL vectors are restored, never the $0300 BASIC vectors.
- [boot_ea_02A8] $02D8-$02EB is a fossil of the generic EA loader template: an 'ERROR' screen-code writer plus retry loop that was overwritten when JSR $C000 / JMP $0800 were patched in. The first 16 bytes of the file ($02A8) are an equally unused tokenised '10 LOAD"EA",8,1' BASIC line.
- [boot_ea_02A8] MSGFLG ($9D) = 0 is the only zero-page write in the chunk; it is a KERNAL variable, not game state, and should not be given a game-specific name in zeropage.inc.

## boot/fastLoaderC000

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| C000 | bootEntry | Public entry of the boot loader: JMP bootMain ($C145). Called once by eaLoadGame ($02D2) after the KERNAL has loaded the file 'load' to $9800-$C3FF. Emitted as .byte in the listing but it is a 3-byte JMP. | none | never returns to the caller (see bootMain) | high |
| C004 | bootLoadSectors | Read A consecutive sectors from track Y starting at sector X into the address set by bootSetDest, one page per sector. On a drive error it stores the status byte in bootLoaderStatus, turns the border red and retries the same sector forever; a good sector turns the border black. Public API used five times by bootMain; the game keeps an identical private copy at $0804. | A = sector count, X = first sector, Y = track, bootDestLo/Hi = destination | C = 0, X = next sector, Y = track, ldCount = 0, bootDestHi advanced by A pages, I flag restored (PHP/PLP), border colour changed | high |
| C031 | bootLoadSectorsFail | Dead 3-byte error exit PLP / SEC / RTS (emitted as .byte). It is the counterpart of writeSectorsFail ($0840) in the game's copy; nothing in the boot loader branches to it because read errors retry forever. | none | C = 1 (unreachable) | high |
| C034 | bootSetDest | Set the next transfer address for bootLoadSectors by patching the two operand bytes bootDestLo/bootDestHi. | X = destination low byte, Y = destination high byte | bootDestLo, bootDestHi | high |
| C03B | bootExchangeByte | Full-duplex 1-bit-at-a-time byte exchange with the drive over CIA2 port A: shifts the byte in A out LSB first on DATA OUT (bit 5) with CLK OUT (bit 4) as strobe while shifting the drive's reply in from DATA IN (bit 7) using CLK IN (bit 6) as handshake (the drive side does the same in $034B). Used to send the command byte, track and sector and afterwards, with a dummy byte, to fetch the status. Seed name bootSendByte; the seed's 'MSB first' is wrong (LSR D_C087 sends bit 0 first). | A = byte to send | A = byte received (LSB first), Z reflects it, C = 0, X = 0, bootSendShiftReg = 0, bootRecvShiftReg = received byte | high |
| C08A | bootReadOneSector | Read one sector: stores track/sector, stops CIA1 timer A (no jiffy IRQ during the timed transfer), calls bootReadSectorBody and restarts timer A. Called only by bootLoadSectors. | Y = track, X = sector, bootDestLo/Hi | C = 1 on error (A = status byte), C = 0 on success; ldTrack, ldSector set; I flag set; CIA1_CRA = 1 | high |
| C09D | bootReadSectorBody | Protocol body of a sector read: sends $80, track, sector; saves VIC_SPR_ENA and the VIC bank bits of CIA2_PRA (ldCia2Bits), raises DATA OUT and clears CLK OUT / ATN / TXD, patches the raster threshold of the receive loop to (VIC_CTRL1 and 7) + $2F, waits for CLK IN to drop, disables sprites, copies bootDestLo/Hi to ldDestPtr, receives 256 bytes, restores sprites and exchanges a dummy byte to get the status (0 = ok). | ldTrack, ldSector, bootDestLo/Hi, VIC_CTRL1 (y-scroll) | C = 0 and Z = 1 on success, C = 1 on error (A = status); 256 bytes stored at the destination; bootSavedSprites, ldCia2Bits, ldDestPtr, bootRasterThreshold updated | high |
| C0EF | bootReceiveSectorData | Timing-critical 2-bit parallel receive loop, identical to $0950 in the game. Waits for CLK IN high, then for each of 256 bytes: waits until the raster line is not the line before a bad line (relative to the patched threshold), drops CLK OUT to request a byte, reads four 2-bit pairs from bits 6/7 at fixed cycle offsets (LSB pair first, EOR-combined after LSR LSR shifts), raises DATA OUT to acknowledge and stores the byte via (ldDestPtr),Y. 'bit zp_80' and the NOPs are pure delays. | ldCia2Bits (VIC bank bits to keep in CIA2_PRA), ldDestPtr, bootRasterThreshold (operand at $C0FE) | 256 bytes at (ldDestPtr); Y = 0; CIA2_PRA left with DATA OUT set | high |
| C145 | bootMain | The boot sequence: blank screen, noise on SID voice 3 (random source), show the title picture, set S=$FD via the patched TXS, init modem and drive, B-E the drive bootstrap, disable IRQs, $01=$35 with NMI/IRQ vectors to rtiVector, then load tracks 22-27 (18 sectors each) to $0800, copy $6F00-$72FF to $0400, load track 33 s0-16 + track 32 s8-15 to $6F00 (overlay B), track 28 s0-12 to $9300, track 29 s11-15 to $FA00 and move that block up by $1B8 to end at $FFD1; finally stores a SID noise sample in randomSeed. Its RTS is an obfuscated jump: the stack holds $60 (pushed by the patched PHA at $C1B1, the value is the RTS opcode read from bootDone) under $04 (stored at $01FE by bootShowTitlePicture), so execution continues at $0461 (memory checksum in the $0400 copy) which ends with JMP $0800. It never returns to eaLoadGame. | disk in drive 8, title data at $9800-$BF3F | memory image of the game ($0400-$87FF, $9300-$9FFF, $FBB8-$FFD1), randomSeed ($09C3), $01=$35, vectors $FFFA/$FFFE = $C229, S = $FE at $0461 | high |
| C229 | rtiVector | Single RTI used as the NMI and IRQ target ($FFFA/$FFFE) while the loader runs with the ROMs banked out. | none | none | high |
| C22A | bootDisableIrqs | SEI, mask and acknowledge all CIA1/CIA2 interrupts ($7F to both ICRs, then read them) and clear VIC_IRQ_MASK. Leaves the I flag set; callers CLI later. | none | CIA1_ICR, CIA2_ICR, VIC_IRQ_MASK; I = 1 | high |
| C23F | bootShowTitlePicture | Disables IRQs, patches $C168 to TXS and $C1B1 to PHA (the obfuscated exit), zeroes the sprite registers, VIC IRQ/collision registers, selects VIC bank 2 (CIA2_PRA = $05, DDR bits 0-1 out), writes $04 to $01FE (high byte of the fake return address), copies 4 pages of colour RAM $9800->$D800 and screen RAM $9C00->$8C00 with self-modifying loops, then sets multicolour bitmap mode (VIC_CTRL2 = $D8, VIC_MEM_PTR = $38 -> screen $8C00 / bitmap $A000, VIC_CTRL1 = $3B) and CLI. | title data at $9800 (colour), $9C00 (screen), $A000 (bitmap) | VIC registers, COLOR_RAM, $8C00-$8FFF, $01FE = $04, code bytes $C168 and $C1B1 patched, loop operands $C273/$C276/$C28A/$C28D advanced; I = 0 | high |
| C2AB | bootDriveInitialize | OPEN 15,8,15,"I0" (initialise drive 0), wait 4 frames, CLOSE 15. Uses the KERNAL (ROMs still banked in). | strInitDrive | drive initialised; A/X/Y clobbered | high |
| C2CA | bootWaitFrames | Wait Y video frames by watching bit 7 of VIC_RASTER go high and then low again once per iteration. | Y = number of frames | Y = 0 | high |
| C2DA | bootModemInit | MEMTOP = $9700 (so the RS-232 buffers allocated by OPEN stay below the loaded file), OPEN 2,2,0 with rs232Params ($06 = 300 baud 8N1, $00 = 3-line handshake), CHKOUT 2, send CR, wait 20 frames, send the 0-terminated Hayes string 'ATE0S0=0' CR, wait 60 frames, CLRCHN, CLOSE 2. Silences a connected modem (echo off, no auto-answer); harmless without one. | rs232Params, strHayesInit | RS-232 channel opened and closed; KERNAL MEMTOP lowered | high |
| C32C | bootBlockExecute | SEI, saves CPU_PORT and banks the ROMs in (ORA #$03), opens buffer channel 2 on the drive with filename '#' (SECOND $F2) and then opens channel 15 with the command 'B-E 2 0 1 17' (SECOND $FF): the drive loads track 1 sector 17 into buffer 2 and executes it, installing the fast loader in the drive. Falls through into bootSendDosCommand for the second command. | strBlockExecute, strBufferChannel, kernalCurrentDevice ($BA = 8) | drive now runs the fast-loader code; CPU_PORT restored; I = 1 | high |
| C341 | bootSendDosCommand | LISTEN the current device ($BA), SECOND with the secondary address in A ($F0 / channel = OPEN), CIOUT the 0-terminated string at strBlockExecute+Y, UNLSN, then restore CPU_PORT from bootSavedCpuPort. Called once by JSR and once by fall-through from bootBlockExecute. | A = secondary address byte, Y = offset into strBlockExecute, kernalCurrentDevice, bootSavedCpuPort | command sent; CPU_PORT restored | high |
| C373 | leftoverLookupC373 | Unreachable leftover from the shared EA loader (listed as .byte): SEC; SBC #$AC; if A < $10 then X = A, $BE6D = $BE68 = $BCC8,X; RTS. Targets do not exist in this build. | A | $BE6D, $BE68 (dead) | low |
| C387 | leftoverSetC387 | Unreachable leftover: TAY; $BE6D = $FF; $4D = A; RTS. | A | $BE6D, $4D (dead) | low |
| C390 | leftoverSetC390 | Unreachable leftover: $4D = $FF; $BE6C = $FF; falls into leftoverSetC397. | none | $4D, $BE6C, $BE6D (dead) | low |
| C397 | leftoverSetC397 | Unreachable leftover: $BE6D = $FF; RTS. | none | $BE6D (dead) | low |
| C39D | leftoverCheckC39D | Unreachable leftover: returns A = 1 if $51 != 0 or ($48 / $57) == 0; otherwise if $48 == 0 sets $51 = $0A, $BE65 = 0, $57 = 0, else $57 = $48; RTS. | $48, $51, $57 | A, $51, $57, $BE65 (dead) | low |
| C3BC | leftoverStoreBE65 | Unreachable leftover: STA $BE65; RTS. | A | $BE65 (dead) | low |
| C3C4 | leftoverCheckDiskId | Unreachable, truncated original of the game's checkDiskId ($0FB1): saves zp $04/$05, calls $CBED (setDest $BA00) and $CBF2 (read 1 sector, track 18 sector 0 = BAM) from a build where the loader lived at $CBxx, then compares the disk id at $BAA2/$BAA3 with bootDiskIdTable entries 'EA' (zp $18 = 2) and 'OZ' (0), branching to $C411 which lies beyond the end of the file. | bootDiskIdTable, BAM at $BA00 (in that build) | zp $18 = matched index / 2, branches outside the file (dead) | medium |

Data:

- `$C132` **strBlockExecuteCopy** (text, 13 bytes): Second, unused copy of the 0-terminated DOS command 'B-E 2 0 1 17' (the labels sub_C133/sub_C137 come from runtime-overlay callers, not from this text)
- `$C13F` **strBufferChannelCopy** (text, 2 bytes): Unused copy of the 0-terminated buffer-channel filename '#'
- `$C143` **bootUnusedC143** (unknown, 1 bytes): Zero byte between bootDestHi and bootSavedSprites, unused
- `$C219` **strGameChar** (text, 9 bytes): 0-terminated 'GAMECHAR' - leftover from the shared EA loader, not referenced
- `$C222` **strHmem** (text, 7 bytes): $00, 'HMEM', $00, $00 - leftover from the shared EA loader, not referenced
- `$C2D8` **strInitDrive** (text, 2 bytes): 'I0' drive initialise command (length passed explicitly to SETNAM)
- `$C320` **rs232Params** (byteTable, 2 bytes): RS-232 control/command bytes for OPEN 2,2,0: $06 = 300 baud, 8 data bits, 1 stop bit; $00 = 3-line handshake, full duplex, no parity
- `$C322` **strHayesInit** (text, 10 bytes): 0-terminated 'ATE0S0=0' + CR sent to a modem (echo off, auto-answer off)
- `$C35F` **strBlockExecute** (text, 13 bytes): 0-terminated DOS command 'B-E 2 0 1 17' (block-execute channel 2, drive 0, track 1, sector 17) sent to channel 15
- `$C36C` **strBufferChannel** (text, 2 bytes): 0-terminated '#' used as the filename when opening buffer channel 2 (reached as strBlockExecute+$0D)
- `$C36E` **bootPaddingC36E** (unknown, 3 bytes): Three zero bytes, padding
- `$C372` **bootStrayC372** (unknown, 1 bytes): Single byte $13, probably the tail of an instruction of the leftover code that preceded it in the original loader
- `$C3C0` **bootDiskIdTable** (text, 4 bytes): Two 2-character disk ids 'OZ' (game disk) and 'EA' (data disk) used only by the unreachable leftoverCheckDiskId; the live copy is diskIdTable at $0FAD

Insights:

- [boot_loader_C000_C000] The loader never returns to eaLoadGame ($02D5 JMP $0800 is dead). bootShowTitlePicture patches $C168 to TXS (S = $FD) and $C1B1 to PHA and stores $04 at $01FE; bootMain then pushes $60 (the RTS opcode read from bootDone) at $01FD, so the final RTS pops $0460 and continues at $0461. That code (in the $0400 copy of track 27 s13-16, still listed as .byte in game/table_0400.s) checksums $0402-$87FF with ADC/EOR/ASL, stores the sum at $8800 and sum EOR $0400 at $0B7E, restores randomSeed and does JMP $0800 with Y = 2, S = $FE. The VICE snapshot (SP=$FE, Y=2, $01FD=$60, $01FE=P pushed by the PHP at $0485) confirms this. The seed's 'obfuscation, no effect' note for $C1B1 is wrong.
- [boot_loader_C000_C000] Fast-loader protocol details: every byte of a command is exchanged full duplex, LSB first, one bit per CLK handshake (bootExchangeByte); the drive answers the command byte with $01 (drive $0561) and the C64 fetches the final status by exchanging a dummy byte (the value in A, here the saved sprite mask). Status 0 = ok; non-zero values are stored in bootLoaderStatus ($C003 / $0803 in the game).
- [boot_loader_C000_C000] Sector data arrives as four 2-bit pairs per byte on CIA2_PRA bits 6/7, LSB pair first, combined with LSR LSR / EOR steps; bits 2-5 of CIA2_PRA (TXD, ATN, CLK OUT, DATA OUT) are held at 0 during the reads so only the VIC bank bits (ldCia2Bits) need EORing out. A byte transfer is started only when (VIC_RASTER - bootRasterThreshold) and 7 != 0, i.e. not on the line before a bad line; bootRasterThreshold = y-scroll + $2F.
- [boot_loader_C000_C000] All strings in the boot loader are 0-terminated (Hayes string, DOS commands); the bit-7 terminated convention belongs to the game, not to this file.
- [boot_loader_C000_C000] Load map performed by bootMain: tracks 22-27 s0-17 -> $0800-$73FF; $6F00-$72FF copied to $0400-$07FF; track 33 s0-16 -> $6F00-$7FFF and track 32 s8-15 -> $8000-$87FF (overlay variant B); track 28 s0-12 -> $9300-$9FFF; track 29 s11-15 -> $FA00-$FEFF then moved up by $1B8 to $FBB8-$FFD1. The move loop copies 16 pages because X still holds ldSector (= $10) from the preceding bootLoadSectors; the extra pages below $FA00 are copied to $EFD2-$FBB7 and are garbage later overwritten.
- [boot_loader_C000_C000] Every copy/move loop in this file is self-modifying and never re-initialised ($C198, $C1B5/$C1B8, $C202/$C205, $C273/$C276, $C28A/$C28D, $C0FE), so the boot loader is strictly single-use; the $C000 page is replaced by the runtime overlay (track 30) anyway.
- [boot_loader_C000_C000] Hardware state left for the game: $01 = $35, $00 = $2F, NMI/IRQ vectors = $C229 (RTI), CIA1/CIA2 interrupts masked, VIC_IRQ_MASK = 0, CIA1 timer A running (CRA = 1), VIC bank 2 with multicolour bitmap title picture at $A000 (screen $8C00, VIC_CTRL1 = $3B, VIC_CTRL2 = $D8, VIC_MEM_PTR = $38), SID voice 3 = noise at frequency $FFFF (random source at $D41B), I flag clear.
- [boot_loader_C000_C000] XREF.txt conflates this file with the runtime overlay at $C000 (both tagged game/ovl_C000): the callers listed for sub_C030, sub_C06A, sub_C075, sub_C07A, sub_C097, sub_C099, sub_C0D7, sub_C0E8, sub_C121, sub_C12B, sub_C133, sub_C137, sub_C17D, sub_C1F0, sub_C307, sub_C34B, sub_C367, sub_C37F, sub_C397, sub_C39F, sub_C3AB, sub_C3E0 and the D_C0B9/D_C0BB/D_C22B/D_C23B/D_C305/D_C33A/D_C33D/D_C3F2 writers refer to the overlay's code, and most of those labels land in the middle of instructions or inside text in this file. Only the call from $02D2 to $C000 is a real reference to the boot loader.

## drive/driveBootstrap

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 0600 | driveBootstrapEntry | Entry point of the 1541 bootstrap sector, executed by DOS 'B-E 2 0 1 17' at the first byte of whatever buffer channel 2 was given (so it runs at an unknown page, not necessarily $0600). It disables IRQs, writes $08 to VIA1 port B (CLK OUT asserted, DATA OUT released, ATNA off), plants an RTS ($60) at $0300 (buffer 0, free to clobber) and JSRs to it; TSX / LDA $0100,X then reads the return-address high byte left on the stack, which is the page the code is currently running in. That page is copied verbatim to $0600 (buffer 3) and execution continues with an absolute JMP to $0624 inside the copy. | None beyond DOS having loaded the sector into a channel buffer and jumped to its first byte. Uses the stack to discover its own page. | $0300 = $60 (RTS, later overwritten by the fast loader read); $14/$15 = pointer to the original page (lo always 0); $0600-$06FF = copy of the sector; VIA1_PRB_SERIAL = $08; I flag set. Falls into queueFastLoaderReads at $0624 via JMP. | high |
| 0624 | queueFastLoaderReads | Copies the 12-byte jobQueueInitTable to drive zero page $00-$0B: job code $80 (read sector) for buffers 0,1,2 ($0300,$0400,$0500), $00 (no job) for buffers 3,4,5, and track/sector pairs 1/18, 1/19, 1/20 for buffers 0-2. Enables IRQs so the DOS job controller runs, then spins while any of the three job bytes still has bit 7 set. When all three have finished it ORs the result codes and requires exactly $01 (all OK); any DOS error code makes the OR differ and the whole queue is re-issued (infinite retry, no error exit). On success it calls ROM_INIT_DRIVE ($D042, re-reads the BAM / disk id) and jumps to the freshly loaded fast loader at $0300. | jobQueueInitTable at $0641. Requires that the code is already running from $0600 (the DOS reads target buffers 0-2, which would otherwise overwrite it). | $0300-$05FF = fast loader (track 1 sectors 18-20, unencrypted); $00-$0B = job queue state; I flag cleared; disk re-initialised. Does not return - ends with JMP $0300. | high |

Data:

- `$0641` **jobQueueInitTable** (byteTable, 12 bytes): Image of drive zero page $00-$0B copied by queueFastLoaderReads: $80,$80,$80 (read jobs for buffers 0-2), $00,$00,$00 (no job for buffers 3-5), then track/sector pairs $01,$12 / $01,$13 / $01,$14 (track 1 sectors 18,19,20).
- `$064D` **eaCopyrightMessage** (text, 179 bytes): Plain ASCII, no terminator, fills the sector to $06FF and is never displayed: '(C)1988 ELECTRONIC ARTS, 1820 GATEWAY DR, SAN MATEO,CA 94404. THE AGE OF MODEM WARFARE HAS BEGUN.  MORE TELEGAMES COMING SOON FROM EA.  WRITE TO US & TELL US YOU LIKE MODEM GAMES.'

Insights:

- [drive_bootstrap_T1S17_0600] The bootstrap is position independent: DOS executes B-E from whichever buffer channel 2 was allocated, so the code locates its own page with JSR to a planted RTS at $0300 followed by TSX / LDA $0100,X (the high byte of the return address is still on the stack after RTS). It then copies itself to buffer 3 ($0600) because the three reads it is about to queue land in buffers 0-2 ($0300-$05FF) and would overwrite it if it stayed there.
- [drive_bootstrap_T1S17_0600] The listing label D_0600 is really the code entry point (suggest driveBootstrapEntry). Loop labels inside the chunk: L_0619 = page copy loop, L_0624 = (re)queue the three read jobs, L_0626 = job-table copy loop, L_062F = wait-for-jobs loop.
- [drive_bootstrap_T1S17_0600] The job completion check is 'OR of the three result bytes == $01': while any job is pending its byte still has bit 7 set (BMI loop); any DOS error code ($02..$0B) makes the OR differ from $01 and all three reads are re-queued. There is no error exit or LED indication - a bad track 1 hangs the drive in a retry loop, and the C64 side (bootLoadSectors) waits on the CLK line indefinitely as well.
- [drive_bootstrap_T1S17_0600] Track 1 sectors 18-20 (the fast loader at $0300-$05FF) are fetched with plain DOS job-code reads, so they cannot be encrypted; the XOR sector cipher only applies to data read later by the fast loader itself. The fast loader's first instruction (LDA #$00 / STA $1800) releases the CLK line that the bootstrap asserted with $08.
- [drive_bootstrap_T1S17_0600] $0300 is patched with $60 before the JSR; this is harmless because the same byte is overwritten moments later by the sector-18 read. ROM_INIT_DRIVE ($D042) is called after the loads so the BAM buffer ($0700) and disk id ($12/$13) are valid when the fast loader starts.
- [drive_bootstrap_T1S17_0600] The byte table at $0641 is shown in the listing as one .byte run that continues into the ASCII message; the table proper is only 12 bytes ($0641-$064C) and the text starts at $064D (179 bytes to the end of the sector). The text is ordinary ASCII with no bit-7 terminator, unlike the game's strings.
- [drive_bootstrap_T1S17_0600] Drive zero page $14/$15 is used as the copy pointer; in stock 1541 DOS these hold the drive-1 disk id, which is never used on a single-drive unit, so nothing is disturbed.

## drive/driveFastLoader

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 0300 | fastloaderMain | Entry point of the 1541 fast loader; the bootstrap sector (T1 s17) reads T1 s18-20 to $0300-$05FF and JMPs here. Idles the serial lines, sets the default track/sector ($06=18,$07=0), issues disk-controller job $B0 on buffer 0 and waits for it to finish, then falls into commandDispatchLoop. | none (entered from the bootstrap) | zp_06=$12, zp_07=$00, zp_00 job issued; falls through to $0313 | high |
| 0313 | commandDispatchLoop | Top of the drive command loop. Resets the stack (SP=$45), receives a command byte from the host via receiveCommand, and dispatches: $C0 resets the drive (JMP through reset vector), $60 goes to handleWriteCommand, anything else is a read (receive track and sector, read the sector, send it back, then report status 0). Re-entered after every operation. | command byte from receiveCommand | zp_50=0; branches to read/write/reset handlers | high |
| 0342 | setHeadReadMode | Sets VIA2 PCR bits 1-3 (ORA #$0E) to put the read/write head electronics into read mode. Called by prepareHeadAndVerifyId before reading or writing a sector. | VIA2_PCR | VIA2_PCR updated | medium |
| 034B | exchangeByteWithHost | Core full-duplex serial byte transfer with the C64. Clocks the byte in A out to the host one bit at a time (LSB first) on VIA1 serial bit1 while simultaneously shifting a received byte in on bit0, using a two-phase CLK/ACK handshake on bits 2/3. This is the drive-side counterpart of the C64 routine sendByteToDrive ($089C). Used for all command/parameter bytes, the status byte, and (in the write path) the 256 data bytes from the host. | A = byte to send (into zp_90) | A = byte received from host (zp_8F); uses zp_14 as handshake temp; X clobbered | high |
| 0391 | sendStatusAndLoop | Sends the status/error byte in A to the host via exchangeByteWithHost, then JMPs back to commandDispatchLoop. Every error path (header not found=$02, checksum=$05, write protect/id error=$08) jumps here with its code in A; the normal read path jumps here with A=0. | A = status byte | status sent to host; loops to $0313 | high |
| 0397 | seekSectorAndSyncData | Finds the requested sector header on disk (findSectorHeader) then waits for and reads into the data-block sync (waitForSyncByte). Called at the start of readSectorFromDisk to position the read at the sector's data field. | zp_3D drive index, zp_12/zp_13, zp_32 header pointer | positioned at data field; V clear, Y=0; error $02 via sendStatusAndLoop | medium |
| 039D | findSectorHeader | Builds the expected 8-byte GCR header image (from the id bytes zp_12/zp_13, the track/sector bytes via (zp_32), and their XOR checksum) with ROM_MAKE_HEADER_GCR into the zp_24 buffer, then reads raw GCR bytes from VIA2 comparing 8 bytes at a time. Returns when the matching header passes under the head; after $5A failed tries reports error $02. Also used by writeSectorToDisk to locate the sector before writing. | zp_3D drive index, zp_12/zp_13 disk id bytes, zp_32 header ptr | header located; zp_16-zp_1A header scratch; error $02 via sendStatusAndLoop | medium |
| 03E3 | waitForSyncByte | Arms VIA1 timer 1 as a timeout, waits for a SYNC mark on VIA2 (bit 7 of the disk-control port), then reads the first byte after sync from VIA2 and returns it. On timeout it branches to the error path ($02 via sendStatusAndLoop). Clears V and Y before returning. | VIA1_T1, VIA2 disk port | A = byte after sync, V clear, Y=0; timeout -> error | medium |
| 03FB | readSectorFromDisk | Reads the sector named by zp_06/zp_07 from disk into buffer $0600: waits for the buffer-0 job, verifies the head/id (prepareHeadAndVerifyId), seeks to the data field (seekSectorAndSyncData), reads 256 GCR data bytes into $0600 plus the GCR overflow into $0100, converts GCR->binary and verifies the data checksum against zp_3A (error $05 on mismatch), then falls through to cryptSectorBuffer to decrypt. Invoked by the read branch of the command loop. | zp_06 track, zp_07 sector, zp_31 buffer page | decrypted sector in $0600; zp_3A checksum; error $05 via sendStatusAndLoop | high |
| 044B | sendSectorToHost | Sends the 256 decrypted bytes in buffer $0600 back to the C64. Each byte is split into two nibbles, each nibble translated through nibbleSendTable and clocked out 4 bits at a time on the VIA1 serial port with a handshake (bit0 = clock). This bulk path is received by the C64 routine receiveSectorData ($0950). Ends via finishSendByte. | $0600 buffer (256 decrypted bytes), nibbleSendTable | 256 bytes clocked to host; VIA1 serial port | high |
| 0490 | handleWriteCommand | Write-command ($60) handler: receives the track and sector bytes from the host into zp_06/zp_07, calls writeSectorToDisk, then returns to commandDispatchLoop. | track/sector from host via exchangeByteWithHost | sector written; loops to $0313 | high |
| 04A0 | writeSectorToDisk | Receives 256 data bytes from the host into $0600, encrypts them in place (cryptSectorBuffer, symmetric with decrypt), computes the data checksum into zp_3A, checks the write-protect sense (VIA2 bit 4; error $08 if protected), converts binary->GCR, positions on the header (findSectorHeader), switches VIA2 to write mode, writes the GCR overflow ($0100) and data (zp_30) fields, restores read mode, and sends the status byte. Called by handleWriteCommand. | 256 bytes from host, zp_06/zp_07, zp_30 buffer ptr | sector written to disk, zp_3A checksum; error $08 via sendStatusAndLoop | high |
| 0525 | prepareHeadAndVerifyId | Selects read mode (setHeadReadMode) and validates the drive id bytes: if zp_12 EOR zp_13 EOR $15 is nonzero it branches to the write-protect/id error path ($08). Called at the start of both readSectorFromDisk and writeSectorToDisk. The exact meaning of the $15 constant is a DOS id/consistency check. | zp_12, zp_13 | read mode set; error $08 via sendStatusAndLoop on mismatch | low |
| 0531 | cryptSectorBuffer | The sector cipher: XORs the 256 bytes in $0600 with a self-modifying rolling key (key0 = ROL((track/$C0)<<1 + sector + carry); key[y]=ROL(y EOR key)). Track 18 ($12) is left in plaintext (BAM/directory/map/setup). Symmetric, so the same routine decrypts on read and encrypts on write. This is the cipher documented in overview section 4. | $0600 buffer, zp_06 track, zp_07 sector | $0600 XORed in place (except track 18) | high |
| 0553 | uploadBytesToDrive | Code/data injection loop reached from receiveCommand when the host sends a count byte >= $F0: receives (256 - count) bytes and stores them consecutively into drive RAM starting at $0580 (the RTS-filled scratch area) using self-modifying stores, then falls through into receiveCommand again to fetch the real command. Lets the host push a small routine or table into the drive. | count in patched $057E, bytes from host | bytes written to $0580+; re-enters receiveCommand | medium |
| 0561 | receiveCommand | Receives one command/parameter byte from the host: clears a VIA2 status bit, sends $01 and receives a byte via exchangeByteWithHost, patches it into the LDA at $057D. If the byte is >= $F0 it is a byte-count preamble and control drops to uploadBytesToDrive to inject that many bytes into $0580 before looping back for the real command; otherwise the received byte is returned in A. | host serial input | A = command byte | medium |
| 05DE | finishSendByte | Tail of sendSectorToHost: writes the final value in A to the VIA1 serial port and returns (leaves the serial lines in the idle state after a bulk read). | A, VIA1 serial port | VIA1 serial port set; RTS | medium |
| 05E2 | checksumBufferAndReport | Small 6502 routine that the disassembler classified as data (it sits after an RTS and has no in-file caller). $05E2 does JSR $05E8 / JMP sendStatusAndLoop; $05E8 sums the 256 bytes of $0600 with a ROL+ADC accumulator and returns. Most likely a verify/checksum helper invoked by code injected via uploadBytesToDrive, or dead code. Followed by a hidden signature. | $0600 buffer | A = rolling checksum, reported to host via sendStatusAndLoop | low |

Data:

- `$043B` **nibbleSendTable** (byteTable, 16 bytes): 16-entry nibble translation table used by sendSectorToHost. Entry n = complement of the nibble n with its two outer bits swapped (0->$0F,1->$07,2->$0D,...). Encodes each nibble into the 4 serial bits sent to the C64 during a bulk read.
- `$0582` **driveUploadScratch** (byteTable, 92 bytes): $0582-$05DD filled with $60 (RTS). This is the RTS-filled scratch/patch area; uploadBytesToDrive stores injected bytes starting at $0580 into this region. Pre-filling with RTS makes any un-overwritten byte a harmless return.
- `$05F4` **hiddenSignature** (text, 12 bytes): Hidden signature bytes at the very end of the loader: ASCII fragments 'DaN' and 'SaRa' (Dan Bunten / family names) mixed with a couple of opcode-looking bytes. Not referenced; an easter-egg credit like the EA message in the bootstrap sector.

Insights:

- [drive_fastloader_T1S18_20_0300] This is the 1541-side fast loader (runs in the drive at $0300-$05FF). Its entry $0300 is JMPed to by the bootstrap sector (drive/bootstrap_T1S17.s at $063E) after that sector queues T1 s18-20 -> $0300-$05FF.
- [drive_fastloader_T1S18_20_0300] Command protocol (drive side of overview section 3): the drive loops at $0313 receiving a command byte via receiveCommand. $C0 = reset (JMP reset vector), $60 = write sector, any other value = read sector. Read/write are followed by track then sector bytes; every operation ends by sending a status byte (0 = ok, else error code) back to the host.
- [drive_fastloader_T1S18_20_0300] Error codes returned to the host: $02 = header not found (findSectorHeader/waitForSyncByte timeout), $05 = data checksum mismatch on read, $08 = write protected or id-check failure. The C64 side turns the border red and retries forever on any nonzero status.
- [drive_fastloader_T1S18_20_0300] Two distinct serial transports are used. Command/parameter/status bytes and the write-path data bytes go through exchangeByteWithHost ($034B), a full-duplex bit-banged handshake on the VIA1 serial port (LSB first) that mirrors the C64's sendByteToDrive ($089C). The read-path bulk data uses the faster nibble-encoded path sendSectorToHost ($044B) via nibbleSendTable, received by the C64's receiveSectorData ($0950).
- [drive_fastloader_T1S18_20_0300] cryptSectorBuffer ($0531) is the canonical implementation of the disk cipher documented in overview section 4: a self-modifying rolling XOR keyed on track and sector, skipped for track 18. The same routine encrypts on write and decrypts on read (symmetric).
- [drive_fastloader_T1S18_20_0300] Code-injection facility: if the host sends a command byte >= $F0, receiveCommand treats it as a count and uploadBytesToDrive ($0553) pokes (256 - count) bytes into drive RAM at $0580 (the RTS-filled scratch area) before fetching the real command. The comm module exercises this same >=$F0 path from the C64 side (ovl_E000 around $EA0D-$EA29, sending through sendByteToDrive), so the game can patch/extend the drive at run time - probably part of the modem-play disk handling.
- [drive_fastloader_T1S18_20_0300] Because the drive's address space ($0300-$05FF) overlaps C64 addresses, XREF.txt entries for $0300/$0313/$04A0 refer to unrelated C64 code (game/setup_0200, table_0400), not to this drive routine - do not cross-reference them.

## game/comcenScreens6F00

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 6F00 | drawDroneHeadingPanel | Draws the top-left panel of the drone screen: window $0D (left 1, top 3, 9x9), the 7x7 character picture at $97D1 at row 4 column 2, the 5x5 picture at $97B6 at row 5 column 3 and a 3x3 colour fill of $6E at row 6 column 4. Sprites 3 and 5 (heading line and direction arrow, positioned at X=$37/$38 Y=$64 by the screen-2 entry) sit on top of this panel, so it is the drone heading / compass indicator. | none (colour state D_2A29-D_2A2C as left by buildDroneScreen) | frame, characters and colour cells drawn; D_2A2D=6, D_2A2E=4, D_9221/D_9222 set by the block drawer | medium |
| 6F36 | drawDroneFuelPanel | Draws the bottom-left panel of the drone screen: window $0E (left 1, top 12, 9x9), the 7x7 picture at $97D1 at row 13 column 2 in the fixed colour $BC, then the three label characters $57,$58,$59 at row 17 columns 4-6 in colour $0C (the caption of the fuel read-out that drawDroneFuelDigits later prints at row 15). | none | frame/characters drawn; D_2A29 and D_2A2A cleared on exit; D_2A2B = 0; zp_18=$11, zp_19=6 | medium |
| 6F89 | drawDroneRadarFrame | Builds the radar-noise masks (buildRadarNoiseMasks), draws the two nested frames around the radar area (windows $0F: left 11, top 4, 18x16 and $10: left 12, top 5, 16x14) and falls into clearRadarWindowBlack, which blanks the 14x12 radar area and centres the view on the own comcen. Called once by buildDroneScreen. Note: sub_6F91 ($6F91) is only a fall-through point in variant B; the JSRs to it in XREF come from overlay A. | D_9236 comcenUnit | frames drawn, D_9210 masks, radar area cleared, zp_A5/zp_A6 = comcen - (6,5), D_920F = 0, D_7134 = 0 | high |
| 6F99 | drawDroneFuelDigits | Shows droneFuel (D_9220) as three tall two-character digits at row 15 columns 4-6: converts it to decimal with sub_C428 ($C44E hundreds, $C44F tens, $C450 ones) and draws them with drawTallGlyph in a fixed colour chosen by the level: $30 for >= 21, $B7 for 11-20, $21 below 11; for even values the two colour nibbles are swapped so a low read-out pulses as it counts down. Called at screen build, at launch (fuel 100), every radar update and when the drone is lost (fuel 0). | D_9220 droneFuel | three glyph pairs at rows 15-16; D_C44E-D_C450; D_2A29/D_2A2A cleared; zp_18=$0F, zp_19=6, zp_1A scratch | high |
| 6FF5 | drawDroneCountPanel | Draws the top-right panel of the drone screen: full-screen text window (sub_C0D7), window $11 (left 29, top 3, 11x7) with inner corner marks, window $12 (left 32, top 4, 5x4), a 3x2 colour fill of $30 at row 5 column 33 (the box for the drones-left digit) and the caption 'DRONE' ($868C) at row 7 column 32 in text colour $CF with OR draw mode, reset afterwards. | none | frames/text drawn; zp_3D=$CF, zp_3F=$20, zp_40=7; draw modes reset | high |
| 7036 | drawDroneCountDigit | Draws dronesLeftDisplay (D_9233) as one tall digit at row 5 column 34 in fixed colour $30, or in colour 0 (invisible) when no drones are left. The 'bne L_7055' at $7053 is a no-op branch (both paths continue at $7055). Called by buildDroneScreen and after each launch. | D_9233 dronesLeftDisplay (0-9) | glyph pair at rows 5-6 column 34; D_2A29/D_2A2A cleared; zp_18=5, zp_19=$22 | high |
| 7061 | blinkDroneCountBox | Blinks the background of the drones-left box: fills the 3x2 colour cells at row 5 column 33 with $30 on odd frames (zp_45 bit 0) or with the nibble-swapped $03 on even frames. The screen-2 loops call it every 11 or 40 frames while fewer than two drones remain. | zp_45 frameCounter bit 0 | 6 colour cells; D_2A2D=5, D_2A2E=$21, D_2A2A; zp_1A scratch | high |
| 708D | drawLaunchStrikeButtons | Draws the right-hand button panel of the drone screen: window $13 (left 29, top 10, 14x11) with inner corner marks, the 2x2 button icon block at $9804 at row 12 column 34 coloured $05 with the caption 'LAUNCH' ($8691) at row 14 column 32 (colour $CD), and the same icon at row 16 column 34 with 'STRIKE' ($8697) at row 18 column 32 (colour $C7). The screen-2 loops later recolour these 2x2 icons ($05 when pressed, $02 for strike, XOR $08 blink). | none | frame, icons and text drawn; zp_3D, zp_3F, zp_40 set; draw modes reset | high |
| 70F5 | buildDroneScreen | Builds the complete drone screen (game screen 2, entered from $7FE6): clears the colour state D_2A29-D_2A2C, sets border and background to $0C, radarBackgroundColour D_9226 = $0A and zp_41 = 1, then draws the drone count panel, count digit, LAUNCH/STRIKE buttons, heading panel, fuel panel, fuel digits and the radar frames, finally the status-line window (sub_28E1) and jumps to L_C5D7 to re-show the current status message. | D_9220, D_9233, D_9236 | whole screen redrawn; D_9003/D_9004 = $0C, D_9226 = $0A, zp_41 = 1, D_2A29-D_2A2C = 0 | high |
| 7135 | clearRadarWindowGrey | Sets radarWipeColour D_7134 = $BB and falls into the shared tail at $7141: fills the 14x12 radar area (row 6 column 13) with that colour, clears the ghost countdown D_920F and sets the view origin zp_A5/zp_A6 to the own comcen minus (6,5) so the comcen sits at the radar centre. Called right after a launch; the nonzero D_7134 also makes the following drawRadarIrisWipe animate with 3-frame pauses. | D_9236 comcenUnit, D_F640/D_F6A4 | D_7134 = $BB, radar area colour $BB, D_920F = 0, zp_A5/zp_A6 | high |
| 713C | clearRadarWindowBlack | Same as clearRadarWindowGrey but with D_7134 = 0: blanks the radar area to colour 0, clears D_920F and centres the view on the own comcen. Used at screen build and when a drone flight ends; with D_7134 = 0 a later iris wipe runs without delays. | D_9236, D_F640/D_F6A4 | D_7134 = 0, radar area black, D_920F = 0, zp_A5/zp_A6 = comcen - (6,5) | high |
| 7163 | playDroneLostAnimation | Visual for a drone that is destroyed, detonated or out of fuel: forces fixed colour, switches sprite 4 (comcen rings) off, flashes the radar area white and wipes it (flashRadarAndWipe), then turns sprite 2 (the drone icon) into a filled disc (circles of radius 3,2,1 with spritePlotMode $40) in white and lets it shrink and fade (erase radius 3 -> colour $0F, radius 2 -> $0C, radius 1 -> $0B with 3/3/5/6 game frames between), disables sprite 2, zeroes droneFuel, redraws the fuel digits and waits 30 frames. The 'sta zp_30' at $716A and the store to D_7271 are dead (zp_30 is only used inside magnifyWindow2x, D_7271 is never read). | D_920B (saved but unused) | D_9022 bits 4 and 2 cleared, sprite 2 shape/colour changed, D_91F3 = 0, D_9220 = 0, D_2A29/D_2A2A = 0, ~50 game frames elapse | high |
| 7204 | flashRadarAndWipe | Fills the 14x12 radar area (row 6, column 13) with colour 1 (white), runs 2 game frames, clears the animation flag D_7134 and falls into drawRadarIrisWipe so the wipe closes instantly. Only used by playDroneLostAnimation. | none | radar area white, D_7134 = 0, then see drawRadarIrisWipe | high |
| 7221 | drawRadarIrisWipe | Iris effect over the radar area: draws window frame $16 (the radar area itself, 14x12 at row 6 column 13), then repeatedly draws a frame box from the 9-byte all-$03 tile set at $9848 starting at row 6 column 14 with 12x12 cells, shrinking by two and moving in by one each pass (drawFrameBox decrements the size once, this loop once more) until the height drops below 3, waiting for raster line $B0 before each frame and, when D_7134 is nonzero, running 3 game frames per pass; finally draws the 2x2 block of character $05 at $980A at the centre (row 11, column 19). Colours come from the current colour override ($AA after a launch, 1 during the loss animation). | D_7134 animate flag, D_2A29/D_2A2A colour | radar area covered by concentric frames; D_2A2D = 11, D_2A2E = $13, D_9221/D_9222 clobbered | high |
| 727A | buildRadarNoiseMasks | Fills the 16-byte table radarNoiseMasks (D_9210) with $FF and clears four random bits in each byte (bit chosen by nextGameRandom & 7 through clearBitMaskTable). drawRadarGlyph ANDs glyph rows with eight consecutive masks to give unspotted enemy blips a static/noise look. Called by drawDroneRadarFrame, drawDroneRadarWindow and the screen-1 map redraw ($7926) and unit marker code ($7BA2). | RNG $57-$59 | D_9210-D_921F, zp_1A = 0, Y = $FF | high |
| 72A1 | applyDroneBlast | Hidden as .byte in the listing but real code, called by cmdDroneDetonate ($536B) with A = $19: damages the 2x2 cells whose top-left corner is (zp_74, zp_75) - (x,y), (x+1,y), (x+1,y+1), (x,y+1) - through the inner routine at $72B3. The blast strength is stored in the operand at $7311 so that each hit ends with 'LDA #strength / JSR applyHitToUnit ($3B22)'. | A = blast strength, zp_74/zp_75 = top-left map cell | units in the four cells damaged; $7311 patched; see applyDroneBlastToCell | high |
| 72B3 | applyDroneBlastToCell | Inner part of applyDroneBlast for one cell (zp_74, zp_75): returns if the cell is off the map or holds no unit (readMapCell $25FA, bit 7 clear). With strength >= 15 and game type != 2 (THE BOMB) the unit is stunned (unitStunTable $F960 /= $0F) and halted (haltStunnedUnit $3FAC); if it is the own comcen: updateComcenCloakLock ($5151), then with the DAMAGE option on and not in solo/film mode the comcen is knocked out (D_9242/D_9243/D_9244 = $80, comcenStunStatus $922A = $FF), otherwise message $0C is queued (sub_C566). The 'LDY $9236 / BNE' after the message is always taken because comcenUnit is 49 or 99. Finally applyHitToUnit with A = strength, Y = unit. | zp_74/zp_75 cell, $7311 strength, D_0BA3 gameTypeOptions, D_0B9B, D_9236 | unit damage/stun, possibly knock-out flags; A = strength, Y = unit | high |
| 731E | steerDrone | Moves the locally controlled drone one cell per command exchange (called from sendOutgoingPacket $5639 while droneStateFlags bit 6 is set, before the $8D report is queued). Converts the last joystick direction zp_5E to a clockwise heading through D_7FCE, turns droneHeading (D_920B) one step toward it the short way round (playing sound $10 on a turn), then translates the heading through headingToDirectionTable and the direction delta tables D_20F0/D_1FB2 into dx/dy and adds them to droneCol/droneRow (D_92F6/D_92F7). | zp_5E joystick direction code, D_920B, D_92F6/D_92F7 | D_920B updated (0-7), D_92F6 += dx, D_92F7 += dy, sound $10 on turn; X/Y clobbered | high |
| 7396 | drawRadarGlyph | Plots one 8x8 map glyph at the bitmap cell zp_B4/zp_B5 and writes its colour to (zp_B6),Y: glyph source is $9C6E + (A & $7F)*8 via setTerrainTilePtr. If radarHalfToneFlag D_922D is set (cloaked unit) the even rows are blanked and only odd rows copied; if A bit 7 is set the rows are ANDed with eight consecutive radarNoiseMasks starting at a random index (ghost blip). Then D_922D is cleared, zp_B4 advances by 8 to the next cell, and the colour is stored: on screen 1 the raw zp_32 byte, otherwise (zp_32 << 4) / radarBackgroundColour. The D_2A29 test at $73F9 can never succeed because setTerrainTilePtr has just cleared it. Also called from drawScreen1Cell ($5DF5) in the main program. A/X/Y are preserved via self-modified operands. | A = glyph (bit 7 = noise), X/Y preserved (Y = colour offset), zp_32 radarGlyphColour, zp_B4-zp_B7, D_922D, D_90FB, D_9226 | bitmap cell and colour cell written; zp_B4/zp_B5 += 8; D_922D = 0; A = glyph & $7F, X, Y restored; D_7395 = Y | high |
| 741C | drawDroneRadarWindow | Redraws the 14x12 radar view of the drone screen (character rows 6-17, columns 13-26, one character per map cell) from the view origin zp_A5/zp_A6: sets radarTerrainColour = 8, own/enemy unit colours from D_9224/D_9225, rebuilds the noise masks, fetches the map pointer with getViewportOriginPointer, computes the bitmap pointer from the $9300 row/column tables (row 6, column 13) and the screen RAM pointer (row 6 + 13), waits for raster line $90 and then for each row (X) and column (Y) runs getRadarCellGlyph and drawRadarGlyph. zp_A5/zp_A6 are walked cell by cell for the off-map test and restored from D_90F6/D_90F7 at the end. Called from the per-turn radar update sub_8279. | zp_A5/zp_A6 view origin, map at $F000, unit arrays, D_9224/D_9225 | radar area redrawn; D_9237-D_9239, D_9210, D_90F6/D_90F7, zp_48/zp_49, zp_B4-zp_B7 clobbered; D_2A29 = 0 | high |
| 74C3 | getRadarCellGlyph | Converts map cell byte A at (zp_A5, zp_A6) into a radar glyph and colour (X/Y preserved). Off-map coordinates (>= 40) give one of four hatch glyphs $28-$2B picked by (row+col)&3. Terrain cells >= $40 give glyph cell-$40 in radarTerrainColour (cells below $40, not expected, give cell-$20). A unit cell ($80/index) gives unitGlyphBaseTable[type]+facing in the own colour D_9237; for the enemy half of the unit array (the BCS at $750A is a side-dependent opcode copied from $206B by loadOverlayB) it uses D_9238 if the unit is spotted (unitEnergy bit 7), otherwise the terrain under the unit is shown except that every 1-8th unspotted enemy (countdown D_920F reseeded from the RNG) is drawn as a noise 'ghost' (glyph / $80) in the terrain colour. Dead units show their underlying terrain. A cloaked unit (unitCloakFlags bit 7) sets radarHalfToneFlag so drawRadarGlyph draws it on alternate scanlines. | A = map cell, zp_A5/zp_A6 cell coordinates, D_9237-D_9239, D_920F, unit arrays F76C/F708/F898/F9C4/FAF0, D_0799 | A = glyph index (bit 7 = draw with noise), zp_32 = colour, D_922D set for cloaked units, D_920F updated; X/Y restored | high |
| 7552 | playStrikeZoomAnimation | Effect for a drone strike: zooms into the centre of the radar four times with magnifyWindow2x on the 7x6 cell block at row 9 column 16, running 12, 10, 8 and 6 game frames between steps (D_7551 counts 3..0), waits 6 more frames and then jumps to playDroneLostAnimation. Called from the strike wait loop ($81CD) after sound $11. | none | radar area magnified and then cleared; D_7551 = $FF; see playDroneLostAnimation | high |
| 7587 | runMissileScreen | Entry of game screen 1 (jumped to by the screen dispatcher sub_06BA): clears cursorMovedFlag, copies the own missile count D_92B0 into missilesLeftDisplay, unpacks the direction sprite shapes at $FD70 into $0200 and builds the screen with sub_7A32 (21x17 cell map window at character (3,3) centred on the own comcen at grid cell (10,8)). Continues at missileScreenResume. | D_92B0, D_9236 | D_7586 = 0, D_9232, screen built; never returns (loop) | high |
| 759C | missileScreenResume | Re-entry after the knock-out sequence ($0764) and main loop of the missile screen. Sets screenLockedFlag D_7585 = $80 when the game type is Scrimmage (no comcen) or the own comcen is cloaked (D_9245 bit 7); if not locked redraws the map window (sub_7688), unit markers (sub_7BA2), resets the sweep (sub_78CB) and shows the comcen cross (sprite 2, sub_3383). Loop at $75B9: updateGameFrame; screen switch via sub_06BA unless D_92F9 bit 0; every 9 frames (zp_6F) XOR-blinks the 2x2 button at row 10 column 32 and, with fewer than two missiles, the count box (sub_7AEC); when locked nothing else runs. Otherwise: D_920E refresh -> redraw map and drone detonation effect (sub_79D8); radar sweep (sub_7894); a fire press (sub_0ED5) after the cursor has moved launches a missile if missiles remain - always when missileJamFlag D_9243 is 0, only on a random bit when set: decrement the count, redraw it, fill the button $05, sound $0F, queue command $90 x y at the cursor cell (sub_7992), draw the trajectory (sub_7713), ring burst at the cursor and sub_79A1; else joystick input moves the grid cursor zp_67/zp_68 through sub_76B2 with auto-repeat (10 frames first, then 4, via D_91CA/D_91C9) and sets cursorMovedFlag = $7F. | D_0BA3, D_9245, D_920E, D_9243, D_9232, zp_5D, zp_6C, zp_6F, D_91C9/D_91CA, D_1FA6 | D_7585, D_7586, zp_6C, zp_6F, D_91C9 = $7F, commands queued, sprites/screen updated; never returns | high |
| 7894 | updateRadarSweep | Per-frame driver of the radar sweep animation on screen 1 (the RADAR/MISSILE screen at $7587). When frameTick92FB reaches the self-modified threshold at $7898 it advances the threshold by 2 and performs one step: the step number radarSweepStep ($7891) is the grid row that gets lit (rows 0-16 via paintRadarSweepRow) and row step-14 (when 0..16) is repainted dark, so a 14-row light band wipes down the 21x17 radar window. Steps 0-30 take about 62 frames; at step 31 the sweep is restarted by restartRadarSweep. Called from the screen-1 main loop ($75F7) and from the missile flight animation ($77F9). | frameTick92FB ($92FB), radarSweepStep ($7891), radarSweepNextTick ($7898 immediate operand) | $7891 incremented (or reset), $7892 = row being painted, $7898 += 2, screen RAM colour cells of the radar window rows; A/X/Y clobbered | high |
| 78CB | restartRadarSweep | Restarts the radar sweep: clears radarSweepStep, radarSweepDarkFlag, frameTick92FB and the tick threshold at $7898. If the comcen radar system is damaged (comcenRadarStatus $9243 nonzero) a random draw (nextGameRandom bit 7) makes the whole next sweep dark (radarSweepDarkFlag = $FF), i.e. the damaged radar fails to show anything on about half of its sweeps. Called when screen 1 is built ($75B3) and at the end of every sweep. | $9243 comcenRadarStatus, RNG $57-$59 | $7891 = 0, $7893 = 0 or $FF, $92FB = 0, $7898 = 0; X clobbered | high |
| 78E7 | paintRadarSweepRow | Repaints the screen-RAM colour bytes of the 21 cells of radar grid row A (0-16, screen row A+3, columns 3-23): computes the screen RAM ($8C00, via the row tables D_9385/D_939E whose index 0 is row 3) and colour RAM ($D800) pointers, then for each cell writes $F0 (multicolour %01 pixels light grey = visible) when the cell's colour RAM nibble is 1 (a spotted enemy unit) and the radar is intact, or when this row is the current sweep row (radarSweepRow == radarSweepStep) and the sweep is not dark; otherwise writes $00 (pixels black = hidden). This is how the sweep band reveals and re-hides the radar blips. | A = grid row 0-16, $7891 radarSweepStep, $7892 radarSweepRow, $7893 radarSweepDarkFlag, $9243 comcenRadarStatus, colour RAM row | 21 screen RAM bytes at $8C00+(row+3)*40+3 set to $F0/$00; zp_B6/B7 = colour RAM pointer, zp_B8/B9 = screen RAM pointer; A/X/Y clobbered | medium |
| 7926 | redrawEnemyUnitsOnRadar | Re-randomises the 16 noise masks at $9210 (sub_727A) and redraws every unit of the opposing side on the radar window: starts at unit comcenUnit EOR $52 (99 for comcen 49, 49 for comcen 99) and walks 50 units downwards, passing cell value $80/unit (alive) or the terrain byte saved under a dead unit (skipped when $FF = not on the map) with the unit's column/row in $54-$56 to drawScreen1Cell ($5D7F). Called from the screen-1 refresh sub_7688 (at screen build and whenever statsNeedRefresh $920E is set after a turn), so enemy blips move and their noise pattern changes every turn. | $9236 comcenUnit, unit arrays $F640/$F6A4/$F76C/$FAF0 | cells redrawn on screen 1; $9210-$921F new masks; zp_18 = unit index, zp_19 = counter, $54-$56 | high |
| 7959 | scrollRadarOnComcenMove | Entered by JMP from drawScreen1Cell ($5D8A) when the cell being redrawn is the player's comcen. Computes the radar grid origin from the comcen position (getRadarGridOrigin); if it equals the current view origin zp_A5/A6 nothing happens. Otherwise the comcen has moved: unless the target cursor sits on the comcen cell (10,8) it is shifted by (old origin - new origin) through $76EC so that it keeps pointing at the same map cell (left unchanged if that would leave the grid), then the whole radar map is redrawn (drawRadarMap) and the sweep row restarts at 0. | $9236 comcenUnit, zp_A5/A6 view origin, zp_67/68 radar cursor | zp_A5/A6 = new origin, zp_67/68 and sprite 2 position possibly updated, radar window redrawn, $7891 = 0 | medium |
| 7987 | isRadarCursorOnComcen | Compares the radar target cursor (zp_67 column, zp_68 row) with the comcen's fixed grid cell (10,8), the centre of the 21x17 radar window. Returns Z=1 when the cursor is on the comcen. | zp_67, zp_68 | Z flag (set = cursor on comcen cell); A clobbered | high |
| 7992 | radarCursorToMapCoords | Converts the radar cursor grid position into map coordinates for an outgoing command: cmdParam1 (zp_74) = zp_67 + view origin column, cmdParam2 (zp_75) = zp_68 + view origin row. Used before queueing the missile strike command $90 ($7667) and by checkStrikeHitsDrone. | zp_67/68 radar cursor, zp_A5/A6 view origin | zp_74 = map column, zp_75 = map row; A clobbered | high |
| 79A1 | checkStrikeHitsDrone | After a missile strike is fired: if a drone is active (droneStateFlags $920D nonzero) and still in flight (droneFlags $92F9 == 0), converts the strike cursor to map coordinates and tests whether the target cell is one of the 2x2 cells occupied by the drone (droneCol - col and droneRow - row both in {-1,0}). If so it queues the 1-byte command $92 (drone destroyed/finished) and sets droneFlags bit 0. Called from the fire handler at $767E. | $920D, $92F9, $92F6 droneCol, $92F7 droneRow, zp_67/68, zp_A5/A6 | command $92 queued (queueCmdBytePending), $92F9 /= 1, zp_74/75 | medium |
| 79D8 | showDroneKillBurst | Visual/sound effect on the radar screen when the local $92 command has been executed (droneFlags bit 4 set by cmdDroneArrived): not in solo/film mode ($0B9B bit 7). Moves droneFlags bit 4 to bit 5, clears droneStateFlags, plays sounds $17 and $09, positions sprite 1 on the drone's cell centre (getUnitSpriteCentreOnGrid) and plays an 11-step ring burst there. Called from the screen-1 loop when statsNeedRefresh is set ($75F4). | $0B9B, $92F9, $92F6/$92F7 drone position | $92F9 bit4 cleared/bit5 set, $920D = 0, $9011/$9019 sprite 1 position, sprite 1 animation, sounds | medium |
| 7A13 | mapCellToRadarGrid | Converts map coordinates (X = column, Y = row) into radar grid coordinates relative to the comcen: X = col + 10 - comcenCol, Y = row + 8 - comcenRow, then falls into isRadarGridCellInRange. Called by the main program's getUnitSpriteCentreOnGrid ($340B) and by the communications module ($EE7C in build 1, $EEE1 in the $EE00 sub-overlay) to place sprites on screen 1. | X = map column, Y = map row, $9236 comcenUnit, $F640/$F6A4 | X = grid column, Y = grid row, C = 0 inside the 21x17 grid, C = 1 outside | high |
| 7A2B | isRadarGridCellInRange | Range test for a radar grid cell: C = 0 when Y < 17 and X < 21 (unsigned, so negative values count as outside), C = 1 otherwise. Also called directly from $33A0, $76FC and $77B9. | X = grid column, Y = grid row | C flag | high |
| 7A32 | initRadarScreen | Builds screen 1 (RADAR/MISSILE screen, game screen id 1 entered at $7587): clears the text draw parameters $2A29-$2A2C, sets border and background colour shadows to $0C, clears $9226, sets zp_41 = 1, then draws the MISSILES panel (drawMissilePanel), the missile count (drawMissileCount), the FIRE button (drawFireButton), the DRONE ALERT panel (drawDroneAlertPanel), the framed radar map window (redrawRadarMapWindow) and the main frame (drawMainWindowFrame), and finally jumps to the $C000 overlay's message-line redraw at L_C5D7. | $9232 missiles left, game state | whole screen drawn; $9003/$9004 = $0C, $9226 = 0, zp_41 = 1, $2A29-$2A2C = 0 | high |
| 7A6B | redrawRadarMapWindow | Draws the two nested frames of the radar window (window 5: 25x21 at (1,1); window 6: 23x19 at (2,2)) and then the 21x17 radar map itself (drawRadarMap). Also the target of clearCurrentScreenWindows ($2DFA) when screen 1 is active. | comcen position, map | frames and map drawn | high |
| 7A78 | drawMissilePanel | Draws the MISSILES panel at the bottom right: resets the text window to the full screen, draws window 7 (12x7 at (27,15)) with inner corners and window 8 (4x4 at (31,16)), fills the 2x2 count box at row 17 column 32 with colour $30, selects OR bitmap mode, text colour $CF, and prints the string 'MISSILES' ($CC9E) at column 29 row 19, then resets the draw modes. | - | panel drawn; zp_3D, zp_3F, zp_40, $2A2D/$2A2E changed | high |
| 7ABA | drawMissileCount | Displays the number of missiles left ($9232) as two tall (2-row) digits at row 17, columns 32 and 33: sub_C428 splits the value into tens ($C44F) and ones ($C450), the fixed draw colour $30 is selected (and remembered in missileCountColour) and drawTallGlyph is called twice. Called at screen build, after each launch ($7647) and from the refresh routine. | $9232 | digits drawn; $7AB9 = $30; $2A29/$2A2A restored to 0; zp_18/zp_19 = row/column | high |
| 7AEC | blinkMissileCount | Toggles missileCountColour between $30 and $03 and fills the 2x2 colour cells of the count box (row 17, column 32) with it, which swaps foreground and background of the digits. Called every 9 frames by the screen-1 loop ($75E4) while fewer than 2 missiles are left. | $7AB9 | $7AB9 toggled, screen RAM colour cells; $2A2D/$2A2E = 17/32 | high |
| 7B05 | drawFireButton | Draws the FIRE button panel: window 9 (8x7 at (29,8)) with inner corners, the 2x2 character picture at $9804 at row 10 column 32 filled with colour 5, text colour $CD and the word 'FIRE' ($869D) at column 31 row 12, then resets the draw modes. The screen-1 loop blinks the button by XORing colour 8 into the same 2x2 cells every 9 frames ($75DA). | - | panel drawn; zp_3D, zp_3F, zp_40, text window reset | high |
| 7B44 | drawDroneAlertPanel | Draws the top-right DRONE ALERT panel made of three nested frames (windows $0A 11x8 at (28,0), $0B 9x6 at (29,1), $0C 7x4 at (30,2)) and falls into resetDroneAlertPanel which prints the text in its inactive colour. | - | panel drawn | high |
| 7B59 | resetDroneAlertPanel | Prints 'DRONE' / 'ALERT' ($86A1, two lines) at column 31 row 3 in colour $0B (replace colour mode), sets droneAlertColour ($7B43) back to 2, stores 11 in zp_70 (never read) and fills the 7x4 panel interior at (2,30) with colour $0B, i.e. the panel is shown dark/inactive. Entered by JMP from the refresh routine sub_7688 when no drone is active ($920D bit 0 clear) and at the end of drawDroneAlertPanel. | - | text and colour cells; $7B43 = 2, zp_3B = 31, zp_3D = $0B, zp_3F = 0, zp_40 = 3, zp_70 = 11 | high |
| 7B86 | flashDroneAlertPanel | Resets the draw modes, toggles droneAlertColour between $02 and $20 (EOR $22) and repaints the 7x4 panel interior with it, making the DRONE ALERT text flash red. Entered by JMP from sub_7688 on every refresh while a drone is active ($920D bit 0 set). | $7B43 | $7B43 toggled, 7x4 colour cells at (2,30) | high |
| 7B91 | fillDroneAlertPanel | Tail shared by the two panel states: fills the 7 column x 4 row colour rectangle at row 2, column 30 with the colour byte in A (fillScreenColourRect). | A = colour byte | $2A2D = 2, $2A2E = 30, 28 colour cells written | high |
| 7BA2 | drawRadarMap | Draws the 21x17 radar map centred on the comcen at screen rows 3-19, columns 3-23, one character per map cell: re-randomises the noise masks (sub_727A), sets the cell colour parameters $9237/$9239 = $0B and $9238 = 1, clears the window's colour cells to 0, sets the view origin zp_A5/A6 = comcen - (10,8) (saved in $90F6/$90F7), gets the map pointer with getViewportOriginPointer and for every row/column computes the bitmap address (row/column tables with the +3 offset) and the colour RAM address ($D800 based), then calls the cell classifier sub_74C3 and glyph plotter sub_7396 of this overlay. Restores zp_A5/A6 and clears useFixedColourFlag at the end. | $9236 comcenUnit, map $F000, unit arrays | radar window drawn; zp_A5/A6 = grid origin, $90F6/$90F7, zp_48/49 map pointer, zp_B4/B5 bitmap pointer, zp_B6/B7 colour RAM pointer, $9237-$9239, $2A29 = 0 | high |
| 7C5C | getRadarGridOrigin | Returns the map coordinates of the top-left cell of the radar grid: X = comcenCol - 10, Y = comcenRow - 8 (may be negative / wrap when the comcen is near the map edge; the drawing code handles that through getViewportOriginPointer). | $9236 comcenUnit, $F640/$F6A4 | X = origin column, Y = origin row; A clobbered | high |
| 7C5C | getComcenCentredViewOrigin | Returns the map origin that centres the player's comcen in overlay B's 21x17-cell screen-1 map window: X = unitCol[comcenUnit]-10, Y = unitRow[comcenUnit]-8. Called from $7959 and $7BC5 (screen 1 comcen drawing). Starts two bytes before the nominal chunk start ($7C6E) but is on the first line of the line range. | $9236 comcenUnit, unitCol $F640, unitRow $F6A4 | X = origin column, Y = origin row, A clobbered | medium |
| 7FE6 | enterDroneScreen | Entry of game screen 2 (the DRONE screen, F5), jumped to by the screen switcher sub_06BA ($06FC) when currentScreenId becomes 2. Copies dronesLeft[0] ($92AE) to $9233, unpacks the eight aircraft silhouettes at $FE24 into the $0200 shape buffer (unpackRleTo0200) and draws the whole screen with sub_70F5, then falls into resetDroneScreen. | $92AE dronesLeft[own] | $9233, $0200-$03FF shape buffer, screen 2 drawn | high |
| 7FF6 | resetDroneScreen | (Re)initialises the drone screen's panel state and enters the idle loop. Also the re-entry point used by the knock-out sequence ($076A) and after a drone flight ends ($81FB). Reloads $9233 from dronesLeft[0], positions sprite 0 (X $37,Y $64, colour 1), sprite 5 (X $38,Y $64, colour 2) and sprite 3 (X $38,Y $65) in the left panel, sets droneJoyHeading zp_5E=0 (north), draws the heading indicator (sub_823F), loads aircraft shape 0 into sprite 5, enables sprites 0+3, and sets droneFuel $9220 to 100 when drones are left (else 0) for the fuel read-out. | $92AE, sprite tables | $9233, $9220, zp_5E, $9010/$9013/$9015, $9018/$901B/$901D, $9008/$900D, $9022=$09, $91CC=5; falls into droneScreenIdleLoop | high |
| 803D | droneScreenIdleLoop | Idle loop of the drone screen while no drone is in flight: runs updateGameFrame and the screen switcher every frame; every 11 frames (zp_6C) blinks the drones-left box (sub_7061, only when fewer than 2 drones remain) and XORs colour 8 into the 2x2 LAUNCH box at row 12 column 34. A fresh fire press (checkFirePressedNotSolo) launches a drone via launchDrone unless the own comcen is cloaked ($9245 bit 7), no drones are left ($9233 = 0), or the drone system is damaged ($9244 != 0), in which case only one press in four (random & 3 == 0) succeeds. | zp_6C, $9233, $9244, $9245, fire button | colour cells toggled; jumps to launchDrone | high |
| 808D | launchDrone | Launch sequence: reads the joystick heading (sub_82A4), droneHeading $920B = joyDirToHeadingTable[zp_5E], queues the 1-byte command $97 (drone launch), paints the LAUNCH box (12,34) colour 5, droneFuel = 100 (printed by sub_6F99), decrements $9233 and reprints it (sub_7036), sets droneStateFlags $920D /= $C0 (active + locally steered), sets the view origin and droneViewOriginCol/Row $92F6/$92F7 to comcen position minus (6,5) so the drone starts over the comcen, draws the comcen ring sprite (sub_3456), loads the targeting reticle at $FF93 into sprite 2 (black, X $AC, Y $88) and enables it, puts the heading silhouette into sprite 5, plays sound $0F, clears the camera window with static (sub_7135), sets colour override $AA, runs the iris-open animation sub_7221 and enters droneFlightLoop. | zp_5D joystick, $9236 comcenUnit, unitCol/unitRow, $FF93 reticle shape | $920B, $920D, $9220, $9233, $92F6/$92F7, zp_A5/zp_A6, sprites 2/4/5, command $97 queued | high |
| 8124 | droneFlightLoop | Per-frame loop while the drone flies: updateGameFrame; a fire press goes to strikeWithDrone. Every 40 frames (zp_6C) blinks the drones-left box (<2 left) and XORs colour 8 into the STRIKE box at (16,34). When zp_6E has expired re-reads the joystick heading (sub_82A4). When exchangeStepFlag $920E bit 7 is set (a lock-step exchange was applied) advances the camera one step (sub_8279); if droneFuel reaches 0 or the game ended ($0BA8 != 0) the drone is released (sub_8230) and the loss animation sub_7163 + sound $1C run; if $920D bit 7 was cleared by the command handlers (drone arrived $92 / remote detonation) the same ending runs without sending $8F. Then endDroneFlight. | zp_6C, zp_6E, $920E, $920D, $9220, $0BA8, fire button | drone view redrawn per step; falls into endDroneFlight | high |
| 8182 | strikeWithDrone | Fire pressed during flight: droneStateFlags = ($920D & $BF) / $20 (stop local steering, detonation pending), paints the STRIKE box (16,34) colour 2, computes the drone's map position (sub_81FE) and queues command $8E col,row (detonate). Then waits: while bit 5 stays set it keeps advancing the camera on each exchange with sound $1C; once cmdDroneDetonate has cleared bit 5 (own command executed) and bit 7 is still set it plays sound $11 and runs the strike flash/loss animation sub_7552. Ends in endDroneFlight. | $920D, $920E, $92F6/$92F7 | command $8E queued, $920D, $74/$75 | high |
| 81D0 | endDroneFlight | Common tail of a drone flight: sound $1C, droneFuel = 0 (reprinted), droneStateFlags &= $3F, view origin back to comcen minus (6,5), camera window cleared without static (sub_713C), then jumps to resetDroneScreen for the next launch. | $9236, unitCol/unitRow | $9220=0, $920D, zp_A5/zp_A6 | high |
| 81FE | getDroneMapPosition | cmdParam1/2 ($74/$75) = droneViewOriginCol+6, droneViewOriginRow+5, i.e. the drone's own map cell (the camera window is kept centred on the drone). Used by sendOutgoingPacket ($563C) to build the $8D heading,col,row move command and by strikeWithDrone for the $8E detonate command. | $92F6/$92F7 | zp_74 = drone column, zp_75 = drone row | high |
| 820F | showDroneAlert | Opponent drone launch notification called by cmdDroneLaunch ($53AF): plays sound 7 and, unless playModeFlags $0B9B bit 7 (solo/film) is set, shows message $20 'DRONE ALERT!' through the $C000 overlay's sub_C566. Assembled as .byte in the listing (misclassified). | $0B9B | sound, message | high |
| 821F | fillDroneViewWindow | Fills the drone camera window (14 columns x 12 rows at row 6, column 13) with colour byte A through fillScreenColourRect. Used to clear the window ($7141), to show static ($AA) when the drone system is damaged, and by the main program at $2E07. | A = colour byte | D_2A2D=6, D_2A2E=13, screen RAM cells | high |
| 8230 | releaseDroneControl | If a drone is active ($920D bit 7) clears bits 6-7 of droneStateFlags and queues the 1-byte command $8F (drone released) so the opponent stops tracking it. Called when fuel runs out or the game ends, and by the comcen knock-out sequence ($0709). | $920D | $920D &= $3F, command $8F queued | high |
| 823F | drawDroneHeadingIndicator | Draws the heading pointer of the left panel: clears sprite 3, draws a solid line in it from the centre (col 12,row 10) to headingLineEndCol/RowTable[joyDirToHeadingTable[zp_5E]] with drawLineInPathSprite, sets sprite 3's colour to $0B (or 6 for north/south, zp_5E < 2), copies the 64-byte shape to sprite 0 (which sits one pixel up-left in white, giving a shadowed line) and enables sprites 0 and 3. | zp_5E droneJoyHeading, tables $7FCE/$7FD6/$7FDE | $8AC0 and $8A00 sprite shapes, $900B, $9022 /= $09, zp_92-zp_95 clobbered | high |
| 8279 | advanceDroneCamera | One drone step on the DRONE screen, run whenever exchangeStepFlag $920E bit 7 is set: clears the flag, updateMarkerSprite, redraws the 14x12 camera window from the map around droneViewOrigin (sub_741C); if the drone system is damaged ($9244) half of the steps (random bit 7 clear) fill the window with static colour $AA; decrements droneFuel, reloads the heading silhouette into sprite 5 and reprints the fuel (sub_6F99). The actual drone movement is done by sendOutgoingPacket via $731E. | $920B, $9220, $9244, $92F6/$92F7 | $920E=0, $9220-1, sprite 5, camera window | high |
| 82A4 | readDroneHeadingFromJoystick | Reads joystickDirectionTable[zp_5D]; if a direction is held (entry >= 0) stores it in droneJoyHeading zp_5E, sets the re-read delay zp_6E = 20 frames and redraws the heading indicator (sub_823F). Called at launch and, when zp_6E has expired, during flight. | zp_5D joystick bits, $1FA6 table | zp_5E, zp_6E, heading sprites | high |
| 82BF | drawShotLineInViewport | Draws the shot of resolveAttack ($3A30) in the 7x5-cell battlefield viewport (2x2 characters per cell at row 1, column 25). First forces the upper colour nibble of the viewport's 14x10 screen RAM cells to 1 (white for bit pair 01). Handles a negative view origin (zp_A5/zp_A6 < 0) by shifting all columns/rows, computes the viewport bounds $82B5-$82B8, and records in $82B9 which endpoints lie inside (attacker -> -1, target -> +2); returns if neither. Then rebases the coordinates (sub_840F), scales to fat pixels (x8, +3/+4 for the cell centre, sub_84B2), runs the line stepper from attacker (zp_93,zp_92) to target (zp_95,zp_94) and plots colour-1 fat pixels via plotFatPixel for points inside the window (X = dx+100, Y = dy+4). The first 4 steps from the shooter are skipped, the 5th gets the 4-pixel diamond (sub_84F1), unless only the target is visible ($82B9 = 2) in which case the whole line is solid. Boomers (attackerType $923D = 2) step 3 cells per plot (dotted). Finally viewRedrawTimer $9234 = $8F so the view is redrawn (line erased) 15 frames later. | zp_92/zp_93 attacker row/col, zp_94/zp_95 target row/col, zp_A5/zp_A6 view origin, $923D attackerType, $9382/$939B screen row tables | bitmap pixels, screen RAM colour nibbles, $9234 = $8F, $82B5-$82BC, zp_92-zp_95 and line stepper state clobbered, zp_B6/zp_B7 | high |
| 840F | rebaseLineCoordsToMin | Subtracts min(viewLeft $82B5, zp_93, zp_95) from all three column values and min(viewTop $82B7, zp_92, zp_94) from the three row values so that the smallest becomes 0. Keeps the following x8 scaling from overflowing a byte. | $82B5, $82B7, zp_92-zp_95 | $82B5, $82B7, zp_92-zp_95 rebased | high |
| 84B2 | scaleLineCoordsToFatPixels | Converts the four line coordinates from map cells to fat-pixel units: zp_94 = row*8+4, zp_95 = col*8+3, zp_92 = row*8+4, zp_93 = col*8+3, saturating to $FF on overflow (the zp_92 case lacks the LDA #$FF and only skips the +4 when the overflowed value is negative - harmless in practice). | zp_92-zp_95 in cells | zp_92-zp_95 in fat pixels | high |
| 84F1 | plotDiamondMarker | Plots four colour-1 fat pixels at (X-1,Y-1), (X+1,Y-1), (X+1,Y+1), (X-1,Y+1) with plotFatPixel and restores X/Y: the small diamond drawn at the muzzle end of a shot line. | X, Y = fat pixel position | 4 pixels; X, Y preserved | high |
| 8518 | drawStatsTab | Fills in the STATS tab of the in-game menu (called from $1460/$14D3): text window left 10, colour $61, clears exchangeStepFlag $920E (the refresh request). Rows 9-12: number of living GRUNTS/RIDERS/BOOMERS/SPIES per side, counted by scanning the 50 unit slots down from each comcen (own comcen $9236 in column 9 = US, the other comcen $9236 EOR $52 in column 14 = THEM). Row 14 DRONES ($92AE/$92AF), row 15 MISSILES ($92B0/$92B1), row 17 COMSPEED (comSpeedDisplayTable[$92B4/$92B5]), row 18 TER PTS ($92AC[side EOR 1] under US, $92AC[side] under THEM), row 19 RECYCLER words via printOptionWord ($92A6[side], $92A6[side EOR 1]), row 20 the MAP ID string at $0BA9 in column 9 for side 0, column 14 for side 1. | $9236, $F76C unitTypeFlags, $92AE-$92B5, $92AC, $92A6, $0B9F playerSide, $0BA9 | text printed; $920E = 0; zp_18, zp_1A, zp_20, zp_21, zp_3B, zp_3D, zp_3F, zp_40 used | high |
| 8601 | printNumberRightAligned | Prints the 8-bit value A right-aligned in a 3-character field at text column X of the current row: sets zp_3F = X, computes the number of leading fill characters (0 for >= 100, 1 for 10-99, 2 for < 10) into the self-modified operands at $8622/$8627/$8629, positions the cursor (sub_C099), prints the fill characters with the $C000 overlay routine at $C0EF and the number with sub_C17D (Y = 0 high byte). The CPY #0/BNE at $8607 is dead (Y is always 0). | A = value, X = column | text; zp_3F; operands $8622, $8627, $8629 | high |
| 862D | setTextColumn | Sets the text cursor column zp_3F = A and repositions the cursor through sub_C099 (X and Y preserved). | A = column relative to textWindowLeft | zp_3F, text cursor pointers | high |
| 8632 | drawComcenSchematic | Unpacks the RLE-compressed comcen picture at $7C6E straight into the bitmap for the REPAIR tab (called from drawRepairTab $176B): marker = first byte ($13) -> rleMarkerByte $9246, $9247 = 0, fetchRleByte's pointer ($681B/$681C) = $7C6F, destination = bitmap cell (row 10, column 13) from calcCellPointersXY. Emits 10 character rows of 160 bytes (20 characters x 8 lines) - 'marker,count,value' runs (count 0 = 256) may cross row boundaries - advancing zp_B4/zp_B5 by 320 per row. The picture is a side view of the tracked comcen vehicle with dish and antenna. | $7C6E compressed data | bitmap $A000 area rows 10-19 columns 13-32; $9246, $9247, $681B/$681C, zp_1A, zp_1B, zp_B4/zp_B5 | high |

Data:

- `$7134` **radarWipeColour** (byteTable, 1 bytes): Fill colour / slow-animation flag for the radar wipe ($BB or 0)
- `$715D` **unusedBytes715D** (unknown, 6 bytes): $21,$A1,$21,$81,$A1,$11 between the RTS at $715C and playDroneLostAnimation; not referenced by variant B (L_7161 is a branch target only in variant A). Probably leftover data or code fragment.
- `$7271` **savedDroneHeading** (byteTable, 1 bytes): Dead copy of droneHeading written by playDroneLostAnimation
- `$7272` **clearBitMaskTable** (byteTable, 8 bytes): $FE,$FD,$FB,$F7,$EF,$DF,$BF,$7F: AND masks clearing bit n, used by buildRadarNoiseMasks
- `$7316` **headingToDirectionTable** (byteTable, 8 bytes): Clockwise heading 0-7 (N,NE,E,SE,S,SW,W,NW) -> direction code 0-7 (0 N,1 S,2 E,3 W,4 NW,5 NE,6 SE,7 SW): 0,5,2,6,1,7,3,4. Used by steerDrone and by the comm sub-overlay at $EF38.
- `$7395` **radarGlyphSavedY** (byteTable, 1 bytes): Saved Y register of drawRadarGlyph
- `$7551` **zoomStepCounter** (byteTable, 1 bytes): Counter of playStrikeZoomAnimation
- `$7585` **missileScreenLocked** (byteTable, 1 bytes): Bit 7 = missile screen inactive
- `$7586` **cursorMovedFlag** (byteTable, 1 bytes): 0 until the missile cursor was moved, then $7F
- `$7891` **radarSweepState** (byteTable, 3 bytes): three state bytes of the radar sweep: radarSweepStep, radarSweepRow, radarSweepDarkFlag (all initialised to 0 on disk)
- `$7A12` **padding7A12** (unknown, 1 bytes): unreferenced $00 byte between the RTS of showDroneKillBurst and mapCellToRadarGrid (alignment filler)
- `$7AB9` **missileCountColour** (byteTable, 1 bytes): colour byte of the missile count box ($30 / $03)
- `$7B43` **droneAlertColour** (byteTable, 1 bytes): flash colour of the DRONE ALERT panel ($02 / $20)
- `$7C6E` **comcenSchematicRle** (unknown, 864 bytes): RLE-compressed 160x80 pixel hires picture (20x10 characters, 1600 bytes unpacked) of the comcen vehicle shown on the REPAIR tab. Byte 0 = escape marker $13; stream of literals and 'marker,count,value' runs (count 0 = 256); ends at $7FCD with the run $13,$00,$00 (256 zero bytes). The labels sub_7CA2/7CAE/7CBF/7D70/7D7D/7D8B/7D98/7DA7/7E4F/7E74/7EA0/7F3F/7FA6/7FCB and the L_/D_ labels inside come from overlay A references and have no meaning in variant B.
- `$7FCE` **joyDirToHeadingTable** (byteTable, 8 bytes): 00 04 02 06 07 01 03 05: maps the joystickDirectionTable index (N,S,E,W,NW,NE,SE,SW) to the clockwise heading 0-7 (N,NE,E,SE,S,SW,W,NW) used for sprite shapes and command $8D. Also read by $731E.
- `$7FD6` **headingLineEndColTable** (byteTable, 8 bytes): 0C 13 16 13 0C 05 03 05: end column inside the 24x21 sprite of the heading pointer line for headings 0-7 (centre column 12).
- `$7FDE` **headingLineEndRowTable** (byteTable, 8 bytes): 00 03 0A 11 14 11 0A 03: end row of the heading pointer line for headings 0-7 (centre row 10).
- `$82B5` **shotLineWorkVars** (byteTable, 8 bytes): Eight zero-initialised work bytes of drawShotLineInViewport (see variables $82B5-$82BC).
- `$82BD` **unusedBytes82BD** (unknown, 2 bytes): $00,$60: no reference in either variant; filler between the shot-line work bytes and drawShotLineInViewport.
- `$8512` **comSpeedDisplayTable** (byteTable, 6 bytes): 01 02 02 02 00 03: COMSPEED value printed on the STATS tab for comcen speed class 0-5 (class 4 = immobile shows 0).
- `$868C` **strDrone** (text, 5 bytes): 'DRONE' (final E has bit 7). Caption of the drones-left box on the drone screen; printed by overlay B code at $7018-$7033 with textColour $CF, column $20 (32), row $07.
- `$8691` **strLaunch** (text, 6 bytes): 'LAUNCH'. Caption under the LAUNCH button icon of the drone screen; printed at $70B6-$70C5, column $20, row $0E (14).
- `$8697` **strStrike** (text, 6 bytes): 'STRIKE'. Caption under the STRIKE button icon of the drone screen; printed by sub_70D9 at $70D9-$70EF with textColour $C7, column $20, row $12 (18).
- `$869D` **strFire** (text, 4 bytes): 'FIRE'. Caption of the missile FIRE button on the main game screen; printed at $7B2E-$7B3D, column $1F (31), row $0C (12).
- `$86A1` **strDroneAlertTwoLine** (text, 11 bytes): 'DRONE' + $0D + 'ALERT' (final T has bit 7): the two-line label of the top-right DRONE ALERT panel. Printed at $7B67-$7B7A with textWindowLeft $1F, column 0, row 3; the surrounding panel colour (droneAlertColour, $02 or $20) is what flashes, not the text.
- `$86AC` **statsTabLabels** (text, 79 bytes): One bit-7-terminated blob of CR-separated row labels for the console STATS tab: CR 'GRUNTS' CR 'RIDERS' CR 'BOOMERS' CR 'SPIES' CR CR 'DRONES' CR 'MISSILES' CR CR 'COMSPEED' CR 'TER PTS' CR 'RECYCLER' CR 'MAP ID' (the final D is $C4). Printed as a single string by the main program at $161D-$1621 right after ownerNameString, with textWindowLeft 10 and column 3; the matching numbers are drawn separately.
- `$86FB` **filmViewWordTable** (byteTable, 12 bytes): Three fixed-width 4-byte entries used to patch msgShowViewTemplate: 'U','S'|$80,$00,$00 / 'T','H','E','M'|$80 / 'B','O','T','H'|$80. Indexed by (playModeFlags & 3) * 4 at $4116-$412A, producing the status line SHOW US / SHOW THEM / SHOW BOTH. The two padding zeroes of the first entry are copied but never printed because the terminator precedes them.
- `$8707` **msgEndOfFilm** (text, 12 bytes): 'END OF FILM.' - message id 2 (sound 3), queued with sub_C566 at $59FC after setFilmPtrToEnd when a recorded game film has been replayed to its end.
- `$8713` **msgUnitRecycled** (text, 14 bytes): 'UNIT RECYCLED.' - message id 5 (sound 4), queued with sub_C560 at $3EA3 after initUnitRecord, but only when the recycled unit's owner (zp_1A) equals localPlayerIndex.
- `$8721` **msgShowViewTemplate** (text, 9 bytes): 'SHOW ****' - message id 6 template (sound 3). Bytes $8726-$8729 are overwritten with a filmViewWordTable entry, which supplies the bit-7 terminator, before the message is queued at $412C. As stored on disk the template has no terminator and would run on into msgSpeedTemplate.
- `$872A` **msgSpeedTemplate** (text, 7 bytes): 'SPEED *' - message id 7 template (sound 3). The '*' at $8730 is replaced by the pressed digit's key code $B1-$B9, whose bit 7 doubles as the terminator, at $40DE; the message is then queued at $40E3. Also unterminated as stored.
- `$8731` **msgSpottedRecycler** (text, 17 bytes): 'SPOTTED RECYCLER!' - message id 8 (sound 4), queued with sub_C560 at $3F8C right after drawRecyclerOnMap / drawMapCell, i.e. when the recycler cell first becomes visible.
- `$8742` **msgComcenStunned** (text, 15 bytes): 'COMCEN STUNNED!' - message id $0C (sound 4), queued at $0741-$0743 in the $0400-page game-tick code when $9162 reads $FF. The same string is also printed directly into the console status area at column 5 by $17C4-$17D4 while comcenStunnedFlag ($922A) bit 7 is set.
- `$8751` **msgSpottedEnemyComcen** (text, 21 bytes): 'SPOTTED ENEMY COMCEN!' - message id $0D (sound 4), queued with sub_C560 at $36EF when a newly visible enemy unit's type field ($F76C,y & 7) equals 4 (COMCEN).
- `$8766` **msgSpyUnderAttack** (text, 17 bytes): 'SPY UNDER ATTACK!' - message id $0E (sound 4), queued with sub_C560 at $3B3B when the unit being hit is type 3 (SPY) and its energy field ($F9C4,y & $3F) is still $32 (50 = full), so it fires once, on the first hit.
- `$8777` **msgSpottedEnemySpy** (text, 18 bytes): 'SPOTTED ENEMY SPY!' - message id $0F (sound 4), queued with sub_C560 at $36E7 when a newly visible enemy unit's type field equals 3 (SPY). Types 0-2 (GRUNT, RIDER, BOOMER) are spotted silently.
- `$8789` **msgWeLostOnPoints** (text, 18 bytes): 'WE LOST ON POINTS!' - message id $11 (sound $0E), selected by computeGameResult at $2963-$296B on the time-out path when the losing side equals localPlayerIndex. Overlaid by code in variant A ($8789-$879E hold 22 bytes of A code ending in jmp $C100), so the string is only intact while variant B is resident.
- `$879B` **msgWeWonOnPoints** (text, 17 bytes): 'WE WON ON POINTS!' - message id $12 (sound $0D), the time-out win. Its first four bytes 'WE W' ($879B-$879E) are also clobbered by variant A code; from $879F on the byte content is identical in both variants.
- `$87AC` **msgItWasADraw** (text, 14 bytes): 'IT WAS A DRAW.' - message id $13, the only result message with no sound ($80). Chosen when the 16-bit score difference is zero at time-out, or when gameOverFlags bits 5-6 equal $60 (both comcens knocked out); both sides are then set to 250 points.
- `$87BA` **msgWeWereKnockedOut** (text, 20 bytes): 'WE WERE KNOCKED OUT!' - message id $14 (sound $0E), selected at $298D-$2995 when the knocked-out side equals localPlayerIndex.
- `$87CE` **msgWeWonByAKnockout** (text, 21 bytes): 'WE WON BY A KNOCKOUT!' - message id $15 (sound $0D), the enemy comcen destroyed. Not written by the T35 comm module despite the XREF: see the misclassified entry for $87CE.
- `$87E3` **msgGameOver** (text, 10 bytes): 'GAME OVER.' - message id $16 (sound $19), queued first by computeGameResult at $2901-$2903 on the time-out path, ahead of the result message.
- `$87ED` **msgDroneAlert** (text, 12 bytes): 'DRONE ALERT!' - has no fixed message id; overlay A installs its address into run-time message slot $20 at $7BA6-$7BAC (ldx #$20 / lda #$ED / ldy #$87 / jsr sub_C924) immediately before jumping to the comm module battle-start entry $EE26. During the battle showDroneAlert ($820F, variant B, called from $53AF) plays sound 7 and queues id $20 unless playModeFlags bit 7 (solo/film) is set.
- `$87F9` **overlayTailPadding** (unknown, 6 bytes): Six $00 bytes between msgDroneAlert and the variant flag, identical in both variants and referenced by nothing.
- `$87FF` **overlayVariantFlag** (byteTable, 1 bytes): The overlay identity byte: $FF in variant B, $00 in variant A. Bit 7 is read at $1036 and $1080 to decide whether the other variant still has to be pulled from disk.

Insights:

- [game_ovl_6F00_B_6F00] Overlay B implements the two comcen screens reached with the function keys through sub_06BA: screen 1 ($7587, runMissileScreen) and screen 2 ($7FE6, the DRONE screen built by buildDroneScreen $70F5). Both are centred on the own comcen (comcenUnit $9236 = 49 or 99). Screen 1 shows a 21x17 cell map window at character (3,3) (one character per map cell, comcen at grid cell (10,8), cursor sprite 2 at zp_67/zp_68) and fires command $90 x y (missile strike) at the cursor cell when fire is pressed after the cursor has moved. Screen 2 launches a drone ($97) with the heading taken from the joystick, shows a 14x12 radar (rows 6-17, columns 13-26) that follows the drone, steers it one cell per command exchange through steerDrone ($731E, called from sendOutgoingPacket before the $8D report), detonates it with fire ($8E x y, resolved by applyDroneBlast $72A1 with strength 25 over a 2x2 area) and releases it ($8F) when droneFuel $9220 (100 exchanges) runs out.
- [game_ovl_6F00_B_6F00] Drone screen layout (window table indices of drawWindowFrame): $0D (left 1, top 3, 9x9) heading/compass panel with sprites 3 and 5 as heading line/arrow; $0E (left 1, top 12, 9x9) fuel panel with 3 tall digits at row 15 columns 4-6 and a 3-character caption at row 17; $0F (left 11, top 4, 18x16) and $10 (left 12, top 5, 16x14) radar frames; $16 (left 13, top 6, 14x12) the radar area itself (used by the iris wipe); $11 (left 29, top 3, 11x7) and $12 (left 32, top 4, 5x4) drones-left box with one tall digit at row 5 column 34 and the caption 'DRONE' ($868C) at row 7 column 32; $13 (left 29, top 10, 14x11) buttons: 2x2 icon block $9804 at row 12 column 34 with 'LAUNCH' ($8691) at row 14, icon at row 16 with 'STRIKE' ($8697) at row 18. Character block data: $97D1 = 7x7 picture, $97B6 = 5x5 picture, $9804 = 2x2 icon (chars $45-$48), $980A = 2x2 of char $05, $9848 = 9 x char $03 frame tile set. Sprite 2 is the drone icon (shape copied from $FF93, at X=$AC Y=$88, the radar centre) and sprite 4 the comcen rings.
- [game_ovl_6F00_B_6F00] Radar rendering pipeline: getRadarCellGlyph ($74C3) maps a map cell to a glyph index in the 8-byte-per-glyph set at $9C6E (terrain = cell - $40; off-map hatch $28-$2B; units = unitGlyphBaseTable $0799[type] + facing: $2C grunt, $30 rider, $34 boomer, $38 spy, $3C comcen) and a colour in zp_32; drawRadarGlyph ($7396) plots it with optional effects: cloaked units on alternate scanlines (D_922D), unspotted enemies hidden except for random noise 'ghosts' (D_920F countdown, masks D_9210 built by buildRadarNoiseMasks $727A). Both routines are also used by the main program's drawScreen1Cell ($5D7F) for screen 1, where the colour goes to colour RAM as a raw byte (D_90FB = 1) instead of (colour << 4) | D_9226 in screen RAM.
- [game_ovl_6F00_B_6F00] Side-dependent opcode inside this overlay: the BCS at $750A ('cpy #$32 / bcs enemyPath' in getRadarCellGlyph) is not constant - loadOverlayB ($10A5) copies the patched opcode from $206B ($B0 for side 0, $90 for side 1, written by overlay A's sub_7BC6) into it right after loading the overlay. Annotators must treat $750A as a variable, and overlay B must be (re)loaded after the side is chosen for the radar to colour units correctly.
- [game_ovl_6F00_B_6F00] Phantom routine labels: sub_6F91, sub_701C, sub_7097, sub_70D9, sub_73EE, sub_7444, sub_74FE and sub_761C exist only because overlay variant A has JSRs to those addresses (XREF merges both variants). In variant B they are plain instruction boundaries in the middle of the routines listed above, not entry points. Likewise the L_/D_ labels inside $72A1-$7315 (L_72B1, L_72D3, L_72D5, L_72E1, L_72E5, L_72F2, L_72F5, sub_72F6, L_730D, L_731A) come from variant A.
- [game_ovl_6F00_B_6F00] Dead or ineffective code found: 'sta zp_30' at $716A (zp_30 is only used by magnifyWindow2x, which initialises it), the store to D_7271 at $717C (never read), 'bne L_7055' at $7053 (both paths fall into $7055), the 'lda D_2A29 / beq' test at $73F9 (setTerrainTilePtr at $73A4 always clears D_2A29 first, so the forced colour 8 is never applied), 'lda #0 / sta D_2A29' at $74B3 (already 0), and 'LDY $9236 / BNE' at $72FB (comcenUnit is never 0).
- [game_ovl_6F00_B_6F00] Fuel read-out colour code (drawDroneFuelDigits): >= 21 colour byte $30, 11-20 $B7, <= 10 $21, with the two nibbles swapped on even values so the warning colours alternate every exchange. The drones-left digit is drawn in colour 0 (invisible) when none remain, and blinkDroneCountBox toggles its box between $30/$03 by frame parity.
- [game_ovl_6F00_B_6F00] applyDroneBlast consequences: each unit in the 2x2 blast gets applyHitToUnit with 25 points and, unless the game type is THE BOMB, a stun counter of 15 plus haltStunnedUnit; if the own comcen is hit and the DAMAGE option ($0BA3 bit 4) is on (and not solo/film), D_9242/D_9243/D_9244 are set to $80 (overview shows terrain only, missile launches fail 50%, drone launches fail 75% and the radar is randomly jammed) and comcenStunStatus $922A = $FF which triggers the knock-out round at $0709; without DAMAGE only message $0C is queued.
- [game_ovl_6F00_B_6F00] The radar wipe (drawRadarIrisWipe $7221) is a concentric-frame 'iris' drawn from the all-$03 tile set at $9848, shrinking by two cells per pass about the radar centre (row 12, column 20) and finishing with a 2x2 block of char $05; D_7134 doubles as the blank colour and the 'animate with 3-frame pauses' flag ($BB after a launch with colour override $AA, 0 for the instant white wipe of playDroneLostAnimation).
- [game_ovl_6F00_B_7891] Screen 1 of the game (entered at $7587, selected with the function key giving screen id 1) is the comcen's RADAR / MISSILE screen in overlay variant B: a 21x17-cell map window at screen (3,3) centred on the player's comcen (grid cell (10,8), one 8x8 character per map cell), a DRONE ALERT panel (windows $0A-$0C, top right), a FIRE button (window 9, 2x2 picture $9804 at (10,32) + 'FIRE') and a MISSILES panel (windows 7/8) showing the missiles left ($9232, copied from missilesLeft[0] $92B0) as two tall digits at (17,32). Border and background are $0C.
- [game_ovl_6F00_B_7891] Radar sweep mechanics: a 14-row light band wipes down the 17 rows every ~62 frames (2 frames per step, 31 steps). It works by writing $F0 or $00 into the screen RAM colour cells (multicolour %01 pixels light grey or black) of one row at a time, so radar blips drawn with %01 pixels appear only while the band passes. Cells with colour RAM nibble 1 (spotted enemy units, $9238) stay lit while the radar system is intact. If the comcen RADAR system is damaged ($9243 = comcenSystemStatus[2]) about half of the sweeps are blank.
- [game_ovl_6F00_B_7891] Enemy units are re-drawn on the radar (redrawEnemyUnitsOnRadar) every time statsNeedRefresh $920E is set, i.e. once per processed turn, with freshly randomised noise masks ($9210-$921F from sub_727A) so unspotted enemy blips shimmer.
- [game_ovl_6F00_B_7891] The missile target cursor is sprite 2 (the cross shape built by showComcenCrossMarker $3383); its grid position is zp_67/zp_68 and is moved by the joystick handler sub_76B2 (outside this chunk). FIRE ($763F/$764A) decrements $9232, queues command $90 col,row (radarCursorToMapCoords -> zp_74/75), animates the missile flight along a line in sprite 3 (sub_7713) and a 3-step ring burst on sprite 1 at the target, then checkStrikeHitsDrone queues $92 if the strike cell is inside the drone's 2x2 footprint - this is the only sender of command $92, which therefore means 'drone destroyed by a missile' rather than 'drone arrived'. showDroneKillBurst then shows an 11-step ring burst at the drone when the own $92 has been executed.
- [game_ovl_6F00_B_7891] When the comcen moves, drawScreen1Cell ($5D7F) jumps to scrollRadarOnComcenMove ($7959), which recomputes the grid origin (comcen - (10,8)), shifts the cursor so it keeps its map cell and redraws the whole window.
- [game_ovl_6F00_B_7891] The $9300 address tables are used with a +3 bias in this overlay: D_9303/D_931C = bitmap row address lo/hi for row 3 onwards (real tables start at $9300/$9319), D_9335/D_935D = bitmap column offset for column 3 onwards (real tables $9332/$935A), D_9385/D_939E = screen RAM ($8C00) row address for row 3 onwards (real tables $9382/$939B); adding $4C to the high byte turns a screen RAM address into the colour RAM address.
- [game_ovl_6F00_B_7891] mapCellToRadarGrid ($7A13) / isRadarGridCellInRange ($7A2B) / getRadarGridOrigin ($7C5C) are the shared coordinate helpers of the radar grid; $7A13 is an entry point used by the main program ($340B) and by both communications-module variants ($EE7C in build 1, $EEE1 in the $EE00 sub-overlay), so its address must stay fixed across overlay builds.
- [game_ovl_6F00_B_7891] Window table entries used here (left,top,width,height): 5=(1,1,25,21) 6=(2,2,23,19) radar frame; 7=(27,15,12,7) 8=(31,16,4,4) missiles panel; 9=(29,8,8,7) fire button; $0A=(28,0,11,8) $0B=(29,1,9,6) $0C=(30,2,7,4) drone alert panel.
- [game_ovl_6F00_B_7891] Strings in overlay B used by this chunk: $869D 'FIRE', $86A1 'DRONE<CR>ALERT' (bit-7 terminated; $868C 'DRONE', $8691 'LAUNCH', $8697 'STRIKE' precede them and belong to screen 2); 'MISSILES' lives in the $C000 overlay at $CC9E.
- [game_ovl_6F00_B_7891] sub_C428 in the $C000 overlay converts A to two decimal digits stored at $C44F (tens) and $C450 (ones) (and a hundreds/leading flag at $C44E); drawTallGlyph ($2BB7) then draws each digit as a 2-row glyph.
- [game_ovl_6F00_B_7C6E] Overlay B screen 2 (currentScreenId 2, F5 key) is the DRONE screen: entry L_7FE6 from the screen switcher sub_06BA ($06FC), re-entry L_7FF6 from the knock-out sequence ($076A). Layout: left panel with a red aircraft silhouette (sprite 5, shapes from the RLE set at $FE24 = 8 headings clockwise from north) overlaid by a white heading line with a dark shadow (sprites 0 and 3, line drawn into $8AC0 and copied to $8A00), a three-digit fuel read-out (sub_6F99), the drones-left digit (sub_7036, box at row 5 column 33), a 14x12-character camera window at row 6 column 13 (one character per map cell, redrawn by sub_741C, reticle sprite 2 from $FF93 at X $AC Y $88), and two 2x2 buttons at column 34: LAUNCH (row 12) and STRIKE (row 16). Strings 'DRONE','LAUNCH','STRIKE','FIRE','DRONE ALERT' follow the chunk at $868C.
- [game_ovl_6F00_B_7C6E] Drone flight protocol as seen from this screen: fire -> $97 (launch), then sendOutgoingPacket emits $8D heading,col,row every exchange while $920D bit 6 is set (steering toward joyDirToHeadingTable[zp_5E] one 45-degree step per exchange in $731E), fuel $9220 counts 100 exchanges; fire again -> $8E col,row (detonate, bit 5 pending until the own command executes); fuel out / game over -> $8F (released). The drone arriving ($92, cleared bit 7) or a remote detonation ends the flight without $8F. The camera advances only when exchangeStepFlag $920E bit 7 is set, i.e. one step per lock-step exchange, keeping both machines in sync.
- [game_ovl_6F00_B_7C6E] The comcen systems table comcenSystemStatus $9241-$9244 maps to: [1] $9242 overview drawing, [2] $9243 screen 1 (scanner) events, [3] $9244 the drone system. With $9244 = $80 only one launch press in four is accepted (random & 3) and half of the camera steps are replaced by static (fillDroneViewWindow $AA).
- [game_ovl_6F00_B_7C6E] drawShotLineInViewport ($82BF) is the routine that draws shots inside the 7x5-cell zoomed battlefield viewport (2x2 characters per cell, characters row 1-10, columns 25-38, fat-pixel origin X=100,Y=4, 8 fat pixels per cell). It is the only user of plotFatPixel ($FC58). Line: gap of 4 steps at the shooter, a 4-pixel diamond, then a solid (or for Boomers dotted) white line; the viewport colour nibbles are forced to white first and viewRedrawTimer $9234 = $8F schedules the erase 15 frames later. Clipping excludes relative coordinate 0 (left column / top row of the window are never plotted).
- [game_ovl_6F00_B_7C6E] The comcen schematic on the REPAIR tab is an RLE-compressed 20x10 character hires bitmap at $7C6E-$7FCD (same stream format as unpackRleTo0200: first byte = marker, marker,count,value runs, count 0 = 256) unpacked by drawComcenSchematic ($8632) directly into the bitmap at row 10 column 13, 160 bytes per character row with a 320-byte row stride; it shares fetchRleByte ($681A) and rleMarkerByte ($9246) with the main program's unpacker.
- [game_ovl_6F00_B_7C6E] The high-memory block labelled unknownShapeTail at $FF90/$FF93 in the FBB8 survey is actually the 64-byte drone camera reticle sprite copied to sprite 2 ($8A80) by launchDrone ($80EB).
- [game_ovl_6F00_B_7C6E] Many sub_/L_/D_ labels inside $7C6E-$868B (sub_7CA2, sub_7CAE, sub_7CBF, sub_7D70, sub_7D7D, sub_7D8B, sub_7D98, sub_7DA7, sub_7E4F, sub_7E74, sub_7EA0, sub_7F3F, sub_7FA6, sub_7FCB, sub_7FDD, sub_8143, sub_81B6, sub_81F4, sub_8364, sub_84EC, sub_8511, sub_85DB, sub_860B, D_81C8, D_829E, D_8319, D_844F, D_846A, D_8493, D_85EC, D_8622...) are artefacts of XREF merging both overlay variants: they are referenced only by variant A code and are plain data bytes or mid-instruction addresses in variant B. Likewise the main program's JMP L_7DB8 ($0C9D), JMP L_7DE3 ($1C78) and JSR/JMP sub_8658 ($25C4/$6002) target overlay A code; in variant B those addresses are inside the compressed picture / the bitmap unpacker.
- [game_ovl_6F00_B_7C6E] STATS tab layout (drawStatsTab $8518, text window left 10): rows 9-12 GRUNTS/RIDERS/BOOMERS/SPIES counts, 14 DRONES, 15 MISSILES, 17 COMSPEED, 18 TER PTS, 19 RECYCLER, 20 MAP ID; column 9 = US (own side), column 14 = THEM. Unit counting scans the 50 slots downward from each comcen (49 / 99), so the comcen is the highest-numbered unit of each side. Numbers are printed right-aligned in 3 columns by printNumberRightAligned ($8601), which uses the $C000 overlay routines $C0EF (print A fill characters zp_3E) and $C17D (print decimal number).
- [game_ovl_6F00_B_868C] $868C-$87FF of overlay variant B contains no code whatsoever: 24 bit-7-terminated strings, one 12-byte word table, six padding zeroes and the variant flag. Every routine-looking label in the range is an artefact of variant A being merged into the same XREF namespace (see misclassified).
- [game_ovl_6F00_B_868C] Complete message id map (lo bytes at $0500, hi at $0524, 36 ids $00-$23, sound per id at $0612 with $80 = silent): 0 blank line of 33 spaces $0548, 1 WORKING... $056C, 2 END OF FILM. $8707, 3 GAME ABORTED. $0576, 4 $8858, 5 UNIT RECYCLED. $8713, 6 SHOW **** $8721, 7 SPEED * $872A, 8 SPOTTED RECYCLER! $8731, 9 NOT A GAME FILM! $0583, $0A SETUP UNITS. PRESS F 1 WHEN DONE. $0593, $0B $8867, $0C COMCEN STUNNED! $8742, $0D SPOTTED ENEMY COMCEN! $8751, $0E SPY UNDER ATTACK! $8766, $0F SPOTTED ENEMY SPY! $8777, $10 $887E, $11 WE LOST ON POINTS! $8789, $12 WE WON ON POINTS! $879B, $13 IT WAS A DRAW. $87AC, $14 WE WERE KNOCKED OUT! $87BA, $15 WE WON BY A KNOCKOUT! $87CE, $16 GAME OVER. $87E3, $17 $8897, $18 $88B6, $19 TIMEOUT. RUN/STOP TO RESUME. $05B4, $1A $88D6, $1B $05D0, $1C $05D7, $1D $05E9, $1E run-time slot ($FFFF), $1F $88F0, $20 and $21 run-time slots ($FFFF), $22 $05FA, $23 $0608. sub_C566 queues id A into the 8-entry sorted queue at $9249; sub_C560 is a wrapper that first does 'bit $0B9B / bpl sub_C566 / rts', i.e. it drops the message during solo/film play.
- [game_ovl_6F00_B_868C] CORRECTION to the existing computeGameResult ($28F1) annotation in game/main_0800.s, which says message $11 means 'won on points'. It is the other way round: $11 is 'WE LOST ON POINTS!' and $14 is 'WE WERE KNOCKED OUT!'. The X register the routine compares against localPlayerIndex is the LOSING side, and the code is consistent because the $92A2/$92A4 pair is indexed by the VICTIM: $3B79-$3B8B picks X = 0 for damaged units 0-49 and X = 1 for units 50-99 and adds the damage to $92A2,x, so $92A2[i] accumulates the score of side i^1. Hence a larger $92A2[i] means side i is losing; $2931's bcs leaves X = 0 when $92A2[0] >= $92A2[1]; the winner's score (300 + min(|diff|/2,150), or 500 on a knockout) is written into the loser's slot and the loser's score (200, or 150 on a knockout) into the winner's slot via setOtherPlayerPoints. The score display at $C6CE-$C6DC confirms the indexing by printing $92A2[localPlayerIndex ^ 1] first and $92A2[localPlayerIndex] second.
- [game_ovl_6F00_B_868C] Two of the messages are self-modifying text and belong to the film replay key handler at $40B2. Digits '1'-'9' ($B1-$B9 in lastKeyPressed $9248) set stepIntervalFrames $92E3 = 8 - (key - $B1) and store the raw key code into $8730 so 'SPEED n' terminates itself; CLR/HOME ($8C) increments playModeFlags $0B9B with a wrap from $83 back to $80 and copies filmViewWordTable[(flags & 3) * 4] over $8726-$8729 to make 'SHOW US' / 'SHOW THEM' / 'SHOW BOTH'. Both templates are stored unterminated on disk and are only safe to print after being patched.
- [game_ovl_6F00_B_868C] The two overlay variants share their last 97 bytes exactly: a byte compare of extracted/runtimeImage_A.bin and _B.bin shows $868C-$879E differ (variant A holds custom-scenario dialog code there, the last of it a 22-byte block at $8789-$879E ending with jmp $C100), $879F-$87FE are byte-identical, and $87FF differs ($00 in A, $FF in B). That shared tail is what makes overlay A's trick at $7BA6 work: A installs $87ED ('DRONE ALERT!') into run-time message slot $20 while A is resident, and the pointer stays valid after the swap to B because the string lives in the identical tail. Messages $11 and the first four bytes of $12 are corrupted while A is resident, which is harmless since results are only shown during the battle, with B resident.
- [game_ovl_6F00_B_868C] Everything drawn from this chunk goes through the same two-step call: sub_C099 positions the cursor (absolute column = zp_3F + zp_3B, row = zp_40, colour in zp_3D) and printString $C133 takes A = string low byte, Y = high byte into zp_A2/zp_A3. The five UI captions are at fixed spots: DRONE col 32 row 7 colour $CF ($7018), LAUNCH col 32 row 14 ($70B6), STRIKE col 32 row 18 colour $C7 ($70D9), FIRE col 31 row 12 ($7B2E), and the two-line DRONE/ALERT panel at window-left 31, col 0, row 3 ($7B67).
- [game_ovl_6F00_B_868C] The whole STATS tab label column is one 79-byte string with embedded CR ($0D) bytes ($86AC), printed by a single printString call at $161D from the main program - not one call per row. Together with the JSR $8518 / $8632 calls the main program makes into this overlay, that means the console STATS screen implicitly requires overlay variant B to be resident; variant A has code at $86AC.
- [game_ovl_6F00_B_868C] Unit spotting messages come from one spot in the visibility code, $36D7-$36F1: a newly visible enemy unit whose type field ($F76C,y & 7) is 3 queues $0F (SPOTTED ENEMY SPY!) and type 4 queues $0D (SPOTTED ENEMY COMCEN!); types 0-2 say nothing. SPY UNDER ATTACK! ($0E) is separate, raised at $3B3B only when the spy still has full energy ($F9C4,y & $3F == $32), so it announces the first hit rather than every hit.

## game/droneAiEE00

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| EE00 | flyTrainerDroneStep | One flight step of the solo-trainer AI's drone. The communications module build 1 (T35) JSRs here from $E0A5 once per command exchange, but only while this track-29 s4-5 page is the resident $EE00 image (droneOverlayState $8977 bit 7 set). It returns at once when droneFlags $92F9 bit 7 says the drone has already arrived or been shot down; otherwise it decrements the drone's fuel counter $8973 and, when that goes negative, fabricates the 1-byte command $8F (drone released) in the received-packet buffer and returns. Any other case falls through into pickDroneAimPointAndDecide. | $92F9 droneFlags, $8973 aiDroneFuel, $8974/$8975 drone cell, $8976 target unit index, $8978 detonate threshold, map/unit arrays $F640/$F6A4/$F960 | fabricated opponent packet in rxPacketBuffer $E020.. with rxPacketLength $E01E = 1 ($8F), 3 ($8E col row) or 4 ($8D heading col row); $8973 decremented; on the move path $896F, $8974, $8975 updated; zp_18,zp_19,zp_1C,zp_1D,zp_22,zp_24,zp_27,zp_28,zp_92-zp_95 clobbered | high |
| EE15 | pickDroneAimPointAndDecide | Fall-through block (branch target of $EE08 only): decides between blowing the drone up here and flying one more cell. It measures the distance from the drone to its target unit ($8976); inside 6 cells and with both drone coordinates on the map it scores the 2x2 block under the drone with scoreTargetBlock ($E9A6) and detonates when the score reaches aiDroneDetonateThreshold $8978, or only 3/4 of it when the target unit is stunned ($F960 low nibble non-zero). Otherwise it scores the four 2x2 blocks that touch the target's cell, leaves the best corner in zp_95/zp_94, sets the line-stepper start from the drone position (clamping a negative coordinate to 0 and shifting the end point by the same amount) and calls getStepDirection ($E80E); a stepper that finds no direction (drone already on the aim cell) also detonates. | $8976, $F640/$F6A4 unit col/row, $8974/$8975 drone cell, $8978, $F960, zp_1C = 0 on entry | zp_1C best block score, zp_1D best player unit, zp_94/zp_95 aim cell, zp_92/zp_93 line start, Y = heading 0-7 from getStepDirection; falls into emitDroneDetonateCommand or maybeJinkDroneHeading | high |
| EE2D | midCheckDroneOnMapEE2D | NOT an entry point in this image: $EE2D is the middle of pickDroneAimPointAndDecide (the 'ldx $8974 / bmi' on-map test before scoreTargetBlock). The sub_ prefix is imported from the other code images that share these addresses (comm build 2 has a real routine at $EE2D). | - | - | high |
| EE55 | midStoreDetonateThresholdEE55 | NOT an entry point in this image: $EE55 is the 'sta zp_18' that stores the 3/4 detonate threshold for a stunned target inside pickDroneAimPointAndDecide. The label comes from the other images of this page. | - | - | high |
| EE85 | midScanDiagonalBlockEE85 | NOT an entry point in this image: $EE85 is the 'dey' inside the four-block scan around the target cell (scoring the block at col-1,row-1). The label is an artifact of the T35/T34 images of this page. | - | - | high |
| EEB6 | emitDroneDetonateCommand | Detonation path (branch target of $EE3F and $EE5B): writes command $8E plus the drone's current cell, mirrored with mirrorBlockCornerForOpponent ($EA38), into the fake received packet and sets its length to 3. The engine's cmdDroneDetonate ($5336) mirrors the coordinates back and plays the blast. | $8974/$8975 drone cell | $E020=$8E, $E021/$E022 = mirrored col/row, $E01E=3; X/Y clobbered; returns | high |
| EED0 | maybeJinkDroneHeading | Flight path (branch target of $EEB4): stores the heading returned by getStepDirection in zp_19 and then, only when the human player still has missiles ($92B0), the drone's cell falls inside the radar window drawn around the human's comcen (mapCellToRadarGrid $7A13 with $9236 unchanged), the radar/drone screen is the one on show ($90FB = 1), the drone still has at least 12 fuel and is at least 3 cells away from its aim point, replaces the heading 2 times in 3 (nextGameRandom >= $56) with a random 0-7 value. Inferred purpose: an evasive weave while the player can watch the drone and shoot it down with a missile strike (confidence medium). | Y = heading from $E80E, $92B0 missilesLeft, $8974/$8975, $9236 comcenUnit, $90FB currentScreen, $8973, zp_94/zp_95 aim cell | zp_19 = desired heading 0-7; zp_22/zp_24/zp_27/zp_28 clobbered; falls into steerDroneAndEmitMove | medium |
| EEED | midCheckFuelForJinkEEED | NOT an entry point in this image: $EEED is the 'lda $8973 / cmp #$0C' fuel test inside maybeJinkDroneHeading. The label is an artifact of the other images of this page. | - | - | high |
| EF0F | steerDroneAndEmitMove | Turns the drone one step toward the desired heading and reports the move. The code is a copy of overlay B's steerDrone ($731E) with the AI's variables and without the turn sound: $896F is rotated by +1 or -1 (the short way round, then AND #$07) toward zp_19, converted through headingToDirectionTable ($7316), directionDeltaTable ($20F0) and diagonalDirectionTable ($1FB2) into dx/dy and added to $8974/$8975. It then emits command $8D with (heading+4)&7 - the heading as the mirrored map sees it - and the new cell mirrored by $EA38, length 4. Note that neither coordinate is clamped, so the drone can walk off the map. | zp_19 desired heading, $896F current heading, $8974/$8975 drone cell | $896F, $8974, $8975 updated; $E020=$8D, $E021=(heading+4)&7, $E022/$E023 mirrored col/row, $E01E=4; returns | high |
| EF9E | droneDistanceToCell | Helper called at $EE26 and $EEFC: copies the drone's cell $8974/$8975 into the distance routine's second point zp_27/zp_28 and tail-jumps to cellDistance ($E82A), which returns the integer distance between (zp_22,zp_24) and the drone in A and X. Y is preserved, which is what lets the caller keep the target unit index in Y across the call. | zp_22/zp_24 = cell to measure against, $8974/$8975 drone cell | A = X = distance 0-16, zp_27/zp_28 = drone cell; Y preserved | high |

Data:

- `$EFAB` **staleCommTailBytes** (unknown, 37 bytes): $EFAB-$EFCF: 37 bytes that are byte-for-byte identical in all four images of this address range (comm build 1 T35, comm build 2 T34, the track-29 s0-3 $EC00 sub-overlay and this one). In build 1 they are the tail of the AI's drone target picker (inc zp_1F / cmp #$0F / ... / sta $8979 / rts) and in build 2 they are the middle of the message 'INSERT DATA DISK AND PRESS SPACE.' ($EFAF, with the 4-character disk name patched into $EFB6). Nothing in this image reaches them - they are carried along only so that the shared strings below land at their fixed addresses.
- `$EFD0` **msgDataDiskName** (text, 4 bytes): 'DATA' - the 4-character disk name that overlay A's promptInsertDataDisk ($81B2) patches into the INSERT-DISK message. The disassembler merges $EFD0-$EFE1 into one label msgDatagamecustomOff4; it is really three strings.
- `$EFD4` **msgGameDiskName** (text, 4 bytes): 'GAME' - the disk name used by overlay A's promptInsertGameDisk ($81A5).
- `$EFD8` **msgCustomOff** (text, 10 bytes): 'CUSTOM OF','F'|$80 - installed as message slot $1E by overlay A ($EFD8), which also points its ON/OFF patch at $EFDF ('OFF').
- `$EFE2` **msgDestroyOff** (text, 11 bytes): 'DESTROY OF','F'|$80 - message slot $1E for the DESTROY toggle; overlay A patches ON/OFF at $EFEA.
- `$EFED` **msgAccepted** (text, 8 bytes): 'ACCEPTE','D'|$80 - printed centred on row 15 by overlay A when the fire button confirms a value.
- `$EFF5` **trailingPadEFF5** (unknown, 11 bytes): $C0,$DF,$00,$FF,$00,$FF,$00,$FF,$00,$FF,$FF - unreferenced trailing bytes, the same in every image of this page except the last byte ($FF here and in build 1, $00 in build 2). Probably padding left by the mastering system.

Insights:

- [game_ovl_EE00_T29_EE00] This page is the solo-trainer AI's drone autopilot. It is one routine at $EE00 that runs once per command exchange and its only product is a fabricated 'received' packet in rxPacketBuffer $E020 / rxPacketLength $E01E: $8D heading,col,row (fly one cell), $8E col,row (detonate) or $8F (drone released, out of fuel). The three coordinates are pre-mirrored with mirrorBlockCornerForOpponent ($EA38) and the heading is sent as (heading+4)&7 because the engine's cmdDroneMove/cmdDroneDetonate turn the opponent's coordinates round again.
- [game_ovl_EE00_T29_EE00] Page-swap protocol (comm module build 1, $E082-$E0DB): $E082 clears rxPacketLength, then if trainerDroneTimer $8977 is positive the module's own $EE00 code is resident - when droneStateFlags $920D bit 0 says a drone is in the air it sets $8977 = $FF, loads track 29 sectors 4-5 over $EE00-$EFFF (destination set by $E128) and JSRs $EE00; if $8977 is negative this page is resident - it JSRs $EE00 while $920D bit 0 stays set and, when the bit clears, reloads track 35 sectors 14-15 over $EE00, sets $8976 = $FF and $8977 = 3. So a drone flight costs two disk loads, and while the drone flies the trainer AI issues nothing except drone commands (all its other think routines live in the swapped-out page).
- [game_ovl_EE00_T29_EE00] Comm build 2 (T34, the modem build) never loads or calls $EE00 - it has no AI. Everything in this file is trainer-only ($0BA5 bit 7 set).
- [game_ovl_EE00_T29_EE00] Difficulty tuning: aiDroneDetonateThreshold $8978 comes from the 5-byte table at $EC0A ($FF,$FF,$08,$0C,$0C) indexed by the skill/play level $8966 (loaded at $ECC9-$ECCE in the track-29 s0-3 sub-overlay, together with $8979 = the strike score threshold). At skill 0-1 the threshold is $FF, so the drone never detonates early and only blows up when getStepDirection reports 'no direction left', i.e. when it is standing on its aim cell.
- [game_ovl_EE00_T29_EE00] Evasion heuristic at $EED0-$EF0E (inference, medium confidence): the drone only weaves when the human could actually shoot it - $92B0 (own missiles) non-zero, the drone's cell inside the radar window built around the human's comcen ($9236 is left at $31 here, unlike the module's own image which temporarily sets it to $63), the radar screen actually on show ($90FB = 1), at least 12 fuel left and at least 3 cells from the aim point. It then takes a random heading with probability 170/256; since the heading only turns one step per exchange the result is a wobble, not a jump.
- [game_ovl_EE00_T29_EE00] $EF0F-$EF9D is a verbatim copy of overlay B's steerDrone ($731E-$7391) with $896F/$8974/$8975 substituted for $920B/$92F6/$92F7 and the turn sound removed - useful confirmation that the heading encoding (0-7 clockwise from N) and the tables headingToDirectionTable $7316 / directionDeltaTable $20F0 / diagonalDirectionTable $1FB2 are shared between the player's and the AI's drone.
- [game_ovl_EE00_T29_EE00] getStepDirection ($E80E) returns Y = a heading 0-7 in exactly that encoding (its search tables $E68A/$E693 are dcol = 00,01,01,01,00,FF,FF,FF and drow = FF,FF,00,01,01,01,00,FF = N,NE,E,SE,S,SW,W,NW) and leaves N=0 on a match, N=1 when nothing matched. The 'bpl $EED0' at $EEB4 relies on that: no direction means the drone is already on its aim cell, so it detonates.
- [game_ovl_EE00_T29_EE00] $EE00-$EFAA of this image differs from the comm module's own $EE00 page in 409 of its 427 bytes, but $EFAB-$EFFF is byte-identical in all four images of the range (verified against the decrypted sectors t29s04/05 vs t35s14/15). The reason is that overlay A hard-codes addresses inside that tail: $EFD0 'DATA', $EFD4 'GAME', $EFD8/$EFDF 'CUSTOM OFF', $EFE2/$EFEA 'DESTROY OFF', $EFED 'ACCEPTED', and $EFAF/$EFB6 for the INSERT-DISK message.
- [game_ovl_EE00_T29_EE00] Latent bug (medium confidence): overlay A's promptInsertDiskAndWaitSpace ($81B6) unconditionally installs $EFAF as message $1E and copies four characters into $EFB6-$EFB9. That message only exists in comm build 2; with build 1 or either track-29 sub-overlay resident, $EFAF reads as the single character 'I' ($C9) and the four-byte patch lands inside the AI code at $EFB6.
- [game_ovl_EE00_T29_EE00] Label hygiene: this file is one of five different code images that share $EE00-$EFFF, and XREF.txt merges all of them under game/ovl_E000_T35. The only true entry points here are $EE00 (JSR from $E0A5) and $EF9E (JSR from $EE26 and $EEFC); sub_EE2D, sub_EE55, sub_EE85, sub_EEED and most L_ labels are imported from the other images. The branch targets that are real in this image are L_EE14, L_EE15, L_EE5D, L_EE8B, L_EE95, L_EEA1, L_EEAF, L_EEB6, L_EED0, L_EF0F, L_EF2B, L_EF35, L_EF4F, L_EF57 and L_EF6D.
- [game_ovl_EE00_T29_EE00] Minor bug at $EE32-$EE35: 'ldy $8975 / bmi L_EE5D' overwrites the target unit index in Y with the drone's row, so when the drone drifts off the top of the map the aim point is fetched from unitColTable/unitRowTable indexed by that row instead of by the target unit. Every other path into $EE5D still has Y = $8976 (cellDistance and its callee distanceSquared preserve Y).

## game/highMemoryFBB8

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| FBB8 | initLineStepper | Bresenham/DDA line set-up between the current cell ($93,$92) = (col,row) and the target cell ($95,$94). Computes /dRow/ -> $8F, /dCol/ -> $90, the step signs $96/$97 (+1 or -1), the major-axis length -> $8E and $91 (steps left), clears both error accumulators ($8C/$8D) and preloads the minor-axis accumulator with major/2. Used by the unit movement / path code in the main program ($1D57, $2512, $306F, $3771, $3A82, $42EC, $4559), by the comm module ($E80E, $E96F) and by overlay B to draw a shot/route line in the zoom window ($83A4). | $92 = start row, $93 = start column, $94 = end row, $95 = end column | A = $91 = number of steps (major axis length, Z set if zero); $8C,$8D = 0 / major/2; $8E = major length; $8F,$90 = /dRow/,/dCol/; $96,$97 = row/col step (+1/-1) | high |
| FBFA | stepLine | Advances the line started by initLineStepper by one cell: adds /dRow/ to $8C and /dCol/ to $8D, and whenever an accumulator reaches the major length it wraps it and moves $92 (row) / $93 (col) by the step sign, recording the applied delta in $90F4 / $90F5. $A9 ends up 0 for an orthogonal step and 1 for a diagonal step (the major axis always advances); if no steps are left nothing moves and $A9 = 0. Returns the remaining step count so callers loop with BEQ. | line state $8C-$8E,$8F,$90,$91,$92,$93,$96,$97 | A = $91 (steps remaining, Z = line finished); $92/$93 advanced; $90F4 = row delta applied (-1/0/+1), $90F5 = column delta applied; $A9 = 1 if diagonal step, 0 otherwise | high |
| FC58 | plotFatPixel | Plots a 'fat' multicolour pixel (one 2x2 hires block) of colour A (0-3) at X = 0..159, Y = 0..95 into the $A000 bitmap by drawing bitmap rows 2Y and 2Y+1 via plotBitmapPixelRow. X and Y are saved in self-modifying code and restored before returning. Only called from overlay B ($83E6 and the 2x2 marker at sub_84F1) to draw a shot/route line in the zoom window. The clip test is wrong: it returns early only when X >= 160 AND Y >= 96 (two BCCs into the plot path), so an out-of-range Y with a valid X is not rejected. | A = colour index 0-3, X = fat pixel column 0-159, Y = fat pixel row 0-95 | bitmap byte(s) modified; X and Y preserved; $FC4D-$FC4F, $B4/$B5 clobbered | high |
| FC74 | plotBitmapPixelRow | Inner routine of plotFatPixel: for bitmap row Y (0-191) and multicolour column X (0-159) computes the byte address bitmapRowAddr[Y>>3] + (Y&7) + colOffset[X>>2] using the tables at $9300/$9319 (row lo/hi, 25 entries, $A000 + 320*row) and $9332/$935A (8*column lo/hi, 40 entries), then merges the 2-bit colour ($FC50[colour] & $FC54[X&3]) into the byte. Colour index must already be in $FC4D. | X = pixel column 0-159, Y = bitmap row 0-191, $FC4D = colour index | one bitmap byte updated; $B4/$B5 = byte address; $FC4E = colour bits, $FC4F = keep mask; A,Y clobbered, X preserved | high |
| FCCD | multiply8x8 | Unsigned 8x8 -> 16 bit multiply by shift-and-add (8 iterations). Used by overlay A ($814A, $846B: value * Y for the option/limit editor) and the $EC00 sub-overlay ($ECD4: A * $96). | A = multiplicand, Y = multiplier | A = Y = product high byte, $18 = product low byte, $19 = product high byte, $1A clobbered, X = 0 | high |
| FCEC | divideSigned16by8 | Divides the signed 16-bit value Y:A (Y = high byte) by the unsigned 8-bit divisor X. A negative dividend is negated first, the quotient is negated afterwards (the remainder is that of the absolute value). Callers: $41E4 (timer mod 10/50 test), the comm module ($E8EA-$E90D) and the $EE00 sub-overlay ($EE86-$EEB6). | X = divisor, A = dividend low, Y = dividend high (sign) | A = quotient (low 8 bits, sign restored), Y = remainder (of /dividend/), $18 = divisor, $19 clobbered | high |
| FD0A | divideUnsigned16by8 | Unsigned 16/8 restoring division, 8 iterations: dividend high byte in A, low byte in $19, divisor in $18. Entered directly from divideSigned16by8 for non-negative dividends. Quotient bits beyond 8 are lost. | A = dividend high byte, $19 = dividend low byte, $18 = divisor | A = quotient (low byte), Y = remainder, $19 = quotient, X = 0 | high |
| FD1E | addRandomJitter | Returns base value A plus a random offset +/- 2*k, where k is the number of consecutive 1 bits at the bottom of a scenario random byte (geometric distribution) capped at Y, and the sign comes from bit 7 of $5B. Y = 0 returns A unchanged. A negative (bit 7 set) result is clamped to 0. Overlay A's scenario generator uses it to scatter unit/terrain positions around a centre (callers $7036, $738B, $7398, which retry while the result is >= 40). | A = base value, Y = maximum offset count; uses scenario RNG $5A-$5C | A = jittered value (0..127); $18, $19 clobbered; RNG state advanced | medium |
| FD4A | nextScenarioRandom | 24-bit shift-register pseudo random generator on $5A/$5B/$5C, returns $5A EOR $5B. This generator is the scenario/map seed: it is zeroed at $7731, seeded from the game RNG at $77E7 or from the typed 5-character MAP ID code (5 bits per character via the 32-entry alphabet at $890C, $77F7-$7821), and consumed only by overlay A's battlefield/unit generator and $4805. Same algorithm as nextGameRandom. | $5A-$5C RNG state | A = random byte, $5A-$5C advanced, C = 0 | high |
| FD5D | nextGameRandom | 24-bit shift-register pseudo random generator on $57/$58/$59, returns $57 EOR $58. Seeded with the constants $49,$19,$02 by the start-up code at $0309-$0313, so both machines of a modem game run the same deterministic sequence; used throughout the main program, overlays, the sound code ($C862) and the comm module. NOTE: this is not an IRQ handler (docs/overview.md calls $FD5D-$FD6F the IRQ handler region; the IRQ vector actually points to $1298 and later $10F1). | $57-$59 RNG state | A = random byte, $57-$59 advanced, C = 0 | high |

Data:

- `$FC50` **multicolorFillPatterns** (byteTable, 4 bytes): $00,$55,$AA,$FF - colour index 0-3 replicated into all four multicolour pixel pairs of a bitmap byte. Read by plotBitmapPixelRow as $FC50,X with X = the colour index.
- `$FC50` **multicolorFillPatterns** (byteTable, 4 bytes): $00,$55,$AA,$FF: colour index 0-3 replicated into all four pixel pairs of a bitmap byte; indexed by plotColorIndex
- `$FC54` **multicolorPixelMasks** (byteTable, 4 bytes): $C0,$30,$0C,$03 - the bit mask of multicolour pixel pair (X AND 3) inside a bitmap byte. ANDed with the fill pattern and merged into the byte by plotBitmapPixelRow.
- `$FC54` **multicolorPixelMasks** (byteTable, 4 bytes): $C0,$30,$0C,$03: bit mask of multicolour pixel pair (X & 3) within a bitmap byte
- `$FD70` **missileCursorShapesRle** (sprite, 180 bytes): RLE-compressed set of eight 24x21 sprites, first byte $01 = the escape marker. runMissileScreen ($7587 in the $6F00 overlay variant B) does LDA #$70 / LDY #$FD / JSR unpackRleTo0200 ($67CB), which expands it to exactly 512 bytes at $0200-$03FF = eight shapes on a 64-byte stride; loadDirectionShapeSprite ($3340) then copies shape number ($920B) from $0200 + 64*n into sprite 0 ($8A00) or sprite 5 ($8B40). The eight shapes are a small arrow/missile marker in the eight compass headings in the order N, NE, E, SE, S, SW, W, NW.
- `$FD70` **arrowGlyphTable** (sprite, 177 bytes): Leading byte $01 followed by eight 22-byte records: a 2-byte header ($01,$16 for the first, $01,$2D for the rest) and seven 3-byte rows holding an 8x7 arrow glyph in the middle byte (last row only 2 bytes). Glyphs are arrows for the 8 compass directions in clockwise order: N ($08,$1C,$7F,$08,$08,$08,$1C), NE, E, SE, S, SW, W, NW. No static reference to this table exists anywhere in the disassembly (no $FD7x/$FE/$FF pointers); it may be unused leftover data.
- `$FE21` **aircraftShapeTable** (sprite, 367 bytes): Eight records, each a 2-3 byte header ($01,$17,$00,$05,$05,$0A / $05,$15 / $05,$18 / $05,$18 / $05,$15 / $05,$18 / $05,$11,$00 / $05,$12) followed by 14-15 rows of 3 bytes (24 px wide) that draw a top-down aeroplane/drone silhouette in 8 orientations (up, diagonal, left, diagonal, down, diagonal, right, diagonal); e.g. rows $02,$38,$80 / $0F,$FF,$E0 x3 / $08,$7C,$20 are the wings. The record framing is not fully resolved (header length and row alignment differ between records) and nothing references the table; probably stale data from the mastering system or an unused earlier unit graphic.
- `$FE24` **droneShapesRle** (sprite, 367 bytes): RLE-compressed set of eight 24x21 sprites, first byte $05 = the escape marker. enterDroneScreen ($7FE6, overlay variant B) does LDA #$24 / LDY #$FE / JSR unpackRleTo0200, giving eight 64-byte shapes at $0200-$03FF that loadDirectionShapeSprite copies into the sprite data. The shapes are a top-down aeroplane silhouette (the drone) in the eight compass headings N, NE, E, SE, S, SW, W, NW. The stream ends with the run $05,$0E,$00 at $FF90-$FF92, which pads the buffer out to 512 bytes.
- `$FF90` **unknownShapeTail** (unknown, 66 bytes): Starts with the same $05,$xx header pattern ($05,$0E) but the following bytes ($08,$78,$08,$08,..,$49,$00,$49) look like 8-pixel-wide character rows rather than 24-px rows; the sector continues past $FFD1 with stale bytes that resemble the end of the $8800 message block (track 29 s9-10) and were not moved by the loader, so this block is most likely unused leftover memory.
- `$FF93` **targetingReticleSprite** (sprite, 63 bytes): One complete uncompressed 24x21 sprite: four corner brackets with a centre cross - the drone screen's targeting reticle. Copied into sprite 2 by overlay B at $80EB (LDA $FF93,Y) when the drone screen is built, so the drone starts over the comcen with this frame drawn around it. It ends exactly at $FFD1, the last byte of the relocated high-memory block.

Insights:

- [data_high_FBB8] The high-memory block ends with graphics, not code: two RLE-compressed eight-direction sprite sets ($FD70 missile marker, $FE24 drone) and one uncompressed 63-byte sprite ($FF93 targeting reticle), filling $FD70-$FFD1 exactly.
- [data_high_FBB8] The RLE format used here and by the $C000 overlay's five blocks ($CD52/$CDD3/$CE3B/$CECD/$CF3A) is: byte 0 = escape marker, then 'marker, count, value' emits value count times and anything else is a literal; unpackRleTo0200 always produces exactly 512 bytes into $0200-$03FF. The missile set compresses 512 bytes to 180 and the drone set to 367.
- [data_high_FBB8] The unpacked buffer uses a 64-byte stride, not 63: loadDirectionShapeSprite computes $0200 + 64*shape ($3340-$3358), so each sprite has one spare byte after it. Decoding either block with a 63-byte stride produces shapes that drift out of frame.
- [data_high_FBB8] Both sprite sets are ordered by compass heading N, NE, E, SE, S, SW, W, NW, matching the direction byte $920B that the callers pass ($5322 in the main program, $802A/$810B/$829E in overlay B).
- [data_high_FBB8] There are no message strings anywhere in $FBB8-$FFD1. The only text-like data at the top of memory is the ten-byte disk serial at $FFDE (read from the BAM) and the opponent's name at $FFE8, both of which are zero on the disk image and filled at run time.
- [data_high_FBB8] highMemoryFBB8.s calls $FD70 arrowGlyphTable ('177 bytes ... no static reference to this table exists anywhere in the disassembly; it may be unused leftover data') and $FE21 aircraftShapeTable ('367 bytes ... the record framing is not fully resolved ... nothing references the table; probably stale data'). Both are wrong on all three counts: they are referenced (from overlay B $7596 and $7FF0), they are not raw shape rows but RLE streams whose first byte is the escape marker consumed by unpackRleTo0200 ($67CB), and the '$01,$16' / '$05,$15' 'headers' the comment describes are simply escape/count/value triples. The boundary is also off by three: block 1 is $FD70-$FE23 (180 bytes) and block 2 starts at $FE24, not $FE21.
- [data_high_FBB8] unknownShapeTail ($FF90, 66 bytes, 'most likely unused leftover memory ... stale bytes that resemble the end of the $8800 message block') is wrong too. $FF90-$FF92 is the last RLE run of the drone block, and $FF93-$FFD1 is a perfectly ordinary 63-byte sprite (the targeting reticle) that overlay B reads at $80EB. The $8800 block's tail ($897F-$89FF) is an unrelated $00/$FF dither pattern and does not match these bytes at all.
- [game_high_FBB8_FBB8] The resident high code ($FBB8-$FD6F, 440 bytes) is a small math/graphics library: Bresenham line stepper ($FBB8/$FBFA), multicolour fat-pixel plotter ($FC58), 8x8 multiply ($FCCD), signed/unsigned 16/8 divide ($FCEC/$FD0A), random jitter ($FD1E) and two identical 24-bit shift-register RNGs ($FD4A on $5A-$5C, $FD5D on $57-$59). Everything from $FD70 to $FFD1 is unreferenced data.
- [game_high_FBB8_FBB8] docs/overview.md is wrong about an 'IRQ handler region $FD5D-$FD6F': $FD5D is nextGameRandom. The game's IRQ/NMI vectors are written at $0A72-$0A7D (all four -> $1298) and $0C6B/$0C72 (IRQ -> $10F1).
- [game_high_FBB8_FBB8] Two RNGs with different roles: $57-$59 is the lock-step game RNG seeded with fixed constants $49,$19,$02 at $0309 (so both modem players generate identical sequences); $5A-$5C is the scenario seed. Overlay A encodes/decodes the 24-bit scenario seed as a 5-character MAP ID using the 32-character alphabet at $890C (5 bits per character, $77F7-$7821) and can also derive it from the game RNG ($77E7).
- [game_high_FBB8_FBB8] Line walker coordinate convention: $93/$95 are columns (X), $92/$94 rows (Y). Evidence: $258F sets $93 = $A5+3 and $92 = $A6+2 and compares them with $F640,x / $F6A4,x; $260D passes $A5+3 in X (column offset) and $A6+2 in Y (row index into the $BFB0/$BFD8 row table built at $0339) to sub_25FA; overlay B feeds $93 into the horizontal pixel coordinate. Overlay A's scenario code ($7371-$739F) swaps the pairs but only uses them symmetrically.
- [game_high_FBB8_FBB8] Map addressing: $BFB0 (lo) / $BFD8 (hi) = 40-entry table of map row addresses ($F000 + 40*row), built by the setup code sub_0339; sub_25FA(X = column, Y = row) returns the cell byte. Unit position arrays: $F640 (column) and $F6A4 (row), 100 entries each, live right after the 1600-byte map.
- [game_high_FBB8_FBB8] The in-game display is a multicolour bitmap at $A000 in VIC bank 2 (same configuration as the title picture: bitmap $A000, screen $8C00, $D018=$38). data_9300 holds the address tables: $9300/$9319 = bitmap row base lo/hi (25 entries, $A000 + 320*row), $9332/$935A = 8*column lo/hi (40 entries), $9382/$939B = screen row lo/hi; sub_2AA3 builds both a bitmap pointer ($B4/$B5) and a screen pointer ($B6/$B7) from them.
- [game_high_FBB8_FBB8] plotFatPixel treats the bitmap as 160x96 fat pixels (each fat pixel = 1 multicolour pixel wide x 2 lines tall). Overlay B ($82BF-$8410) uses it with initLineStepper/stepLine to draw a route/shot line in a 14x10 character zoom window at columns 25-38, rows 1-10 (fat pixel X 100-155, Y 4-43, cells drawn 8x8 fat pixels via sub_84B2 = coord*8+4/+3); sub_84F1 plots a 2x2 marker.
- [game_high_FBB8_FBB8] plotFatPixel clip bug: 'cpx #$A0 / bcc plot / cpy #$60 / bcc plot / rts' rejects a point only when both coordinates are out of range; harmless in practice because overlay B range-checks before calling.
- [game_high_FBB8_FBB8] stepLine's $A9 (0 orthogonal, 1 diagonal) is used at $45DF to add 8 extra movement cost for diagonal moves; $90F4/$90F5 give the per-step row/column delta to the unit movement code ($1D89) and the comm module ($E819).
- [game_high_FBB8_FBB8] The labels D_FD1A and D_FED2 are artifacts of the boot loader's block move loop ($C200/$C203: LDA $FD1A,Y / STA $FED2,Y with decrementing page bytes), not real data references; $FD1A is the TAY inside divideUnsigned16by8.

## game/mainProgram0800

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 0800 | gameEntry | Entry point jumped to by the boot code at $02B8; just JMPs to mainStart. | none | none | high |
| 0804 | loadSectors | Fast-loader read: reads A consecutive sectors of track Y starting at sector X into the address set by setLoadDest (one page per sector). On a drive error it shows msgDiskError via $C3D8 and retries the same sector forever; once a retried read succeeds it calls $C6FF with $92A1 to restore the message area. | A=sector count, Y=track, X=first sector, loadDestLo/Hi | C=0, memory filled, loadDestHi advanced, ldTrack/ldSector/ldCount | high |
| 0843 | writeSectors | Fast-loader write: writes A consecutive sectors to track Y from sector X, source address from setLoadDest. Used by the overlay-A save routines ($7E85, $8194). | A=count, Y=track, X=first sector, loadDestLo/Hi | C=1 on failure, C=0 on success | high |
| 085C | setLoadDest | Stores X/Y as the low/high byte of the next transfer address (self-modifying operands loadDestLo/loadDestHi). | X=lo, Y=hi | loadDestLo, loadDestHi | high |
| 0863 | writeOneSector | Sends command $60, track and sector to the drive, stops CIA1 timer A during the transfer, sends 256 bytes and reads the status byte. | Y=track, X=sector, loadDestLo/Hi | C=1 on error (from finishTransfer) | high |
| 088F | sendSectorData | Sends the 256 bytes at (wrSrcPtr) to the drive one byte at a time, then falls into finishTransfer to read the status. | wrSrcPtr | C=1 on error | high |
| 089C | sendByteToDrive | Clocks one byte to the drive MSB first through CIA2 port A bit 5 with a handshake on bits 4/6/7; simultaneously shifts in a byte from the drive into recvByteShift, which is returned in A (used to fetch the status byte). | A=byte | A=byte received from the drive, C=0, X=0 | high |
| 08EB | readOneSector | Requests track Y sector X with command $80 and receives 256 bytes into the load destination; CIA1 timer A is stopped for the duration. | Y=track, X=sector, loadDestLo/Hi | C=1 on error, ldTrack/ldSector | high |
| 08FE | readSectorBody | Sends the read command, saves and clears the sprite enable register, computes the bad-line avoidance constant ($D011&7 + $2F) into the operand at $095F, then receives the sector and the status byte. | ldTrack, ldSector, loadDestLo/Hi | C=1 on error, savedSpriteEnable, ldCia2Bits | high |
| 0943 | finishTransfer | Restores the sprite enable register and reads the drive status byte; C=1 if the status is nonzero. | savedSpriteEnable | C, A=status | high |
| 0950 | receiveSectorData | Timing-critical 2-bit-at-a-time receive loop for 256 bytes; waits for a raster line that is not a bad line before each byte. | ldDestPtr, ldCia2Bits | 256 bytes at (ldDestPtr) | high |
| 09A6 | manualLookupCheck | Copy protection (disabled in this build: the JSR at $0A9B was patched to BIT). Saves mapSeed ($0B95-$0B97) and $0BA9-$0BAE, picks answer entry ($09C3 index & $1F) from the 32x3 table at $0401 into mapSeed, draws the map (sub_6F04, sub_5AD7, sub_261C) and prints msgIdentifyMap; the player gets 3 tries ($92C7) to type the 3 answer bytes into $5A-$5C, otherwise memory is wiped from $C000 down and the machine hangs. On success it restores the saved bytes. | $0401 table, $5A-$5C player input | $0B95-$0B97 and $0BA9-$0BAE restored, zp_3D/zp_42=0 | high |
| 0A6C | mainStart | Game start-up: disables CIA IRQs, points NMI/IRQ at irqRti ($1298), loads the runtime overlays ($C000, overlay A, comm module), loads the setup block, runs the setup-block code at $0300, initialises IRQ/input state, sets currentScreen=0, calls sub_2636/sub_5E03, (manual check disabled), sets playerSide=$FE and falls into returnToMainMenu. | none | whole system initialised | high |
| 0AA3 | returnToMainMenu | Re-entry point used to abort to the main menu (also jumped to from $5A3F, $C876, $EE0C and by the IRQ handler on SHIFT+C=+F7): VIC IRQs off, sprites off, stack reset to $FD, sub_2D41, initIrqAndInputState, message queue reset, loadingDepth=0, currentScreen=3, showMainMenu, then continues at L_1A86 (link status / menu flow). | none | currentScreen=3, loadingDepth=0, stack reset | medium |
| 0AC8 | startGameFromSetup | Jumped to from overlay A ($7E3F) when setup is complete. Resets the game variables, waits for the game disk, reloads the comm module if the link type flag changed ($0BA5 vs $E030), initialises the comm module, shows the modem prompt and waits for A/O when switching to modem mode, loads the map and setup block, reloads overlay B if needed, builds the owner name string and jumps to sub_1FD0 (battle start). | $0B9B, $0B9C, $0BA5, $0BA6, $87FF, $E030 | map at $F000, setup at $0200, comm module configured, loadingDepth=0 | medium |
| 0B4F | loadCommTailBuild1 | Inside startGameFromSetup: when $0BA5 bit 7 is set, loads $EC00-$EFFF from track 35 s12-15 (comm build 1 tail). Not a separate routine (the setLoadDest arguments are set up at $0B4B). | X=0,Y=$EC | $EC00-$EFFF | high |
| 0BB5 | initIrqAndInputState | Silences the sound, installs the raster IRQ, enables sub_3F57 (patches its first byte to NOP), sets the input lockout to $9E frames, $0549=$9E, clears $92C9/$92E4/$922A and sets currentKey=$FF. | none | IRQ installed, inputLockoutTimer, currentKey | medium |
| 0BD8 | disableCiaInterrupts | Writes $7F to both CIA ICRs and reads them back to acknowledge pending interrupts. | none | CIA interrupts disabled | high |
| 0BE7 | initInterruptVectors | Resets the BRK-exit operand of the IRQ handler ($10FF/$1100) to irqExit ($1293), and sets the comm module's NMI vector copy ($E031/$E032) and the hardware NMI vector to irqRti ($1298). | none | $10FF/$1100, $E031/$E032, $FFFA/$FFFB | medium |
| 0C02 | setNmiVectorRti | Sets the NMI vector to irqRti ($1298); jumped to after loading comm build 1. | none | $FFFA/$FFFB | high |
| 0C0D | jumpToCd00IfPatched | If $CD00 holds a JMP opcode ($4C) it disables VIC IRQs and jumps there, else returns via the RTS at $0C0C. Not referenced by any traced code; at run time $CD00 holds data in the $C000 overlay, so this looks like dead/debug code. | $CD00 | VIC_IRQ_MASK=0 when taken | low |
| 0C1C | resetGameVariables | Clears zero page $15-$7F and $8C-$FF and the variable area $9008-$92FF, sets $9248-$9250, $90EA and $B1 to $FF, calls sub_58D5, sets currentScreen=3 and $0548=$9E (via $C62A). Also called from the setup-block code at $0300. | none | variables cleared | medium |
| 0C66 | installRasterIrq | Installs rasterIrqHandler ($10F1) as the IRQ vector (and its shadow irqVectorLo/Hi), sets the raster line to $FA, clears irqInProgress, acknowledges and enables the VIC raster interrupt. | none | $FFFE/$FFFF, zp_AA-zp_AC, VIC_RASTER, VIC_IRQ_MASK | high |
| 0C8C | showModemSetupPrompt | Stores the caller's string (A/Y) in message slot $20 and 'PRESS SPACE WHEN READY.' ($8837) in slot $21, calls resetMessagesAndBeep and jumps into overlay A at $7DB8. Called from overlay A ($79E3). | A/Y=string address | message slots $20/$21 | medium |
| 0CA0 | resetMessagesAndBeep | Resets the message queue, queues message $20, plays sound 3 twice with a 20-frame pause and sets messageState ($92A1) to $21. | none | $92A1=$21 | medium |
| 0CBD | waitForAnswerOriginateKey | After resetMessagesAndBeep, loops (stirring the RNG) until the last typed key ($9248) is 'A' ($C1) or 'O' ($CF), stores 0 (answer) or 1 (originate) in the comm module flag $E011 and resets the message queue. | $9248 key from IRQ | $E011 answer/originate, $0B7D=0 | high |
| 0CE8 | buildOwnerNameString | Builds the 21-character ownerNameString at $0B7F from the two 10-byte name fields at $FFDE and $FFE8 (first field right-aligned, second left-aligned, spaces squeezed) and terminates it with bit 7. If the first field is blank ($A0) it is replaced by 'OURS'. | $FFDE-$FFF1 | $0B7F-$0B93 | medium |
| 0D49 | setJoystickRepeatDelay | Joystick auto-repeat: increments joyHoldCounter ($91CA) masked by joyCenteredMask ($91C9, cleared by the IRQ when the stick is centred) and sets timer zp_6C to 16 frames for the first step or 6 frames for repeats; preserves A. | $91C9, $91CA | zp_6C, $91CA, $91C9=$7F | medium |
| 0DB7 | scanKeyboard | Keyboard matrix scanner installed into the comm module vector $E013/$E014 and called through $E012/$E015. Returns no key if a joystick is disturbing either CIA1 port, tests RSHIFT, LSHIFT and C= as modifiers, then scans rows 7..0 using the masks at $2F8D/$2F85 and translates the first pressed key through keyMatrixCodeTable ($FE entries are skipped, $FF = CTRL aborts). | CIA1 ports | A=key code ($FF none), Y=$80 if shift held, X=$80 if C= held | high |
| 0DB7 | scanKeyboard | Full C64 keyboard-matrix scanner; the game polls the keyboard itself because the KERNAL is banked out ($01=$35). Installed into the comm module's key vector $E013/$E014 by initCommModule and invoked once per frame through $E015 -> pollKeyboardEvent. It first drives PA=$FF and checks that PB and PA both read $FF (a joystick plugged into port 1 or 2 pulls lines low), then that at least one key is down with all rows selected; it tests RSHIFT (PA6/PB4), LSHIFT (PA1/PB7) and C= (PA7/PB5) as modifiers, then scans rows 7..0 (masks from D_2F8D) and columns 7..0 (masks from spriteBitMasks) and translates the first key found through keyMatrixCodeTable[row*8+col]. Table value $FE (the shift/C= cells) is skipped and scanning continues, $FF (CTRL) aborts with 'no key'. | CIA1_PRA/PRB/DDRA/DDRB; keyMatrixCodeTable ($0D67); row masks D_2F8D ($2F8D); column masks spriteBitMasks ($2F85) | A = key code or $FF for none; Y = $80 when SHIFT is held else $00; X = $80 when C= is held else $00. Leaves CIA1_DDRA=$FF, CIA1_DDRB=$00 and CIA1_PRA holding the last row mask (the IRQ relies on DDRA=$FF to read joystick 2 afterwards). Self-modifies the operands at $0E2B, $0E3A, $0E41, $0E43. | high |
| 0E45 | testKeyMask | Selects keyboard row Y on CIA1 port A, reads port B debounced and ANDs it with mask A (self-modified operand). Z=1 when that key is pressed. | Y=row mask, A=column bit | A/Z | high |
| 0E45 | testKeyMask | Helper for scanKeyboard: selects the keyboard row given by the port-A pattern in Y, reads CIA1 port B twice until two reads agree (debounce) and ANDs the result with the column mask A (patched into the operand at $0E54). Z=1 means that single key is pressed. | Y = row select pattern for CIA1_PRA, A = column bit mask | A = masked port B value, Z=1 if the key is down. Self-modifies $0E54. | high |
| 0E56 | pollKeyboardEvent | Called from the IRQ every frame: calls the comm module key entry $E015, stores raw key/shift/C= in $92A0/$92E4/$92E5, handles key repeat with a 20-frame timer (zp_70), sets currentKey (zp_66), lastKeyCode ($90E9) and newKeyEvent ($90EA); for codes >= $80 also stores lastTypedKey ($9248) and applies shiftedKeyTable to $AC-$BB when shift is held. | $E015 result | zp_66, zp_70, $90E9, $90EA, $9248, $92A0, $92E4, $92E5 | medium |
| 0E56 | pollKeyboardEvent | Per-frame keyboard front end, called only from rasterIrqHandler. Calls the comm module key entry $E015 (which vectors to scanKeyboard), stores the raw results in rawKeyCode ($92A0), shiftHeldFlag ($92E4) and commodoreHeldFlag ($92E5), then does auto-repeat: a key identical to lastKeyCode ($90E9) is ignored until the 20-frame timer zp_70 expires. A fresh key sets zp_70=$14, newKeyEvent ($90EA) and lastKeyCode; codes >= $80 (printable PETSCII/$80) are also stored in lastTypedKey ($9248) and in currentKey (zp_66), and when SHIFT is held codes $AC-$BB (',' '-' '.' '/' '0'-'9' ':' ';') are replaced from shiftedKeyTable. Codes < $80 (F1/F3/F5/F7 = 0..3, RETURN, DEL, cursor keys) go to currentKey only and clear newKeyEvent. | comm module entry $E015 -> scanKeyboard results; zp_70; $90E9 | zp_66 currentKey, zp_70 repeat timer, $90E9 lastKeyCode, $90EA newKeyEvent, $9248 lastTypedKey, $92A0/$92E4/$92E5 raw scan results | high |
| 0EB1 | checkFirePressed | Edge-detects a new fire button press: returns A=0 (Z=1) when fire is down (zp_5F=0) after having been released (zp_79), starting a 10-frame debounce (zp_6D) and playing click sound 1; otherwise A=1. Also fails when sub_59B3 returns C=1. Callers loop on BNE to wait for the button. | zp_5F, zp_79, zp_6D | A/Z, zp_79, zp_6D | medium |
| 0EB1 | checkFirePressed | Edge-detects a fresh joystick fire-button press and is the standard 'wait for the button' primitive (callers loop while it returns non-zero). Returns A=1 (Z=0) when the command ring is nearly full (isCmdRingNearlyFull $59B3 returns C=1), while the 10-frame debounce timer zp_6D is running, or while both zp_5F (fire) and zp_79 (armed flag) are zero. On a genuine new press (zp_5F=0 after zp_79 was set) it reloads zp_6D=10, plays click sound 1 and returns A=0; a release just re-arms zp_79. | zp_5F fire state (0 = pressed), zp_79 armed flag, zp_6D debounce timer, $908D/$908E via isCmdRingNearlyFull | A/Z (0 = fresh press), zp_79, zp_6D, sound 1 queued | high |
| 0ED5 | checkFirePressedNotSolo | Like checkFirePressed but when a press is detected returns soloModeFlag ($0B9B) if negative, else $923F & $40 - i.e. A=0 only for a fresh press in non-solo mode with $923F bit 6 clear. | zp_5F, $0B9B, $923F | A/Z | low |
| 0ED5 | checkFirePressedInteractive | checkFirePressed plus a 'is the local player allowed to act' gate; renamed from the phase-1 name checkFirePressedNotSolo, which is a misnomer (the solo trainer sets $0B9B=0, film playback sets $0B9B=$82). Returns A=0 only for a fresh fire press while filmPlaybackFlag ($0B9B) is not negative and gameOverFlags ($923F) bit 6 is clear, i.e. not while a game film is being watched and not after the game has ended. | zp_5F/zp_79/zp_6D, $0B9B, $923F | A/Z (0 = press accepted) | medium |
| 0EE5 | getSoloOrFlag923F | Orphan entry (A=0 then jumps into checkFirePressedNotSolo at $0EDA): returns $0B9B if negative else $923F & $40. Not referenced by traced code. | $0B9B, $923F | A/Z | low |
| 0EE5 | isPlayerInputAllowed | The gate half of checkFirePressedInteractive without the button test: loads A=0 and jumps into $0EDA, so it returns Z=1 only when filmPlaybackFlag ($0B9B) is not negative and gameOverFlags ($923F) bit 6 is clear. Called from the REPAIR tab menu loop at $658B to decide whether attemptSystemRepair may run for the selected system. (Phase 1 named this getSoloOrFlag923F and recorded it as unreferenced; the XREF shows the call from $658B.) | $0B9B, $923F | A/Z (Z=1 = the player may act) | medium |
| 0EEA | isFireHeld | Returns A=0 while the fire button is held (after one release), without debounce or sound; A=1 otherwise. | zp_5F, zp_79, zp_6D | A/Z, zp_79 | medium |
| 0EEA | isFireHeld | Level (not edge) version of checkFirePressed with no debounce reload, no sound and no ring-buffer check: returns A=0 while the fire button is held after having been released once, A=1 otherwise. Used by the map/console loops at $2376 and $407A where a held button must keep an action running. | zp_5F, zp_79, zp_6D | A/Z, zp_79 | high |
| 0EFE | stopAllSound | Plays 'sound 0' = silences all voices ($C8A8 with A=0). NOTE: the seed labelled this loadRuntimeOverlays, but the overlay loading actually starts at $0F03. | none | $92E7-$92EA | high |
| 0EFE | stopAllSound | Silences everything by calling the $C000 overlay sound entry $C89A with A=0, which fills the four sound-request slots $92E7-$92EA with $FF/$00. Called from initIrqAndInputState ($0BB5) and $1AF7. The seed disassembly labelled this loadRuntimeOverlays; the actual overlay loading starts at $0F03. | none | $92E7-$92EA reset, all SID voices released | high |
| 0F03 | loadRuntimeOverlays_0F03 | Called once from mainStart: $C000 <- track 30 s0-15, then loadOverlayA (+strings+comm tail), then falls into loadCommModule. | $0BA5 | $C000-$CFFF, $6F00-$89FF, $E000-$EFFF | high |
| 0F03 | loadRuntimeOverlays_0F03 | One-shot start-up loader called from mainStart ($0A81): fast-loads track 30 sectors 0-15 into $C000-$CFFF (the runtime overlay that replaces the boot loader), then loadOverlayA (which chains message strings and the comm tail) and falls straight into loadCommModule. | $0BA5 (read further down the chain) | $C000-$CFFF, $6F00-$87FF (variant A), $8800-$89FF, $EC00-$EFFF, $E000-$EBFF, comm module initialised | high |
| 0F16 | loadCommModule | Loads the communications module to $E000: build 1 from track 35 s0-11 when linkTypeFlag ($0BA5) bit 7 is set (then NMI vector -> RTI), otherwise build 2 from track 18 s7 + track 34 s1-11; ends in initCommModule. | $0BA5 | $E000-$EBFF, comm module initialised | high |
| 0F16 | loadCommModule | Loads the $E000 communications module and initialises it. With linkTypeFlag ($0BA5) bit 7 set (solo trainer / build 1) it disables the CIA interrupts, reads track 35 sectors 0-11 and then points the NMI vector at irqRti; otherwise it falls into loadCommBuild2 for the modem/null-modem build. Called from $0AF4 as well as via fall-through from loadRuntimeOverlays_0F03. | $0BA5 bit 7 | $E000-$EBFF, comm module initialised, NMI vector (build 1) | high |
| 0F31 | loadCommBuild2 | Loads comm build 2: track 18 sector 7 to $E000 then track 34 s1-11 to $E100. | loadDest=$E000 | $E000-$EBFF | high |
| 0F31 | loadCommBuild2 | Loads communications module build 2 with interrupts off: track 18 sector 7 supplies the first page at $E000 (that sector lives on the unencrypted directory track), then track 34 sectors 1-11 fill $E100-$EBFF, and control falls into loadCommFinish. | load destination already set to $E000 | $E000-$EBFF | high |
| 0F41 | loadCommFinish | Shared tail: loadSectors with the prepared registers, then initCommModule. | A,X,Y for loadSectors | comm module loaded and initialised | high |
| 0F41 | loadCommFinish | Shared tail of the two comm-module load paths: performs the final loadSectors with the registers the caller prepared and falls into initCommModule. | A = sector count, Y = track, X = first sector | comm module loaded and initialised | high |
| 0F44 | initCommModule | Calls comm entry $E003 with X=1 (initialise), installs scanKeyboard ($0DB7) into the comm module's keyboard vector $E013/$E014 and enables interrupts. | comm module at $E000 | $E013/$E014, I flag cleared | high |
| 0F44 | initCommModule | Calls the comm module's init entry ($E003) with X=1, then patches the module's keyboard vector $E013/$E014 to point at scanKeyboard ($0DB7) so the module's $E012/$E015 entries call the game's own matrix scanner, and finally CLIs. Also called directly from $0AFC. | comm module resident at $E000 | $E013/$E014 = $0DB7, I flag cleared | high |
| 0F55 | loadSectorsDelayed | Waits 8 frames ($C37F is a raster-timed frame delay) then performs loadSectors. Seed name loadSectorsRetry. | A=count, Y=track, X=sector | as loadSectors | high |
| 0F55 | loadSectorsDelayed | Wrapper around loadSectors that first waits 8 frames through the $C000 raster-timed delay $C37F, giving the drive time to settle (and the previous fast-load handshake time to finish) before the next request. Every overlay/map/setup load in this chunk goes through it. | A = sector count, Y = track, X = first sector, load destination from setLoadDest | as loadSectors (memory filled, C = error) | high |
| 0F5F | suspendCommModule | If the link is active ($E03B != 0) writes request $C0 to $E039 and waits until $E03A bit 7 is set (comm module acknowledges it is paused); preserves A. Used before disk access, which needs interrupts off. | $E03B | $E039=$C0 | medium |
| 0F5F | suspendCommModule | Asks the comm module to pause before the game touches the disk (fast loading needs interrupts off and exact timing). When the link is up ($E03B non-zero) it writes request code $C0 to $E039 and spins until the module acknowledges by setting bit 7 of $E03A. A is preserved across the call. | $E03B link-active flag | $E039 = $C0, blocks until $E03A bit 7 set | medium |
| 0F71 | resumeCommModule | If the link is active writes 0 to $E039 and waits until $E03A bit 7 clears (comm module resumed). | $E03B | $E039=0 | medium |
| 0F71 | resumeCommModule | Counterpart of suspendCommModule: when the link is up it clears the request byte $E039 and spins until the module clears bit 7 of $E03A. | $E03B | $E039 = 0, blocks until $E03A bit 7 clear | medium |
| 0FB1 | checkDiskId | Reads the BAM (track 18 sector 0) to $0200 and compares the disk id at $02A2/$02A3 with diskIdTable entry X (X=0 'OZ' game disk, X=2 'EA' data disk; X saved in $92F0). For the game disk in non-solo mode copies the 10-byte name/serial at $02AB to $FFDE. A on entry is remembered: bit 7 set = fail silently, otherwise shows 'INSERT GAME DISK.' or 'NOT A DATA DISK!'. | A=silent flag, X=id index | C=0 ok, C=1 wrong disk/error, $FFDE-$FFE7, $92F0 | high |
| 0FB1 | checkDiskId | Verifies which disk is in the drive. Reads the BAM (track 18 sector 0, unencrypted) to $0200 through loadSectorsDelayed and compares $02A2/$02A3 with the two-character id from diskIdTable[X] (X=0 -> 'OZ' game disk, X=2 -> 'EA' data disk); X is remembered in $92F0. On a match with the game disk while filmPlaybackFlag ($0B9B) is not negative it also copies the ten bytes at $02AB (disk name/serial) to $FFDE-$FFE7, which the link protocol uses to check that both players run the same disk. A on entry is a silent flag: bit 7 set returns C=1 without a message, otherwise the mismatch shows 'INSERT GAME DISK.' or (when the expected id starts with 'E') 'NOT A DATA DISK!' through showDiskMessageAndFail. Called from waitForGameDisk and from the film save/load code in overlay A ($7E53/$7E5C); $100D is an alternate entry used by overlay A ($7E71) just to show 'NOT A DATA DISK!'. | A = silent flag (bit 7), X = 0 or 2 index into diskIdTable | C=0 ok / C=1 wrong disk or drive error, $92F0, $FFDE-$FFE7, $0200-$02FF clobbered by the BAM, self-modifies $0FDD/$0FE4/$0FFF | high |
| 1018 | diskErrorExit | Shows 'DISK ERROR.' through showDiskMessageAndFail. | none | C=1 | high |
| 1018 | diskErrorExit | Loads the address of msgDiskError ('DISK ERROR.') and falls into showDiskMessageAndFail. Reached from checkDiskId when the BAM read fails and jumped to from overlay A ($7E9D). | none | C=1 | high |
| 101C | showDiskMessageAndFail | Plays sound 3, shows the message at A/Y, waits 100 frames, clears the message area ($C39F) and returns C=1. | A/Y=message | C=1 | medium |
| 101C | showDiskMessageAndFail | Common failure exit for the disk routines: plays sound 3 (error beep), displays the message whose address is in A/Y through the $C000 text entry $C3D8, waits 100 frames ($C37F), clears the message area ($C39F) and returns with C=1. | A = message address low byte, Y = high byte | C=1, message area restored | high |
| 1030 | reloadOverlayForVariant | Suspends the comm module, waits for the game disk, then reloads according to overlayVariantFlag ($87FF): bit 7 clear -> strings and comm tail only (variant B stays), set -> overlay A (+strings+tail); finally resumes the comm module. | $87FF | overlays reloaded | medium |
| 1030 | reloadOverlayForVariant | Re-reads whichever $6F00 overlay variant should be resident after another overlay temporarily occupied that space. Suspends the comm module, waits for the game disk, then: overlayVariantFlag ($87FF) bit 7 clear means variant B is already in place so only the message strings and the comm tail are refreshed, bit 7 set means variant A is reloaded in full; finally the comm module is resumed. Called from $1C72 with $0BA7 bumped around it. | $87FF bit 7 | overlay/strings reloaded, comm module suspended and resumed | medium |
| 1047 | loadOverlayA | $6F00 <- track 31 s0-16, $8000 <- track 32 s0-7, then falls into loadStringsAndCommTail. | none | $6F00-$87FF variant A | high |
| 1047 | loadOverlayA | Loads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00 and track 32 sectors 0-7 to $8000, then falls into loadStringsAndCommTail. Variant A holds the setup/menu/film code. | none | $6F00-$87FF = variant A, plus $8800-$89FF and $EC00-$EFFF from the fall-through | high |
| 1060 | loadStringsAndCommTail | $8800 <- track 29 s9-10 (message strings) then falls into loadCommTailBuild2. | none | $8800-$89FF, $EC00-$EFFF | high |
| 1060 | loadStringsAndCommTail | Loads the message-string block from track 29 sectors 9-10 into $8800-$89FF, then falls into loadCommTailBuild2. | none | $8800-$89FF, $EC00-$EFFF | high |
| 1070 | loadCommTailBuild2 | $EC00 <- track 34 s12-15 (comm build 2 tail). Also called from overlay A ($7B33) to restore it after a sub-overlay was loaded there. | none | $EC00-$EFFF | high |
| 1070 | loadCommTailBuild2 | Loads the last page-group of comm build 2 (track 34 sectors 12-15) into $EC00-$EFFF. Also called from overlay A ($7B33) to put the comm tail back after the $EC00 sub-overlay from track 29 was temporarily loaded there. | none | $EC00-$EFFF | high |
| 1080 | loadOverlayBIfNeeded_1080 | Loads overlay B when overlayVariantFlag ($87FF) bit 7 is clear (i.e. the resident overlay should be B), else returns. | $87FF | see loadOverlayB | high |
| 1080 | loadOverlayBIfNeeded_1080 | Called from $0B5F during game start: loads overlay B only when overlayVariantFlag ($87FF) bit 7 is clear, i.e. when B is the variant that should be resident but A is currently in memory; otherwise returns immediately. | $87FF bit 7 | see loadOverlayB | high |
| 1086 | loadOverlayB | Suspends the comm module, $6F00 <- track 33 s0-16, $8000 <- track 32 s8-15, patches overlay byte $750A with the value at $206B, then resumes the comm module. | $206B | $6F00-$87FF variant B, $750A | high |
| 1086 | loadOverlayB | Loads the $6F00-$87FF overlay variant B (the in-game battlefield code): suspends the comm module, reads track 33 sectors 0-16 to $6F00 and track 32 sectors 8-15 to $8000, copies the byte at $206B into overlay location $750A (a per-side branch/opcode patch established by the main program), then resumes the comm module. | $206B | $6F00-$87FF = variant B, $750A patched | high |
| 10AB | loadMapAndSetup_10AB | $F000 <- track 18 s8-14 (40x40 battlefield map) then falls into loadSetupBlock. | none | $F000-$F6FF, $0200-$03FF | high |
| 10AB | loadMapAndSetup_10AB | Loads the 40x40 battlefield map from track 18 sectors 8-14 into $F000-$F6FF and then falls into loadSetupBlock. Called from startGameFromSetup ($0B35) with $0BA7 bumped so the IRQ suppresses keyboard input while the disk is busy. | none | $F000-$F6FF map, $0200-$03FF setup block | high |
| 10BB | loadSetupBlock | $0200 <- track 18 s15-16 (saved setup block including the code at $0300). | none | $0200-$03FF | high |
| 10BB | loadSetupBlock | Loads the saved setup block (track 18 sectors 15-16) into $0200-$03FF; it carries the persisted game settings and the little startup code at $0300. Also called on its own from mainStart ($0A84). | none | $0200-$03FF | high |
| 10CB | suspendCommForDiskAccess | suspendCommModule then $C853 (sets messageState=1 and shows the message line - presumably a loading notice). | none | $92A1=1 | medium |
| 10CB | suspendCommForDiskAccess | Standard prologue for disk access: suspendCommModule followed by the $C000 entry $C853, which sets messageState ($92A1) to 1 and paints the status line (the drive-busy notice). Used by all the overlay/film load and save paths. | none | comm module paused, $92A1 = 1 | medium |
| 10D1 | resumeCommAfterDiskAccess | resumeCommModule, clears messageState ($92A1) and redraws/clears the message area via $C39F. | none | $92A1=0 | medium |
| 10D1 | resumeCommAfterDiskAccess | Epilogue for disk access: resumeCommModule, clear messageState ($92A1) and clear/redraw the message area through $C39F. | none | comm module running, $92A1 = 0 | medium |
| 10DC | waitForGameDiskInserted | Suspends the comm module and loops: checkDiskId(X=0, verbose); on failure calls $C85B (shows 'press button' prompt and waits for fire) and retries until the game disk is present. | none | returns only when the OZ disk is in the drive | high |
| 10DC | waitForGameDiskInserted | Suspends the comm module and then loops at $10DF: call checkDiskId(X=0, verbose); if the game disk is not present, show the 'press button' prompt and wait through $C85B, then retry. Returns only once the OZ disk is in the drive. Called from mainStart ($0ADC), reloadOverlayForVariant and overlay A ($81AC). | none | blocks until C=0 from checkDiskId; comm module left suspended for the caller | high |
| 10F1 | rasterIrqHandler | Raster IRQ at line $FA (and BRK handler). Ignores BRK (jumps through the patchable operand at $10FF). Re-arms the raster line and vector, guards against re-entry with irqInProgress; copies the VIC shadow registers ($9000-$9006) and calls comm module entries $E000 (X=0) and $E00C; returns early if loadingDepth bit 7 is set. Then copies the sprite shadows (colours with 2-nibble flashing, positions plus fine-scroll offsets from $92F4/$92F5 when $92C8 != 0, MSB, expand, enable), bumps the 24-bit frame counter, enables IRQs, and unless inputLockoutTimer is counting down polls the keyboard (pollKeyboardEvent) and joystick port 2 into joyDirection/joyFireReleased. Updates joystick idle/centre counters, the RNG, six countdown timers zp_6C-zp_71, detects SHIFT+C=+F7 (abort to main menu), blinks the cursor colour, updates frame timers, handles chat send/receive, sound tick ($C8E9) and overlay hooks $6728/$6637. | VIC, CIA1, shadows at $9000-$9024, comm module | VIC registers, zp_45-zp_47, zp_5D/zp_5F/zp_60/zp_61, zp_66, timers, chat buffers | high |
| 10F1 | rasterIrqHandler | The game's only interrupt handler, entered from the RAM vector $FFFE at raster line $FA (installed by installRasterIrq $0C66). It first checks the B flag in the pushed status byte and treats a BRK as a no-op by jumping through the patchable operand at $10FF/$1100 (initInterruptVectors sets it to irqExit; it is effectively a debug hook). It then acknowledges the VIC IRQ, re-arms the raster line and the $FFFE/$FFFF vector from the shadows zp_AA/zp_AB/zp_AC, and guards against re-entry with irqInProgress (zp_38). Phase 1: copy VIC_CTRL1/CTRL2 from the shadows $9000/$9001 and call the comm module entries $E000 (X=0, bit/byte timing) and $E00C (carrier tracking) - this half runs even while a disk load is in progress. Phase 2 (skipped only if $0BA7 were negative): copy the rest of the VIC shadows ($9002-$9006, $9020, $9023, $9024), then for sprites 7..0 write the colour shadow $9008+n (values >= $10 are two packed nibbles that alternate every other frame to make the sprite flash), the X shadow $9010+n and the Y shadow $9018+n, each biased by the fine-scroll offsets derived from $92F4/$92F5 when $92C8 is non-zero, and finally the enable mask $9022 (also stashed in spriteEnableScratch). It bumps the 24-bit frame counter zp_45/46/47, CLIs, and unless inputLockoutTimer ($0B7D) is counting down polls the keyboard (pollKeyboardEvent) and - only when no key is pending - reads joystick 2 from CIA1_PRA into zp_5F/zp_61 (fire) and zp_5D/zp_60 (direction). It then maintains the idle counter $90EE, steps the RNG, decrements the six countdown timers zp_6C-zp_71, suppresses keys while $0BA7 is non-zero, detects SHIFT+C=+F7 as the abort hotkey (sets $0BA7=1, inputLockoutTimer=$FF and JMPs to returnToMainMenu, which rebuilds the stack, so this IRQ never returns), blinks the cursor colour, and calls updateFrameTimers, handleChatSend, receiveChatCharacter, the sound dequeue $C8E9 (starting the returned sound through startSound $6728) and soundPlayerTick ($6637). Finally it clears irqInProgress and falls into irqExit. | VIC/CIA1 registers, shadows $9000-$9024, $0B7D, $0BA7, $92C8/$92F4/$92F5, comm module entries $E000/$E00C | all VIC registers and sprite registers, zp_45-zp_47, zp_5D/zp_5F/zp_60/zp_61, zp_66, zp_38, timers zp_6C-zp_71, $90E9/$90EA/$90EE/$9248/$92A0/$92E4/$92E5, chat buffers, sound state | high |
| 1293 | irqExit | Restores Y, X, A and RTIs; also the BRK exit target patched into $10FF/$1100. | stack | none | high |
| 1293 | irqExit | Pulls Y, X and A back off the stack and falls into irqRti. It is both the normal end of rasterIrqHandler and the target patched into the BRK-exit JMP at $10FF/$1100 by initInterruptVectors. | stack layout pushed by rasterIrqHandler | registers restored, RTI | high |
| 1298 | irqRti | A bare RTI used as the NMI vector target (and the IRQ vector during start-up). | none | none | high |
| 1298 | irqRti | A bare RTI. Used as the NMI vector target for the whole game ($FFFA/$FFFB and the comm module's copy at $E031/$E032 are set to $1298 by initInterruptVectors and setNmiVectorRti), so RS-232 NMIs from a modem cannot disturb the game. | none | none | high |
| 129C | updateFrameTimers | Per-frame housekeeping: increments $92F8/$92FB; every 8 frames bumps $9223 (while negative, saturating) and $92C9 (while positive, up to $80) and decrements zp_7F; every 4 frames decrements zp_72 and $9163; every frame decrements $90FC, $91C8, $9234 (unless $80) and $923E while nonzero. | zp_45, zp_B1 | the timers listed | medium |
| 129C | updateFrameTimers | Per-frame housekeeping called from rasterIrqHandler. Increments the two free-running animation ticks $92F8 and $92FB. Every 8 frames (every 16 while zp_B1 = $FF, i.e. during actual play) it advances linkTimeoutCounter ($9223) while that counter is negative, saturating at $FF - this is the packet-exchange watchdog that checkLinkTimeout ($5A14) arms with bit 7 and that a completed exchange clears at $4E69. In the same 8-frame slot it counts gameEndCountdown ($92C9) up from 1 towards $80 and decrements zp_7F. Every 4 frames it decrements zp_72 and $9163; every frame it decrements $90FC, $91C8, $9234 (unless it holds the sentinel $80 = frozen) and $923E, each only while non-zero. | zp_45 frame counter, zp_B1 game phase | $92F8, $92FB, $9223, $92C9, zp_7F, zp_72, $9163, $90FC, $91C8, $9234, $923E | high |
| 130A | stepRandomSeed | Seeds the 24-bit RNG state zp_57-zp_59 with the joystick direction when it is all zero, then steps the LFSR (sub_FD5D). | zp_57-zp_59, zp_5D | zp_57-zp_59, A=random byte | medium |
| 130A | stepRandomSeed | Called from the IRQ every frame: if the 24-bit RNG state zp_57/zp_58/zp_59 is all zero it re-seeds the low byte from the current joystick direction zp_5D (so the generator can never lock up and is entropy-fed by the player), then advances the LFSR through nextGameRandom ($FD5D in the high-memory resident block). | zp_57-zp_59, zp_5D | zp_57-zp_59 advanced, A = the new random byte | high |
| 131B | blinkCursorColor | Every 30 frames (counter at $131A) toggles bit 0 of sprite colour shadow $9008 and copies it to $900A - a blinking cursor sprite colour; only run when currentScreen=0 and zp_B1 is non-negative. | $131A, $9008 | $9008, $900A | medium |
| 131B | blinkCursorColor | Blinks the map cursor: a 30-frame countdown in cursorBlinkTimer ($131A) toggles bit 0 of sprite colour shadow $9008 and copies the result to $900A, so sprites 0 and 2 alternate between two adjacent colours. The IRQ only calls it while currentScreen ($90FB) is 0 (the battlefield screen) and zp_B1 is not negative. | $131A, $9008 | $131A, $9008, $900A | high |
| 1331 | handleChatSend | Chat/message-typing state machine run each frame when the link is active ($E03B) and $92E6 is zero: RETURN ($8D in newKeyEvent) or chatState $7F enters typing mode (chatState=$C0, $9278=$9E, sound 2); when chatState bit 7 is set it sends the pending character count/marker to the remote via $C7D5 and comm entry $E009. Clears chatState when the link is down. | $929C, $90EA, $E03B, $E02E, $92E6 | $929C, $9278, $90EA, $9248 | low |
| 1331 | handleChatSend | Outgoing half of the modem chat ('message to opponent') feature, run every frame from the IRQ. It does nothing and clears chatState ($929C) when the link is down ($E03B = 0) or while the film-name entry is using the same line buffer ($92E6 non-zero). Otherwise: with chatState positive, pressing RETURN (newKeyEvent = $8D) or a chatState of $7F opens typing mode - chatState = $C0, the cursor glyph $9E is put at the head of the outgoing line buffer $9278, the key is swallowed and sound 2 plays; this only starts when the comm transmit queue $E02E is empty. With chatState negative it takes the character count from bits 0-5, and if the transmit queue is not full (< $10 bytes) it calls the $C000 line editor $C7D5 with A=$A3 (max 35 characters, scroll when full) to consume the next key into $9278, then queues that character to the opponent through the comm send entry $E009 and stores the new count back as ($count ^ $80) / $40. If the queue has filled up it instead sends RETURN ($8D) and leaves typing mode by clearing bit 7 of chatState. | $E03B, $E02E, $92E6, $929C, $90EA, $9248 | $929C, $9278.. line buffer, $90EA/$9248 cleared, one byte queued to $E009, sounds 2 | medium |
| 1399 | receiveChatCharacter | Fetches one received byte from the comm module ($E006; C=1 none) and, for codes >= $80, handles RETURN (marks message complete, $9277=$80, sound 5), DEL (backspace in the incoming message) or appends the character via appendChatCharacter. | $E006 result, $9277 | $9253 buffer, $9277, zp_72 | medium |
| 1399 | receiveChatCharacter | Incoming half of the chat, called every frame from the IRQ. Pulls one byte from the comm module receive ring ($E006 returns C=1 when empty) and ignores anything below $80. RETURN ($8D) marks the message complete by setting incomingChatLength ($9277) to $80 and falls into the completion tail; DEL ($88) backspaces by overwriting the previous character with a space through appendChatCharacter; everything else falls into appendChatCharacter. | comm module receive ring via $E006, $9277 | $9253-$9276 buffer, $9277, zp_72, sound 5 | high |
| 13C4 | appendChatCharacter | Appends character A to the 36-byte incoming message buffer at $9253 (index $9277, scrolling the buffer left when full) and rewrites the text with bit 7 set into the display line at $0549 (with $0548=$1E). | A=char, $9277 | $9253-$9276, $0548-$056C, $9277 | medium |
| 13C4 | appendChatCharacter | Appends character A to the 36-byte incoming-message buffer at $9253 (index/length in $9277; a negative $9277, meaning the previous message was complete, restarts at 0, and when the buffer is full at 35 characters the whole buffer is scrolled left one byte). It then rewrites the whole visible status line: the leading glyph $1E goes to $0548 and the message text to $0549 onwards, with only the newest character carrying bit 7 - the game's string terminator convention - so the line always ends exactly at the last character received. Finally it clears the message hold timer zp_72 and plays sound 5 (the incoming-character click). | A = character, $9277, $9253 buffer | $9253-$9276, $9277, $0548-$056C status line, zp_72 = 0, sound 5 | medium |
| 1414 | enterMainMenuKeepSelection | Main-menu entry from the F-key screen switch at $076D: picks the default item (selectDefaultMenuItem), clears the text window and joins mainMenuLoop at the item-changed handler. | none | menuSelection | medium |
| 141D | enterMainMenu | Main-menu entry from the F7 screen switch at $0706: sets $1412=$7F, menuSelection from linkState bit 7, draws the menu (showMainMenu) and enters mainMenuLoop. | $922A | menuSelection, screen | medium |
| 1439 | mainMenuLoop | Main menu: calls sub_40B2/sub_16B7 (highlight blink), redraws link-dependent text, checks F-key screen switches (sub_06BA), moves the selection with joystick left/right among 3 items (20-frame repeat, sound 2) and on fire dispatches through menuItemHandlerTable; when the selection changes it draws the item panel (item 0 = game setup summary, 1 = sub_175A, 2 = sub_19F3). | zp_5D, zp_6C, $90F9 | $90F9 menuSelection, screen | medium |
| 14F0 | snapshotSetupId | Copies $9241-$9243 to $92D7-$92D9 and map-table byte $F9C4[$9236] & $3F to $92D6 - remembers the identity of the current setup. | $9241-$9243, $9236 | $92D6-$92D9 | low |
| 1507 | selectDefaultMenuItem | Chooses the default main-menu item: 2 if $0BA8 is nonzero, else 1 if linkState bit 7 is set or the current setup differs from the snapshot in $92D6-$92D9, else 2. | $0BA8, $922A, $9241-$9243, $92D6-$92D9 | $90F9 | medium |
| 153B | showMainMenu | Draws the main-menu screen: clears $2A29-$2A2C, background colour shadow $9004=$0C, $9226=0, zp_41=1, draws the title/credits text (sub_1643), the menu frame (drawMenuFrame) and continues at $C5D7. | none | screen | medium |
| 1565 | drawMenuFrame | Draws two frame elements (sub_2C7F with X=$18 and $19) then falls into clearMenuTextWindow. | none | screen | low |
| 156F | clearMenuTextWindow | Sets the menu text attribute and window bounds and clears the window ($C06A). | $922A, $90F9 | zp_39-zp_40, zp_3D | low |
| 1575 | setMenuTextWindow | Sets the text attribute from link state (sub_16EB) then the menu window bounds. | $922A | zp_3D, zp_39-zp_40 | low |
| 1578 | setMenuWindowBounds | Text window for the menu panel: cursor col 0, top row 7, cursor row 7, left column 2, bottom 22, right column 36. | none | zp_39=7, zp_3A=$16, zp_3B=2, zp_3C=$24, zp_3F=0, zp_40=7 | medium |
| 158F | clearMenuWindowPlain | Attribute 0, menu window bounds, clear window; jumped to from $2E0A. | none | zp_3D=0, screen | low |
| 1599 | drawGameSetupSummary | Main-menu item 0 panel: prints 'STANDARD' or 'CUSTOM' (gameOptions $0BA3 bit 6), the game type name from the 8-byte table at $CCC8 (Scrimmage/QB Sneak/The Bomb/Face-Off/Sluggers/Full War/Defender, $0BA3 & 7), '1ST PLAY'/'2ND PLAY' from firstPlayFlag ($0BA1), the owner name string and overlay text $86AC, and for custom games the text at $0636. | $0BA3, $0BA1, $0B7F | screen, zp_3B/zp_3D/zp_3F | medium |
| 1643 | drawMenuTabBar | Draws the header of the in-game STATS / REPAIR / MISC screen (screen id 3): full-screen text window, prints 'STATS' (colour $CD), 'REPAIR' ($C6) and 'MISC' ($C2) on row 3 from column 10, then colours the three tab boxes with setTabNormalColour for X=2..0 and tail-calls sub_2A56 (frame/shape draw). Called once from sub_153B when the menu screen is entered. | none | screen updated; zp_21 used as loop counter; zp_39-zp_40, zp_3D, zp_3F clobbered | high |
| 168C | setTabHighlightColour | Colours tab X with its bright colour from tabHighlightColourTable (falls into setTabNormalColour if the entry is 0). Used by the tab-bar loop when a tab becomes selected and by blinkSelectedTab. | X = tab index 0..2 | fills a 2x2 colour rectangle at row 1, column tabColumnTable[X] via sub_2A64 after drawing the tab shape ($9804) with sub_2B18; self-modifies L_16AC/L_16AE operands | high |
| 1691 | setTabNormalColour | Colours tab X with its normal colour from tabColourTable: sets D_2A2D=1 (row) and D_2A2E=tabColumnTable[X] (column), draws the tab shape at $9804 with sub_2B18 and fills 2x2 colour cells with sub_2A64. | X = tab index 0..2 | screen colour RAM updated; D_2A2D/D_2A2E set | high |
| 16B7 | blinkSelectedTab | Called every pass of the menu-screen main loop (L_1439). When countdown zp_6E reaches 0 it reloads it with 10 and toggles blinkPhase (D_1889), recolouring the selected tab (D_90F9) bright or normal so it blinks. | zp_6E countdown, D_90F9 selected tab, D_1889 | zp_6E=10, D_1889 toggled, tab recoloured | high |
| 16D6 | printEnergyLabel | Prints the string 'ENERGY' ($CBB8) at column $1C, row 8 of the REPAIR tab using the menu text colour. | none | zp_3D, zp_3F, zp_40 set; text printed | high |
| 16EB | setMenuTextColour | Sets the text colour byte zp_3D for the menu screen: $61 normally, $21 when the REPAIR tab (D_90F9==1) is shown and the comcen is stunned (D_922A bit 7). | D_922A, D_90F9 | zp_3D | high |
| 16FF | printEnergyValue | Prints the comcen energy as a percentage at column $22 row 9: reads unitEnergyTable[comcenUnit] & $3F, doubles it (0..100), right-aligns with leading spaces and prints it with sub_C17D only if it changed since lastEnergyShown. Then classifies it into comcenSystemStatus[0]: 0 when 100%, $40 when >= 21%, $80 (critical) below. | D_9236 comcen unit index, D_F9C4 energy table, D_16FE | D_16FE updated, D_9241 (energy status) updated, text printed; saves/restores the text window (sub_C41B/sub_C40E) | high |
| 175A | drawRepairTab | Draws the REPAIR tab contents: background colour $66 (or $22 when stunned), clears the window, draws the comcen schematic (sub_8632 in overlay B), paints a 20x10 colour block at row 10 column 13, resets the blink state and energy snapshot, then draws the four system status markers (unless stunned), prints the ENERGY label/value and, when stunned, 'COMCEN STUNNED!' at column 5. Remembers the stun flag in D_1413 so the main loop can redraw on change. | D_922A stun status, D_9236, D_F9C4 | D_1413, zp_6E=0, D_188A=0, D_1889, D_16FE=$FF, D_17D8 energy snapshot, D_91D5 (=4 when markers drawn), screen updated | high |
| 17DA | repairTabAction | Fire-button action of the REPAIR tab (entered through the address table D_1535, index 1; the disassembly shows it as data). Only works when the DAMAGE option ($0BA3 bit 4) is on, not in film playback ($0B9B bit 7) and a game is running ($0BA8==0). If the comcen is stunned it prints 'ATTEMPTING REPAIR...', plays sound $18, waits 200 frames and, with a 1-in-8 chance while the stun counter is below 8, queues command byte $9E and plays sound $13 (else $12). Otherwise (L_1823) it queues a 3-byte command $80,unit,x,y for the comcen if its flag byte allows, blanks the markers, shows the 'REPAIR WHICH:' selection screen (sub_652E, also mis-typed as data), redraws the markers and snapshots the statuses (sub_14F0). | D_0BA3, D_0B9B, D_0BA8, D_922A, D_9236, D_FB54, D_F640/D_F6A4 | command bytes queued via sub_55EC/sub_560A, sounds, screen; D_1889, D_188A, D_1412 loop counter, zp_73-zp_75 | medium |
| 17DA | repairTabAction | Fire-button handler of the REPAIR tab (entry 1 of menuItemHandlerTable at $1535, called through the self-modified JSR at $148D). Does nothing unless the DAMAGE option is on (gameTypeOptions $0BA3 bit 4), the game is not a film playback ($0B9B bit 7 clear) and a game is running ($0BA8 == 0). When the comcen is stunned (comcenStunStatus $922A bit 7) it runs the stun-repair attempt: prints 'ATTEMPTING REPAIR...', plays sound $18, advances the battle 200 frames, clears the message, and - only while the stun counter ($922A & $0F) is below 8 - takes a 1-in-8 random chance to queue the 1-byte command $9E (cmdComcenRepairedOrSetupReady, which clears the comcen stun counter on both machines) with success sound $13, otherwise failure sound $12, then runs 20 more frames. When the comcen is not stunned (L_1823) it first halts the comcen if needed: if unitDisplayFlags[comcen] bit 7 is clear (still moving) or bit 6 is set (group order active) it queues command $80 (cmdMoveUnit) with the comcen's own current column/row, which halts it and drops its group membership. Then it erases all four system markers, sets showOkMarkers and opens the 'REPAIR WHICH:' selection menu (runRepairWhichMenu $652E), and on return consumes a pending space key, restores the menu text window, redraws the four markers and tail-calls snapshotSetupId. | D_0BA3 gameTypeOptions bit 4, D_0B9B filmPlaybackFlag, D_0BA8 noGameInProgress, D_922A comcenStunStatus, D_9236 comcenUnit, D_FB54 unitDisplayFlags, D_F640/D_F6A4 unit column/row, RNG nextGameRandom | commands $9E (queueCmdBytePending) or $80 (queueCmd4 with zp_73/74/75) queued into the outgoing ring; sounds $12/$13/$18 via sub_C89A; battle advanced by runGameFrames; blinkPhase=1, showOkMarkers toggled, menuFlag1412 used as a 3..0 loop counter; markers redrawn; comcenStatusSnapshot $92D6-$92D9 refreshed | high |
| 188B | toggleSystemMarker | Blink helper used by the repair selection menu (called from $6108 and blinkRepairMarker): toggles blinkPhase and either draws the status marker of system D_91D5 (drawSystemMarker) or erases it (eraseSystemMarker with colour $16). | D_91D5 selected system 0..3, D_1889 | D_1889 toggled; marker drawn/erased | high |
| 188B | toggleSystemMarker | Blink helper for the REPAIR WHICH menu. Toggles blinkPhase ($1889) and then either draws (phase 0) or erases with colour $16 (phase 1) the marker of the currently highlighted comcen system D_91D5. Called from blinkRepairMarker and from the menu-row highlight toggler at $6108, which invokes it whenever the menu selector runs in repair mode (selectedUnit $90F8 == $FF) and the highlighted row is < 4, so the schematic marker blinks in step with the menu row. | D_91D5 selected system 0..3, blinkPhase $1889 | blinkPhase toggled; falls through to drawSystemMarker or jumps to eraseSystemMarker (X=$16) | high |
| 189D | drawSystemMarker | Draws the status marker of comcen system Y on the schematic as a plus-shaped group of five colour cells at column systemMarkerColumnTable[Y], row systemMarkerRowTable[Y]: colour $15 (green) when comcenSystemStatus[Y]==0, $12 (red) when bit 7 set (damaged), $17 (yellow) when $40. An OK system is erased instead (3x3 of $16) unless showOkMarkers (D_188A) is set. Refreshes the energy reading first. | Y = system index 0..3, D_9241, D_188A | screen colour cells; D_2A2A, D_2A2D, D_2A2E; self-modifies L_18A3 | high |
| 189D | drawSystemMarker | Draws the status marker of comcen system Y on the comcen schematic of the REPAIR tab. Saves Y in the self-modified operand at $18A4 (printEnergyValue clobbers Y), refreshes the energy reading and hence comcenSystemStatus[0], then picks the colour byte: $15 (white/green) when comcenSystemStatus[Y]==0, $12 (white/red) when bit 7 is set (damaged), $17 (white/yellow) otherwise ($40 = degraded energy). A healthy system is erased with $16 instead of drawn unless showOkMarkers is set (only true while the REPAIR WHICH menu is open). The marker itself is a plus of five colour cells: origin (row=systemMarkerRowTable[Y], col=systemMarkerColumnTable[Y]), cells (r,c+1), (r+2,c+1), (r+1,c), (r+1,c+1), (r+1,c+2), each painted with a 1x1 fillScreenColourRect. | Y = system index 0..3, D_9241 comcenSystemStatus, showOkMarkers $188A, systemMarkerColumnTable/systemMarkerRowTable | screen RAM colour cells at $8C00; D_2A2A fill colour, D_2A2D row, D_2A2E column; self-modifies $18A4; energy readout refreshed | high |
| 1919 | eraseSystemMarker | Fills the 3x3 colour block of system Y's marker with colour X (normally $16 = blank) via sub_2A64. | Y = system index, X = colour byte | D_2A2D/D_2A2E set, rectangle filled | high |
| 1919 | eraseSystemMarker | Fills the 3x3 colour-cell block of system Y's marker (origin systemMarkerRowTable[Y] / systemMarkerColumnTable[Y]) with the colour byte in X, which every caller passes as $16 - the schematic background colour used when the REPAIR panel is drawn with text colour $66. Used to blank markers before the REPAIR WHICH menu and as the 'off' phase of the marker blink. | Y = system index 0..3, X = colour byte (always $16) | D_2A2D/D_2A2E set, 3x3 colour rectangle filled via fillScreenColourRect | high |
| 192D | sendEnergyRepairCommand | Queues the 3-byte command $9A, comcenUnit, repairAmount (D_17D9) to the opponent/game engine via sub_560F. Shown as data in the disassembly; called from attemptSystemRepair. | D_9236, D_17D9 | zp_74, zp_75 set; command queued | medium |
| 192D | sendEnergyRepairCommand | Queues the 3-byte command $9A (cmdAddEnergy) with parameters comcenUnit ($9236) and repairAmount ($17D9) through queueCmd3, so both machines add the repaired energy to the comcen's entry in unitEnergyTable. The only part of a comcen repair that travels over the link; the status of systems 1-3 is purely local. | D_9236 comcenUnit, repairAmount $17D9 | zp_74 = comcen unit, zp_75 = amount; command $9A queued, pendingOwnCmdCount $929F incremented | high |
| 193C | attemptSystemRepair | Called from the 'REPAIR WHICH:' menu code at $6590 (also mis-typed as data) with the chosen system in D_91D5. Prints 'ATTEMPTING REPAIR...', sound $18, then succeeds when (random & 3)==0 ((random & 1) for the energy system) and the system is damaged: energy gets (50-energy)/8+1 added (capped at 50) through sendEnergyRepairCommand, other systems have comcenSystemStatus[Y] cleared; plays sound $13 on success, $12 on failure, blinking the marker for about 60 ticks each time. Finally resets the energy snapshot, zp_6E, blinkPhase and clears the message line. | D_91D5, D_9241, D_17D8, RNG sub_FD5D | D_17D8, D_17D9, D_9241[Y], zp_6E=0, D_1889=0; sounds, command queued | medium |
| 193C | attemptSystemRepair | One repair attempt on the comcen system selected in D_91D5, called from the REPAIR WHICH menu loop at $6590 (skipped in solo/film mode). Prints 'ATTEMPTING REPAIR...', plays sound $18 and rolls the RNG: the attempt succeeds with probability 1/2 for the energy system (index 0, random & 1) and 1/4 for systems 1-3 (random & 3), and only if the system is not already OK. On success for the energy system it computes repairAmount = (50 - comcenEnergySnapshot)/8 + 1, sends it with sendEnergyRepairCommand and updates the snapshot (capped at 50); for the other systems it simply clears comcenSystemStatus[Y]. Plays sound $13 on success, $12 on failure, with a marker blink before and after the sound, then re-reads the comcen energy into the snapshot, clears zp_6E and blinkPhase and erases the message line. | D_91D5 selected system, D_9241 comcenSystemStatus, comcenEnergySnapshot $17D8, D_9236, D_F9C4 unitEnergyTable, nextGameRandom | repairAmount $17D9, comcenEnergySnapshot $17D8, D_9241[Y] cleared on success, command $9A queued for the energy system, sounds $12/$13/$18, zp_6E=0, blinkPhase=0, message line cleared | high |
| 19B0 | blinkRepairMarker | Blinks the selected system marker: zp_6F = 60 ticks; every 20 frames (sub_4094) calls toggleSystemMarker until zp_6F expires or the game-end flag D_92C9 is set. | D_91D5, D_92C9 | zp_6F, marker toggled | high |
| 19B0 | blinkRepairMarker | Blinks the marker of the selected comcen system for about one second while a repair attempt resolves: sets the IRQ frame countdown zp_6F to 60 and, every 20 game frames (runGameFrames, so the real-time battle keeps running), calls toggleSystemMarker until zp_6F reaches 0. Aborts immediately if the game-over flag D_92C9 is nonzero. | D_91D5, D_92C9 gameEndFlag | zp_6F counted down by the IRQ, blinkPhase toggled, marker drawn/erased, battle advanced | high |
| 19C9 | printAttemptingRepair | Sets the repair message window and prints 'ATTEMPTING REPAIR...' ($CBE5), then restores the previous text window (sub_C40E). | none | text printed | high |
| 19C9 | printAttemptingRepair | Positions the repair message line (setRepairMessageWindow) and prints the string 'ATTEMPTING REPAIR...' at $CBE5 with sub_C133, then restores the previously saved text window with sub_C40E. | none | text drawn into the bitmap at column 10, row 20; text window restored from $92B6 | high |
| 19D6 | clearRepairMessage | Sets the repair message window and prints 20 spaces (the runtime routine at $C0EF prints A copies of the fill character; the label bootReceiveSectorData is stale), then restores the window. | none | message line cleared | high |
| 19D6 | clearRepairMessage | Erases the repair message line: positions the same window and prints 20 copies of the text fill character zp_3E (routine $C0EF in the $C000 overlay; its label bootReceiveSectorData is a stale leftover from the boot loader that used to occupy $C000), then restores the text window. | zp_3E fill character (0 while the repair menus are up) | 20 characters of the message line blanked; text window restored | high |
| 19E1 | setRepairMessageWindow | Saves the current text window (sub_C41B), sets the menu text colour, the stats window geometry (sub_1578: left 2, rows 7..$16, width $24) and positions the cursor at column 10, row 20. | none | zp_39-zp_40, zp_3D set, window saved at $92B6 | high |
| 19E1 | setRepairMessageWindow | Saves the current 12-byte text-window state to $92B6 (sub_C41B), sets the menu text colour (setMenuTextColour, $61 or $21 when stunned), applies the menu panel window geometry (setMenuWindowBounds: left column 2, rows 7..$16, width $24) and places the cursor at column 10, row 20 by falling into sub_C097/sub_C099. Shared prologue of printAttemptingRepair and clearRepairMessage. | D_922A, D_90F9 (through setMenuTextColour) | zp_39-zp_40 window, zp_3D colour, cursor pointer recomputed; previous window saved at $92B6 | high |
| 19F3 | printOptionsList | Prints the MISC tab: window at column 6 rows 9..$1D, colour $61, '   OPTIONS'. While a game is running ($0BA8==0 and D_92C9==0) it lists 'ABORT GAME', 'VOICE PAUSE' (only when not in film playback and the modem build flag $0BA5 is clear) and ' (EXIT)'. Otherwise it lists the main menu: COMPETE WITH MODEM OPPONENT / PRACTICE WITH SOLO TRAINER / WATCH GAME FILM / SAVE GAME FILM / LOAD GAME FILM and, when the comm link is active (D_E03B), DISCONNECT/SET VOICE. | D_0BA8, D_92C9, D_0B9B, D_0BA5, D_E03B | text printed; zp_39-zp_3F set | high |
| 1A49 | miscTabAction | Fire-button action of the MISC tab (D_1535 entry 2). With a game running it runs the options menu (sub_65A0 with 2 or 3 items); item 0 ABORT GAME sends command $8C (reason 0) to a connected opponent, waits up to $26 ticks for the transmitter, hangs up (hangUpModem) and redisplays the status message. Then, and whenever no game is running, it loops: clear window, restoreSettingsAfterFilm, printOptionsList, runMainMenu. | D_92C9, D_0BA8, D_0B9B, D_0BA5, D_E03B, D_0B9C, D_E030, D_92A1 | D_91D4/D_91D5/D_91D6 menu state, D_92A1, zp_73, D_9163, zp_7F; may end the game via endGameToMainMenu | medium |
| 1ADC | endGameToMainMenu | Resets game-session state when a game ends (also jumped to from sub_C86D when D_92C9 is negative): clears D_92C9, zp_BE, the outgoing command ring indices $908D/$908E and D_92FD; reloads the runtime overlays unless D_67C8 is $0D/$0E; for comm build 1 (D_E030 bit 7) clears the link flag and makes PRACTICE the default item; marks no game in progress ($0BA8=1); when not in setup/film mode calls sub_57F2 and makes WATCH GAME FILM the default; finally sets commSessionState $0B9C to $80 if the link is active (flushing it with sub_56F1 if needed) or 0. | D_67C8, D_E030, D_E03B, zp_B1, D_0B9B, D_0B9C | D_92C9=0, zp_BE=0, D_908D/D_908E=0, D_92FD, D_0BA8, D_0B9C, D_E03B | medium |
| 1B33 | hangUpModem | Entry that hangs up with a resulting commSessionState of 0 (falls into hangUpModemSetState with A=0). | none | see hangUpModemSetState | high |
| 1B35 | hangUpModemSetState | If the link is active and this is the modem build (D_E030 bit 7 clear) shows message $18 'PRESS SPACE, WAIT, HANGUP PHONE.' and waits for space, then waits up to $26 ticks for D_E03C bit 7 to clear; calls the comm module sub_E003 with X=3 (link down) and stores A into $0B9C. | A = new value for D_0B9C, D_E03B, D_E030, D_E03C | D_0B9C, comm flags cleared by sub_E003, zp_7F; self-modifies L_1B59 | high |
| 1B5F | openCommLink | If no link is active: on the modem build ($0BA5 bit 7 clear) shows message $17 'PICK UP PHONE THEN PRESS SPACE.' and waits for space, then calls sub_E003 with X=0 which sets the comm flags D_E03B/D_E03C/D_E03D to $FF. | D_E03B, D_0BA5 | comm link flags set | high |
| 1B74 | openLinkAndWaitForConnection | openCommLink followed by waitForConnection. | none | see callees | high |
| 1B77 | waitForConnection | On the modem build: shows persistent message $10 'WAITING FOR CONNECTION...', runs waitForCarrier (zp_18 preset to $FF), clears bit 7 of zp_BE, clears the message and restores the status area (sub_C39F). | D_0BA5 | D_92A1, zp_18, zp_BE | high |
| 1B9A | waitForCarrier | Polling loop: services the message/status system (sub_C4EF), handles comm status code zp_B1==$FF via sub_5A14, waits 2 frames and decrements zp_18 while D_E03D bit 6 is clear (held at 1 instead of wrapping), until D_E03C bit 6 (connected) is set; then silences sound (sub_C89A 0). The stray $60 at $1BC0 is an unreachable RTS. | zp_B1, D_E03C, D_E03D, zp_18 | zp_18, sound off | medium |
| 1BC1 | showMessageWaitForSpace | Shows status message A (stored in D_92A1 as the persistent message), clears D_9277/D_929C/D_0B7D, then loops servicing sub_C4EF and stirring the RNG until lastKeyPressed (D_9248) == $A0 (space); clears the message (sub_C6EF), sets D_0B7D=$1E and D_9248=$FF. | A = message id | D_92A1, D_9277, D_929C, D_0B7D, D_9248 | high |
| 1BF7 | voicePauseReconnect | Voice pause: hangUpAndPause, then openLinkAndWaitForConnection; stores A in zp_7B and clears bit 7 of D_91D5. Called from the VOICE PAUSE menu item (sub_65A0), sub_5A14 and the $C000 overlay ($C768). | D_0B9C | zp_7B, D_91D5 | medium |
| 1C08 | hangUpAndPause | hangUpModemSetState with A = current $0B9C (or 0 if it is negative), then waits 250 frames (sub_C37F). | D_0B9C | D_0B9C | high |
| 1C17 | disconnectFromOpponent | If a session is active ($0B9C bit 7): queues command $8C with reason byte 5 (zp_73), flushes the outgoing queue (sub_562B) and hangs up (hangUpModem). Used for DISCONNECT/SET VOICE and before starting a solo practice game. | D_0B9C | zp_73=5, command queued, link closed, D_0B9C=0 | medium |
| 1C2C | runMainMenu | Runs the main menu drawn by printOptionsList: 5 items (6 = DISCONNECT/SET VOICE when $0B9C bit 7), initial item D_92FD, first row $0B; sets D_90F8 and zp_B1 to $F0, colour $60, and calls the menu selector sub_5FD1. Item 5 -> disconnectFromOpponent and return. Otherwise waits 15 frames, disconnects first for item 1 (PRACTICE WITH SOLO TRAINER), reloads the $6F00 overlay/strings (sub_1030 with $0BA7 bumped) and jumps to L_7DE3 in the overlay to act on the choice. | D_0B9C, D_92FD, zp_3B | D_91D4, D_91D5, D_91D6, D_90F8=$F0, zp_B1=$F0, zp_3D/zp_42=$60, D_0BA7 | medium |
| 1C7B | isAsymmetricGameType | Tests the game type (D_0BA3 & 7): returns Z=1 when it is 1 (QB SNEAK) or 6 (DEFENDER), the scenarios where '1ST PLAY'/'2ND PLAY' is shown on the STATS tab. | D_0BA3 | Z flag, A = type | medium |
| 1C87 | restoreSettingsAfterFilm | When the film-playback flag $0B9B bit 7 is set: clears it and, if pendingGameType $0BA4 != $FF, copies the pending settings back into the current ones: $0BAF-$0BB4 -> $0BA9-$0BAE, $0B98-$0B9A -> $0B95-$0B97, $0BA2 -> $0BA1, $0B9E -> $0B9D, $0BA4 -> $0BA3. | D_0B9B, D_0BA4, D_0BAF.., D_0B98.., D_0BA2, D_0B9E | D_0B9B=0, D_0BA9.., D_0B95.., D_0BA1, D_0B9D, D_0BA3 | medium |
| 1CC1 | setViewVerticalLimit | Sets zp_F8, the exclusive limit for the view centre row: 40 normally, 20 when zp_B1 (setup/film phase) is nonzero, in which case the incoming row delta Y is also biased by -20 (the caller adds it back). | Y = row delta, zp_B1 | zp_F8, Y adjusted | medium |
| 1CD3 | scrollViewBy | Scrolls the battlefield view by X columns and Y rows (-1/0/+1): keeps the view centre (zp_A5+3, zp_A6+2) and, when a group box is active (D_920A bit 7), the box offsets D_922E-D_9231 inside the 40x40 map; the actually applied deltas go to zp_F0/zp_F1 (0 if clamped), zp_A5/zp_A6 are updated, D_91CC cleared, sub_3049 refreshes the cursor cell, then it continues into scrollViewBitmap. | X = dx, Y = dy, zp_A5, zp_A6, zp_B1, D_920A, D_922E-D_9231 | zp_A5, zp_A6, zp_F0, zp_F1, zp_F8, D_91CC=0; screen scrolled | high |
| 1D4E | scrollViewToUnit | Scrolls the view step by step along a Bresenham line from the current cursor cell (zp_92 row, zp_93 column) to unit X's position until the unit is centred. A nonzero A animates: each step waits for countdown zp_6E (reloaded with A) while polling input (sub_40B2) and aborts as soon as the joystick leaves centre (zp_5D != $0F). Callers: $25B3 (A=1, selected unit D_90F8) and $485C (A=0). | A = ticks per step (0 = immediate), X = unit index, zp_92/zp_93 current cell | D_1D4C, D_1D4D, zp_6E, zp_92-zp_97, zp_8C-zp_91, zp_A5/zp_A6, screen | high |
| 1D97 | setLineTargetToUnit | Loads the Bresenham target from the unit position tables: zp_95 = unitXTable[X], zp_94 = unitYTable[X]. | X = unit index | zp_94, zp_95 | high |
| 1DE4 | scrollViewBitmap | Shifts the 7x5-cell battlefield view (16x16-pixel cells at screen column 25, row 1: bitmap $A208, screen RAM $8C41, colour RAM +$4C00) by one cell in the direction given by zp_F0/zp_F1 (0/1/2 = -1/0/+1) using two self-modified copy loops parameterised from the tables at $1DA2-$1DE3, then redraws the newly exposed column (5 cells) and/or row (7 cells) from the map with sub_25FA/sub_5D1A. Entered only by jmp from scrollViewBy. | zp_F0, zp_F1, zp_A5, zp_A6 | zp_F2-zp_F7, zp_54-zp_56, bitmap/screen/colour RAM; self-modifies L_1E81-L_1EBF and D_1E89/D_1EBE opcodes | high |
| 1F4E | keepGroupBoxInsideMap | Nudges the view origin zp_A5/zp_A6 one cell at a time until the group box (centre + D_922E..D_9231 offsets) fits inside the map (width 40, height zp_F8), then redraws the whole view (sub_26C1) if anything moved. Called from $319D after a group box is set up. | zp_A5, zp_A6, D_922E-D_9231, zp_B1 | zp_A5, zp_A6, zp_18, zp_19, zp_F8; view redrawn | high |
| 1FC3 | enterMapScreen | Entry used by the game-mode dispatcher (sub_06BA, mode 0): restores the viewport origin from savedViewportX/Y ($92C3/$92C4), redraws the whole game screen (sub_2639) and falls into runMapMainLoop. | D_92C3/D_92C4 saved viewport origin | zp_A5/zp_A6 viewport origin; screen redrawn; does not return (falls into sub_1FD0) | high |
| 1FD0 | runMapMainLoop | Main interactive loop of the battlefield screen. Initialises cursor state (clears last key, info-panel unit, path marker, target mode), loads the cursor sprites, then loops (L_2018): saves the viewport origin, runs the per-frame housekeeping (sub_40B2), shows the unit info panel when the joystick rests on an own unit for 15+ frames, selects a unit (sub_5E17) on a fire click (or, during the setup phase, sends command $91 to pick the unit up), and scrolls the viewport with joystick auto-repeat (zp_6C timer, sub_0D49, sub_20F4). In the setup phase (zp_B1 != 0) it calls the overlay-A hook sub_8658 each pass and returns when zp_66 == 0 or sub_C470 reports a stop; otherwise sub_06BA handles game-mode switches. | zp_A5/zp_A6, zp_B1 game phase, zp_5D joystick, zp_66, D_90EE idle counter, D_0B9B, D_92C9, D_92FC, D_9236, D_9245, D_92F3, branch opcodes at $206B/$2084 patched for the local side | D_92C3/D_92C4, D_90F8 selected unit, D_9248, D_91D7, D_24E1, zp_BB, zp_41, D_9209, D_91CC, zp_3D, D_9204/D_9205, D_92A1, zp_7B, zp_6C, D_0B7D, zp_73 | high |
| 20F4 | scrollViewportByDirection | Converts a direction code 0-7 (from joystickDirectionTable) into dx/dy using directionDeltaTable and diagonalDirectionTable (diagonals also stretch the repeat timer zp_6C by 1.5x) and scrolls the viewport by one cell via sub_1CD3. When zp_BB > 0 (range-limited target mode) the move is only allowed if the new cursor cell stays within the selected unit's range (sub_3870 distance vs. D_35B0/D_35BB tables indexed by the terrain under the unit). Afterwards sets the fire debounce timer zp_6D = 10 and disarms the fire button (zp_79 = 0). | A = direction code 0-7, zp_6C, zp_BB, zp_A5/zp_A6, D_90F8, D_FAF0 terrain under unit | viewport scrolled (zp_A5/zp_A6 changed by sub_1CD3), zp_6C, zp_6D = 10, zp_79 = 0, zp_22/zp_24/zp_27/zp_28 scratch | high |
| 2185 | runTargetCursorLoop | Lets the player move the cursor to choose a target cell after a command was chosen (called from 4BE1, 5F64, 650A, 83FF). Picks the cursor shapes according to zp_BB (>0 crosshair sprite 0 via sub_2FF3; 0 normal marker, or the group zoom view sub_30C0 when D_9209 >= 2; <0 marker) and loops: housekeeping, path preview toward the cursor after 90 idle frames, joystick scrolling, and on a fire click executes the command: zp_BB == 0 -> sub_347F (move, D_9209 < 2) or sub_4A35; zp_BB > 0 -> sub_3C20 (attack); zp_BB < 0 -> just stores the cursor cell in D_91D8/D_91D9 and returns immediately. Normal exits recenter the viewport on the unit (sub_258F), reset zp_BB/D_920A, clear sprites 0/1 and restore the cursor frame. | zp_BB target mode, D_9209, D_90F8, zp_B1, D_90FC, D_90EE, zp_5D, zp_6C | D_91D8/D_91D9 (zp_BB < 0), zp_BB = 0, D_920A = 0, D_91D7 = $FF, D_91CC = 0, sprites 0/1 cleared, D_9023/D_9024 bits 0-1 cleared | high |
| 2264 | previewPathToCursor | Idle-time path preview: if the cursor is not on the selected unit, resets the path marker to the unit and repeatedly animates the mini-map marker (sprite 1) from the unit toward the cursor cell (sub_24F3) until sub_2367 reports user input; then hides sprite 1. | D_90F8, zp_A5/zp_A6, D_F640/D_F6A4, D_F76C | zp_94/zp_95 target, sprite 1 moved/hidden | high |
| 22A2 | showUnitInfoPanel | Displays the info panel (rows 12-20, cols 25-38) for unit D_90F8: unit type name from the 8-byte string table at $C964 (GRUNT/RIDER/BOOMER/SPY/COMCEN), unit number, the labels GROUP/ENERGY/WEAPON/ACTION ($CAE9), the status values (sub_2384) and the unit picture sprites (sub_CD0A) if available. Then waits while the cursor stays on the unit, refreshing the status when the unit is in combat (== D_90FA or D_91F0) or D_920E bit 7 is set, animating the destination marker, until sub_2367 or a cursor move ends it. Clears the panel and sprites 5-7 on exit. | D_90F8, D_9209 (text colour via D_5E14), unit arrays, D_90FC, D_90FA, D_91F0, D_920E, D_9205, zp_BB, zp_B1 | D_91D7 (unit shown, $FF on exit), D_90EF = 0, zp_3D, D_920E cleared, D_9022 bits 5-7 cleared, text window reset | high |
| 2367 | checkUserInterrupt | Returns C = 0 while a passive display may continue: requires sub_C451 != 0 (no comm/key interruption), zp_66 negative (no pending game-mode change), joystick centred and no fresh fire click (sub_0EEA). Otherwise returns C = 1 and sets the joystick lockout timer D_0B7D = 30 frames. | zp_66, zp_5D, zp_5F/zp_79 | C flag, D_0B7D = $1E when interrupted | high |
| 2384 | printUnitStatusLines | Prints the value column of the info panel (col 31, rows 13..) for unit D_90F8: group name (ALPHA..GROVER from FB54 bits 2-4 when bit 6 set, else '-'), energy percent ((F9C4 & $3F)*2), weapon state (READY/RELOAD/FIRED, '-' for SPY/COMCEN, STUN'D when F960 & $0F; blinks via zp_6F), action (-/REPAIR/MOVE/PINNED/STUN'D/DIGGIN from F7D0/F834/FA28/FB54/F960), then BLITZ/CLOAKED/DUG IN flags (F8FC/F898/F960 bit 7) and '*' markers for F76C bits 3-4. | D_90F8, D_F76C, D_F7D0, D_F834, D_F898, D_F8FC, D_F960, D_F9C4, D_FA28, D_FB54, D_90FA, D_91CB, zp_6F | text printed, zp_3B/zp_3C/zp_3F/zp_40 text window changed, zp_6F = 20 when the fired-blink starts, zp_18/zp_19 scratch | high |
| 24E4 | animatePathMarkerToDestination | If unit D_90F8 has a destination (F7D0 bit 7 clear) sets it as the line target (zp_95/zp_94) and falls into animatePathMarkerStep; otherwise hides sprite 1. | D_90F8, D_F7D0, D_F834 | zp_94/zp_95, sprite 1 | high |
| 24F3 | animatePathMarkerStep | One animation step of the mini-map path marker (sprite 1): only when the player has been idle >= 90 frames and the zp_6C timer expired; advances the marker point (D_24E2/D_24E3) one Bresenham step (sub_FBB8/sub_FBFA) toward zp_95/zp_94 every 4 frames and positions sprite 1 there; at the end of the line decrements D_24E1 and blinks the sprite, after 5 passes resets the marker to the unit (sub_2540). | D_90EE, zp_6C, D_24E1/D_24E2/D_24E3, zp_94/zp_95 target | zp_92/zp_93, D_24E2/D_24E3, zp_6C = 4, sprite 1 position/enable (D_9011/D_9019/D_9022) | high |
| 2540 | resetPathMarker | Hides sprite 1, resets the path marker to the selected unit's position and reloads sprite 1's image: the grey 4x4 block shape (sub_2FFA with D_91CC = 1) in normal mode, or a copy of the sprite 0 cursor box in the zoomed mini-map mode (D_920A bit 7). | D_90F8, D_920A | D_91CC = 0, sprite 1 image/colour ($8A40, D_9009), D_24E1-D_24E3 | high |
| 256F | resetPathMarkerToUnit | Sets the path marker repeat counter D_24E1 = 5 and the marker point D_24E2/D_24E3 to the selected unit's map position, then falls into positionMarkerSprite. | D_90F8, D_F640, D_F6A4 | D_24E1 = 5, D_24E2/D_24E3, sprite 1 position | high |
| 2585 | positionMarkerSprite | Converts map cell X,Y to mini-map sprite coordinates (sub_3023: x*4+$1F, y*4+$39, or the zoomed mapping) and stores them as sprite 1 position shadows. | X = map column, Y = map row | D_9011 (sprite 1 X), D_9019 (sprite 1 Y) | high |
| 258F | recenterViewportOnUnit | If the cursor cell (A5+3, A6+2) is not the selected unit's position, scrolls the viewport step by step (sub_1D4E with A = 1) until the unit is under the cursor. | D_90F8, zp_A5/zp_A6 | zp_92/zp_93 (start point), viewport scrolled | high |
| 25B7 | consumeSpaceKey | If the last key code D_9248 is $A0 (space) clears it to $FF so it is not processed again after the info panel. | D_9248 | D_9248 | medium |
| 25C4 | callSetupOverlayHook | Trampoline to sub_8658 in overlay variant A (setup-phase key handler that toggles D_92F3 bit 7). Only valid while overlay A is resident; in overlay B $8658 is the middle of an instruction. | zp_B1 must be nonzero (setup phase) | C flag from sub_8658 | high |
| 25C7 | getViewportOriginPointer | Sets zp_48/zp_49 to the map address of the viewport's top-left cell (zp_A5, zp_A6), correctly handling negative origins (viewport partly outside the 40x40 map) by subtracting 40 per negative row and adding the negative column. | zp_A5, zp_A6, D_BFB0/D_BFD8 map row tables | zp_48/zp_49, A = map byte (only meaningful when in range), Y = 0 | high |
| 25E3 | getViewportRowPointer | Sets zp_48/zp_49 to the map address of cell (zp_A5, Y); if zp_A5 is negative uses column 0 and adds the signed offset afterwards. | Y = map row, zp_A5 | zp_48/zp_49, A = map byte, Y = 0 | high |
| 25FA | readMapCell | Generic map read: zp_48/zp_49 = rowTable[Y] + X and returns the map byte at column X, row Y of the 40x40 map at $F000. Called from ~25 places including the overlays and the comm module. | X = map column 0-39, Y = map row 0-39, D_BFB0/D_BFD8 row address tables (built at $0345/$034A) | A = map byte (bit 7 set = unit, low 7 bits = unit index; else terrain code, $40 = blank), zp_48/zp_49 pointer, Y = 0 | high |
| 260D | readCellUnderCursor | Reads the map cell at the cursor, which is always the centre of the 7x5 viewport: (zp_A5+3, zp_A6+2). | zp_A5, zp_A6 | A = map byte, X/Y = cursor column/0, zp_48/zp_49 | high |
| 261C | clearInfoPanel | Sets the info-panel text window (sub_2622) and clears it with the text engine's window clear (sub_C06A). | zp_3D text colour, zp_3E fill character | zp_39-zp_3C window, zp_3F/zp_40 cursor, panel area cleared | high |
| 2622 | setInfoPanelWindow | Resets the text window to full screen (sub_C0D7) then defines the info-panel window: left column 25 (zp_3B), width 14 (zp_3C), top row 12 (zp_39), bottom row 21 (zp_3A). | - | zp_39 = $0C, zp_3A = $15, zp_3B = $19, zp_3C = $0E | high |
| 2636 | clearScreenAndRedrawGameScreen | Clears the bitmap screen to grey (sub_2D41) and falls into redrawGameScreen. Called at start-up ($0A95) and from overlay A. | - | whole screen | high |
| 2639 | redrawGameScreen | Rebuilds the battlefield screen: patches the own-side branches for the local player (sub_2A08), sets VIC D016 shadow $D8 (multicolour), clears the full-screen text window, zeroes D_2A29-D_2A2C, sets background colour 7, draws the three panel frames (mini-map, viewport, info panel) and the bottom status bar (sub_28E1); unless zp_B1 bit 7 is set draws the strategic mini-map (sub_5AD7) and, unless zp_B1 bit 6 is set, the viewport (sub_26C1); finally redraws the status-bar message (L_C5D7). | zp_B1, D_90FB, map, zp_7B | D_9001 = $D8, D_9004 = 7, D_9226 = 0, zp_3D, D_2A29-D_2A2C = 0, screen | high |
| 268F | drawMiniMapFrame | Draws panel 0: the 22x22 character frame at column 0, row 0 around the strategic mini-map, in colour $0C. | - | zp_3D = $0C, X = 0 -> sub_2C7F | high |
| 2698 | drawViewportFrame | Draws panel 1: the 16x12 frame at column 24, row 0 around the tactical viewport, then falls into clearViewportTextArea. | - | zp_3D, frame drawn, viewport interior cleared | high |
| 26A1 | clearViewportTextArea | Defines the viewport interior as a text window (rows 1-10, column 25, width 14, colour 0) and clears it. | - | zp_39 = 1, zp_3A = $0B, zp_3B = $19, zp_3C = $0E, zp_3D = 0, area cleared | high |
| 26B8 | drawInfoPanelFrame | Draws panel 2: the 16x11 frame at column 24, row 11 around the unit info panel. | - | zp_3D = $0C, X = 2 -> sub_2C7F | high |
| 26C1 | drawViewport | Draws the 7x5-cell tactical viewport (each map cell = 2x2 characters = 16x16 pixels in the multicolour bitmap) into character rows 1-10, columns 25-38. Walks the map from the viewport origin (zp_A5, zp_A6) row by row, builds each cell's 32-byte tile in $04C8 with sub_2772 (terrain, units, highlight) and copies it to the bitmap address from sub_2A9F (second character row at +$140). Preserves zp_18/zp_19 and the viewport origin (D_90F6/D_90F7). | zp_A5/zp_A6 viewport origin, map at $F000, unit arrays | bitmap/colour RAM updated, zp_2F/zp_31 counters, zp_48/zp_49, zp_B4-zp_B7, D_2A2D/D_2A2E, D_90F6/D_90F7 | high |
| 2772 | renderMapTile | Builds one 16x16 pixel multicolour map tile (2x2 characters, 32 bytes) in the tile buffer at $04C8-$04E7 for the map cell byte in A at map coordinates (zp_A5, zp_A6). Off-map coordinates (>= 40 or negative) use border shapes $68-$6B; terrain $40 gives an empty tile; otherwise the 8x8 1-bit terrain shape at $9C6E+shape*8 is doubled in both directions through nybbleToMulticolourTable and the terrain colour (D_07BE/D_07EA) is written to the four screen-RAM cells at (zp_B6). If bit 7 of the cell byte is set the low 7 bits are a unit index: the 32-byte unit glyph (D_6C35 table, index = (D_F708&3) + D_0799[type] - $2C) is ORed into the buffer, inverted when the unit is the selected unit (D_9204) or belongs to the selected group, drawn on every other scanline when D_F898 bit 7 is set, and the unit colour (zp_77 own / zp_78 enemy, 1 = white when flashing) is written to colour RAM. Enemy units are skipped unless spotted (D_F9C4 bit 7), reveal-all (D_92FC bit 7) or the omniscient replay view (D_0B9B >= $82) is active. The caller (sub_26C1 and $5D45) copies the buffer to the bitmap at (zp_B4). | A = map cell byte; zp_A5/zp_A6 = map column/row; zp_B6/B7 = screen RAM cell pointer; zp_B1 bit 6 suppresses unit drawing; patched branch opcodes at $2840/$2846 select which unit-index half (0-49 or 50-99) is 'own' | $04C8-$04E7 tile buffer; 4 screen RAM bytes at (zp_B6)+0,1,40,41; 4 colour RAM bytes when a unit is drawn (zp_B7 is advanced by $4C to point into $D800); zp_32 = colour; clobbers A,X,Y, zp_B2/B3, self-modified operands at $2808,$27D4,$27F2,$28B8,$28C0 | high |
| 28E1 | drawMainWindowFrame | Sets the multicolour text colour zp_3D = $0C and draws framed window number 3, or number 4 when D_90FB is non-zero, through sub_2C7F (window table at D_2BDD/D_2BF8/D_2C13/D_2C2E/D_2C49). | D_90FB | zp_3D; window frame on screen; D_2A2D/D_2A2E/D_9221/D_9222 set by sub_2C7F | medium |
| 28F1 | computeGameResult | Game-over scoring. Clears the message queue (sub_C6EF), sets D_923F = $C0 and, unless D_0BA8 bit 7 (knockout) is set, queues message $16 ('GAME OVER') and compares the two 16-bit player point totals D_92A2/D_92A4 (player 0) and D_92A3/D_92A5 (player 1): equal -> both become 250 and message $13 (draw); otherwise the winner's total becomes 300 + min(/difference//2, 150), the loser gets 200, and message $11 ('won on points') or $12 is queued depending on whether the winner is the local player (D_0B9F). For a knockout, D_0BA8 bits 5-6 = $60 means a draw (250 each), otherwise bit 6 selects the winner who gets 500 and the loser 150 with message $14/$15. Finally the chosen message is queued with sub_C566 and D_92A1/D_0548 are set to $9E (no message showing) and D_929C cleared. Called from sub_4027. | Y (preserved); D_0BA8, D_0B9F, D_92A2-D_92A5 | D_92A2-D_92A5 rewritten with result points; D_92A1 = $9E, D_0548 = $9E, D_929C = 0, D_923F = $C0; messages queued; zp_18/19 scratch | medium |
| 29B3 | setOtherPlayerPoints | Helper of computeGameResult: stores Y as the 16-bit point total of the player other than X (X ^ 1), high byte 0. | X = player index, Y = value | D_92A2[X^1] = Y, D_92A4[X^1] = 0; X = X^1 | high |
| 29C1 | shakeScreenScroll | Screen shake: takes a pseudo-random byte from sub_FD5D and uses bits 0-2 as the vertical fine scroll and bits 3-5 as the horizontal fine scroll, each only if non-zero and smaller than the shake counter D_92C8, otherwise the normal values ($0B for $D011, $08 for $D016). The results are merged into the shadow registers D_9000 ($D011) and D_9001 ($D016) and remembered in D_92F5/D_92F4. Called once per frame at $41C7 while D_92C8 counts down. | D_92C8, D_9000, D_9001, PRNG state zp_57-59 | D_9000, D_9001, D_92F4, D_92F5; self-modified operands $29D6,$29E6,$29F3,$2A03 | high |
| 2A08 | patchUnitSideBranches | Copies the BCC/BCS opcode stored at $2840 (set from the local player index by sub_7BC6 in the overlay) to the visibility-test branch at $2846 in renderMapTile and to $5BFE in the other map renderer, inverting it (BCC <-> BCS, EOR $20) when D_0B9B bit 0 is set, i.e. when the replay viewpoint is the other player's side. Called at start-up ($0AD9), after the map redraw setup ($2639) and when the replay viewpoint key cycles D_0B9B ($4134). | D_0B9B bit 0, opcode byte at $2840 | opcode bytes at $2846 and $5BFE | high |
| 2A1A | printOptionWord | Prints the 4-character option word number A from the table at $0652 ('FULL','HALF','NONE', bit 7 terminated) through the text printer sub_C133. Used by the overlays to show the per-player option D_92A6. | A = word index (0-2) | text printed at the current text cursor; A/Y = string pointer passed to sub_C133 | high |
| 2A38 | setBitmapDrawMode | Selects how glyph bitmap bytes are combined with the existing bitmap: Y = 0 replace (LDA), 1 OR, 2 EOR, 3 keep existing (CMP). Patches the opcode at $2AF3 in plotGlyph and the identical instruction at $C22B in the $C000 overlay's text plotter. A is preserved. | Y = mode 0-3, A preserved | opcode at $2AF3 and $C22B; operand at $2A45 | high |
| 2A47 | setColourDrawMode | Selects how the colour byte D_2A2A is combined with existing screen RAM colour cells: Y = 0 replace, 1 OR, 2 EOR, 3 keep. Patches the opcode at $2B00 (plotGlyph) and $2A85 (fillScreenColourRect). A is preserved. | Y = mode 0-3, A preserved | opcode at $2B00 and $2A85; operand at $2A54 | high |
| 2A56 | resetDrawModes | Sets both the bitmap and the colour draw modes back to 0 (replace). Y is preserved. | Y preserved | opcodes at $2AF3, $C22B, $2B00, $2A85 = LDA | high |
| 2A64 | fillScreenColourRect | Fills an X columns by Y rows rectangle of screen RAM (the multicolour bitmap's colour cells, base $8C00) starting at window origin (D_2A2D row, D_2A2E column) with the colour byte A, combined according to the colour draw mode. Stores A in D_2A2A first. | A = colour byte, X = width, Y = height, D_2A2D/D_2A2E origin | screen RAM cells; D_2A2A = A; zp_B6/B7, zp_1B, zp_1C clobbered | high |
| 2A9F | calcCellPointers | Computes the bitmap address and screen RAM address of the character cell at row zp_18, column zp_19 (falls into calcCellPointersXY). | zp_18 = row 0-24, zp_19 = column 0-39 | zp_B4/B5 = bitmap cell address ($A000 + row*320 + col*8), zp_B6/B7 = screen cell address ($8C00 + row*40 + col); Y = row, X = col | high |
| 2AA3 | calcCellPointersXY | Same as calcCellPointers but takes Y = row and X = column in registers, using the row/column address tables at $9300 (bitmap row lo/hi D_9300/D_9319, column*8 lo/hi D_9332/D_935A, screen row lo/hi D_9382/D_939B). Also used by the $C000 overlay text printer and the overlays. | Y = row, X = column | zp_B4/B5 bitmap pointer, zp_B6/B7 screen RAM pointer | high |
| 2AC3 | drawCharAt | Draws character A at row zp_18, column zp_19: computes the cell pointers and falls into plotGlyph. | A = character code, zp_18/zp_19 | see plotGlyph | high |
| 2AC8 | plotGlyph | Draws the 8x8 glyph of character A (font at $9426, 8 bytes per character) into the bitmap cell at (zp_B4) using the bitmap draw mode, then writes the cell colour to screen RAM at (zp_B6): the per-character colour from D_93B4 when D_2A29 is zero and the table entry is non-zero, otherwise D_2A2A combined with the colour draw mode. In multicolour mode (D_9001 bit 4) it also writes zp_3D into colour RAM (zp_B7 + $4C). | A = character code (0-113); zp_B4/B5, zp_B6/B7 pointers; D_2A29, D_2A2A, zp_3D, D_9001 | 8 bitmap bytes, 1 screen RAM byte, optionally 1 colour RAM byte; zp_B2/B3 = glyph pointer; zp_B7 advanced to colour RAM in multicolour mode | high |
| 2B18 | drawCharBlock | Draws a rectangular block of characters at window origin (D_2A2D row, D_2A2E column). A/Y point to the data: byte 0 = width, byte 1 = height, then width*height character codes row by row. If D_2A2B is non-zero the data characters are ignored and every cell is filled with the character D_2A2C instead (the header is still read). | A/Y = pointer to block data; D_2A2D, D_2A2E, D_2A2B, D_2A2C | D_9221 = width, D_9222 = height; characters drawn; zp_B8/B9 = data pointer, zp_18/19, zp_1B (row), zp_1C (column), zp_1D (data index) clobbered | high |
| 2B75 | drawWindowInnerCorners | Draws character $38 at the four cells just inside the corners of the current window (rows D_2A2D+1 and D_2A2D+D_9222-1, columns D_2A2E+1 and D_2A2E+D_9221-1). Used by the overlay menus, probably to mark a selected/framed item. | D_2A2D, D_2A2E, D_9221, D_9222 | 4 characters drawn; zp_18/19/1A/1B clobbered | medium |
| 2BB7 | drawTallGlyph | Draws the two-character tall symbol number A at row zp_18, column zp_19: the pair of character codes at D_9810[A*2] (top) and D_9810[A*2+1] (bottom, drawn one row lower). zp_18 is restored. | A = symbol index, zp_18/zp_19 | two characters drawn | high |
| 2BCE | xorColourBlock2x2 | Sets the colour draw mode to EOR, XORs the colour byte A into the 2x2 screen RAM colour cells at the window origin (one map tile), then resets both draw modes to replace. Used by the overlays to flash/highlight a tile. | A = colour byte, D_2A2D/D_2A2E | 4 screen RAM cells toggled; draw modes reset; D_2A2A = A | medium |
| 2C7F | drawWindowFrame | Draws one of the 27 predefined screen windows/panels selected by X: copies left column, top row, width and height from windowLeftTable/windowTopTable/windowWidthTable/windowHeightTable into fillColumn/fillRow/windowWidth/windowHeight, fetches the window's 9-tile frame set pointer from windowFramePtrTable and falls into drawFrameBox. Used by every screen builder (battlefield frames 0-2, main menu $18/$19, overlay B setup/stat screens 5-$16). | X = window index 0-26 | fillColumn, fillRow, windowWidth, windowHeight set; frame drawn; windowWidth/windowHeight left decremented by drawFrameBox; A/X/Y clobbered | high |
| 2CA1 | drawFrameBox | Draws a rectangular frame out of a 9-tile set (layout: 0 top-left, 1 top edge, 2 top-right, 3 left edge, 4 unused centre, 5 right edge, 6 bottom-left, 7 bottom edge, 8 bottom-right) at fillRow/fillColumn with windowWidth x windowHeight cells, corners first then the four edges via drawFrameRow/drawFrameColumn. Also called directly by overlay B ($724A) with its own tile set. | A/Y = lo/hi pointer to the 9-byte tile set; fillRow, fillColumn, windowWidth, windowHeight; current colour state (colourOverride/fillColourValue/charColourTable) | frame drawn into the bitmap and screen RAM; windowWidth and windowHeight decremented by 1 (become right/bottom extents); zp_18/zp_19 = last cell, zp_1A = right column, zp_1B = bottom row, zp_B8/zp_B9 = tile set pointer | high |
| 2D1D | drawFrameColumn | Draws tile (zp_B8),Y repeatedly down column zp_19 starting at row zp_18 until the row reaches zp_1B (exclusive). Used for the left and right frame edges. | Y = tile offset in the set, zp_B8/zp_B9 = tile set, zp_18 = first row, zp_19 = column, zp_1B = stop row | zp_18 = zp_1B; zp_1C = tile code; cells drawn | high |
| 2D2F | drawFrameRow | Draws tile (zp_B8),Y repeatedly along row zp_18 starting at column zp_19 until the column reaches zp_1A (exclusive). Used for the top and bottom frame edges. | Y = tile offset, zp_B8/zp_B9 = tile set, zp_18 = row, zp_19 = first column, zp_1A = stop column | zp_19 = zp_1A; zp_1C = tile code; cells drawn | high |
| 2D41 | initBitmapScreen | Sets up the game's bitmap display from scratch: full-screen text window (sub_C0D7), zp_41 = 1, border and background shadows + registers = grey ($0C), colour RAM filled with $0C, screen RAM $8C00-$8FFF filled with $CC, bitmap $A000-$BF3F cleared, then $D016 = $C8 (hires, 40 columns), $D018 = $38 (screen $8C00, bitmap $A000 in VIC bank 2), $D011 = $3B (bitmap mode) with the shadows at $9000-$9002 updated, and finally initSpritePointers. Called from the startup code ($0318), the screen switcher ($06E8), mainStart ($0AAF) and drawBattlefieldScreen ($2636). | none | VIC registers and shadows $9000-$9004, colour RAM, screen RAM, bitmap cleared; zp_41 = 1, zp_7E = 0, zp_48/zp_49 clobbered, spriteEnableShadow = 0, sprite pointers $8FF8-$8FFF = $28-$2F | high |
| 2DBD | setTerrainTilePtr | Computes glyphPtr (zp_B2/zp_B3) = $9C6E + A*8 and clears colourOverride. A is a map byte minus $40 (the caller at $27B2 subtracts $40), so $9C6E holds the 8-byte terrain tile definitions that $27C2 expands into the 16x16-pixel battlefield cells. | A = terrain tile index (map byte - $40) | zp_B2/zp_B3 = tile pointer, colourOverride ($2A29) = 0, Y = 0 | medium |
| 2DDB | clearMessageLineAndScreen | Clears the message line via $C39F and falls into clearCurrentScreenWindows. Called from the screen-switch code at $070C. | currentScreen ($90FB) | see clearCurrentScreenWindows | medium |
| 2DDE | clearCurrentScreenWindows | Re-clears the content areas of whichever screen is active (currentScreen $90FB): 0 = battlefield: sprites off, text colour 0, message window cleared (sub_261C, skipped while commPauseFlags is nonzero), overview window cleared (sub_5E03), then view window cleared (sub_26A1); 1 = overlay screen (jumps to $7A6B in overlay B); 2 = enables only sprite 1 and jumps to $821F with A = 0; 3 = jumps to the menu redraw at L_158F. Also called from the $C000 overlay ($C75E) after saving the text window, e.g. when resuming after a pause. | currentScreen ($90FB), commPauseFlags (zp_BE) | spriteEnableShadow = 0 (or 2 for screen 2), zp_3D = 0 for the battlefield; tail-calls the per-screen routine | medium |
| 2E0F | magnifyWindow2x | In-place 2x zoom of the cell rectangle at fillRow/fillColumn with windowWidth x windowHeight cells, centred on the rectangle: every source cell (8x8 bitmap block plus its colour byte) is redrawn as a 2x2 cell block at (fillRow - h/2 + 2*row, fillColumn - w/2 + 2*col) using the $C000 overlay's double-size glyph renderer sub_C24B. Processes the four quadrants from the outer edges inward so no source cell is overwritten before it is read; odd widths/heights set magnifyWidthOdd/magnifyHeightOdd and pass edge flags in zp_BA so the middle row/column is drawn half size. Waits for raster line $80 first. Used by overlay B ($7552) for the repeated zoom-in animation of a 7x6 cell area at row 9 column 16. | fillRow, fillColumn, windowWidth, windowHeight | bitmap/screen rewritten; windowWidth/windowHeight decremented when even; zp_20 = ceil(w/2), zp_21 = ceil(h/2), zp_3F = dest left column, zp_40 = dest top row, zp_30/zp_31 = row/column counters, zp_3D = colour byte of the current cell, zp_BA = edge flags, zp_B2-zp_B9 clobbered | medium |
| 2F34 | setMagnifyCellPointers | Helper of magnifyWindow2x: computes the pointers of source cell (fillRow + zp_30, fillColumn + zp_31) and copies them to zp_B2/zp_B3 (bitmap, used as the glyph source) and zp_B8/zp_B9 (screen RAM, to read the colour byte), then computes the destination cell (zp_40 + 2*zp_30, zp_3F + 2*zp_31) into bitmapPtr/screenPtr via sub_2A9F. | zp_30 = source row offset, zp_31 = source column offset, fillRow, fillColumn, zp_3F/zp_40 = destination origin | zp_B2/zp_B3 = source bitmap cell, zp_B8/zp_B9 = source screen cell, zp_B4-zp_B7 = destination cell pointers, zp_18/zp_19 = destination row/column | high |
| 2F6A | initSpritePointers | Clears spriteEnableShadow and writes sprite pointers $28..$2F to $8FF8-$8FFF (the last 8 bytes of the $8C00 screen), so sprites 0-7 take their shapes from $8A00, $8A40, ... $8BC0. Called only from initBitmapScreen. | none | spriteEnableShadow = 0, $8FF8-$8FFF set, zp_48/zp_49 = $8FF8, A/X/Y clobbered | high |
| 2FA5 | clearAllSpriteData | Zeroes $8A00-$8BFF, the shape data of all eight sprites. Entered by JMP from the startup code at $0386 (labelled L_2FA5 in the listing but it is a routine ending in RTS). | none | $8A00-$8BFF = 0, X = 0, A = 0 | high |
| 2FB2 | clearSpriteShape | Zeroes the 64-byte shape of sprite A (address $8A00 + A*64 via spriteOffsetLoTable/spriteOffsetHiTable, patched into the STA at L_2FC8). Called before any sprite shape is built (cursor shapes, formation sprite, circles, overlay screens). | A = sprite number 0-7 | sprite shape cleared; A = 0, Y = $FF | high |
| 2FCF | setCursorShapeSquare | Loads the 18-byte (6 rows x 24 px) hollow-square shape at $9E6E into sprite 0 after clearing it, sets sprite 0 colour shadow ($9008) to 0 and enables sprite 0 via enableSprites. The cursor sprite is Y-expanded so the 6 rows cover a 12-pixel cell. Called when a unit is selected/being ordered ($2015, $4C53, $5E40, $5E87, $61F5, $6393). | none | sprite 0 shape and colour, spriteEnableShadow bit 0 set | medium |
| 2FF3 | setCursorShapeCross | Same as setCursorShapeSquare but with the thick cross/plus shape at $9E80 (used when zp_BB is positive, see $2195). | none | sprite 0 shape = cross, colour 0, sprite 0 enabled | medium |
| 2FFA | setCursorShapeBlock | Loads the small solid-block shape at $9E92 into sprite cursorSpriteIndex ($91CC) after clearing it, and sets that sprite's colour shadow to $0C unless it is sprite 0 (sprite 0 keeps its blinking colour). Used for the plain battlefield cursor ($21A8) and for sprite 1 when no group box is active ($2550). | cursorSpriteIndex ($91CC) = sprite number | sprite shape rewritten, spriteColorShadow[n] = $0C for n != 0 | medium |
| 3023 | mapCellToSpriteXY | Converts a map cell (X = column, Y = row) to sprite coordinates on the overview map. When groupBoxActive ($920A) bit 7 is set it first adds the group-box left/top offsets ($922F/$9231), converts, then adds 1 to both results; otherwise it falls straight into mapCellToSpriteXYRaw. | X = map column, Y = map row, groupBoxActive, $922F, $9231 | X = sprite X, Y = sprite Y | medium |
| 303A | mapCellToSpriteXYRaw | Sprite X = column*4 + $1F, sprite Y = row*4 + $39: the 40x40 battlefield is shown on the overview panel at 4x4 pixels per cell starting at screen pixel (7,7) (sprite origin offsets $18/$32). | X = map column 0-39, Y = map row 0-39 | X = sprite X coordinate, Y = sprite Y coordinate | high |
| 3049 | positionCursorSprite | Places sprite cursorSpriteIndex ($91CC) over the view centre cell (viewOriginCol+3, viewOriginRow+2) on the overview map: converts with mapCellToSpriteXY (group-box aware), stores into spriteXShadow/spriteYShadow and enables the sprite with the mask from spriteBitMasks. Does nothing when cursorSpriteIndex is negative. | zp_A5, zp_A6, cursorSpriteIndex, groupBoxActive and offsets | spriteXShadow[n], spriteYShadow[n], spriteEnableShadow bit n | high |
| 306C | drawLineInPathSprite | Draws a line into sprite 3 (the path/route sprite at $8AC0) using the high-memory line stepper: initLineStepper ($FBB8) from (lineCurRow,lineCurCol) to (lineEndRow,lineEndCol), then plots the current cell and steps until lineStepsLeft is 0. If A has bit 7 set, two extra steps are taken per plotted point, giving a dotted line. Called from $3B11 and overlay B $8266. | A = 0 solid / $80 dotted; zp_92-zp_95 line endpoints | pixels set in sprite 3; line stepper state consumed | medium |
| 308A | plotPathCross | Plots the four orthogonal neighbours of (lineCurRow, lineCurCol) into sprite 3: (row, col-1), (row-1, col), (row+1, col), then falls into plotPathSpritePixel for (row, col+1). Leaves lineCurCol incremented by 1. Marks the end point of a path ($3B14). | zp_92 = row, zp_93 = column | 4 pixels set in sprite 3; zp_93 = column + 1 | medium |
| 30A1 | plotPathSpritePixel | Sets the pixel (column zp_93, row zp_92) in sprite 3's shape at $8AC0: byte offset = row*3 + column/8, bit mask from the $C000 table at $C008. Also used directly by overlay B ($7852, $785E). | zp_92 = row 0-20, zp_93 = column 0-23 | one bit set in $8AC0-$8AFF; A/X/Y clobbered, operand at $30B4 patched | high |
| 30C0 | buildGroupFormationSprite | Builds the group 'formation ghost' for a group move: gets the bounding box of the destination positions (unitDestCol/unitDestRow, selected by Y=$80) of all live units in group selectedGroupId via sub_4B5E, stores its min/max columns/rows in $922F/$922E and $9231/$9230, plots every member as a 2x2 pixel dot at ((destCol-minCol)*2, (destRow-minRow)*2) into spriteBuildBuffer ($9025) with spritePlotMode = set, copies the buffer to sprite 0, sets X/Y expansion for sprites 0 and 1, converts the box extents into offsets relative to the selected unit's destination (so the box follows the cursor), sets groupBoxActive = $FF, calls keepGroupBoxInsideMap and positionCursorSprite. Called from $21A2 when the group has 2 or more units. | selectedGroupId ($9206), selectedUnit ($90F8), unit arrays $F76C/$FB54/$F7D0/$F834 | sprite 0 shape, spriteBuildBuffer, $922E-$9231, spriteXExpandShadow/spriteYExpandShadow bits 0-1, groupBoxActive = $FF, spritePlotMode = $40, view origin possibly nudged | medium |
| 31A3 | enableSprites | ORs mask A into spriteEnableShadow ($9022) unless linkState ($922A) bit 6 is set (sprites suppressed, e.g. while the link is being set up). Widely used by cursor/marker code and the $C000 overlay. | A = sprite bit mask | spriteEnableShadow updated (or unchanged); A = combined mask | high |
| 31AF | plotSpritePixel | Plots pixel (X = column 0-23, Y = row 0-20) in the shape of sprite A according to spritePlotMode ($91F3): bit 7 set = toggle (EOR), bit 6 set = set (ORA, shares code with setSpritePixel), otherwise clear (AND with the inverted mask). A negative sprite number selects the 64-byte spriteBuildBuffer at $9025 instead of $8A00 + n*64. Self-modifies the operation opcode at $3232 and its mask operand. | A = sprite number (bit 7 = use $9025 buffer), X = column, Y = row, spritePlotMode | one pixel modified; zp_98/zp_99 = sprite row pointer, spritePlotTemp clobbered | high |
| 31CA | setSpritePixel | Entry of plotSpritePixel that forces the ORA (set) operation; the mask is still inverted when spritePlotMode has neither bit 6 nor 7 set, so callers set spritePlotMode = $40 first (buildGroupFormationSprite does). | A = sprite number, X = column, Y = row | pixel set | high |
| 3239 | drawCircleInSprite | Draws a circle outline of radius X (1-10) centred at (12,10) into sprite A using 8-way octant symmetry: takes the dx and dy point lists for that radius from the tables at $9F37/$9F41 (dx pointers), $9F4B/$9F55 (dy pointers) and the step count from $9F5F, then for each step computes and plots the 8 symmetric points (spritePlotMode decides set/clear/toggle). Radius 1 gives a filled 3x3 dot. Used for range rings and blob markers ($3471, $3D13, $3D43, overlay B $7190-$71DA). | A = sprite number (stored to cursorSpriteIndex $91CC), X = radius index 1-10, spritePlotMode | circle plotted; $91CC = A, circleStepsLeft ($91F2) = -1, zp_AD-zp_B0 = table pointers | high |
| 3266 | computeCircleOctantPoints | For circle step Y reads a = (zp_AD),Y and b = (zp_AF),Y and fills circlePointX[0..7] = 12+a,12+b,12-b,12-a,12-a,12-b,12+b,12+a and circlePointY[0..7] = 10+b,10+a,10+a,10+b,10-b,10-a,10-a,10-b (tables at $32DA and $32E2). | Y = step index, zp_AD/zp_AE and zp_AF/zp_B0 = dx/dy table pointers | $32DA-$32E9 filled | high |
| 32BF | plotCircleOctantPoints | Plots the 8 points prepared by computeCircleOctantPoints into sprite cursorSpriteIndex via plotSpritePixel, iterating circlePointIndex ($3238) from 7 down to 0. | $32DA-$32E9, cursorSpriteIndex ($91CC), spritePlotMode | 8 pixels plotted; circlePointIndex = $FF | high |
| 32EA | hideSprite1 | Clears bit 1 of the sprite-enable shadow ($9022) and writes the result straight to VIC_SPR_ENA, turning sprite 1 off immediately (sprite 1 is the unit-selection sprite drawn by sub_3CF2). Called from the battlefield cursor / selection code ($229F, $22F4, $2359, $253C, $2540, $3D4E). | spriteEnableShadow $9022 | $9022 and $D015 with bit 1 cleared; A clobbered | high |
| 32F6 | hideSprite0Dead | Unreferenced copy of hideSprite1 that clears bit 0 (sprite 0) instead; rendered as .byte in the listing. | none | would clear $9022/$D015 bit 0 | high |
| 3302 | showCrosshairSprite | Loads the 63-byte diamond/crosshair shape ($9EE3-$9F21, offset X=$7D into the $9EA4 shape pair) into sprite 2 data ($8A80) and falls into the shared body at $3308 which makes sprite 2 black, double-size, positioned at X=256 (MSB set, $9012=0) Y=$4D - the centre of the 7x5 battlefield view - and enables it. Used when a target/attack cursor is active ($21AB). | none | sprite 2 data, $900A=0, $9023/$9024 bit 2 set, $9012=0, $9020 bit 2 set, $901A=$4D, sprite 2 enabled via sub_31A3 | medium |
| 3306 | showCursorBoxSprite | Same as showCrosshairSprite but copies the square box shape ($9EA4-$9EE2, offset X=$3E) - the normal battlefield centre cursor - into sprite 2 and shows it black and expanded at (256,$4D). Called when the battlefield is (re)entered ($1FEB, $2260) and from $4C56. | none | sprite 2 data and VIC shadow registers as for showCrosshairSprite | medium |
| 3340 | loadDirectionShapeSprite | Copies the 63-byte sprite shape number A (0-7, a facing/direction) from the decompressed shape buffer at $0200+64*A into sprite 0 data ($8A00) when activeSpriteIndex $91CC is 0, otherwise into sprite 5 data ($8B40), and then enables sprite $91CC. The buffer is filled by the RLE decompressor sub_67CB from the compressed sets at $FD70/$FE24. Callers pass the direction byte $920B ($5322, overlay B $802A/$810B/$829E). | A = shape index 0-7, $91CC = target sprite (0 or 5) | $8A00 or $8B40 sprite data, sprite enabled; zp_98/zp_99 = source pointer (clobbered), X/Y clobbered | medium |
| 3383 | showComcenCrossMarker | Clears sprite 2 (sub_2FB2 A=2), copies the 21-byte cross/plus shape at $9F22 into rows 7-13 of sprite 2 ($8A95), colours it light red ($900A=$0A), positions it at grid cell (10,8) converted by gridCellToSpriteCoords (the comcen cell of overlay B's 21x17 strategic grid) and enables sprite 2. Called by overlay B at $75B6 when screen 1 is built. | none | sprite 2 data/colour/position, zp_67=10, zp_68=8, sprite 2 enabled | medium |
| 33B1 | gridCellToSpriteCoords | Converts an 8x8-pixel grid cell (X = column, Y = row) of the overlay B strategic map into sprite coordinates: X = X*8+$27, Y = Y*8+$43. | X = grid column, Y = grid row | X = sprite X, Y = sprite Y | high |
| 33C8 | setMarkerSpriteDead | Unreferenced older routine (listed as .byte $33C2-$340A, with a 6-byte offset/colour table at $33C2): stores A in its own operand, and either patches the marker cell into updateMarkerSprite ($3425/$3437) and starts the 90-frame countdown $923E, or on the battlefield places sprite 4 on the overview map via sub_303A minus table offsets. Its JSR $3458 lands mid-instruction in the current build, proving it is stale. | A = index 0/1, X/Y = cell | dead code | medium |
| 340B | getUnitSpriteCentreOnGrid | Calls overlay B's sub_7A13 (map cell X/Y -> grid cell relative to the comcen, C=1 when outside the 21x17 grid); if inside, converts the cell with gridCellToSpriteCoords and adds 4 to both so the result is the cell centre. Returns C=0 on success, C=1 when off-grid. | X = map column, Y = map row (passed to sub_7A13) | C = 0 and X/Y = sprite coordinates of the cell centre, or C = 1 | medium |
| 341F | updateMarkerSprite | If the countdown $923E is nonzero and the marker cell (immediate operands at $3425/$3437, column 0 / row $FF in this build) lies within 14 columns and 12 rows of the view origin, positions sprite 4 at X=col*8+$78, Y=row*8+$5C and enables it; otherwise disables sprite 4. Nothing in this build sets $923E or patches the operands (only the dead code at $33C8), so in practice it only clears sprite 4. Called by overlay B's screen loop at $827E. | $923E, zp_A5/zp_A6 view origin, patched operands $3425/$3437 | $9014/$901C sprite 4 position, sprite 4 enable bit set or cleared | medium |
| 3456 | drawComcenRingSprite | Disables sprite 4, sets spritePlotMode $91F3=$40 (OR pixels), clears sprite 4 data (sub_2FB2 A=4) and draws two concentric circles of radius 2 and 1 into it with sub_3239 (centre (12,10)), then gives sprite 4 the flashing colour pair $12. Overlay B calls it at $80E1 right after centring the view on the comcen, so this is the comcen marker of screen 2. | none | sprite 4 data, $900C=$12, $91CC=4, $91F3=$40; self-modifies the loop counter at $346E | medium |
| 347F | orderSelectedUnitToCursor | Builds a move order for the selected unit ($90F8): cmdParam1/2 (zp_74/zp_75) = cursor cell (viewOriginCol+3, viewOriginRow+2), clears bit 6 of the unit's status flags ($FB54), sends command $80 unit,col,row through sub_560A, then calls updateUnitAtDestinationFlag. Returns C=0. Fire-button handler of the battlefield ($2212). | $90F8 selected unit, zp_A5/zp_A6 view origin | zp_73/74/75 command parameters, $FB54[unit] bits 6/7, 4 bytes queued in the outgoing command ring, C=0 | medium |
| 34A4 | updateUnitAtDestinationFlag | Sets bit 7 of unitStatusFlags ($FB54) for unit zp_73 when the unit's map position ($F640/$F6A4) equals the ordered destination in zp_74/zp_75, clears it otherwise. Also called from the command executors at $4A4D and $4C10. | zp_73 = unit, zp_74 = destination column, zp_75 = destination row | $FB54[unit] bit 7; self-modifies the ORA operand at $34C1 | medium |
| 34C6 | applyMoveOrderOld | Unreferenced older executor of the move order (listed as .byte $34C6-$355C): for unit zp_73 clears $F708 bit 6, calls sub_4C6B, snaps a comcen's destination to itself under some condition ($92B4/$92B5 == 4), stores the clamped destination (bit 7 set when equal to the position) in $F7D0/$F834, updates $FB54 bit 7, copies the position into the low bits of $F898/$F8FC unless it is an opponent unit in a linked game ($92FE bit 7), and clears $F960 bits 5-7 and $F76C bit 6. The live version is around $3F9E-$4016. | zp_73/zp_74/zp_75 | dead code | medium |
| 355D | clampMapCoordOld | Helper of applyMoveOrderOld: negative values become 0, values >= 40 become 39. | A = coordinate | A clamped to 0..39 | high |
| 3568 | markAllUnitsVisible | Sets bits 6 and 7 (spotted this cycle + visible) in unitEnergy ($F9C4) for all 100 units, i.e. reveals every unit. Called at the end of the reset routine at $4018 and by ageUnitVisibility when no game is in progress. | none | $F9C4[0..99] /= $C0; Y = $FF | high |
| 3576 | ageUnitVisibility | If noGameInProgress ($0BA8) is nonzero, jumps to markAllUnitsVisible. Otherwise sets the unit-scan side filter to 'all units' (compare operand $37FF=0, opcode $3800=BCC) and for every unit copies energy bit 6 into bit 7 and clears bit 6: units spotted during the last scan cycle stay visible, everything else fades. Called once per unit-processing cycle at $4660, before the spotting passes. | $0BA8, $F9C4[] | $F9C4[] visibility bits, $37FF/$3800 scan filter | high |
| 35C3 | scanVisibleEnemies | Builds the list of enemy units that unit Y can see (X bit 7 clear = spotting pass) or shoot at (X bit 7 set = attack-target pass) into scanUnitList $0400 / scanDistSqList $0432 / scanDirectionList $0464. Range = sightRangeByType[type (+5 for attack)] + terrainSightBonus[terrain class under the unit] (doubled for spotting), capped at 10; collectUnitsInRadius gathers opposite-side units in the square, then each candidate is rejected ($0400[n]=$FF) if its squared distance exceeds rangeDistanceSq[range] (+1 penalty when not in the preferred direction zp_6B), if it is destroyed, if (attack pass) it is not yet visible, if (spotting) it is a spy seen by a non-spy or farther than dist2 20, or a 'hidden' unit ($F898 bit 7) farther than dist2 12 (6 for non-spies). Grunts/Riders (and all spotting) then need a clear line of sight (checkLineOfSightStep over each cell, enemy units block, friendly units block only from flat ground, spies are transparent, terrain class 2 blocks, a class-2 target farther than dist2 6 is hidden unless already spotted); Boomers/spies/comcen skip the LOS test when attacking. First sighting of an enemy spy/comcen queues status message $0F/$0D. | X = mode (bit 7: 0 spotting, 1 attack targets), Y = scanning unit; unit tables, map, $077E terrain classes | $0400/$0432/$0464 list with zp_2F = last index ($FF none), $91F1 = range, $9227/$9228/$9229 = unit terrain cell/type/height, zp_18-zp_29 scratch; self-modifies $35F9, $362B | medium |
| 3746 | checkLineOfSightStep | Feeds one more terrain class X into the running line-of-sight test: compares terrainHeight[X] with the previous height zp_1E; a rise is remembered in zp_1F, a fall after a rise means a ridge hides the target and returns C=1. Equal heights or falls while no rise has happened yet (looking down from the start) return C=0. | X = terrain class of the next cell, zp_1E = previous height, zp_1F = $80 until the line went uphill | C = 1 blocked / 0 clear, zp_1E updated, zp_1F = rise amount | high |
| 375D | initLineBetweenUnits | Sets the line-walker target (zp_95/zp_94) to unit Y's position and falls into initLineFromUnit with X as the start unit. | X = start unit, Y = end unit | see initLineFromUnit | high |
| 3767 | initLineFromUnit | Sets the line-walker start (zp_93/zp_92) to unit X's position, calls initLineStepper ($FBB8) and returns the principal direction of the line as 0 = north, 1 = south (row-major lines, by zp_96) or 2 = east, 3 = west (column-major lines, by zp_97). The value becomes the unit's facing (bits 0-1 of $F708) at $4464. Also called from $428E. | X = unit, zp_94/zp_95 = target row/column | A = direction code 0-3, line walker state zp_8C-zp_97 | high |
| 378B | setScanFilterOppositeSide | Configures collectUnitsInRadius to keep only units of the side opposite to unit Y: compare operand $37FF = $32 and branch opcode $3800 = BCC ($90) when Y < 50 (side 0 scanner keeps units >= 50) or BCS ($B0) when Y >= 50. | Y = unit index | $37FF, $3800 patched; A clobbered | high |
| 379E | collectAllUnitsInRadiusDead | Unreferenced alternate entry to collectUnitsInRadius (listed as .byte): saves A, sets the side filter to 'all units' ($37FF = 0, $3800 = BCC), restores A and falls into $37AA. The two bytes before it ($379C-$379D, $90 $FA) are a leftover branch that overlay A's side-patch routine still writes to ($7BDB), harmlessly. | A = radius, X/Y = centre | dead code | medium |
| 37AA | collectUnitsInRadius | Scans the map square of half-width A around cell (X,Y), clipped to 0..39, for cells holding a unit (bit 7 set, low bits = unit index) that passes the side filter at $37FE/$3800, storing the unit index in $0400[n] and its squared distance (distanceSquared) in $0432[n]. When more than one unit was found and playerSide ($0B9F) is nonzero the list is reversed, so both players get mirror-symmetric tie breaking. Returns X = last index ($FF when empty). | A = radius, X = centre column, Y = centre row, $0B9F, map via sub_25FA | $0400/$0432 lists, X = zp_29 = count-1, zp_22-zp_29 scratch, zp_48/zp_49 map pointer; self-modifies $385A | high |
| 3870 | distanceSquared | Returns /zp_22-zp_27/^2 + /zp_24-zp_28/^2 using squaresTable (each delta capped at 15, sum saturating at $FF). | zp_22/zp_24 = cell column/row, zp_27/zp_28 = centre column/row | A = squared distance (max $FF), X clobbered | high |
| 38C2 | resolveAttack | Resolves an attack of unit X on unit Y. Only Grunts/Riders/Boomers (types 0-2) attack. Damage = baseDamageTable[attacker type][target type]; a moving attacker ($F8FC bit 7) whose first-strike flag ($F708 bit 7) is set sacrifices up to 15 energy (5 for Boomers) once: the amount is added to the damage and 3/4 of it is taken from the attacker. +2 if the attacker has $F76C bit 6, -25% if the target has it; + height difference of the terrain (terrainHeightTable); +10 if the target stands on terrain class 1, +2 on class 3 or 5; for non-Boomers a facing comparison adds +20 when the target faces the same way (hit from behind) and +10 for a flank hit. A zero/negative result becomes 2; half of the total is applied by applyHitToUnit. On the battlefield screen (and comcen not knocked out) the shot is drawn by overlay B's sub_82BF and drawShotLineSprite. Finally plays sound 9 if the target died, else 11 for a Boomer shot or 10. Called from the unit-processing loop at $44A0. | X = attacker unit, Y = target unit; unit tables, $077E terrain classes, $90FB, $922A | $38BB-$38C1 scratch, $923D attacker type, target energy/status updated, sprites 3 and zoom-window line, sound; self-modifies $3A4E, $393F, $3971 | medium |
| 3A5A | drawShotLineSprite | Shows the shot between (zp_93,zp_92) and (zp_95,zp_94) on the 4-pixel-per-cell overview map using sprite 3: disables and clears the sprite, converts both cells to sprite pixels (sub_303A), walks the Bresenham line until the remaining segment fits a 23x20 pixel box, places sprite 3 at the segment's top-left corner (+2) in white, draws the segment with sub_306C (dotted when the attacker is a Boomer, $923D = 2) plus the diamond end marker sub_308A, enables sprite 3 and sets shotLineTimer $91C8 = 8 frames. | zp_92-zp_95 = attacker row/col and target row/col (map cells), $923D | sprite 3 data ($8AC0), $9013/$901B, $900B=1, $9022 bit 3, $91C8=8; zp_48 used as step counter | high |
| 3B22 | applyHitToUnit | Applies A damage points to unit Y. If the unit belongs to the local player (branch at $3B2C patched per side): a full-energy spy being hit queues message $0E, a comcen hit sets the shake counter $92C8 = 8 and plays sound 8. For any comcen: the player's own comcen gets a random system damaged (sub_3D57), the damage is halved and capped at the remaining energy, scoreDirtyCounter $9208 is bumped and 2*damage is added to the 16-bit score of the attacking side ($92A2/$92A4 indexed by target side). Then reduceUnitEnergy and recordUnitHit run; when the energy reaches 0 the unit is marked destroyed ($F76C bit 7, hit effect $85, redraw) - except a comcen, which stays but ends the game: $0BA8 /= $80 / ($20 or $40 by side) and sub_4027 is entered with X = 1 if the opponent's comcen fell, 0 for the local one. | A = damage, Y = target unit | $9240 = applied damage, $F9C4/$FA28/$FA8C/$FB54/$F76C of the target, $9208, $92A2-$92A5, $92C8, $0BA8, sounds/messages, unit redraw via sub_48A2 | medium |
| 3BDF | reduceUnitEnergyBy | Stores A into hitDamage $9240 and falls into reduceUnitEnergy. Used by resolveAttack for the first-strike energy sacrifice and by $3EF8 (1 point). | A = amount, Y = unit | see reduceUnitEnergy | high |
| 3BE2 | reduceUnitEnergy | Subtracts hitDamage $9240 from the low 6 bits of unitEnergy[Y] with a floor of 0, preserving the visibility bits 6-7. | Y = unit, $9240 | $F9C4[Y]; self-modifies the ORA operand at $3BF7 | high |
| 3BFC | recordUnitHit | Caps the damage A at 20, stores $80/damage as the unit's hit effect ($FA8C), subtracts it from the signed suppression value $FA28 (clamped at -127 on overflow) and sets bit 1 of unitStatusFlags ($FB54) to flag the unit for a status refresh. | A = damage, Y = unit | $FA8C[Y], $FA28[Y], $FB54[Y] bit 1; self-modifies $3C0F | medium |
| 3C20 | orderAttackUnitAtCursor | Reads the map cell under the battlefield cursor (viewOriginCol+3, viewOriginRow+2). If it holds a unit whose side bit ($F708 bit 5) differs from the selected unit's, sends command $87 with cmdParam1 = selected unit and cmdParam2 = target unit (sub_560F); otherwise plays the error sound 3. Fire handler at $21F2. | zp_A5/zp_A6, $90F8 selected unit, map, $F708 | zp_74/zp_75, 3 command bytes queued, or sound 3 | medium |
| 3C50 | assignBoomerTargetOld | Unreferenced older handler (listed as .byte): X = zp_73, counts the Boomers before it with sub_3C5D and stores zp_75/$80 into boomerTargetTable $91DC[Y]. The live equivalent writes $91DC at $4454. | zp_73 = unit, zp_75 = target | dead code | medium |
| 3C50 | setBoomerTargetCmd | Receive handler of command $87 (SET TARGET for a boomer): X = cmdParam0 (boomer unit index), countBoomersBelow gives its ordinal in Y, then boomerTargetTable[Y] = cmdParam2 / $80 (target unit index with the lock bit). The disassembly shows these 13 bytes as data; they are reached by the JMP $3C50 at $5182 inside the command dispatcher (also mis-listed as data at $515D-$523D). The sending side is sub_3C20 (cursor on an enemy unit while a unit is selected). | zp_73 = boomer unit index, zp_75 = target unit index (bits 0-6) | D_91DC[boomer ordinal] = target / $80; Y = ordinal; X = unit index | high |
| 3C5D | countBoomersBelow | Counts the units with type 2 (boomers) whose index is lower than X, i.e. returns the ordinal of boomer X among all boomers. The limit X is patched into the CPX operand at $3C70. Used to index boomerTargetTable. | X = unit index (exclusive upper limit) | Y = number of boomers with index < X; X = input X; A = last type byte; C = 1 | high |
| 3C74 | getBoomerTarget | If unit X is a boomer (unitTypeTable & 7 == 2) returns its entry of boomerTargetTable (bit 7 set = target locked, bits 0-6 = target unit index) with C = 1; for any other unit type returns with C = 0 and A = type ^ 2 (nonzero, positive). Callers use C/N: the unit option menu shows CLEAR TARGET/SET TARGET, and the unit update loop clears the lock when a boomer can no longer fire. | X = unit index | C = 1 and A = boomerTargetTable[Y], Y = boomer ordinal (boomer); C = 0 otherwise | high |
| 3C85 | selectBoomerTarget | Chooses the firing target for boomer Y. Runs the spotting scan (sub_35C3 with X=$80 = weapon-range mode) which fills candidateUnitList/candidateDistList/candidateDirList with visible enemies, then walks the list: if the previously locked target (boomerTargetTable entry ^ $80) is still listed it is kept and the routine returns at once; otherwise the enemy comcen (unit 49 or 99) is preferred, else the nearest candidate, and the lock in boomerTargetTable is cleared. Called from the per-unit update loop at $4425 for units of type 2. | Y = boomer unit index; D_0400/D_0432/D_0464 lists produced by sub_35C3; zp_2F = list count-1 | D_91F0 = chosen target unit ($FF none), D_91F1 = its distance, zp_6B = direction to it (0-3); D_91DC[ordinal] cleared when the old lock is lost; zp_18/19/1A scratch | high |
| 3CF2 | animateRingBurst | Plays an expanding-then-contracting ring animation on hardware sprite 1 (colour shadow $9009 = $12, i.e. flashing white/red): clears the sprite shape, enables sprite bit 1, then for radius index 1..A-1 draws a circle frame (sub_3239 with spritePlotMode $40 = set pixels), waiting one tick each (sub_4094); at the largest radius plays sound $17, waits 5 ticks, then erases the frames in reverse order (spritePlotMode 0) and disables the sprite. Overlay B calls it with A=3 (unit placement / kick-off, $767B) and A=$0B (comcen knock-out explosion, $7A0E). | A = number of radius steps (patched into the CPX at $3D25); sprite 1 position already set in $9011/$9019 | sprite 1 shape and enable bit modified, D_3CF1 loop counter, D_91F3 plot mode (left 0), zp_7D = 0 (never read), sound $17 | medium |
| 3D57 | rollComcenSystemDamage | When the player's comcen is hit (called from the damage routine at $3B63 only for unit == comcenUnit) and the DAMAGE option (gameTypeOptions bit 4) is on, in a linked game (soloModeFlag bit 7 clear) with a game in progress, rolls for each of the three comcen systems 3..1 that is not yet damaged: if (random & $3F) < damageAmount the system status is set to $80 (damaged). | D_9240 = damage amount of the hit; D_0B9B, D_0BA3, D_0BA8 | D_9241[1..3] may become $80; D_3D56 loop index; A/Y clobbered | high |
| 3D8A | initAllUnits | Game start: runs initUnitRecord for every unit 99..0 (zp_18 = index), which resets each unit from its start coordinates and places it on the map, then clears the 16 groupStatusTable bytes. Called from overlay A at $7AEF. | unit start positions in unitCol/unitRow, unitTypeTable | all per-unit arrays reset, map cells marked with $80/unit, D_91F4-D_9203 = 0 | high |
| 3DA0 | initUnitRecord | Resets the order/destination state of unit zp_18 (X): waypoint = destination = current position (unitDestCol gets bit 7 = idle), clears unitTypeTable bits 3-4, unitStunTable = 0, keeps only bits 2-4 and 6 of unitFlagsTable (group and membership), sets the facing bits from initialFacingForUnit and bit 5 (side) for units >= 50, then falls into placeUnitOnMap. | zp_18 = X = unit index | F898/F8FC/F7D0/F834/F76C/F960/F708 of the unit; continues in placeUnitOnMap | high |
| 3DDA | placeUnitOnMap | Second half of the unit reset, also called on its own by overlay A ($7D83, all units) to re-place units: unitStunTable bit 7 = dug-in bit (unitTypeTable bit 6), pinnedTimer = 0, movePoints = 10, energy = 50 (100%), unitDisplayFlags bit 7 = idle bit, copies group/side bits into unitDisplayFlags (sub_4C6B), computes the map pointer of the unit's cell, saves the cell contents in unitUnderCell and writes $80/index into the map; a dead unit (type byte bit 7) instead becomes an empty slot ($FF in unitTypeTable and unitUnderCell). | zp_18 = X = unit index; unitCol/unitRow | F960/FA8C/FA28/F9C4/FB54/FAF0 of the unit, map cell at (col,row), zp_48/49 = map pointer | high |
| 3E23 | recycleDeadUnits | Per-tick recycler logic (called at $465A). For each side 1..0 that has a recycler (recyclerCol bit 7 clear) whose cell is not occupied by a unit: scans the side's 50 units from the comcen downwards for dead units (type byte bit 7 set but not $FF). If recyclerMode[side] is 0 the first dead unit is respawned immediately; otherwise two dead units of the same type are needed - the first is freed (slot $FF) and the second is respawned. The respawned unit gets the recycler coordinates, its type, flags 0 and goes through initUnitRecord. For the local side it stores the unit in recycledUnit and queues message 5 (UNIT RECYCLED). | D_0BA8 (no game -> return), D_92A8/D_92AA recycler coordinates, D_92A6 recycler mode, D_0C8A last unit index per side, unit arrays | unit arrays and map updated, D_923B, message 5; zp_18/19/1A/1B/1C/1D scratch | high |
| 3EBD | removeUnitFromMap | Takes unit X off the map if it is on it (unitUnderCell bit 7 clear): restores the saved terrain byte into the unit's map cell, marks unitUnderCell = $FF and clears pinnedTimer. Falls into removeUnitLeavingCell with A = saved cell. | X = unit index, unitUnderCell[X], unitCol/unitRow | map cell restored, FAF0[X] = $FF, FA8C[X] = 0, zp_48/49 | high |
| 3EC2 | removeUnitLeavingCell | Like removeUnitFromMap but the caller supplies the byte to leave in the unit's map cell in A (used at $4894 to leave a specific marker behind). Stores A at the unit's map position, then unitUnderCell = $FF and pinnedTimer = 0. | A = new map cell value, X = unit index | map cell, FAF0[X] = $FF, FA8C[X] = 0; Y = 0 | high |
| 3EE2 | applyBlitzStepCost | Called after a successful movement step of unit Y when the blitz bit (unitWaypointRow bit 7) is set: if the unit is moving (unitDestCol bit 7 clear) and still has energy, sets unitFlagsTable bit 7 (charge bonus for the next attack), takes 1 point of energy and adds 25 movement points (saturating at $7F). Plays sound $16 when the unit is the player's own comcen in a live game and no other sound ($14/$16) is busy. | Y = unit index; zp_B1, D_0B9B, D_9236, D_67CA | F708[Y] bit 7, F9C4[Y] energy-1 (via sub_3BDF, D_9240 = 1), FA28[Y] += 25 | medium |
| 3F27 | drawRecyclerOnMap | Writes the recycler glyph ($67) into the map at side Y's recycler position (recyclerCol/recyclerRow). If a unit occupies that cell the glyph goes into that unit's unitUnderCell entry instead (via the $FA70,Y = $FAF0 + (Y & $7F) trick) so it reappears when the unit leaves. Returns the cell coordinates and the byte to draw so the caller can JSR drawMapCell. Returns immediately if the side has no recycler. | Y = side index (0/1); D_92A8/D_92AA | X = column, Y = row, A = cell value to draw ($67 or the $80/unit byte); map or unitUnderCell updated; operands at $3F51/$3F53/$3F55 patched | high |
| 3F57 | checkEnemyRecyclerSpotted | Once per tick until it succeeds: collects the local player's units within radius 3 of the opponent's recycler (sub_378B/sub_37AA with the opponent's comcen index as the 'enemy' reference) and, if any of them is not cloaked (unitWaypointCol bit 7 clear), draws the enemy recycler on the map (drawRecyclerOnMap + drawMapCell), queues message 8 (SPOTTED RECYCLER!) and patches its own first byte to RTS so it never runs again. The byte is restored to NOP at game init ($0BBD) and set to RTS by the comm module when the opponent has no recycler ($EE3E). | D_0B9F playerSide, D_0C8A, D_92A8/D_92AA of the opponent | map cell, message 8, self-patch at $3F57; candidate lists at $0400/$0432 | high |
| 3F97 | initialFacingForUnit | Returns the initial facing code for unit X: 0 (up) for units of the local player's side, 1 (down) for the other side. The branch opcode at $3F9E is patched by overlay A ($7BD5) to BCC for side 0 and BCS for side 1 so that 'own' means index < 50 or >= 50 respectively. The leading LDA D_0B9F is dead (overwritten by LDA #0). | X = unit index; opcode at $3F9E | A = 0 or 1 | high |
| 3FA3 | mirrorMapCoordinate | Returns 39 - A: mirrors a map column/row across the 40-cell battlefield. Used by the comm module and overlays to convert coordinates between the two players' viewpoints (each player sees his own side at the bottom). | A = coordinate 0..39 | A = 39 - A | high |
| 3FAC | haltStunnedUnit | Called from the unit update loop while unit Y is stunned (stun counter nonzero). Unless gameTypeOptions bit 3 is set it only acts on the player's comcen; and when linkTypeAtStart bit 7 is set it ignores units of side 1 (index >= 50). It stops the unit: waypoint and destination = current position, unitDestCol bit 7 (idle) set, unitFlagsTable bit 6 (group membership) cleared, unitDisplayFlags bit 7 set and bit 6 cleared. | Y = unit index; D_0BA3, D_9236, D_92FE | F898/F7D0/F8FC/F834/F708/FB54 of the unit | medium |
| 3FE8 | haltAllUnitsAndReveal | Game-over helper: clears D_9242/D_9243/D_9244, then for all 100 units clears the fired/hit bits and sets the idle bit of unitDisplayFlags and sets the waypoint to the current position (keeping the cloak/blitz bits), marks every unit as spotted (sub_3568 sets energy bits 6-7) and, if the battlefield screen is active, redraws the overview (sub_5AD7) and the view (sub_26C1). | D_90FB currentScreen | FB54/F898/F8FC/F9C4 of all units, D_9242-D_9244 = 0, screen redraw | high |
| 4027 | endGame | Ends the battle after the caller has stored the end reason in D_0BA8 ($08 = clock ran out at $4608, $A0/$C0 = a comcen was knocked out at $3BD3): clears D_920D and D_92F9, halts and reveals all units, shows the result/score message (sub_28F1) and starts the 120-tick game-end countdown D_92C9. | D_0BA8 end reason, scores D_92A2-D_92A5 (via sub_28F1) | D_920D = 0, D_92F9 = 0, D_92C9 = $78, all units halted | high |
| 403B | markComcenRevealed | Misclassified as data (35 bytes, see misclassified). Called from the radar ($53BC) and drone ($53F1) command handlers after a launch: selects the side (own side = playerSide when X=0, opponent side = playerSide EOR 1 when X<>0), sets comcenRevealTimer[side] = 6 rounds, sets the visibility bits 6-7 of that side's comcen in unitEnergyTable (comcenIndexBySide gives $31/$63), and for the opponent's action also queues the comcen for redraw in pendingRedrawUnit. Using radar or drones therefore exposes the launching comcen for 6 rounds. | X = 0 own side / nonzero opponent side; playerSide $0B9F; comcenIndexBySide $0C8A | comcenRevealTimer[side] = 6; unitEnergyTable[comcen] /= $C0; pendingRedrawUnit = comcen index when X<>0; A/Y clobbered | high |
| 406F | waitFramesOrInput | Runs the game engine for up to A frames (one runGameFrames(1) per iteration) but returns early on a fresh fire-button press (sub_0EEA returns Z: button down after having been released, debounce expired) or the joystick leaves centre (joyDirection <> $0F), or when gameEndFlag becomes nonzero. Used by the overlay/menu code ($63C5, $6457, $64C5) for interruptible pauses while the battle keeps running. The frame count is kept in a self-modified operand at $4086. | A = number of frames; Y = value to return in Y (stored into the operand at $4092) | Y restored to the entry value; A/X clobbered; game state advanced by the ticks that ran | high |
| 4094 | runGameFrames | Advances the game by A frames: each iteration waits one frame (sub_C37F with A=1) and calls updateGameFrame. Stops early when gameEndFlag ($92C9) is nonzero. A=0 returns immediately. Called from many wait loops in the main program and both overlays so that the real-time battle continues while the UI is busy. Entry Y is preserved via the self-modified operand at $40B0. | A = frame count; Y preserved | Y = entry value; A/X clobbered | high |
| 40B2 | updateGameFrame | The per-frame game update, called from every main/UI loop. (1) sub_C86D leaves to the menu when gameEndFlag bit 7 is set. (2) Film playback: digit keys 1-9 set the step interval stepIntervalFrames = 9 - digit and queue message 7 'SPEED n'; otherwise during the comcen knock-out round ($922A bit 6) it only runs the chat handler and skips to the engine. (3) Status line update (sub_C4EF); on the battlefield screen the HOME key ($8C) is consumed and, in a film, cycles filmPlaybackFlag $80/$81/$82 = SHOW US/THEM/BOTH (names from $86FB copied into message 6 at $8726) and redraws the radar map and view. (4) Chat/pause keys (sub_C70A); expired timers switch off marker sprites 3 and 4 and redraw the view when viewRedrawTimer reached $80. (5) Pending single-unit (pendingRedrawUnit) and group (pendingRedrawGroup) redraws. (6) Engine: when stepCountdown ($90FC) is 0, reload it with stepIntervalFrames (0 in the setup phase), and every 10th unit (50th in setup) of the round apply the queued/received commands for time slice commandSlice = unitOrderIndex/10 via sub_4DB7, postponing the step while opponentWaitTimer is nonzero; then tick the hit-flash timers and process the next unit of the round order (processUnitStep at L_4213) or, after all 100 units, finish the round (finishRound at L_45E6). | $0B9B filmPlaybackFlag, $9248 lastKeyPressed, $90FB currentScreenId, $90FC stepCountdown, $9162 unitOrderIndex, $90FD unitProcessOrder, $92E3 stepIntervalFrames, zp_B1 gamePhase, timers $9234/$91C8/$923E, $923A/$923B redraw requests | advances one unit step or finishes a round; updates map/screen, sprites ($9022, $9010/$9018), message queue; may not return (menu exit via sub_C86D, knock-out round via L_0709) | high |
| 4213 | processUnitStep | Internal entry of updateGameFrame (reached by the unit-step branch, not by JSR) that processes one unit X of the round order: sets currentUnit $90FA; in battle shuffles two entries of unitBaseOrder with the lock-step RNG; skips dead units and everything while unitsFrozenFlags bit 7 is set; stunned units lose one stun count, get 0 movement points and are halted (sub_3FAC). Otherwise it refreshes the stationary flag (updateIdleFlag), turns the unit toward its destination (facing bits 0-1 of unitStateFlags via sub_3767), realises a unit whose path position is not its map position by walking back toward it to the first free cell (moveUnitToCell), then takes one step toward the destination with tryStepTowardTarget: blocked -> movement points = 15; a step onto a unit only updates the path position ($F898/$F8FC); a step onto free terrain moves the unit (moveUnitToCell); blitzing units pay energy for +25 points (sub_3EE2). It redraws the vacated cell, follows the tracked unit (followDirectedUnit), copies the stationary flag into unitOrderFlags bit 7, spots enemies in sight range (sub_35C3 X=0 -> visibility bits), chooses the nearest enemy in fire range (sub_35C3 X=$80, boomers via sub_3C85; comcens win ties; spies/comcens/cloaked units never fire), fires every other round ((roundsRemaining EOR unit) AND 1 = 0) through sub_38C2 and clears the reload bit on the same parity, then handles stationary units: blitz off, repair flag cleared, dig-in progress (+$20 per round in $F960 bits 5-7, fortified flag bit 6 of unitTypeTable when complete) or +1 energy per round with the repair flag set. Finally the comm-build-1 hook sub_E0E8 (probably the practice-mode AI) is called, the pinned bit is cleared and the unit is redrawn. | X = unit index, $9162 position in the round order; unit record arrays $F640-$FBB7; zp_B1 gamePhase; $0BA8 noGameInProgress | unit record updated (position, path position, movement points $FA28, flags, energy), map cells and screen updated, unitOrderIndex incremented at L_4537, comcenStunStatus refreshed from the own comcen's stun count | medium |
| 4559 | tryStepTowardTarget | Moves the line walker one cell from the current cell ($93,$92) toward the target ($95,$94) and decides whether currentUnit may enter that cell. Returns C=1 with $19 = 0 when the line has no steps (already there). Off-map cells (col/row >= 40) are refused (C=0, X=$FF). If the cell holds a unit, entering is allowed only for spies (type 3), for a cloaked mover when the occupant is cloaked too, for a same-side occupant (bit 5 of unitStateFlags = side) or when the occupant is a spy; otherwise C=0, X=$FF. Then the movement cost is looked up: terrain class = group class ($91F4 bits 3-5) for grouped units, else terrainClassTable[$077E][terrain under the unit] (0 for spies), cost = terrainMoveCostTable[class] (+8 for a diagonal step, zp_A9). With a negative movement-point balance or insufficient points it returns C=0 with X = unit index (wait). Otherwise C=1, A = remaining movement points. | $92/$93 current row/col, $94/$95 target row/col (from initLineStepper inputs), $90FA currentUnit, unit arrays $F76C/$F898/$F708/$FA28/$FAF0, $91F4 groupMoveTable | C=1: step accepted, A = movement points left, $19 = destination cell byte, $92/$93 advanced, zp_48 -> destination cell, X = unit; C=0: X=$FF blocked/off-map or X=unit not enough points; line state $8C-$97 modified | high |
| 45E6 | finishRound | Internal label reached by JMP from updateGameFrame once all 100 units were processed. In a running game it advances the game clock: roundsRemaining ($91CB) is decremented (in the setup phase only when unitActivityFlag was set); at 0 the game ends on time (noGameInProgress = 8, sub_4027 decides the winner on points); when it equals lastQuarterRound message $1D 'LAST QTR WARNING!' is queued and $92FA = $80 (setup phase: wrap to $FF). It then runs the per-side comcenRevealTimer (keeps an exposed comcen visible), and in battle calls the comm-build-1 hook sub_E079, checkRecyclerSpotted (sub_3F57), recycleDeadUnits (sub_3E23), rebuildGroupTables and the visibility shift sub_3576, gives every unit its movement points (stationary/dead units: 15; moving units: + speedClassRateTable[class] where class = type, the group's class, or the comcen classes $92B4/$92B5, saturating at 127) and clears the $0400 scratch. Finally it builds the next round's processing order unitProcessOrder: units are taken from unitBaseOrder, but a unit whose next step lands on a not-yet-ordered unit is pushed on a stack ($0464) so that the blocking unit is ordered first ($0400 marks: 0 = unordered, 1 = on the stack, 5 = ordered), then stepCountdown and unitOrderIndex are reset to 0. | $91CB, $92EF, $92C2, zp_B1, $0BA8, $92B2/$92B3, unit arrays, $9164 unitBaseOrder | $90FD unitProcessOrder rebuilt, $9162 = 0, $90FC = 0, $FA28 movement points, $91F4, visibility bits, messages; zp_1A/zp_1B used as stack/list indices | high |
| 4751 | moveUnitToCell | Moves currentUnit to the cell ($93,$92) whose byte is in $19: writes unit/$80 into the destination cell (zp_48 must still point at it), restores the saved terrain (unitSavedTerrain) into the old cell, updates unitCol/unitRow and saves the new terrain. Then scores territory: the column is mirrored for side-1 units (sub_3FA3) so that col < 10 is the far end zone (value 2) and col < 20 the near zone (value 1); a unit scoring a zone for the first time (bits 3/4 of unitTypeTable from zoneScoredBitTable) adds scoreValueBySide[side] points (x16 for a comcen, x2 for the far zone) to the 16-bit opponent score pointsAgainstSide[side EOR 1] and bumps scoreChangeCounter. Sets unitMovedFlag and unitActivityFlag. | $90FA currentUnit, $93/$92 destination col/row, $19 destination cell byte, zp_48 -> destination cell, $9228 currentUnitType, $92AC scoreValueBySide | map cells, $F640/$F6A4/$FAF0/$F76C of the unit, $92A2/$92A4 scores, $9208, $406E = 1, $92C2 = 1; zp_48 low byte clobbered | high |
| 47ED | shuffleUnitOrder | Swaps two randomly chosen entries of unitBaseOrder ($9164, 100 unit indices) using randomUnitIndex, which draws from the lock-step scenario RNG so both machines shuffle identically. Called once per processed unit during battle and by overlay A at game start ($7BBC). The leading NOP is probably a disabled instruction byte. | $9164 unitBaseOrder, RNG state $5A-$5C | two entries of $9164 exchanged; A/X/Y clobbered | high |
| 4805 | randomUnitIndex | Returns a uniformly distributed unit index 0-99: takes nextScenarioRandom ($FD4A) AND $7F and retries while the value is >= 100. | RNG state $5A-$5C | A = 0..99; RNG advanced | high |
| 480F | followDirectedUnit | While the battlefield screen is shown and trackedUnit ($91D7) equals currentUnit: computes the cursor cell (viewOriginCol+3, viewOriginRow+2) into $91DA/$91DB and, if the unit is not on it, places sprite 0 (radar cursor, $9010/$9018 via sub_3023), enables it (sub_31A3) and scrolls the view instantly to the unit (sub_1D4E with A=0). Keeps the view locked on the unit whose menu/targeting is open. | $90FB currentScreenId, $91D7 trackedUnit, $90FA currentUnit, zp_A5/zp_A6 view origin, $F640/$F6A4 | $91DA/$91DB = cursor cell, sprite 0 position/enable, view scrolled ($92/$93, zp_A5/zp_A6) | medium |
| 4860 | updateHitFlashTimers | For every unit with a nonzero unitHitFlashTimer ($FA8C) decrements the low nibble (high nibble kept). When it reaches 0 the byte is cleared and the cell is redrawn: a living unit is drawn again (unit/$80), a dead one has its saved terrain written back into the map (sub_3EC2) and drawn. Called once per engine step. | $FA8C unitHitFlashTimer, $F76C unitTypeTable, $FAF0 unitSavedTerrain | $FA8C updated, map/screen cells redrawn; zp_18 used as loop index | high |
| 48A2 | drawUnitAtPosition | Draws unit Y on the map display: A = Y/$80 (the map byte of a unit) and falls into drawValueAtUnit. | Y = unit index | see drawValueAtUnit | high |
| 48A5 | drawValueAtUnit | Draws cell value A at unit Y's position: loads X = unitCol[Y], Y = unitRow[Y] and jumps to drawMapCell (sub_5C7C), which updates the battlefield view and the radar map. | A = cell byte to draw, Y = unit index | screen updated; A/X/Y clobbered | high |
| 48B1 | updateIdleFlag | Sets bit 7 of unitDestCol ($F7D0) for currentUnit when its destination ($F7D0/$F834 low 6 bits) equals its map position, clears it otherwise; the other bits are kept. Bit 7 = unit is stationary (no pending move). Returns the new byte in A and Y = currentUnit. | $90FA currentUnit, $F640/$F6A4/$F7D0/$F834 | A = new $F7D0 byte (bit 7 = stationary), Y = currentUnit, $F7D0 updated | high |
| 48DD | rebuildGroupTables | Recomputes the 16-entry groupMoveTable ($91F4) at the end of each round. Saves the old entries to $0430, clears per-group scratch (member count $0400, worst terrain cost $0410, best rate $0420 = $FF). For every living grouped unit (unitStateFlags bit 6; group = bits 2-5) it computes the speed class (type, or comcenSpeedClassOwn/Opponent for the comcens) and terrain class (0 for class 3 = spies, else terrainClassTable of the terrain under the unit): the group keeps the highest terrain cost (class in bits 3-5) and the lowest rate (class in bits 0-2), i.e. a group moves like its slowest member on the worst ground. Groups with members get bits 6-7 of their previous entry back. | unit arrays $F76C/$F708/$FAF0, $92B4/$92B5 comcen speed classes, $9236 ownComcenUnit, tables $405E/$4066/$077E | $91F4-$9203 rewritten; $0400-$043F scratch; zp_18/zp_19/zp_1A clobbered | high |
| 4987 | averageGroupMoveVector | Untraced code (shown as .byte). Sums, over every live unit whose F708 group bits (and #$3C) equal zp_75 and whose bit 6 (in group) is set, the signed vector destination - position (unitDestX & $7F - unitX, unitDestY & $7F - unitY) as two 16-bit totals and divides each by the member count with divideSigned16by8 ($FCEC). Helper of followGroupMovement. | zp_75 = group id bits (value of F708 & $3C); unit arrays F640/F6A4/F708/F76C/F7D0/F834 | A = member count (Z=1 when the group is empty), X = average dx, Y = average dy; zp_18-zp_1C clobbered; self-modifies the sign-extension operands at $49C6/$49E3 | high |
| 4987 | averageGroupMoveVector | Averages the outstanding movement of one group. Over all 100 units it sums (unitDestCol & $7F) - unitCol and (unitDestRow & $7F) - unitRow as two sign-extended 16-bit totals, counting only live units (unitTypeTable bit 7 clear) whose authoritative unitFlagsTable ($F708) has the member bit 6 set and whose group key (bits 2-5) equals cmdParam2, then divides both totals by the member count with divideSigned16by8 ($FCEC). Sole helper of followGroupMovement, so it runs when a unit joins a group and has to inherit the group's current heading. | zp_75 = group key (F708 & $3C: group number in bits 2-4, side in bit 5); unit arrays $F640 col, $F6A4 row, $F708 flags, $F76C type, $F7D0/$F834 destination | A = member count (Z=1 when the group is empty), X = average column delta, Y = average row delta; zp_18-zp_1C destroyed (zp_18/zp_19 also by the divide); self-modifies the sign-extension ADC operands at $49C6 and $49E3 | high |
| 4A02 | followGroupMovement | Untraced code. Gives unit Y the group's average movement: if averageGroupMoveVector returns a non-zero vector the unit's destination becomes its position plus that vector, otherwise its destination becomes its own position (stand still); the destination is applied through the set-destination routine at $34D5 (itself mis-typed as data). Called by joinUnitToGroup so a unit that joins a moving group starts moving with it. | Y = unit index, zp_75 = group id bits | zp_74/zp_75 = new destination, unitDestX/Y of the unit updated via $34D5; zp_18/zp_19/zp_1D clobbered | high |
| 4A02 | followGroupMovement | Makes unit Y move with the group it is joining: averageGroupMoveVector supplies the group's average movement vector and the unit's new destination becomes its own position plus that vector, applied through the shared destination setter sub_34D5 (which clamps to 0-39 and sets the at-destination bit). If the group has no members the unit is left untouched. Called only from joinUnitToGroup (command $88). | Y = unit index, zp_75 = group key, $F640/$F6A4 unit position | zp_74/zp_75 = new destination column/row, unitDestCol/unitDestRow of the unit written by sub_34D5, unitDisplayFlags bit 7 refreshed there; zp_18/zp_19 = average dx/dy, zp_1D = saved unit index | high |
| 4A35 | sendGroupMoveToCursor | UI action (called from $2218 when a group is selected, D_9209 >= 2): queues command $81 (move group) with the selected unit and the cursor cell (viewLeftCol+3, viewTopRow+2), then calls $34A4 to set the local at-destination flag of the unit. Always returns C=0. | D_90F8 selected unit, zp_A5/zp_A6 view origin | zp_73/zp_74/zp_75 = unit, x, y; 4 bytes queued in the outgoing command ring ($560A); C=0 | high |
| 4A35 | sendGroupMoveToCursor | Fire-button action of the battlefield target cursor when a whole group is selected (runTargetCursorLoop $2218, taken when uiLevel D_9209 >= 2): queues command $81 with the selected unit as the group leader and the cursor cell (viewOriginCol+3, viewOriginRow+2) as the ordered position, then refreshes the leader's local at-destination flag. Unlike the single-unit order $347F ($80) it deliberately leaves the leader's member bit in $FB54 alone, so the unit stays in its group. | D_90F8 selectedUnit, zp_A5/zp_A6 view origin | zp_73/zp_74/zp_75 = unit, column, row; 4 bytes queued in the outgoing ring by queueCmd4 ($560A); $FB54[unit] bit 7 updated by updateUnitAtDestinationFlag; C=0 always | high |
| 4A52 | moveGroupKeepingFormation | Untraced code: tail of the $81 command handler ($501A -> here). Computes the offset between the leader's current destination and the ordered cell (x,y) and moves every live member of the leader's group (same F708 group bits, bit 6 set) by that offset: member destination = member destination - offset, applied through $34D5. The formation of the group is therefore preserved. | zp_73 = leader unit, zp_74/zp_75 = ordered x/y (already mirrored for remote commands) | unitDestX/Y of all members updated; zp_73 overwritten with the group key, zp_4A used as loop index; self-modifies the operands at $4A8E/$4A98 | high |
| 4A52 | moveGroupKeepingFormation | Executor of command $81, run in lock step on both machines (tail of cmdMoveGroupRelative $501A). It computes the offset between the leader's current destination and the ordered cell and subtracts that offset from the destination of every live member of the leader's group, so the leader ends up exactly on the ordered cell and the relative formation is preserved. Membership comes from the authoritative unitFlagsTable: (F708 & $7C) == (leader's F708 & $3C) / $40. | zp_73 = leader unit index, zp_74/zp_75 = ordered column/row (already mirrored for remote commands by mirrorCmdCoordsIfRemote $4FE4) | unitDestCol/unitDestRow of every member set through sub_34D5 (clamped to the map, at-destination bit maintained); zp_73 overwritten with the group key, zp_4A = 99..0 loop counter; self-modifies the SBC operands at $4A8E (dx) and $4A98 (dy) | high |
| 4AA3 | addSelectedUnitToGroup | UI action (GROUP menu item ADDED, called from $61C6 and $6483): checks with isPointWithinGroupRange that the selected unit is within 8 cells of all current members of group D_9206; if not, shows 'TOO FAR!' and returns C=1. Otherwise sets the local group bits in unitLocalFlags (FB54 = (old & $83) / group / $40) and queues command $88 unit group. | D_9206 selected group bits, D_90F8 selected unit | C=0 added (command $88 queued, FB54 updated), C=1 too far (message shown) | high |
| 4AA3 | addSelectedUnitToGroup | 'ADDED' half of the membership editor, called from the JOIN GROUP menu ($61C6) and from the group membership editor ($6483). isPointWithinGroupRange rejects a unit that is more than 8 cells from the group's current bounding box: showTooFarMessage is displayed and C=1 is returned. Otherwise the local flag copy is updated at once for instant feedback (unitDisplayFlags = (old & $83) / groupKey / $40) and command $88 is queued so both machines perform the join. | D_9206 selectedGroupId (group number bits 2-4 plus side bit 5), D_90F8 selectedUnit | C=0 accepted: $FB54[unit] updated, zp_74/zp_75 = unit/group and 3 bytes ($88, unit, group) queued by queueCmd3 ($560F); C=1 rejected with the TOO FAR! message and sound 3 | high |
| 4AD4 | joinUnitToGroup | Untraced code: tail of the $88 command handler ($5185 -> here). Makes unit zp_74 follow the movement of group zp_75 (followGroupMovement), then writes the group id and the in-group bit into unitGroupByte (F708 = (old & 3) / group / $40) and mirrors them into unitLocalFlags with syncLocalGroupFlags. | zp_74 = unit, zp_75 = group id bits | F708/FB54 of the unit updated, destination updated; self-modifies the immediates at $4AE2/$4AE9 | high |
| 4AD4 | joinUnitToGroup | Executor of command $88 (tail of $5185). It first gives the unit the group's current movement through followGroupMovement, then writes the group key and the member bit into the authoritative unitFlagsTable ($F708 = (old & $03) / groupKey / $40) and mirrors the new bits into unitDisplayFlags with syncLocalGroupFlags. Because only the two facing bits survive the mask, joining a group also clears the unit's pending first-strike bit 7 and takes the side bit from the command parameter. | zp_74 = unit index, zp_75 = group key (both from readCmdArgs2) | $F708 and $FB54 of the unit updated, destination set to the group's average movement; self-modifies the LDX operand at $4AE2 (unit index) and the ORA operand at $4AE9 (group key) | high |
| 4AF2 | removeSelectedUnitFromGroup | UI action (GROUP menu item REMOVED, called from $64B1): clears the in-group bit (bit 6) of the selected unit's unitLocalFlags immediately and queues command $89 unit so both machines remove it from unitGroupByte when the command executes. | D_90F8 selected unit | FB54 bit 6 cleared, zp_73 = unit, 2 bytes queued via $561F | high |
| 4AF2 | removeSelectedUnitFromGroup | 'REMOVED' half of the membership editor (called from $64B1). Clears the member bit 6 in the selected unit's local flag copy unitDisplayFlags straight away so the viewport highlight updates immediately, and queues the 2-byte command $89, whose executor cmdLeaveGroup ($519B) clears bit 6 in the authoritative unitFlagsTable on both machines and re-syncs $FB54. | D_90F8 selectedUnit | $FB54[unit] &= $BF, zp_73 = unit, 2 bytes queued by queueCmd2 ($561F) | high |
| 4B04 | isPointWithinGroupRange | Tests whether a point may join / be the target of group A: computes the group's bounding box with computeGroupBounds, then requires the point to be less than 9 cells from each of the four box edges (so the group never spans more than 8 cells). Y < $80 selects unit Y's position and the members' positions; Y = $80 selects the order target (D_91D8/D_91D9) and the members' destinations. An empty group always fits. | A = group id bits, Y = unit index or $80 | C=0 fits, C=1 too far; zp_18-zp_1D clobbered; self-modifies the ldy operand at $4B0F | high |
| 4B04 | isPointWithinGroupRange | Range test that keeps a group from spreading over more than 8 cells. It builds the group's bounding box with computeGroupBounds and then requires the tested point to be less than 9 cells away from each of the four box edges (which also bounds the box after the point is added). Y < $80 tests unit Y's map position against the members' positions (used when a unit joins); Y = $80 tests the picked order cell D_91D8/D_91D9 against the members' destinations (used when a group member is re-positioned). An empty group always passes. | A = group key, Y = unit index or $80; D_91D8/D_91D9 when Y = $80 | C=0 within range, C=1 too far; zp_18-zp_1D clobbered (box and mode); self-modifies the LDY operand at $4B0F | high |
| 4B5E | computeGroupBounds | Scans all 100 units and, for live units whose unitLocalFlags group bits (FB54 & $7C) equal A / $40, accumulates the member count and the bounding box of either their positions (zp_1D bit 7 clear) or their destinations (bit 7 set, F7D0/F834 & $7F). Also called by $30C5 (group box display) with Y = $80. | A = group id bits, Y = mode flag (bit 7 = use destinations) | A = zp_1A = member count, X = zp_18 = min x, Y = zp_19 = min y, zp_1B = max x, zp_1C = max y; self-modifies the cmp operand at $4B80 | high |
| 4B5E | computeGroupBounds | Scans all 100 units and accumulates the member count and the bounding box of one group, using the local flag copy unitDisplayFlags for the membership test ((FB54 & $7C) == key / $40) and skipping dead units. zp_1D bit 7 (the Y register on entry) selects whether the box is built from the members' current positions ($F640/$F6A4) or from their destinations ($F7D0/$F834 & $7F). Used by isPointWithinGroupRange and by buildGroupFormationSprite ($30C5), which draws the group's formation ghost from the destination box. | A = group key, Y = mode (bit 7 set = use destinations); $F76C, $FB54, $F640/$F6A4 or $F7D0/$F834 | A = zp_1A = member count, X = zp_18 = min column, Y = zp_19 = min row, zp_1B = max column, zp_1C = max row (min stays $FF and max 0 for an empty group); self-modifies the CMP operand at $4B80 | high |
| 4BBF | showTooFarMessage | Selects and clears the 14-column message window at column 25 ($261C), plays sound 3 and prints 'TOO FAR!' ($CB31 in the $C000 overlay) on window row $10. | - | text window changed, message printed | high |
| 4BBF | showTooFarMessage | Error feedback shared by the two group range checks: selects and clears the 14-column info panel window (columns 25-38, rows 12-21), plays sound 3 (the error beep) and prints 'TOO FAR!' ($CB31 in the $C000 overlay) on window row $10 in whatever colour the caller left in zp_3D. | zp_3D text colour, zp_3E fill character | info-panel window selected and cleared, sound 3 queued, message printed (tail call to printString) | high |
| 4BD3 | orderGroupNewPosition | UI action (GROUP menu item NEW POSITION, called from $64AB): highlights the selected unit (D_9204 = unit, D_9205 = $FF), redraws the view ($26C1, $2185) and, if input is allowed ($C451), checks that the target cell D_91D8/D_91D9 lies within 8 cells of the group's destinations. If so it queues command $98 unit x y, sets the local at-destination flag ($34A4), counts consecutive orders to the same group in D_929D/D_929E and prints 'RE-POSITIONED' ($CB99) centred in colour 4; otherwise shows 'TOO FAR!'. Ends by setting zp_BB = $80 and refreshing the sprites ($2FCF, $3306). | D_90F8 selected unit, D_9206 group bits, D_91D8/D_91D9 target cell | command $98 queued or message shown; D_9204/D_9205, D_929D/D_929E, zp_BB, zp_3D/zp_42 updated; self-modifies the string pointer operands at $4C46/$4C48 | medium |
| 4BD3 | orderGroupNewPosition | NEW POSITION action of the custom-formation editor (called from $64AB). It highlights just the selected unit (highlightedUnit = unit, highlightedGroupId = $FF), redraws the viewport and runs the target cursor loop, which in this mode (cursorTargetMode < 0) only reports the picked cell in D_91D8/D_91D9. If the pick was not aborted and lies within 8 cells of the group's destination box it queues command $98 unit,col,row, refreshes the local at-destination flag, counts the order in the pending-order pair D_929D/D_929E and prints 'RE-POSITIONED' centred in colour 4; otherwise it shows TOO FAR! and re-centres the viewport. It ends by restoring the editor's cursor state: cursorTargetMode = $80 (the loop zeroes it on an abort) plus the square cursor sprite 0 and the centre box sprite 2. | D_90F8 selectedUnit, D_9206 selectedGroupId, D_91D8/D_91D9 picked cell, checkUiContinue ($C451) | command $98 queued (queueCmd4) or a message shown; D_9204/D_9205, D_929D/D_929E, zp_BB = $80, zp_3D/zp_42 = 4; self-modifies the string pointer operands at $4C46/$4C48 | high |
| 4C59 | setGroupMemberDestination | Untraced code: tail of the $98 command handler ($5470 -> here). If the unit's group bits equal the group remembered in D_929D it decrements the pending-order counter D_929E, then sets the unit's destination to (zp_74, zp_75) through $34D5. Unlike command $80 the unit stays in its group. | zp_73 = unit, zp_74/zp_75 = x/y (mirrored for remote commands) | unitDestX/Y updated, D_929E possibly decremented | high |
| 4C59 | setGroupMemberDestination | Executor of command $98 (tail of cmdMoveKeepGroup $5470). If the unit's local group key matches the group remembered in D_929D it decrements the pending-order counter D_929E (this is the acknowledgement the formation editor waits for), then sets the unit's destination to the ordered cell through sub_34D5. Unlike command $80 it never touches the group bits, so the re-positioned unit stays a member. | zp_73 = unit index, zp_74/zp_75 = column/row (mirrored for remote commands), D_929D | unitDestCol/unitDestRow updated via sub_34D5, D_929E decremented when the group matches | high |
| 4C6B | syncLocalGroupFlags | Copies the authoritative group bits of unit X from unitGroupByte (F708 & $7C: group id and in-group bit) into unitLocalFlags (FB54, keeping bits 0,1 and 7). Called after every command that changes group membership ($34C6, $4AD4, $51A8) and when a unit is created ($3DFA). | X = unit index | FB54[X] updated; self-modifies the ora operand at $4C79 | high |
| 4C6B | syncLocalGroupFlags | Copies the authoritative group bits of unit X (unitFlagsTable & $7C = group number, side bit and member bit) into the local flag copy unitDisplayFlags, keeping the latter's bits 0, 1 and 7 (its own per-unit UI flags and the at-destination bit). Called after every command that changes group membership ($34D0 in the move executor, joinUnitToGroup, cmdLeaveGroup $51A8) and when a unit is created/reset ($3DFA), so that the UI copy can never drift from the simulation. | X = unit index | $FB54[X] = (old & $83) / (F708[X] & $7C); self-modifies the ORA operand at $4C79 | high |
| 4C84 | rotateGroupFormation | Untraced code: tail of the $A3 command handler ($5596 -> here). Rotates the formation of the leader's group about the leader's destination: for each member the offset (dx,dy) from the leader's destination is transformed according to mode zp_75 using formationRotateRules (0: (dy,-dx), 1: (-dy,dx), 2: (-dx,-dy)) and the new destination leader + offset is applied with $34D5. During the setup phase (zp_B1 != 0) the row is clamped to the issuing player's half of the map (rows 0-19 for own commands, 20-39 for remote ones). | zp_74 = leader unit, zp_75 = rotation mode 0-2, zp_76 bit 7 = remote command, zp_B1 = game phase | unitDestX/Y of all members updated; heavy self-modification of immediates ($4CC4, $4CD0, $4CDA, $4CDC, $4CE5, $4CEF, $4CF1, $4CFA, $4CFE, $4D05) | medium |
| 4C84 | rotateGroupFormation | Executor of command $A3 (tail of cmdRotateFormation $5596), the SWEEP LEFT / SWEEP RIGHT / REVERSE items of the FORMATION menu. It rotates the whole formation about the leader's destination: for every live member the offset (dx,dy) from the leader's destination is transformed according to the mode using the two 3-byte rule sets at formationRotateRules (0: (dy,-dx) = 90 degrees counter-clockwise on screen, 1: (-dy,dx), 2: (-dx,-dy)), and the new destination leader+offset is applied with sub_34D5. During the setup phase (gamePhase != 0) the resulting row is clamped into the issuing player's half of the map: rows 20-39 for own commands, rows 0-19 for commands received from the opponent. | zp_74 = leader unit index, zp_75 = rotation mode 0-2, zp_76 bit 7 = command came from the opponent, zp_B1 gamePhase | unitDestCol/unitDestRow of every member of the leader's group updated; zp_74/zp_75 reused as the destination; ten self-modified immediates ($4CC4 loop index, $4CD0 group key, $4CDA/$4CFD leader column, $4CDC/$4CE5 dx rule, $4CEE/$4D04 leader row, $4CF1/$4CFA dy rule) | high |
| 4D9F | getCommandLength | Looks up the total length (command byte plus parameters) of command A in commandLengthTable and stores it in currentCommandLength. Used by the packet merger, the dispatcher, the outgoing packet builder ($565C) and the comm module ($EA4C/$EA92). | A = command byte ($80-$A6) | A = D_92EE = length, X = command index (A & $3F) | high |
| 4DA9 | serviceCommandExchange | Per-frame entry of the lock-step command exchange (called from the main loops of every screen and from the overlays). Services the message system ($C4EF) and then, unless the throttle timer D_9163 is still running (ignored during film playback), falls into processCommandExchange. | D_0B9B bit 7 film playback, D_9163 throttle | see processCommandExchange | high |
| 4DB7 | processCommandExchange | Film playback (D_0B9B bit 7): reads film records whose time stamp (sub-tick D_923C and clock D_91CB) has come, copies their command bytes into turnCommandBuffer and executes them; record $FE stops, $FF runs the end-of-film handler ($59D0); afterwards keeps the link alive via L_56B1 when commSessionState is non-zero. Live play: when commSessionState bit 7 is set only the link keep-alive runs; otherwise, if the game is not ending ($C86D) and the link is active, waits for D_E01D bit 6 (packet exchange complete, polling $5A14 for timeouts), then stops sound $15, reloads the throttle (10) and timeout (0), merges own and received packets so that player 0's commands always come first (mergeOutgoingPacket / mergeIncomingPacket, D_92EC = start of player 1's part) and executes them with executeTurnCommands. | D_0B9B, D_0B9C, D_0BA8, D_E03B, D_E01D, D_0B9F, film pointer zp_BC/zp_BD, D_923C, D_91CB | turnCommandBuffer filled and executed, D_92EB/D_92EC/D_9163/D_9223/zp_69 set; self-modifies $4DE9 (time-stamp byte), $4DF4 (last clock) and $4E20 (first command index) | high |
| 4E91 | executeTurnCommands | Executes the merged command buffer: walks turnCommandBuffer from 0 to turnCommandCount with turnCommandCursor, sets currentCommandOrigin/zp_76 to $80 for commands issued by the remote player (toggles at player1CommandStart, initial value from localPlayerIndex), sets bit 0 of D_92C2, decrements pendingOutgoingCmdCount for own commands and dispatches each command byte (bit 7 set) through commandHandlerTable with the self-modified JSR at $4EEE; bytes with bit 7 clear are skipped by their length (inferred: cancelled commands, e.g. the $8E handler overwrites its own command byte with $0E at $534F). In live play it then records the turn to the film ($578A), acknowledges the packet (sub_E000 X=2) and builds/sends the next outgoing packet ($562B). | D_908F buffer, D_92EB count, D_92EC player-1 start, D_0B9F local player, D_0B9B film flag | game state changed by the handlers; D_90B7, D_92ED, zp_76, D_92C2, D_929F updated; next packet sent | high |
| 4F05 | mergeIncomingPacket | Appends the received packet (D_E020, D_E01E bytes) to turnCommandBuffer at index zp_69 with zp_76 = $80 (remote). Commands are copied whole using advancePacketCursor; $97 additionally clears D_92F9, $92 sets D_92F9 = $80, while $8D (opponent cursor/drone report) is handled at once by $52C8 and $8C (abort/disconnect + reason) by $523E without being copied. | D_E01E, D_E020-D_E026, zp_69 | D_908F[zp_69..] filled, zp_69 advanced, zp_6A scratch, zp_76 = $80, D_92F9 possibly changed | high |
| 4F5E | advancePacketCursor | Helper of the packet mergers: preserves the command byte in A, looks up its length (getCommandLength) and sets zp_6A = Y + length, i.e. the index of the next command in the packet. | A = command byte, Y = its index in the packet | A preserved, zp_6A = next index, D_92EE = length | high |
| 4F6B | mergeOutgoingPacket | Appends the local machine's own packet (D_E027, D_E01F bytes, the copy of what was just sent) to turnCommandBuffer at zp_69 with zp_76 = 0 (own). $8D is not copied: it re-centres the view origin zp_A5/zp_A6 on D_92F6/D_92F7 and decrements pendingOutgoingCmdCount; $8C is executed immediately through $523E; $92 sets D_92F9 = $80 and is copied like all other commands. | D_E01F, D_E027-D_E02D, zp_69 | D_908F filled, zp_69 advanced, zp_6A scratch, zp_76 = 0, zp_A5/zp_A6 or D_929F possibly changed | high |
| 4FC4 | readCmdArgs3 | Fetches the three parameter bytes that follow the current command byte in the merged command buffer ($908F, read index $90B7) into cmdParam0/1/2 ($73/$74/$75) and leaves $90B7 pointing at the next command. Listed as data; every 4-byte command handler starts with JSR $4FC4. | $90B7 = index of the command byte in $908F | $73,$74,$75 = parameters; $90B7 advanced by 4; Y clobbered | high |
| 4FC4 | readCmdArgs3 | Argument reader for the 4-byte commands ($80,$81,$8A,$8B,$98,$9C,$A6): reads mergedCmdBuffer[idx+1] into cmdParam0 ($73), bumps mergedCmdReadIdx by 1 and falls through into readCmdArgs2, which picks up the remaining two bytes and leaves the index on the next command. Net advance is 4, matching commandLengthTable. | D_90B7 = offset of the command byte in D_908F; D_908F merged command buffer | zp_73/zp_74/zp_75 = the three parameters; D_90B7 += 4; A/Y clobbered | high |
| 4FD0 | readCmdArgs2 | Entry into readCmdArgs3 used by 3-byte commands: reads the two bytes after the command byte into cmdParam1/cmdParam2 ($74/$75) and advances $90B7 by 3. Hidden as data. | $90B7 | $74,$75; $90B7 += 3 | high |
| 4FD0 | readCmdArgs2 | Argument reader for the 3-byte commands ($87,$88,$8E,$90,$9A,$A3,$A5) and the fall-through tail of readCmdArgs3: reads the two bytes after the command byte into cmdParam1/cmdParam2 and advances mergedCmdReadIdx by 3 (by 2 more when entered from $4FC4). | D_90B7, D_908F | zp_74, zp_75; D_90B7 += 3; A/Y clobbered | high |
| 4FE4 | mirrorCmdCoordsIfRemote | If the command being executed came from the opponent (cmdFromOpponent $76 bit 7) converts cmdParam1/cmdParam2 from the opponent's coordinate system with mirrorMapCoordinate ($3FA3, 39-x) so that both machines run the same simulation while each player sees his own side at the bottom. | $76 bit 7, $74/$75 = column/row | $74/$75 mirrored when remote | high |
| 4FE4 | mirrorCmdCoordsIfRemote | Point mirror for map coordinates carried by a command: when the command currently being executed came from the opponent (cmdFromOpponent zp_76 bit 7 set) both cmdParam1 and cmdParam2 are put through mirrorMapCoordinate ($3FA3, 39-x). Both machines run one shared simulation while each player sees his own side at the bottom of the map, so every coordinate received over the link has to be rotated 180 degrees. | zp_76 bit 7, zp_74 = column, zp_75 = row | zp_74/zp_75 replaced by 39-x/39-y for a remote command, untouched for an own command; A clobbered | high |
| 4FF7 | readCmdArg1 | Reads the single byte after the command byte into cmdParam0 ($73) and advances $90B7 by 2. Hidden as data; used by all 2-byte unit/group commands. | $90B7 | $73; $90B7 += 2 | high |
| 4FF7 | readCmdArg1 | Argument reader for the 2-byte commands (all the per-unit and per-group orders $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2, $A4): reads the single byte after the command byte into cmdParam0 and advances mergedCmdReadIdx by 2. | D_90B7, D_908F | zp_73; D_90B7 += 2; A/Y clobbered | high |
| 5005 | mirrorCmdCoordsIfRemoteDec | For a remote command mirrors cmdParam1/2 and then decrements both, i.e. mirrors the top-left corner of a 2x2-cell object (the drone); no-op for own commands. | $76, $74/$75 | $74/$75 = 38-x, 38-y when remote | medium |
| 5005 | mirrorCmdCoordsIfRemoteDec | Coordinate mirror for the drone, which occupies a 2x2 cell block: for a remote command it calls mirrorCmdCoordsIfRemote and then decrements both coordinates, giving 38-x / 38-y, i.e. the top-left corner of the mirrored 2x2 block. No-op for an own command. Used by cmdDroneMove and cmdDroneDetonate. | zp_76, zp_74, zp_75 | zp_74/zp_75 = 38-x, 38-y when remote; A clobbered | medium |
| 5011 | cmdMoveUnit | Handler of command $80 (MOVE, 4 bytes: unit, col, row): reads and mirrors the arguments, then jumps to the move-order routine $34C6 which clears the unit's group-membership bit, refreshes its display flags and sets the destination. Hidden as data; reached through cmdHandlerTable ($4D2A). | $908F command bytes | unit destination tables updated via $34C6 | high |
| 5011 | cmdMoveUnit | Handler of command $80 MOVE (4 bytes: unit, col, row) reached through commandHandlerTable. Reads and mirrors the arguments, then tail-jumps into applyMoveOrderOld ($34C6), which drops the unit out of its group, refreshes its display flags and sets the destination. | D_908F/D_90B7 | unit destination/waypoint tables via $34C6 | high |
| 501A | cmdMoveGroupRelative | Handler of command $81 (4 bytes: unit, col, row): reads/mirrors the arguments and jumps to $4A52, which moves every member of the unit's group by the same offset as the named unit (formation move). Hidden as data. | $73 unit, $74/$75 target | group destinations via $4A52 | high |
| 501A | cmdMoveGroupRelative | Handler of command $81 (4 bytes: unit, col, row): reads and mirrors the arguments and tail-jumps into moveGroupKeepingFormation ($4A52), which shifts every member of the named unit's group by the same offset so the formation is preserved. | D_908F/D_90B7 | destinations of all group members via $4A52 | high |
| 5023 | cmdCloakOn | Handler of command $82 (2 bytes: unit): first clears the unit's blitz bits through cmdBlitzOff (the read index is saved in the self-modified operand at $502D and restored), then re-reads the unit and, unless it is type 3 (spy), sets unitWaypointCol bits 6-7 (cloak ordered + active). Falls into the shared tail at $5058 (own command: pendingRedrawUnit $923B = unit) and updateComcenCloakLock. Hidden as data. | $908F, $90B7 | F898[unit] /= $C0, F8FC[unit] &= $3F, $923B, $9245 | high |
| 5023 | cmdCloakOn | Handler of command $82 CLOAK ON (2 bytes: unit). Cloak and blitz are mutually exclusive, so it first runs cmdBlitzOff on the same unit; because that handler consumes the argument it saves mergedCmdReadIdx in the self-modified LDA operand at $502C and restores it before re-reading. Type 3 (SPY) units are rejected; otherwise unitWaypointCol bits 6-7 are set (cloak ordered + active) and control falls into the shared tail at $5058. | D_908F, D_90B7, F76C unit type | D_F898[unit] /= $C0, D_F8FC[unit] &= $3F, D_923B and D_9245 via the tail; self-modifies $502D | high |
| 504B | cmdCloakOff | Handler of command $84 (2 bytes: unit): clears bits 6-7 of unitWaypointCol (cloak off). Tail at $5058: for an own command records the unit in pendingRedrawUnit ($923B) so the tick loop redraws it, then updateComcenCloakLock. Hidden as data; also called as a subroutine by cmdBlitzOn. | $73 after readCmdArg1 | F898[unit] &= $3F; $923B; $9245 | high |
| 504B | cmdCloakOff | Handler of command $84 CLOAK OFF (2 bytes: unit); also called as a subroutine by cmdBlitzOn. Clears bits 6-7 of unitWaypointCol. The shared tail at $5058 records the unit in pendingRedrawUnit ($923B) for an own command only (the opponent's cloaking must not trigger a local redraw) and then falls into updateComcenCloakLock. Note there is no SPY exemption here, unlike the group handler. | zp_73 after readCmdArg1; zp_76 | D_F898[unit] &= $3F; D_923B = unit for an own command; D_9245 | high |
| 5062 | cmdBlitzOff | Handler of command $95 (2 bytes: unit): clears bits 6-7 of unitWaypointRow (blitz ordered/active). Hidden as data; also called by cmdCloakOn. | $908F | F8FC[unit] &= $3F | high |
| 5062 | cmdBlitzOff | Handler of command $95 BLITZ OFF (2 bytes: unit); also called as a subroutine by cmdCloakOn. Clears bits 6-7 of unitWaypointRow (blitz ordered/active). | D_908F/D_90B7 | D_F8FC[unit] &= $3F; zp_73 = unit; Y = unit | high |
| 5070 | cmdBlitzOn | Handler of command $93 (2 bytes: unit): calls cmdCloakOff for the unit (cloak and blitz are exclusive), re-reads the unit and, unless it is a spy (type 3), sets unitWaypointRow bits 6-7 and jumps to applyBlitzStepCost ($3EE2). Hidden as data. | $908F | F8FC[unit] /= $C0, F898 cleared, energy/movement via $3EE2 | high |
| 5070 | cmdBlitzOn | Handler of command $93 BLITZ ON (2 bytes: unit). Mirror image of cmdCloakOn: saves/restores mergedCmdReadIdx around a call to cmdCloakOff (blitz cancels cloak), rejects type 3 (SPY), sets unitWaypointRow bits 6-7 and tail-jumps into applyBlitzStepCost ($3EE2), which pays the energy for the extra movement points. | D_908F, D_90B7, F76C unit type | D_F8FC[unit] /= $C0, D_F898[unit] &= $3F, energy/movement via $3EE2; self-modifies $507A | high |
| 5098 | cmdGroupCloakOn | Handler of command $83 (2 bytes: group id): first runs cmdGroupBlitzOff for the group, then re-parses the argument with X=$80 through the shared group routine at $50D4 (table F898, OR $C0): every living non-spy member of the group gets its cloak bits set, groupStatusTable[group] gets bit 7 set, and for an own command pendingRedrawGroup ($923A) = group. Hidden as data. | $908F | F898 of members /= $C0, F8FC &= $3F, $91F4[g], $923A, $9245 | high |
| 5098 | cmdGroupCloakOn | Handler of command $83 (2 bytes: group id byte). Runs cmdGroupBlitzOff for the group first (exclusive states, index saved/restored via $50A2), then enters applyGroupCloakOrBlitz at $50D4 with X=$80 so the shared body works on table $F898 with OR $C0 and sets bit 7 of groupStatusTable. | D_908F/D_90B7; group id byte carries bits 2-5 (bits 2-4 group index, bit 5 side) | D_F898 of every living non-SPY member /= $C0, D_F8FC &= $3F, D_91F4[g] /= $80, D_923A, D_9245 | high |
| 50AA | cmdGroupBlitzOn | Handler of command $94 (2 bytes: group id): runs cmdGroupCloakOff for the group, then the shared group routine with table F8FC, OR $C0 and the applyBlitzStepCost call enabled: all living non-spy members get blitz set and pay the blitz cost; groupStatusTable[group] = (old & $BF) / $40. Hidden as data. | $908F | F8FC of members /= $C0, $91F4[g] | high |
| 50AA | cmdGroupBlitzOn | Handler of command $94 (2 bytes: group id byte). Runs cmdGroupCloakOff first, then enters the shared body at $50C1 with X=$C0 / A=$40 so it works on table $F8FC with OR $C0, calls applyBlitzStepCost for each member and sets groupStatusTable[g] = (old & $BF) / $40. | D_908F/D_90B7 | D_F8FC of every living non-SPY member /= $C0, per-member blitz cost, D_91F4[g] | high |
| 50BE | cmdGroupBlitzOff | Handler of command $96 (2 bytes: group id) and entry of the self-modifying group routine: patches the operands at $512D/$514B/$514D/$5134/$5139/$513B (table F8FC, AND $3F, OR 0, no blitz cost) and continues at $50EB. groupStatusTable[group] &= $BF. Hidden as data; also used by cmdGroupCloakOn. | $908F | F8FC of members &= $3F, $91F4[g] &= $BF | high |
| 50BE | cmdGroupBlitzOff | Handler of command $96 (2 bytes: group id byte) and the second patch entry of the self-modifying group routine. With X=A=0 it patches the operands at $512D (blitz-cost gate = 0 -> skipped), $514D (groupStatusTable OR = 0), $514B (groupStatusTable AND = $BF) and selects table $F8FC, then continues at $50EB. | D_908F/D_90B7 | D_F8FC of members &= $3F, D_91F4[g] &= $BF | high |
| 50D2 | cmdGroupCloakOff | Handler of command $85 (2 bytes: group id): X=0 -> shared routine with table F898, OR 0: clears the cloak bits of every member, groupStatusTable[group] &= $7F. The common body at $50EB-$5150 reads the group byte, ORs $40 into it to match unitFlagsTable & $7C, loops over all 100 units and finally jumps to updateComcenCloakLock. Hidden as data. | $908F; patched operands | F898 of members &= $3F, $91F4[g] &= $7F, $9245 | high |
| 50D2 | cmdGroupCloakOff | Handler of command $85 (2 bytes: group id byte): enters the patch block at $50D4 with X=0, selecting table $F898, OR 0 and groupStatusTable AND $7F, so the cloak bits of every member are cleared. | D_908F/D_90B7 | D_F898 of members &= $3F, D_91F4[g] &= $7F, D_923A for an own command, D_9245 | high |
| 50EB | applyGroupCloakOrBlitz | Shared, heavily self-modified body of the four group cloak/blitz handlers (entered by branch, never by JSR). It stores the chosen table address into the LDA/STA operands at $5134/$513B, reads the group id argument, records pendingRedrawGroup ($923A) for an own command when the cloak variant is running (the AND operand at $514B equals $7F), ORs $40 into the group byte so it matches unitGroupByte & $7C, then walks units 99..0 skipping dead slots ($F76C bit 7), non-members and SPYs (type 3), optionally calling applyBlitzStepCost, and rewrites table[unit] = (old & $3F) / patchedBits. Finally it updates groupStatusTable[(groupByte & $3C) >> 2] and falls into updateComcenCloakLock. | patched operands at $512D/$5134/$5139/$513B/$514B/$514D; zp_73 group byte; D_F708, D_F76C | cloak or blitz bits of all matching units, D_91F4[group], D_923A, D_9245 | high |
| 5151 | updateComcenCloakLock | Shared tail of the cloak/blitz handlers: unitInspectLockFlags ($9245) = unitWaypointCol[comcenUnit] & $80, i.e. bit 7 set while the player's own comcen is cloaked. Hidden as data. | $9236, F898 | $9245 | medium |
| 5151 | updateComcenCloakLock | Common tail of all cloak/blitz handlers and the only writer of $9245: copies bit 7 of unitWaypointCol[ownComcen] into unitInspectLockFlags, so $9245 bit 7 is set exactly while the local player's own COMCEN is cloaked. Read by the console/inspection code at $2064/$2096 and by the overlays ($75A5, $8048). Besides the fall-through from the cloak handlers it is called directly by both $6F00 overlay variants at $72E7. | D_9236 own comcen index, D_F898; no arguments | D_9245 = D_F898[comcen] & $80; A/Y clobbered | high |
| 515D | cmdClearTarget | Handler of command $86 (2 bytes: boomer unit): getBoomerTarget ($3C74) gives the boomer ordinal in Y and boomerTargetTable[Y] is cleared (CLEAR TARGET menu item). Hidden as data. | $908F | $91DC[ordinal] = 0 | high |
| 515D | cmdClearTarget | Handler of command $86 CLEAR TARGET (2 bytes: BOOMER unit): getBoomerTarget ($3C74) converts the unit index into the BOOMER ordinal in Y and boomerTargetTable[Y] is zeroed. | D_908F/D_90B7 | D_91DC[ordinal] = 0 | high |
| 516B | cmdSetTarget | Handler of command $87 (3 bytes: boomer unit, target unit): reads the two arguments, moves them so that $73 = boomer and $75 = target (the EOR #$80 on $76 is undone again and has no effect) and jumps to setBoomerTargetCmd ($3C50). Hidden as data. | $908F | $91DC[ordinal] = target / $80 | high |
| 516B | cmdSetTarget | Handler of command $87 SET TARGET (3 bytes: BOOMER unit, target unit). After readCmdArgs2 it rearranges the parameters so zp_73 = BOOMER and zp_75 = target and jumps to setBoomerTargetCmd ($3C50). The EOR #$80 applied to cmdFromOpponent at $5171 is undone by the PLA/STA at $517B, so it has no effect - dead code left from an earlier version. | D_908F/D_90B7 | D_91DC[ordinal] = target / $80 via $3C50; zp_73/zp_75 rewritten | high |
| 5185 | cmdJoinGroup | Handler of command $88 (3 bytes: unit, group id): the register shuffle is a no-op apart from $73 = group; jumps to $4AD4 which adds unit $74 to group $75 (MEMBERSHIP/JOIN GROUP). Hidden as data. | $908F | unit group bits via $4AD4 | high |
| 5185 | cmdJoinGroup | Handler of command $88 JOIN GROUP (3 bytes: unit, group id byte): jumps to joinUnitToGroup ($4AD4) with zp_74 = unit and zp_75 = group. The eight instructions at $5188-$5197 are a redundant shuffle - only 'zp_73 = group' survives and nothing downstream reads zp_73, so the whole block is effectively dead code. | D_908F/D_90B7 | D_F708[unit] = (old & 3) / group / $40 and D_FB54 via $4AD4 | high |
| 519B | cmdLeaveGroup | Handler of command $89 (2 bytes: unit): clears bit 6 (group membership active) of unitFlagsTable and jumps to $4C6B which copies bits 2-6 of unitFlagsTable into unitDisplayFlags. Hidden as data. | $908F | F708[unit] &= $BF, FB54[unit] | high |
| 519B | cmdLeaveGroup | Handler of command $89 (2 bytes: unit): clears bit 6 (in-group) of unitGroupByte and jumps to syncLocalGroupFlags ($4C6B), which copies bits 2-6 of unitGroupByte into unitDisplayFlags so the map stops drawing the group marker. | D_908F/D_90B7 | D_F708[unit] &= $BF, D_FB54[unit] | high |
| 51AB | cmdPlaceUnit | Handler of command $8A (4 bytes: unit, col, row): teleports the unit - position, destination (with the idle bit), waypoint (keeping the cloak/blitz bits which are read into the patched ORA operands at $51C9/$51E5) and unitDisplayFlags bit 7 are all set to col/row. Used when units are positioned (setup phase / deployment). Hidden as data. | $908F | F640,F6A4,F7D0,F834,F898,F8FC,FB54 of the unit | medium |
| 51AB | cmdPlaceUnit | Handler of command $8A (4 bytes: unit, col, row): teleports a unit during setup/deployment. It first saves the unit's current cloak bits (F898 & $C0) and blitz bits (F8FC & $C0) into the ORA operands at $51C9 and $51E5, then writes the mirrored column to unitCol, unitWaypointCol (keeping the cloak bits) and unitDestCol (with the idle bit 7), sets bit 7 of unitDisplayFlags and writes the row to unitRow, unitDestRow and unitWaypointRow (keeping the blitz bits). | D_908F/D_90B7, zp_76 | D_F640, D_F6A4, D_F7D0, D_F834, D_F898, D_F8FC, D_FB54 of the unit; self-modifies $51C9/$51E5 | high |
| 51EA | cmdSetCustomMapSeed | Handler of command $A6 (4 bytes: 3 seed bytes): sets mapKindFlag ($0B9D) = 1 and falls into cmdSetMapSeed. Sent by overlay A ($79BD) during game setup. Hidden as data. | $908F | $0B9D = 1, then as cmdSetMapSeed | medium |
| 51EA | cmdSetCustomMapSeed | Handler of command $A6 (4 bytes: three seed bytes): sets mapKindFlag ($0B9D) = 1 and falls into cmdSetMapSeed. Sent by overlay A ($79BD) when the map is the flipped/custom variant; the receiving side's map generator uses $0B9D to decide how to build the terrain. | D_908F/D_90B7 | D_0B9D = 1, then everything cmdSetMapSeed does | medium |
| 51EE | cmdSetMapSeed | Handler of command $8B (4 bytes): mapKindFlag ($0B9D) = 0, copies the three argument bytes into the map seed $0B95-$0B97 and clears bit 7 of gamePhase ($B1). Hidden as data. | $908F | $0B9D, $0B95-$0B97, $B1 &= $7F | medium |
| 51EE | cmdSetMapSeed | Handler of command $8B (4 bytes: three seed bytes): mapKindFlag = 0, copies the three arguments into mapSeed $0B95-$0B97 and clears bit 7 of gamePhase zp_B1. Player 0 sends this at the start of a session; player 1's waitForMapSeedCommand ($7C1C) spins on gamePhase bit 7 until this handler runs, so both machines generate the identical battlefield. | D_908F/D_90B7 | D_0B9D = 0, D_0B95-D_0B97 = seed, zp_B1 &= $7F | high |
| 520C | cmdComcenRepairedOrSetupReady | Handler of the 1-byte command $9E. During the battle ($B1 == 0) it clears the stun counter (low nibble of unitStunTable) of the sending side's comcen - the successful comcen repair from repairTabAction. In the setup phases ($B1 != 0) it is the 'ready' acknowledgement: clears the sender's bit in pauseState ($BE) via getPauseClearMask, sets uiDelayTimer $0B7D = $20, injects key code $20 into newKeyEvent ($90EA) and resets chatState ($929C). Hidden as data. | $B1, $76, $0B9F, $0C8A | F960[comcen] &= $F0 or $BE/$0B7D/$90EA/$929C | medium |
| 520C | cmdComcenRepairedOrSetupReady | Handler of the 1-byte command $9E, which has two meanings. When gamePhase zp_B1 is zero (battle running) it clears the low nibble (stun counter) of unitStunTable for the sending side's COMCEN - the result of a successful COMCEN repair from the REPAIR console tab. When gamePhase is nonzero it is the phase-sync/READY acknowledgement of sendPhaseSyncAndWait ($7BF9): it clears the sender's bit in gamePhase with getPauseClearMask, sets inputLockoutTimer = $20, injects key code $20 (SPACE) into newKeyEvent so any 'press space' wait loop falls through, and resets chatState. | zp_B1, zp_76, D_0B9F playerSide, perSideValueTable $0C8A | battle: D_F960[comcen] &= $F0. Setup: zp_B1 &= mask, D_0B7D = $20, D_90EA = $20, D_929C = 0. D_90B7 += 1 in both cases | high |
| 523E | cmdSessionControl | Handler of command $8C (2 bytes: reason). Executed immediately while the packets are merged (not queued). Reason 0 = opponent aborted / left the game: sets $923F = $C0, gameEndCountdown $92C9 = $62 (no game running) or $71 (game running, with 'GAME ABORTED.' when remote) and aborts the dispatcher by dropping two return addresses and jumping to the comm module with X=2. Reasons 1-2 do nothing here (2 = READY handshake handled by readyForNewGameHandshake). Reason 3 = pause/timeout: pauseState = 3, 'TIMEOUT. RUN/STOP TO RESUME.' kept as persistent message, input lockout 60 frames. Reason 5 = disconnect: pauseState = $80. Reason 4 (and others) = resume: clears the sender's pauseState bit (getPauseClearMask); when no bits remain the message is cleared and 'RESUMED' shown. | A = reason byte, X/Y preserved through saveRegsForCmdReturn, $76, $0BA8, $0B9B | $BE, $92A1, $0B7D, $929C, $92C9, $923F, messages | high |
| 523E | cmdSessionControl | Handler of command $8C (2 bytes: reason). It is never dispatched from the merged buffer - both packet mergers ($4F36 and $4F9C) call it directly with A = the reason byte while the packets are being merged, so the effect is immediate and the byte is not copied into the film. Reason 0 = the opponent aborted: gameOverFlags $923F = $C0, the message queue is cleared, 'GAME ABORTED.' (message 3) is shown for a remote abort, gameEndCountdown $92C9 = $62 (no game running) or $71 (game running), and the dispatcher is abandoned by pulling two return addresses off the stack and jumping to the comm module with X=2. Reasons 1 and 2 return at once ($8C,02 is the menu-level READY-for-new-game handshake handled by handleMenuSessionPacket). Reason 3 = pause/timeout (RUN/STOP, sent by $C74B): pauseState zp_BE = 3, persistent message $19 'TIMEOUT. RUN/STOP TO RESUME.', 60-frame input lockout. Reason 5 = hang up (menu item at $65C7): pauseState = $80. Any other reason (4 = resume, sent by $C786) clears the sender's pauseState bit via getPauseClearMask and, when no bits are left, clears the persistent message and shows 'RESUMED' (message $1B). Everything except reason 0 is skipped entirely during film playback ($0B9B bit 7). | A = reason byte, X/Y = the merge loop registers, zp_76, D_0B9B, D_0BA8 | zp_BE, D_92A1, D_0B7D, D_929C, D_92C9, D_923F, zp_72, message queue; returns through $532A with X/Y restored, or never returns for reason 0 | high |
| 52AE | getPauseClearMask | Returns the AND mask used to clear pauseState bits when one side resumes: $FC (both bits) for comm build 1 ($0BA5 bit 7), $FD for an own resume, $FE for the opponent's resume; in the latter case, if the opponent's bit (bit 1 of X = old pauseState) was set, 'OPPONENT READY.' (message 4) is queued. | X = current pauseState, $0BA5, $76 | A = mask | medium |
| 52AE | getPauseClearMask | Returns the AND mask that clears the resuming side's bit of the two-bit pause/ready state (used on pauseState zp_BE by cmdSessionControl and on gamePhase zp_B1 by cmdComcenRepairedOrSetupReady). Comm build 1 ($0BA5 bit 7 set) has no independent opponent, so it returns $FC and clears both bits at once. Otherwise an own command returns $FD (clear bit 0) and a remote command returns $FE (clear bit 1); in the remote case, if bit 1 of X was still set it also queues message 4 'OPPONENT READY.'. | X = current pause/phase byte, D_0BA5, zp_76 | A = AND mask $FC/$FD/$FE; may queue message 4; Y clobbered | high |
| 52C8 | cmdDroneMove | Handler of command $8D (4 bytes: heading, col, row), executed immediately by the packet merger, never queued. Stores heading/col/row in $73-$75, mirrors for a remote drone (mirrorCmdCoordsIfRemoteDec), and unless droneFlags $92F9 bits 4-5 are set, sets bit 0 of $920D and - when screen 1 (the map/radar screen) is active - disables sprite 0 in spriteEnableShadow, stores the drone position in $92F6/$92F7, converts it to sprite coordinates with $340B, sets sprite 0 shape via $3340 from (heading+4)&7 and colour 1. | $E020 packet bytes at Y, $76 | $92F6/$92F7, $920D, sprite 0 shadows; X/Y restored to the saved values | medium |
| 52C8 | cmdDroneMove | Handler of command $8D (4 bytes: heading, col, row), the opponent's live drone telemetry. Only mergeIncomingPacket calls it (at $4F28, with Y pointing one past the command byte in the received packet $E020); mergeOutgoingPacket handles its own $8D inline by re-centring the view instead, and the commandHandlerTable slot for $8D is deliberately $FFFF because the byte is never copied into the merged buffer. It stores heading/col/row, mirrors the 2x2 drone origin for the remote frame, and unless droneFlags bits 4-5 say the drone is already finished it sets bit 0 of droneStateFlags and, only while screen 1 is showing, disables sprite 0, remembers the drone cell in $92F6/$92F7, converts the cell to sprite coordinates with getUnitSpriteCentreOnGrid (leaving sprite 0 hidden when the drone is off the visible grid), selects activeSpriteIndex 0, turns the heading into shape (heading+4) mod 8 - adding 4 to an 8-way heading reverses it, which is exactly the direction change the 180-degree map mirror demands - loads that shape with loadDirectionShapeSprite and sets sprite 0 colour 1. | Y = index in D_E020, zp_76, D_92F9, D_90FB current screen | zp_73/74/75, D_920D /= 1, D_9022, D_92F6/D_92F7, D_9010/D_9018, D_91CC, D_9008, sprite 0 shape; X/Y restored from the operands patched by saveRegsForCmdReturn | high |
| 532F | saveRegsForCmdReturn | Stores X and Y into the LDX/LDY operands at $532A/$532C so that cmdSessionControl and cmdDroneMove return with the registers the packet merger had (the merge loop index). | X, Y | patched $532B/$532D | high |
| 532F | saveRegsForCmdReturn | Two-instruction helper used by cmdSessionControl and cmdDroneMove: copies X and Y into the LDX/LDY immediate operands at $532B/$532D so the common exit at $532A hands the packet merger back exactly the loop registers it had. Necessary because both handlers are called from the middle of the merge loop rather than from the table dispatcher. | X, Y | self-modifies $532B and $532D | high |
| 5336 | cmdDroneDetonate | Handler of command $8E (3 bytes: col, row): own command clears $920D bit 5; if droneFlags bit 7 is already set (drone finished) the command byte in the buffer is overwritten with $0E so the film records a no-op. Otherwise droneFlags = $40, coordinates mirrored/decremented for the opponent, sound $11, A=$19 to overlay B $72A1 (blast), explosion effect at col/row through $33C8 with A=1, and for a remote command $920D &= $C0. Hidden as data. | $908F, $76, $92F9, $0B9B | $92F9, $920D, $908F command byte, sound/graphics | medium |
| 5336 | cmdDroneDetonate | Handler of command $8E (3 bytes: col, row). For an own command it clears bit 5 of droneStateFlags. If droneFlags bit 7 is already set (the drone had reached its target/expired before the order arrived) the detonation is cancelled: the command byte in the merged buffer is neutered in place by writing $0E over it (D_908C,y with y = readIdx+3 addresses mergedCmdBuffer[readIdx]) - clearing bit 7 makes executeTurnCommands skip the command, and because getCommandLength masks with $3F the length still comes out as 3, so the film records a correctly sized no-op. Otherwise droneFlags = $40 (detonated), the coordinates are mirrored/decremented, sound $11 is played (suppressed for a silent own command outside film playback), applyDroneBlast ($72A1 in overlay B) is called with A=$19 to damage everything in the blast radius, a blast ring sprite is drawn at the cell ($33C8 with A=1), and a remote detonation finally clears bits 0-5 of droneStateFlags through the shared tail at $537E. | D_908F/D_90B7, zp_76, D_92F9, D_0B9B | D_92F9, D_920D, mergedCmdBuffer command byte, sound, blast damage and sprite | high |
| 537B | cmdDroneRelease | Handler of the 1-byte command $8F: advances the read index and, for a remote command, clears bits 0-5 of $920D (drone control finished on the other side). Hidden as data. | $76 | $920D | low |
| 537B | cmdDroneRelease | Handler of the 1-byte command $8F: just advances mergedCmdReadIdx and falls into the shared tail at $537E which, for a remote command only, clears bits 0-5 of droneStateFlags - the opponent has let go of the drone, so the local copy of his drone-control state is reset while bits 6-7 (own control / drone screen) are preserved. | zp_76, D_90B7 | D_920D &= $C0 when remote | medium |
| 538B | cmdDroneArrived | Handler of the 1-byte command $92 (sent by overlay B $79CA when the drone is within one cell of its target): $920D &= $3F and for an own command droneFlags /= $10. The packet merger additionally sets droneFlags = $80 as soon as a $92 byte is seen in either packet. Hidden as data. | $76 | $920D, $92F9 | medium |
| 538B | cmdDroneArrived | Handler of the 1-byte command $92, sent by overlay B ($79CA) when the drone comes within one cell of its target: clears bits 6-7 of droneStateFlags and, for an own command, sets bit 4 of droneFlags. Both packet mergers additionally set droneFlags = $80 the moment a $92 byte is seen in either packet, and sendOutgoingPacket drops a queued $92 while droneFlags bit 6 is set. | zp_76, D_920D, D_92F9 | D_920D &= $3F; D_92F9 /= $10 for an own command | high |
| 53A3 | cmdDroneLaunch | Handler of the 1-byte command $97: droneFlags = 0; for the opponent's launch plays sound 7 and shows 'DRONE ALERT!' (overlay B $820F); decrements the launcher's remaining drone count dronesLeft[$92AE + 0 own / 1 opponent] if nonzero and calls $403B (stats refresh). Hidden as data. | $76, $92AE | $92F9 = 0, $92AE | medium |
| 53A3 | cmdDroneLaunch | Handler of the 1-byte command $97: clears droneFlags (fresh drone in flight). For the opponent's launch it shows 'DRONE ALERT!' with sound (showDroneAlert, overlay B $820F) and selects index 1. Then, if that side still has drones, it decrements dronesLeft[side] and calls markComcenRevealed ($403B), which exposes the launching COMCEN for six rounds - the price of using a drone. | zp_76, D_92AE | D_92F9 = 0, D_92AE[side] decremented, comcen reveal timer and visibility bits via $403B | high |
| 53C0 | cmdMissileStrike | Handler of command $90 (3 bytes: col, row). In the setup phase ($B1 != 0) it just stores the two values as dronesLeft/missilesLeft for the sender ($92AE/$92B0 index 0 own, 1 opponent). In battle: if the sender still has missiles and the game runs, decrements missilesLeft, calls $403B, mirrors the coordinates, and if on the map hits the unit standing there with 14 energy (7 for a comcen, via $3B22), draws the explosion effect ($33C8 with A=0) and plays sound $0C. Hidden as data. | $908F, $B1, $76, $0BA8 | $92AE/$92B0, unit damage, effects | medium |
| 53C0 | cmdMissileStrike | Handler of command $90 (3 bytes), which carries two different payloads. During a setup phase (gamePhase zp_B1 nonzero) the two bytes are the sender's drone and missile allowances and are stored in dronesLeft/missilesLeft for that side (index 0 own, 1 opponent) - this is what overlay A's editDronesAndMissiles sends. During the battle they are the target column/row: if the firing side still has missiles and no game-end reason is set, missilesLeft is decremented, markComcenRevealed exposes the firing COMCEN, the coordinates are mirrored for a remote shot, and if the cell is on the 40x40 map and holds a unit ($F000 cell bit 7) that unit takes 14 energy of damage - halved to 7 for either COMCEN (index $31 or $63) - through applyHitToUnit. A blast ring sprite is drawn at the cell ($33C8 with A=0) and sound $0C plays unless it is a silent own shot outside film playback. | D_908F/D_90B7, zp_B1, zp_76, D_0BA8, map $F000 | D_92AE/D_92B0[side], unit energy/score via $3B22, sprite 4 blast ring, sound | high |
| 542E | cmdSelfDestruct | Handler of command $91 (2 bytes: unit, bit 7 = whole group of that unit): destroys the unit, or every living member of the unit's group, through destroyUnit. Hidden as data. | $908F | unitTypeTable entries = $FF | high |
| 542E | cmdSelfDestruct | Handler of command $91 (2 bytes: unit; bit 7 of the argument means 'the whole group of that unit'). For a single unit it falls straight into destroyUnit. For a group it builds the match value (unitGroupByte & $3C) / $40 into the CMP operand at $544E - note the $3C mask keeps the side bit 5, so a group never spans both sides - and walks units 99..0 calling destroyUnit on every member. | D_908F/D_90B7, D_F708 | D_F76C entries set to $FF; self-modifies $544E; zp_73 used as the loop counter | high |
| 5459 | destroyUnit | Kills unit $73 unless it is a comcen (49 or 99): sound/explosion $34 via $3B52 and unitTypeTable[unit] = $FF (slot freed). Hidden as data. | $73 = unit | F76C[unit] = $FF | medium |
| 5459 | destroyUnit | Removes unit zp_73 from play, except a COMCEN (index $31 or $63), which is never allowed to self-destruct. It applies 52 ($34) damage through sub_3B52 - more than the 50-point maximum energy, so the kill is certain and goes through the normal explosion/score/redraw path - and then frees the slot outright by writing $FF into unitTypeTable. | zp_73 = unit index | D_F76C[unit] = $FF, explosion/sound/score side effects of $3B52; Y = unit | high |
| 5470 | cmdMoveKeepGroup | Handler of command $98 (4 bytes: unit, col, row): reads/mirrors the arguments and jumps to $4C59, which decrements pendingGroupOrderCount ($929E) when the unit belongs to pendingGroupOrderId ($929D) and then sets the destination through $34D5 without touching the group bits (MOVE TO GOAL for group members). Hidden as data. | $908F | destination tables, $929E | medium |
| 5470 | cmdMoveKeepGroup | Handler of command $98 (4 bytes: unit, col, row), the MOVE TO GOAL order issued to each member of a group in turn: reads and mirrors the arguments and jumps to setGroupMemberDestination ($4C59), which decrements pendingGroupOrderCount when the unit belongs to pendingGroupOrderId and then sets the destination through $34D5 without disturbing the group membership bits. | D_908F/D_90B7 | destination tables, D_929E | high |
| 5479 | cmdAddEnergy | Handler of command $9A (3 bytes: unit, amount): unless the game ended by knockout ($0BA8 bit 7) adds amount to the unit's energy (bits 0-5, capped at 50) keeping the spotted bits 6-7. Sent by the comcen energy repair. Hidden as data. | $908F, $0BA8 | F9C4[unit] | high |
| 5479 | cmdAddEnergy | Handler of command $9A (3 bytes: unit, amount) sent by the COMCEN energy repair: unless the game has already ended by knockout ($0BA8 bit 7) it adds the amount to bits 0-5 of unitEnergyTable, saturating at 50 ($32), and restores the two visibility/spotted bits 6-7 that were saved into the ORA operand at $5499. | D_908F/D_90B7, D_0BA8 | D_F9C4[unit]; self-modifies $5499 | high |
| 549E | cmdHaltGroup | Handler of command $9B (2 bytes: group id): for every living member of the group sets destination and waypoint to the current position, the idle bits (unitDestCol bit 7, unitDisplayFlags bit 7), keeps the cloak bits but clears the blitz bits. Hidden as data. | $908F | F7D0,F834,F898,F8FC,FB54 of members | high |
| 549E | cmdHaltGroup | Handler of command $9B (2 bytes: group id byte) - the group HALT order. For every living member (walking units 99..0 and matching unitGroupByte & $7C against groupByte / $40) it copies the unit's current position into its destination and waypoint, sets the idle bits (unitDestCol bit 7 and unitDisplayFlags bit 7), preserves the cloak bits in unitWaypointCol via the ORA operand patched at $54D0, and clears the blitz bits by overwriting unitWaypointRow with the plain row. | D_908F/D_90B7, D_F708, D_F76C | D_F7D0, D_F834, D_F898, D_F8FC, D_FB54 of every member; self-modifies $54D0 | high |
| 54E1 | cmdSetRecycler | Handler of command $9C (4 bytes: mode, col, row): recyclerMode[side] = mode (side = sender's side); if mode < 2 the coordinates are mirrored for a remote sender and stored in recyclerCol/recyclerRow[side]; an own command also draws the recycler on the map (drawRecyclerOnMap with Y=$FF). Sent by overlay A ($8436) during setup. Hidden as data. | $908F, $0B9F, $76 | $92A6/$92A8/$92AA | high |
| 54E1 | cmdSetRecycler | Handler of command $9C (4 bytes: mode, col, row), sent by overlay A's chooseRecycler ($8436) during setup for game types 4-6 (SLUGGERS/FULL WAR/DEFENDER). recyclerMode[side] = mode with side = the sender's absolute side (playerSide, EOR 1 when remote); for modes 0 and 1 the coordinates are mirrored for a remote sender and stored in recyclerCol/recyclerRow[side]. An own command additionally draws the recycler on the overview map with drawRecyclerOnMap, Y = the side index restored from the operand patched at $550C. | D_908F/D_90B7, D_0B9F, zp_76 | D_92A6/D_92A8/D_92AA indexed by side; map redraw; self-modifies $550C | high |
| 5511 | cmdChooseSide | Handler of command $9D (2 bytes: chosen side). Comm build 1: local side forced to 0. Otherwise only the opponent's choice matters: if it differs from ours (selectedUnit $90F8 holds our pick) playerSide = (our pick > theirs), else playerSide = random / $80 (conflict to be resolved); gamePhase $B1 = $DF ends the side-selection phase. Hidden as data. | $908F, $0BA5, $76, $90F8 | $0B9F, $B1 | medium |
| 5511 | cmdChooseSide | Handler of command $9D (2 bytes: the sender's chosen side), the side-selection handshake. In comm build 1 ($0BA5 bit 7) there is no real opponent and playerSide is forced to 0. Otherwise only the opponent's copy of the command matters: our own pick was stashed in $90F8 by sendChooseSide, and if the two picks differ playerSide becomes 0 or 1 according to whether ours was the higher (ROL of the carry from the CPY). If both players picked the same side the tie is broken by nextGameRandom with bit 7 set as a 'conflict, retry' marker. Either way gamePhase zp_B1 is set to $DF, which is what startGameSession waits for. | D_908F/D_90B7, D_0BA5, zp_76, D_90F8 | D_0B9F playerSide, zp_B1 = $DF | high |
| 553B | sendChooseSide | Queues command $9D with the locally chosen side (playerSide copied to cmdParam0 and selectedUnit). Called from overlay A's side selection ($7976). | $0B9F | outgoing ring, $90F8 | medium |
| 553B | sendChooseSide | Not a command handler but the sender side of the $9D exchange, called from overlay A's side-selection screen ($7976): copies playerSide into cmdParam0 and into selectedUnit $90F8 (where cmdChooseSide later finds our own pick) and queues the 2-byte command $9D through queueCmd2. | D_0B9F | zp_73, D_90F8, two bytes appended to the outgoing command ring | high |
| 5548 | cmdDigIn | Handler of command $9F (2 bytes: unit): setUnitDigBits with $20 -> unitStunTable bits 5-7 = 001 (digging in progress). Hidden as data. | $908F | F960[unit] | high |
| 5548 | cmdDigIn | Handler of command $9F DIG IN (2 bytes: unit): calls setUnitDigBits with $20, i.e. unitStunTable bits 5-7 = 001, which processUnitStep then advances by $20 each round the unit stays still until the unit counts as fortified. | D_908F/D_90B7 | D_F960[unit] = (old & $1F) / $20 | high |
| 554C | cmdDigOut | Handler of command $A0 (2 bytes: unit): setUnitDigBits with 0 (clears dig-in/dug-in bits) and clears the dug-in bit 6 of unitTypeTable. Hidden as data. | $908F | F960[unit] &= $1F, F76C[unit] &= $BF | high |
| 554C | cmdDigOut | Handler of command $A0 DIG OUT (2 bytes: unit): setUnitDigBits with 0 clears the dig progress bits 5-7 of unitStunTable, and bit 6 of unitTypeTable (the 'dug in / fortified' flag consulted by the combat code) is cleared as well. | D_908F/D_90B7 | D_F960[unit] &= $1F, D_F76C[unit] &= $BF | high |
| 555A | setUnitDigBits | Patches the ORA operand at $5568 with A, reads the unit argument and sets unitStunTable[unit] = (old & $1F) / A. Returns Y = unit. Hidden as data. | A = new bits 5-7 | F960[unit]; Y | high |
| 555A | setUnitDigBits | Shared prologue of cmdDigIn/cmdDigOut: patches the ORA operand at $5568 with A, reads the unit argument and falls into applyDigBitsToUnit. | A = new bits 5-7 ($20 or $00) | self-modifies $5568; zp_73 and Y = unit; D_F960[unit] | high |
| 5562 | applyDigBitsToUnit | Inner two-line worker shared by the single-unit and group dig handlers: unitStunTable[Y] = (old & $1F) / patchedBits, i.e. the low nibble stun counter and bit 4 survive while the dig-state bits 5-7 are replaced. | Y = unit index, patched ORA operand at $5568 | D_F960[Y] | high |
| 556D | cmdGroupDigIn | Handler of command $A1 (2 bytes: group id): every living member of the group gets unitStunTable bits 5-7 = 001 via the loop at $5573. Hidden as data. | $908F | F960 of members | high |
| 556D | cmdGroupDigIn | Handler of command $A1 (2 bytes: group id byte): patches the dig bits to $20 and runs the group loop at $5573. | D_908F/D_90B7 | D_F960 of every living member | high |
| 5571 | cmdGroupDigOut | Handler of command $A2 (2 bytes: group id): clears the dig bits of every living member (shared loop with cmdGroupDigIn, ORA operand patched at $5568). Hidden as data. | $908F | F960 of members | high |
| 5571 | cmdGroupDigOut | Handler of command $A2 (2 bytes: group id byte) and entry of the shared group dig loop: patches the dig bits to 0, reads the group byte, ORs $40 into it and walks units 99..0 calling applyDigBitsToUnit for every living unit whose unitGroupByte & $7C matches. Note that, unlike cmdDigOut, the group variant does not clear bit 6 of unitTypeTable, so a group dug in as a group can only be un-fortified unit by unit. | D_908F/D_90B7, D_F708, D_F76C | D_F960 of every member; self-modifies $5568 | high |
| 5596 | cmdSetFormation | Handler of command $A3 (3 bytes: group id, formation): reads the arguments and jumps to $4C84, which re-arranges the destinations of all group members around the group position according to the formation table at $4C7E. Hidden as data. | $908F | group destinations | medium |
| 5596 | cmdSetFormation | Handler of command $A3 (3 bytes: group id byte, formation index): reads the two arguments and jumps to rotateGroupFormation ($4C84), which re-arranges the destinations of all group members around the group centre using the offset table at $4C7E. | D_908F/D_90B7 | destinations of the group members | high |
| 559C | cmdSetGameType | Handler of command $A4 (2 bytes: options byte): gameTypeOptions $0BA3 = argument. Sent by overlay A ($833E) so both machines use the same game type/options. Hidden as data. | $908F | $0BA3 | high |
| 559C | cmdSetGameType | Handler of command $A4 (2 bytes): stores the argument into gameTypeOptions $0BA3 (bits 0-2 game type, bit 6 custom rules, bit 7 custom map). Sent by overlay A's runMapTypeMenu ($833E) so both machines agree on the scenario before the map is generated. | D_908F/D_90B7 | D_0BA3 | high |
| 55A5 | cmdSetupOption | Handler of command $A5 (3 bytes: kind, value), sent by overlay A's setup screens. kind 0: adds value*10 to the sender's side's 16-bit point total ($92A2/$92A4); kind 1: $92AC[other side] = value; kind >= 2: $92B4[0 own / 1 opponent] = value (the per-player setting that $34D5 compares with 4 to forbid moving the comcen). Hidden as data. | $908F, $0B9F, $76 | $92A2/$92A4, $92AC, $92B4 | medium |
| 55A5 | cmdSetupOption | Handler of command $A5 (3 bytes: kind, value), the generic setup-option carrier of overlay A's custom-rules editors. Kind 0 (HANDICAP, applyHandicapPoints $84AA) adds value*10 to the 16-bit counter $92A2/$92A4 indexed by the sender's absolute side; since scores are indexed by the side being scored against, this is a head start credited to the sender's opponent. The multiply clobbers X, so the index is restored from the operand patched at $55C2. Kind 1 (TERRAIN PTS, editTerrainPoints $84EC) stores the value in $92AC[senderSide EOR 1], the per-zone bonus later read at $47A9 when a unit penetrates the enemy half. Kind 2 and above (only kind 3 is ever sent - COMCEN SPEED, editComcenSpeed $85BC) stores the value in $92B4 indexed 0 = own / 1 = opponent; value 4 there is what makes $34E1 refuse to move a COMCEN at all. | D_908F/D_90B7, D_0B9F, zp_76 | D_92A2/D_92A4, D_92AC, D_92B4; self-modifies $55C2 | high |
| 55EC | queueCmdBytePending | Queues the single command byte in A (1-byte commands such as $92/$97/$9E/$8F) and increments pendingOwnCmdCount ($929F). | A | ring buffer, $929F | high |
| 55EF | queueCmdByte | Appends A to the 40-byte outgoing command ring ($9065, write index $908D, wraps at 40) and writes an $FF end marker after it. Does nothing during film playback ($0B9B bit 7). | A, $908D, $0B9B | $9065[], $908D | high |
| 560A | queueCmd4 | Queues a 4-byte command: A, cmdParam0, cmdParam1, cmdParam2 (e.g. $80 move, $81, $98, $8D) and counts it once in pendingOwnCmdCount. | A = code, $73/$74/$75 | ring buffer, $929F | high |
| 560F | queueCmd3 | Queues a 3-byte command: A, cmdParam1, cmdParam2 (e.g. $87, $88, $90, $8E, $A3, $A5) and increments pendingOwnCmdCount. | A = code, $74/$75 | ring buffer, $929F | high |
| 561F | queueCmd2 | Queues a 2-byte command: A, cmdParam0 (unit/group/reason commands $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2, $A4) and increments pendingOwnCmdCount. | A = code, $73 | ring buffer, $929F | high |
| 562B | sendOutgoingPacket | Builds the next packet for the opponent and hands it to the comm module. If the drone is under local control ($920D bit 6, not paused, droneFlags bit 7 clear) first steers it (overlay B $731E/$81FE) and queues $8D heading,col,row. Then copies whole commands from the ring (read index $908E) into the packet buffer $E027 as long as the total stays below 5 bytes; a $92 (drone arrived) is dropped while droneFlags bit 6 is set, an $8E (detonate) found while $920D bit 7 is clear flushes the whole ring (read index = write index) and sends an empty packet. Sets $E01F = packet length and calls sub_E000 with X=1 (transmit). Called after every merged exchange ($4F02) and by disconnectFromOpponent and overlay B. | $9065/$908D/$908E, $920D, $92F9, $BE | $E027.., $E01F, $908E, $562A | high |
| 56B1 | pollMenuSession | Jump target of pollOpponentLink while commSessionState ($0B9C) bit 7 is set (link up but no game running): if the carrier ($E03C bit 6) is gone -> disconnectFromOpponent, else handleMenuSessionPacket. | $E03C | - | high |
| 56BB | handleMenuSessionPacket | Menu-level receive: sets linkPollDelay $9163 = 10 and returns unless a packet was received ($E01D bit 7). A 2-byte [$8C,05] means the opponent disconnected -> hangUpModem; [$8C,02] shows 'OPPONENT READY FOR NEW GAME.' (rate limited by frameCounterMid $46 >= 3). Then completeExchange. | $E01D, $E01E, $E020/$E021, $46 | message $1F, falls into completeExchange | high |
| 56F1 | completeExchange | Makes sure the current packet exchange finishes: nothing pending -> return; exchange complete (bit 6) -> consume it (sub_E000 X=2); a send already requested or in progress (bits 0/5) -> wait; otherwise sends an empty packet ($E01F=0, A = $0B9C, X=1). Waits up to $4B ticks (zp_7F) for completion, then consumes the packet; on timeout hangs up the modem. | $E01D, $0B9C | $E01F, $7F, comm module state | high |
| 572B | readyForNewGameHandshake | Before a new game: completeExchange, persistent message $0B 'WAITING FOR OPPONENT...', then repeatedly sends [$8C,02] (buildReadyPacket) and waits for the exchange to complete. When the opponent's packet is also [$8C,02]: commSessionState = 2 (game session), message cleared, packet consumed and the screen redrawn ($C39F). [$8C,05] -> hangUpModem. Anything else is consumed and the READY packet resent. Called from overlay A ($795A). | $E01D/$E01E/$E020/$E021 | $0B9C = 2, $92A1 | high |
| 577C | buildReadyPacket | Fills the outgoing packet buffer with $8C,$02 and sets $E01F = 2. | - | $E027/$E028/$E01F | high |
| 578A | recordExchangeToFilm | Appends the merged command buffer of one exchange to the game film in RAM under I/O: skipped if empty, not in the battle phase ($B1 != 0) or when filmPtr has reached filmEndPtr. Record = [ (splitIndex<<4) / length ] [ timestamp, 1 or 2 bytes ] [ length command bytes ], written with writeFilmByte; finally advanceFilmPtr. | $908F, $92EB, $92EC, $BC/$BD, $92F1/$92F2 | film bytes, $BC/$BD | high |
| 57BE | writeFilmTimestamp | Writes the time stamp of a film record at ($BC),Y: delta = gameClock ($91CB) - previous clock - 1 (previous value kept in the operand at $57C3); if the clock moved by 0..-14 a single byte (delta<<4 / exchangeCounter $923C) is written, otherwise two bytes: exchangeCounter then the absolute clock. Updates the previous-clock operand. | Y = offset, $91CB, $923C | 1-2 film bytes, Y advanced | high |
| 57E7 | advanceFilmPtr | Adds Y to the 16-bit filmPtr ($BC/$BD). | Y | $BC/$BD | high |
| 57F2 | finalizeFilm | Called from endGameToMainMenu: if there is room, writes the end-of-film marker ($FF when gameEndReason bit 0 is set = aborted game, $FE = game reached its own end) plus a time stamp; then saveFilmEndAndRewind, writes the header: film length (end - $D000) at $D000/1, setupByte0B94 at $D02F, gameClock at $D031, both 16-bit scores at $D00D-$D010, the checksum at $D033, and restores filmPtr to the end. | $0BA8, $BC/$BD, $91CB, $92A2-$92A5, $0B94, $0B9F | film header $D000-$D033, $92F1/$92F2 | high |
| 585C | setFilmPtrToEnd | filmPtr ($BC/$BD) = filmEndPtr ($92F1/$92F2). | $92F1/$92F2 | $BC/$BD | high |
| 5867 | saveFilmEndAndRewind | filmEndPtr = filmPtr, then filmPtr = $D000 (rewindFilmPtr). | $BC/$BD | $92F1/$92F2, $BC/$BD | high |
| 5874 | readFilmByte | Reads the byte at (filmPtr),Y from the RAM hidden under the I/O area ($D000-$DFFF): disables CIA2 interrupts, sets $01 = $34, reads, restores $01 = $35 and the CIA2 ICR mask from the comm module's $E033. X preserved (self-modified at $5891). Also used by overlay A and the comm module. | Y = offset, $BC/$BD | A = byte | high |
| 5893 | writeFilmByte | Stores A at (filmPtr),Y in the RAM under I/O with the same interrupt/banking protocol as readFilmByte. X preserved. | A, Y, $BC/$BD | RAM under I/O | high |
| 58B2 | copyPageUnderIo | Copies one 256-byte page with the I/O area banked out; source and destination are the self-modified operands at $58C3/$58C4 and $58C6/$58C7 (patched by writeFilmSetupSnapshot, overlay A and the comm module). Used to snapshot the unit tables into the film header and to restore them. | patched operands | 256 bytes copied | high |
| 58D5 | rewindFilmPtr | filmPtr ($BC/$BD) = $D000, the start of the film/saved-game buffer. | - | $BC/$BD | high |
| 58DE | verifyFilmHeader | Checks a loaded film: recomputes the header checksum (computeFilmHeaderChecksum) and compares it with $D033, and compares $D02F with setupByte0B94. C=0 and A = $D004 when valid, C=1 otherwise (caller shows 'NOT A GAME FILM!'). Called from overlay A $81F4. | film header | C, A = $D004 | high |
| 5900 | computeFilmHeaderChecksum | Sums the 51 header bytes $D000-$D032 with carry chaining into zp_18 (rewindFilmPtr first). | film header | $18 = checksum, $BC/$BD = $D000 | high |
| 5915 | waitForExchangeIdle | Calls pollOpponentLink until linkTimeoutCounter ($9223) is zero, i.e. until the pending packet exchange has completed. Used by overlay A after sending setup commands. | $9223 | - | high |
| 591E | compareFilmPtrToEnd | Compares filmPtr with filmEndPtr: C=1 when filmPtr >= filmEndPtr (buffer full while recording, end reached while playing). | $BC/$BD, $92F1/$92F2 | C | high |
| 5929 | writeFilmSetupSnapshot | Writes the game-start snapshot into the film header: the 20 setup bytes $92A2-$92B5 (scores, recycler settings, drone/missile counts, ...) to $D034-$D047, then the first eight unit arrays $F640-$F95F (col,row,flags,type,dest,waypoint) to $D048-$D367 with copyPageUnderIo, and the game clock to $D030. Called from overlay A ($7B80) when a game starts. | $92A2.., $F640.., $91CB | $D030-$D367 | high |
| 5983 | getFilmSnapshotDestAddr | Returns A=$68, X=$D2: the address $D268 used as last page destination of the unit snapshot (also called by the comm module / $EE00 overlay when restoring). | - | A/X | medium |
| 5988 | getFilmSnapshotSrcAddr | Returns A=$60, X=$F8: the address $F860 (last page source of the unit snapshot). | - | A/X | medium |
| 598D | setFilmLimitToBufferEnd | filmEndPtr = $DFF0, the recording limit of the film buffer. Called from overlay A $78D1 when recording starts. | - | $92F1/$92F2 | high |
| 5998 | initFilmStream | Writes the command-stream start offset $0368 to $D002/$D003 and sets filmPtr = $D368 (first byte after the snapshot). Called from overlay A $7B8C. | - | $D002/$D003, $BC/$BD | high |
| 59B3 | isCmdRingNearlyFull | Computes the free space of the outgoing command ring ((readIdx - writeIdx) mod 40) and returns C=1 when it is below 5 bytes (the caller checkFirePressed then refuses new orders). Equal indices mean empty -> C=0. | $908D, $908E | C | high |
| 59D0 | finishFilmPlayback | Reached when the $FF end marker of an aborted-game film is read: rewinds, restores gameClock from $D031 and the two 16-bit scores from $D00D-$D010, increments gameEndReason (game over), restores filmPtr, queues message 2, sets $923F = $C0, clears the status line ($92A1/$0548 = $9E) and starts gameEndCountdown at $7D. | film header, $0B9F | $91CB, $92A2-$92A5, $0BA8, $923F, $92A1, $0548, $92C9 | high |
| 5A14 | checkLinkTimeout | Link watchdog while waiting for the opponent's packet: sets bit 7 of linkTimeoutCounter ($9223, advanced every 8 frames by the IRQ). Below $E1 returns C=0; from $E1 shows 'PHONE TROUBLE.' (unless sound $15 is playing) and returns C=1; at $FF: in battle phase voicePauseReconnect and restart, in setup phases commSessionState = $FF, disconnect, 'TRY AGAIN.', prevLinkTypeFlag = $C8 and jump to returnToMainMenu. | $9223, $B1, $67C8 | C, $9223, $0B9C, $0BA6 | high |
| 5AD7 | drawOverviewMap | Draws the strategic overview (mini-map) in the 20x20 character window at screen (1,1)-(20,20): background colour 7, then for each 2x2 block of map cells builds one 8-byte character (four 2x2-pixel quadrants via buildOverviewQuadrant), adds the quarter lines, writes it to the bitmap, colour $62 into screen RAM and the terrain colour ($32) into colour RAM (+$4C00). Map pointer $48/$49 walks $F000 by 2 cells and skips every other row. | $F000 map, unit tables | bitmap/screen/colour RAM, $9004 = 7 | high |
| 5B6C | drawOverviewQuarterLine | Overlays the field lines on the top scanline of an overview character: at character row 11 (map row 20, midfield) the 2nd and 4th pixel, at rows 6 and 16 (map rows 10/30) only the 4th pixel are set to colour 1 unless the pixel pair is colour 2/2 ($A). Works on the 8-byte buffer $90B8. | Y = character row, $90B8 | $90B8[0] | medium |
| 5BAD | buildOverviewQuadrant | Builds quadrant X (0 top-left,1 top-right,2 bottom-left,3 bottom-right) of an overview character for map cell A: looks up the terrain colour (D_07EA[D_07BE[idx]] -> $32) then falls into buildOverviewQuadrantNoColour. Terrain index = cell-$20 for $20-$3F, cell-$40 for $41-$60, $40 = blank. | A = map cell, X = quadrant, Y preserved | $32, $90B8 bytes | high |
| 5BD4 | buildOverviewQuadrantNoColour | ORs the 4-scanline pattern for map cell A into quadrant X of $90B8 (mask from quadrantPixelMask, rows from quadrantRowOffset). Units ($80/idx): hidden unless own side (patched BCC/BCS at $5BF7/$5BFE select the side), replay view >= $82, revealAllFlag or the unit's spotted bit (F9C4 bit 7); pattern $5AB7, or $5ABF when cloaked, masked with overviewUnitMask $922B (complemented for the other side); dead units and $9242 bit 7 draw the terrain underneath (FA70). Terrain uses the pattern pointed to by overviewPatternLo/Hi. | A = cell, X = quadrant, Y = return value | $90B8, X/Y restored | high |
| 5C7C | drawMapCell | Redraws one map cell everywhere it is visible: A = cell byte, X = column, Y = row (saved in $54-$56). Skipped during the knock-out sequence ($922A bit 6). On the battlefield screen: if the cell lies inside the 7x5 viewport -> drawViewportCell; then the overview pixel quadrant is rewritten (clear quadrant mask, buildOverviewQuadrantNoColour, quarter lines). On screen 1 -> drawScreen1Cell. Called from $3F89, $437D, $48AE (unit moves/redraws). | A/X/Y, $90FB, $A5/$A6 | bitmap updates; A/X/Y restored | high |
| 5D1A | drawViewportCell | Draws map cell ($54,$55,$56) into the 7x5 battlefield viewport: screen character = ((col-viewX)*2+25, (row-viewY)*2+1), temporarily sets viewOrigin to the cell, renderMapTile builds the 2x2-character tile in tileBuffer ($04C8) and the 32 bytes are copied to the bitmap (second character row at +$140). Also used by scrollViewBitmap ($1F1B/$1F44). | $54/$55/$56, $A5/$A6 | bitmap, $90F6/$90F7 scratch | high |
| 5D7F | drawScreen1Cell | Redraws map cell ($54,$55,$56) on overlay-B screen 1 (the 21x17-cell map window at character (3,3), one character per cell): a unit equal to comcenUnit is handed to overlay B $7959; otherwise, if the cell is within 21 columns/17 rows of the view origin, computes the bitmap pointer (row/col tables +3) and the colour-RAM pointer, sets $9237/$9239 = $0B and $9238 = 1 and calls overlay B $74C3 and $7396 with viewOrigin temporarily set to the cell. | $54-$56, $A5/$A6, $9236 | bitmap/colour RAM via overlay B, $9237-$9239 | medium |
| 5E03 | clearOverviewWindow | Sets the text window to left 1, top 1, width 20, bottom 21 (the overview area) and clears it with $C06A. Called at start-up and by clearCurrentScreenWindows. | - | $39-$3C, window cleared | high |
| 5E17 | handleOwnUnitClick | Called from the battlefield main loop ($20CC) when the player clicks on one of his own, unstunned units (index in D_90F8). A unit that belongs to a group (FB54 bit 6) is routed to runGroupOptionsMenu ($61CF); otherwise it highlights the unit (D_9204), sets uiLevel 1, the square cursor, clears the info panel in colour 5 and shows the info panel (sub_22A2, which waits until the player moves the stick or clicks again). A deflected stick then starts SETTING DESTINATION (sub_6500); a second click (checkFirePressed) opens the unit options menu (L_5E99) only when gameTypeOptions bit 3 is set (game types 3-6). Dead units exit at once. | D_90F8 unit, D_FB54/D_F76C/D_F898/D_F8FC/D_F960 unit arrays, zp_5D joystick, D_0BA3 bit 3, zp_B1, D_9236 | D_9204/D_9205/D_9209/D_612E/D_91D5 set, viewport redrawn, info panel used, commands queued via sub_561F, falls into exitUnitMenu (D_9209 = 0, D_91D7 = $FF) | high |
| 5E24 | runUnitOptionsMenu | Single-unit branch of handleOwnUnitClick, also entered by JMP from the group menu item DIRECT UNIT ($62F6). After the info panel wait and the second click it builds the UNIT OPTIONS menu at rows 15+: CLOAKING ON/NO CLOAKING (F898 bit 6), DIG OUT/DIG IN when the unit is stationary (FB54 bit 7; DIG IN only if no dig is in progress, F960 bits 5-7) else SET BLITZ/BLITZ OFF (F8FC bit 6), JOIN GROUP unless the unit is the comcen, CLEAR TARGET/SET TARGET for a boomer during battle (sub_3C74), and (EXIT). Runs runMenuLoopWatchUnit and acts on the returned item id: 2/3 toggle F898 bit 6 and send $82/$84; 4 sends $86; 5 prints SETTING/TARGET and runs the target cursor (zp_BB = 5, sub_2185); 7 runs runJoinGroupMenu; 10/11 toggle F8FC bit 6 and send $93/$95; 17/18 send $A0/$9F. Y = $80 from the loop (idle flag changed) rebuilds the menu. | D_90F8, unit arrays, D_0BA3 bit 3, zp_5D, D_9236, zp_B1 | commands queued (zp_73 = unit), D_F898/D_F8FC bit 6 toggled locally, D_612E = $FF, D_9209 = 1, menu state D_91CE/D_91D4/D_91D5/D_91D6, L_602A+1 patched with FB54 / 1 | high |
| 5FA6 | exitUnitMenu | Shared tail of all unit/group menu paths: uiLevel D_9209 = 0 and info-panel unit D_91D7 = $FF, then returns to the battlefield loop. | - | D_9209 = 0, D_91D7 = $FF | high |
| 5FB1 | addMenuItem | Appends item id X to the menu list D_91CE[D_91D4++] and prints the item name on the current text line. Ids 0-11 use the 16-byte strings at $C9D2 (sub_C100 with X = id*2), ids 12-18 the 8-byte strings at $CAB2 (X = id - 12); each string ends with a CR so the cursor moves to the next row. | X = item id (0 FORMATION, 1 MOVE TO GOAL, 2 CLOAKING ON, 3 NO CLOAKING, 4 CLEAR TARGET, 5 SET TARGET, 6 MEMBERSHIP, 7 JOIN GROUP, 8 DIRECT UNIT, 9 SELF DESTRCT, 10 SET BLITZ, 11 BLITZ OFF, 12 (EXIT), 13 ENERGY, 14 BATTLE, 15 RADAR, 16 DRONE, 17 DIG OUT, 18 DIG IN), D_91D4, text cursor | D_91CE[n] = id, D_91D4 incremented, text printed; A/X/Y clobbered | high |
| 5FD1 | runMenuLoopWithFlag | Entry used by the repair/misc menus and the overlays ($1C59, $7F40, $835D, $83CE): with C = 1 it behaves like runMenuLoop; with C = 0 the slow-down row operand at L_60BA+1 is set to $FF so there is no extra delay before the last item. | C flag, menu state D_91D4/D_91D5/D_91D6 | see runMenuLoopWatchUnit | medium |
| 5FD7 | runMenuLoop | Menu selection loop without the unit idle-flag watch (patches the operand at L_602A+1 to 0) and with the slow-down row = count - 2; falls into runMenuLoopWatchUnit. Used by the join-group, repair and formation menus. | menu state D_91D4/D_91D5/D_91D6, D_90F8, D_612E | A = D_91CE[D_91D5] item id, Y = D_91D5 selection | high |
| 5FDC | runMenuLoopWatchUnit | Generic vertical menu selector used by every in-game option menu. Sets a 20-frame input lockout (D_0B7D, zp_6C), highlights the current row and loops: steps the RNG, runs the per-tick game update (sub_40B2 in battle, sub_8658/sub_EEB9/sub_4DA9 in setup or film phases), keeps the info-panel unit, exits with the last item selected if the unit died, the game ended or sub_C451 reports an abort, returns A = Y = $80 if the unit's idle flag (FB54 bit 7) differs from the snapshot in L_602A+1, updates the map highlight according to D_612E (negative/odd: animate the path marker; else D_9206 = selection*4 / D_612E is the candidate group key and the viewport is redrawn when it changes), blinks the selected row every 20 frames (zp_6E), and handles the joystick: up/down move the selection (no wrap, sound 2, auto-repeat via sub_0D49, 30-frame pause before entering the last row), fire (checkFirePressed) confirms. On exit the selected row is left highlighted. | D_91D4 count, D_91D5 selection, D_91D6 first row, D_612E mode, D_90F8, zp_5D, zp_6C, zp_6E, zp_B1, D_92C9, D_0BA8, L_602A+1 idle snapshot, L_60BA+1 slow row | A = D_91CE[D_91D5] item id, Y = D_91D5 (or A = Y = $80 when the idle flag changed), D_91D5 updated, D_9206/D_9205 updated in group-pick mode, D_92C5, D_0B7D, zp_6C, L_60BE+1 patched with the move direction | high |
| 60E5 | unhighlightMenuRow | Removes the highlight from the selected menu row if it is currently highlighted (D_92C5 == 0); otherwise returns. Falls into toggleMenuRowHighlight. | D_92C5, D_91D5, D_91D6, zp_3C, zp_3D | row colours restored, D_92C5 = 1 | high |
| 60EA | toggleMenuRowHighlight | Toggles the highlight of menu row D_91D6 + D_91D5 by EORing the screen-RAM colour byte of each of the zp_3C cells with the text colour zp_3D, flips D_92C5 and restarts the 20-frame blink timer zp_6E. Special case for the REPAIR menu (D_90F8 == $FF, selection < 4): if the comcen is stunned (D_922A bit 7) it discards its return address and jumps to L_607E so the menu loop exits with the last item; otherwise it blinks the comcen system marker (sub_188B). | D_91D5, D_91D6, zp_3C, zp_3D, D_90F8, D_922A, D_92C5 | zp_6E = 20, colour bytes toggled, D_92C5 ^= 1, zp_B6/zp_B7 and text cursor changed | high |
| 612F | runJoinGroupMenu | JOIN GROUP: lists the seven groups ALPHA..GROVER (8-byte names at $C92C) with their current member counts (countGroupMembers) and (EXIT) from row 13, preselecting the unit's current group number (FB54 bits 2-4). While the selection moves the menu loop highlights the candidate group on the viewport (D_612E = side bit). Choosing a group calls sub_4AA3, which sends command $88 and returns C = 0 when the unit joined; if the unit is too far from the group (C = 1, TOO FAR! shown) the list is redrawn and the choice repeated. | D_90F8, D_FB54, D_F76C | D_9209 = 2, D_91D4 = 8, D_91D5/D_91D6, D_612E = FB54 & $20, D_9207 = $FF (dead), D_9206 chosen group key, command $88 queued | high |
| 61CF | runGroupOptionsMenu | Group branch of handleOwnUnitClick for a unit that is a group member: D_9206/D_9205 = group key (FB54 & $3C), uiLevel 2, members highlighted in the viewport, header GROUP + group name. Loops with the path-marker animation until the stick moves (after waiting for pending repositions, sub_65E7, the whole group gets SETTING DESTINATION via sub_6500) or a click opens the GROUP OPTIONS menu (gameTypeOptions bit 3 required): CLOAKING ON/NO CLOAKING, DIG OUT/DIG IN or SET BLITZ/BLITZ OFF, MEMBERSHIP, FORMATION, DIRECT UNIT, (EXIT). Actions: 2/3 toggle F898 bit 6 and send $83/$85 with the group key; 6 runs runGroupEditor in membership mode (zp_BB = 0); 0 runs the formation sub-menu sub_65FF and, for CUSTOM (Y = 3), runGroupEditor with zp_BB = $80; 8 jumps to runUnitOptionsMenu; 10/11 toggle F8FC bit 6 and send $94/$96; 17/18 send $A2/$A1. Ends with clearGroupHighlight and exitUnitMenu. | D_90F8, D_FB54, D_F898, D_F8FC, D_F960, zp_5D, D_0BA3 bit 3 | D_9206, D_9205, D_9204 = $FF, D_9209 = 2, D_612E = 1, commands queued with zp_73 = group key, menu state | high |
| 633C | printGroupTitleAndName | Prints GROUP (printGroupTitle) and, on the next row at column 4, the name of group (D_9206 >> 2) & 7 from the table at $C92C, followed by a newline. Used as the header of the group menu. | D_9206 | text printed, zp_3F = 4 | high |
| 6358 | printGroupTitle | Sets the text column to 4 and prints the string GROUP ($CB1F, ends with CR). | text window | text printed, zp_3F = 4 | high |
| 6366 | clearGroupHighlight | Clears the viewport group highlight (D_9205 = $FF) and redraws the viewport (sub_26C1). | - | D_9205 = $FF, viewport redrawn | high |
| 636E | printSettingHeader | Clears the info panel in colour 4, positions at row 15 and prints SETTING centred followed by MEMBERSHIP ($CA32) when zp_BB >= 0 or FORMATION ($C9D2) when zp_BB is negative. Header of the group editor. | zp_BB | zp_3D = zp_42 = 4, text printed | high |
| 6387 | printFormationCentred | Prints the word FORMATION ($C9D2) centred in the text window (sub_C121). Also called by the formation sub-menu sub_65FF. | text window | text printed | high |
| 638E | runGroupEditor | Interactive editor for the group in D_9206, in MEMBERSHIP mode (zp_BB >= 0) or custom FORMATION mode (zp_BB < 0). Halts the group (sendHaltGroupCmd $9B; formation mode first waits for pending repositions), then lets the player scroll the viewport (joystick auto-repeat, sub_20F4) with hover info on own units after 30 idle frames. A click on an own unit: not a member -> membership mode adds it (sub_4AA3: ADDED or TOO FAR!), formation mode refuses with sound 3 and NOT IN GROUP; a member -> membership mode removes it (sub_4AF2, REMOVED), formation mode asks for a NEW POSITION (sub_4BD3, command $98); the comcen only gives sound 3. A click on an empty cell or enemy prints FINISHED and ends the editor. sub_C451 aborts at any time. Ends with clearGroupHighlight and exitUnitMenu. | D_9206 group key, zp_BB mode, zp_5D, zp_6C, D_90EE, D_9236, D_FB54, D_63E2 own-side branch | D_90F8 = unit under the cursor (bit 7 set = none/enemy), D_9209 = 2, D_91D7 = $FF, D_9207 = $FF (dead), D_9205, D_91C9/D_91CA, D_0B7D, commands $9B/$88/$89/$98 queued, viewport scrolled/redrawn | high |
| 64DA | sendHaltGroupCmd | Queues command $9B with the group key D_9206 as parameter; the handler ($549E) stops every member of the group (destination = position, idle bit set, cloak/blitz orders cleared). | D_9206 | zp_73 = D_9206, two bytes queued via sub_561F | high |
| 64E4 | printSettingHeaderForLevel | Clears the info panel using the uiLevel colour from uiLevelTextColourTable[D_9209], moves to row 15 and prints SETTING centred plus a newline (falls into printSettingWord). The caller prints the object (TARGET, DESTINATION, NEW POSITION) next. | D_9209 | zp_3D = zp_42 = colour, text printed | high |
| 64F6 | printSettingWord | Prints SETTING ($CB4F) centred in the text window followed by a newline. | text window | text printed | high |
| 6500 | setUnitDestination | SETTING / DESTINATION: prints the header (the string following SETTING, DESTINATION at $CB56, centred via sub_C12B), sets zp_BB = 0 and runs the target cursor loop sub_2185, which issues the move order for the selected unit or, with uiLevel 2, the whole group. Entered when the player moves the stick after selecting a unit or group. | D_90F8, D_9209, D_9206 | zp_BB = 0, move order queued by sub_2185 | high |
| 650E | countGroupMembers | Counts the live units (F76C bit 7 clear) whose FB54 & $7C equals the group key in A with the membership bit added (A / $40). Used to show the member count next to each group name in the join-group list. | A = group key (group number * 4 / side bit $20) | A = zp_18 = count, zp_19 = key / $40, Y = $FF | high |
| 652E | runRepairWhichMenu | REPAIR tab menu (the disassembly shows it as data; it is JSR'd from $1860 inside repairTabAction, itself mis-typed). Opens a 7-column text window at column 3, rows 9-19 (colour $61), prints 'REPAIR\rWHICH:\r\r', appends menu items 13-16 (ENERGY, BATTLE, RADAR, DRONE) and 12 ((EXIT)) with addMenuItem, then runs the menu selector with selectedUnit=$FF (repair-menu mode, which makes the highlight blink the comcen system marker). Index 4 = (EXIT) clears the window with colour $66 and returns; otherwise, unless film playback is active or D_923F bit 6 is set (getSoloOrFlag923F), it calls attemptSystemRepair for system D_91D5, removes the highlight and re-runs the menu. | D_0B9B/D_923F via sub_0EE5, D_91D5 after the selector, comcen status via attemptSystemRepair | zp_39-zp_3D window/colour, zp_43=0, D_90EF=0, zp_3E=0, D_91D4/D_91D5/D_91D6 menu state, D_90F8=$FF, zp_1C loop counter; window cleared on exit | high |
| 652E | runRepairWhichMenu | Draws and runs the 'REPAIR WHICH:' comcen-system menu of the REPAIR console tab. Opens a 7-column text window at column 3 spanning rows 9..19 (colour $61, fill char 0, centring and raw-code modes off), prints the header string 'REPAIR<CR>WHICH:<CR><CR>' at $CBD6, records the resulting cursor row as menuFirstRow ($91D6 = 13), appends menu items 13..16 (ENERGY, BATTLE, RADAR, DRONE) and item 12 ((EXIT)) with addMenuItem, then repeatedly runs the menu selector with selectedUnit = $FF (the sentinel that makes toggleMenuRowHighlight blink the matching comcen system marker). Called only from $1860 inside repairTabAction, which has just erased the four markers and set showOkMarkers = 1 so undamaged systems are shown too. | none in registers; reads D_0B9B (film/solo flag) and D_923F (game-over flags) through getSoloOrFlag923F, D_91D5 after each selector pass, comcen state via attemptSystemRepair, text cursor row zp_40 after the header | zp_39=$09, zp_3A=$13, zp_3B=$03, zp_3C=$07, zp_3D=$61 then $60 then $66, zp_3E=0, zp_43=0, D_90EF=0, D_91D4=5 (items), D_91D5=selection, D_91D6=first row 13, D_91CE[0..4]={13,14,15,16,12}, D_90F8=$FF, zp_1C=item-id loop counter; text window painted and, on exit, cleared with colour $66 via a JMP to sub_C06A; may queue the $9A energy-repair command and clear D_9241[y] through attemptSystemRepair | high |
| 65A0 | runInGameOptionsMenu | Runs the MISC-tab options menu while a game is in progress (items printed by printOptionsList: ABORT GAME, optional VOICE PAUSE, (EXIT)); called from miscTabAction ($1A78). Sets selectedUnit=$FE (plain menu) and colour $60, runs the selector and returns (after removing the highlight) for any index other than 1. Index 1 = VOICE PAUSE, valid only when not in film/solo mode and the comm build flag D_0BA5 is clear: if the carrier is up (D_E03C bit 6) it sends command $8C with reason 5 to the opponent, sets D_91D5=$81 and spins in serviceFrame until bit 7 is cleared by voicePauseReconnect (invoked from the $C000 overlay at $C768 when zp_BE flags the pause); otherwise it calls voicePauseReconnect directly. Then it restarts the menu. | D_91D4 item count and D_91D6 first row prepared by the caller, D_0B9B, D_0BA5, D_E03C, zp_73 | Y = D_91D5 selected index (0 = ABORT GAME, last = (EXIT)), D_90F8=$FE, zp_3D=$60, command $8C/5 queued, highlight removed | high |
| 65E7 | waitForGroupRepositionsDone | If the selected group id D_9206 equals lastRepositionedGroup D_929D, loops while pendingRepositionCount D_929E is nonzero, calling serviceFrame and checkUiContinue each pass. When checkUiContinue returns 0 (game over / screen switch) it pops the return address and returns to the caller's caller, abandoning the group operation. Used by the group (GROUP/FORMATION) screens at $621D and $63B7 so they operate on settled unit positions. | D_9206, D_929D, D_929E | none (may unwind one stack level) | medium |
| 65FF | runFormationOptionsMenu | Clears the info panel, prints the centred 'FORMATION' title, '   OPTIONS\r\r' ($CB13) and the item list 'SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT)' ($CA8C), sets item count 5 and runs the menu selector. For indices 0-2 it sends command $A3 with cmdParam1 = selectedUnit and cmdParam2 = the chosen option (sub_560F). Returns Y = selected index (3 = CUSTOM, 4 = (EXIT)). Called from $62FD. | D_90F8 selected unit (or group leader) | Y = D_91D5, zp_74/zp_75 command parameters, command $A3 queued, D_91D4=5 | high |
| 6637 | soundPlayerTick | Per-frame SID sound driver, called from the raster IRQ at $128C (the IRQ first dequeues a sound with $C8E9 and calls startSound at $1289). Returns immediately while sndInitLock ($67C5) is nonzero. Writes sndVolume ($67C2) to SID_VOLUME and, if nonzero, updates voices 0 and 1 through updateSoundVoice and falls into it for voice 2 (X = 0, 2, 4). The whole $6637-$6781 block is shown as .byte in the disassembly but is code. | $67C5, $67C2, per-voice state at $6792-$67C1, script pointers zp_D8-zp_DD | SID registers $D400-$D418, voice state, $67C8-$67CA | high |
| 6651 | updateSoundVoice | Advances one sound voice (X = voice*2). Does nothing while the voice duration counter ($6792,x) is 0 (idle). Adds the 16-bit frequency delta ($67B6/$67B7,x) to the frequency ($67A4/$67A5,x) and the pulse delta ($67BC/$67BD,x) to the pulse width ($67AA/$67AB,x), decrements the duration and when it reaches 0 runs the voice script (runSoundScript); if the script ended (duration still 0) the voice's sound id ($67C8 + voice) is cleared. Finally writes frequency, pulse width, AD, SR and the control byte ($6793,x) to the SID voice at offset sidVoiceRegOffsetTable[x]. | X = 0/2/4, voice state arrays $6792-$67C1 | SID voice registers, $67C8+voice cleared on end, X preserved | high |
| 66C7 | runSoundScript | Interprets the voice's script bytecode (pointer zp_D8/zp_D9 indexed by X) until a 'set duration' command. Byte formats: $80-$FF = add zp_5A (scenario RNG byte, used as jitter) to the frequency low byte; $40-$7F = loop: the low 6 bits select a counter slot ($6792+slot+x), followed by a 16-bit target address - if the counter is 0 the jump is always taken, otherwise it is decremented and taken while nonzero ($46 = counter A set by command 2, $47 = counter B set by command 3); $00-$0A = store the following byte(s) into the state slot given by sndCommandSlotTable: 0 duration (ends processing), 1 control/waveform, 2 loop counter A, 3 loop counter B, 4/5 spare slots, 6 frequency (2 bytes), 7 pulse width (2), 8 AD/SR (2), 9 frequency delta (2), 10 pulse delta (2). A duration of 0 ends the voice. $66BF is the bit-7 jitter handler that falls back into this loop. | X = voice*2, script at (zp_D8,x), $67C3 current byte | voice state $6792-$67C1 updated, script pointer advanced, $67C4 scratch | high |
| 671C | fetchSoundScriptByte | Reads the next byte of the voice script through the zero-page pointer (zp_D8,x), stores it in sndLastByte ($67C3) for the BIT test and post-increments the 16-bit pointer. | X = voice*2, zp_D8/zp_D9 + X | A = byte, $67C3 = byte, pointer incremented | high |
| 6728 | startSound | Starts sound A (0-28) on up to three voices; called from the IRQ ($1289) with the id dequeued by $C8E9. Sets sndInitLock=3 (loop counter, also blocks soundPlayerTick), sndVolume=$0C, sndCurrentSound=A, indexes soundPointerTable by A*6 and for every voice whose pointer high byte is nonzero: copies the pointer to zp_D8/zp_D9 (+voice*2), zeroes the eight 6-byte state rows for that voice ($6792-$67C1 stepping by 6), records A in sndVoiceSoundId[voice] and sets the duration to 2 so the script starts on the second tick. Voices with a zero pointer are left untouched (still playing their previous sound). | A = sound id, soundPointerTable $6831 | $67C2-$67C7, $67C8-$67CA, zp_D8-zp_DD, voice state; X/Y clobbered | high |
| 67CB | unpackRleTo0200 | Run-length decoder: unpacks the block at A/Y into $0200-$03FF (512 bytes, zp_B2/zp_B3 destination, zp_1A page counter). The first byte of the block is the escape marker (saved in D_9246; D_9247 is cleared); in the stream 'marker, count, value' emits value count times, any other byte is literal. The source pointer is self-modified into fetchRleByte. Callers: overlay B ($7596 with the block at $FD70, $7FF0 with $FE24) and the $C000 overlay sub_CD0A (five blocks at $CD52/$CDD3/$CE3B/$CECD/$CF3A), which then copy the unpacked page elsewhere. | A/Y = source block address | $0200-$03FF filled, D_9246 = marker, D_9247 = 0, zp_B2/zp_B3/zp_1A/zp_1C clobbered, sub_681A operand patched | high |
| 681A | fetchRleByte | Returns the next byte of the RLE stream from the self-modified absolute address at $681B/$681C and increments that address. Shared with the overlay-B bitmap unpacker at $8644-$868B (which also compares against D_9246). | patched operand $681B/$681C | A = X = byte, operand incremented | high |

Data:

- `$0803` **loaderStatus** (byteTable, 1 bytes): bit 7 = a read error occurred
- `$08E8` **sendByteShift** (byteTable, 1 bytes): shift register for the byte being sent
- `$08E9` **recvByteShift** (byteTable, 2 bytes): shift register for the byte received during a send; $08EA unused
- `$0993` **strBlockExecuteLeftover** (text, 15 bytes): 'B-E 2 0 1 17',0,'#',0 - leftover drive command strings from the shared EA loader, unused by the game
- `$09A2` **loadDestLo** (byteTable, 1 bytes): transfer address low byte (self-modified)
- `$09A3` **loadDestHi** (byteTable, 2 bytes): transfer address high byte; $09A4 unused
- `$09A5` **savedSpriteEnable** (byteTable, 1 bytes): sprite enable saved during a sector transfer
- `$0A47` **padA47** (unknown, 1 bytes): single $20 byte before msgIdentifyMap
- `$0A48` **msgIdentifyMap** (text, 36 bytes): copy-protection prompt
- `$0B7D` **inputLockoutTimer** (byteTable, 2 bytes): $0B7D lockout timer, $0B7E settings byte
- `$0B7F` **ownerNameString** (text, 21 bytes): owner name string built at run time
- `$0B94` **gameSettingsBlock** (byteTable, 33 bytes): $0B94-$0BB4: game settings/state bytes (mapSeed $0B95-97, flags $0B9B-$0BA8, two 6-byte text fields $0BA9/$0BAF)
- `$0C8A` **perSideValueTable** (byteTable, 2 bytes): $31,$63 indexed by player side
- `$0CE4` **strOurs** (text, 4 bytes): 'OURS' default owner name
- `$0D67` **keyMatrixCodeTable** (byteTable, 64 bytes): 8x8 keyboard matrix -> key code (row = CIA1 PA line 0-7, column = PB bit 0-7); letters/digits are PETSCII|$80, F1/F3/F5/F7 = 0-3, DEL $88, RETURN $8D, CRSR $8E/$8B, SPACE $A0, $FE = modifier (ignored), $FF = CTRL (no key)
- `$0DA7` **shiftedKeyTable** (byteTable, 16 bytes): shifted replacements for key codes $AC-$BB (',' '-' '.' '/' '0'-'9' ':' ';')
- `$0F81` **msgNotADataDisk** (text, 16 bytes): 'NOT A DATA DISK!'
- `$0F81` **msgNotADataDisk** (text, 16 bytes): 'NOT A DATA DISK!' - shown when the 'EA' data disk was expected but not found; the last byte carries bit 7 as the terminator.
- `$0F91` **msgInsertGameDisk** (text, 17 bytes): 'INSERT GAME DISK.'
- `$0F91` **msgInsertGameDisk** (text, 17 bytes): 'INSERT GAME DISK.' - shown when the 'OZ' game disk is missing.
- `$0FA2` **msgDiskError** (text, 11 bytes): 'DISK ERROR.'
- `$0FA2` **msgDiskError** (text, 11 bytes): 'DISK ERROR.' - shown by diskErrorExit when the BAM read fails.
- `$0FAD` **diskIdTable** (text, 4 bytes): 'OZ','EA' disk ids (first char at $0FAD+X, second at $0FAE+X)
- `$0FAD` **diskIdTable** (byteTable, 4 bytes): 'OZEA': two two-character disk ids read as diskIdTable[X] / $0FAE[X], X=0 for the game disk ('OZ') and X=2 for the data disk ('EA'). The overlapping layout means index 1 would give 'ZE' (never used).
- `$10EF` **spriteEnableScratch** (byteTable, 2 bytes): $10EF written by the IRQ with the sprite enable shadow; $10F0 unused
- `$10EF` **spriteEnableScratch** (byteTable, 2 bytes): $10EF receives a copy of the sprite-enable shadow $9022 every IRQ and is never read back; $10F0 is untouched padding before the IRQ handler.
- `$1299` **padIrq1299** (unknown, 3 bytes): $00,$09,$13 between irqRti and updateFrameTimers; unreferenced
- `$1299` **padIrq1299** (unknown, 3 bytes): Three unreferenced bytes $00,$09,$13 between irqRti and updateFrameTimers - alignment padding or an assembler leftover.
- `$131A` **cursorBlinkTimer** (byteTable, 1 bytes): 30-frame countdown used by blinkCursorColor
- `$131A` **cursorBlinkTimer** (byteTable, 1 bytes): 30-frame countdown used by blinkCursorColor; stored immediately before the routine that owns it.
- `$1412` **menuFlag1412** (byteTable, 2 bytes): $1412 menu flag, $1413 link-state copy
- `$1535` **menuItemHandlerTable** (addrTable, 6 bytes): 3 handler addresses for fire on menu item 0-2: $158E (RTS), $17DA, $1A49
- `$163A` **menuItemColumnTable** (byteTable, 3 bytes): $0B,$13,$1B - column/position of the three menu items
- `$163A` **tabColumnTable** (byteTable, 3 bytes): screen column of tab 0..2 boxes ($0B,$13,$1B)
- `$163D` **menuItemNormalAttrTable** (byteTable, 3 bytes): attribute bytes for unhighlighted menu items
- `$163D` **tabColourTable** (byteTable, 3 bytes): normal colour byte of tabs (green $05, blue $06, red $02)
- `$1640` **menuItemHighlightAttrTable** (byteTable, 3 bytes): attribute bytes for the highlighted menu item (0 = use normal)
- `$1640` **tabHighlightColourTable** (byteTable, 3 bytes): bright colour byte of tabs ($0D,$0E,$0A)
- `$16FE` **lastEnergyShown** (byteTable, 1 bytes): energy value last printed ($FF forces a reprint)
- `$17D8` **comcenEnergySnapshot** (byteTable, 1 bytes): comcen energy (0..50) when the REPAIR tab was drawn / after a repair
- `$17D9` **repairAmount** (byteTable, 1 bytes): energy added by the last repair attempt
- `$1881` **systemMarkerColumnTable** (byteTable, 4 bytes): screen column of the four comcen system markers ($0D,$10,$14,$19)
- `$1881` **systemMarkerColumnTable** (byteTable, 4 bytes): Screen column of the four comcen system markers on the REPAIR schematic: $0D, $10, $14, $19 (energy, then the three systems). Read by drawSystemMarker and eraseSystemMarker.
- `$1885` **systemMarkerRowTable** (byteTable, 4 bytes): screen row of the four comcen system markers ($0E,$0A,$0A,$0C)
- `$1885` **systemMarkerRowTable** (byteTable, 4 bytes): Screen row of the four comcen system markers: $0E, $0A, $0A, $0C.
- `$1889` **blinkPhase** (byteTable, 1 bytes): 0/1 blink phase shared by the tab blink and the marker blink
- `$1889` **blinkPhase** (byteTable, 1 bytes): Shared 0/1 blink phase for the menu tab blink and the comcen system marker blink.
- `$188A` **showOkMarkers** (byteTable, 1 bytes): nonzero: draw markers for undamaged systems too (during the REPAIR WHICH screen)
- `$188A` **showOkMarkers** (byteTable, 1 bytes): Flag: draw markers for undamaged systems as well (set only while the REPAIR WHICH menu is open).
- `$1BC0` **orphanRts1BC0** (unknown, 1 bytes): unreachable $60 (RTS) between waitForCarrier and showMessageWaitForSpace
- `$1D4C` **scrollTargetUnit** (byteTable, 1 bytes): unit index parameter of scrollViewToUnit
- `$1D4D` **scrollStepDelay** (byteTable, 1 bytes): ticks per step parameter of scrollViewToUnit (0 = immediate)
- `$1DA2` **scrollViewParamTables** (byteTable, 66 bytes): Twenty-two parallel 3-byte tables (one entry per direction: 0 = -1, 1 = 0, 2 = +1) that parameterise scrollViewBitmap ($1DE4), which shifts the 7x5-cell tactical view one cell without redrawing it. In order: $1DA2 bitmap copy start index ($5F,$00,$00), $1DA5 end index ($FF,$70,$60), $1DA8 the DEY/INY opcode patched into both copy loops ($88,$C8,$C8), $1DAB/$1DAE bitmap source and destination byte offsets (0,0,$10 / $10,0,0), $1DB1/$1DB4 bitmap source address lo/hi ($AAC8,$A208,$A488), $1DB7/$1DBA bitmap destination lo/hi ($AD48,$A208,$A208), $1DBD/$1DC0 bitmap row stride lo/hi ($FEC0 = -320 up, $0140 = +320), $1DC3 rows to copy (8,10,8), $1DC6/$1DC9 screen copy start/end index, $1DCC/$1DCF screen source/destination column offsets, $1DD2/$1DD5 screen source lo/hi ($8D59,$8C41,$8C91), $1DD8/$1DDB screen destination lo/hi ($8DA9,$8C41,$8C41) and $1DDE/$1DE1 screen row stride lo/hi ($FFD8 = -40 up, $0028 = +40). Colour RAM is handled by adding $4C to the screen high byte.
- `$1DA2` **scrollXStartIndex** (byteTable, 3 bytes): per horizontal direction (0=-1,1=0,2=+1): start Y index of the bitmap byte copy ($5F,$00,$00)
- `$1DA5` **scrollXEndIndex** (byteTable, 3 bytes): end Y index of the bitmap byte copy ($FF,$70,$60)
- `$1DA8` **scrollXStepOpcode** (byteTable, 3 bytes): DEY ($88) or INY ($C8) opcode patched into both copy loops
- `$1DAB` **scrollXBitmapSrcOffset** (byteTable, 3 bytes): byte offset added to the bitmap source address (0,0,$10)
- `$1DAE` **scrollXBitmapDstOffset** (byteTable, 3 bytes): byte offset added to the bitmap destination address ($10,0,0)
- `$1DB1` **scrollYBitmapSrcLo** (byteTable, 3 bytes): per vertical direction: bitmap source address lo ($AAC8,$A208,$A488)
- `$1DB4` **scrollYBitmapSrcHi** (byteTable, 3 bytes): bitmap source address hi
- `$1DB7` **scrollYBitmapDstLo** (byteTable, 3 bytes): bitmap destination address lo ($AD48,$A208,$A208)
- `$1DBA` **scrollYBitmapDstHi** (byteTable, 3 bytes): bitmap destination address hi
- `$1DBD` **scrollYBitmapStrideLo** (byteTable, 3 bytes): bitmap row stride lo ($FEC0 = -320 when scrolling up, $0140 = +320 otherwise)
- `$1DC0` **scrollYBitmapStrideHi** (byteTable, 3 bytes): bitmap row stride hi
- `$1DC3` **scrollYRowCount** (byteTable, 3 bytes): character rows to copy (8,10,8)
- `$1DC6` **scrollXScreenStartIndex** (byteTable, 3 bytes): start Y index of the screen/colour copy ($0B,0,0)
- `$1DC9` **scrollXScreenEndIndex** (byteTable, 3 bytes): end Y index of the screen/colour copy ($FF,$0E,$0C)
- `$1DCC` **scrollXScreenSrcOffset** (byteTable, 3 bytes): column offset of the screen source (0,0,2)
- `$1DCF` **scrollXScreenDstOffset** (byteTable, 3 bytes): column offset of the screen destination (2,0,0)
- `$1DD2` **scrollYScreenSrcLo** (byteTable, 3 bytes): screen RAM source address lo ($8D59 row 8, $8C41 row 1, $8C91 row 3; column 25)
- `$1DD5` **scrollYScreenSrcHi** (byteTable, 3 bytes): screen RAM source address hi (colour RAM = +$4C00)
- `$1DD8` **scrollYScreenDstLo** (byteTable, 3 bytes): screen RAM destination address lo ($8DA9, $8C41, $8C41)
- `$1DDB` **scrollYScreenDstHi** (byteTable, 3 bytes): screen RAM destination address hi
- `$1DDE` **scrollYScreenStrideLo** (byteTable, 3 bytes): screen row stride lo ($FFD8 = -40 up, $0028 = +40)
- `$1DE1` **scrollYScreenStrideHi** (byteTable, 3 bytes): screen row stride hi
- `$1FA6` **joystickDirectionTable** (byteTable, 12 bytes): Indexed by the joystick nibble (zp_5D): FF FF FF FF FF 06 05 02 FF 07 04 03 (continues with FF 01 00 FF at $1FB2, which the disassembler split off): $0E up -> 0 (N), $0D down -> 1 (S), $07 right -> 2 (E), $0B left -> 3 (W), $0A -> 4 (NW), $06 -> 5 (NE), $05 -> 6 (SE), $09 -> 7 (SW); $FF = no/invalid direction. Logically the table is 16 bytes ($1FA6-$1FB5).
- `$1FB2` **diagonalDirectionTable** (byteTable, 17 bytes): Bytes 0-3 (FF 01 00 FF) are really the tail of joystickDirectionTable. Bytes 4-7 (30 20 21 31) give, for direction codes 4-7, (dx index << 4) | dy index into directionDeltaTable: NW = (-1,-1), NE = (+1,-1), SE = (+1,+1), SW = (-1,+1). Bytes 8-15 (01 00 03 02 06 07 04 05) look like an opposite-direction table (N<->S, E<->W, NW<->SE, NE<->SW) but no reference to $1FBA was found; byte 16 is a $00 pad.
- `$20F0` **directionDeltaTable** (byteTable, 4 bytes): Signed deltas: index 0 = -1 (dy for N), 1 = +1 (dy for S), 2 = +1 (dx for E), 3 = -1 (dx for W).
- `$24E1` **pathMarkerState** (byteTable, 3 bytes): Variables embedded in code: repeat counter (initial 5), marker X, marker Y of the animated mini-map path marker.
- `$2762` **nybbleToMulticolourTable** (byteTable, 16 bytes): expands a 4-bit terrain shape nybble to one multicolour bitmap byte: each set bit becomes the pixel pair %10 (bit0->$02, bit1->$08, bit2->$20, bit3->$80)
- `$2A29` **textDrawParams** (byteTable, 7 bytes): parameter block of the glyph plotters: useFixedColourFlag, drawColourByte, blockFillFlag, blockFillChar, windowRow, windowCol, one filler byte
- `$2A30` **bitmapDrawOpcodes** (byteTable, 4 bytes): opcodes LDA/ORA/EOR/CMP (zp),Y ($B1,$11,$51,$D1) for the bitmap draw modes 0-3
- `$2A34` **colourDrawOpcodes** (byteTable, 4 bytes): opcodes LDA/ORA/EOR/CMP abs ($AD,$0D,$4D,$CD) for the colour draw modes 0-3
- `$2BDD` **windowLeftTable** (byteTable, 27 bytes): Left screen column of the 27 predefined windows 0-26, read by drawWindowFrame with X = window index. 0 = the overview panel (column 0), 1 = the tactical view (column $18), 2 = the status panel, 5-$16 the $6F00 overlay screens, $18/$19 the main menu frames.
- `$2BDD` **windowLeftTable** (byteTable, 27 bytes): Left screen column of windows 0-26 (0 overview panel, 1 battlefield view, 2 status panel, 4, 5-$16 overlay B screens, $18/$19 main menu frames, $1A)
- `$2BF8` **windowTopTable** (byteTable, 27 bytes): Top screen row of windows 0-26.
- `$2BF8` **windowTopTable** (byteTable, 27 bytes): Top screen row of windows 0-26
- `$2C13` **windowWidthTable** (byteTable, 27 bytes): Width in character cells including the frame: window 0 is $16 = 22 wide (the 20x20 overview map plus its frame), window 1 is $10 = 16 (the 7x5-cell tactical view at 2 cells per map cell plus frame), window $18 is the full 40 columns.
- `$2C13` **windowWidthTable** (byteTable, 27 bytes): Width in cells (including the frame) of windows 0-26; e.g. window 0 = 22x22 around the 20x20 overview map, window 1 = 16x12 around the 7x5 cell (14x10 char) battlefield view
- `$2C2E` **windowHeightTable** (byteTable, 27 bytes): Height in character cells of windows 0-26 (window 0 = $16 = 22, window 1 = $0C = 12).
- `$2C2E` **windowHeightTable** (byteTable, 27 bytes): Height in cells of windows 0-26
- `$2C49` **windowFramePtrTable** (addrTable, 54 bytes): 27 little-endian pointers selecting each window's 9-byte frame tile set out of the ten sets at $9824-$987D in the graphics block. Windows 0-3 use the four decorated sets $985A/$9863/$986C/$9875, the rest reuse $9824 (thin box), $982D, $9836 (octagon), $983F (shadowed), $9848 (solid) and $9851 (blank/erase).
- `$2C49` **windowFramePtrTable** (addrTable, 54 bytes): 27 little-endian pointers to 9-byte frame tile sets in $9824-$987D (sets at $9824, $982D, $9836, $983F, $9848 (solid), $9851 (blank), $985A, $9863, $986C, $9875)
- `$2E0D` **magnifyOddFlags** (byteTable, 2 bytes): magnifyWidthOdd and magnifyHeightOdd work variables
- `$2F85` **spriteBitMasks** (byteTable, 16 bytes): $01,$02,...,$80 (set masks for sprite n) followed by $FE,$FD,...,$7F (clear masks, used by $337D)
- `$2F95` **spriteOffsetLoTable** (byteTable, 8 bytes): Low byte of n*64 for sprite 0-7 ($00,$40,$80,$C0 repeating); added to $8A00
- `$2F9D` **spriteOffsetHiTable** (byteTable, 8 bytes): High byte of n*64 for sprite 0-7 (0,0,0,0,1,1,1,1)
- `$3237` **spritePlotWork** (byteTable, 2 bytes): spritePlotTemp and circlePointIndex work bytes
- `$32DA` **circlePointBuffer** (byteTable, 16 bytes): RAM: 8 X coordinates ($32DA-$32E1) and 8 Y coordinates ($32E2-$32E9) of the current octant point of a circle, filled by sub_3266 and plotted by sub_32BF
- `$33C2` **markerOffsetTableDead** (byteTable, 6 bytes): three 2-entry tables (X offset $0A/$08, Y offset $08/$09, colour $02/$04) used only by the dead routine at $33C8
- `$35A6` **sightRangeByType** (byteTable, 10 bytes): range in cells per unit type: entries 0-4 spotting (Grunt 3, Rider 3, Boomer 3, Spy 5, Comcen 3), entries 5-9 attacking (3, 3, 6, 5, 3)
- `$35B0` **rangeDistanceSqTable** (byteTable, 11 bytes): r*(r+1) for r = 0..10: squared-distance threshold that a target must be below to be within range r
- `$35BB` **terrainSightBonusTable** (byteTable, 8 bytes): extra range by terrain class 0-7 (0,0,0,1,1,2,2,0), doubled for spotting
- `$3860` **squaresTable** (byteTable, 16 bytes): n*n for n = 0..15 (also used by overlay A $7831 and the comm module $E832)
- `$38A4` **baseDamageTable** (byteTable, 15 bytes): base damage [attacker type 0-2][target type 0-4]: Grunt 4,5,25,8,12; Rider 3,4,20,6,8; Boomer 12,16,8,8,20
- `$38B3` **terrainHeightTable** (byteTable, 8 bytes): height/cover level by terrain class 0-7 (0,0,0,1,2,3,4,0) used for line of sight and the combat height bonus
- `$38BB` **combatScratch** (byteTable, 7 bytes): RAM: $38BB target unit, $38BC attacker unit, $38BD damage accumulator, $38BE/$38BF attacker row/col, $38C0/$38C1 target row/col (saved around the overlay shot drawing)
- `$3CF1` **ringAnimStep** (byteTable, 1 bytes): loop counter of animateRingBurst
- `$3D56` **comcenSystemIndex** (byteTable, 1 bytes): loop counter of rollComcenSystemDamage
- `$405E` **terrainMoveCostTable** (byteTable, 8 bytes): movement points charged per step by terrain class 0-7: 20,65,30,40,20,40,20,20. Class 8 (terrain $68-$6B/$6F-$72 via $077E) reads into speedClassRateTable[0] = 12.
- `$4066` **speedClassRateTable** (byteTable, 5 bytes): movement points gained per round by speed class 0-4: 12,25,12,25,0 (class 4 = immobile comcen; comcen classes come from $92B4/$92B5)
- `$406B` **zoneScoredBitTable** (byteTable, 3 bytes): bit masks ORed into unitTypeTable when a unit scores a zone: [1] = $08 near zone (col 10-19 mirrored), [2] = $10 far zone (col 0-9); entry 0 ($28) unused
- `$406E` **unitMovedFlag** (byteTable, 1 bytes): 1-byte variable (not a table): set when moveUnitToCell moved the current unit
- `$4C7E` **formationRotateRules** (byteTable, 6 bytes): Two 3-byte rule sets indexed by the rotation mode of command $A3: $4C7E for dx, $4C81 for dy. Bit 7 of a rule = negate the component, bit 0 = store it as the new dy (zp_75) instead of dx (zp_74). Values {$81,$01,$80} and {$00,$80,$81} give the rotations (dy,-dx), (-dy,dx), (-dx,-dy).
- `$4C7E` **formationRotateRules** (byteTable, 6 bytes): Two 3-byte rule sets indexed by the rotation mode of command $A3 ($4C7E transforms the member's dx, $4C81 its dy). Bit 7 of a rule = negate the component, bit 0 = store the result as the new row offset (zp_75) instead of the new column offset (zp_74); the code loads each rule into X so the LDX sets N for the negate test and uses the masked copy as the index of 'STA zp_74,X'. Values {$81,$01,$80} / {$00,$80,$81} give the three menu rotations: 0 SWEEP LEFT (dy,-dx), 1 SWEEP RIGHT (-dy,dx), 2 REVERSE (-dx,-dy).
- `$4D2A` **commandHandlerTable** (wordTable, 78 bytes): 39 little-endian handler addresses for command bytes $80-$A6, read as D_4D2A,x / D_4D2B,x with x = (cmd & $3F) * 2 and JSRed through $4EEE. Entry $8D is $FFFF because that command is consumed by the packet mergers. Handlers: $80 5011, $81 501A, $82 5023, $83 5098, $84 504B, $85 50D2, $86 515D, $87 516B, $88 5185, $89 519B, $8A 51AB, $8B 51EE, $8C 523E, $8E 5336, $8F 537B, $90 53C0, $91 542E, $92 538B, $93 5070, $94 50AA, $95 5062, $96 50BE, $97 53A3, $98 5470, $99 56B1, $9A 5479, $9B 549E, $9C 54E1, $9D 5511, $9E 520C, $9F 5548, $A0 554C, $A1 556D, $A2 5571, $A3 5596, $A4 559C, $A5 55A5, $A6 51EA.
- `$4D78` **commandLengthTable** (byteTable, 39 bytes): Total length in bytes (command byte included, 1-4) of each command $80-$A6, indexed by cmd & $3F; read by getCommandLength.
- `$562A` **outPacketFill** (byteTable, 1 bytes): work variable of sendOutgoingPacket (bytes placed in the outgoing packet)
- `$5A59` **overviewPatternLo** (byteTable, 33 bytes): Low bytes of the overview-map pattern pointer for terrain index $00-$20 (33 entries), read by buildOverviewQuadrant at $5C40 with Y = mapCell - $40, or mapCell - $20 when the cell is below $40, clamped below $21 ($5BC3). Index 0 (blank ground) -> $5A9B; indices 1-8 cycle through the four contour-edge patterns $5A9F/$5AA3/$5AA7/$5AAB; 9-$0C -> $5AAF; $0D-$14 -> $5AB3; $15-$18 (river cells $55-$58) -> $5AC7/$5ACB/$5ACF/$5AD3; $19-$1C -> $5AC3; $1D-$20 (forest cells $5D-$60) -> $5ABB.
- `$5A59` **overviewPatternLo** (byteTable, 33 bytes): low bytes of the 4-byte quadrant pattern for terrain indices 0-32 (patterns at $5A9B-$5AD6)
- `$5A7A` **overviewPatternHi** (byteTable, 33 bytes): High bytes of the same 33 pointers - all $5A, because every pattern lives in this block.
- `$5A7A` **overviewPatternHi** (byteTable, 33 bytes): high bytes (all $5A) of the pattern pointers; the listing merges this with the patterns that follow
- `$5A9B` **overviewPatterns** (byteTable, 60 bytes): Fifteen patterns of four bitmap scanline bytes each. The overview draws each 2x2 block of map cells as one 8x8 character, so a pattern carries both horizontal halves as nibble pairs ($00/$33/$CC/$FF) and the quadrant mask picks the half that belongs to the cell. Contents: $5A9B 00 00 00 00 (blank), $5A9F 00 00 33 33, $5AA3 00 CC 33 33, $5AA7 33 00 00 00, $5AAB 33 CC 00 00 (the four contour edges), $5AAF 33 33 33 33, $5AB3 00 FF 00 00, $5AB7 FF FF FF FF (a live unit), $5ABB 33 CC 33 CC (forest checker), $5ABF FF 00 FF 00 (a cloaked unit), $5AC3 CC FF FF 33, $5AC7 33 FF FF FF, $5ACB CC FF FF FF, $5ACF FF FF FF 33, $5AD3 FF FF FF CC (the four river blob quadrants). $5AB7 and $5ABF are the only two not reachable through overviewPatternLo/Hi - buildOverviewQuadrant uses them directly for units.
- `$5A9B` **overviewPatterns** (byteTable, 60 bytes): 15 patterns of 4 scanline bytes (multicolour, both horizontal halves present, the quadrant mask selects one): $5A9B blank, $5AB7 unit blob, $5ABF cloaked unit, others terrain textures
- `$5B64` **quadrantPixelMask** (byteTable, 4 bytes): AND mask for quadrant 0-3 of an overview character: $F0 left half, $0F right half
- `$5B68` **quadrantRowOffset** (byteTable, 4 bytes): first scanline of quadrant 0-3: 0,0,4,4 (4 rows each)
- `$5E14` **uiLevelTextColourTable** (byteTable, 3 bytes): Info-panel text colour per uiLevel: $09 (browsing), $05 (unit selected), $04 (group). Read at $22AE (showUnitInfoPanel) and $64E7.
- `$612E` **menuLoopMode** (byteTable, 1 bytes): Variable byte embedded in code: map-highlight mode of the menu loop ($FF unit menu, $01 group menu, $00/$20 join-group picker side bit).
- `$6782` **sidVoiceRegOffsetTable** (byteTable, 5 bytes): SID register offset per voice, indexed by voice*2 so the driver can use the same X for its own state and for the chip: $00, (pad), $07, (pad), $0E.
- `$6782` **sidVoiceRegOffsetTable** (byteTable, 5 bytes): SID register offset per voice, indexed by voice*2: $00, (pad), $07, (pad), $0E
- `$6787` **sndCommandSlotTable** (byteTable, 11 bytes): Sound-script command 0-10 -> byte offset of its state slot from sndVoiceState ($6792): 0 duration ($00, also ends processing), 1 control/waveform ($01), 2 loop counter A ($06), 3 loop counter B ($07), 4/5 spare ($0C/$0D), 6 frequency ($12, 2 bytes), 7 pulse width ($18, 2), 8 AD/SR ($1E, 2), 9 frequency delta ($24, 2), 10 pulse delta ($2A, 2). runSoundScript adds voice*2 to the entry and treats every slot from $12 up as 16-bit ($66DF CPY #$12).
- `$6787` **sndCommandSlotTable** (byteTable, 11 bytes): script command 0-10 -> offset of its state slot from $6792 ($00 duration, $01 control, $06 loop A, $07 loop B, $0C/$0D spare, $12 freq, $18 pulse, $1E ADSR, $24 freq delta, $2A pulse delta)
- `$6792` **sndVoiceState** (byteTable, 48 bytes): Work RAM, zero on disk: eight 6-byte rows of per-voice sound state (duration, control, loop counters, spares, frequency, pulse width, ADSR, and the two deltas), cleared per voice by startSound.
- `$6792` **sndVoiceState** (byteTable, 48 bytes): work RAM: eight 6-byte rows of per-voice sound state (see variables $6792-$67BD); zeroed per voice by startSound
- `$67C2` **sndDriverVars** (byteTable, 6 bytes): Work RAM, zero on disk: sndVolume, sndLastByte ($67C3, the current script byte), sndScratch, sndInitLock, sndCurrentSound, sndStartVoiceIndex.
- `$67C2` **sndDriverVars** (byteTable, 6 bytes): work RAM: sndVolume, sndLastByte, sndScratch, sndInitLock, sndCurrentSound, sndStartVoiceIndex
- `$67C8` **sndVoiceSoundId** (byteTable, 3 bytes): Work RAM, zero on disk: the sound id currently playing on each of the three voices (0 = free). $67CA is polled by the text engine at $C909/$C913 before it plays a UI beep.
- `$67C8` **sndVoiceSoundId** (byteTable, 3 bytes): work RAM: sound id currently playing on each of the three voices (0 = free)
- `$6831` **soundPointerTable** (wordTable, 174 bytes): 29 sound effects x three little-endian voice-script addresses (6 bytes per sound, $0000 = the sound does not use that voice). Sound $00 points all three voices at the 60-tick silence script $68DF (stop everything) and $1A/$1B/$1C silence voice 0/1/2 individually. Multi-voice effects are $06 ($6980/$69C9/$68DF), $0D ($6AA7/$6AB5/$6AC3), $0E ($6ADC/$6AEC/$6AFC), $15 ($68E7/$68F0/$68DF) and $19 ($6BFB/$6C15/$6C1B). Sounds $01-$07 and $0C use voice 0, $08-$0B voice 1 and $0F-$18 voice 2. messageSoundTable ($0612) picks the sound played with each status message.
- `$6831` **soundPointerTable** (wordTable, 174 bytes): 29 sounds x 3 voice script addresses (6 bytes per sound, 0 = voice not used). Sound 0 plays the 60-tick silence script $68DF on all voices (stop all), 26/27/28 silence voice 0/1/2 only; 6, 13, 14, 21 and 25 are multi-voice; 8-11 use voice 1, 15-24 voice 2, the rest voice 0
- `$68DF` **soundScriptData** (byteTable, 854 bytes): The bytecode for all the voice scripts, in the format interpreted by runSoundScript ($66C7): a byte $00-$0A stores the following one or two bytes into the state slot named by sndCommandSlotTable (slot 0 = duration ends the frame, duration 0 ends the voice); $40-$7F is a loop whose low six bits select a counter slot and which is followed by a 16-bit target address; $80-$FF adds the scenario random byte zp_5A to the frequency low byte (noise jitter). $68DF is the shared 60-tick silence script and $68E3 ('01 00 00 00') is the common end-of-sound target that the many '46 E3 68' jumps branch to.
- `$68DF` **soundScriptData** (byteTable, 854 bytes): bytecode scripts for the sound driver (format described in runSoundScript); $68E3 '01 00 00 00' is the shared end-of-sound target of the '46 E3 68' jumps
- `$6C35` **unitGlyphPointerTable** (addrTable, 40 bytes): 20 little-endian pointers to the 32-byte unit glyphs that follow, indexed by unitType*4 + facing - the caller computes (unitFlags AND 3) + unitGlyphBaseTable[$0799 + unitType] - $2C and doubles it.
- `$6C35` **unitGlyphPointerTable** (addrTable, 40 bytes): 20 word pointers to the 32-byte unit glyphs below, indexed by (unitFlags & 3) + D_0799[unitType] - $2C, i.e. unitType*4 + facing
- `$6C5D` **unitGlyphShapes** (charset, 640 bytes): 20 multicolour unit glyphs of 16x16 pixels, 32 bytes each, stored as four 8-byte character cells in the order top-left, top-right, bottom-left, bottom-right. Every lit pixel is the bit pair %11, so the byte values are combinations of $00/$03/$0C/$30/$C0/$33/$3C/$F0/$FC/$FF. The 20 shapes are five unit types x four facings, all clearly recognisable when rendered: 0-3 GRUNT (a stubby figure with a head, up/down/left/right), 4-7 RIDER (a mounted figure with a wide base), 8-11 BOOMER (a heavy block with a protruding barrel), 12-15 SPY (a small chevron/arrow), 16-19 COMCEN (a large domed block). These are the big glyphs used in the 16x16-per-cell tactical view; the single-cell versions are the 8x8 shapes at $9DCE.
- `$6C5D` **unitGlyphShapes** (charset, 640 bytes): 20 multicolour 2x2-character (16x16 pixel) unit glyphs of 32 bytes (TL, TR, BL, BR 8 bytes each), all pixels %11: grunt soldier (0-3), rider (4-7), boomer with barrel (8-11), spy chevron (12-15), comcen dome (16-19); within each type the facings are up, down, left, right
- `$6EDD` **mainProgramPadding** (unknown, 35 bytes): $6EDD-$6EFF, all zero: filler to the end of the main program image (track 22-27). Unreferenced.
- `$6EDD` **padding6EDD** (unknown, 35 bytes): zero filler to the end of the main program at $6EFF

Insights:

- [data_main_0800] There are no message strings in any of the pure-data tails of the main program - all of the game's text lives in the $0400 block, the $8800 block, the $C000 overlay and the $6F00 overlays.
- [data_main_0800] The 20 big unit glyphs at $6C5D and the 20 small ones at $9DCE are the same five types x four facings, reached through the same index arithmetic (unitGlyphBaseTable $0799 + facing): the tactical view uses the 16x16 multicolour pair table at $6C35, the overview map and the radar screens use the 8x8 one-bit shapes at $9C6E + code*8.
- [data_main_0800] The sound system is a three-voice bytecode interpreter: 29 sounds x 3 voice scripts in a pointer table ($6831), 854 bytes of script ($68DF-$6C34), an 11-entry command/slot map ($6787), a 3-entry SID register offset table ($6782) and 54 bytes of work RAM ($6792-$67C7). Scripts can loop with two counters and add RNG jitter to the frequency, which is how the engine gets varied explosion noises out of so little data.
- [data_main_0800] scrollViewBitmap never redraws the tactical view when the cursor moves one cell: it block-copies the bitmap, screen RAM and colour RAM by one cell using self-modified loops whose 22 parameters (including the DEY/INY opcode itself) come from the table group at $1DA2, then fills in only the newly exposed column and/or row.
- [data_main_0800] The 27 windows the game can draw are fully data-driven: left/top/width/height at $2BDD/$2BF8/$2C13/$2C2E and a frame style pointer at $2C49 that selects one of ten 9-glyph tile sets in the graphics block. Window 0 is the 22x22 overview frame and window 1 the 16x12 tactical frame.
- [data_main_0800] The strategic overview compresses two map cells per character in each axis, so each of the 33 terrain indices maps to a 4-byte pattern that already contains both horizontal halves; only 15 distinct patterns exist ($5A9B-$5AD6), including a solid $FF blob for a live unit and an $FF/$00 stripe for a cloaked one, which are the only two the index table never points at.
- [data_main_0800] $6792-$67CA is described in the listing as five byteTables; on disk it is all zero and at run time it is the sound driver's work RAM (eight 6-byte voice-state rows plus nine scalars), not table data. The individual D_6793..D_67CA labels are its fields.
- [data_main_0800] The $1DA2-$1DE3 tables are named individually in the listing but they are one column-store: 22 parallel arrays indexed by the same direction value 0-2, all consumed by the single routine scrollViewBitmap.
- [data_main_0800] $6C35 is listed as pointing at 'the 32-byte unit glyphs below' with only the first target named (unitGlyphShapes); the other 19 targets ($6C7D..$6EBD) have no labels at all.
- [game_main_0800_0800] The raster IRQ handler is at $10F1 (line $FA), installed by $0C66 with the vector shadowed in zp $AB/$AC and re-written into $FFFE/$FFFF every interrupt; $1298 is the RTI used for NMI. The handler also receives BRK and routes it through the patchable JMP operand at $10FF/$1100 (normally to the plain exit $1293) - the comm module contains BRK instructions at $E01D/$E039.
- [game_main_0800_0800] VIC shadow registers live at $9000-$9024: $9000 D011, $9001 D016, $9002 D018, $9003-$9006 border/background colours, $9008-$900F sprite colours (>= $10 means two nibble colours that flash), $9010 sprite X, $9018 sprite Y, $9020 X MSB, $9022 enable, $9023 X expand, $9024 Y expand. When $92C8 is nonzero the sprite positions get fine-scroll offsets from $92F4 (x & 7) and $92F5 ((y & 7) - 3). Game code should write the shadows, never the VIC directly.
- [game_main_0800_0800] Input model: the IRQ polls the keyboard through the comm module entry $E015 (which calls the game's scanKeyboard at $0DB7 via the vector $E013/$E014) and joystick port 2 ($DC00 with all rows deselected). Results: zp_5D direction (active-low bits, $0F centred), zp_5F fire released (1 = up), zp_66 current key, $90EA new key event, $9248 last typed PETSCII key, $92E4/$92E5 shift and C= flags. F1/F3/F5/F7 produce key codes 0-3 and sub_06BA switches screens on them ($90FB). SHIFT + C= + F7 aborts to the main menu ($0AA3).
- [game_main_0800_0800] Six frame countdown timers at zp $6C-$71 are decremented by the IRQ: $6C joystick auto-repeat (16 then 6 frames via sub_0D49), $6D fire debounce (10), $6E menu blink (10), $70 key repeat (20). The 24-bit frame counter is zp $45-$47. sub_FD5D in high memory is a 24-bit LFSR RNG on zp $57-$59.
- [game_main_0800_0800] Disk access protocol with the comm module: before any load the game calls suspendCommModule ($0F5F: $E039=$C0, wait for $E03A bit 7) and afterwards resumeCommModule ($0F71). $E03B is the link-active flag, $E011 answer/originate (0/1), $E030 identifies the resident comm build, $E006 = get received byte (C=1 none), $E009 = send byte, $E000/$E00C are called every IRQ.
- [game_main_0800_0800] The settings block $0B7D-$0BB4 holds the start-up/game options: $0B9B solo mode (bit 7), $0B9C link option, $0B9F/$0BA0 player side (0/1, $FE unset), $0BA1 first play, $0BA3 game type (bits 0-2 index the names at $CCC8: Scrimmage, QB Sneak, The Bomb, Face-Off, Sluggers, Full War, Defender; bit 6 custom), $0BA5/$0BA6 link type (comm build), $0BA7 loading depth, $0BA8 resume flag. $0B7F is a 21-char owner name built from the disk's personalised BAM bytes ($FFDE/$FFE8), default 'OURS'.
- [game_main_0800_0800] Seed label corrections: $0EFE is stopAllSound (lda #0 / jmp $C89A), the overlay loader described in the seed note is sub_0F03; $10A4 (seed loadMapAndSetup) is not an instruction boundary - the routine is sub_10AB; $1081 (seed loadOverlayBIfNeeded) is mid-instruction - the routine starts at $1080; $0F55 does an 8-frame delay ($C37F) rather than a retry. Calls rendered as bootReceiveSectorData ($C0EF) from game code actually hit the runtime $C000 overlay routine at $C0EF (print A newlines), not the boot loader.
- [game_main_0800_0800] Incoming chat text is buffered at $9253 (36 chars, length in $9277) and mirrored with bit 7 set at $0549 (marker $0548); RETURN on a live link enters typing mode ($929C=$C0). Message slots for the status line are a pointer table at $0500/$0524 (lo/hi, index X) written by $C924, and a 9-entry message queue at $9249-$9252 managed by $C566/$C6EF.
- [game_main_0800_163A] The in-game menu screen (screen id 3, entered via sub_06BA -> L_141D when zp_66 requests it) has three tabs STATS / REPAIR / MISC selected with joystick left/right and activated with fire; the main loop is L_1439 and the fire action is dispatched through the JSR address table D_1535 = {$158F, $17DA, $1A49}. sub_1507 picks the initial tab: MISC when no game is running, REPAIR when the comcen is stunned or any comcen status changed since the last visit (snapshot at $92D6), else STATS.
- [game_main_0800_163A] The display is a VIC bank 2 bitmap: bitmap $A000, screen (colour) RAM $8C00, colour RAM $D800 (offset $4C00 from screen RAM). 'Text' is drawn into the bitmap by the $C000 overlay (font at $987E) and all colour operations (tabs, markers) write colour bytes with sub_2A64(A=colour, X=width, Y=height at row D_2A2D, column D_2A2E). The row address tables live at $9300/$9319 (bitmap) and $9382/$939B (screen RAM), column x8 at $9332/$935A.
- [game_main_0800_163A] The battlefield view is 7x5 map cells of 16x16 pixels at screen column 25, row 1 (bitmap $A208, screen $8C41). zp_A5/zp_A6 are the map column/row of its top-left cell, the cursor/centre cell is (zp_A5+3, zp_A6+2) and is mirrored in zp_93/zp_92. scrollViewBitmap ($1DE4) shifts the bitmap and both RAMs by one cell using self-modified loops parameterised by the 22 three-byte tables at $1DA2-$1DE3, then redraws the exposed column/row via sub_25FA (map cell read, row pointers $BFB0/$BFD8) and sub_5D1A (draw cell).
- [game_main_0800_163A] $C000 overlay text API as used here: zp_39/zp_3A/zp_3B/zp_3C = window top/bottom/left/width, zp_3F/zp_40 = cursor column/row, zp_3D = colour byte; sub_C099 = set cursor, sub_C097 = set row then cursor, sub_C06A = clear window, sub_C0D7 = full-screen window, sub_C041 = newline, sub_C01A = print char, sub_C133(A/Y=string) = print bit-7-terminated string, sub_C137 = print the string following the last one, sub_C100(A/Y=table,X) = print 8-byte table entry X, sub_C17D(A/Y) = print decimal, $C0EF (stale label bootReceiveSectorData) = print A fill characters, sub_C41B/sub_C40E = save/restore the window to $92B6, sub_C37F = wait A frames, sub_C89A = play sound A, sub_C6FF = show status message A (table at $0500/$0524), sub_C853 = show 'WORKING...'.
- [game_main_0800_163A] Status message ids used by the comm code: $10 'WAITING FOR CONNECTION...', $17 'PICK UP PHONE THEN PRESS SPACE.', $18 'PRESS SPACE, WAIT, HANGUP PHONE.', $0C 'COMCEN STUNNED!', $01 'WORKING...'. Game-type names are an 8-byte-per-entry table at $CCC8 (SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER, ...) indexed by $0BA3 & 7; $CC11 holds STANDARD/CUSTOM selected by $0BA3 bit 6.
- [game_main_0800_163A] Outgoing commands to the opponent/engine are queued byte-wise by sub_55EF into the 40-byte ring at $9065 (indices $908D/$908E, pending count $929F) and suppressed during film playback ($0B9B bit 7). Commands seen here: $80 unit x y (comcen order before repair), $8C reason (abort/disconnect, reason 0 = abort game, 5 = disconnect), $9A unit amount (energy repair), $9E (repair attempt while stunned).
- [game_main_0800_163A] Comm module jump table entry sub_E003: X=0 opens the link (D_E03B/D_E03C/D_E03D := $FF), X>=1 closes it (:= 0, D_E01F := 0). The modem prompts are skipped when $0BA5/D_E030 bit 7 is set (comm build 1 = T35), suggesting build 1 is the direct/null-modem variant and build 2 (T34) the Hayes modem variant.
- [game_main_0800_163A] Comcen damage model: D_9241[0..3] hold energy/system statuses (0 ok, $40 degraded, $80 damaged); sub_3D57 randomly damages systems 1-3 when the DAMAGE option ($0BA3 bit 4) is on; repairs succeed with probability 1/4 (1/2 for energy) and energy repairs add (50-energy)/8+1. Energy lives in unitEnergyTable $F9C4 bits 0-5 (0..50, displayed x2 as %). D_922A mirrors the comcen stun counter from $F960 with bit 7 = stunned.
- [game_main_0800_163A] The file's line numbering does not match the chunk spec (lines 1607-2668 are $13C0-$1D4C); this survey covers the address range $163A-$1FA5 (file lines 1903-3019).
- [game_main_0800_1FA6] Screen layout (multicolour bitmap, VIC bank 2, screen RAM $8C00): left 22x22-char frame at column 0 holds the strategic mini-map (40x40 map cells drawn 4x4 pixels each, sprite X = cell*4+$1F, Y = cell*4+$39); right 16x12 frame at column 24 holds the tactical viewport (7x5 map cells, each 2x2 chars, rows 1-10 / columns 25-38); below it a 16x11 frame at column 24 row 11 is the unit info panel (text window rows 12-20, cols 25-38); the bottom status bar is at row 22 (panel 3/4, 38 wide) and shows messages from the table at $0500/$0524 (e.g. 10 = 'SETUP UNITS. PRESS F 1 WHEN DONE.', 16 = 'WAITING FOR CONNECTION...').
- [game_main_0800_1FA6] The player's cursor is always the centre cell of the viewport: cursor = (zp_A5 + 3, zp_A6 + 2). Moving the joystick scrolls the viewport (sub_1CD3 -> the self-modifying bitmap scroll at $1DE4-$1F4D) rather than moving a cursor; sprite 0 shows the cursor box on the mini-map (sub_3049) and the expanded sprite 2 frames the centre cell of the viewport. sub_1CC1 limits the cursor to map rows 20-39 while zp_B1 != 0 (own half during unit setup).
- [game_main_0800_1FA6] Unit records are parallel 100-byte arrays following the map: $F640 X, $F6A4 Y, $F708 owner/flags, $F76C type (0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN) with bit 7 = dead, $F7D0/$F834 destination, $F898 cloaked, $F8FC blitz, $F960 stun/dug-in, $F9C4 energy (x2 = percent), $FA28 move state, $FA8C draw flags, $FAF0 terrain under the unit, $FB54 group (bits 2-4 = ALPHA..GROVER, bit 6 = grouped). A map byte with bit 7 set holds the unit index in bits 0-6; terrain codes are $40 (blank) and up, off-map cells are drawn with pattern $68 + ((x+y) & 3).
- [game_main_0800_1FA6] Which half of the unit array is 'ours' is decided by self-modifying branch opcodes: 'cmp #$32' followed by bcs/bcc at $206B, $2084, $2840, $2846, $36D5, $379C, $3B2C, $3BD0, $3F9E, $4783, $5BF7, $5BFE, $63E2 are patched with $90/$B0 by sub_2A08 and overlay A ($7BCF-$7BEF) from D_0B9B bit 0.
- [game_main_0800_1FA6] Joystick/keyboard are sampled in the raster IRQ ($10F1, misclassified as data): zp_5D = direction nibble ($0F centred), zp_5F = 1 when fire released, D_90EE counts idle frames, D_0B7D freezes input for N frames, zp_6C-zp_71 are six per-frame countdown timers. sub_0EB1/sub_0EEA turn zp_5F into edge-triggered clicks using the armed flag zp_79 and the debounce timer zp_6D; the main loop uses D_1FA6 to map the nibble to 8 direction codes and D_20F0/D_1FB2 to deltas.
- [game_main_0800_1FA6] Hover behaviour: after 15 idle frames on an own unit the info panel (sub_22A2/sub_2384) is displayed and stays while the cursor rests there; after 90 idle frames in target mode a path preview animates the mini-map marker (sprite 1) from the unit to the cursor (sub_2264/sub_24F3) using the Bresenham routines sub_FBB8 (init: zp_93/zp_92 -> zp_95/zp_94, returns steps in zp_91) and sub_FBFA (step, leaves dx/dy in D_90F5/D_90F4).
- [game_main_0800_1FA6] Text engine in the $C000 overlay: window = zp_39 top, zp_3A bottom, zp_3B left, zp_3C width; cursor zp_3F (col) / zp_40 (row); zp_3D colour, zp_3E fill char; sub_C0D7 full screen, sub_C06A clear window, sub_C097 set row, sub_C041 newline, sub_C01A print char, sub_C133 print string (A/Y), sub_C100 print entry X of a table of 8-byte strings at A/Y, sub_C17D print decimal number, sub_C075 clear to window bottom. Strings end with bit 7 set; unit names at $C964, group names at $C92C, weapon/action words at $C98C/$C9A4.
- [game_main_0800_1FA6] Game modes: zp_66 requests a mode, D_90FB holds the current one, sub_06BA ($0400 area) resets the stack and dispatches (0 = L_1FC3 battlefield map, 1 = $7587 and 2 = $7FE6 in the overlay, 3 = $141D). sub_25C4 jumps into overlay A at $8658 and is only meaningful while zp_B1 != 0 (setup phase with overlay A loaded); in overlay B that address is mid-instruction.
- [game_main_0800_1FA6] The header line numbers given for this chunk (2669-3522) did not match the stated addresses; the survey follows the address range $1FA6-$2761 (file lines 3022-4055).
- [game_main_0800_1FA6] Function keys drive the screen flow: the IRQ key handler ($0E56, misclassified as data) stores F1/F3/F5/F7 as 0-3 in zp_66 and sub_06BA switches game modes accordingly; F1 (0) = return to / start the battlefield map, which is why the setup message says 'PRESS F 1 WHEN DONE'.
- [game_main_0800_2762] Screen layout: the game runs in multicolour bitmap mode with the bitmap at $A000 and screen RAM at $8C00 (VIC bank 2, as left by the title picture). Screen RAM colour = screenCellPtr, colour RAM = screenCellPtr + $4C00. Tables at $9300 give bitmap row base (D_9300 lo / D_9319 hi, 25 rows of 320 bytes), column*8 (D_9332 lo / D_935A hi, 40 columns) and screen row base (D_9382 lo / D_939B hi).
- [game_main_0800_2762] Font: 114 characters of 8 bytes at $9426-$97B5; D_93B4 (114 bytes) is the default screen RAM colour byte for each character. D_9810 holds pairs of character codes forming two-character tall symbols (drawTallGlyph). Text option words FULL/HALF/NONE are at $0652 (4 bytes each, bit 7 terminated).
- [game_main_0800_2762] Map tiles are 2x2 characters (16x16 pixels). Terrain shapes are 8 bytes of 8x1-bit pixels (two nybbles per row) at $9C6E + shape*8, shape = cell byte - $40 (cell $40 = blank, $68-$6B are the off-map border shapes); renderMapTile doubles them 2x and writes them through the tile buffer $04C8-$04E7, whose caller sub_26C1 copies 16 bytes to the bitmap cell and 16 bytes to the cell one character row lower (+320). Terrain colour = D_07EA[D_07BE[shape]] (D_077E is the same table indexed by the raw cell byte).
- [game_main_0800_2762] Map cell encoding: bit 7 set -> low 7 bits = unit index (0-99) and the terrain underneath is kept in unitTerrainUnder ($FAF0+unit). Unit records are parallel 100-byte arrays spaced $64 apart from $F640 ($F640 x, $F6A4 y, $F708, $F76C type, $F7D0, $F834, $F898, $F8FC, $F960, $F9C4, $FA28, $FA8C, $FAF0, $FB54). Units 0-49 belong to player 0, 50-99 to player 1.
- [game_main_0800_2762] Unit glyphs: 20 glyphs of 32 bytes (2x2 chars, multicolour %11 pixels coloured through colour RAM) at $6C5D-$6E7C, addressed by the word table D_6C35; glyph = (D_F708 & 3) + D_0799[unitType] - $2C, i.e. 5 unit types x 4 frames. Own units are blue (zp_77 = 6), enemy red (zp_78 = 2).
- [game_main_0800_2762] Several branches that decide 'own vs enemy' on the unit index are self-modified: sub_7BC6 (overlay A) writes $90/$B0 into $2840, $63E2, $3F9E, $36D5, $379C, $4783, $5BF7 and the inverse into $206B, and sub_2A08 copies $2840 to $2846 and $5BFE with an optional inversion for the replay viewpoint D_0B9B. Annotators of those addresses should treat the opcode byte as a variable.
- [game_main_0800_2762] Drawing primitives in this chunk are shared by the whole game (the $C000 overlay text printer uses calcCellPointersXY and has its own copy of the glyph copy loop at $C227-$C231, which setBitmapDrawMode also patches at $C22B). The seed label 'rtiVector' at $C229 only describes the boot loader; after the runtime overlay is loaded $C229 is inside the glyph plot loop.
- [game_main_0800_2762] sub_FD5D in the resident high code is the pseudo-random number generator (24-bit shift/feedback on zp_57-59, returns A); the overview's 'IRQ handler region $FD5D-$FD6F' is actually this PRNG.
- [game_main_0800_2762] Game-over result strings live at the end of overlay A ($879F..): 'WON ON POINTS!', 'IT WAS A DRAW.', 'WE WERE KNOCKED OUT!', 'WE WON BY A KNOCKOUT!', 'GAME OVER.', 'DRONE ALERT!', matching message ids $11-$16 queued by computeGameResult.
- [game_main_0800_2762] Outside this chunk: the .byte block at $403B-$405D decodes as valid code (lda D_0B9F / cpx #0 / beq / eor #1 / tay / lda #6 / sta $92B2,y / lda $0C8A,y / tay / lda $F9C4,y / ora #$C0 / sta $F9C4,y / cpx #0 / beq / sty $923B / rts) and is probably an orphaned or indirectly reached routine that the disassembler missed.
- [game_main_0800_2BDD] Screen cell addressing: sub_2A9F takes zp_18 = row (0-24) and zp_19 = column (0-39) and builds bitmapPtr (zp_B4/B5) from the row tables $9300 (lo)/$9319 (hi) plus the column tables $9332/$935A, and screenPtr (zp_B6/B7) from $9382/$939B plus the column. Bitmap $A000, screen RAM $8C00, colour RAM = screen + $4C00. The game font is 8 bytes per character at $9426 (sub_2AC8: glyphPtr = $9426 + char*8); $93B4 is a per-character default colour table (0 = use fillColourValue).
- [game_main_0800_2BDD] Character drawing modes are self-modified opcodes: sub_2A38 (Y = 0 LDA/1 ORA/2 EOR/3 CMP=keep) patches the bitmap operation at $2AF3 and also the $C000 overlay's copy at $C22B; sub_2A47 patches the colour-byte operation at $2B00 and $2A85 (absolute forms). sub_2A56 resets both to plain copy. sub_2BCE toggles a 2x2 colour block with EOR (cursor highlight).
- [game_main_0800_2BDD] The window system: 27 windows defined by four parallel byte tables at $2BDD/$2BF8/$2C13/$2C2E plus the pointer table $2C49 to 9-tile frame sets at $9824-$987D (tile order TL, T, TR, L, centre, R, BL, B, BR). Window 0 (col 0,row 0,22x22) frames the overview map, window 1 (col 24,row 0,16x12) frames the 7x5 battlefield view, window 2 (col 24,row 11,16x11) the message/status panel; windows $18/$19 are the main menu frames. sub_2B75 draws the small ring glyph $38 (a rivet) at the four inner corners of the current window; sub_2BB7 draws two-row tall digits from the pair table $9810.
- [game_main_0800_2BDD] Sprites: initSpritePointers puts $28-$2F in $8FF8-$8FFF so sprite n's shape lives at $8A00 + n*64 ($8A00-$8BFF, the 'work RAM' in the overview). Sprite 0 (and 1) are the battlefield cursor / group formation ghost (Y-expanded, 18-byte shapes at $9E6E square, $9E80 cross, $9E92 block), sprite 3 ($8AC0) holds the path/route line, sprite 4 gets circle blobs. $9025-$9064 is an off-screen 64-byte sprite build buffer selected by a negative sprite number in plotSpritePixel. All sprite register writes go through the shadows $9008/$9010/$9018/$9022/$9023/$9024 copied by the IRQ.
- [game_main_0800_2BDD] Overview map geometry: mapCellToSpriteXYRaw maps map cell (col,row) to sprite coordinates (col*4+$1F, row*4+$39), i.e. the whole 40x40 battlefield is drawn at 4x4 pixels per cell in the 20x20-character overview panel at screen column 1, row 1 (window 0), while the 7x5-cell zoomed view at column 25 uses 16x16 pixel cells. The cursor/centre cell is (viewOriginCol+3, viewOriginRow+2).
- [game_main_0800_2BDD] Group move mechanics: buildGroupFormationSprite ($30C0) collects all live units whose unitFlagsTable & $7C equals selectedGroupId | $40, uses their destination positions (unitDestCol $F7D0 / unitDestRow $F834, low 7 bits) to plot a 2x2 dot per unit into a sprite, and stores the box extents relative to the selected unit in $922F (left, <= 0), $922E (right), $9231 (top, <= 0), $9230 (bottom). The existing 163A survey names for $922E/$922F and $9230/$9231 appear swapped (left<->right, top<->bottom); sub_4B5E and keepGroupBoxInsideMap confirm the assignment given here.
- [game_main_0800_2BDD] Unit record layout seen here (100 units, parallel arrays $64 apart): $F640 unitCol, $F6A4 unitRow, $F76C status (bit 7 = dead/unused), $F7D0 destination column, $F834 destination row, $FB54 flags (bits 2-6 group id).
- [game_main_0800_2BDD] Circle drawing: drawCircleInSprite ($3239) uses precomputed octant point lists for radii 1-10 at $9F69.. (pointers at $9F37/$9F41 dx, $9F4B/$9F55 dy, counts at $9F5F) and plots the 8 symmetric points about sprite pixel (12,10); spritePlotMode ($91F3: $80 toggle, $40 set, 0 clear) chooses the pixel operation. Radius 1 is a filled 3x3 dot; larger radii are outlines (range rings).
- [game_main_0800_2BDD] magnifyWindow2x ($2E0F) is an in-place 2x zoom of a cell rectangle about its centre, used by overlay B ($7552) as a repeated zoom-in animation; it relies on the $C000 overlay's double-size glyph renderer sub_C24B (also used by the text printer when zp_44 bit 7 is set) and the edge flags in zp_BA.
- [game_main_0800_2BDD] The disassembly file's line numbers do not match the chunk specification: lines 4395-5251 hold $29C9-$30A1; the address range $2BDD-$32D9 is at file lines 4714-5603. This survey covers the address range.
- [game_main_0800_32DA] Address/line mismatch: the chunk spec says lines 5252-6187 but those lines hold $3040-$3625; the address range $32DA-$3C4F is at file lines 5604-6835 and that is what this survey covers.
- [game_main_0800_32DA] Unit records are 15 parallel 100-byte arrays starting at $F640 (stride $64): $F640 col, $F6A4 row, $F708 flags (facing bits 0-1, side bit 5, first-strike bit 7), $F76C type (0 GRUNT 1 RIDER 2 BOOMER 3 SPY 4 COMCEN, bit 7 destroyed), $F7D0/$F834 destination col/row (bit 7 pending), $F898/$F8FC secondary col/row with mode bits, $F960 stun, $F9C4 energy (0-50) + visibility bits 6/7, $FA28 signed suppression, $FA8C hit effect, $FAF0 terrain cell under the unit, $FB54 status flags. Units 0-49 belong to side 0 with the comcen at 49, units 50-99 to side 1 with the comcen at 99 ($0C8A table, $9236).
- [game_main_0800_32DA] The map cell of an occupied square holds $80|unitIndex; the terrain the unit covers is kept in $FAF0. Code often indexes the tables with Y = cell value by using a base 128 lower ($F688,y = $F708+unit, $F6EC,y = $F76C+unit, $FA70,y = $FAF0+unit). Terrain values map to classes 0-8 through $077E, and classes to heights through $38B3 (LOS and combat) and sight bonuses through $35BB.
- [game_main_0800_32DA] Side-dependent branches are patched at run time: overlay A's routine at $7BC6-$7BF8 writes BCC/BCS ($90/$B0 chosen by playerSide $0B9F) into the 'cpy #$32' tests at $206B, $2084, $2840, $36D5, $379C (stale), $3B2C, $3BD0, $3F9E, $4783, $5BF7, $63E2 and sets comcenUnit $9236. The unit scanner has its own filter patch ($37FF compare value, $3800 opcode) set by setScanFilterOppositeSide.
- [game_main_0800_32DA] Visibility model: each unit-processing cycle calls ageUnitVisibility (energy bit 7 := bit 6, bit 6 := 0), then every own unit runs scanVisibleEnemies in spotting mode (X=0) which sets bit 6 on everything it sees and bit 7 + redraw on newly seen units, followed by an attack-target pass (X=$80) whose best candidate (closest, comcen preferred) is attacked via resolveAttack. Sight ranges come from $35A6 (+terrain bonus, max 10) and distance tests use squared distances against r*(r+1) from $35B0; line of sight uses the Bresenham walker $FBB8/$FBFA with a ridge rule (a descent after an ascent blocks) in checkLineOfSightStep. Boomers fire at range 6 without line of sight but only at already-visible targets; spies see 5 cells and are the only units that can spot spies beyond dist2 20.
- [game_main_0800_32DA] Combat (resolveAttack/applyHitToUnit): damage = baseDamageTable[attacker][target] + optional one-time first-strike energy sacrifice (up to 15, Boomers 5) + terrain height difference + 10 on class-1 terrain (+2 on class 3/5) + facing bonus (20 from behind, 10 from the flank, 0 head-on) +/- the $F76C bit 6 modifier; half of that is applied. Comcens take half again, capped at remaining energy, and every comcen hit adds 2x damage to the attacker's 16-bit score ($92A2/$92A4 per side) and bumps $9208. A comcen reaching 0 energy ends the game: $0BA8 |= $80|$20<<(side+0/1) and sub_4027 is entered with X=1 if the opponent's comcen died, 0 if the local one did.
- [game_main_0800_32DA] Sprite allocation seen here (data at $8A00+64*n): sprite 1 = unit selection marker (hidden by sub_32EA), sprite 2 = black double-size centre cursor of the battlefield view (box $9EA4 or diamond $9EE3 at X=256,Y=$4D) and the comcen cross marker in overlay B, sprite 3 = white shot line on the 4px/cell overview map (shown 8 frames, $91C8), sprite 4 = comcen rings / vestigial marker, sprites 0/5 = direction shapes from the $0200 buffer, sprites 5-7 = unit picture via sub_CD0A. sub_31A3 is 'enable sprites in A' (suppressed while the comcen is knocked out), sub_2FB2 clears sprite A's data, sub_31AF/sub_31CA plot a pixel (X,Y) into sprite $91CC with mode $91F3, sub_3239 draws a circle of radius X from the octant tables at $9F37-$9F68.
- [game_main_0800_32DA] $0200-$03FF is reused at run time as an RLE-decompressed sprite-shape buffer (sub_67CB, escape-byte run-length format; 8 shapes of 64 bytes). Overlay B decompresses $FD70 (screen 1) and $FE24 (screen 2) from the 'unreferenced' high-memory data into it; sub_CD0A decompresses the five unit pictures at $CD52/$CDD3/$CE3B/$CECD/$CF3A and copies the first 192 bytes into sprites 5-7.
- [game_main_0800_32DA] Five unreferenced code fragments in this range are rendered as .byte: $32F6 (hide sprite 0), $33C8 (old marker-sprite setter, stale JSR into the middle of sub_3456), $34C6 (old move-order executor with clamp helper $355D), $379E (all-units scan entry), $3C50 (old Boomer target assignment). Together with the never-written $923E/$3425/$3437 they show this build kept an older, partly removed feature (a timed marker sprite).
- [game_main_0800_32DA] Self-modifying operands in this chunk: $346E loop counter, $34C1 ORA value, $3373/$3374 copy destination, $359C ORA value, $35F9 range-table index, $362B distance threshold, $37FF/$3800 scan filter, $385A reverse-loop bound, $393F sacrifice amount, $3971 damage reduction, $3A4E sound number, $3BF7 visibility bits, $3C0F hit amount, and the marker cell at $3425/$3437. Annotators should treat these as variables, not constants.
- [game_main_0800_32DA] The scan list $0400/$0432/$0464 (50 entries each) overlays the copy-protection table, which is only needed during the manual check at start-up.
- [game_main_0800_3C50] Unit records: 100 units, index 0-49 = side 0, 50-99 = side 1, the last unit of each side (49, 99) is the comcen. Fourteen parallel 100-byte arrays start at $F640 right after the 40x40 map: F640 col, F6A4 row, F708 flags (facing/group/side), F76C type (0 grunt, 1 rider, 2 boomer, 3 spy, 4 comcen; bit 6 dug in, bit 7 dead, $FF empty), F7D0/F834 destination (F7D0 bit 7 = idle), F898/F8FC waypoint (bit 6/7 cloak flags resp. blitz flags), F960 stun/dig-in, F9C4 energy (0-50) + spotted bits, FA28 movement points, FA8C pinned timer, FAF0 saved map byte under the unit, FB54 display flags.
- [game_main_0800_3C50] A unit on the battlefield map is stored as $80 | unitIndex in the cell; sub_25FA(X = col, Y = row) returns the cell byte in A (N flag = unit present) and the cell address in zp_48/49. Routines index other per-unit arrays with the raw cell byte by using base-$80 addresses (e.g. $FA70,Y = unitUnderCell, $F688,Y = unitFlagsTable, $F818,Y = unitWaypointCol, $F6EC,Y = unitTypeTable).
- [game_main_0800_3C50] The map cell value $67 is the recycler glyph. Each side's recycler position is D_92A8/D_92AA (bit 7 = none, when recyclerMode D_92A6 = 2). The own recycler is drawn at game start by the comm module (JMP sub_3F27); the enemy recycler only appears after one of the player's uncloaked units comes within radius 3 (sub_3F57, message 8 'SPOTTED RECYCLER!', then the routine patches itself to RTS). Dead units respawn at the recycler (sub_3E23, message 5 'UNIT RECYCLED'); with recyclerMode 1 two dead units of one type are needed for one respawn.
- [game_main_0800_3C50] Candidate lists at $0400 (unit index), $0432 (distance metric dx^2+dy^2 from table D_3860, capped $FF) and $0464 (direction 0-3) are built by sub_37AA (A = radius, X/Y = centre) for the units of the side opposite to the reference unit passed to sub_378B, which patches the CMP #$32 / BCC-BCS pair at $37FE/$3800. sub_35C3 then removes the ones without line of sight (entry = $FF). zp_2F holds count-1. These lists overwrite the copy-protection answer table at $0401, which is only used at start-up.
- [game_main_0800_3C50] Boomers (type 2) keep a persistent target lock in boomerTargetTable ($91DC, indexed by the boomer's ordinal among all boomers, computed by sub_3C5D). SET TARGET / CLEAR TARGET in the unit option menu send commands $87 (handler $3C50, hidden as data) and the clear command (handler at $515D, also hidden as data). sub_3C85 keeps the lock while the target stays visible, otherwise prefers the enemy comcen, then the nearest visible enemy.
- [game_main_0800_3C50] Game end: the caller stores the reason in D_0BA8 ($08 time up, $A0/$C0 comcen of side 0/1 destroyed) and calls sub_4027, which halts and reveals all units, shows the result (sub_28F1 compares the 16-bit scores D_92A2/D_92A4 vs D_92A3/D_92A5) and starts the $78-tick countdown D_92C9. The comcen never gets the dead bit; its destruction sets D_0BA8 instead.
- [game_main_0800_3C50] Overlay A's sub_7BC6 patches branch opcodes in the resident code per player side ($90 BCC for side 0, $B0 BCS for side 1) at $63E2, $2840, $3F9E, $36D5, $379C, $4783, $5BF7 and $206B (EOR $20 variant): every 'is this one of my units' test in main code is a CPX/CPY #$32 followed by a patched branch. In this chunk that affects sub_3F97 (initial facing: own units face up).
- [game_main_0800_3C50] Message ids used here map through the pointer tables at $0500/$0524 (lo/hi) into the $8800 strings: 5 = 'UNIT RECYCLED', 8 = 'SPOTTED RECYCLER!', $0C = 'COMCEN STUNNED!'. sub_C560 drops the message in solo/film mode ($0B9B bit 7); sub_C566 always queues it (priority-sorted 8-entry queue at $9249).
- [game_main_0800_3C50] The unit option menu strings live in the $C000 overlay: items 0-$0B at $C9D2 + 16*n (FORMATION, MOVE TO GOAL, CLOAKING ON, NO CLOAKING, CLEAR TARGET, SET TARGET, MEMBERSHIP, JOIN GROUP, DIRECT UNIT, SELF DESTRUCT, SET BLITZ, BLITZ OFF) and items $0C+ at $CAB2 + 8*n ((EXIT), ENERGY, BATTLE, RADAR, DRONE, DIG OUT, DIG IN). The menu code at $5E99 shows which unit bits they toggle: F898 bit 6 cloak, F8FC bit 6 blitz, FB54 bit 7 / F960 bits 5-7 dig in.
- [game_main_0800_403B] Line/address mismatch: the chunk spec said lines 7048-7882 but those lines hold $3DF5-$4480; this survey covers the address range $403B-$4986 (file lines 7360-8542), which is what the chunk name refers to.
- [game_main_0800_403B] Game engine structure: updateGameFrame ($40B2) is the per-frame heartbeat called from every UI loop (directly or via runGameFrames $4094 / waitFramesOrInput $406F). Every stepIntervalFrames ($92E3, default 4, film speed keys 1-9 give 8..0) it processes ONE unit from the round order $90FD; after all 100 units finishRound (L_45E6) ticks the game clock roundsRemaining ($91CB), hands out movement points and rebuilds the order. Commands are applied in lock step every 10 units (commandSlice $923C = unitOrderIndex/10, 50 in the setup phase) via sub_4DB7, and the step is postponed while opponentWaitTimer ($9223) is nonzero; the random shuffle of the base order uses the lock-step scenario RNG ($FD4A) so both machines stay in sync.
- [game_main_0800_403B] Unit records are 14 parallel 100-byte arrays starting at $F640 with stride $64: $F640 col, $F6A4 row, $F708 state flags (facing/group/side/blitz charge), $F76C type+dead+zone bits, $F7D0 dest col (+bit 7 stationary), $F834 dest row (+bit 7 repairing), $F898 path col (+cloak bits 6-7), $F8FC path row (+blitz bits 6-7), $F960 stun/dig-in, $F9C4 energy+visibility, $FA28 movement points, $FA8C hit-flash timer, $FAF0 terrain under the unit, $FB54 order flags (reload/pinned/group/stationary). Units 0-49 are side 0 (comcen $31), 50-99 side 1 (comcen $63); a map cell holding a unit contains unit|$80, so code also indexes the arrays with the map byte through aliases $80 lower ($F688 = $F708, $F6EC = $F76C, $F818 = $F898, $FA70 = $FAF0).
- [game_main_0800_403B] Movement model: each step walks one Bresenham cell from the unit's PATH position ($F898/$F8FC) toward its destination ($F7D0/$F834) and charges terrainMoveCostTable[$405E][class] (+8 diagonal) from the signed balance $FA28, which refills every round by speedClassRateTable[$4066][class] (12 or 25; a group uses its slowest member and worst terrain via groupMoveTable $91F4). Spies (type 3) ignore terrain and may enter occupied cells; other units may step onto same-side units or enemy spies, and cloaked units onto cloaked ones - in that case only the path position moves and the map position is realised later by walking back to the first free cell. A unit reaching a unit-occupied destination gets 100 points. Blitz (menu SET BLITZ, $F8FC bits 6-7) spends energy via sub_3EE2 for +25 points per step and a boosted shot ($F708 bit 7); it switches off automatically when the unit becomes stationary. Cloak ($F898 bits 6-7) and blitz are mutually exclusive and unavailable to spies.
- [game_main_0800_403B] Combat/visibility per step: spot scan (sub_35C3 X=0) sets visibility bits 6/7 of $F9C4 on enemies in sight and draws newly seen ones; fire scan (X=$80, boomers through sub_3C85) picks the nearest enemy (comcens preferred); a unit fires (sub_38C2) only on rounds where (roundsRemaining EOR unitIndex) AND 1 = 0 and sets the reload bit, which is cleared on the same parity two rounds later. Spies, comcens and cloaked units never fire. Stationary unstunned units either dig in ($F960 += $20 per round, fortified when bit 7 sets, also mirrored as $F76C bit 6) or regain 1 energy per round (max 50) with $F834 bit 7 = REPAIR shown in the unit status.
- [game_main_0800_403B] Territory scoring (moveUnitToCell): columns are mirrored for side-1 units (sub_3FA3 = 39 - col, also used to mirror opponent commands), col 0-9 is the far zone (2x) and 10-19 the near zone; a unit scores once per zone (bits 3/4 of $F76C) scoreValueBySide[$92AC] points (comcen x16) into the 16-bit scores $92A2/$92A4, which are indexed by the LOSING side (index i = points scored by side i^1; $28F1 compares them for WE WON/LOST ON POINTS).
- [game_main_0800_403B] Per-side scenario data at $92A6-$92B5: $92A6 recycler unit type, $92A8/$92AA recycler position (sub_3E23 respawns dead units there, 'UNIT RECYCLED.'; sub_3F57 reveals the enemy recycler with terrain byte $67 when an own unit comes within 3 cells, 'SPOTTED RECYCLER!'), $92AC zone score value, $92B2 comcen reveal timer; relative (0 = us, 1 = them): $92AE radar uses left, $92B0 drones left, $92B4 comcen speed class. $403B (misclassified as data) is called after a radar/drone launch and keeps the launching comcen visible for 6 rounds.
- [game_main_0800_403B] The comm-module build 1 (T35, $0BA5 bit 7) is not only a link variant: the engine calls its hooks sub_E0E8 after every unit step and sub_E079 every round (only in battle, not in films); $E0E8 inspects the side-1 unit records, which looks like the computer opponent / practice-mode AI (medium confidence).
- [game_main_0800_403B] Film playback controls inside updateGameFrame: keys 1-9 set the replay speed (message 7 'SPEED n' via the digit slot at $8730), CLR/HOME ($8C in the key table at $0D67, row 6 column 3) cycles $0B9B bits 0-1 through SHOW US/THEM/BOTH using the 4-byte names at $86FB copied into message 6 at $8726.
- [game_main_0800_403B] Large misclassified code regions adjacent to this chunk: $4987 onward (next chunk) is code (LDA #0 / STA $18 ... LDY #$63 / LDA $F76C,Y), and the command handlers $5011-$523D plus $5336-$54xx are rendered as .byte although the dispatch table at $4D2A (lo/hi pairs, command $80+n) points into them ($80->$5011 move, $82->$5023 cloak on, $84->$504B cloak off, $93->$5070 blitz on, $95->$5062 blitz off, $8E->$5336, $90->$53C0 radar, $92->$538B, $97->$53A3 drone?).
- [game_main_0800_4987] Command protocol: an order is a byte string consisting of a command byte $80-$A6 (bit 7 set) followed by 0-3 parameters; the total length comes from commandLengthTable ($4D78) and the handler from commandHandlerTable ($4D2A). Parameters are conventionally (unit, x, y) and are fetched by the untraced helpers $4FC4 (3 params -> zp_73/74/75), $4FD0 (2 params -> zp_74/75) and $4FF7 (1 param -> zp_73), all of which advance turnCommandCursor ($90B7). Remote commands (zp_76 bit 7) have their x/y mirrored by $4FE4 (39 - v via $3FA3) because each player sees the map from his own side; the setup phase puts own units in rows 0-19 and the opponent's in rows 20-39 of local coordinates.
- [game_main_0800_4987] Lock-step exchange: every 'turn' both machines swap one packet of at most 4 command bytes (whole commands only, built by $562B from the 40-byte outgoing ring at $9065 into $E027/$E01F and sent with sub_E000 X=1; the opponent's packet arrives in $E020/$E01E and D_E01D bit 6 signals completion). processCommandExchange then builds turnCommandBuffer ($908F) with player 0's commands first and player 1's second (player1CommandStart $92EC), executes them identically on both machines, records the turn into the film ($578A), acknowledges with sub_E000 X=2 and immediately sends the next packet. A throttle of 10 x 4 frames ($9163) and a timeout counter ($9223, evaluated by $5A14) frame the exchange. Practice mode must rely on the comm module / $EC00-$EFFF sub-overlays producing the 'received' packet locally, since the exchange code is the only route by which commands take effect.
- [game_main_0800_4987] Film (WATCH FILM) format, as consumed by processCommandExchange and written by $578A/$57BE: per turn [header = player1CommandStart << 4 | length][time stamp][command bytes]; the time stamp is one byte (clockDelta << 4 | subTick) when the game clock ($91CB, counting down) moved by 0-14 since the previous record, else two bytes (subTick, clock). Record byte $FE or $FF marks the end of the film (bit 0 = D_0BA8 & 1; $FF triggers the end-of-film score display $59D0). The film lives in RAM under the I/O area from $D034 (pointer zp_BC/zp_BD, end D_92F1/D_92F2) and is accessed with $01 = $34 by $5874 (read) / $5893 (write) with interrupts and CIA2 IRQs masked. $8D reports never reach the film because the mergers consume them.
- [game_main_0800_4987] Group model: a unit's group is bits 2-5 of unitGroupByte ($F708) with bit 6 = member; unitLocalFlags ($FB54) holds a second copy of the same bits that the UI changes immediately (ADDED/REMOVED) and that syncLocalGroupFlags ($4C6B) re-copies from F708 whenever a command changes membership - two sources of truth by design (local preview vs. lock-step state). selectedGroupBits ($9206) stores the id in the same bits-2-5 format. A group may never span more than 8 cells: isPointWithinGroupRange ($4B04) rejects a joining unit or a NEW POSITION target that is 9 or more cells from any edge of the group's bounding box ('TOO FAR!', string $CB31). GROUP menu strings in the $C000 overlay: 'ADDED' $CB3B, 'REMOVED' $CB40, 'NOT IN GROUP' $CB8D, 'RE-POSITIONED' $CB99, 'NEW POSITION' $CBA6.
- [game_main_0800_4987] Group commands implemented in this chunk: $81 unit x y = move the whole group so that the formation relative to the leader's destination is kept (moveGroupKeepingFormation); $88 unit group = join (joinUnitToGroup, the unit adopts the group's average movement vector); $89 unit = leave; $98 unit x y = move a member without leaving the group (setGroupMemberDestination, counted in D_929D/D_929E); $A3 unit mode = rotate the formation about the leader's destination by (dy,-dx), (-dy,dx) or (-dx,-dy) (rotateGroupFormation). Other handlers seen while tracing (medium confidence): $80 move unit and leave its group ($5011 -> $34C6), $8A place unit at x,y during setup ($51AB), $8C reason = abort/disconnect ($523E), $8D = cursor/drone position report (consumed by $52C8, shows a white sprite on screen 1), $90 x y = strike on a cell with D_92B0 strikes per side ($53C0), $91 = remove unit(s) ($542E), $9A unit amount = energy repair ($5479), $9B = halt a whole group ($549E), $9C = per-player option level ($54E1), $9D = choose side ($5511), $A4 = game type options -> $0BA3 ($559C), $A5 = points/per-player values ($55A5), $A6/$8B = scenario seed -> $0B95-$0B97 ($51EA/$51EE).
- [game_main_0800_4987] Mis-typed code outside this chunk (reached only through commandHandlerTable or from these handlers, so the tracer missed it): $4FC4-$4FE3 and $4FF7-$5004 (parameter fetchers), $5011-$523D and $5336-$55EB (all command handlers except sub_523E/sub_52C8/sub_532F), and $34C6-$3567 (unit leaves group + set destination: $34D5 with Y = unit, zp_74/75 = x/y clamps to 0-39, sets F7D0/F834, flags bit 7 of F7D0 and FB54 when the destination equals the position, refreshes F898/F8FC low bits with the position and clears F960 bits 5-7 and F76C bit 6). All of them decode cleanly with tools/m6502.py.
- [game_main_0800_4987] Self-modifying style: nearly every routine here patches immediate operands instead of using zero page (e.g. $49C6/$49E3 sign extension, $4B0F/$4B80 group keys, $4CD0-$4D05 in rotateGroupFormation, $4DE9/$4DF4/$4E20 film parsing, $4EEF handler vector). rotateGroupFormation even relies on the N flag left by 'ldx #rule' (bit 7 of the rule byte) to decide whether to negate, so the BPL at $4CDD/$4CF2 tests the rule, not the value.
- [game_main_0800_4987] D_0C8A = {$31,$63} is the unit index of each side's COMCEN (49 and 99; units 0-49 belong to player 0, 50-99 to player 1); the code at $44EB-$44F1 and $5402-$5410 special-cases these two indices. The game clock $91CB counts down in units subdivided by $923C (0-15, computed at $41EB); the game ends on time when it reaches 0 ($4606 sets D_0BA8 = 8) and message $1D is shown when it equals D_92EF.
- [game_main_0800_4FC4] Lock-step networking: every 10 game ticks ($41DD-$41EE: $9162 mod 10, or mod 50 in setup) $923C = tick/10 and sub_4DB7 runs. It waits for the comm module's $E01D bit 6 (own packet sent AND opponent's packet received), merges the two packets into mergedCmdBuffer $908F - always side 0's commands first, then side 1's ($92EC = split index, $92EB = total) - executes them through the dispatcher sub_4E91 and then records the merged buffer into the film, acknowledges the packet (sub_E000 X=2) and sends the next packet from the outgoing ring (sendOutgoingPacket). Because both machines execute identical command streams in identical order, only player commands are ever transmitted; the simulation itself is deterministic (shared RNG seed).
- [game_main_0800_4FC4] Command format: 1 command byte with bit 7 set ($80-$A6) followed by 0-3 parameter bytes; cmdLengthTable $4D78 (indexed by code & $3F) gives the total length, cmdHandlerTable $4D2A/$4D2B (lo/hi pairs) the handler. Bytes without bit 7 are skipped by their length (handler $5336 rewrites a suppressed $8E as $0E so the film keeps a no-op). zp_76 bit 7 tells a handler the command came from the opponent; coordinates are then mirrored with 39-x/39-y (mirrorCmdCoordsIfRemote) because each player sees his own side at the bottom.
- [game_main_0800_4FC4] Command codes (handler, length): $80 move unit,x,y ($5011,4); $81 move group relative ($501A,4); $82 cloak on unit ($5023,2); $83 group cloak on ($5098,2); $84 cloak off ($504B,2); $85 group cloak off ($50D2,2); $86 clear boomer target ($515D,2); $87 set target boomer,target ($516B,3); $88 join group unit,group ($5185,3); $89 leave group ($519B,2); $8A place unit,x,y ($51AB,4); $8B map seed x3 ($51EE,4); $8C session control reason ($523E,2: 0 abort/left game, 2 ready for new game, 3 pause/timeout, 4 resume, 5 disconnect); $8D drone move heading,x,y ($52C8,4, executed at merge time); $8E drone detonate x,y ($5336,3); $8F drone released ($537B,1); $90 missile strike x,y / setup counts ($53C0,3); $91 self destruct unit|$80=group ($542E,2); $92 drone arrived ($538B,1); $93 blitz on ($5070,2); $94 group blitz on ($50AA,2); $95 blitz off ($5062,2); $96 group blitz off ($50BE,2); $97 drone launch ($53A3,1); $98 move keeping group ($5470,4); $99 unused; $9A add energy unit,amount ($5479,3); $9B halt group ($549E,2); $9C set recycler mode,x,y ($54E1,4); $9D choose side ($5511,2); $9E comcen repaired / setup ready ($520C,1); $9F dig in ($5548,2); $A0 dig out ($554C,2); $A1 group dig in ($556D,2); $A2 group dig out ($5571,2); $A3 formation group,kind ($5596,3); $A4 game type ($559C,2); $A5 setup option kind,value ($55A5,3); $A6 custom map seed x3 ($51EA,4).
- [game_main_0800_4FC4] Outgoing side: queueCmdByte appends to the 40-byte ring $9065 (write $908D, read $908E); queueCmd2/3/4 and queueCmdBytePending add the parameters from $73-$75 and count the command in pendingOwnCmdCount $929F. sendOutgoingPacket moves at most 4 bytes (whole commands only) per exchange into $E027 and sets $E01F, so a packet never exceeds 4 bytes; isCmdRingNearlyFull makes checkFirePressed refuse new orders when less than 5 bytes are free. Nothing is queued during film playback ($0B9B bit 7).
- [game_main_0800_4FC4] Game film: recorded in the RAM hidden under the I/O area ($D000-$DFEF, accessed with $01=$34 and CIA2 interrupts masked: readFilmByte/writeFilmByte/copyPageUnderIo). Header $D000-$D367 (length, stream offset $0368, final scores, setupByte0B94, clock, checksum of $D000-$D032 at $D033, copy of $92A2-$92B5, copy of unit arrays $F640-$F95F), then records: [split<<4 | len] [time stamp: (clockDelta<<4 | exchangeCounter) or exchangeCounter, absoluteClock] [len command bytes]; end marker $FE (game ended by itself) or $FF (aborted game; playback then restores the final scores via finishFilmPlayback). Playback in pollOpponentLink waits until $923C and $91CB match the time stamp, copies the payload into $908F and runs the same dispatcher.
- [game_main_0800_4FC4] pollOpponentLink (sub_4DA9, $4DA9-$4E90, just before this chunk) is the idle hook called from menus and wait loops (11 call sites): housekeeping $C4EF, then film playback, or menu-level session polling (pollMenuSession when $0B9C bit 7), or in-game waiting for the opponent's packet with checkLinkTimeout (PHONE TROUBLE after ~13 s, reconnect/give up after ~17 s). waitForExchangeIdle loops on it until linkTimeoutCounter is 0.
- [game_main_0800_4FC4] Session handshake: readyForNewGameHandshake exchanges [$8C,02] packets until both sides sent it ($0B9C = 2); at menu level a received [$8C,02] prints 'OPPONENT READY FOR NEW GAME.' and [$8C,05] hangs up. Pause/resume uses $8C reason 3/4 with pauseState zp_BE bits (both players must resume unless comm build 1); during the setup phase the 1-byte $9E plays the same 'ready' role (F1 when done), while in battle $9E un-stuns the sender's comcen.
- [game_main_0800_4FC4] Strategic overview (mini-map): 20x20 characters at screen (1,1), 2x2 map cells per character, each cell a 2x2 multicolour-pixel quadrant ($90B8 work buffer, masks $5B64/$5B68, patterns $5A9B via $5A59/$5A7A). Units are 'blobs' ($5AB7, $5ABF cloaked) coloured through the mask $922B (complemented for the other side); enemy units only when spotted (F9C4 bit 7), revealAllFlag or replay view $82. Dotted quarter lines at character rows 6/11/16 (map rows 10/20/30). drawMapCell (A=cell,X=col,Y=row) is the single entry used by the engine to refresh a cell in the viewport (drawViewportCell, 2x2 characters at column 25 row 1), the overview and overlay-B screen 1 (drawScreen1Cell, 21x17 cells at character (3,3)).
- [game_main_0800_4FC4] Side-dependent branch patching also hits this chunk: $5BF7 (patched by overlay A $7BE1) and $5BFE (patched by sub_2A08 at $2A16) are BCC/BCS opcodes selecting whether unit index < 50 means 'own unit' in the overview drawer.
- [game_main_0800_4FC4] The given line range 8824-9954 does not correspond to $4FC4-$5E13; the chunk by address is lines 9188-10643 of main_0800.s and that is what this survey covers. $5E14-$5E16 (3 data bytes) belong to the next chunk.
- [game_main_0800_5E14] Unit command UI flow: the battlefield loop calls handleOwnUnitClick ($5E17) on a fire click over an own unstunned unit. showUnitInfoPanel (sub_22A2) then waits; moving the stick starts SETTING DESTINATION (sub_6500 -> runTargetCursorLoop with zp_BB = 0, uiLevel 1 = single unit, 2 = whole group), a second click opens the UNIT OPTIONS or GROUP OPTIONS menu. The option menus exist only when gameTypeOptions ($0BA3) bit 3 is set; the per-game-type option bytes at $78CA (overlay A) are 00,01,02,13,14,1D,1E, i.e. Face-Off, Sluggers, Full War and Defender have the menus, Scrimmage/QB Sneak/The Bomb do not.
- [game_main_0800_5E14] Menu engine ($5FB1-$612D) shared by all in-game menus (unit, group, join-group, formation sub-menu at $65FF, repair menu at $652E, misc menu via $1C59 and the overlay menus at $7F40/$835D/$83CE): addMenuItem stores item ids in D_91CE (max 6) and prints the names; D_91D4 count, D_91D5 selection, D_91D6 first screen row; the selected row is highlighted by EORing the screen-RAM colour bytes of the row with zp_3D and blinks every 20 frames (zp_6E, phase D_92C5). Joystick up/down move without wrapping (sound 2, auto-repeat via sub_0D49, extra 30-frame pause before the last row which is normally (EXIT)); fire confirms; death of the unit, game end (D_92C9) or a sub_C451 abort (SPACE, function key, game over) select the last item and leave. The loop returns A = item id, Y = index, or A = Y = $80 when the unit's idle bit (FB54 bit 7) changed so the caller rebuilds the menu (DIG IN/OUT vs BLITZ items). Three self-modified operands: $602B idle snapshot, $60BB slow row, $60BF direction.
- [game_main_0800_5E14] Menu item ids and strings in the $C000 overlay: 0 FORMATION, 1 MOVE TO GOAL, 2 CLOAKING ON, 3 NO CLOAKING, 4 CLEAR TARGET, 5 SET TARGET, 6 MEMBERSHIP, 7 JOIN GROUP, 8 DIRECT UNIT, 9 SELF DESTRCT, 10 SET BLITZ, 11 BLITZ OFF (16 bytes each at $C9D2 + id*16, printed via sub_C100 with X = id*2); 12 (EXIT), 13 ENERGY, 14 BATTLE, 15 RADAR, 16 DRONE, 17 DIG OUT, 18 DIG IN (8 bytes each at $CAB2 + (id-12)*8). Items 1 and 9 are never offered by this code. Group names ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER are 8-byte strings at $C92C. Other strings used: $CB0A '  UNIT'/'  OPTIONS', $CB13 'OPTIONS', $CB1F 'GROUP', $CB25 'WHICH GROUP:', $CB31 '  TOO FAR!', $CB3B 'ADDED', $CB40 'REMOVED', $CB47 'FINISHED', $CB4F 'SETTING', $CB56 'DESTINATION', $CB61 'TARGET', $CB8D 'NOT IN GROUP', $CB99 'RE-POSITIONED', $CBA6 'NEW POSITION', $CBD6 'REPAIR/WHICH:'.
- [game_main_0800_5E14] Outgoing command bytes queued by the menus (sub_561F: command byte + zp_73; sub_560F/sub_560A: + zp_74 + zp_75): $82/$83 cloak on unit/group, $84/$85 cloak off unit/group, $86 clear boomer target, $87 set boomer target (from the target cursor), $88 join group (unit, group key), $89 leave group (unit), $93/$94 blitz on unit/group, $95/$96 blitz off unit/group, $98 reposition unit in formation (unit, x, y), $9B halt group (group key), $9F/$A0 dig in/out unit, $A1/$A2 dig in/out group, $A3 formation sweep left/right/reverse (unit, option 0-2). Group commands carry the group key FB54 & $3C (group number bits 2-4 | side bit 5).
- [game_main_0800_5E14] Received-command dispatch (outside this chunk, hidden as data): sub_4D9F/$4EDB takes the command byte & $3F as an index into the word table at $4D2A (entries for $80-$A6) and JSRs through the patched operand at $4EEF. Handlers $4FC4-$55EE are also shown as .byte: $5023 cloak on ($F898 |= $C0, spies excluded), $504B cloak off, $5062 blitz off, $5070 blitz on, $5098/$50AA/$50BE/$50D2 group variants, $515D clear target, $516B set target -> $3C50, $5185 join -> $4AD4, $519B leave, $5470 reposition -> $4C59 (decrements D_929E), $549E halt group, $5548/$554C dig in/out unit ($F960 bits 5-7), $556D/$5571 group dig, $5596 formation -> $4C84.
- [game_main_0800_5E14] Group bookkeeping: D_9206 = selected group key, D_9205 = key highlighted in the viewport (drawViewport inverts the members when uiLevel is 2), membership test everywhere is (FB54 & $7C) == key | $40. sub_4AA3 adds the selected unit to group D_9206 only if it is less than 9 cells away from every edge of the members' bounding box (sub_4B04/sub_4B5E; an empty group always accepts), else TOO FAR! (sound 3, message at row 16). Formation repositions are tracked in D_929D/D_929E and sub_65E7 ($65E7, outside the chunk) waits for the engine to process them before the group is moved again.
- [game_main_0800_5E14] The self-modifying own-side test pattern (CMP #$32 followed by a branch opcode patched to BCC/BCS by overlay A) appears here at $63E2; countGroupMembers ($650E) instead derives the side bit from the unit index (>= 50 -> OR $20) without patching, because the side bit in FB54 is absolute.
- [game_main_0800_5E14] Line numbers in this assignment did not match the addresses: $5E14-$652D are file lines 10644-11584 of disassembly/game/main_0800.s; the survey follows the address range.
- [game_main_0800_652E] The chunk spec's line numbers were wrong; $652E-$6EFD is lines 11586-12009 of main_0800.s (the address range was used).
- [game_main_0800_652E] Sound architecture: game code calls sub_C89A (A = sound id 0-28, X/Y preserved) which uses the priority/class table D_C87D and the 4-slot queue D_92E7-D_92EA ($FF = empty; ids >= $1D are rejected, id 0 clears the queue and silences everything). The raster IRQ ($1283) dequeues with $C8E9 into X, starts it with startSound ($6728) and then runs soundPlayerTick ($6637) every frame. sndVoiceSoundId $67C8[voice] tells the game which sound a voice is playing (endGameToMainMenu at $1AEC skips the overlay reload while voice 0 plays 13/14, sub_C8FA checks voice 2 for $14/$16).
- [game_main_0800_652E] Sound script bytecode (one stream per voice, pointers zp_D8-zp_DD): 00 n = wait n ticks (00 00 = end), 01 n = SID control byte, 02 n / 03 n = loop counters A/B, 06 lo hi = frequency, 07 lo hi = pulse width, 08 ad sr = envelope, 09 lo hi = per-tick frequency delta, 0A lo hi = per-tick pulse delta, 46 lo hi / 47 lo hi = jump (unconditional while the counter is 0, else decrement and jump while nonzero, so 02 03 + 46 target loops three times), any byte >= $80 = add zp_5A to the frequency low byte. Per-voice state lives in eight 6-byte rows at $6792-$67C1 indexed by voice*2.
- [game_main_0800_652E] Sound id usage seen at the call sites of sub_C89A: 0 stop all ($0EFE stopAllSound), 1 fire-button click, 2 menu move/typing/connection, 3 error/alert beep (disk messages, aborts), 5 chat RETURN received, 8 own comcen hit, 9 ($3A54/$79F8), $0F/$10/$11 overlay-A screens, $14 and $1C comcen sounds (sub_C8FA), $16 ($3F14 recycler), $17 ($3D2C/$79F3), $18 repair attempt. Sounds 13 and 14 are three-voice rising/falling chords (likely the win/lose fanfares), 25 a three-voice alternating siren.
- [game_main_0800_652E] Menu system used here (selector sub_5FD7/sub_5FDC/sub_5FD1 and addMenuItem sub_5FB1 are in the previous chunk): addMenuItem appends item id X to menuItemIdList $91CE and prints it - ids 0-11 are 16-byte strings at $C9D2 (FORMATION, MOVE TO GOAL, CLOAKING ON, NO CLOAKING, CLEAR TARGET, SET TARGET, MEMBERSHIP, JOIN GROUP, DIRECT UNIT, SELF DESTRCT, SET BLITZ, BLITZ OFF), ids 12-18 are 8-byte strings at $CAB2 ((EXIT), ENERGY, BATTLE, RADAR, DRONE, DIG OUT, DIG IN). The selector returns A = id of the chosen row and Y = D_91D5 = row; when the UI is interrupted (checkUiContinue $C451 = 0, game over, comcen stunned in repair mode) it selects the last row, which every menu here makes (EXIT). selectedUnit $FF selects the repair-menu behaviour (system marker blink), $FE a plain menu. Menus printed by other code (printOptionsList, the formation list) set D_91D4/D_91D6 themselves and skip addMenuItem, so A on return is meaningless and only Y is used.
- [game_main_0800_652E] Voice pause protocol (sub_65A0): with a carrier, command $8C reason 5 is sent, D_91D5 is parked at $81 and the frame service loop runs until the $C000 overlay (sub_C86D -> $C768, triggered by zp_BE bit 7) calls voicePauseReconnect ($1BF7), which hangs up, waits for a new connection and clears bit 7 of D_91D5.
- [game_main_0800_652E] unpackRleTo0200 ($67CB) reveals that the 'unreferenced' high-memory data at $FD70 and $FE24 (survey game_high_FBB8) and five blocks in the $C000 overlay ($CD52, $CDD3, $CE3B, $CECD, $CF3A via sub_CD0A) are RLE-packed 512-byte blocks (first byte = escape marker, marker/count/value runs). Overlay B has a sibling at $8644 that unpacks 10 rows of 160 bytes straight into the bitmap with the same fetchRleByte ($681A).
- [game_main_0800_652E] Unit glyphs: the 2762 survey's range $6C5D-$6E7C is short; the pointer table has 20 entries and the shapes run $6C5D-$6EDC (5 types x 4 facings up/down/left/right, matching unitFlagsTable bits 0-1 and D_0799 = $2C,$30,$34,$38,$3C). The rest of the page to $6EFF is zero.
- [game_main_new_0DB7] Input model of the whole game, confirmed here: the KERNAL is banked out ($01=$35), so rasterIrqHandler ($10F1, raster line $FA) does all input itself. Every frame it calls pollKeyboardEvent -> comm entry $E015 -> scanKeyboard ($0DB7), and then - only if no key is pending - reads joystick 2 from CIA1_PRA (which scanKeyboard has just left configured as all-outputs-high). A pending keypress therefore suppresses joystick input for that frame, and joystick movement in either port makes scanKeyboard report 'no key'.
- [game_main_new_0DB7] keyMatrixCodeTable ($0D67) is indexed by row*8+column with row = the CIA1 PA line and column = the PB bit; the codes are PETSCII|$80 for printable keys, 0/1/2/3 for F1/F3/F5/F7, $88 DEL, $8D RETURN, $8B/$8E cursor, $A0 space, $FE for the shift/C= cells (skipped) and $FF for CTRL (aborts the scan).
- [game_main_new_0DB7] SHIFT + C= + F7 is the global abort hotkey: rasterIrqHandler detects it at $124F-$126B ($92E4 and $92E5 both negative and currentKey = 3), sets $0BA7=1 and inputLockoutTimer=$FF and JMPs to returnToMainMenu ($0AA3), which rebuilds the stack pointer - so that IRQ never RTIs.
- [game_main_new_0DB7] The IRQ is split in two halves. Phase 1 (VIC_CTRL1/CTRL2 from $9000/$9001 plus comm entries $E000 and $E00C) always runs; phase 2 (all other VIC shadows, the eight sprite colour/position/enable shadows, frame counter, input, timers, chat, sound) is guarded by $0BA7. But $0BA7 is only ever incremented from 0 around disk loads, so the bit-7 test at $112F never fails and the early exit at $1134 looks unreachable; the effective use of $0BA7 is the keypress suppression at $1246.
- [game_main_new_0DB7] Sprite colour shadows $9008-$900F encode flashing: a value < $10 is a plain colour, a value >= $10 packs two colours in its nibbles and the IRQ alternates between them on odd frames by comparing the current $D027+n against the low nibble. Positions come from $9010/$9018 with fine-scroll biases $92F4 & 7 and ($92F5 & 7) - 3 applied when $92C8 is non-zero, so sprites track the smooth-scrolled tactical view.
- [game_main_new_0DB7] Modem chat protocol: typing is opened with RETURN, one typed character per frame is queued to the opponent through comm entry $E009 (16-byte tx ring, depth at $E02E) and $8D (RETURN) ends the message. The receiving side pulls bytes with $E006 and appendChatCharacter builds them up in $9253-$9276 (length $9277, $80 = complete) while mirroring the text into the status line at $0548/$0549. The outgoing edit buffer $9278 is shared with the SAVE GAME FILM name entry, which is why handleChatSend stands down while $92E6 is non-zero.
- [game_main_new_0DB7] String terminator convention seen from the writer's side: appendChatCharacter stores the whole line plain and sets bit 7 only on the newest character, rewriting the earlier ones from the plain buffer each time. So bit 7 always marks exactly the current end of the line.
- [game_main_new_0DB7] updateFrameTimers ($129C) is the game's single timer tick. It also drives the link watchdog $9223: checkLinkTimeout ($5A14) arms it by ORing in bit 7, the tick counts it up to $FF every 8 frames (every 16 while zp_B1 = $FF, i.e. slower during actual play) and a completed packet exchange clears it at $4E69; $E1 is the retry threshold.
- [game_main_new_0DB7] All disk access in this chunk follows the same pattern: suspendCommForDiskAccess ($10CB) -> loadSectorsDelayed ($0F55, an 8-frame settle delay before every fast-load request) -> resumeCommAfterDiskAccess ($10D1), with $0BA7 bumped by the caller so keystrokes are dropped meanwhile.
- [game_main_new_0DB7] Dead code found: appendChatCharacter at $1400 loads messageQueueHead ($9251), compares it with messageQueueTail ($9252) and branches to $1408 - but the fall-through path is $1408 as well, so the test has no effect. Likewise zp_60 and zp_61 are written by the IRQ at $1216/$1210 and have no reader anywhere in the traced code.
- [game_main_new_0DB7] Corrections to the phase-1 knowledge base: $0EE5 (previously getSoloOrFlag923F, recorded as unreferenced) is called from $658B to gate attemptSystemRepair, and $0B9B is the film-playback flag ($82) rather than a solo-mode flag (the solo trainer sets $0B9B=0 and $0BA5=$FF), so the name checkFirePressedNotSolo at $0ED5 is misleading.
- [game_main_new_0DB7] $100D is a second entry point into checkDiskId's failure tail, jumped to from overlay A at $7E71 purely to display 'NOT A DATA DISK!' and return C=1.
- [game_main_new_17DA] The whole chunk implements the REPAIR tab of the in-game console. It is reached only through menuItemHandlerTable ($1535, entry 1 = $17DA) via the self-modified JSR at $148D in mainMenuLoop, so nothing calls repairTabAction directly and it does not appear in XREF.txt as a callee.
- [game_main_new_17DA] Comcen damage model: comcenSystemStatus ($9241) holds four bytes - [0] is a derived display value for the energy (0 = 100%, $40 = 21..99%, $80 = under 21%, recomputed by printEnergyValue), while [1..3] are the three repairable systems, set to $80 only by rollComcenSystemDamage ($3D57) and cleared only by attemptSystemRepair. The three system states are never transmitted: each machine damages and repairs its own comcen locally. Only the energy repair goes over the link, as command $9A (cmdAddEnergy, unit + amount), so that both copies of unitEnergyTable stay in sync.
- [game_main_new_17DA] Repair odds are hard-coded: energy repairs succeed 1 time in 2 (random & 1), the other three systems 1 time in 4 (random & 3), and the comcen stun repair 1 time in 8 (random & 7) - and the stun repair is refused outright while the stun counter ($922A & $0F) is 8 or more. A successful stun repair queues the single-byte command $9E (cmdComcenRepairedOrSetupReady), which zeroes the comcen's stun counter in unitStunTable on both machines. Each energy repair adds (50 - currentEnergy)/8 + 1 points, i.e. repairs get smaller as the comcen recovers.
- [game_main_new_17DA] Before opening the REPAIR WHICH menu, repairTabAction issues command $80 (cmdMoveUnit) with the comcen's own current column and row whenever unitDisplayFlags[comcen] bit 7 is clear (still moving) or bit 6 is set (group order active). Because the $80 handler clears the group-membership bit and sets the destination, this is the game's idiom for 'halt this unit and take it out of its group' - the comcen must stand still while it is being repaired.
- [game_main_new_17DA] Marker geometry and colours: each system marker is a plus of five 1x1 colour cells built by five fillScreenColourRect calls around the origin (systemMarkerRowTable[i], systemMarkerColumnTable[i]); erasing uses a single 3x3 fill. The colour bytes are screen-RAM nibble pairs with a fixed high nibble 1: $15 ok, $17 degraded, $12 damaged, $16 blank. $16 matches the schematic background that drawRepairTab paints as (textColour & $0F) | $10, which is $16 for the normal panel colour $66 - eraseSystemMarker hard-codes $16, which would be wrong for the stunned panel ($12), but markers are never drawn while the comcen is stunned so the case cannot occur.
- [game_main_new_17DA] drawSystemMarker keeps its system index in the self-modified LDY operand at $18A4 across the call to printEnergyValue (which clobbers Y); the same trick is used by runGameFrames at $40B0. Reading $18A4 as data would be wrong - it is an immediate operand.
- [game_main_new_17DA] zp_6C-zp_71 are six general frame countdowns decremented once per frame by the IRQ code at $123D (floor 0). blinkRepairMarker uses zp_6F = 60 frames (one second) and advances the real-time battle 20 frames per blink step with runGameFrames, so a repair attempt keeps the game running for roughly two seconds while the marker flashes.
- [game_main_new_17DA] The menu selector shares its highlight blink with the schematic: when it runs in repair mode (selectedUnit $90F8 = $FF) the toggler at $6108 calls toggleSystemMarker for rows 0-3, and it also aborts the menu entirely ($6103: two PLAs and a jump) the moment the comcen becomes stunned.
- [game_main_new_17DA] Dead code: enterMainMenu stores $7F into $1412 at $141F, but the only reads of $1412 are the two loop-counter reads inside repairTabAction ($1856, $1879), which always follow a store of 3. The initial $7F is never used.
- [game_main_new_17DA] Stale label: clearRepairMessage calls $C0EF in the $C000 overlay under the name bootReceiveSectorData. That address now holds the text routine 'print A copies of the fill character zp_3E' (loop over sub_C0C0); the name is a leftover from the boot loader that occupied $C000 before the runtime overlay replaced it.
- [game_main_new_4987] Group model (verified here and in runJoinGroupMenu $6174): a group key occupies bits 2-5 of the unit flag bytes - bits 2-4 are the group number 0-6 (ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER, names at $C92C) and bit 5 is the side bit, which runJoinGroupMenu ORs in for unit indices >= $32 and which unitFlagsTable already carries. Bit 6 = 'is a member'. Every membership test in the game is (flags & $7C) == key | $40, so each side has its own seven groups and the two sides can never share one.
- [game_main_new_4987] Two copies of the group bits exist by design: unitFlagsTable ($F708) is authoritative and is only changed by command executors that run identically on both machines, while unitDisplayFlags ($FB54) is a local copy the menus change immediately (ADDED / REMOVED appear before the command has travelled). syncLocalGroupFlags ($4C6B) copies $F708 & $7C back over $FB54 after every executor. Consequently the UI range checks in this chunk (computeGroupBounds, isPointWithinGroupRange) read $FB54, while the executors (averageGroupMoveVector, moveGroupKeepingFormation, rotateGroupFormation) read $F708.
- [game_main_new_4987] Each group command is split into a UI half in this chunk that only queues bytes, and an executor half that is also in this chunk but is reached exclusively by JMP from the dispatch table $4D2A: $81 move group ($4A35 -> $4A52), $88 join ($4AA3 -> $4AD4), $89 leave ($4AF2 -> $519B), $98 re-position one member ($4BD3 -> $4C59), $A3 rotate formation ($65FF -> $4C84). The single-unit move $80 ($347F -> $34C6) is the odd one out: it clears the member bit, so ordering one unit of a group to a cell takes it out of the group, while $81 and $98 keep membership.
- [game_main_new_4987] sub_34D5 (unnamed, entered four times from this chunk) is the shared 'set unit destination' tail: it snaps a comcen destination to its own cell in some setup cases, sets bit 7 of unitDestCol when the destination equals the position, clamps both coordinates to 0-39 with clampMapCoordOld ($355D) and copies the position into the low bits of the path position $F898/$F8FC. All the arithmetic in this chunk is therefore allowed to produce out-of-range offsets.
- [game_main_new_4987] Pending-order handshake: orderGroupNewPosition remembers the group in lastOrderedGroupBits ($929D) and counts its queued $98 orders in pendingGroupOrderCount ($929E); setGroupMemberDestination decrements the counter when the command finally executes. waitForGroupRepositionsDone ($65E7), called by the GROUP and FORMATION screens, spins on that counter so the editor only ever reads destinations that both machines already agree on. This is the clearest example in the game of the UI waiting for the lock-step command stream.
- [game_main_new_4987] rotateGroupFormation encodes the three rotations as data (formationRotateRules) rather than as code: each rule byte carries 'negate' in bit 7 and 'this component becomes the row offset' in bit 0, and the store is done with STA zp_74,X where X is the rule's bit 0. The rotation needs no coordinate mirroring for remote commands because the opponent's frame differs by a 180-degree rotation, which commutes with the 90-degree rotations applied to the offsets.
- [game_main_new_4987] Setup-phase clamp in rotateGroupFormation ($4D09-$4D1F): while gamePhase != 0 a rotated destination row is forced into the issuing player's half - own commands (cmdFromOpponent bit 7 clear) are clamped up to row 20 and remote ones down to row 19. The existing block comment in the disassembly states this the wrong way round.
- [game_main_new_4987] followGroupMovement contains a harmless coding slip: at $4A0F it does TYA / ORA zp_18 with Y holding the unit index, where the intent was clearly LDA zp_19 / ORA zp_18 (is the average vector zero?). Both branches compute position + (dx,dy) for a zero vector, so the result is identical for every unit except index 0, which stands still instead of following a purely vertical group movement.
- [game_main_new_4987] The $88 handler at $5188-$5197 contains a dead register shuffle (74->73, 73->74, 75->73, 73->75) that leaves zp_74/zp_75 unchanged and only sets zp_73 = zp_75; it looks like a swap that was compiled away. joinUnitToGroup uses zp_74/zp_75, so the sequence has no effect.
- [game_main_new_4987] joinUnitToGroup rebuilds unitFlagsTable as (old & $03) | key | $40, so besides the group bits it also clears bit 7 (the one-shot first-strike/energy-sacrifice flag consumed at $3910) and re-takes the side bit from the command parameter. A unit reset ($3DC3) instead preserves the group bits with AND #$5C, so group membership survives a unit being recycled.
- [game_main_new_4987] Message strings used from here live in the $C000 text overlay: 'TOO FAR' $CB31, 'ADDED' $CB3B, 'REMOVED' $CB40, 'NOT IN GROUP' $CB8D, 'RE-POSITIONED' $CB99, 'NEW POSITION' $CBA6; the info panel is the 14-column window at columns 25-38, rows 12-21 (clearInfoPanel $261C) and printStringCentred ($C121) centres inside it.
- [game_main_new_4987] isPointWithinGroupRange enforces the 8-cell rule with four absolute-difference tests against the box edges rather than a span test, which also caps the box after the new point is added; an empty group always passes, which is what makes the first ADDED to a fresh group work.
- [game_main_new_4FC4] The whole range $4FC4-$55E5 is the command-handler block reached through commandHandlerTable ($4D2A). Every handler starts by consuming its own arguments with one of the three readers at the head of the block: readCmdArgs3 ($4FC4, 4-byte commands), readCmdArgs2 ($4FD0, 3-byte), readCmdArg1 ($4FF7, 2-byte); 1-byte handlers just INC $90B7 themselves. The advance always matches commandLengthTable ($4D78), which I checked entry by entry for all 39 commands.
- [game_main_new_4FC4] Coordinate mirroring is the core of the two-machine design: both C64s run one identical simulation, so every map coordinate that arrives from the link is turned into 39-x/39-y by mirrorCmdCoordsIfRemote ($4FE4) before it is used, and each player simply looks at the shared world from the other end. The drone, which occupies a 2x2 cell block, needs mirrorCmdCoordsIfRemoteDec ($5005) so its top-left corner mirrors to 38-x/38-y.
- [game_main_new_4FC4] zp_76 (cmdFromOpponent) is more than a mirror flag: it also selects the side index for the per-side arrays ($92A6/$92AE/$92B0/$92B4), and gates all the purely local side effects - pendingRedrawUnit/Group, sounds, the DRONE ALERT message - so that the opponent's actions never trigger the local player's feedback.
- [game_main_new_4FC4] Cancelling a command in flight: cmdDroneDetonate ($534F) writes $0E over its own $8E command byte in mergedCmdBuffer (addressed as D_908C,readIdx+3). Clearing bit 7 makes executeTurnCommands skip the command, and because getCommandLength masks the byte with $3F the skip length is still the correct 3. This is the general mechanism for 'cancelled command' bytes with bit 7 clear in the film stream.
- [game_main_new_4FC4] Group id bytes on the wire are not plain 0-6 indices: they carry bits 2-5 of unitGroupByte (bits 2-4 = group, bit 5 = side). All group handlers OR $40 into the byte and compare it with unitGroupByte & $7C, so a group can never span both sides. groupStatusTable $91F4 is indexed by (groupByte & $3C) >> 2, i.e. group + 8*side, 16 entries.
- [game_main_new_4FC4] The four group cloak/blitz handlers ($5098/$50AA/$50BE/$50D2) are one self-modifying routine: the entry patches the table address ($F898 cloak / $F8FC blitz), the AND/OR masks for the unit and for groupStatusTable, and a gate byte at $512D that decides whether applyBlitzStepCost is called per member. Cloak and blitz are mutually exclusive - each 'on' handler calls the opposite 'off' handler first, saving and restoring $90B7 around it in a self-modified operand.
- [game_main_new_4FC4] SPY units (type 3) are exempt from cloak and blitz in every handler that sets bits, and the group body also skips them when clearing, so a SPY's F898/F8FC bits are never touched by a group order. The single-unit cmdCloakOff/cmdBlitzOff have no such check.
- [game_main_new_4FC4] Commands $8C (session control) and $8D (drone telemetry) never enter mergedCmdBuffer: both packet mergers execute them in place ($4F28/$4F36 in mergeIncomingPacket, $4F84/$4F9C in mergeOutgoingPacket) and skip the copy. cmdSessionControl therefore receives A = the reason byte rather than the command byte, and saveRegsForCmdReturn ($532F) exists purely to give the merge loop its X/Y back. The commandHandlerTable slot for $8D is $FFFF; the slot for $8C at $4D42 still points at $523E but is unreachable, and if it ever were reached A would be $8C, not a reason code.
- [game_main_new_4FC4] Command $9E is overloaded by gamePhase: during the battle it is 'my COMCEN finished repairing' (clears the stun nibble of the sender's COMCEN), and during setup it is the phase-sync handshake of sendPhaseSyncAndWait ($7BF9) - it clears the sender's bit in gamePhase zp_B1, sets a 32-frame input lockout and injects a SPACE key so any 'press space' loop drops out. getPauseClearMask ($52AE) is shared between this use on zp_B1 and the pause/resume use on zp_BE.
- [game_main_new_4FC4] Command $90 is likewise overloaded by gamePhase: in setup it delivers the sender's drone and missile allowances into $92AE/$92B0, in the battle it is a missile strike doing 14 energy of damage (7 to a COMCEN).
- [game_main_new_4FC4] Command $A5 is the generic setup-option carrier: kind 0 = HANDICAP (value*10 added to the 16-bit score pot of the sender's side, which is the pot the opponent scores into), kind 1 = TERRAIN PTS into $92AC of the opposite side, kind 3 = COMCEN SPEED into $92B4 indexed 0 own / 1 opponent. Kind 2 is accepted by the handler (it falls into the >= 2 branch) but nothing sends it.
- [game_main_new_4FC4] destroyUnit ($5459) kills by applying 52 points of damage through sub_3B52 - two more than the 50-point energy maximum - so the normal explosion, scoring and redraw path runs; only then is the slot freed with $FF. COMCENs ($31/$63) are refused outright, so SELF DESTRUCT can never end the game by knockout.
- [game_main_new_652E] REPAIR tab flow: repairTabAction ($17DA) -> at $1860 it erases the four comcen system markers, sets showOkMarkers ($188A) = 1 so even undamaged systems get a marker, JSRs runRepairWhichMenu ($652E), then consumes the SPACE key, restores the standard menu text window and redraws all four markers. runRepairWhichMenu itself only returns after the player picks (EXIT) or the selector aborts.
- [game_main_new_652E] Screen layout of the REPAIR WHICH panel: text window at column 3, width 7, rows 9..19, colour $61. sub_C06A clears it, sub_C041 emits one CR (row 9 -> 10), the string at $CBD6 ('REPAIR',CR,'WHICH:',CR,CR|$80) puts the two header words on rows 10 and 11 and leaves the cursor on row 13, which becomes menuFirstRow. Items therefore occupy rows 13 (ENERGY), 14 (BATTLE), 15 (RADAR), 16 (DRONE) and 17 ((EXIT)).
- [game_main_new_652E] menuSelectedItem ($91D5) is deliberately a single source of truth here: the same byte is the highlighted row for the menu engine, the comcen system index for toggleSystemMarker ($188B) / drawSystemMarker, and the system index attemptSystemRepair ($193C) uses to index comcenSystemStatus ($9241). That only works because addMenuItem is called with ids 13,14,15,16 in exactly the system order ENERGY, BATTLE, RADAR, DRONE.
- [game_main_new_652E] selectedUnit ($90F8) = $FF is a sentinel, not a unit: only toggleMenuRowHighlight ($60EE) looks at it, and only to enable the marker blink. It has to be re-written on every pass of the L_657C loop because runMenuLoopWatchUnit copies it into $91D7 and the repair attempt in between can change it.
- [game_main_new_652E] Non-local exit: if the comcen is stunned while the menu is up, toggleMenuRowHighlight pops its own return address ($6103/$6104) and jumps to L_607E inside the selector, which sets menuSelectedItem = menuItemCount-1 = 4. Control comes back to $6584 with Y = 4, so the CPY #$04 test at $6587 takes the (EXIT) path and the window is cleared - the abort is funnelled through the normal exit. The same holds for a game-over or a sub_C451 abort (SPACE / function key).
- [game_main_new_652E] getSoloOrFlag923F ($0EE5) gates the repair: repair is attempted only when D_0B9B bit 7 is clear (not a film / solo replay) and D_923F & $40 is clear (game not over). This corrects docs/knowledgeBase.md line 274, which calls $0EE5 an orphan 'not referenced by traced code' - XREF.txt shows exactly one caller, $658B, and it is this routine.
- [game_main_new_652E] The exit path is a JMP sub_C06A ($659D), i.e. the routine tail-calls the window clear with colour $66 and returns to repairTabAction from there; there is no RTS at the end of this chunk.
- [game_main_new_652E] Earlier notes (knowledgeBase line 639 and annotations/11_survey_notes_game_main_0800.json) still say this routine 'shows as data in the disassembly'. That is stale: main_0800.s now disassembles $652E-$659F correctly as code, and repairTabAction at $17DA is code too.

## game/mapGenerator6F00

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 6F04 | generateBattlefieldMap | Entry point of the random battlefield generator (variant A of the $6F00 overlay). Derives the map-style flags at $032E from the game type in $0BA3 bits 0-2 (type 1 QB SNEAK -> $80, type 6 DEFENDER -> $40, else 0), sets $032D bit 7 when the low 5 bits of the map seed $0B95 are all set (= map code ending in '=', produces a point-symmetric map), shows status message 1 (sub_C853), blanks the 40x40 map at $F000 to $40 (sub_EC00), copies the 3-byte map seed $0B95-$0B97 into the scenario RNG state $5A-$5C (sub_EC2B) and falls into runMapGeneratorPasses. Called by manualLookupCheck ($09E0, to show the map the player must identify), by the map/setup screens of overlay A ($76AD, $76FF, $7A02, $7D23). | $0BA3 bits 0-2 game type, $0B95-$0B97 map seed, $EC00 sub-overlay (T34 s12-15) present, contour rule table at $0200 (T18 s15) present | $032D, $032E; map at $F000-$F63F filled; $5A-$5C RNG state; $0BA9-$0BAD map-id text; $0300-$032C generator parameters; clobbers $0400-$04EF and $9025-$9028 | high |
| 6F36 | runMapGeneratorPasses | Body of the map generator (fall-through from generateBattlefieldMap; the JSR at $7125 listed in XREF belongs to variant B). Pass order: encode the seed in A/Y/X as the 5-character map id at $0BA9 (sub_7850); carve a river (sub_7548, tiles $55-$5C, marks a keep-clear zone with $80/$81); pick hill summit sites (pickHillSummitSites); paint the summits (paintHillSummits, tile $65); turn $80 markers back into $40 (sub_74FE); applyContourTiles($65) with $0324=0 draws the upper-slope contour $41-$54; grow lower hills $61 beside the slopes (growLowerHillsBesideSlopes); clear $81 markers; applyContourTiles($61) with $0324=$20 draws the lower contour $21-$34; place forest clusters (placeForestClusters, tiles $5D-$60). For symmetric maps ($032D bit 7) the top half is then mirrored onto the bottom half (sub_EC54) and, if the river crossed the centre ($032D bit 6), cells (19,19)/(19,20)/(20,20) are patched to $55/$58/$40 so the two river halves join. Ends with jmp sub_C6EF (clear the status message). serviceLinkWhileGenerating is called between passes. | A/Y/X = map seed bytes (from sub_EC2B), $032D, $032E, RNG $5A-$5C | battlefield map $F000-$F63F, $0BA9-$0BAD map id text, $0300-$032C, map cells $F30B/$F333/$F334 | high |
| 6F91 | serviceLinkWhileGenerating | Keeps the two-player link alive during the long map generation: unless $0B9B bit 7 (solo/film mode) is set, and if zp_B1 bit 7 (non-battle phase flag) is set, jumps to serviceCommandExchange ($4DA9). Otherwise returns immediately. Called four times from runMapGeneratorPasses and from $773A. | $0B9B bit 7, zp_B1 bit 7 | side effects of sub_4DA9 (message system / command exchange) | medium |
| 6F9E | pickHillSummitSites | Chooses how many hill summits to place in the top half of the map and where. Count = (rnd&3)+(rnd&1), re-rolled to 1..4 if it came out 1, forced to 4 for map style $40 (DEFENDER) and masked to 0/1 for map style $80 (QB SNEAK); stored in $0300. For each site a blob size 2..5 goes to $0307[i] (mirrored into $0307[count+i]) and four candidate rows 7..14 go to $0325/$031C[0..3]. findBlankSitesOnLanes then resolves the sites; afterwards $0300 is doubled, the candidate rows are mirrored to 39-row (sub_3FA3) and findBlankSitesOnLanes runs again for the bottom half, so every summit has a point-mirrored twin. Exits early (count 0) via L_7088 with $0300 = 0. | $032E bits 6/7, RNG | $0300 total site count, $0307[0..7] sizes, $0314[i] columns, $031C[i] rows, $0325[0..3] rows, $0329[0..3] lane order; zp_28-zp_2B, zp_18 | medium |
| 701C | findBlankSitesOnLanes | Resolves site positions for entries zp_2A .. $0300-1: shuffles the four lane columns (shuffleLaneColumns) and, for each site, takes the next lane (zp_2B cycling 3..0) and the next candidate row (zp_29 cycling zp_28..0), tries up to 20 jittered columns (addRandomJitter, +/-6, must be < 40) until the map cell is blank ($40); if a row yields nothing it tries the other candidate rows and finally drops the site (dec $0300). Column goes to $0314[i], row to $031C[i]. Also entered by fall-through for the mirrored second pass. On exit $0300 = number of sites actually placed (zp_2A). | zp_2A first site index, $0300 requested count, zp_28 last row-candidate index, $0325[] candidate rows | $0314[]/$031C[] site coordinates, $0300 corrected count, $0329[] shuffled lanes, zp_1D/zp_1E/zp_1F/zp_29/zp_2B | medium |
| 708E | advanceLaneIndex | zp_2B = zp_2B - 1, wrapping from 0 back to 3 (cycles through the four lane columns in $0329). | zp_2B | zp_2B | high |
| 7097 | advanceRowCandidate | zp_29 = zp_29 - 1, wrapping from 0 back to zp_28 (cycles through the candidate rows in $0325). | zp_29, zp_28 | zp_29 | high |
| 70A0 | shuffleLaneColumns | Builds a random permutation of 0..3 in $9025-$9028 (identity, then four random swaps) and stores laneColumnTable[perm[y]] (6, 15, 25, 34) into $0329-$032C so hills are spread over the four vertical lanes of the map in random order. | RNG, laneColumnTable $6F00 | $0329-$032C, $9025-$9028 clobbered, zp_18 | high |
| 70D9 | paintHillSummits | For every site i < $0300 grows a blob of $0307[i] (2..5) cells of summit tile $65 (random variant $65/$66) at ($0314[i], $031C[i]) via growTerrainBlob with horizontal spread $C8/256 and vertical spread $20/256, $0304 = 0 (no 2x2 constraint). Does nothing when $0300 is 0. | $0300, $0307[], $0314[], $031C[] | map cells; $0301 = $65, $0304 = 0, zp_2A, zp_A7/zp_A8 | high |
| 710A | growLowerHillsBesideSlopes | Scans every interior cell (x,y in 1..38). For a blank cell whose N, W, E or S neighbour is an upper-slope contour tile (terrain class 5, tiles $41-$54) it grows, with probability 1/4 (N,S) or 1/2 (W,E) per neighbour, a 5-cell blob of lower-hill tile $61 ($61-$64 variants) from that cell (growTerrainBlob, spreads $40/$10). This widens each summit into a hill body before the second contour pass. Skips entirely when $0300 is 0 (no hills). | $0300, map, terrainClassTable $077E, RNG | map cells; $0301 = $61, $0304 = 0, zp_A7/zp_A8 scan position | medium |
| 71B0 | growTerrainBlob | Generic random blob painter. Starting at (zp_A7, zp_A8) it paints blobTile $0301 (randomised through sub_7872) on up to A blank ($40) cells using a depth-first walk with an explicit stack at $0400 (col) / $0450 (row) / $04A0 (direction flags), max 80 entries. From each cell it tries N, W, E, S once each (flag bits 0-3 of zp_6B), accepting a neighbour when it is inside the map, blank and a random byte is below $91DB (vertical) or $91DA (horizontal); a new cell never grows back toward the origin row/column (bits preset from its position relative to zp_A7/zp_A8). With $0304 bit 6 set a cell is only painted if checkAlignedBlockForForest allows it. Returns when A cells are placed, the stack is exhausted or full. Note: overwrites $0400-$04EF, the page that also holds the copy-protection answer table. | A = cells to place, X = horizontal spread (0-255), Y = vertical spread, zp_A7/zp_A8 origin, $0301 tile, $0304 flags | map cells; $91DA/$91DB, zp_18, zp_1A, zp_6B, zp_A5/zp_A6, zp_B2 stack pointer, zp_B3 placed count, $0400-$04EF | high |
| 72F6 | checkAlignedBlockForForest | For the blob cell (zp_A5, zp_A6) looks at the 2x2-aligned block containing it (coordinates with bit 0 cleared) and returns C = 0 only if all four cells are terrain class 0 (blank) or 2 (forest $5D-$60); otherwise C = 1. On success zp_48/49 are re-pointed at the original cell. Forest blobs are therefore confined to aligned 2x2 blocks that contain nothing but blank/forest. | zp_A5/zp_A6, map, terrainClassTable | C flag, zp_48/49, X/Y clobbered | medium |
| 7340 | placeForestClusters | Scatters forest ($0301 = $5D, variants $5D-$60, $0304 = $40 so growth respects 2x2 alignment). Cluster count-1 = (rnd&7)+2 normally (1/32 chance of no forest at all when rnd&$1F = 0), fixed 9 for map style $80 (QB SNEAK), and additionally masked with 3 for style $40 (DEFENDER). Each cluster picks a centre (rnd&$1F)+4 in both axes and makes 6 seeds jittered around it (addRandomJitter +/-8 columns, +/-4 rows, retried while >= 40), each seed painted by paintForestClusterSeeds. Also stores $41 into $0302, which nothing reads (dead store). | $032E, RNG | map cells; $0301, $0302, $0304, zp_2A cluster counter, zp_2B seed counter, zp_92-zp_95 | medium |
| 73AD | paintForestClusterSeeds | Paints a 4x2 block of forest blob seeds around (zp_93, zp_92): calls paintForestBlobAt at (x,y), (x+1,y), (x+1,y+1), (x,y+1), (x-2,y), (x-1,y), (x-1,y+1) and falls into it for (x-2,y+1), stepping zp_92/zp_93 in between. | zp_93 column, zp_92 row | map cells; zp_92/zp_93 restored to start values; zp_A7/zp_A8 | medium |
| 73D4 | paintForestBlobAt | If zp_93 (column) and zp_92 (row) are both < 40, copies them to zp_A7/zp_A8 and grows a 4-cell blob with growTerrainBlob (spreads $DC/$DC, tile $0301, flags $0304). Out-of-range coordinates are ignored. | zp_92/zp_93, $0301/$0304 | map cells; zp_A7/zp_A8 | medium |
| 73EE | applyContourTiles | Contour/edge pass for the hill terrain whose interior tile is A ($65 summit or $61 lower hill). zp_8E = terrain class of A, zp_91 = zp_8E-1 = class of its contour ring. Scans all 1600 cells: a cell of class zp_91 is erased to $40 and re-evaluated; cells of class >= zp_8E and river cells (class 1) are left alone; every other cell goes through applyContourRuleAtCell, which may paint a contour tile (rule value - $0324), fill the cell with the interior tile, or erase a neighbour; when it returns C = 1 the scan backs up two rows/columns and continues. Called with A = $65 ($0324 = 0 -> contour $41-$54) and A = $61 ($0324 = $20 -> contour $21-$34). | A = interior tile, $0324 contour offset, contour rule table $0200, terrainClassTable, map | map cells; $0312 = interior tile, zp_8E, zp_91, zp_2D/zp_2E scan row/col, zp_8C/zp_8D, zp_21 | medium |
| 7444 | applyContourRuleAtCell | Builds an 8-bit mask (zp_21) of the neighbours of cell (zp_2E, zp_2D) whose terrain class is >= zp_8E (bit order NW,N,NE,W,E,SW,S,SE from neighbourOffsetTable/neighbourBitTable), clears bits that point off the map, and looks the mask up in the contour rule table at $0200: 0 = leave the cell; $01-$7F = paint tile (value - $0324) with a random variant (sub_7872) at the cell, C = 0; $FF = fill the cell with the interior tile $0312 and return C = 1 (rescan from two cells back); $80-$FE = irregular pattern: with a 50% random bit (taken from zp_8D, refilled every 7 calls) erase the neighbour indexed by the low 3 bits if it is of class zp_8E (C = 1), otherwise fill the cell when the value is >= $90 (C = 1) or leave it (C = 0). On C = 1 zp_2E/zp_2D are decremented by 2 (floor 0). | zp_2E/zp_2D cell, zp_48 pointer to (x-1,y-1), zp_8E, $0200 table, $0324, $0312 | C flag, map cells, zp_2D/zp_2E, zp_21, zp_18, zp_8C/zp_8D | medium |
| 74FE | clearMapMarkers | Map generator clean-up pass: scans all 1600 cells of the 40x40 map ($F000-$F63F, six full pages by self-modified pointer then the 64-byte tail at $F600) and turns every cell whose value equals the marker byte in A (bit 7 set: $80 or $81) back into blank terrain $40. generateBattlefieldMap (sub_6F04) calls it with A=$80 after the first terrain pass and with A=$81 after the second, removing the river reservation markers placed by markRiverBanks. | A = marker value to erase ($80 / $81); map at $F000 | map cells equal to A become $40; operands at $7517/$7522 (page) and $751C/$753C (marker) self-modified; A/Y clobbered | high |
| 7548 | generateRiver | Draws the river (terrain codes $55-$5C: class 1, move cost 65, cyan) across the map from column 0 to 39 as part of generateBattlefieldMap. No river for QB Sneak (mapGenGameFlags bit 7). Start row: 20 on symmetric maps (mapSymmetryFlags bit 7), otherwise random 12..27; Defender (bit 6) always gets a river, other types skip it with probability 1/8. Each step writes $55 + current segment type at (genCol, genRow), picks the next segment (chooseNextRiverSegment), reserves the banks (markRiverBanks) and moves (advanceRiverPosition) until genCol reaches 40. | mapGenGameFlags $032E, mapSymmetryFlags $032D, scenario RNG $5A-$5C (nextScenarioRandom $FD4A), map at $F000 | river tiles written to the map, $80/$81 reservation markers around them, genCol/genRow/riverCurSegment/genTileType updated, may set mapSymmetryFlags bit 6 | high |
| 759E | chooseNextRiverSegment | Chooses the next river segment type into genTileType ($0312). Types: 0 = bend down/right ($55), 1 = bend left/down ($56), 2 = bend up/right ($57), 3 = bend left/up ($58), 4-7 = horizontal straight variants ($59-$5C). Rules: after 1 comes 2, after 3 comes 0, after a bend a straight, after a straight 50% straight / else 1 or 3; columns 0-1 and 38-39 are forced horizontal (column 37 may not start a vertical jog). Symmetric maps stay horizontal on row 20; at column 20 with probability 7/8 the river jogs one row up (genCol -= 2, genRow = 19, mapSymmetryFlags /= $40) so that the 180-degree mirrored bottom half joins it at the centre. | riverCurSegment $0311, genCol $030F, genRow $0310, mapSymmetryFlags $032D, scenario RNG | genTileType $0312 = next segment 0-7; may modify genCol/genRow and set $032D bit 6 | high |
| 761C | advanceRiverPosition | Moves the river cursor according to the current segment type riverCurSegment: patches the JMP operand at $762C with riverStepLo/Hi[type] and jumps to one of the three stubs riverStepRight ($762E, genCol+1), riverStepUp ($7632, genRow-1) or riverStepDown ($7636, genRow+1). Types 1 -> down, 3 -> up, all others -> right. | riverCurSegment $0311, tables $763A/$7642 | genCol or genRow changed; operand at $762C self-modified | high |
| 762E | riverStepRight | Step stub reached through advanceRiverPosition: INC genCol; RTS. Listed as data (.byte) in the disassembly. | none | genCol $030F + 1 | high |
| 7632 | riverStepUp | Step stub reached through advanceRiverPosition: DEC genRow; RTS (segment type 3). Listed as data in the disassembly. | none | genRow $0310 - 1 | high |
| 7636 | riverStepDown | Step stub reached through advanceRiverPosition: INC genRow; RTS (segment type 1). Listed as data in the disassembly. | none | genRow $0310 + 1 | high |
| 764A | markRiverBanks | Reserves the cells around the current river cell so that later terrain passes keep the banks clear: for columns 2-37 it addresses the 6x6 block whose origin is (genCol & ~1) - 2, (genRow & ~1) - 2, marks the 16 inner cells (riverZoneInnerOffsets) with $81 when they are blank or already marked, and the 20 perimeter cells (riverZoneOuterOffsets) with $80 when blank. In columns 0-1 and 38-39 only the four cells of the river's own column (rows evenRow-1..evenRow+2) get $81. The markers are removed again by clearMapMarkers. | genCol $030F, genRow $0310, map via readMapCell ($25FA -> zp_48/49), tables $7894/$78A8 | $80/$81 written into blank map cells; zp_48/49 pointer | high |
| 76AD | mapNameEntryScreen | 'TYPE MAP NAME' screen of the game setup (entered by JMP from $8339 when the map item is chosen). Draws the current map (generateBattlefieldMap, optionally flipped 180 degrees by $EC58 when mapFlipFlag is set), the overview map, clears the name field, prints 'TYPE MAP NAME / (F7 RANDOM) / (F5 SYM/RND)' ($0677) and 'BTN TO ACCEPT / (F1 TO FLIP)' ($069F), then loops on enterMapName: each completed name (C=0) becomes the new mapSeed, the map is cleared ($EC00), redrawn and the prompt repeated; the fire button (C=1) prints 'ACCEPTED' ($EFED) centred on row 15 and returns to the setup menu. | mapSeed $0B95-$0B97, mapFlipFlag $0B9D, keyboard/joystick | mapSeed, mapFlipFlag (cleared after a new name), map regenerated, text window/colour zp_3D/zp_42 = 9 | high |
| 7717 | enterMapName | Interactive entry of the 5-character map name at column 4, row 16 (field shown by printMapName, underlined with five $25 glyphs on the next row). Accepted characters are the 32-entry alphabet at $890C (space, A-Z, + - * / =), typed via newKeyEvent ($90EA); DEL ($88) erases, other keys beep (sound 3), a colour-$11 block marks the cursor cell. F1 (currentKey 0) toggles mapFlipFlag and flips/redraws the map. Once five characters are present they are packed 5 bits each (last character in the low bits) into $5A-$5C and the routine returns C=0. F7 (key 3) or F5 (key 2) instead take a random seed from the game RNG ($57-$59; F5 ORs $1F into the low byte = symmetric map), convert it with seedToMapName and return C=0. A fire-button press returns C=1 (accept). Also used by the copy-protection manualLookupCheck ($09F9) to read the manual answer. | currentKey zp_66, newKeyEvent $90EA, fire button (checkFirePressed $0EB1), rngState $57-$59, mapNameAlphabet $890C | C = 0: $5A-$5C = 24-bit seed and mapName $0BA9-$0BAD = its text; C = 1: fire pressed, nothing chosen; mapNameLength $0313, mapFlipFlag may toggle, zp_18 scratch, text cursor state | high |
| 782D | setMapNameCursor | Positions the text cursor at the map-name field: textCursorCol = 4, row 16 (sub_C097). | text window zp_3B/zp_40 | zp_3F = 4, zp_40 = $10, screen pointer zp_B6/B7 | high |
| 7836 | clearMapNameField | Fills the five map-name characters $0BA9-$0BAD with spaces ($20). | none | $0BA9-$0BAD = $20; Y = $FF | high |
| 7841 | printMapName | Moves the cursor to the name field (setMapNameCursor), terminates the name with $8D (CR with the end-of-string bit) at $0BAE and prints mapName $0BA9 with the text engine ($C133). | mapName $0BA9-$0BAD | $0BAE = $8D, text printed, cursor on the next row | high |
| 7850 | seedToMapName | Converts the 24-bit seed in A (low), Y (mid), X (high) into the five-letter map name: five times take the low 5 bits as an index into the alphabet at $890C, store at $0BA9+4 down to $0BA9, shift the seed right by 5. Called with the map seed registers loaded by $EC2B (from generateBattlefieldMap) and by enterMapName after a random F5/F7 seed. | A/Y/X = seed bytes 0/1/2 (same layout as $5A/$5B/$5C) | mapName $0BA9-$0BAD; zp_18-zp_1A used as the shift register; X/Y clobbered | high |
| 7872 | randomTileVariant | Picks a random visual variant of a terrain code: A = base tile code; the mask table at $0770 (indexed by code - $40, or code - $20 for codes below $40) gives the number of extra variants (0, 1 or 3); the result is A + (nextScenarioRandom AND mask). Y is preserved through the self-modified operand at $7892. Used by the terrain fill passes of the map generator ($72A4, $74E7). Starts with a NOP (probably a disabled byte). | A = base tile code, Y preserved, table $0770, scenario RNG | A = tile code with random variant bits; operands at $788D/$7890/$7892 self-modified | high |
| 78D1 | startGameSession | Game-start sequence called from startGameFromSetup ($0B40) after the setup block and unit templates were loaded. Resets battle state (film limit, redraw/recycler/clock markers = $FF, unitCol[0] = $FF so the unit template gets copied, colours own=6/enemy=2, overview unit pattern $55, screen 3), then if filmPlaybackFlag bit 7 jumps to the film replay start ($7CD1). Otherwise: revealAll = trainer flag, step interval 4, decides the side (alternates via firstPlayFlag when preset, else negotiated over the link after openCommLink/waitForConnection or readyForNewGameHandshake), sends the side ($553B) and waits for the exchange, patches the own-side branches, player 0 sends the map seed with command $8B (or $A6 when flipped) while player 1 shows message $1A and waits for it (waitForMapSeedCommand), exchanges identities ($ED43, version check $90F8 == $DB else 'OPPONENT USING NON-STANDARD GAME.'), copies the unit start template for the game type ($ECF3), records the setup in the film header ($D000: +4 trainer flag, +5 side, +6 name, +$C options, +$11 owner names, +$2B seed, +$2E first-play/flip), generates the map, rotates map/units for player 1 ($EC58/$EC91), initialises all units, for the trainer build loads the $EC00 sub-overlay from track 29 and runs $EC0F, runs the unit setup phase (runMapMainLoop with gamePhase $3F) for game types other than Scrimmage and The Bomb, waits for the opponent's packet, sets the game clock from roundsByGameType, maps the comcen speed classes, writes the film snapshot, places the recyclers, shuffles the unit order, sets the last-quarter warning round and jumps into the comm tail at $EE26 to start the battle. | settings block $0B95-$0BB4, filmPlaybackFlag $0B9B, commSessionState $0B9C, commBuildFlag $0BA5, comm module state $E01D/$E03B, unit templates at $F000 | entire battle state initialised (map, units, clock, film header, message slots, gamePhase zp_B1); does not return normally (ends in JMP $EE26 or $7CD1) | medium |
| 7BB2 | shuffleInitialUnitOrder | Seeds the scenario RNG from mapSeed ($EC2B), resets unitBaseOrder $9164 and unitProcessOrder $90FD to the identity permutation ($7D70), performs 51 random swaps with shuffleUnitOrder ($47ED) and jumps to L_4657 (the round-order rebuild at the end of finishRound). Both machines get the same order because the RNG is seeded from the shared map seed. | mapSeed $0B95-$0B97 | $9164/$90FD shuffled, scenario RNG $5A-$5C, zp_18 counter; continues at L_4657 | high |
| 7BC6 | patchOwnSideBranches | Installs the local player's side into the engine's self-modifying side tests: writes BCC ($90, side 0) or BCS ($B0, side 1) into the branch opcodes at $63E2, $2840, $3F9E, $36D5, $379C, $4783, $5BF7 and the inverted opcode into $206B, $2084, $3B2C, $3BD0 (all follow a CMP/CPY #$32 'unit index < 50' test), and sets comcenUnit $9236 = comcenUnitBySide[side] (49 or 99). Called at game start and at film replay start ($7D20). | playerSide $0B9F, comcenUnitBySide $0C8A | 11 branch opcodes patched, $9236 | high |
| 7BF9 | sendPhaseSyncAndWait | Queues the 1-byte phase-sync command $9E (queueCmdBytePending), lets $C7C3 show 'WAITING FOR OPPONENT' (message $0B) when gamePhase bit 0 is set on a modem game, then runs frames (updateGameFrame when gamePhase bits 6-7 are clear, otherwise waitForExchangeIdle) until the $9E command has been executed on both machines, which clears the low two bits of gamePhase zp_B1 via getPauseClearMask. Finally clears messageState. | gamePhase zp_B1, comm exchange | zp_B1 low bits cleared by the command handler, $92A1 = 0 | high |
| 7C1C | waitForMapSeedCommand | Calls waitForExchangeIdle repeatedly while gamePhase zp_B1 bit 7 is set; the bit is cleared by the handler of the map-seed command $8B/$A6 ($51EA), i.e. this waits until both machines have received the map seed. Then clears messageState $92A1. | zp_B1, comm exchange | $92A1 = 0 | high |
| 7C2E | applyGameTypeDefaults | Loads the per-side game parameters for the selected game type from the two 5x5 default tables at $892C (side 0) and $8945 (side 1) in the $8800 strings overlay. The type ($0BA3 & 7) is first folded into a table column 0-4 (0 SCRIMAGE, 1 QB SNEAK, 2 THE BOMB/FACE-OFF/SLUGGERS, 3 FULL WAR, 4 DEFENDER); the column also indexes setupPhaseModeTable ($7C29) -> $92C6. Rows 0-1 (terrain points $92AC, recycler mode $92A6) are stored per absolute side, rows 2-4 (comcen speed class $92B4, drones $92AE, missiles $92B0) per relative index (0 = own side, 1 = opponent). Then QB SNEAK removes units 0-6 from play and THE BOMB removes units 0-98, re-creates unit 49 as a COMCEN (type 4) and gives both sides one extra drone. Called once from the game start code ($79E9) after the game type has been chosen. | D_0BA3 bits 0-2, D_0B9F, tables $892C/$8945, D_7C29 | D_92C6, D_92AC/D_92A6 (per side), D_92B4/D_92AE/D_92B0 (own, opponent), D_F76C/D_FAF0 entries set to $FF for removed units, D_F79D=4, zp_18, zp_48/49; self-modifies $7C86 and $7CC0 | high |
| 7CA2 | removeUnitsUpToY | Loop that marks units Y down to 0 as absent: writes $FF into unitTypeTable ($F76C) and unitUnderCell ($FAF0). Entered with Y=6 (QB SNEAK) or Y=$62 (THE BOMB) by applyGameTypeDefaults; the RTS at $7CAD is also the shared exit of applyGameTypeDefaults. | Y = highest unit index to remove | D_F76C[0..Y] = D_FAF0[0..Y] = $FF; Y = $FF | high |
| 7CAE | selectSideDefaultsTable | Points zp_48/zp_49 at the game-type defaults table of side X: $892C for side 0, $8945 for side 1 (5 rows of 5 bytes each, in the $8800 strings overlay). | X = side 0/1 | zp_48/zp_49 = table pointer | high |
| 7CBF | readDefaultAndAdvanceRow | Reads byte [column] of the current 5-byte table row through zp_48 (the LDY #$FF operand at $7CC0 is patched with the game-type column by applyGameTypeDefaults) and advances zp_48/zp_49 to the next row (+5). | zp_48/zp_49 = current row, patched column index | A = table value, zp_48/zp_49 += 5 | high |
| 7CD1 | startFilmPlayback | Game-start path taken when filmPlaybackFlag ($0B9B) bit 7 is set (WATCH GAME FILM). Restores the game parameters from the film header in RAM under I/O at $D000 (read with sub_5874, zp_BC/BD already = $D000): byte 4 -> linkTypeAtStart ($92FE), 5 -> playerSide ($0B9F), $0C -> gameTypeOptions ($0BA3), $11-$24 -> the two 10-byte player names at $FFDE, $2B-$2D -> mapSeed ($0B95), $2E bit 0 -> mapOrientationFlag ($0B9D) and bits 1-2 -> playsFirstFlag ($0BA1), 6-$0B -> settings text ($0BA9). Then patches the side-test branches (sub_7BC6), regenerates the map from the seed (sub_6F04), rotates it 180 degrees if the film side differs from the map orientation (sub_EC58), restores the film's saved unit/score state (sub_EE4A), writes 'CUSTOM' into the settings text when bit 6 is set (sub_86C3), places all units (placeAllUnitsOnMap), homes the view (sub_EC1C), redraws the screen, sets lastQuarterFlag and draws the opponent's recycler, then joins the common game start at L_7B8F. | film header at $D000 (zp_BC/zp_BD = $D000), D_0B9B bit 7 | D_0BA8=0, D_92FC=0, D_92FE, D_0B9F, D_0BA3, D_FFDE..D_FFF1, D_0B95-D_0B97, D_0B9D, D_0BA1, D_0BA9-D_0BAE, D_90FB=0, D_92FA=$FF, map at $F000 regenerated; jumps to L_7B8F | high |
| 7D70 | initUnitOrderLists | Fills the two 100-entry unit order lists unitBaseOrder ($9164) and unitProcessOrder ($90FD) with the identity permutation 0..99 (99 down to 0). Called by sub_7BB2 before the lists are shuffled with the scenario RNG. | none | D_9164[0..99] = D_90FD[0..99] = index; Y=$FF | high |
| 7D7D | placeAllUnitsOnMap | Calls placeUnitOnMap (sub_3DDA) for every unit 99 down to 0 (unit index passed in X and zp_18). Used at game start ($7B83) and when a film is replayed (startFilmPlayback). | unit tables | map cells at $F000 updated for all live units; zp_18 = $FF | high |
| 7D8B | setGameClockForType | Looks up the game length in rounds for the game type ($0BA3 & 7) in gameClockByTypeTable ($78C3: 63,63,31,127,127,254,254) and stores it in gameClock ($91CB); also returns it in A (the caller at $7B9E shifts it right twice to get the quarter length $92EF). | D_0BA3 bits 0-2, D_78C3 | A = D_91CB = rounds | high |
| 7D98 | clearUnitsAndRecyclersFromMap | Removes every unit 99..0 from the map (removeUnitFromMap restores the terrain under it) and then blanks the recycler cell of both sides via clearRecyclerCell. Called from the game start sequence ($7B7A) before the units are re-placed. | unit tables, D_92A8/D_92AA | map at $F000 without units and recyclers | high |
| 7DA7 | clearRecyclerCell | If side X has a recycler (recyclerCol $92A8,x bit 7 clear) computes the map address of (recyclerCol, recyclerRow) with readMapCell and writes the blank terrain byte $40 into it. | X = side, D_92A8/D_92AA | map cell := $40, zp_48/zp_49 | high |
| 7DB8 | waitForSpaceKey | Clears lastKeyCode, chatState, inputLockoutTimer and chatInputLength, then loops serviceCommandExchange + nextGameRandom until the space key ($A0) is seen in lastKeyCode ($9248). Afterwards clears the message queue, sets inputLockoutTimer to 30 frames and resets lastKeyCode. Entered by JMP from showModemSetupPrompt ($0C9D) so the RTS returns to that routine's caller; stirring the RNG while waiting randomises the map seed. | D_9248 (set by the IRQ keyboard scan) | D_9248=$FF, D_929C=0, D_9277=0, D_0B7D=$1E, message queue cleared, RNG advanced | high |
| 7DE3 | handleMainMenuChoice | Acts on the main-menu item in menuSelectedItem ($91D5), entered by JMP from runMainMenu ($1C78). 0 COMPETE WITH MODEM OPPONENT: linkTypeFlag $0BA5=0, $0B9B=0. 1 PRACTICE WITH SOLO TRAINER: $0BA5=$FF, $0B9B=0. 2 WATCH GAME FILM: requires a valid film in memory (checkFilmInMemory), keeps $0BA5, $0B9B=$82. These three jump to startGameFromSetup ($0AC8). 3 SAVE GAME FILM (needs a valid film) and 4 LOAD GAME FILM: prompt for the data disk, suspend the comm module, checkDiskAndLoadFilmDirectory (returns on failure), resume, set defaultMainMenuItem to 2 (WATCH GAME FILM), filmDirectoryPage=0, filmMenuMode=item-3, draw the directory page and run runSaveFilmMenu or runLoadFilmMenu, then promptInsertGameDisk and return. | D_91D5, D_0BA5, film in $D000 | D_0BA5, D_0B9B, D_92FD=2, D_7FA1=0, D_7FA2; jumps to $0AC8 for items 0-2 | high |
| 7E39 | setModeFlagsAndStartGame | Tail of handleMainMenuChoice: stores X into linkTypeFlag ($0BA5) and A into filmPlaybackFlag ($0B9B) and jumps to startGameFromSetup ($0AC8). | X = new $0BA5, A = new $0B9B | D_0BA5, D_0B9B; jumps to $0AC8 | high |
| 7E4F | checkDiskAndLoadFilmDirectory | Identifies the disk in the drive and loads the film directory. First tries the game disk silently (checkDiskId X=0, A=$80); if that fails tries the data disk with messages (X=2, A=0) and additionally requires BAM bytes $0203-$028F to be all zero (otherwise 'NOT A DATA DISK' via L_100D). With a valid disk it falls into loadFilmDirectory (track 18 sectors 2-3 -> $0300-$04FF). diskIdIndex ($92F0) is left 0 for the game disk and 2 for a data disk, which later selects 1 or 3 directory pages. | disk in drive | C=1 no usable disk (message shown), C=0 directory loaded at $0300; D_92F0, D_0200 BAM buffer | high |
| 7E74 | writeFilmDirectory | Waits 8 frames, then writes the film directory $0300-$04FF (2 sectors) to track 18 sectors 2-3 with writeSectors. A write failure goes to diskErrorExit. | directory at $0300 | track 18 s2-3 rewritten; C=0 | high |
| 7E8B | loadFilmDirectory | Reads track 18 sectors 2-3 into $0300-$04FF with loadSectorsRetry (the 30 x 16-byte film names); failure -> diskErrorExit. Tail of checkDiskAndLoadFilmDirectory. | none | $0300-$04FF = film directory, C=0 | high |
| 7EA0 | drawFilmDirectoryPage | Draws one page of the film directory menu: clears the menu window, prints the centred title 'SAVE GAME FILM' ($CC66) or 'LOAD GAME FILM' ($CC75) depending on filmMenuMode, then from the page pointer (setFilmDirectoryPagePtr) lists 10 entries as right-aligned slot number + space + 16-character name, then ' (MORE)' ($EF81) when a data disk is in use and ' (EXIT)' ($CAB2). Sets up the selector: menuItemCount 11 or 12, window left $0B width $12, menuFirstRow 9, menuSelectedItem 0, colour $60. | D_7FA1, D_7FA2, D_92F0, directory at $0300 | screen text, D_91D4, D_91D5=0, D_91D6=9, zp_3B=$0B, zp_3C=$12, zp_3D=$60, zp_48/zp_49, D_90F8 and D_9220 used as counters | high |
| 7F3F | runLoadFilmMenu | LOAD GAME FILM: runs the menu selector (sub_5FD1, C=1) on the directory page. Item 10 with a data disk = next page (nextFilmDirectoryPage, then restart); item 10/11 otherwise = exit. For a slot 0-9: suspends the comm module, converts the slot to a track (filmSlotToTrack), reads the track's 16 sectors one at a time into $0200 and copies each page under the I/O area to $D000-$DFFF (copyPageUnderIo with patched pointers $58C3/$58C6), resumes the comm module and validates the film (checkFilmInMemory); a valid film clears the message queue. | D_91D5, D_92F0, D_7FA1, disk | $D000-$DFFF = film, D_91D5 = track, zp_18/zp_19, D_58C3/4, D_58C6/7 | high |
| 7FA6 | setFilmDirectoryPagePtr | zp_48/zp_49 = $0300 + filmDirectoryPage * $A0 (10 entries of 16 bytes per page) and stores the page's first slot number (filmPageFirstSlotTable: 1, 11, 21) in $90F8 for the numbering loop. | D_7FA1, D_7FA3 | zp_48/zp_49, D_90F8 | high |
| 7FCB | nextFilmDirectoryPage | Advances filmDirectoryPage modulo 3 and redraws the page (jumps to drawFilmDirectoryPage). | D_7FA1 | D_7FA1, redrawn page | high |
| 7FDD | runSaveFilmMenu | SAVE GAME FILM slot picker with inline name entry - a variant of the generic menu selector sub_5FD1. Highlights the current slot; while a slot 0-9 is selected keys are fed to the text-entry handler sub_C7D5 (max 16 chars into the line buffer $9278, length in $92E6) and the typed name is echoed on the slot's row with a cursor block ($11 in screen RAM). Joystick up/down moves the bar (with auto-repeat via setJoystickRepeatDelay), which discards the typed text. Fire or RETURN accepts: item 10 on a data disk = next page and restart, otherwise exit (EXIT); for a slot with typed text the name is copied into the 16-byte directory entry (space padded, bit 7 on the last byte), the row is redrawn and saveFilmToSlot writes directory and film. | D_91D4, D_91D5, D_91D6, D_7FA1, keyboard/joystick (D_90EA, zp_5D), zp_6C/zp_6E timers | Y = D_91D5 (track after a save), D_92E6=0, D_92C5, D_9278, directory entry, disk; zp_4A/zp_4B = entry pointer | high |
| 810C | printFilmSlotEntry | Positions the text cursor on the current menu row (menuFirstRow + menuSelectedItem, column 0) with colour A, clears the line and prints a space followed by the entry text: the 16-byte name at zp_4A/zp_4B for slots 0-9, ' (MORE)' for item 10 on a data disk, else ' (EXIT)'. | A = text colour, D_91D5, D_91D6, D_92F0, zp_4A/zp_4B | screen text, zp_3D, zp_3F=0 | high |
| 8143 | filmSlotToTrack | Converts the selected directory slot into a disk track: page*10 + item + 2, plus 1 when the result is >= 18 so the directory track is skipped (slots 0-29 -> tracks 2-17 and 19-32). The track replaces menuSelectedItem ($91D5) and is returned in A. | D_7FA1, D_91D5 | A = D_91D5 = track | high |
| 815E | saveFilmToSlot | Suspends the comm module, converts the slot to a track, rewrites the directory (writeFilmDirectory) and then copies the 16 pages of the film at $D000-$DFFF (RAM under I/O) one at a time to $0200 and writes each as sector 0-15 of the track with writeSectors. Finally resumes the comm module. | D_91D5 slot, film at $D000 | track sectors 0-15 written, D_91D5 = track, zp_18/zp_19, D_58C3/4, D_58C6/7 | high |
| 81A5 | promptInsertGameDisk | Patches 'GAME' ($EFD4) into the message 'INSERT xxxx DISK AND PRESS SPACE.' (promptInsertDiskAndWaitSpace), then waitForGameDiskInserted and resumeCommAfterDiskAccess. End of the save/load film flow. | none | game disk present, comm module resumed | high |
| 81B2 | promptInsertDataDisk | Entry of promptInsertDiskAndWaitSpace with A/Y = $EFD0 ('DATA'). | none | see promptInsertDiskAndWaitSpace | high |
| 81B6 | promptInsertDiskAndWaitSpace | Copies the 4-character disk name at A/Y into the message template at $EFB6 ('INSERT DATA DISK AND PRESS SPACE.' at $EFAF, in the comm-module tail), installs that string as message $1E (sub_C924), shows it (statusMessageId $92A1 = $1E, sub_C566), waits for the space key while servicing the link, clicks, clears the message id and erases the message line (sub_C39F). | A/Y = address of a 4-byte name ('DATA' $EFD0 or 'GAME' $EFD4) | D_EFB6..D_EFB9 patched, message slot $1E, D_92A1, D_9248 | high |
| 81F4 | checkFilmInMemory | Validates the film header at $D000 with sub_58DE (checksum of bytes 0-$32 in byte $33 and version byte $2F == $0B94). Valid: C=0. Invalid: queues message 9 'NOT A GAME FILM!', waits about 4 seconds (waitFramesServicingComm) and returns C=1. | film header at $D000 | C flag, zp_BC/zp_BD = $D000, A = header byte 4 when valid | high |
| 81FE | waitFramesServicingComm | Sets joyRepeatTimer zp_6C to $F0 and calls serviceCommandExchange until the IRQ has counted it down (about 4 seconds), then returns C=1. Also called from the main program's queue flush at $563C (that call lands in different code when overlay B is resident). | none | C=1, zp_6C=0 | high |
| 820B | beginGameTypeSelection | Entry used by the side that picks the game ($79A1, side 0): clears the message queue, sets currentScreen to 0, clears the CUSTOM-ON message id patch ($868D) and installs 'CUSTOM OFF' ($EFD8) as message $1E, then falls into runGameTypeMenu. | none | D_90FB=0, message slot $1E, $868D=0 | high |
| 821F | runGameTypeMenu | Game-type selection screen. Points the CUSTOM message patch at $EFDF, derives customModeFlag ($92F3 bit 7) from $0BA3 bit 6, clears and redraws the screen, resets the selection (resetSelectionAndView), clears the info panel; in solo-trainer mode toggles the display set (sub_EEDD) and with custom rules shows the CUSTOM status (sub_8673). Prints 'GAME TYPE:' ($CCBD) and the 7 type names ($CCC8..) and runs the selector starting at the current type. The choice selects a preset from gameTypePresetTable ($78CA): with custom rules the preset (or the unchanged $0BA3 when the same type is re-chosen) gets bit 6 and two ON/OFF sub-menus follow ('UNIT MENUS' -> bit 3, 'DAMAGE' -> bit 4 via sub_860B). Continues in runMapTypeMenu. Also reached by JMP from $2E07 (screen redraw, relevant to the overlay-B routine at this address). | D_0BA3, D_0BA5, D_92F3, menu input | D_0BA3, D_92F3, D_91D4=7, D_91D5, zp_B1=$BF (custom path), zp_3D/zp_42=9; self-modifies $829E, $8319, $869A/$869B | high |
| 8279 | printGameTypeNamesLoop | Not a routine in variant A: loop body of runGameTypeMenu that prints the next type name (sub_C137) and a newline 7 times (zp_18 counts down). The label exists because variant B has a routine at $8279. | zp_18 | screen text | high |
| 82A4 | applyCustomPresetTail | Not a routine in variant A: continuation inside runGameTypeMenu that ORs the custom bit $40 into the chosen preset, stores it in $0BA3 and runs the UNIT MENUS and DAMAGE toggles. Variant B has a routine at this address. | A = preset | D_0BA3, zp_B1=$BF | high |
| 82BF | runMapTypeMenu | Second half of the game setup menus: resets the selection, clears the info panel, prints 'MAP TYPE' ($EF77) with the items STANDARD/CUSTOM ($CC11) preselected from $0BA3 bit 7, and runs the selector; the result becomes $0BA3 bit 7. Then sends the game type to the opponent (sendGameTypeCommand $A4), copies the scenario layout for the type (sub_ECF3), sets gamePhase zp_B1=$FF and playsFirstFlag=1 when $0BA3 changed against pendingGameTypeOptions ($0BA4). CUSTOM map -> jumps to the map editor at L_76AD. STANDARD map -> derives mapSeed ($0B95-$0B97) from the RNG state zp_57-zp_59 (low 5 bits forced to 1 for symmetric types; for QB SNEAK/DEFENDER only when this side plays first). The main program also calls $82BF from resolveAttack ($3A30), which targets the overlay-B routine at this address. | D_0BA3, D_0BA4, D_0BA1, zp_57-zp_59, menu input | D_0BA3, D_0BA1, D_0B95-D_0B97, zp_B1=$FF, zp_73, command $A4 queued | high |
| 833C | sendGameTypeCommand | Stores A in cmdParam0 (zp_73) and queues command $A4 with that single parameter (sub_561F) - tells the opponent the chosen gameTypeOptions byte. | A = game type options byte | zp_73, command queued | high |
| 8343 | printMenuTitle | Sets menuSelectedItem to X (the preselected item), prints the string at A/Y centred in the current window (sub_C121), sets the window left column to $1C, positions the cursor and records the cursor row as menuFirstRow ($91D6). | X = initial item, A/Y = title string | D_91D5, D_91D6, zp_3B=$1C | high |
| 8356 | runMenuWidth10 | Runs the menu selector with a highlight width of 10 (falls into runMenuWithWidth). | menu variables | A = D_91D5 = chosen item | high |
| 8358 | runMenuWithWidth | Sets the text window width zp_3C to A, moves the left column one to the left, and runs the generic selector sub_5FD1 with C=0; returns the chosen item in A (from $91D5). | A = width, D_91D4, D_91D5, D_91D6 | A = D_91D5, zp_3B--, zp_3C | high |
| 8364 | resetSelectionAndView | Clears the selection state for the setup menus: unitDrawOverride $9242 = $FE, selectedUnit $90F8 = $FE (none), currentScreen $90FB = 0, and moves the view origin zp_A5/zp_A6 to (45,45), i.e. completely off the 40x40 map so the viewport is blank. | none | D_9242, D_90F8, D_90FB, zp_A5, zp_A6 | high |
| 8378 | chooseRecycler | Recycler setup (called from the options flow at $85B6 for game types >= 4). In solo mode both recycler modes are forced to 2 (NONE). If recyclerMenuEnabled ($8450) is set, shows the 'RECYCLER' menu with FULL/HALF/NONE (printOptionWord) preselected from recyclerMode[side]; otherwise keeps the current mode and returns immediately when it is NONE. NONE sends command $9C with column/row $FF. Otherwise prints 'LOCATING'/'RECYCLER', homes the view (sub_EC1C), shows the cursor and viewport and lets the player pick a map cell (runTargetCursorLoop with cursorTargetMode=$FF) until a cell whose terrain is blank ($40, looking under a unit if one stands there) is chosen; then hides sprites, resets the view and sends command $9C (mode, col, row) via sub_560A. | D_0BA5, D_0B9F, D_92A6, D_8450, player input, D_91D8/D_91D9 | D_92A6 (solo), D_91D5, zp_73-zp_75, command $9C queued, D_9022=0, zp_BB=$FF; self-modifies $8433 | high |
| 843B | printOptionWordLine | Prints option word A (0 FULL, 1 HALF, 2 NONE via printOptionWord) followed by a newline. | A = option index | screen text | high |
| 8441 | clearInfoPanelSetRow | Sets text colour zp_3D and message colour zp_42 to 8, clears the info panel (clearInfoPanel) and sets the text cursor to row A (sub_C097). | A = cursor row | zp_3D=zp_42=8, zp_40=A, cursor positioned | high |
| 8451 | editValueWithJoystick | Generic numeric option editor used by the custom-scenario setup. A = exclusive upper limit (patched into the CMP at $8492), Y = display multiplier (patched into $8469). Loops: un-highlights the one-row menu, prints editValue*Y in decimal at column 1 of the value window (multiply8x8 $FCCD -> A=lo/Y=hi, then $C17D), clears the rest of the line, services the link (serviceCommandExchange), blinks the row, and applies joystick up/down as +1/-1 (rejecting values >= limit or below 0, playing sound 2 on a change, auto-repeat via setJoystickRepeatDelay). Returns when the fire button is pressed (checkFirePressed). | A = limit (value must stay < A), Y = multiplier for display, D_844F initial value, zp_5D joystick, zp_6C repeat timer, zp_6E blink timer, menu window set up by beginValueEdit | D_844F = edited value; D_92C5 = 1; text printed in the info panel; sounds 2 | high |
| 84AA | applyHandicapPoints | Tail of runGameStartOptions (reached by JMP from $85B9). Computes the default handicap for the asymmetric scenarios: game type 1 (QB SNEAK) gives side 0 a handicap of 2 (20 points), game type 6 (DEFENDER) gives side 1 a handicap of $14 (200 points), everything else 0. When the custom toggle (D_92F3 bit 7) is set the player edits the value instead ('HANDICAP', 0..50 shown x10) starting from that default. A nonzero result is sent as command $A5 kind 0 (value*10 points for the sender's side, cmdSetupOption); zero is not sent. | D_0BA3 bits 0-2 game type, D_0B9F playerSide, D_92F3 bit 7 custom flag | command $A5 00 value queued via queueCmd3 (zp_74 = 0, zp_75 = value); info panel used | high |
| 84EC | editTerrainPoints | Custom option 'TERRAIN PTS': starts from D_92AC[opponent side] (points awarded for entering the enemy zone), lets the player pick 0..2 with editValueWithJoystick and sends command $A5 kind 1 (cmdSetupOption stores it in $92AC of the other side). | D_0B9F playerSide, D_92AC | command $A5 01 value queued (zp_74 = 1, zp_75 = value) | medium |
| 84F1 | editTerrainPointsForSide | Entry into editTerrainPoints with the side index already in A (A -> Y, X = D_92AC[Y]). In variant A nothing calls it; the XREF caller at $83DE belongs to overlay variant B, where $84F1 is a different routine. | A = side index 0/1 | as editTerrainPoints | medium |
| 8511 | editDronesAndMissiles | Custom options 'DRONES' (0..9, from D_92AE) and 'MISSILES' (label $CC9E, 0..36, from D_92B0), each edited with beginValueEdit + editValueWithJoystick. The two results are sent as one command $90 x=drones y=missiles; in the setup phase cmdMissileStrike ($53C0) stores them as dronesLeft/missilesLeft for the sender. | D_92AE dronesLeft, D_92B0 missilesLeft | command $90 drones missiles queued (zp_74 = drones, zp_75 = missiles) | medium |
| 8518 | editDronesAndMissilesBody | Not a real entry point in variant A: it is the JSR beginValueEdit inside editDronesAndMissiles (A/Y/X must already hold the label and value). The label exists because the main program calls $8518 at $1460/$14D3, which is a routine of overlay variant B (menu screen). | A/Y = label, X = value (must be preset) | as editDronesAndMissiles | high |
| 8547 | runGameStartOptions | Called once from the new-game start sequence ($7AF7) after the setup has been saved and before unit placement. Sets gamePhase $7F, hides the viewport (sub_8364), draws the overview (side 0: drawOverviewMap, side 1: clearScreenAndRedrawGameScreen), shows the 'STANDARD/CUSTOM <type> SCENARIO - PRESS BUTTON WHEN READY.' banner (showScenarioBannerAndWaitForButton), copies gameTypeOptions bit 6 into D_8450 and (shifted to bit 7) into D_92F3. If CUSTOM rules are on it shows the custom summary message, prints the game type name and a 2-item STANDARD/CUSTOM menu (rows at column 28, width 10, runMenuLoopWithFlag); choosing CUSTOM (D_8450 = 1) runs editComcenSpeed, editTerrainPoints and editDronesAndMissiles. Then, for game types 4-6 (SLUGGERS, FULL WAR, DEFENDER), sub_8378 sets up the recycler, and finally it jumps to applyHandicapPoints. | D_0BA3 gameTypeOptions, D_0B9F playerSide, menu/text engine state | zp_B1 = $7F, D_8450, D_92F3, setup commands $A5/$90/$9C queued, screen redrawn | medium |
| 85BC | editComcenSpeed | Custom option 'COMCEN SPEED': starts from D_92B4 (own comcen speed setting 0..3), edits it with editValueWithJoystick (limit 4) and sends command $A5 kind 3 (cmdSetupOption writes $92B4 of the sender's side; $7B6E later maps the setting through the table at $78BF). | D_92B4 | command $A5 03 value queued (zp_74 = 3, zp_75 = value) | medium |
| 85DB | beginValueEdit | Common prologue of the value editors: stores the label pointer (patched into $85EA/$85EC), sets editValue = X, clears the info panel in colour 8 and sets row 15 (sub_8441), prints the label centred, moves the text window left edge to column 29, starts a new line and records that row as menuFirstRow with menuSelectedItem = 0. Falls into the tail at $8601 which sets selectedUnit = $FE (plain menu, no unit) and the window width to 5 for the highlight bar. | A/Y = label string (bit-7 terminated), X = initial value | D_844F = X, D_91D5 = 0, D_91D6 = row, D_90F8 = $FE, zp_3B = $1D, zp_3C = 5, label printed | high |
| 8601 | setValueEditWindow | Tail of beginValueEdit: selectedUnit = $FE and textWindowWidth = 5. The sub_ label only exists because $8601 is a routine entry in overlay variant B; in A it is only reached by fall-through. | - | D_90F8 = $FE, zp_3C = 5 | high |
| 860B | editOptionBitOnOff | Two-row OFF/ON menu for one bit of gameTypeOptions, used by the scenario setup screen for 'UNIT MENUS' (mask $08, $82B3) and 'DAMAGE' (mask $10, $82BC). Stores the label pointer and mask in self-modifying operands, hides the viewport, clears the panel to row 14, prints the label centred with the current state pre-selected (row 0 = OFF from $064D, row 1 = ON via sub_C137), runs runMenuLoopWithFlag with width 5 at column 27 and then clears or sets the mask bit in D_0BA3 according to the chosen row. | A/Y = label string, X = bit mask, D_0BA3 | D_0BA3 bit updated; D_91D4 = 2, D_91D5 = choice | high |
| 862D | runOnOffMenuAndApplyBit | Tail of editOptionBitOnOff (menuItemCount = 2, print OFF/ON, run the menu, apply the mask at $8621+1). Only an entry point in overlay variant B. | label already printed, mask operand at $8622 | D_0BA3 updated | high |
| 8632 | printOnOffMenuAndApplyBit | Inner tail of editOptionBitOnOff starting at the OFF/ON printing. The main program's JSR $8632 at $176B targets overlay variant B's routine of the same address, not this code. | menu state, mask operand at $8622 | D_0BA3 updated | high |
| 8658 | handleStarKeyToggle | Per-frame hook called while overlay A is resident in non-battle phases (battlefield loop via the trampoline $25C4 when zp_B1 != 0, and runMenuLoop at $6002 when zp_B1 == $FF). If no chat entry is pending (D_929C bit 7 clear) and the last key is '*' ($AA) it consumes the key, toggles bit 7 of D_92F3 and falls into showToggleStateMessage (returns C = 1). Otherwise returns C = 0. During unit placement the flag means DESTROY mode (clicking an own unit sends $91 self destruct, $209F); on the scenario setup screen it means CUSTOM rules. | D_929C chatState, D_9248 lastKeyCode, D_92F3 | C = 1 and D_92F3 bit 7 toggled when '*' was pressed (D_9248 = $FF), else C = 0 | high |
| 8673 | showToggleStateMessage | Shows the state of the '*' toggle: empties the message queue (tail = head), patches the word 'ON' ($0650) or 'OFF' ($064D) into the message text (3 bytes copied to the address patched at $869A/$869B: $EFDF inside 'CUSTOM OFF' or $EFEA inside 'DESTROY OFF'), sets persistentMessageId to $1E while the mode is on (or to the operand at $868D when off: 0 on the setup screen, 10 = 'SETUP UNITS...' during placement), clears the message timer and queues message $1E. Returns C = 1. Also called directly from the setup screen ($8253) to show 'CUSTOM ON' initially. | D_92F3 bit 7, operands patched by $820B/$821F or setDestroyModeMessage | D_9252 = D_9251, D_92A1, zp_72 = 0, message $1E queued, C = 1, zp_48/49 = word pointer | high |
| 86AA | setDestroyModeMessage | Prepares the '*' toggle for the unit-placement phase (called at $7B4B before the placement loop): off-state persistent message = 10 ('SETUP UNITS. PRESS F 1 WHEN DONE.'), message slot $1E -> 'DESTROY OFF' ($EFE2 in the T34 $EC00 sub-overlay), and the ON/OFF patch address = $EFEA (the 'OFF' inside that string). The setup-screen code at $820B/$821F does the same with 'CUSTOM OFF' ($EFD8/$EFDF) and off-message 0. | - | $868D = $0A, message slot $1E (D_0500/D_0524 index $1E) = $EFE2, $869A/$869B = $EFEA | high |
| 86C3 | showCustomSummaryMessage | Builds the status line 'CUSTOM:' [+ 'MAP,' when D_0BA3 bit 7 is set] + ' MENUS OFF,/ON,  DAMAGE OFF./ON.' into the buffer at $0332 (prefix from $CC19 'CUSTOM' and $EF77 'MAP TYPE'; the option part is filled into the template at $0636 ' MENUS XXXX DAMAGE XXX.' by appendOnOffWord with bits 3 and 4 of gameTypeOptions) and registers it as the persistent message $1E (setPersistentMessage). Called when a game starts or resumes with CUSTOM rules ($7D4B, $856D). | D_0BA3 bits 3, 4, 7; templates at $0636 and $064D; strings $CC19, $EF77 | $0332.. text, $0636 template overwritten, message slot $1E = $0332, D_92A1 = $1E, message queued | high |
| 8722 | setPersistentMessage | Registers the string A/Y as message slot $1E (sub_C924 writes the pointer tables at $0500/$0524), makes $1E the persistent status message (D_92A1) and queues it with sub_C566. Used for 'PRESS BUTTON WHEN READY.' and the custom summary; the entry at $8722 is also the tail of showCustomSummaryMessage. | A/Y = string pointer | D_0500[$1E]/D_0524[$1E], D_92A1 = $1E, message queued, X = $1E | high |
| 872E | appendOnOffWord | Writes 'OFF,' (A = 0) or 'ON, ' (A != 0) into the template at $0636 starting at index X: copies the bit-7 terminated word from $064D (OFF) or $0650 (ON), adds a comma, and for ON adds a trailing space so both forms are 4 characters; returns X pointing at the comma. | A = option state (0 = off), X = destination index in $0636 | $0636+X.. written, X = index of the comma | high |
| 8756 | showScenarioBannerAndWaitForButton | Clears the info panel (row 15) and prints, centred in 13 columns, 'STANDARD' or 'CUSTOM' (table $CC11, chosen by D_0BA3 bit 6), the game type name (table $CCC8, bits 0-2) and the string '  SCENARIO' ($CC04). Sets 'PRESS BUTTON WHEN READY.' ($CBBE) as persistent message $1E, then loops on serviceCommandExchange until checkFirePressed reports a click, and finally clears the status line (sub_C39F). | D_0BA3, fire button, link | info panel text, D_92A1 = $1E, status line cleared | high |
| 8795 | printCentredTableEntry | Sets textCentreWidth (D_90EF) to 13 and prints entry X of a table of 8-byte bit-7 terminated strings at A/Y through sub_C100 (used for the STANDARD/CUSTOM words and the game type names). | A/Y = table base, X = entry index | text printed, D_90EF = $0D | high |

Data:

- `$6F00` **laneColumnTable** (byteTable, 4 bytes): $06,$0F,$19,$22: the four lane columns (6, 15, 25, 34) along which hill summits are placed; shuffled into $0329 by shuffleLaneColumns. Labelled sub_6F00 only because variant B has code here.
- `$71AB` **unusedBitTable71AB** (byteTable, 5 bytes): $08,$04,$02,$01,$00 - not referenced by any code in variant A (the $71AC reference in XREF is variant B code).
- `$74EE` **neighbourOffsetTable** (byteTable, 8 bytes): $00,$01,$02,$28,$2A,$50,$51,$52: offsets of the 8 neighbours (NW,N,NE,W,E,SW,S,SE) from a pointer at (x-1,y-1). The disassembler split it at L_74F3 because of a variant-B branch target; it continues past the chunk end.
- `$74EE` **neighbourOffsetTable** (byteTable, 8 bytes): Cell offsets 0,1,2,$28,$2A,$50,$51,$52 of the eight neighbours of the centre cell (+$29) of a 3x3 block; indexed with a random 0-7 by the terrain fill pass at $74A7. The label L_74F3 in the middle comes from variant B.
- `$74F6` **neighbourBitTable** (byteTable, 8 bytes): $01..$80: bit for each neighbour in neighbourMask (just past the chunk end, used by applyContourRuleAtCell).
- `$74F6` **bitMaskTable** (byteTable, 8 bytes): $01,$02,$04,$08,$10,$20,$40,$80 - single-bit masks used by the terrain fill pass at $745B.
- `$763A` **riverStepLo** (addrTable, 8 bytes): low bytes of the step routine for river segment types 0-7: $762E (right) for 0,2,4-7, $7636 (down) for 1, $7632 (up) for 3; high bytes in riverStepHi.
- `$7642` **riverStepHi** (addrTable, 8 bytes): high bytes ($76) of the river step routine addresses.
- `$7894` **riverZoneOuterOffsets** (byteTable, 20 bytes): Offsets of the 20 perimeter cells of a 6x6 block (column 0 rows 1-4, row 0 cols 0-5, column 5 rows 1-4, row 5 cols 0-5); the first four (same column, rows 1-4) are used alone near the map edges. Marked $80 by markRiverBanks. Labelled sub_7894 because variant B has code here.
- `$78A8` **riverZoneInnerOffsets** (byteTable, 16 bytes): Offsets of the inner 4x4 cells (rows 1-4, cols 1-4) of the 6x6 block; marked $81 by markRiverBanks.
- `$78B8` **unitLayoutByGameType** (byteTable, 7 bytes): Index (0-3) of the 400-byte unit start-position template at $F000 + 400*n used for game types 0-6: 2,2,2,1,3,0,0. Read by the comm tail routine $ECF3 (T34 build).
- `$78BF` **comcenSpeedClassMap** (byteTable, 4 bytes): Translates the comcen speed setting 0-3 into the engine speed class: 4,0,1,5 (applied to $92B4/$92B5 at battle start).
- `$78C3` **roundsByGameType** (byteTable, 7 bytes): Game length in rounds for game types 0-6: $3F,$3F,$1F,$7F,$7F,$FE,$FE (Scrimmage 63, QB Sneak 63, The Bomb 31, Face-Off 127, Sluggers 127, Full War 254, Defender 254). Read by sub_7D8B.
- `$78CA` **gameTypeOptionsByMenuItem** (byteTable, 7 bytes): Default gameTypeOptions byte for the seven game-type menu items: $00,$01,$02,$13,$14,$1D,$1E (bits 3/4 = unit options and damage on for Face-Off, Sluggers, Full War, Defender). Read at $828F; labelled L_78CA/sub_78CB from variant B.
- `$7C29` **setupPhaseModeTable** (byteTable, 5 bytes): Indexed by the game-type class 0-4 of applyGameTypeDefaults: {0,0,1,1,$80} -> $92C6 (0 no setup phase, 1 both sides deploy, $80 only side 0 deploys).
- `$7FA1` **filmDirectoryPage** (byteTable, 1 bytes): Variable: current film directory page (0-2).
- `$7FA2` **filmMenuMode** (byteTable, 1 bytes): Variable: 0 = save film, 1 = load film.
- `$7FA3` **filmPageFirstSlotTable** (byteTable, 3 bytes): {1,11,21}: slot number printed for the first entry of each directory page.
- `$844F` **editValue** (byteTable, 1 bytes): current value of the joystick value editor
- `$8450` **customSetupChosen** (byteTable, 1 bytes): nonzero = CUSTOM options chosen at game start
- `$879F` **msgWonOnPointsTail** (text, 13 bytes): 'ON ON POINTS!' - the tail of message $12 'WE WON ON POINTS!' whose first four bytes ($879B-$879E) are overlaid by code in variant A (pla / jmp sub_C100); overlay variant B holds the complete string at $879B. Message pointer table slots $11-$16 ($0500/$0524) point to $8789, $879B, $87AC, $87BA, $87CE, $87E3 and are only valid with variant B resident.
- `$87AC` **msgItWasADraw** (text, 14 bytes): 'IT WAS A DRAW.' = message $13 (game result)
- `$87BA` **msgWeWereKnockedOut** (text, 20 bytes): 'WE WERE KNOCKED OUT!' = message $14
- `$87CE` **msgWeWonByKnockout** (text, 21 bytes): 'WE WON BY A KNOCKOUT!' = message $15. Comm build 1 (T35, solo trainer) reuses $87CE+unit as a per-unit work table ($EB29/$EB53 store the game clock there), so with the trainer resident these strings are clobbered.
- `$87E3` **msgGameOver** (text, 10 bytes): 'GAME OVER.' = message $16
- `$87ED` **msgDroneAlert** (text, 12 bytes): 'DRONE ALERT!' - installed as message slot $20 by overlay A at $7BA6 (sub_C924 X=$20)
- `$87F9` **overlayTailPadding** (unknown, 6 bytes): six zero bytes of padding before the variant flag
- `$87FF` **overlayVariantFlag** (byteTable, 1 bytes): $00 = variant A, $FF = variant B (bit 7 tested by the main program at $1036/$1080)

Insights:

- [game_ovl_6F00_A_6F00] Overlay A $6F00-$74ED is the random battlefield generator. Pipeline (runMapGeneratorPasses): seed -> 5-char map id; river ($55-$5C) with $80/$81 keep-clear markers; hill summits ($65/$66) on 4 lanes with point-mirrored twins; upper contour ring ($41-$54) from the $0200 rule table; lower hills ($61-$64) grown beside the ring; lower contour ring ($21-$34); forest clusters ($5D-$60) in 2x2-aligned blocks; optional point mirroring of the whole map (sub_EC54) when the seed's low 5 bits are $1F (map code ends in '=').
- [game_ovl_6F00_A_6F00] Terrain classes (table $077E) line up with the hill model: class 0 plain ($40), 3 lower contour ($21-$34, height 1), 4 lower hill ($61-$64, height 2), 5 upper contour ($41-$54, height 3), 6 summit ($65-$66, height 4), 1 river ($55-$5C, 8 directional pieces), 2 forest ($5D-$60), 7 recycler ($67). Colour table $07EA gives class 1 cyan, class 2 green, classes 3-6 orange, class 0 yellow.
- [game_ovl_6F00_A_6F00] The 256-byte table at $0200 (track 18 sector 15, loaded by sub_10BB) is a neighbourhood -> tile rule table for drawing contour rings: bit i set = neighbour i (NW,N,NE,W,E,SW,S,SE) is higher terrain; entries are a contour tile, $FF = fill the cell, or $80+n = randomly delete neighbour n (>= $90 also fills). The same page is reused as BAM buffer and sprite shape buffer, so it must be intact when generateBattlefieldMap runs.
- [game_ovl_6F00_A_6F00] Map cell values with bit 7 set are treated as occupied everywhere in the generator; the river generator uses $80 (wide zone) and $81 (near ring) as temporary keep-clear markers that sub_74FE converts back to $40 after the summit pass and after the lower-hill pass respectively.
- [game_ovl_6F00_A_6F00] Game type affects the map: $0BA3 & 7 = 1 (QB SNEAK) -> $032E = $80: no river, 0-1 hill, 10 forest clusters; = 6 (DEFENDER) -> $032E = $40: river always, 4 hills, 1-4 forest clusters; other types: river 7/8 of the time, 0-4 hills, 3-10 clusters (1/32 chance of none).
- [game_ovl_6F00_A_6F00] growTerrainBlob ($71B0) is a reusable DFS blob painter with its stack in $0400/$0450/$04A0 - the copy-protection answer table at $0401 is destroyed by it, which is harmless because manualLookupCheck reads its answer before calling generateBattlefieldMap and the check is disabled anyway.
- [game_ovl_6F00_A_6F00] Several sub_ labels inside this chunk are entry points only in overlay variant B (sub_6F00, sub_6F89, sub_6FF5, sub_7036, sub_7061, sub_708D, sub_7135, sub_7163, sub_7204, sub_7221, sub_731E, sub_7396, sub_741C, sub_74C3); in variant A they fall in the middle of routines. The main program's calls to $731E ($5639), $74C3 ($5DF2) and $7396 ($5DF5) therefore require variant B to be loaded.
- [game_ovl_6F00_A_6F00] The map id text at $0BA9-$0BAD is the 24-bit seed $0B95-$0B97 written 5 bits per character (last character = seed bits 0-4) through the 32-character alphabet ' ABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/=' at $890C (sub_7850); the same seed is what manualLookupCheck swaps in from the $0401 table to show the player a map from the manual.
- [game_ovl_6F00_A_74EE] The battlefield is NOT loaded from disk: generateBattlefieldMap ($6F04, overlay A) clears $F000-$F63F to $40 ($EC00) and builds the terrain procedurally from the 24-bit mapSeed ($0B95-$0B97) using the scenario RNG $5A-$5C (nextScenarioRandom $FD4A). Track 18 s8-14 ($F000-$F6FF, the 'map' in docs/overview.md) actually holds four 400-byte unit start-position templates (unitCol[100], unitRow[100], flags[100], type[100]) selected by unitLayoutByGameType ($78B8) and copied to $F640 by the comm-tail routine $ECF3 before the map is generated.
- [game_ovl_6F00_A_74EE] Map names: a map is identified by a 5-letter name over the 32-symbol alphabet at $890C (space, A-Z, + - * / =); the name is the seed packed 5 bits per letter (last letter in the low bits of $5A/$0B95), so seed and name are the same data (seedToMapName $7850 / enterMapName $7717). A low-5-bit value of $1F (name ending in '=') marks a symmetric map: the river runs along row 20 and after generation $EC54 overwrites the bottom half with the 180-degree rotation of the top half (tile codes rotated through the table at $EC3B). F7 = random name, F5 = symmetric random (ORs $1F), F1 = flip (mapFlipFlag, $EC58 rotates the whole map).
- [game_ovl_6F00_A_74EE] Map generation order in sub_6F04: clear, seed RNG, name, generateRiver ($7548, river tiles $55-$5C = terrain class 1, cost 65, cyan), sub_6F9E/sub_70D9 feature passes, clearMapMarkers($80), terrain fill $65 (sub_73EE), sub_710A, clearMapMarkers($81), terrain fill $61, sub_7340, then mirror for symmetric maps. markRiverBanks reserves a 4x4 (aligned to even coordinates) zone around every river cell with $81 and a 1-cell ring with $80 so the fill passes keep the banks clear; the markers look like units (bit 7) until clearMapMarkers erases them. River tiles: $55 bend down/right, $56 left/down, $57 up/right, $58 left/up, $59-$5C horizontal variants; vertical runs are always a single cell (a bend follows immediately). On symmetric maps a 1/8 chance leaves the river on row 20 where the mirror erases it - effectively the same 1/8 'no river' chance that asymmetric maps get explicitly.
- [game_ovl_6F00_A_74EE] Each machine keeps the map and the unit arrays in its own orientation: startGameSession rotates the map by 180 degrees when playerSide XOR mapFlipFlag is 1 ($EC58) and mirrors all unit coordinates (39 - v via $3FA3, arrays $F640/$F6A4/$F7D0/$F834/$F898/$F8FC) for player 1 ($EC91), so every player's own units start in the bottom half (rows 20-39). This is why received commands have their x/y mirrored and why the own-side test is a self-modified BCC/BCS after CMP #$32 (patchOwnSideBranches $7BC6 lists all 11 patch sites).
- [game_ovl_6F00_A_74EE] Game start protocol (startGameSession $78D1): side chosen or negotiated -> command $9D (sendChooseSide) -> player 0 sends the map seed as command $8B (or $A6 = flipped; handler $51EA stores mapSeed and clears gamePhase bit 7) -> identity/version exchange $ED43 (result in $90F8 must be $DB) -> unit template copy -> film header written at $D000 (+4 trainer flag, +5 side, +6..+$B map name, +$C gameTypeOptions, +$11..+$24 owner names, +$2B..+$2D seed, +$2E firstPlay<<1 | flip; the film replay start $7CD1 reads the same offsets) -> map generation -> optional unit setup phase (all types except Scrimmage and The Bomb, gamePhase $3F) -> wait for $E01D bit 6 -> clock from roundsByGameType, comcen speed classes, film snapshot, recyclers, shuffled unit order -> JMP $EE26 in the comm tail. Phase transitions are synchronised with the 1-byte command $9E (sendPhaseSyncAndWait $7BF9, handler $520C clears gamePhase bits 0-1).
- [game_ovl_6F00_A_74EE] Comm build 1 (track 35, commBuildFlag $0BA5 bit 7) is the TRAINER: the opponent name is the fixed string 'TRAINER' at $884E, revealAllUnitsFlag is set from the flag, and at game start overlay A loads the track-29 sub-overlay to $EC00, runs its entry $EC0F and then restores the track-34 tail ($1070).
- [game_ovl_6F00_A_74EE] enterMapName contains a debug trap: if a character in mapName is not found in the alphabet it executes BRK ($780D); the BRK vector path in the IRQ handler ($10FF -> irqExit) makes this a no-op in the release build.
- [game_ovl_6F00_A_74EE] The XREF merges both overlay variants. In variant A the following labels inside this chunk are NOT routine entries or branch targets (they come from variant B code at the same addresses): sub_7552, D_754D, D_754F, L_7557, D_7586, L_7587, sub_7688, D_7702, sub_7879, D_7891, D_7893, sub_7894, sub_78CB, sub_78E7, D_7979, sub_7987, sub_79A1, sub_79D8, sub_7A32, sub_7A78, sub_7ABA, sub_7AEC, sub_7B44, sub_7BA2, D_77E0, D_782B. Callers seen for this chunk in variant A: sub_74FE from $6F47/$6F5F, sub_7548 from $6F39, sub_7850 from $6F36, sub_7872 from $72A4/$74E7, L_76AD from $8339, sub_7717/sub_7836 also from the copy protection at $09F6/$09F9, sub_78D1 from $0B40, sub_7BC6 from $7D20.
- [game_ovl_6F00_A_74EE] Variables at $030F-$0313 and $032D/$032E live inside the setup block ($0200-$03FF loaded from track 18 s15-16) and are used as map-generator scratch at run time; they are not settings.
- [game_ovl_6F00_A_7C29] Chunk boundaries: the spec's line range 2026-2895 does not match the address range $7C29-$844E (file lines 2080-3198); this survey covers the address range $7C29-$844E.
- [game_ovl_6F00_A_7C29] Game films: a recorded game lives in the 4 KB of RAM under the I/O area at $D000-$DFFF (accessed with $01=$34 by sub_5874/sub_5893/sub_58B2). Header layout as written by $7A93-$7AD7 and read back by startFilmPlayback: +4 soloTrainerFlag, +5 playerSide, +6..+$0B settings text, +$0C gameTypeOptions, +$11..+$24 the two 10-byte player names ($FFDE/$FFE8), +$2B..+$2D mapSeed, +$2E = playsFirst<<1 | mapOrientation, +$2F format byte (must equal $0B94), +$33 checksum of bytes 0-$32, +$34.. scores/unit state restored by sub_EE4A.
- [game_ovl_6F00_A_7C29] Film disk format: track 18 sectors 2-3 ($0300-$04DF) hold a directory of 30 names x 16 bytes (3 pages of 10, bit 7 terminates a name). Slot n of page p is stored on track 2 + 10p + n (+1 when >= 18, so tracks 2-17 and 19-32), 16 sectors = the whole $D000-$DFFF block. On the game disk only page 0 (tracks 2-11, the $01 filler tracks) is offered; a data disk (id 'EA', BAM bytes $0203-$028F all zero) gets all three pages via the ' (MORE)' item. checkDiskId leaves diskIdIndex $92F0 = 2 for a data disk and the directory code keys the page count on it.
- [game_ovl_6F00_A_7C29] Main menu items (strings at $CC1F..): 0 COMPETE WITH MODEM OPPONENT -> $0BA5=0; 1 PRACTICE WITH SOLO TRAINER -> $0BA5=$FF; 2 WATCH GAME FILM -> $0B9B=$82; 3 SAVE GAME FILM; 4 LOAD GAME FILM; 5 DISCONNECT/SET VOICE. Hence $0BA5 bit 7 selects the SOLO TRAINER: comm-module build 1 (track 35) is the AI opponent, not a null-modem driver, and overlay A additionally loads track 29 s0-3 to $EC00 ($7B24) only when that bit is set (AI extension that references the $8800-$89FF work area and unit tables).
- [game_ovl_6F00_A_7C29] Game-type presets (gameTypePresetTable $78CA, 7 bytes): SCRIMAGE $00, QB SNEAK $01, THE BOMB $02, FACE-OFF $13, SLUGGERS $14, FULL WAR $1D, DEFENDER $1E -> $0BA3 (bit 3 UNIT MENUS, bit 4 DAMAGE). Game length per type (gameClockByTypeTable $78C3): 63,63,31,127,127,254,254 rounds. Per-side defaults come from two 5x5 tables at $892C/$8945 in the $8800 strings overlay (rows: TERRAIN PTS, RECYCLER, COMCEN SPEED, DRONES, MISSILES; columns: SCRIMAGE, QB SNEAK, BOMB/FACE-OFF/SLUGGERS, FULL WAR, DEFENDER). QB SNEAK removes units 0-6; THE BOMB leaves only the two comcens (units 49 and 99) plus drones/missiles.
- [game_ovl_6F00_A_7C29] The label bootReceiveSectorData ($C0EF) is stale for the runtime $C000 overlay: there $C0EF is 'print A copies of the pad character zp_3E' (repeats sub_C0C0), which is what $7F12/$7F1E call with A=3.
- [game_ovl_6F00_A_7C29] Status-message slot $1E is a dynamic slot: sub_C924(X=$1E, A/Y) points it at 'INSERT xxxx DISK AND PRESS SPACE.' ($EFAF, xxxx patched at $EFB6 with DATA $EFD0 or GAME $EFD4), 'CUSTOM OFF' ($EFD8, with OFF/ON patched at $EFDF) or 'DESTROY OFF' ($EFE2). These strings live in the comm-module tail loaded from track 34 s12-15 together with the option names MAP TYPE $EF77, (MORE) $EF81, LOCATING $EF89, RECYCLER $EF92, UNIT MENUS $EF9B, DAMAGE $EFA7. Message 9 = 'NOT A GAME FILM!'.
- [game_ovl_6F00_A_7C29] sub_8279 and sub_82A4 are routine starts only in overlay variant B; in variant A they are mid-routine labels of runGameTypeMenu. Conversely the main program calls $81FE ($563C), $821F ($2E07) and $82BF ($3A30) which are routine starts in both variants with unrelated code - those main-program call sites are meant for the variant-B code (battle engine), while the variant-A routines at these addresses belong to the setup menus.
- [game_ovl_6F00_A_7C29] Setup-menu conventions used throughout overlay A: printMenuTitle (X = preselected item, A/Y title) + menuItemCount + runMenuWithWidth wrap the generic selector sub_5FD1; resetSelectionAndView ($9242=$FE, $90F8=$FE, view origin (45,45)) blanks the viewport; commands $A4 (game type), $9C (recycler mode/col/row) and $A5 (numeric options, next chunk) are sent to the opponent with sub_561F/sub_560A/sub_560F.
- [game_ovl_6F00_A_844F] Chunk location: the task quoted file lines 2896-3677 but in disassembly/game/ovl_6F00_A.s the address range $844F-$87FF is at lines 3198-3676 (lines 2896-3197 hold $820B-$844E); this survey covers the address range.
- [game_ovl_6F00_A_844F] This chunk is the custom-scenario option dialog of overlay A, run once per new game from $7AF7 (runGameStartOptions). Dialog order: banner 'STANDARD/CUSTOM <game type> SCENARIO' + 'PRESS BUTTON WHEN READY.'; if CUSTOM rules are on: STANDARD/CUSTOM menu, then COMCEN SPEED (0-3), TERRAIN PTS (0-2), DRONES (0-9) and MISSILES (0-36); for game types 4-6 the recycler (mode FULL/HALF/NONE only in custom mode, then 'LOCATING RECYCLER' free cell pick on a plain cell, command $9C mode x y); finally the handicap (default 2 = 20 points for side 0 in QB SNEAK, 20 = 200 points for side 1 in DEFENDER, or custom 0-500 step 10) as command $A5 kind 0. The values reach both machines only through the lock-step commands $A5 kind/value, $90 drones/missiles and $9C.
- [game_ovl_6F00_A_844F] Value editor pattern: beginValueEdit(A/Y label, X start) + editValueWithJoystick(A limit, Y display multiplier) -> D_844F. The editor is a one-row 'menu' (width 5 at column 29) so it reuses the menu highlight/blink code (sub_60E5/sub_60EA, D_92C5, zp_6E). The display multiplies with multiply8x8 ($FCCD), which returns A = product low byte and Y = high byte (the game_high_FBB8 survey's description 'A = Y = high byte' is inverted) and prints via sub_C17D(A=lo, Y=hi).
- [game_ovl_6F00_A_844F] The '*' key toggle (handleStarKeyToggle, $8658) is the overlay-A hook behind the main program's trampoline sub_25C4 and the runMenuLoop call at $6002. D_92F3 bit 7 is reused for two meanings: CUSTOM rules on the scenario setup screen ($821F initialises it from D_0BA3 bit 6 and message text 'CUSTOM OFF' $EFD8) and DESTROY mode during unit placement (setDestroyModeMessage $86AA selects 'DESTROY OFF' $EFE2; with the flag set a click on an own unit sends $91 self destruct at $209F). The ON/OFF word is patched directly into the string in the T34 $EC00 sub-overlay ($EFDF / $EFEA) using the words at $064D/$0650, and the message is always slot $1E, which is a run-time slot (pointer $FFFF on disk) rewritten with sub_C924; persistentMessageId D_92A1 decides what the status line falls back to.
- [game_ovl_6F00_A_844F] Message slot $1E is the overlay's scratch message: 'PRESS BUTTON WHEN READY.' ($CBBE), the 'CUSTOM:MAP, MENUS ON,  DAMAGE OFF.' summary built at $0332 from the template at $0636, 'CUSTOM ON/OFF' or 'DESTROY ON/OFF'. Slot $20 is set to 'DRONE ALERT!' ($87ED) at $7BA6.
- [game_ovl_6F00_A_844F] gameTypeOptions D_0BA3 bit map as used here: bits 0-2 game type, bit 3 UNIT MENUS (editOptionBitOnOff with label $EF9B), bit 4 DAMAGE (label $EFA7), bit 6 CUSTOM rules (banner word, D_8450, D_92F3), bit 7 custom MAP (adds 'MAP,' to the summary; corresponds to the $A6 custom map seed command / D_0B9D).
- [game_ovl_6F00_A_844F] Labels sub_84F1, sub_8518, sub_8601, sub_862D and sub_8632 are entry points of overlay variant B only (the main program's JSR $8518 at $1460/$14D3 and JSR $8632 at $176B target variant B code); in variant A they fall in the middle of editTerrainPoints, editDronesAndMissiles, beginValueEdit and editOptionBitOnOff. Likewise the XREF callers 83A1/83DE listed for $84B2/$84F1 are variant-B call sites.
- [game_ovl_6F00_A_844F] The game-result strings at $879B-$87EC (message ids $11-$16 via the pointer table at $0500/$0524) and the variant flag $87FF are the shared tail of both overlay variants; variant A's code runs through $879E, so message $11 ($8789) and the first four bytes of message $12 are code in A. They are only displayed during battle when variant B is resident, so no visible corruption results. The $0332 buffer used for the custom summary lives in the $0200-$03FF setup block area and is not referenced by anything else (XREF).

## game/modemDriverE000

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| E000 | commRequest | Jump-table entry 0 (JMP L_E111). The game's per-frame/protocol request call: X selects the function (0 = service tick, 1 = request that the outgoing packet be sent, >=2 = acknowledge/consume a completed exchange). Called from the raster IRQ ($1129 with X=0) and from the packet-exchange code in main and in the $EC00 sub-overlay. | X = function code | see L_E111; updates $E01D exchange flags | high |
| E003 | commLinkControl | Jump-table entry 1 (JMP L_E29A). Link life-cycle control: X=0 open the link, X=1 cold-initialise the module, X=2 shut the UART down, X>=3 hang up then shut down. Called from $0F46 (X=1, right after the module is loaded), $1B70 (X=0, openCommLink) and $1B56 (X=3, hangUpModemSetState). | X = function code | see L_E29A | high |
| E006 | getLinkByte | Jump-table entry 2 (JMP L_E0A9). Pops one byte that arrived from the opponent (or was echoed by the modem in terminal mode) out of the 8-byte host-input ring at $E086. Used by receiveChatCharacter ($1399). | none | A = byte (bit 7 inverted back), C=0 if a byte was returned, C=1 if the ring was empty | high |
| E009 | putLinkByte | Jump-table entry 3 (JMP L_E0F6). Pushes one byte into the 16-byte host-output ring at $E091 for transmission to the opponent (chat characters); the byte is stored EOR $80. Reached by JMP from handleChatSend ($1396). | A = byte to send | $E091 ring, $E02E count; silently drops the byte when the ring already holds 16 | high |
| E00C | pollLinkStatus | Jump-table entry 4 (JMP L_E3CB). Called once per raster IRQ from $112C: services the NMI suspend/resume handshake and samples + debounces the modem carrier-detect line. | $E039, $E03A, $E03B, CIA2_PRB | $E03C (debounced carrier), $E03D (raw sample), $E043 (debounce counter) | high |
| E012 | keyboardScanHook | Jump-table entry 5. In the shipped image it is JMP L_E3B2 (= return A=$FF, no key), but the main program patches the operand bytes $E013/$E014 at $0F4B/$0F50 to $0DB7 = scanKeyboard, so at run time this entry is the keyboard matrix scanner. Called only by readKeyAndHandleModemHotkeys. | none | A = key code ($FF = none), Y = $80 if SHIFT held, X = $80 if the Commodore key is held | high |
| E015 | commKeyEntry | Jump-table entry 6 (JMP L_E2EC). The game's only keyboard read: called every frame by pollKeyboardEvent ($0E56). Runs the keyboard scan through the comm module so that Commodore-key hot keys can control the modem before the game sees them. | none | A = key code or $FF when the key was swallowed as a modem hot key | high |
| E018 | clearPacketVars | LDX #$13 / JSR clearInlineVarBlock followed by the 19 bytes it clears. Zeroes the packet-exchange variables $E01D-$E02F (exchange flags, rx/tx lengths, both 7-byte packet buffers, host-output count). Because clearInlineVarBlock discards the return address, control returns to this routine's caller. Called from initCommModule ($E2B4). | none | $E01D-$E02F = 0 | high |
| E034 | clearLinkVars | LDX #$0F / JSR clearInlineVarBlock followed by the 15 bytes it clears. Zeroes the link-state variables $E039-$E047 (NMI suspend request/ack, link-active flag, carrier state, modem command phase, connection phase, error counter). Entered by JMP from L_E2AE, i.e. it is the tail of the 'close the link' path. | none | $E039-$E047 = 0; returns to the caller of the routine that jumped here | high |
| E067 | clearInlineVarBlock | Self-modifying helper used five times in this module. It pulls the return address off the stack, patches it into the STA abs,X at $E071 and then zeroes X bytes starting one byte past the JSR, i.e. the inline data block that immediately follows the call. Since the return address was consumed, the RTS returns to the caller's caller. | X = number of bytes to clear; return address on the stack points at the inline block | inline block zeroed; A=0, X=0; returns two levels up | high |
| E078 | clearProtocolVars | LDX #$2B / JSR clearInlineVarBlock followed by the 43 bytes it clears ($E07D-$E0A7): exchange counter, packet rx/tx state machines, both host rings and their indices, the in-flight counters and the service lock. $E0A8 is deliberately left alone. Called from initCommModule ($E2B1). | none | $E07D-$E0A7 = 0 | high |
| E0A9 | popHostInRing | Implementation of the $E006 entry: if $E0A1 (count) is zero return C=1; otherwise read $E086,X with X = read index $E0A2, EOR #$80, decrement the index (wrapping 0 -> 7) and the count, return C=0. | $E0A1, $E0A2, $E086-$E08D | A = byte, C = 0/1, $E0A1, $E0A2 updated | high |
| E0C3 | popHostOutRing | Pops one byte out of the 16-byte host-output ring at $E091 (count $E02E, read index $E08F, index decrements and wraps 0 -> 15). Used to feed the packet transmitter, to drain the ring when the link drops, and to feed the modem transmitter in terminal mode. | $E02E, $E08F, $E091-$E0A0 | A = byte, C=0, or C=1 when empty | high |
| E0DD | pushHostInRing | Pushes A into the 8-byte host-input ring at $E086 (count $E0A1, write index $E0A3). Silently drops the byte when the ring already holds 8. Used by the packet receiver for incoming chat bytes and by the modem terminal loop to echo modem responses to the screen. | A = byte | $E086 ring, $E0A1, $E0A3 | high |
| E0F6 | pushHostOutRing | Implementation of the $E009 entry: if the ring already holds 16 bytes do nothing, else store A EOR $80 at $E091,X with X = write index $E090, decrement/wrap the index and bump $E02E. | A = byte | $E091 ring, $E02E, $E090 | high |
| E111 | commRequestDispatch | Dispatcher behind $E000. X=0 -> serviceCommTick; X=1 -> requestPacketSend; X>=2 -> if $E01D bit 6 (exchange complete) is set, clear $E01E, $E01F and $E01D, i.e. acknowledge and consume the finished exchange. | X = function code, $E01D | see the three cases | high |
| E128 | requestPacketSend | $E000 with X=1: increments $E01D (setting the low 'send requested' bits) and increments $E085, the count of outstanding send requests. The packet transmitter picks the request up on the next service tick. | none | $E01D +1, $E085 +1 | high |
| E12F | serviceCommTick | $E000 with X=0, called from the raster IRQ every frame. DEC/INC of $E0A7 forms a re-entrancy lock (the body only runs when $E0A7 is 1). It always runs runModemStateMachine, and when the connection has reached phase 3 and the link is active it also runs the packet receive and packet transmit state machines. | $E0A7, $E040, $E03B | drives the whole protocol stack; $E0A7 restored to 1 | high |
| E14D | receivePacketFsm | Packet-layer receiver driven by $E081. State 0 reads a header byte through receiveLinkByte: the low 3 bits are the payload length, bit 7 means the payload is chat/text bytes that go straight into the host-input ring, otherwise the payload is a command packet stored at $E020 with the length in $E01E. Header values $70-$7F are a special case: the payload is copied into the scratch buffer at $EB03 and a zero-length $7x packet does JSR $EB03. A completed packet sets bit 7 of $E01D through setExchangeFlags. | $E081, $E082, $E080, receiveLinkByte | $E020-$E026, $E01E, $E01D bit 7, host-input ring, $EB03 buffer | medium |
| E170 | beginReceivePacket | Mid-routine entry of receivePacketFsm reached only by the T35 build ($E07E): clears $E01E and adds 2 to $E01D (marking 'a packet is being received'), then falls into the body that stores the payload. | $E082 | $E01E = 0, $E01D +2 | medium |
| E1DD | sendPacketFsm | Packet-layer transmitter driven by $E083. When nothing is in flight it either sends the pending command packet (header = $E01F / $E07F, then $E01F bytes from $E027) or, when the host-output ring holds chat bytes, a chat frame (header $80 / count, up to 3 bytes popped with popHostOutRing). Once the transmitter has drained it marks the send complete with bit 5 of $E01D and clears $E083. | $E083, $E085, $E02E, $E01F, $E027-$E02D, $E0A6 | bytes handed to sendLinkByte, $E0A6 = bytes+1, $E01D bits 3/5, $E084, $E08E | medium |
| E283 | countCompletedExchange | Increments the 16-bit counter $E07D/$E07E and then sets bit 6 of $E01D. Reached from setExchangeFlags whenever bits 7 and 5 are both set but bit 6 is not, i.e. exactly once per completed send+receive exchange. The counter itself is never read anywhere in the image, so it looks like a leftover statistic. | $E07D/$E07E | $E07D/$E07E +1, $E01D bit 6 set | medium |
| E28D | setExchangeFlags | ORs A into the exchange flag byte $E01D; if the result has bits 7 and 5 set and bit 6 clear (mask $E0 == $A0) the exchange is complete, so it falls into countCompletedExchange which sets bit 6 as well. A = $80 marks 'opponent packet received', $20 marks 'own packet sent', $40 marks 'complete', $08 is used as an idle/keep-alive marker. | A = flag bits | $E01D updated, possibly $E07D/$E07E | high |
| E29A | commLinkControlDispatch | Dispatcher behind $E003. X=0 -> openCommLink; X=1 -> initCommModule; X=2 -> stopCommNmi + clearLinkVars; X>=3 -> hangUpModem, spin until $E03F <= 1, then stopCommNmi + clearLinkVars. | X | see cases; ends by clearing $E039-$E047 | high |
| E2B1 | initCommModule_E2B1 | $E003 with X=1, called from $0F46 immediately after the comm module has been loaded. Clears the protocol vars, the packet vars, the UART/ARQ ring vars and the ARQ state block, and switches the three link I/O vectors to the framed (ARQ) implementations via selectFramedLinkVectors. | none | $E07D-$E0A7, $E01D-$E02F, $E414-$E46D, $EAB7-$EB02 zeroed; $E3B7/$E3BA/$E3BD patched | high |
| E2BD | clearArqVars | Tail of initCommModule: JSR $EAB2 (which clears $EAB7-$EB02, the frame/ARQ layer state) and RTS. | none | $EAB7-$EB02 = 0 | high |
| E2C1 | openCommLink_E2C1 | $E003 with X=0, called from openCommLink ($1B70) in main. If the link is not already up it reloads the baud parameters for the current baud index, installs the NMI vector at $FFFA/$FFFB, restarts the UART and sets $E03B. It then always configures the user-port lines and, unless $E041 says otherwise, queues the Hayes init string selected by $E011 (answer: 'ATQ0V1X1A', originate: 'ATQ0V1X1D'), and finally releases the service lock ($E0A7 = 1). | $E03B, $E055, $E041, $E011 | $E03B = 1, NMI vector, CIA2 configured, modem command queued, $E0A7 = 1 | high |
| E2EC | readKeyAndHandleModemHotkeys | Implementation of $E015. Calls the patched keyboard hook, optionally paints the border with the link status colour from borderColourTable (indexed by connectionPhase*2 + carrier), and then, when the Commodore key is held (X bit 7) and a new key code with bit 7 set arrives, interprets it as a modem hot key: $C8 'H' toggles DTR/PB5 via the mask $E04C, $C3 'C' toggles the carrier-override bit in $E04A, $B3 '3' selects 300 baud, $B1 '1' selects 1200 baud, $8D RETURN re-opens the link without re-sending the init string, $D0 'P' hangs up, $C1 'A' selects answer mode and $CF 'O' selects originate mode. A handled key is swallowed by returning $FF. | keyboard hook result (A/X/Y), $E03B, $E03C, $E040, $E046 | A = key code or $FF, VIC_BORDER, $E046, $E011, $E055, $E04A, CIA2_PRB, modem command queued | medium |
| E353 | loadBaudParameters | Copies the 3-byte entry at $E059+X into $E056-$E058 (bit-period low, bit-period high, transmit pacing divisor) using the trick 'LDY #$FD / STA $DF59,Y' so that Y=$FD..$FF addresses $E056..$E058. X=0 selects {$50,$0D,$01} = $0D50 cycles = 300 baud, X=3 selects {$53,$03,$02} = $0353 cycles = 1200 baud. Returns A=$FF (so the caller in the hot-key dispatcher swallows the key). | X = 0 or 3 | $E056-$E058, A=$FF | high |
| E38D | hangUpModem_E38D | Drops DTR (dropDtrLine writes $22 to CIA2_PRB) and then queues the string at $EB00+$EBFD ($EBD6), which the transmitter emits in reverse as [pause] '+++' [pause] CR [pause] 'ATH0' CR - the Hayes escape sequence with guard times followed by the hang-up command. Reached by $E003 X>=3 and by the 'P' hot key. | $EBFD, $E048 bit 7 | CIA2_PRB, host-output ring loaded with the hang-up string, $E03F = $FF | high |
| E393 | queueModemCommandString | Loads a canned modem command string into the host-output ring. A is the low byte of a string address in page $EB; the routine patches the LDA $EB00,X at $E3A1, copies bytes until it copies a zero, and then sets the ring write index to 0, the read index and the count to the number of bytes copied. Because the ring pops with a decrementing index, the string is transmitted in reverse of how it is stored, which is why the AT strings in the $EBD6-$EBFC table are written backwards. Does nothing (returns A=$FF) when bit 7 of the modem option byte $E048 is clear. | A = offset in page $EB, $E048 | $E092.., $E08F, $E090, $E02E, $E03F = $FF, A = $FF | high |
| E3B2 | returnNoKey | LDA #$FF / RTS - the 'no key / handled' return shared by the hot-key dispatcher and by queueModemCommandString when no modem is configured. Also the unpatched target of the $E012 jump-table entry. | none | A = $FF | high |
| E3B6 | sendLinkByte | JMP ($E3BF) - indirect 'send one byte to the link'. The operand byte at $E3B7 selects between $E3BF (raw: push into the UART transmit ring) and $E3C1 (framed: push into the ARQ transmit ring). initCommModule always selects the framed variant. | A = byte | C=1 when the target ring is full | high |
| E3B9 | receiveLinkByte | JMP ($E3C3) - indirect 'get one byte from the link'. The operand at $E3BA selects $E3C3 (raw: pop the UART receive ring) or $E3C5 (framed: pop the ARQ receive ring). | none | A = byte, C=0, or C=1 when empty | high |
| E3BC | reportLinkError | JMP ($E3C7) - indirect link-error hook called by the NMI receiver on a framing error, a bad stop bit or a receive-ring overflow. The operand at $E3BD selects $E3C7 (no-op stub at $E408) or $E3C9 (count the error at $E403). initCommModule selects the counting variant. | none | $E047 incremented (framed variant) | high |
| E3CB | pollCarrierState | Implementation of the $E00C entry, run once per raster IRQ. Calls serviceCarrierAndSuspendRequest to take a fresh carrier sample into $E03D, then debounces: while the sample differs from the accepted state $E03C it counts up in $E043 and only accepts the new state after $F0 (240) consecutive differing samples; otherwise it merges only bit 7 of the sample into $E03C. Bit 6 of $E03C is the 'carrier present' flag the game tests. | $E03D, $E03C, $E043 | $E03C, $E043 | high |
| E3F3 | selectFramedLinkVectors | Patches the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD to $C1/$C5/$C9, moving sendLinkByte, receiveLinkByte and reportLinkError from the raw-UART implementations to the framed (ARQ) implementations and the error counter. Called once from initCommModule. | none | $E3B7, $E3BA, $E3BD | high |
| E403 | countLinkError | NOP / INC $E047 / RTS - the framed link error hook: bumps the link error counter that runModemStateMachine inspects at $E7FA. Currently rendered as .byte data by the disassembler. | none | $E047 +1 | high |
| E408 | ignoreLinkError | PHP / PHA / NOP / PLA / PLP / RTS - the raw-mode link error hook, a no-op that preserves A and the flags. Followed by a stray RTS at $E40E. Currently rendered as .byte data. | none | none | high |
| E40F | clearUartVars | LDX #$5A / JSR clearInlineVarBlock followed by the 90 bytes it clears ($E414-$E46D): the UART transmit ring (22), the UART receive ring (20), the ARQ transmit ring (18) and the ARQ receive ring (18) together with their counts and read/write indices. Called from initCommModule. | none | $E414-$E46D = 0 | high |
| E46E | popArqRxRing | Pops one byte from the 18-byte ARQ receive ring at $E45C (count $E459, read index $E45A, index decrements and wraps 0 -> 17); C=1 when empty. This is the 'framed' target of receiveLinkByte, so it is how the packet layer gets the payload the frame layer has validated. Currently rendered as .byte data by the disassembler. | $E459, $E45A, $E45C-$E46D | A = byte, C = 0/1 | high |
| E486 | popArqTxRing | Pops one byte from the 18-byte ARQ transmit ring at $E447 (count $E444, read index $E445). Reached through the thunk at $E733 from the frame builder at $E8AE/$E8D4, i.e. this is where the frame layer takes the bytes the packet layer queued. | $E444, $E445, $E447-$E458 | A = byte, C = 0/1 | high |
| E49E | pushArqRxRing | Pushes A into the 18-byte ARQ receive ring at $E45C (count $E459, write index $E45B); drops the byte when the ring is full. Reached through the thunk at $E736 from the frame receiver at $EA91. | A | $E45C ring, $E459, $E45B | high |
| E4B7 | pushArqTxRing | Pushes A into the 18-byte ARQ transmit ring at $E447 (count $E444, write index $E446); returns with the ring unchanged when it already holds 18. This is the 'framed' target of sendLinkByte. Currently rendered as .byte data by the disassembler. | A | $E447 ring, $E444, $E446 | high |
| E4D0 | returnRingEmpty | SEC / RTS - the shared 'ring empty' exit used by four of the ring pop routines in the $E486-$E51B group. | none | C = 1 | high |
| E4D2 | popUartRxRing | Pops one byte from the 20-byte raw UART receive ring at $E430 (count $E42D, read index $E42E). Used directly by the modem terminal loop ($E799, $E80A), through the thunk at $E739, and as the raw target of receiveLinkByte. | $E42D, $E42E, $E430-$E443 | A = byte, C = 0/1 | high |
| E4EA | popUartTxRing | Pops one byte from the 22-byte raw UART transmit ring at $E417 (count $E414, read index $E415). Called by the NMI transmitter (startNextTxChar) and by flushUartTxRing. | $E414, $E415, $E417-$E42C | A = byte, C = 0/1 | high |
| E502 | pushUartRxRing | Pushes A into the 20-byte raw UART receive ring at $E430 (count $E42D, write index $E42F); returns C untouched and leaves the ring alone when full, which the NMI receiver treats as an overflow error. Called only from the NMI at $E71F. | A = received character | $E430 ring, $E42D, $E42F | high |
| E51B | pushUartTxRing | Pushes A into the 22-byte raw UART transmit ring at $E417 (count $E414, write index $E416); drops the byte when full. Called from queueByteForTransmit and, in raw mode, from sendLinkByte. | A | $E417 ring, $E414, $E416 | high |
| E534 | flushUartTxRing | Drains the raw UART transmit ring by popping until it is empty, then sets the pending-byte count $E0A5 to the current transmitter-busy flag $E5BB. Used when the connection state changes so that stale bytes are not sent. | $E414 ring, $E5BB | transmit ring emptied, $E0A5 | high |
| E540 | configureUserPortLines | Sets up CIA2 for the user-port serial link: TA latch $0003 and CRA = $51 (TA running, serial port in output mode), writes the pattern from $E051 indexed by ((3 AND $E011) EOR $E055) into CIA2_SDR, sets CIA2_DDRB to $26 (PB1/PB2/PB5 outputs = RTS, DTR and one spare), drives CIA2_PRB with $E04F,X ($26 = RTS and DTR asserted) and finally raises PA2 (TXD) to the mark level. The purpose of the CIA2_SDR write is unclear - SP2 is a user-port pin, so it may drive an external handshake line, or it may be vestigial. | $E011, $E055, $E04D, $E04F, $E051 | CIA2_CRA, CIA2_TA, CIA2_SDR, CIA2_DDRB, CIA2_PRB, CIA2_PRA | medium |
| E574 | dropDtrLine | Writes $E04E ($22) to CIA2_PRB, which clears PB2 = DTR while leaving PB1/PB5 high. On a Hayes modem dropping DTR terminates the call. Called only from hangUpModem. | $E04E | CIA2_PRB | high |
| E57B | installCommNmiVector | Writes $E685 into the RAM NMI vector at $FFFA/$FFFB so that CIA2 interrupts enter this module's bit-bang UART handler. Called from openCommLink. | none | $FFFA/$FFFB = $E685 | high |
| E586 | restartUart | Clears the six bit-level UART state bytes ($E5BB-$E5C0), sets $E042 = 1 (ask the modem state machine to re-evaluate), primes the carrier poll timer $E045 with $92 and falls into setCiaNmiMask with A=$92, which enables the CIA2 FLAG (start bit) and timer B (transmit) NMI sources and marks the module as running. | none | $E5BB-$E5C0 = 0, $E042 = 1, $E045 = $92, CIA2 ICR/CRA/CRB, $E033, $E039, $E03A = 0 | high |
| E593 | setCiaNmiMask | Stores A into the ICR shadow $E033, acknowledges pending CIA2 interrupts, writes the new mask to CIA2_ICR, stops both timers, and derives the suspend flags: A EOR $FF AND $80 gives $00 for an enable mask ($92 or $83) and $80 for the disable mask ($7F), which is written to both $E039 and $E03A. Ends by jumping to the 'raise TXD' tail at $E56B. | A = CIA2 ICR command byte | $E033, CIA2_ICR/CRA/CRB, $E039, $E03A, CIA2_PRA bit 2 | high |
| E5B6 | clearUartState | LDX #$06 / JSR clearInlineVarBlock followed by the 6 bytes it clears ($E5BB-$E5C0): transmitter-active flag, bit-count reload, transmit bit counter, transmit shift register, receive bit counter and receive shift register. | none | $E5BB-$E5C0 = 0 | high |
| E5C3 | stopCommNmi | Sets $E042 = $FF (ask the modem state machine to fall back to phase 0), calls setCiaNmiMask with $7F to disable every CIA2 NMI source (which sets $E039/$E03A = $80 = suspended) and, if a character was still being transmitted, decrements the pending count $E0A5. | $E5BB | CIA2 interrupts off, $E039/$E03A = $80, $E042 = $FF, $E0A5 | high |
| E5D6 | serviceCarrierAndSuspendRequest | First half of the $E00C service. Handles the suspend handshake used by the disk loader: $E039 bit 7 set means 'suspend' and calls stopCommNmi unless $E03A already acknowledges it, $E039 clear while $E03A still says suspended calls restartUart. Then, when the link is active, it samples carrier detect - CIA2_PRB bit 4 (DCD), XORed with the polarity/override byte $E04A - and stores $F8 (carrier) or $80 (no carrier) into $E03D. $E045 is a free-running counter that periodically re-arms the sample. | $E039, $E03A, $E03B, $E04A, CIA2_PRB, $E045 | $E03D, $E045, NMI enabled/disabled | medium |
| E5FF | rearmCarrierTimer | Continuation inside serviceCarrierAndSuspendRequest (also called directly by the T35 build): stores the current carrier sample into $E045 and clears bit 7 of the value that goes into $E03D, marking the sample as 'not yet stable'. | A | $E045, A | medium |
| E607 | startNextTxChar | Starts the transmission of the next character. Returns immediately when timer B is already running or a character is still in flight; outside terminal mode it also paces characters with the $E044 counter and requires carrier. It then pops a byte from the UART transmit ring into the shift register $E5BE, sets $E5BB, the 9-bit count $E5BC, reloads the pacing counter from $E058, programs timer B with a short $0150 initial delay and starts it (CRB = $11), and finally falls through into setBitPeriod which reloads the TB latch with the full bit period so every subsequent underflow is one bit time. | $E040, $E044, $E03C, $E03D, $E0A5, transmit ring | $E5BB, $E5BE, $E5BC, $E044, CIA2_TB, CIA2_CRB | high |
| E654 | setBitPeriodFull | LDA $E057 then falls into setBitPeriod: programs the timer selected by X (X=0 timer A, X=2 timer B) with the full bit period $E057:$E056. | X = 0 or 2 | CIA2 TA or TB latch, A = $11 | high |
| E657 | setBitPeriod | Writes A into CIA2_TA_HI+X and $E056 into CIA2_TA_LO+X, then loads A with $11 (start + force load) for the caller to store into the corresponding control register. X=0 addresses timer A (receive clock), X=2 timer B (transmit clock). | A = high byte, X = 0 or 2, $E056 | CIA2 timer latch, A = $11 | high |
| E663 | nmiStartNextChar | Timer B path of the NMI when the bit counter has run out. If a character is active it reloads the bit counter from $E5BC (9) and drives TXD low to emit the start bit; otherwise it calls startNextTxChar to fetch the next byte. | $E5BB, $E5BC, CIA2_PRA | $E5BD, CIA2_PRA bit 2 | high |
| E685 | commNmiHandler | The module's CIA2 NMI handler (installed at $FFFA/$FFFB by installCommNmiVector; the disassembly leaves this address unlabelled). It saves A and X, masks CIA2 interrupts, reads the ICR (with a timer-B race workaround around CIA2_TB_HI) and, when the interrupt is not from CIA2, restores and chains through JMP ($E031) to the game's NMI handler. Timer B drives the software transmitter one bit at a time out of $E5BE onto PA2 (TXD); the CIA2 FLAG line (RXD start bit) arms timer A at half a bit time and then a full bit time, and each timer A interrupt shifts CIA2_PRB bit 0 (RXD) into $E5C0 until a whole character with a valid stop bit is pushed into the UART receive ring. Framing errors, bad stop bits and receive overflow call reportLinkError. | CIA2_ICR, CIA2_PRB bit 0, $E033, $E5BB-$E5C0 | CIA2_PRA bit 2 (TXD), UART receive ring, $E0A5, $E033, $EB00, $E045; RTI or chain to ($E031) | high |
| E69C | nmiMergeIcrFlags | Two-byte continuation inside commNmiHandler (ORA CIA2_ICR) used to merge any interrupt flags that arrived between the two ICR reads. | A | A | medium |
| E733 | popArqTxRingThunk | JMP popArqTxRing - the entry the frame layer at $E8AE/$E8D4 uses to fetch payload bytes. | none | A, C | high |
| E736 | pushArqRxRingThunk | JMP pushArqRxRing - the entry the frame layer at $EA91 uses to deliver a validated payload byte to the packet layer. | A | ring updated | high |
| E739 | popUartRxRingThunk | JMP popUartRxRing - used by receiveByteTrackZeros and by the frame receiver at $E9E9. | none | A, C | high |
| E73C | queueByteForTransmit | Pushes A into the raw UART transmit ring and increments the pending-byte count $E0A5, which the NMI decrements as each character finishes. Used by the frame layer and by the terminal loop. | A | transmit ring, $E0A5 | high |
| E743 | receiveByteTrackZeros | Pops one byte from the UART receive ring and maintains $EAB8, a counter of consecutive zero bytes (reset to 0 by any non-zero byte). The frame layer uses this to detect an idle or garbage line. | $EAB8, receive ring | A, C, X, $EAB8 | medium |
| E756 | runModemStateMachine | Called from serviceCommTick every frame; drives $E040, the connection phase. $E042 requests a re-evaluation ($FF forces phase 0). Phase 0 with no carrier drains the host-output ring, flushes the transmitter and enters phase 1 (modem command/terminal mode); in phase 1 characters coming back from the modem are echoed into the host-input ring so the player sees the modem's responses, and when a verbose result line ends in '0' before CR (i.e. 'CONNECT 1200') it switches to 1200 baud. Bytes going the other way are taken from the host-output ring; a byte with bit 7 set is not sent but loaded into $EB01 as a delay countdown, which is how the guard times inside the '+++'/ATH0 strings are implemented. Once carrier is detected the routine continues past $E7C9 into the connect handshake that advances to phase 2 and then phase 3, where the packet layer runs. | $E042, $E040, $E03C, $E03F, $E047, $E048, $EB01, $EB02, both host rings, UART rings | $E040, $E03F, $E044, $E047, $E0A4, $E0A6, $EB01, $EB02, baud parameters, host-input ring | medium |
| E773 | clearInFlightFlags | Mid-routine entry of runModemStateMachine (also jumped to from the T35 build): clears the frame-in-flight flag $E0A4 and the packet-in-flight count $E0A6, which is what tells sendPacketFsm that the packet has actually left the transmitter. | X = 0 | $E0A4 = 0, $E0A6 = 0 | medium |
| E7CA | sendNextModemCommandChar | Tail of the per-tick link service routine sub_E756 (reached by BPL from E7C3 once the inter-character delay counter modemDelayCounter/$EB01 has expired). Takes the next byte of the pending modem command string from the module's outbound character queue (sub_E0C3, buffer $E091), and either arms a new delay (byte with bit 7 set, e.g. $A0/$C0 = 96/64 ticks around the '+++' escape) or transmits it with queueTxByte/sub_E73C. After a CR it loads modemReplyTimer/$E03F with $79 (or 2 when the carrier bit $E03C bit 6 is clear); when that timer expires it fabricates a CR into the module's inbound character queue (sub_E0DD) so the game does not hang waiting for a modem answer. | D_EB01 delay counter, D_E03F reply timer, D_E03C bit 6 (carrier/direct-connect), outbound char queue $E091/$E02E via sub_E0C3 | byte queued for transmission (sub_E73C -> $E417 ring, L_E0A5++), D_EB01, D_E03F updated; may push CR into the game-visible receive queue $E086 via sub_E0DD; falls through to the link state machine at E7F7 | medium |
| E7F7 | runLinkStateMachine | Second half of the per-tick link service (sub_E756). Dispatches on commState/$E040: state >= 2 with resyncRequest/$E047 non-zero, or any state below 2, restarts the byte-sync phase at E805; state 2 continues the sync phase at E828; state 3 runs the packet phase at E858. | D_E040 commState, D_E047 resyncRequest | D_E040 advanced; branches into the sync or packet code | medium |
| E805 | beginByteSyncPhase | Enters commState 2: drains the UART receive ring (sub_E4D2 until empty), flushes the transmit ring (sub_E534), clears resyncRequest/$E047, rxPacketRemaining/$EAB9 and syncFfCount/$EAB7, then loops sending $00 and $FF and counting the peer's answers. It leaves for state 3 (E858) once the peer's $FF has been seen; after ten unsuccessful rounds (syncFfCount >= 10) it restarts the whole sync from E80F. | UART rings ($E417 tx / $E430 rx), D_EAB7 syncFfCount, D_EAB8 zeroRunLength, L_E0A5 pending tx byte count | D_E040 = 2 then 3, D_EAB7/D_EAB8/D_EAB9/D_E047 reset, $00/$FF bytes transmitted, D_E044 /= 2, D_EAC0 ackPending set on success | medium |
| E858 | runPacketPhase | commState 3: the reliable packet layer. Calls receivePacket (sub_E9AA), then, when fewer than two bytes are still queued for transmission, sends the deferred raw sync bytes ($00 when sendZeroFlag/$EAC2 is negative), handles a retransmission request ($EABF) by resending the oldest unacknowledged message, would build a new message from the game's outbound ring when txEnable/$E0A6 is set (never set in this build - see insights), and otherwise emits a bare acknowledgement packet when ackPending/$EAC0 is set. | L_E0A5, D_EAC1/D_EAC2 raw-byte flags, D_E0A4 txBusy, D_E0A6 txEnable, D_EABF resendRequest, D_EAC5/D_EAC6 outstanding-message indices, D_EAC0 ackPending | packets transmitted through sub_E909; D_EAC0/D_EAC1/D_EAC2/D_EABF cleared; D_EAC3/D_EAC4/D_EAC5/D_EAC6/D_EABE updated | medium |
| E8F8 | buildAckPacket | Builds a two-byte bare acknowledgement packet in packetBuffer/$EAEB: length code $55 (= one payload byte) followed by control byte $EC, then falls into the common packet tail at E90C which adds the sequence bits and the checksum and transmits it. | none | D_EAEB = $55, D_EAEC = $EC, D_EAEA = 2, then packet sent; D_EAC0 cleared | high |
| E909 | sendPacket | Sends the message stored in the transmit ring at index A: builds the packet with buildPacketFromRing, ORs the low two bits of rxSeqCounter/$EABD into the control byte, appends the checksum from computePacketChecksum and clocks all bytes out through queueTxByte/sub_E73C. Entry point E90C skips the build step (used by buildAckPacket). | A = index into txMessageRing $EAC7; D_EABD sequence counter | D_EAEA/D_EAEB..: finished packet; bytes pushed into the UART tx ring; D_EAC0 cleared; D_EABA, D_EAC3 clobbered | high |
| E937 | computePacketChecksum | Rotate-and-add checksum over packetBuffer[0..packetLength-1]: A starts at $EE and for each byte does ASL / ADC #0 (fold the carry back in) / ADC byte / ADC #0. Used both when sending (result appended after the payload) and when receiving (result EORed with the received check byte, zero = good). | D_EAEA packet length, D_EAEB.. packet bytes | A = checksum, X = D_EAEA (index of the check byte slot) | high |
| E94A | buildPacketFromRing | Copies one queued message out of the 35-byte transmit ring into packetBuffer. A = ring index of the message's length byte; the length is turned into one of the 16 transition-rich code bytes from packetLengthCodeTable and written as packet byte 0, then length+1 further ring bytes (the command byte and its arguments) are appended. | A = ring index; D_EAC7 ring, D_EAA0 length-code table | D_EAEB.. packet, D_EAEA = number of bytes written, D_EAC3 advanced past the message, D_EABA clobbered | high |
| E96D | writeTxRingByte | Appends A to the 35-byte transmit message ring $EAC7 at index txRingIndex/$EAC3 and advances the index modulo $23. | A = byte, D_EAC3 | ring updated, D_EAC3 advanced, X = new index | high |
| E975 | readTxRingByte | Reads the byte at txRingIndex/$EAC3 from the transmit ring and advances the index (wrapping at $23). | D_EAC3 | A = byte, D_EAC3 advanced, X = new index | high |
| E978 | readTxRingByteAt | Reads the transmit-ring byte at the arbitrary index X, then sets txRingIndex/$EAC3 to X+1 (wrapped). Used to re-read a message for retransmission and to inspect an acknowledged message's header. | X = ring index | A = byte, D_EAC3 = X+1 mod $23, X = new index | high |
| E986 | noteIdlePoll | Called when receivePacket found no data. Only counts the poll as a stall if something is actually outstanding (oldest unacked index $EAC6 negative, or current message index $EAC5 negative, or txBusy $E0A4 set); otherwise stallCounter/$EB00 is reset to 0. Entry sub_E997 always bumps the counter. When the counter reaches 16 it drops the caller's return address (PLA/PLA) and jumps back to beginByteSyncPhase. | D_EAC5, D_EAC6, D_E0A4, D_EB00 | D_EB00 updated; on overflow the stack is unwound and control jumps to E805 (link resync) | high |
| E9AA | receivePacket | Assembles one incoming packet from the UART receive ring. Skips single $00 bytes (a lone $00 sets resendRequest/$EABF = 1; two or more in a row force a resync) and $FF bytes (which set ackPending/$EAC0). The first non-sync byte is looked up in packetLengthCodeTable to get the payload length; the routine then waits until that many bytes are queued ($E42D), copies them into packetBuffer and verifies the checksum. A bad checksum decrements sendZeroFlag/$EAC2 (making the sender emit a $00 nak on the next tick) and returns; a good packet falls into dispatchReceivedPacket. | UART rx ring $E430/$E42D via sub_E739/sub_E743, D_EAB9 (resumable state), D_EAA0 table | D_EAB9, D_EAFF, D_EAEA, D_EAEB.., D_EABF, D_EAC0, D_EAC2, D_EB00; may resync via L_E9A2 | high |
| EA0D | dispatchReceivedPacket | Acts on a checksum-verified packet. Control byte >= $F0 is forwarded to the 1541 with sendByteToDrive/$089C inside an SEI (see problems - the payload index looks wrong in this build). Control byte $E0..$EF is a pure acknowledgement: it retires the outstanding message(s) by comparing the sequence field of the acked message against the ack byte and swapping the two outstanding-message slots $EAC5/$EAC6/$EAC4. Anything below $E0 is a data packet: it is accepted only when its 2-bit sequence field ((byte>>2)-rxSeq-1)&3 == 0 and the game's inbound ring is empty ($E459 == 0), then rxSeqCounter/$EABD is bumped, the original command byte is rebuilt as (control & $F0) / (payloadLength & $0F) and the command plus its arguments are pushed into the game's inbound ring through sub_E736. | D_EAEB.. packet, D_EAEA length, D_EABD rxSeq, D_E459 inbound ring count, D_EAC4/D_EAC5/D_EAC6 | D_EABB/D_EABC/D_EABD/D_EABF/D_EAC0/D_EAC5/D_EAC6 updated; decoded command bytes pushed into the $E45C ring | medium |
| EA7B | deliverPacketPayload | Inner loop of dispatchReceivedPacket: length-2 payload bytes plus the reconstructed command byte are pushed one at a time into the game's inbound command ring with sub_E736 ($E49E, ring $E45C). | D_EAEA, D_EAEB.., D_EAEC | bytes pushed into $E45C, D_E459 incremented, D_EAEA/D_EAFE clobbered | high |
| EAA0 | packetReturn | A single RTS that doubles as index 0 of packetLengthCodeTable (the table is addressed as D_EAA0,x with x = 1..16). Several branches in the packet layer target it as their exit. | none | returns; the byte $60 is also read as table entry 0 (never matched, since a length of 0 is impossible) | high |
| EAB2 | clearPacketState | Zeroes the whole 76-byte packet-protocol state block $EAB7-$EB02 by calling the shared 'zero X bytes that follow the JSR' helper sub_E067 with X = $4C. Because sub_E067 pops the return address to build its self-modified STA, this routine returns to its caller's caller ($E2BD, the module init path reached through the $E003 jump-table entry). | none | $EAB7-$EB02 = 0; returns two levels up | high |
| EB03 | remoteCodeBuffer | Entry of the downloadable-code buffer. The message engine at $E196 copies the payload of every received message whose command byte is >= $70 to $EB03 onward (self-modified index at $E1AE) and executes it with JSR $EB03 when a $70+ message arrives with an empty payload. In the shipped image the first byte is $60 (RTS), so the call is a no-op until the peer has downloaded code. | code previously written by $E1B3 | whatever the downloaded code does | medium |
| EC00 | clearBattlefieldMap | Fills the 1600-byte battlefield map $F000-$F63F with $40 (empty terrain) using the zp_48/zp_49 source pointer, zp_4A/zp_4B limit and the shared pointer-advance/compare helper sub_C3F9. Called by overlay A from generateBattlefieldMap ($6F30) and from the map editor ($76F2). | none | $F000-$F63F = $40; zp_48/49 = $F640, zp_4A/4B = $F63F, zp_4E/4F advanced by 1600 | high |
| EC1C | setViewOriginToHome | Sets the battlefield view origin to column $11 (17) row $1C (28) both in the live zero-page pair zp_A5/zp_A6 and in the saved copy $92C3/$92C4, which puts the cursor cell (origin+3, origin+2) at map (20,30), the middle of the player's own end. Called by overlay A at $7AEC (battle start), $7D51 and $83EF (recycler placement). | none | zp_A5 = $11, zp_A6 = $1C, D_92C3 = $11, D_92C4 = $1C | medium |
| EC2B | loadMapSeedIntoScenarioRng | Copies the 24-bit map seed from the game settings block ($0B95-$0B97) into the scenario random-number state zp_5A/zp_5B/zp_5C, so that both machines generate the identical procedural battlefield. Called by overlay A at $6F30-$6F33 (generateBattlefieldMap) and $7BB2. | D_0B95, D_0B96, D_0B97 | zp_5A/zp_5B/zp_5C, X = $0B97 value, Y = $0B96 value | high |
| EC54 | makeMapPointSymmetric | Entry with mirrorMode/zp_18 = $FF: walks a forward pointer from $F000 and a backward pointer from $F63F over the whole map and writes mirrorTerrainCode(map[i]) into map[1599-i]. Because the mirror is an involution the second half of the pass is a no-op, so the net effect is that the second half of the map becomes the point-mirrored copy of the first half - the map generator's symmetry step. Called from overlay A $6F77. | map $F000-$F63F | map made point-symmetric; zp_18 = $FF, zp_48/49, zp_4A/4B, zp_4E/4F clobbered | medium |
| EC58 | rotateMapAndMirrorCodes | Entry with mirrorMode/zp_18 = $00: exchanges map[i] and map[1599-i] for the whole map, passing both bytes through mirrorTerrainCode, i.e. rotates the battlefield 180 degrees and flips the direction-dependent terrain codes with it. Used so that each player always sees his own side at the bottom. Called from overlay A at $76B5, $7749, $7AE1 and $7D2E. | map $F000-$F63F | map rotated in place; zp_18 = 0, zp_48/49, zp_4A/4B, zp_4E/4F clobbered | high |
| EC91 | mirrorAllUnitCoordinates | Mirrors the six 100-entry unit coordinate arrays that follow the map - unitCol $F640, unitRow $F6A4, unitDestCol $F7D0, unitDestRow $F834, unitWaypointCol $F898 and unitWaypointRow $F8FC - by calling mirrorCoordinateArray on each. Called from overlay A $7AE9 when the local player is side 1, so his units line up with the rotated map. | the six coordinate arrays | all six arrays mirrored; zp_48/zp_49 clobbered | high |
| ECB8 | mirrorCoordinateArray | For the 100-byte array at A/Y, replaces the low 6 bits of every entry with mirrorMapCoordinate ($3FA3, i.e. 39-n) while preserving the top two flag bits (which carry cloak/blitz/idle markers). | A = array low byte, Y = array high byte | array entries mirrored; zp_48/zp_49 = array pointer; self-modifies the ORA operand at $ECCD | high |
| ECD4 | mirrorTerrainCode | Maps one map cell byte to its 180-degree-rotated equivalent using mirroredTerrainCodeTable. Bytes with bit 7 set (cells holding a unit index) are returned unchanged; codes $40..$58 are looked up directly; codes $20..$3F are looked up through the same table with a -$20 shift; anything else is passed through. | A = map cell byte | A = mirrored cell byte, X clobbered | high |
| ECF3 | loadUnitStartTemplate | If $F640 has bit 7 set (template data still present), copies 512 bytes from $F000 + 400*unitLayoutByGameType[gameType & 7] to $F640, installing the unit start-position template (unitCol, unitRow, flags, type) for the selected game type; track 18 sectors 8-14 hold four such 400-byte templates rather than a battlefield map. Called from overlay A at $79E6 and $82FD, before the map is generated. | D_F640 bit 7, D_0BA3 bits 0-2 = game type, unitLayoutByGameType $78B8, template data at $F000 | $F640-$F83F overwritten with the template; self-modifies the source/destination operands at $ED31/$ED32 and $ED34/$ED35 | high |
| ED43 | exchangePlayerNamesAndSettings | Start-of-session handshake. In trainer mode ($0BA5 bit 7 set) it just copies the ten-character string 'TRAINER   ' from $884E into opponentName $FFE8 and returns. Otherwise it waits for the link, releases any pending message, exchanges the two halves of the ten-byte owner name with exchangeNameHalf (offsets 0 and 5), substitutes 'ENEMY' ($CC99) when the peer's name starts with a space, and then swaps a four-byte settings message ($FF, commBuildId $E030, gameSettingsBlock $0B94, $0B7E), storing the peer's build id in $E02F and the EOR of the peer's last two bytes in $90F8. Any protocol error goes to abortLinkAndReturnToMenu. Called from overlay A $79D1. | D_0BA5, ownName $FFDE, D_E030, D_0B94, D_0B7E, comm API $E000/$E01D-$E02A | opponentName $FFE8-$FFF1, D_E02F, D_90F8; link messages sent/consumed | high |
| EDBE | sendEmptyMessage | Sets the outgoing message length $E01F to 0 and asks the comm module to transmit it (jump-table entry $E000 with X = 1). Used to close the handshake and as part of the error path. | none | D_E01F = 0, transmit triggered | high |
| EDC9 | exchangeNameHalf | Sends a six-byte message ($FF followed by ownName[X..X+4]) to the peer, waits for the reply, checks that it is six bytes starting with $FF and copies the peer's five characters into opponentName $FFE8+X; on any mismatch it falls into abortLinkAndReturnToMenu. X is preserved across the exchange in the self-modified LDX at $EDEB. | X = offset 0 or 5 into the ten-byte name, D_FFDE ownName | $FFE8+X..+4 = peer name half; comm buffers used; self-modifies $EDEB | high |
| EDFC | abortLinkAndReturnToMenu | Handshake failure path: plays sound 3 (sub_C6FF), waits 60 frames (sub_C37F), runs completeExchange ($56F1), sends an empty message and jumps to returnToMainMenu ($0AA3). | none | never returns to its caller; game returns to the main menu | high |
| EE0F | storeOpponentNameHalf | Success tail of exchangeNameHalf: copies the five payload bytes of the received message into opponentName $FFE8+X and releases the receive buffer with the comm API (X = 2). | X = offset, D_E020.. received message | $FFE8+X..+4 written, receive buffer released | high |
| EE20 | waitForLinkMessage | Spins until bit 6 of the comm status byte $E01D is set, i.e. until a complete message has been received (or the pending transmit has completed). Used by the handshake code. | D_E01D | returns when $E01D bit 6 is set; no registers changed | high |
| EE26 | finaliseRecyclerSetup | For both sides (X = 1 then 0): if recyclerMode/$92A6[X] is 2 ('none'), marks that side's recycler coordinates $92A8/$92AA as $FF, and when the side is the opponent also patches the first byte of checkEnemyRecyclerSpotted ($3F57) with $60 (RTS) so the 'SPOTTED RECYCLER!' scan never runs. Finally draws the local player's recycler on the map (drawRecyclerOnMap with Y = playerSide $0B9F). Entered by JMP from overlay A $7BAF. | D_92A6, D_0B9F playerSide | D_92A8/$92AA set to $FF for sides without a recycler, $3F57 possibly patched to RTS, map cell drawn | high |
| EE4A | loadFilmStartSnapshot | Restores the initial game state from the game-film recording: points the film pointer zp_BC/zp_BD at $D034, reads 20 bytes into the score/option block $92A2, copies one page from $D048 to the unit arrays at $F640 with copyPageUnderIo, copies three more pages using the snapshot source/destination addresses returned by getFilmSnapshotDestAddr/$5983 and getFilmSnapshotSrcAddr/$5988, rewinds the film pointer and finally reads the clock byte into $91CB and the film restart pointer into zp_BC/zp_BD. Called from overlay A $7D41 (start of film playback). | film data at $D000+, readFilmByte $5874, copyPageUnderIo $58B2, rewindFilmPtr $58D5 | D_92A2..$92B5 (20 bytes), unit arrays at $F640+, D_91CB, zp_BC/zp_BD film pointer, $58C3/$58C4/$58C6/$58C7 copy operands, zp_19 | medium |
| EEB9 | handleDisplayToggleKey | Trainer-mode F1 handler, polled every frame from the main idle loop at $600A (only when $0BA5 bit 7 is set). Acts only when currentKey/zp_66 is 0 (the F1 key code); then sets a 60-frame input lockout ($0B7D) and falls into announceAndToggleDisplayMode, whose first store puts $3C into zp_66 so the key does not retrigger. | zp_66 currentKey | D_0B7D = $3C, zp_66 = $3C, display mode toggled and messages queued | medium |
| EEC3 | announceAndToggleDisplayMode | Queues message slot $21 with 'BEGINNER DISPLAY SET.' ($EF28) or 'STANDARD DISPLAY SET.' ($EF3D) - the message names the mode that is about to be entered, since the flag is still the old value - and falls into toggleBattleDisplayMode. | A = $3C on entry (stored into zp_66), D_92FC revealAllUnits flag | zp_66 = $3C, message slot $21 pointer set (sub_C924) and queued (sub_C566) | medium |
| EEDD | toggleBattleDisplayMode | Flips revealAllUnits/$92FC (bit 7 set = beginner display, all enemy units visible), then patches the eight-character slot at $EF10 with 'BEGINNER' or 'STANDARD' (whichever F1 will switch to next) and queues the resulting prompt 'F 1 FOR xxxxxxxx BATTLE DISPLAY.' ($EF08) as message slot $20. Also called on its own from overlay A $824B when a trainer battle starts. | D_92FC | D_92FC toggled, $EF10-$EF17 patched, message slot $20 registered and queued | medium |

Data:

- `$E000` **commJumpTable** (addrTable, 24 bytes): Seven JMP entries at $E000/$E003/$E006/$E009/$E00C/$E012/$E015, with a two-byte constant pool at $E00F ($60 = RTS, $E010 = $40 = RTI, the file's default nmiChainVector target) and the isOriginateMode flag at $E011 wedged between them.
- `$E01D` **packetVarBlock** (byteTable, 19 bytes): Inline variable block zeroed by clearPacketVars: exchangeFlags, rx/tx packet lengths, the two 7-byte packet buffers, hostOutCount and remoteIdentityByte. The disassembler renders the first byte as BRK.
- `$E030` **commBuildAndNmiVars** (byteTable, 4 bytes): commBuildId ($00 for this modem build), the two-byte nmiChainVector ($E010 in the file, patched to $1298 by the game) and the CIA2 ICR mask shadow ($90 in the file, $92/$83/$7F at run time).
- `$E039` **linkVarBlock** (byteTable, 15 bytes): Inline variable block zeroed by clearLinkVars: NMI suspend request/ack, isLinkActive, carrier state and sample, $E03E (unused), modemCommandPhase, connectionPhase, skipModemInitString, uartRestartRequest, carrierDebounceCount, txPaceCounter, carrierPollTimer, lastHotkeyCode and linkErrorCount. Its file content spells 'DTH' at $E045-$E047, which suggests the page was captured from a running machine (this page comes from track 18 sector 7, outside the encrypted area).
- `$E048` **modemConfigTables** (byteTable, 31 bytes): Constants that survive every clear: modemOptionFlags $CC, carrierOverrideFlags, the toggle/port masks $40/$24/$26/$22, the CIA2_PRB table, the CIA2_SDR pattern table, baudIndex, the live bit-period triple, the two-entry baudParameterTable and the eight-entry linkStatusBorderTable at $E05F-$E066.
- `$E07D` **protocolVarBlock** (byteTable, 44 bytes): Inline variable block zeroed by clearProtocolVars ($E07D-$E0A7) plus the constant packetSlotIndex at $E0A8: the completed-exchange counter, the rx/tx packet state machines, the 8-byte host-input ring and the 16-byte host-output ring with their counts and indices, the in-flight counters and the service lock.
- `$E3B5` **padByteE3B5** (unknown, 1 bytes): Stray $10 between returnNoKey and the indirect link I/O jumps; not referenced.
- `$E3BF` **linkIoVectorTable** (addrTable, 12 bytes): Three pairs of alternative targets for the indirect jumps at $E3B6/$E3B9/$E3BC: send = $E51B (raw UART) or $E4B7 (framed), receive = $E4D2 (raw UART) or $E46E (framed), error = $E408 (ignore) or $E403 (count). selectFramedLinkVectors picks the second of each pair.
- `$E414` **ringBufferBlock** (byteTable, 90 bytes): Inline variable block zeroed by clearUartVars ($E414-$E46D): uartTxCount/indices + 22-byte ring, uartRxCount/indices + 20-byte ring, arqTxCount/indices + 18-byte ring and arqRxCount/indices + 18-byte ring. The last two bytes of the ARQ receive ring ($E46C/$E46D) sit immediately before the popArqRxRing code.
- `$E5BB` **uartBitStateBlock** (byteTable, 6 bytes): Inline variable block zeroed by clearUartState: txCharActive, txBitCountReload, txBitCounter, txShiftRegister, rxBitCounter and rxShiftRegister - the entire state of the software UART.
- `$EAA1` **packetLengthCodeTable** (byteTable, 17 bytes): The 16 byte values $55,$56,$59,$5A,$65,$66,$69,$6A,$95,$96,$99,$9A,$A5,$A6,$A9,$AA followed by an $FF sentinel. Every entry is a byte whose four bit pairs are each 01 or 10, so a length code can never be confused with the $00/$FF sync bytes and always carries plenty of transitions. Addressed as D_EAA0,x with x = payload length (1..16); the table must stay sorted because the receiver searches it with CMP/BCS.
- `$EAB7` **packetProtocolState** (byteTable, 76 bytes): The complete state block of the packet layer, $EAB7-$EB02, zeroed in one go by clearPacketState. Contains the sync counters, sequence numbers, ring indices, the 35-byte transmit message ring at $EAC7, the packet assembly buffer at $EAEB and the modem-delay/terminal bytes at $EB01/$EB02.
- `$EAC7` **txMessageRing** (byteTable, 35 bytes): 35-byte circular buffer of queued outgoing messages. Each entry is [argumentCount][commandByte with the 2-bit sequence in bits 2-3][arguments...]. Wraps at $23; indices are held in $EAC3/$EAC4/$EAC5/$EAC6.
- `$EAEB` **packetBuffer** (byteTable, 19 bytes): Assembly/receive buffer for one packet: [length code][control byte][payload...][checksum]. Maximum 1+16+1 bytes, so it occupies $EAEB-$EAFD.
- `$EB03` **remoteCodeBuffer** (unknown, 211 bytes): $EB03-$EBD5: buffer that receives 6502 code sent by the peer in messages with a command byte >= $70 (copied by $E1B3, executed by JSR $EB03). Its shipped contents are one RTS at $EB03 followed by 210 bytes of leftover build-1 game code (see misclassified).
- `$EBD6` **modemHangUpString** (text, 13 bytes): Modem command string, stored back to front because the character queue at $E091 is read downwards: emitted as <delay $A0> '+' '+' '+' <delay $A0> CR <delay $C0> 'A' 'T' 'H' '0' CR. Bytes with bit 7 set are guard-time delays (96 and 64 ticks), which is exactly the Hayes escape-sequence guard time. Terminated by the $00 at $EBE2.
- `$EBE3` **modemAnswerInitString** (text, 13 bytes): Reversed modem init string for answer mode: 'ATQ0V1X1A' + CR preceded by a 64-tick guard delay. Selected when the player presses 'A' (command byte $C1 at $E37A).
- `$EBF0` **modemDialInitString** (text, 13 bytes): Reversed modem init string for originate mode: 'ATQ0V1X1D' + CR preceded by a 64-tick guard delay ('D' = dial). Selected when the player presses 'O' (command byte $CF at $E37E).
- `$EBFD` **hangUpStringPtrLo** (byteTable, 1 bytes): Low byte ($D6) of modemHangUpString, read by sub_E38D and passed to the string-to-queue routine sub_E393 (the high byte $EB is hard coded in the self-modified LDA at $E3A1).
- `$EBFE` **modemInitStringPtrLoTable** (byteTable, 2 bytes): Low bytes $E3 and $F0 of the answer and dial init strings, indexed by modemModeIndex $E011 at $E2E0.
- `$EC3B` **mirroredTerrainCodeTable** (byteTable, 25 bytes): Maps terrain codes $40..$58 (index 0..24) to their 180-degree-rotated equivalents: $40 stays, then A<->D, B<->C, E<->H, F<->G, I<->K, J<->L, M<->T, N<->Q, O<->S, P<->R, U<->X, V<->W. Also used with a -$20 shift for the second code range $20..$3F.
- `$EF08` **msgDisplayPromptPrefix** (text, 8 bytes): 'F 1 FOR ' - start of the trainer prompt printed as message slot $20; it runs straight into displayModeNameSlot and msgBattleDisplaySuffix.
- `$EF10` **displayModeNameSlot** (text, 8 bytes): Eight-character patch area inside the prompt, filled at run time with 'BEGINNER' or 'STANDARD' by toggleBattleDisplayMode. The shipped image contains '********'.
- `$EF18` **msgBattleDisplaySuffix** (text, 16 bytes): ' BATTLE DISPLAY' plus the bit-7 terminator '.' - the tail of the trainer F1 prompt.
- `$EF28` **msgBeginnerDisplaySet** (text, 21 bytes): 'BEGINNER DISPLAY SET' + bit-7 '.'. The first eight characters double as the source of the 'BEGINNER' patch word.
- `$EF3D` **msgStandardDisplaySet** (text, 21 bytes): 'STANDARD DISPLAY SET' + bit-7 '.'. Reached as msgBeginnerDisplaySet+$15; its first eight characters are the 'STANDARD' patch word.
- `$EF52` **gameOptionLabels** (text, 93 bytes): Run of bit-7 terminated labels used by overlay A's setup menus: 'HANDICAP' ($EF52), 'TERRAIN PTS' ($EF5A), 'DRONES' ($EF65), 'COMCEN SPEED' ($EF6B), 'MAP TYPE'+CR+CR ($EF77), ' (MORE)'+CR ($EF81), 'LOCATING'+CR ($EF89), 'RECYCLER'+CR ($EF92), 'UNIT MENUS'+CR+CR ($EF9B) and 'DAMAGE'+CR+CR ($EFA7).
- `$EFAF` **msgInsertDiskTemplate** (text, 33 bytes): 'INSERT DATA DISK AND PRESS SPACE' + bit-7 '.'. Overlay A's promptInsertDiskAndWaitSpace ($81B2) patches the four characters at $EFB6 with either 'DATA' or 'GAME' before showing it as message slot $1E.
- `$EFD0` **wordData** (text, 4 bytes): 'DATA' - patch word for msgInsertDiskTemplate (no terminator).
- `$EFD4` **wordGame** (text, 4 bytes): 'GAME' - patch word for msgInsertDiskTemplate (no terminator).
- `$EFD8` **msgCustomOff** (text, 10 bytes): 'CUSTOM OF' + bit-7 'F' - installed as message slot $1E by overlay A $8218.
- `$EFE2` **msgDestroyOff** (text, 11 bytes): 'DESTROY OF' + bit-7 'F' - installed as message slot $1E by overlay A $86B1; $EFEA is used as the ON/OFF patch address.
- `$EFED` **msgAccepted** (text, 8 bytes): 'ACCEPTE' + bit-7 'D' - printed by overlay A $7710 when the fire button confirms a setting.
- `$EFF5` **commTailFiller** (unknown, 11 bytes): $C0,$DF,$00 followed by four $FF,$00 pairs; unreferenced padding at the end of the sector (the last byte $EFFF also differs between the T34 and T35 tails).

Insights:

- [game_ovl_E000_T34_E000] The T34 $E000 module is a complete three-layer software modem stack. Layer 1 is a bit-banged RS-232 UART on the C64 user port driven entirely from the CIA2 NMI at $E685: CIA2 FLAG (user-port pin B = RXD edge) detects the start bit, timer A samples the data bits mid-bit, timer B clocks the transmitter, PA2 = TXD, PB0 = RXD, PB1 = RTS, PB2 = DTR, PB4 = DCD, and CIA2_DDRB = $26. Layer 2 (outside this chunk, $E8xx-$EAxx) builds checksummed, sequence-numbered frames with retransmission. Layer 3 ($E14D/$E1DD, in this chunk) carries the game's 7-byte command packets and chat characters.
- [game_ovl_E000_T34_E000] The layers are wired together by four ring buffers plus a three-entry indirect vector table at $E3BF. The vectors ($E3B6 send, $E3B9 receive, $E3BC error) can point either at the raw UART rings ($E51B/$E4D2/$E408) or at the frame-layer rings ($E4B7/$E46E/$E403); initCommModule always selects the framed set, so the raw set is effectively dead code. Ring inventory: $E417 (22, UART tx), $E430 (20, UART rx), $E447 (18, frame tx), $E45C (18, frame rx), $E086 (8, host in, read by $E006) and $E091 (16, host out, written by $E009). Every ring uses a decrementing index and a separate count byte.
- [game_ovl_E000_T34_E000] Baud rates are exact CIA timer latches: $0D50 = 3408 cycles = 300 baud and $0353 = 851 cycles = 1200 baud, held in $E056/$E057. Both the transmitter and the receiver start their timer with a deliberately short first interval (transmit $0150, receive half a bit time) and then fall through into setBitPeriod to reload the latch with the full bit period, so the first edge is quick and every following one is a whole bit apart.
- [game_ovl_E000_T34_E000] Hayes modem control is real. $EBD6-$EBFC holds three AT strings stored BACKWARDS (the host-output ring pops with a decrementing index, so queueModemCommandString stores them forwards and they come out reversed): 'ATQ0V1X1A' (answer, offset $E3), 'ATQ0V1X1D' (originate/dial, offset $F0) and the '+++'/'ATH0' escape+hangup pair (offset $D6). Bytes with bit 7 set inside these strings are not transmitted - runModemStateMachine loads them into $EB01 as a delay countdown, which is exactly the guard time the '+++' escape sequence needs. Hang-up also drops DTR by writing $22 to CIA2_PRB.
- [game_ovl_E000_T34_E000] Because the init strings use V1 (verbose) and X1 (extended result codes), the terminal loop can auto-detect the line speed: when a CR arrives and the previous character was '0' it assumes the response line was 'CONNECT 1200' and switches $E055 to the 1200-baud parameters ($E7A7-$E7B4). A plain 'CONNECT' (300 baud) ends in 'T' and is ignored.
- [game_ovl_E000_T34_E000] Modem hot keys are Commodore-key combinations intercepted inside $E015 before the game ever sees the key (the key is swallowed by returning $FF): C=+A answer mode, C=+O originate/dial, C=+3 300 baud, C=+1 1200 baud, C=+H toggle DTR, C=+C toggle the carrier override, C=+P hang up, C=+RETURN re-open the link without re-sending the init string. This is also where the border colour link indicator is painted: in terminal mode the border is light blue with no carrier and blue with carrier; while C= is held the other phases show black/yellow (down), light red/red (handshaking) and light green/green (connected).
- [game_ovl_E000_T34_E000] $E000 X=0 is called from the raster IRQ every frame and is protected by a DEC/INC re-entrancy lock on $E0A7 rather than by SEI. It runs the modem/connection state machine ($E756) unconditionally and the packet send/receive state machines only once connectionPhase ($E040) has reached 3.
- [game_ovl_E000_T34_E000] Packet framing on the wire: one header byte whose low three bits are the payload length. Bit 7 set means the payload is chat/text bytes that go straight into the host-input ring for $E006; bit 7 clear means a game command packet that lands in $E020 with the length in $E01E. Header values $70-$7F are a third case - the payload is accumulated in the scratch buffer at $EB03 and a zero-length $7x packet executes it with JSR $EB03. $EB03 contains only an RTS in the shipped image, so nothing uses this remote-code path unless the peer sends one.
- [game_ovl_E000_T34_E000] The $E000 modules of tracks 34 and 35 are NOT two builds of the same communications code. T35 contains no CIA references at all (grep for CIA2_ in ovl_E000_T35.s returns zero hits) and instead touches nextGameRandom, the map at $F6xx and work RAM at $89xx, while several of its jump-table entries point at bare SEC/CLC/RTS stubs. T34 is the real modem module; T35 looks like the solo-trainer / computer-opponent module that synthesises the opponent's packets locally behind the same seven-entry interface. That fits main's behaviour (build 1 skips all modem prompts, makes PRACTICE the default menu item and is selected by $0BA5/$E030 bit 7) and corrects the earlier guess that build 1 is a null-modem variant.
- [game_ovl_E000_T34_E000] The five 'BRK' instructions the earlier survey noticed in this module ($E01D, $E039, $E07D, $E414, $E5BB, plus $EAB7) are not BRKs at all. Each is the first byte of an inline variable block that follows a 'LDX #n / JSR $E067' pair; clearInlineVarBlock pulls the return address off the stack, uses it as the base of a self-modified STA abs,X to zero the n bytes after the JSR, and its RTS therefore returns to the caller's caller. Nothing ever executes those bytes.
- [game_ovl_E000_T34_E000] $E039/$E03A are a two-way handshake, not a single flag: the game writes $C0 to $E039 and spins until $E03A goes negative before any disk access (suspendCommModule $0F5F), and writes 0 and spins until $E03A goes positive afterwards. The comm module only acts on the request inside its own frame service, so the NMI is never torn down mid-character.
- [game_ovl_E000_T34_E000] $E07D/$E07E is a 16-bit count of completed packet exchanges maintained at $E283 that no code in the entire image ever reads - almost certainly a leftover debug statistic.
- [game_ovl_E000_T34_E7CA] Packet protocol (build 2 only, $E7F7-$EAA0). On top of the raw CIA2 serial link the T34 comm module runs a small ARQ protocol. A packet is [length code][control byte][payload 0..15][checksum]. The length code is one of 16 values from packetLengthCodeTable ($EAA1) whose bit pairs are all 01 or 10, so it can never be mistaken for the raw sync bytes $00/$FF. The control byte carries the original game command in bits 4-7 and a 2-bit sequence number in bits 2-3; bits 0-3 are refilled with the payload length on delivery, restoring the game's command byte exactly. The checksum ($E937) seeds A with $EE and does ASL / ADC #0 / ADC byte / ADC #0 over every packet byte. Raw $FF means 'acknowledge me', raw $00 means 'resend', control byte $EC is a bare ACK, control bytes $E0-$EF are ACKs generally and >= $F0 is a drive pass-through.
- [game_ovl_E000_T34_E7CA] Link state machine. sub_E756 (called every tick from the $E000 jump-table entry with X=0) drives commState/$E040: 0 = idle, 1 = terminal/modem-command mode (characters flow between the game and the modem through the two small character rings at $E086 and $E091, each converted with EOR #$80), 2 = byte sync (both ends spam $00/$FF until ten rounds have passed or the peer's $FF is seen), 3 = packet protocol. Sixteen consecutive service ticks without progress ($EB00) or two consecutive received $00 bytes force a return to state 2.
- [game_ovl_E000_T34_E7CA] Modem command strings are stored BACK TO FRONT. sub_E393 copies a NUL-terminated string from $EBxx into the queue at $E092 and sets the read index to the string length; the reader sub_E0C3 walks the index downwards, so the bytes come out in reverse. Characters with bit 7 set are not text but guard-time delays loaded into $EB01 ($A0 = 96 ticks, $C0 = 64). $EBD6 therefore emits ' +++ ' with a one-second guard on each side followed by 'ATH0' (hang up), $EBE3 emits 'ATQ0V1X1A' (answer) and $EBF0 'ATQ0V1X1D' (dial); $E011 (0 = answer after key 'A', 1 = originate after key 'O') picks between the last two through the pointer bytes at $EBFE.
- [game_ovl_E000_T34_E7CA] Downloadable code channel. In build 2 only, any received message whose command byte is >= $70 has its payload appended to $EB03 (self-modified index at $E1AE) and, when such a message arrives with an empty payload, $EB03 is called. The shipped image has RTS there, so the peer can effectively upload and run 6502 code on the other machine. Build 1 (T35) has no such call; instead $EB67 there is ordinary live game code.
- [game_ovl_E000_T34_E7CA] CORRECTED 2026-08-22: the packet-building path at $E8A8 IS reachable. The original claim here was that $E0A6 is only ever written with 0 (at $E778); that came from a byte-level scan which missed the INDEXED store at $E265, 'sta uartPendingCount,x' with X = inFlightSlotIndex $E0A8 = 1, whose effective address is $E0A6. sendPacketFsm sets it there and reads it back at $E1EC and $E26E, and the listing's own comments at $E262/$E26B already said the in-flight counter lives at $E0A6. Consequence: $E0A5 and $E0A6 must stay adjacent in that order, and the message-builder branch at $E8A8 is live. The rest of the original observation still holds: $EAC1 (the raw-$FF sender at $E872) really is never written - no direct store and no indexed store reaches it - and outgoing game traffic does go through the engine at $E1DD, which sub_E3F3 re-points at the packet rings $E447/$E45C by rewriting the indirect-JMP operands at $E3B7/$E3BA/$E3BD.
- [game_ovl_E000_T34_E7CA] Buffer map of the comm module: $E086 (8) game-visible receive chars, $E091 (16) chars to send to the modem, $E417 (22) UART transmit ring, $E430 (20) UART receive ring, $E447 (18) game -> packet layer messages, $E45C (18) packet layer -> game messages, $EAC7 (35) retransmit ring. All of them are descending circular buffers (index decremented, wrapping to capacity-1) with a separate count byte.
- [game_ovl_E000_T34_E7CA] $EC00-$EFFF in this file is the 'comm tail' loaded from track 34 sectors 12-15 at start-up by $1070/loadCommTailBuild2, and it is not really comm code at all - it holds map and unit helpers used by overlay A: clearBattlefieldMap ($EC00), setViewOriginToHome ($EC1C), loadMapSeedIntoScenarioRng ($EC2B), makeMapPointSymmetric/rotateMapAndMirrorCodes ($EC54/$EC58), mirrorAllUnitCoordinates ($EC91), mirrorTerrainCode ($ECD4), loadUnitStartTemplate ($ECF3), the link handshake ($ED43), the recycler finaliser ($EE26), the film-snapshot loader ($EE4A) and the trainer display toggle ($EEB9), plus a block of message text at $EF08-$EFF4.
- [game_ovl_E000_T34_E7CA] Handshake and player names. $ED43 exchanges the ten-byte owner name that lives at $FFDE (read from the disk BAM) in two five-byte messages and stores the peer's name at $FFE8; a blank peer name is replaced with 'ENEMY' ($CC99) and, in solo trainer mode ($0BA5 bit 7), the whole exchange is skipped and 'TRAINER   ' ($884E) is used. A four-byte settings message then swaps the comm build id ($E030), the first byte of the settings block ($0B94) and $0B7E. Any length or magic mismatch beeps, waits a second and drops back to the main menu ($EDFC).
- [game_ovl_E000_T34_E7CA] Map mirroring. The map cell alphabet has direction-dependent codes; mirrorTerrainCode ($ECD4) pairs them (A<->D, E<->H, I<->K, M<->T, N<->Q, O<->S, P<->R, U<->X, V<->W) so the battlefield can be rotated 180 degrees. rotateMapAndMirrorCodes and mirrorAllUnitCoordinates together let both players see themselves at the bottom of the screen; the same table also drives the map generator's symmetry pass ($EC54) so the two halves of a generated map are exactly fair.
- [game_ovl_E000_T34_E7CA] The trainer-only F1 toggle: $92FC bit 7 = 'beginner battle display' (all enemy units drawn, tested by renderMapTile at $284F). $EEB9 is polled from the main idle loop at $600A but only when $0BA5 bit 7 is set, fires when zp_66 (currentKey) is 0, announces the mode being entered ('BEGINNER/STANDARD DISPLAY SET.') and rewrites the prompt at $EF10 so it always advertises the other mode ('F 1 FOR xxxxxxxx BATTLE DISPLAY.').

## game/scratchAndMessages0400

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 0461 | verifyProgramChecksum | Boot-time integrity check, entered by the boot loader's obfuscated RTS (the stack was rigged to hold $0460). Saves randomSeed ($09C3) in zp_19 and zeroes it so the sum is seed independent, then runs a self-modifying ADC/EOR/ROL checksum over $0402-$87FF (132 pages; the operand high bytes at $047C and $0484 are incremented per page and end at $88), adds the carry counter zp_1B, stores the sum at $8800, stores sum EOR $0400 ($B0) in programChecksum ($0B7E), restores randomSeed and does JMP $0800 with Y=2. The ADC carry is deliberately chained from one word to the next (the CLC at $0479 is outside the loop) and the PHP/ASL/BCC/ORA/PLP block is a ROL that preserves that carry. | memory $0402-$87FF, $09C3 (random seed, zeroed during the sum), $0400 = $B0 | $8800 = raw sum ($69 on this disk image), $0B7E = sum EOR $B0 ($D9), $09C3 restored, zp_19/zp_1A/zp_1B clobbered, $047C/$0484 left at $88, JMP $0800 with Y=2 and S=$FE | high |
| 06BA | switchToRequestedScreen | Called from every screen's main loop to honour an F1/F3/F5/F7 screen change. Returns immediately when the chat editor is active (D_929C != 0), when zp_66 has bit 7 set (no request) or when the request equals the current screen. Otherwise it makes the request current, resets the stack to $FD (so it never returns to the caller), turns all sprites off (VIC_SPR_ENA and the shadows $9020/$9022/$9023/$9024), clears $92F9 unless $0B9B bit 7 is set, redraws the bitmap frame and jumps to the new screen's entry point. | zp_66 (requested screen 0-3, $FF = none), D_90FB (current screen), D_929C (chat state), D_0B9B bit 7 | D_90FB updated, sprites disabled, S=$FD, never returns: JMPs to enterMapScreen (0, also sets $9020=$C4), runMissileScreen (1), enterDroneScreen (2) or enterMainMenu (3) | high |
| 0709 | runComcenStunnedBlackout | The 'COMCEN STUNNED!' cut-scene, entered by JMP from $4546 when the command centre status byte D_922A has reached $FF and the game is not in solo/film mode. It releases drone control, wipes the message line and screen, resets the stack, then runs the engine unattended (updateGameFrame) with input locked out and D_922A bit 6 set (which keeps sprites off) until unitOrderIndex ($9162) comes back to the value it had on entry, i.e. exactly one full 100-unit update round. It then queues message $0C ('COMCEN STUNNED!'), clears the lockout, masks D_922A with $8F and re-enters whichever screen was active. | D_9162 (captured into the CMP operand at $073E), D_922A, D_90FB (current screen), D_92C9 and D_0BA8 (abort conditions), D_90FC (per-frame gate) | one blacked-out engine round; inputLockoutTimer set then cleared; D_922A bits 4-6 cleared; message $0C queued via sub_C566; never returns: jumps to runMapMainLoop (after drawOverviewMap/drawViewport), missileScreenResume, resetDroneScreen or enterMainMenuKeepSelection | medium |

Data:

- `$0400` **checksumXorConstant** (byteTable, 1 bytes): $B0. The constant verifyProgramChecksum ($0461) EORs into the final sum before storing it in programChecksum $0B7E. It sits one byte below the start of the checksummed range ($0402), so it can never influence itself.
- `$0400` **checksumXorConstant** (byteTable, 1 bytes): $B0 - the constant that verifyProgramChecksum XORs into the final sum before storing it at $0B7E. Excluded from the checksummed range (which starts at $0402).
- `$0401` **mapNameSeedTable** (byteTable, 96 bytes): 32 entries of 3 bytes for the manual look-up protection at $09A6: randomSeed ($09C3) AND $1F picks an entry, the triple is copied to $0B95-$0B97 and used as the 24-bit map seed, the generated map is shown with 'IDENTIFY THIS MAP (SEE MANUAL)' ($0A48) and the five-character name the player types is packed through mapNameAlphabet ($890C) into zp_5A-zp_5C and compared with the same triple. Three failures wipe memory ($0A1E). Note $0401 (entry 0's first byte) is the only table byte outside the checksummed range.
- `$0401` **mapNameSeedTable** (byteTable, 96 bytes): 32 x 3 bytes used by the manual look-up protection at $09A6: the random seed ($09C3, the immediate operand of the LDA at $09C2) AND $1F picks an entry, the triple is copied to $0B95-$0B97, generateBattlefieldMap draws that map and the player must type its name from the manual; the typed name is packed into zp_5A-zp_5C and compared with the same three bytes. So the triple is both the map generator seed and the expected answer. Also the first byte checksummed is $0402, so entry 0's first byte is outside the sum.
- `$04AE` **scratchPageTail** (unknown, 82 bytes): $00 on disk. At run time this is shared scratch: the 2x2 tile buffer $04C8-$04E7 filled by the terrain drawing code at $279F/$27CD, statistics accumulators, and the map generator's blob stacks. The trainer's playbook load (track 29 s6-8 -> $0200-$04FF) also overwrites it.
- `$04AE` **scratchPageTail** (unknown, 82 bytes): $00 filler on disk; at run time part of the shared $0400 scratch page (tile buffer at $04C8-$04E7, statistics accumulators, blob stacks).
- `$0500` **messagePtrLoTable** (byteTable, 36 bytes): Low bytes of the 36 status-message pointers, indexed by message id $00-$23; read by the message system at $C536/$C5E5 and patched at run time by setMessageSlot ($C924). Ids $1E, $20 and $21 are $FFFF placeholders on disk: $20/$21 are filled with $8800/$881C by mainProgram $0B1D/$0B26, with $895E/$8837 by showModemSetupPrompt ($0C8C), and with the modem driver's own $EFxx strings by $EED6/$EEFF.
- `$0500` **messagePtrLoTable** (byteTable, 36 bytes): Low bytes of the 36 status-message string pointers, indexed by message id $00-$23; read at $C536/$C5E5 and patched at run time by sub_C924 (ids $1E, $20, $21 are $FFFF placeholders on disk).
- `$0524` **messagePtrHiTable** (byteTable, 36 bytes): High bytes of the same 36 pointers. $05/$06 = strings in this block, $87 = strings in the $6F00 overlay variant B, $88 = strings in the loaded $8800 message block, $FF = unset placeholder.
- `$0524` **messagePtrHiTable** (byteTable, 36 bytes): High bytes of messagePtrLoTable. Ids resolve to strings in this file ($0548-$0615), in overlay B ($8707-$87F5) and in the loaded message block ($8858-$88FF).
- `$0548` **chatMessageLine** (text, 36 bytes): Message id $00, the chat / typed line. $0548 is a leading marker glyph ($1E = cursor, or $9E written by sub_C62A to terminate at once = blank line) and $0549-$056B hold up to 35 characters mirrored from $9253 with bit 7 set on the last. On disk it is 35 spaces followed by $A0.
- `$0548` **chatMessageLine** (text, 36 bytes): Message id $00: the chat/typed line. $0548 is a leading marker glyph ($1E cursor; $9E by sub_C62A to terminate immediately = blank line), $0549-$056B hold up to 35 characters mirrored from $9253 with bit 7 set on the last one. On disk it is 35 spaces plus $A0.
- `$056C` **msgWorking** (text, 10 bytes): Message id $01 "WORKING..." (bit-7 terminated), sound $80 = silent.
- `$056C` **msgWorking** (text, 10 bytes): Message id $01 'WORKING...' (bit-7 terminated), sound $80 = silent.
- `$0576` **msgGameAborted** (text, 13 bytes): Message id $03 "GAME ABORTED.", sound $03.
- `$0576` **msgGameAborted** (text, 13 bytes): Message id $03 'GAME ABORTED.', sound $03.
- `$0583` **msgNotAGameFilm** (text, 16 bytes): Message id $09 "NOT A GAME FILM!", sound $03 - a film load was rejected.
- `$0583` **msgNotAGameFilm** (text, 16 bytes): Message id $09 'NOT A GAME FILM!', sound $03 (film load rejected).
- `$0593` **msgSetupUnitsPressF1WhenDone** (text, 33 bytes): Message id $0A "SETUP UNITS. PRESS F 1 WHEN DONE.", silent - the unit setup phase prompt.
- `$0593` **msgSetupUnitsPressF1WhenDone** (text, 33 bytes): Message id $0A 'SETUP UNITS. PRESS F 1 WHEN DONE.', silent.
- `$05B4` **msgTimeoutRunStopToResume** (text, 28 bytes): Message id $19 "TIMEOUT. RUN/STOP TO RESUME.", sound $19.
- `$05B4` **msgTimeoutRunStopToResume** (text, 28 bytes): Message id $19 'TIMEOUT. RUN/STOP TO RESUME.', sound $19.
- `$05D0` **msgResumed** (text, 7 bytes): Message id $1B "RESUMED", sound $19.
- `$05D0` **msgResumed** (text, 7 bytes): Message id $1B 'RESUMED', sound $19.
- `$05D7` **msgTheBattleBegins** (text, 18 bytes): Message id $1C "THE BATTLE BEGINS!", sound $03.
- `$05D7` **msgTheBattleBegins** (text, 18 bytes): Message id $1C 'THE BATTLE BEGINS!', sound $03.
- `$05E9` **msgLastQtrWarning** (text, 17 bytes): Message id $1D "LAST QTR WARNING!", sound $19 - the game is played in quarters.
- `$05E9` **msgLastQtrWarning** (text, 17 bytes): Message id $1D 'LAST QTR WARNING!', sound $19 (the game is played in quarters).
- `$05FA` **msgPhoneTrouble** (text, 14 bytes): Message id $22 "PHONE TROUBLE.", sound $15 - modem link failure.
- `$05FA` **msgPhoneTrouble** (text, 14 bytes): Message id $22 'PHONE TROUBLE.', sound $15 (modem link failure).
- `$0608` **msgTryAgain** (text, 10 bytes): Message id $23 "TRY AGAIN.", sound $03.
- `$0608` **msgTryAgain** (text, 10 bytes): Message id $23 'TRY AGAIN.', sound $03.
- `$0612` **messageSoundTable** (byteTable, 36 bytes): Sound effect number played when message id X is shown (read at $C557, played through sub_C89A); $80 = silent. $0E for the two 'we lost' messages, $0D for the two 'we won' messages, $19 for GAME OVER / TIMEOUT / RESUMED / LAST QTR WARNING, $15 for PHONE TROUBLE, $06 for the two phone prompts.
- `$0612` **messageSoundTable** (byteTable, 36 bytes): Sound effect number played when message id X is displayed (read at $C557, played through sub_C89A); $80 = no sound. Notable: $0E for the two 'we lost' messages, $0D for the two 'we won' messages, $19 for GAME OVER / TIMEOUT / RESUMED / LAST QTR WARNING, $15 for PHONE TROUBLE.
- `$0636` **optionSummaryTemplate** (text, 23 bytes): " MENUS XXXX DAMAGE XXX." (bit-7 terminated) - a rules summary line whose XXXX and XXX fields are overwritten in place by the $6F00 overlays ($870C, $8739-$8751) with OFF,/ON, and FULL/HALF/NONE before it is printed.
- `$0636` **optionSummaryTemplate** (text, 23 bytes): ' MENUS XXXX DAMAGE XXX.' (bit-7 terminated) - a rules summary line whose XXXX/XXX fields are overwritten in place by overlay A/B ($870C, $8739-$8751) with OFF,/ON, and FULL/HALF/NONE before it is displayed.
- `$064D` **optionWordTable** (text, 17 bytes): Five bit-7 terminated words at fixed offsets, copied into optionSummaryTemplate: +0 "OFF", +3 "ON", +5 "FULL", +9 "HALF", +13 "NONE". XREF.txt calls this label eaCopyrightMessage, which is wrong (that name belongs to the boot-time `ea` file that once occupied $02A8-$030B).
- `$064D` **optionWordTable** (text, 17 bytes): Five bit-7 terminated option words at fixed offsets used to fill optionSummaryTemplate: +0 'OFF', +3 'ON', +5 'FULL', +9 'HALF', +13 'NONE'. XREF.txt calls this label eaCopyrightMessage, which is wrong.
- `$065E` **msgClicks** (text, 10 bytes): "   CLICKS:" - the score header printed by $C6BC (LDA #$5E / LDY #$06 / JSR $C133). "Clicks" are Modem Wars' score unit.
- `$065E` **msgClicks** (text, 10 bytes): '   CLICKS:' - the score header printed by $C6BC (lda #$5E / ldy #$06 / jsr sub_C133). 'Clicks' are Modem Wars' score units.
- `$0668` **msgUs** (text, 7 bytes): "    US:" - printed straight after msgClicks, followed by the local player's 16-bit score from $92A2/$92A4.
- `$0668` **msgUs** (text, 7 bytes): '    US:' - printed straight after msgClicks by sub_C137, followed by the local player's 16-bit score from $92A2/$92A4.
- `$066F` **msgThem** (text, 8 bytes): "   THEM:" - the opponent's score label, printed by the same $C6B6 routine.
- `$066F` **msgThem** (text, 8 bytes): '   THEM:' - the opponent's score label, printed by the same $C6B6 routine.
- `$0677` **msgTypeMapName** (text, 40 bytes): Three lines separated by $0D: "TYPE MAP NAME" / " (F7 RANDOM)" / " (F5 SYM/RND)" - the prompt of the map name editor in overlay A, also used by the manual look-up protection at $09EF.
- `$0677` **msgTypeMapName** (text, 40 bytes): Three lines separated by $0D: 'TYPE MAP NAME' / ' (F7 RANDOM)' / ' (F5 SYM/RND)' - the prompt of enterMapName (also used by the manual look-up protection at $09EF).
- `$069F` **msgBtnToAccept** (text, 27 bytes): Two lines separated by $0D: "BTN TO ACCEPT" / "(F 1 TO FLIP)" - shown while the generated map is offered for acceptance.
- `$069F` **msgBtnToAccept** (text, 27 bytes): Two lines separated by $0D: 'BTN TO ACCEPT' / '(F 1 TO FLIP)' - shown while the generated map is offered for acceptance.
- `$0770` **tileVariantMaskTable** (byteTable, 40 bytes): Indexed by map cell value minus $40 (cells below $40 use cell-$20), covering cells $40-$67. The value is ANDed with a scenario random number and added to the base tile by randomTileVariant ($7872, overlay A): cells $49, $4B and $65 -> mask 1; cells $4D, $51, $59, $5D, $61 -> mask 3; everything else 0.
- `$0770` **tileVariantMaskTable** (byteTable, 40 bytes): Indexed by map cell value minus $40 (cells below $40 use cell-$20), covering cells $40-$67. The value is a mask ANDed with a scenario random number and added to the base tile by randomTileVariant ($7872, overlay A): cells $49 and $4B and $65 -> mask 1, cells $4D, $51, $59, $5D, $61 -> mask 3, everything else 0 (no variation).
- `$0798` **tileVariantPad** (unknown, 1 bytes): $00 pad between tileVariantMaskTable and unitGlyphBaseTable.
- `$0798` **tileVariantPad** (unknown, 1 bytes): $00 pad between tileVariantMaskTable and unitGlyphBaseTable.
- `$0799` **unitGlyphBaseTable** (byteTable, 6 bytes): Base glyph index of the one-character overview symbol per unit type: $2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN (plus one $00 pad). The facing (unitFlags AND 3) is added; the result indexes the 8x8 shapes at $9C6E + code*8, and, doubled after subtracting $2C, the 16x16 glyph pointer tables ($28A0, $6C35, $7512, $7538).
- `$0799` **unitGlyphBaseTable** (byteTable, 5 bytes): Base glyph index of the 1-character overview-map symbol per unit type: $2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN. The facing (unit flags AND 3) is added, then $2C is subtracted and the result doubled to index the glyph pairs ($28A0, $7512, $7538).
- `$079F` **terrainClassTable** (byteTable, 75 bytes): Map cell value -> terrain class 0-8. Addressed as $077E,Y with the raw cell value, so the body runs from $077E+$21 = $079F to $077E+$6B = $07E9: cells $21-$34 -> 3 (lower contour), $35-$3F -> $FF (invalid), $40 -> 0 (blank), $41-$54 -> 5 (upper contour), $55-$5C -> 1 (river), $5D-$60 -> 2 (forest), $61-$64 -> 4 (lower hill), $65-$66 -> 6 (summit), $67 -> 7 (recycler), $68-$6B -> 8 (end zone). The same bytes are read as $07BE,X by $5BC8 with the index already reduced to cell-$40. The trainer also reads it as terrainClassTable $077E from $E773.
- `$079F` **terrainClassTable** (byteTable, 75 bytes): Map cell value -> terrain class 0-8. It is addressed as D_077E,y with the raw cell value, so the body starts at $077E+$21 = $079F and ends at $077E+$6B = $07E9: cells $21-$34 -> 3 (lower contour), $35-$3F -> $FF (invalid), $40 -> 0 (blank), $41-$54 -> 5 (upper contour), $55-$5C -> 1 (river), $5D-$60 -> 2 (forest), $61-$64 -> 4 (lower hill), $65-$66 -> 6 (summit), $67 -> 7 (recycler), $68-$6B -> 8. The same bytes are also read as D_07BE,x by $5BC8 with the index already reduced to cell-$40 (clamped to < $21).
- `$07EA` **terrainColourTable** (byteTable, 9 bytes): Colour-RAM nibble per terrain class 0-8: 7,3,5,8,8,8,8,0,0 (yellow plain, cyan river, green forest, orange for every contour/hill/summit class, black for classes 7-8). Read at $27AB and $5BCB immediately after a terrainClassTable lookup.
- `$07EA` **terrainColourTable** (byteTable, 9 bytes): Colour-RAM nibble per terrain class 0-8: 7,3,5,8,8,8,8,0,0 (yellow plain, cyan river, green forest, orange contours/hills/summit, black for classes 7-8). Read at $27AB and $5BCB right after a terrainClassTable lookup.
- `$07F3` **table0400Tail** (unknown, 13 bytes): $00 filler to the end of the page; unreferenced.
- `$07F3` **table0400Tail** (unknown, 13 bytes): $00 filler to the end of the page.

Insights:

- [data_table_0400] $0548 chat line placeholder (35 spaces)
- [data_table_0400] $056C "WORKING..."
- [data_table_0400] $0576 "GAME ABORTED."
- [data_table_0400] $0583 "NOT A GAME FILM!"
- [data_table_0400] $0593 "SETUP UNITS. PRESS F 1 WHEN DONE."
- [data_table_0400] $05B4 "TIMEOUT. RUN/STOP TO RESUME."
- [data_table_0400] $05D0 "RESUMED"
- [data_table_0400] $05D7 "THE BATTLE BEGINS!"
- [data_table_0400] $05E9 "LAST QTR WARNING!"
- [data_table_0400] $05FA "PHONE TROUBLE."
- [data_table_0400] $0608 "TRY AGAIN."
- [data_table_0400] $0636 " MENUS XXXX DAMAGE XXX." (template, fields overwritten in place)
- [data_table_0400] $064D "OFF" / "ON" / "FULL" / "HALF" / "NONE" (five words at +0/+3/+5/+9/+13)
- [data_table_0400] $065E "   CLICKS:"
- [data_table_0400] $0668 "    US:"
- [data_table_0400] $066F "   THEM:"
- [data_table_0400] $0677 "TYPE MAP NAME" CR " (F7 RANDOM)" CR " (F5 SYM/RND)"
- [data_table_0400] $069F "BTN TO ACCEPT" CR "(F 1 TO FLIP)"
- [data_table_0400] Complete message id map, decoded from messagePtrLoTable/HiTable + messageSoundTable: $00 chat line ($0548, silent), $01 WORKING... ($056C, silent), $02 END OF FILM. ($8707, snd 3), $03 GAME ABORTED. ($0576, 3), $04 OPPONENT READY. ($8858, 3), $05 UNIT RECYCLED. ($8713, 4), $06/$07 SHOW ****/SPEED * ($8721/$872A, 3), $08 SPOTTED RECYCLER! ($8731, 4), $09 NOT A GAME FILM! ($0583, 3), $0A SETUP UNITS. PRESS F 1 WHEN DONE. ($0593, silent), $0B WAITING FOR OPPONENT... ($8867, 3), $0C COMCEN STUNNED! ($8742, 4), $0D SPOTTED ENEMY COMCEN! ($8751, 4), $0E SPY UNDER ATTACK! ($8766, 4), $0F SPOTTED ENEMY SPY! ($8777, 4), $10 WAITING FOR CONNECTION... ($887E, silent), $11 WE LOST ON POINTS! ($8789, $0E), $12 WE WON ON POINTS! ($879B, $0D), $13 IT WAS A DRAW. ($87AC, silent), $14 WE WERE KNOCKED OUT! ($87BA, $0E), $15 WE WON BY A KNOCKOUT! ($87CE, $0D), $16 GAME OVER. ($87E3, $19), $17 PICK UP PHONE THEN PRESS SPACE. ($8897, 6), $18 PRESS SPACE, WAIT, HANGUP PHONE. ($88B6, 6), $19 TIMEOUT. RUN/STOP TO RESUME. ($05B4, $19), $1A OPPONENT PICKING GAME/MAP. ($88D6, 3), $1B RESUMED ($05D0, $19), $1C THE BATTLE BEGINS! ($05D7, 3), $1D LAST QTR WARNING! ($05E9, $19), $1E unset ($FFFF, 3), $1F OPPONENT READY FOR NEW GAME. ($88F0, silent), $20/$21 unset ($FFFF) and patched at run time, $22 PHONE TROUBLE. ($05FA, $15), $23 TRY AGAIN. ($0608, 3). The $87xx strings live in the $6F00 overlay variant B.
- [data_table_0400] The terrain data model is entirely in this block: terrainClassTable maps the raw cell code to one of nine classes, terrainColourTable gives the colour-RAM nibble per class, tileVariantMaskTable gives the random variation mask per base tile, and unitGlyphBaseTable maps the five unit types to their symbol. Every cell code $21-$34 has the same class as $41-$54 shifted by $20, which is the contour offset the map generator uses ($0324 contourTileOffset).
- [data_table_0400] Only two of the nine terrain classes get a distinct colour: everything from class 3 to class 6 (contours, hills, summit) is drawn orange ($08), so the elevation rings differ only in shape.
- [data_table_0400] The label eaCopyrightMessage that XREF.txt attaches to $064D is stale - the `ea` boot file lived at $02A8-$030B, not here. $064D is optionWordTable (OFF/ON/FULL/HALF/NONE).
- [data_table_0400] D_0410/D_0420/D_0430/D_0432/D_0450/D_0464 inside mapNameSeedTable and D_04C8/D_04C9/D_04D0/D_04D1/D_04DF/D_04E7 inside scratchPageTail are run-time scratch addresses (unit scan lists, group scratch, the 2x2 tile buffer), not fields of the tables the disk holds there.
- [data_table_0400] $0400-$04FF is not stable data at all: only $0500-$07FF survives a game session. The page is successively the protection table + checksum routine, the BAM buffer for checkDiskId, the 512-byte RLE sprite buffer, overlay A's map-generator variables, the film directory, and the trainer's playbook.
- [game_table_0400_0400] The $0400 page has three lives. On disk (track 27 s13-16, copied from $6F00-$72FF by the boot loader) it holds the manual look-up protection table at $0401 and the checksum routine at $0461-$04AD. During the game the whole page is a shared scratch area (unit scan lists at $0400/$0432/$0464, group scratch $0410/$0420/$0430, 2x2 tile buffer $04C8-$04E7, blob stacks of the map generator, 16-bit statistics accumulators whose low halves are at $88D2-$8901). The default setup block (track 29 s6-8) is loaded to $0200-$04FF and overwrites the whole first half of it. Only $0500-$07FF is stable data.
- [game_table_0400_0400] verifyProgramChecksum ($0461) sums $0402-$87FF - i.e. everything from the protection table up to and including the loaded overlay ($6F00-$87FF is overlay B at that moment) - with a carry-chained ADC + EOR + ROL word loop, then stores sum EOR $B0 in $0B7E. Verified against the real machine: the VICE snapshot has $8800=$69 and $0B7E=$D9. The disk image's own mastered byte at $0B7E is $DB and overlay A compares the value received from the opponent against the immediate $DB at $79CC, so on THIS d64 the check would fail - strong evidence that the image has been patched by a couple of bits somewhere in $0402-$87FF (a crack or a version difference). Confidence medium on the interpretation, high on the arithmetic.
- [game_table_0400_0400] The checksum is a link-level anti-tamper measure, not just a boot check: comm build 2 puts $0B94 (settings/film version) in $E029 and $0B7E in $E02A, sends packet type 4 and computes $90F8 = received $E023 EOR $E022; overlay A preloads $90F8 with $DB and, if it differs after the handshake, prints the string at $895E 'OPPONENT USING NON-STANDARD GAME.'
- [game_table_0400_0400] Complete status-message system: 36 ids ($00-$23) with the address split over messagePtrLoTable ($0500) / messagePtrHiTable ($0524) and a parallel sound id in messageSoundTable ($0612, $80 = silent). $C536 prints the string through sub_C3D8 and sets zp_72=$3C (60 frames); sub_C566 queues an id into the 8-entry priority ring at $9249; sub_C560 is the same but suppressed while $0B9B bit 7 (solo/film) is set. The texts are: 00 chat line, 01 WORKING..., 02 END OF FILM., 03 GAME ABORTED., 04 OPPONENT READY., 05 UNIT RECYCLED., 06/07/08 SPOTTED RECYCLER variants, 09 NOT A GAME FILM!, 0A SETUP UNITS. PRESS F 1 WHEN DONE., 0B WAITING FOR OPPONENT..., 0C COMCEN STUNNED!, 0D SPOTTED ENEMY COMCEN!, 0E SPY UNDER ATTACK!, 0F SPOTTED ENEMY SPY!, 10 WAITING FOR CONNECTION..., 11 WE LOST ON POINTS!, 12 WE WON ON POINTS!, 13 IT WAS A DRAW., 14 WE WERE KNOCKED OUT!, 15 WE WON BY A KNOCKOUT!, 16 GAME OVER., 17 PICK UP PHONE THEN PRESS SPACE., 18 PRESS SPACE, WAIT, HANGUP PHONE., 19 TIMEOUT. RUN/STOP TO RESUME., 1A OPPONENT PICKING GAME/MAP., 1B RESUMED, 1C THE BATTLE BEGINS!, 1D LAST QTR WARNING!, 1E/20/21 run-time slots patched by sub_C924, 1F OPPONENT READY FOR NEW GAME., 22 PHONE TROUBLE., 23 TRY AGAIN.
- [game_table_0400_0400] Message id 0 is the chat line: $0548 is a marker glyph ($1E while typing, $9E to blank the line) and $0549.. is a bit-7 terminated mirror of the raw chat buffer at $9253, rebuilt character by character at $13EF. That is why the 'blank' string at $0548 is exactly 36 bytes - the width of the message panel.
- [game_table_0400_0400] Screen dispatch: zp_66 holds the last key ($FF = none, 0-3 from F1/F3/F5/F7) and switchToRequestedScreen ($06BA), called from all four screen loops ($1463 console, $2033 map, $75C3 missile, $8040 drone), resets the stack and jumps to enterMapScreen / runMissileScreen / enterDroneScreen / enterMainMenu. It refuses to switch while the chat editor is active, so screens can never be changed mid-message.
- [game_table_0400_0400] The terrain tables at $0770-$07F2 are addressed through three different biased bases and must not be treated as one table per label: tileVariantMaskTable at $0770 is indexed by cell-$40, terrainClassTable is indexed by the raw cell value from base $077E (body $079F-$07E9) and also read as $07BE,x with the index pre-reduced, and terrainColourTable at $07EA is indexed by the resulting class 0-8. Earlier surveys extended terrainClassTable past cell $6B and so read the colour table as if it were class data (that is where the bogus '$6C -> 7, $6D -> 3, $6E -> 5, $6F-$72 -> 8' entries come from).
- [game_table_0400_0400] The manual look-up protection ($09A6) does not store a separate 'answer': the 3-byte entry picked from $0401 is copied to $0B95-$0B97, used as the seed for generateBattlefieldMap, and then compared with the packed name the player types (zp_5A-zp_5C). The entry is selected by 'LDA #seed / AND #$1F' where the immediate operand at $09C3 is the SID noise byte the boot loader poked in - so the byte the checksum has to neutralise is literally an instruction operand.
- [game_table_0400_0400] runComcenStunnedBlackout ($0709) is the visible consequence of a COMCEN hit: control is taken away, the screen is cleared and the engine is stepped for exactly one full 100-unit round with sprites suppressed (D_922A bit 6) and input locked, then 'COMCEN STUNNED!' is queued and the previous screen is restored. It is skipped in solo/film mode ($0B9B bit 7).

## game/textEngineC000

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| C01A | printChar | Central character output of the game. Saves A/X/Y (saveRegsAXY), and unless rawGlyphMode (zp_43 bit 7) is set masks bit 7 off, sends codes < $20 to handleControlChar and translates codes $20-$5F through asciiToGlyphTable ($987E) into a glyph index; then plots the glyph and advances the cursor. Registers are restored before it returns, so callers can print inside loops. | A = character (ASCII $20-$5F, control $08/$0D, or a raw glyph index when zp_43 bit 7 is set), text window state zp_39-zp_44, cursor pointers zp_B4-zp_B7 | one glyph drawn in the bitmap plus its colour byte, cursor advanced (zp_3F/zp_40 and zp_B2-zp_B7 updated); A/X/Y preserved via zp_54/zp_55/zp_56 | high |
| C030 | restoreRegsAXY | Loads A, X and Y back from the zp_54/zp_55/zp_56 save slots filled by saveRegsAXY. Tail of printChar and also used directly by the viewport drawing code at $5CA1/$5D11/$5DE7 to recover a cell coordinate triple. | zp_54 (A), zp_55 (X), zp_56 (Y) | A, X, Y restored; N/Z reflect A | high |
| C03D | handleControlChar | Handles the two control codes understood by printChar: $0D = carriage return (printNewline) and $08 = backspace (step back one column, print the blank glyph 0 to erase, step back again). Any other code below $20 is ignored. | A = control code | cursor moved, possibly one cell erased; cursor pointers recomputed | high |
| C041 | printNewline | Carriage return: cursor column zp_3F = 0 and cursor row zp_40 += 1 (+2 when doubleSizeText zp_44 bit 7 is set), then recomputes the cell pointers. The window is never scrolled - the comparison of the new row against the window bottom zp_3A at $C04D-$C051 branches to the next instruction either way (vestigial code). | zp_39-zp_44 window state | zp_3F = 0, zp_40 advanced, zp_B4-zp_B7 recomputed | high |
| C06A | clearTextWindow | Homes the cursor to the top left of the current window (column 0, row zp_39) and falls into clearToWindowBottom, so the whole window is filled with the current blank character zp_3E in the current colour zp_3D. The standard 'clear window' call of the whole game. | window zp_39-zp_3E | window filled with zp_3E, cursor left at the top left | high |
| C075 | clearToWindowBottom | Fills from the current cursor position up to and including the window bottom row zp_3A with the blank character zp_3E, then restores the cursor position it started from. | cursor zp_3F/zp_40, window zp_3A/zp_3C, blank char zp_3E, colour zp_3D | cells filled; cursor unchanged on exit | high |
| C07A | clearToEndOfLine | Same fill loop as clearToWindowBottom but with the stop row set to zp_40 + 1, i.e. it blanks the remainder of the current line only. Used to pad the message line after a status string and by the overlay screens. | cursor zp_3F/zp_40, window width zp_3C, blank char zp_3E | rest of the line filled with zp_3E; cursor restored; self-modifies the compare operand at $C090 | high |
| C097 | setCursorRow | Sets the cursor row zp_40 = A and falls into updateCursorPointers. Convenience entry used all over the game to move to a row without touching the column. | A = row | zp_40 = A, zp_B4-zp_B7 recomputed | high |
| C099 | updateCursorPointers | Recomputes the bitmap and colour pointers for the current cursor cell: absolute column = zp_3F + window left zp_3B, row = zp_40, passed to calcCellPointersXY ($2AA3). When the display is in multicolour mode (D_9001 = $D016 shadow, bit 4 set) it adds $4C to zp_B7, turning the screen-RAM pointer ($8C00) into a colour-RAM pointer ($D800). X and Y are preserved through self-modified operands. | zp_3F, zp_40, zp_3B, D_9001 | zp_B4/zp_B5 = bitmap cell address, zp_B6/zp_B7 = screen or colour RAM address; X/Y preserved | high |
| C0C0 | plotCharAndAdvance | Plots glyph A at the cursor (plotGlyph), advances the column by 1 (by 2 when doubleSizeText zp_44 bit 7 is set), wraps to the next line when the column reaches the window width zp_3C, and recomputes the cell pointers. | A = glyph index, window/cursor state | glyph drawn, cursor advanced, pointers updated | high |
| C0D7 | setFullScreenWindow | Resets the text window to the whole screen: top zp_39 = 0, left zp_3B = 0, bottom zp_3A = $19 (25), width zp_3C = $28 (40). Called on its own by screen builders and as the tail of saveTextWindow. | none | zp_39 = 0, zp_3A = $19, zp_3B = 0, zp_3C = $28 | high |
| C0E8 | saveRegsAXY | Stores A, X and Y in the scratch triple zp_54/zp_55/zp_56 so they can be recovered with restoreRegsAXY. Used at the head of printChar and by the viewport code at $5C81. | A, X, Y | zp_54 = A, zp_55 = X, zp_56 = Y | high |
| C0EF | printFillChars | Prints the current blank character zp_3E A times through plotCharAndAdvance (does nothing for A = 0). Used for padding fields, for the centring pad inside printStringAtPointer and for drawing horizontal bars. | A = repeat count, zp_3E = fill character, zp_3D = colour | A cells filled, cursor advanced | high |
| C100 | printTableEntry | Prints entry X of a table of fixed 8-byte, bit-7-terminated strings whose base address is passed in A (low) / Y (high): computes base + X*8 with a 16-bit shift and tail-calls printString. Used for unit type names, menu item names and the STANDARD/CUSTOM words. | A/Y = table base, X = entry index | string printed; zp_18/zp_19/zp_1A clobbered | high |
| C121 | printStringCentred | Sets the centring field width D_90EF to the current window width zp_3C and tail-calls printString, so the string is padded on the left to appear centred in the window. A is preserved across the D_90EF store. | A/Y = string pointer, zp_3C = window width | D_90EF = zp_3C (cleared again by printStringAtPointer), string printed centred | high |
| C12B | printNextStringCentred | Like printStringCentred but continues with the string that follows the one just printed (pointer zp_A2/zp_A3 already advanced past the terminator), i.e. prints the next line of a multi-string block centred in the window. | zp_A2/zp_A3 = string pointer, zp_3C = window width | D_90EF = zp_3C, next string printed centred | high |
| C133 | printString | Sets the string pointer zp_A2/zp_A3 = A/Y and falls into printStringAtPointer. This is the main string output call of the game (used from the main program, both $6F00 overlay variants and the comm module). | A = pointer low, Y = pointer high; the string ends with a character that has bit 7 set | string printed at the cursor, zp_A2/zp_A3 left just past the terminator | high |
| C137 | printStringAtPointer | Prints the bit-7-terminated string at zp_A2/zp_A3. If the centring width D_90EF is non-zero it first measures the string and prints (D_90EF - length)/2 blank characters in the pad colour zp_42 (zp_3D is saved and restored around the pad), then prints every character through printChar, terminator included, and finally clears D_90EF. Entered directly to print the next string of a block. | zp_A2/zp_A3 = string, D_90EF = field width or 0, zp_42 = pad colour, zp_3E = blank character | text drawn, zp_A2/zp_A3 advanced past the terminator, D_90EF = 0; self-modifies the length operand at $C159 | high |
| C17D | printDecimal | Prints an unsigned 24-bit value as decimal without leading zeros: converts it with convertToDecimalDigits and then prints the digits (printDecimalDigits). | A = value low, Y = value middle, zp_7E = value high byte | digits printed, digit buffer $92DA filled, zp_7E cleared, zp_18-zp_1F clobbered | high |
| C183 | convertToDecimalDigits | Converts the 24-bit value in A (low) / Y (middle) / zp_7E (high) to decimal by repeated 24-bit division by 10 (24-iteration shift/subtract loop). Digits are stored as $B0 + digit (ASCII with bit 7 set) from the end of the buffer at $92DA backwards; zp_1E ends up pointing at the most significant digit and zp_7E is cleared. | A/Y/zp_7E = 24-bit value | $92DA+1..$92DA+8 digits, zp_1E = index of the first digit, zp_7E = 0, zp_18-zp_1F clobbered | high |
| C1E2 | printDecimalDigits | Prints the digit buffer at $92DA from index zp_1E up to index 8 through printChar. Only entered by the JMP at the end of printDecimal, but it is a separate routine body. | zp_1E = first digit index, $92DA buffer | digits printed | high |
| C1F0 | plotGlyph_C1F0 | Draws glyph A into the bitmap cell at zp_B4/zp_B5 and writes the colour byte zp_3D through zp_B6/zp_B7. The glyph source is $98BE + A*8 in multicolour mode (D_9001 bit 4 set) or $9A96 + A*8 in hires mode - two 60-glyph fonts with identical shapes. When doubleSizeText (zp_44 bit 7) is set it hands over to plotGlyphDoubleSize instead. The 8-byte copy loop at $C227-$C231 loads the existing bitmap byte first and the following opcode at $C22B is patched by setBitmapDrawMode ($2A41) to LDA/ORA/EOR/CMP (zp),Y, giving replace/OR/EOR/transparent drawing. | A = glyph index, zp_B4-zp_B7 cell pointers, zp_3D colour, zp_44, D_9001 | 8 bitmap bytes and one colour byte written; zp_B2/zp_B3 = glyph pointer; A preserved | high |
| C24B | plotGlyphDoubleSize | Draws the glyph at zp_B2/zp_B3 magnified 2x2 into the four character cells starting at zp_B4/zp_B5 (top-left cell, +8 = top-right, +$138 = bottom-left, +8 = bottom-right) and colours them with colourDoubleSizeCell. The 8 source bytes are copied to the scratch buffer $90B8 first; each quadrant expands 4 source rows and one nibble of each row. The mask zp_BA suppresses quadrants (bit 0 = top cells, bit 1 = bottom cells, bit 2 = left cells, bit 3 = right cells) so half-size edges can be drawn. A, X and Y are preserved through self-modified operands. Called by magnifyWindow2x ($2E7B/$2EA5/$2EEA/$2F14) and by plotGlyph when zp_44 bit 7 is set. | zp_B2/zp_B3 = glyph, zp_B4-zp_B7 = destination cell, zp_3D = colour, zp_BA = edge mask | up to 4 cells (32 bitmap bytes) and their colour bytes written; $90B8-$90BF, zp_1A-zp_1D clobbered; operands at $C301/$C303/$C305/$C33A/$C33B/$C33D/$C33E self-modified | high |
| C307 | plotGlyphLeftHalf | Quadrant helper of plotGlyphDoubleSize for the left-hand cell: returns immediately when zp_BA bit 2 is set, otherwise selects the high nibble (zp_1D = $F0) and runs expandGlyphNibbles. | zp_BA, zp_1B = source row offset (0 or 4) | 8 bitmap bytes of one cell; zp_1D = $F0 | high |
| C315 | plotGlyphRightHalf | Quadrant helper of plotGlyphDoubleSize for the right-hand cell: returns immediately when zp_BA bit 3 is set, otherwise selects the low nibble (zp_1D = $0F) and falls into expandGlyphNibbles. | zp_BA, zp_1B = source row offset | 8 bitmap bytes of one cell; zp_1D = $0F | high |
| C320 | expandGlyphNibbles | Inner loop of the 2x magnifier: takes 4 glyph rows from the scratch buffer $90B8 starting at offset zp_1B, masks each with zp_1D (shifting the high nibble down), looks the nibble up in pixelDoubleTable ($C23B) to double its pixels horizontally and stores the result to two destination bytes (the self-modified absolute addresses at $C33A and $C33D, one bitmap row apart), doubling it vertically as well. | $90B8 glyph rows, zp_1B = 0 or 4, zp_1D = $F0 or $0F, destination operands at $C33A/$C33D | 8 bitmap bytes written; zp_1A = byte counter | high |
| C34B | colourDoubleSizeCell | Writes the colour byte zp_3D into the colour/screen cells of the 2x2 block drawn by plotGlyphDoubleSize: the first row pair unless zp_BA bit 0 is set, then zp_B6/zp_B7 is advanced by 40 and the second row pair is written unless bit 1 is set. | zp_B6/zp_B7 = cell colour pointer, zp_3D, zp_BA | up to 4 colour bytes written; zp_B6/zp_B7 advanced by one screen row | high |
| C367 | colourCellPair | Writes zp_3D into the two horizontally adjacent colour cells at (zp_B6),0 and (zp_B6),1, skipping the left one when zp_BA bit 2 is set and the right one when bit 3 is set. | zp_B6/zp_B7, zp_3D, zp_BA | up to 2 colour bytes written | high |
| C37F | waitFrames | Busy-waits A frames by polling the raster register: for each frame it waits until VIC_RASTER reads $F0 and then until it changes again. Returns at once for A = 0. Used for the pauses in the boot/menu sequences, sound effects and the modem dialogue. | A = number of frames | returns after A frames; A = 0 | high |
| C397 | waitRasterBelowScreen | Spins until VIC_RASTER >= $E0, i.e. until the raster is below the visible screen, so the caller can update the bitmap without tearing. Only caller is $3CFA. | none | returns with the raster in the lower border; A = raster value | high |
| C39F | clearMessageLine | Blanks the bottom status/message line: saves the current text window, switches to the message-line window, clears that line and restores the window. Called when a message expires, after disk access and from clearMessageLineAndScreen ($2DDB). | D_90FB (screen id), D_9001 | message line filled with zp_3E; text window restored from $92B6 | high |
| C3AB | setMessageLineWindow | Sets the text window to the one-line status bar: left column zp_3B = 2, width zp_3C = $24 (36 columns, i.e. columns 2-37), cursor column 0, row zp_40 = window bottom zp_3A = $17 (23), or $16 (22) when currentScreenId D_90FB >= 3 (the STATS/REPAIR/MISC menu screen). The colour zp_3D and pad colour zp_42 are set to 0 in multicolour mode and to 7 in hires mode. | D_90FB, D_9001 | zp_3A-zp_3D, zp_3F, zp_40, zp_42 set; cell pointers recomputed | high |
| C3D8 | showMessageString | Prints the bit-7-terminated string A/Y centred in the 36-column status line and restores the previous text window. This is the standard 'put this text in the status bar' call (disk errors, queued messages, the clock line). | A/Y = string pointer | status line rewritten; text window restored | high |
| C3E0 | printOnMessageLine | Worker behind showMessageString: remembers the string pointer in the self-modified operands at $C3F0/$C3F2, sets the centring width D_90EF = X, saves the text window, selects the message-line window, prints the string and pads the rest of the line. The caller restores the window (restoreTextWindow). | A/Y = string pointer, X = centring width (0 = left aligned) | message line drawn, window still set to the message line on return | high |
| C3F9 | advanceFillPointer | Loop helper for the block-fill routines: compares the 16-bit pointer zp_48/zp_49 with the limit zp_4A/zp_4B (carry set when the limit has been reached), then increments the pointer and the 16-bit counter zp_4E/zp_4F. Callers store a byte and loop while carry is clear (memory clear at $0C44, the setup block at $0333 and two loops in the $EC00 sub-overlay). | zp_48/zp_49 = current address, zp_4A/zp_4B = last address | C = 1 when the pointer had reached the limit; zp_48/zp_49 and zp_4E/zp_4F incremented | high |
| C40E | restoreTextWindow | Copies the 12 saved bytes at $92B6 back into zp_39-zp_44 (window geometry, colours, cursor and mode flags) and recomputes the cell pointers. Counterpart of saveTextWindow; there is only one save slot, so the pair cannot be nested. | $92B6-$92C1 | zp_39-zp_44 restored, cell pointers recomputed | high |
| C41B | saveTextWindow | Saves zp_39-zp_44 (the whole text-window state) to $92B6 and then resets the window to the full screen (setFullScreenWindow). Used before drawing the message line, the repair window and everything else that must not disturb the caller's window. | zp_39-zp_44 | $92B6-$92C1 = window state; window reset to 0,0,25,40 | high |
| C428 | splitDecimalDigits | Splits the byte in A into three decimal digits by repeated subtraction: ones -> $C450, tens -> $C44F, hundreds -> $C44E. The digits are used as glyph indices by drawTallGlyph for the drone fuel and missile counters ($6F9C, $7ABD). The 'bne/lda #$00/beq' sequence at $C444-$C448 has no effect (leftover of a leading-zero suppression). | A = value 0-255 | $C44E/$C44F/$C450 = hundreds/tens/ones; X clobbered | high |
| C451 | checkUiContinue | Decides whether a modal UI (menu selector, info panel, group editor) may keep running. Returns A = 1 to continue. Returns A = 0 and arms a 60-frame input lockout ($0B7D) when the game is ending (D_92C9 != 0 and not in film/solo mode), when a screen change is pending (currentKey zp_66 >= 0), when SPACE was typed (D_9248 = $A0) or when checkSetupUiContinue says stop. A pause (zp_BE != 0) or gamePhase zp_B1 bits 6/7 keep the UI alive. | D_0B9B (film/solo flag), D_92C9, zp_BE, zp_B1, zp_66, D_9248 | A = 1 continue / 0 abort; $0B7D = $3C on abort | high |
| C470 | checkSetupUiContinue | Tail of checkUiContinue, also called on its own from the map main loop at $202D during the setup phase: returns 1 while gamePhase zp_B1 is 0 or D_92C6 is 0 (or D_92C6 is negative and the local side D_0B9F is 0), otherwise 1 only if the low two bits of zp_B1 are both set, else 0 with the input lockout armed. | zp_B1 gamePhase, D_92C6, D_0B9F | A = 1 continue / 0 abort; $0B7D = $3C on abort | medium |
| C493 | serviceChatDisplay | First half of updateStatusMessage: handles the chat/typing state D_929C. A positive non-zero state means the local typing line must be shown - it clears the state, draws the typed line, remembers the pending message id in zp_7B, sets zp_7A = $81 and expires the display timer. While typing (bit 7 set), holding key $8B shows message 0 (the received chat line at $0548) instead and sets bit 6; releasing it clears bit 6 and redraws the typed line. In both typing cases it pulls two bytes off the stack so that updateStatusMessage returns to its own caller without touching the message queue. | D_929C chat state, D_92A0 raw key code, zp_7A | D_929C updated, status line redrawn, zp_7A/zp_7B/zp_72 updated; may discard updateStatusMessage's return address | medium |
| C4DA | drawTypedChatLine | Draws the locally typed chat line: prints the bit-7-terminated string buffer at $9278 left aligned on the message line, then sets the colour to $11 and prints one $A0 (space) as the cursor block, and restores the text window. | $9278 line buffer | message line redrawn with a cursor; zp_3D = $11; window restored | high |
| C4EF | updateStatusMessage | Per-frame service of the status bar, called from every polling loop (waitForCarrier, updateGameFrame, the command exchange at $4DA9, ...). Runs serviceChatDisplay and the message review handler $C631, returns while the display timer zp_72 is still running, otherwise finishes the current message (popNextMessage $C5EE), takes the pending id zp_7A, prints its string from the pointer tables $0500 (lo) / $0524 (hi), restarts the 60-tick timer and plays the message sound. When nothing is pending it refreshes the clock/score line ($C6B6) whenever the game clock D_91CB has changed during the battle. Starts with a NOP that no code patches. | zp_72, zp_7A, zp_7B, D_9277, D_92FA, zp_B1, D_91CB, D_9208, message tables $0500/$0524 | status line updated, zp_72 = $3C, zp_7A bit 7 set, D_9208 = clock; self-modifies the sound flag at $C54B | high |
| C557 | playMessageSound | Plays the sound effect assigned to message id Y: the sound number comes from the per-message table at $0612, a negative entry ($80) means silent. | Y = message id, table $0612 | sound started via sub_C89A | high |
| C560 | queueMessageUnlessFilm | Queues message id A like queueMessage but drops it silently while D_0B9B bit 7 (film playback / solo trainer) is set. Used for the combat messages ('UNIT RECYCLED', 'COMCEN STUNNED!', ...). | A = message id, D_0B9B | message queued or ignored | high |
| C566 | queueMessage | Inserts message id A into the 8-entry ring queue at $9249 (write index D_9251, read index D_9252) keeping it sorted so that higher ids are shown first: it scans from the read index for the first entry <= A, shifts the entries in between one slot forward and stores the new id there. The queue is dropped when full. If the insert lands at the read index the display timer zp_72 is zeroed so the message appears immediately, and a currently displayed message (zp_7B bit 7 set) is marked so that updateStatusMessage pops the queue on its next pass. Y is preserved. | A = message id, Y preserved, $9249-$9252 | queue updated, zp_72/zp_7B possibly updated; self-modifies operands at $C5B0, $C5B4 and $C5BB | medium |
| C5C9 | decMessageIndex | Decrements the message-queue index in Y modulo 8 (wraps to 7). | Y | Y = (Y - 1) mod 8 | high |
| C5CF | incMessageIndex | Increments the message-queue index in Y modulo 8 (wraps to 0). | Y | Y = (Y + 1) mod 8 | high |
| C5D7 | redrawStatusMessage | Redraws whatever the status bar should currently show after a screen rebuild (jumped to from $1562, $268C and the overlays at $7131/$7A68): the message currently displayed (zp_7B with bit 7 masked off), message 0 (the received chat line) when chat text is buffered ($9277 != 0), or the next queued message via popNextMessage. | zp_7B, D_9277 | status line redrawn | high |
| C5E4 | showMessageById | Looks message id A up in the pointer tables $0500 (low) / $0524 (high) and tail-calls showMessageString, so the message text is drawn centred in the status line straight away without touching the queue. | A = message id 0-$23 | status line redrawn; X = message id | high |
| C5EE | showNextQueuedMessage | Picks the next status-line message. If the 8-entry message queue is not empty (tail $9252 != head $9251) it takes the id at the tail, advances the tail and leaves it in currentMessageId zp_7A. If the queue is empty it either re-arms the idle state (zp_7A = $80, chat line cleared with clearChatStatusLine, then persistentMessageId $92A1 decides: negative = redraw the CLICKS/US/THEM score line at L_C6B6, zero = blank the status line via sub_C39F, otherwise print that message with sub_C5E4) or, when a chat line is pending (chatLineMarker $0548 not negative and zp_7A != $80), selects message id 0 (the chat line) and suppresses its sound by patching $C54B to $FF. | D_9251/D_9252 queue indices, D_9249 queue, zp_7A, D_0548, D_92A1 | zp_7A, D_9252, D_0548 (via sub_C62A), sound-suppress operand $C54B; may print the status line and clobber A/Y | high |
| C62A | clearChatStatusLine | Stores $9E (a bit-7 terminator that prints nothing) into chatLineMarker $0548, i.e. makes message slot 0 - whose text pointer is $0548 - an empty status line. Called from showNextQueuedMessage and by JMP from the game's start-up init at $0C63. | none | D_0548 = $9E, A = $9E | high |
| C631 | handleMessageReviewKeys | Per-frame hook of the message service (called from sub_C4EF at $C4F3). CRSR up/down (lastKeyCode $9248 == $8B) enters/steps the message-history scrollback: the current queue tail is saved in messageReviewIndex $C630 with bit 7 set, then each press walks one entry backwards through the ring $9249 (sound 3 when there is no older entry), shows it immediately (message timer zp_72 = 0, sound suppressed via $C54B = $80) and sets $0548 = $1E so the chat line stays live; when zp_72 drops below $23 without another press the saved index is restored and review mode ends. CRSR left/right ($8E) is consumed and, only during battle (zp_B1 == 0), arms a 60-frame input lockout, sets zp_72 = $3C, clears bit 7 of zp_7B and falls into drawStatusScoreLine. | D_C630, D_9248, zp_72, D_9251/D_9252, D_9249, zp_7A, zp_7B, zp_B1 | D_C630, D_9252, D_9248 = $FF, zp_7A, zp_7B, zp_72, D_0548, $C54B, inputLockoutTimer $0B7D; may draw the score line | high |
| C6B6 | drawStatusScoreLine | Redraws the bottom status line as '   CLICKS:<n>    US:<n>   THEM:<n>'. Saves the text window (sub_C41B), selects the status-line window (sub_C3AB), prints the string trio at $065E/$0668/$066F with sub_C133/sub_C137, the game clock gameClockClicks $91CB and the two 16-bit point totals through printPlayerPoints (first the local side D_0B9F EOR 1, then the opponent D_0B9F), pads the line (sub_C07A) and restores the window (sub_C40E). Entered by JMP only - from $C521 (clock changed while the last-quarter score display flag $92FA is set) and from handleMessageReviewKeys. | D_91CB, D_0B9F, D_92A2/D_92A4, strings at $065E | status line redrawn; zp_39-zp_44 window state saved/restored via $92B6; zp_7E, D_92DA digit buffer clobbered | high |
| C6E5 | printPlayerPoints | A = player side index (0/1). Loads that side's 16-bit point total from playerPointsLo $92A2,x / playerPointsHi $92A4,x and tail-calls the decimal printer sub_C17D. | A = side index; D_92A2, D_92A4 | digits printed; X = side index, zp_18-zp_1F and D_92DA clobbered | high |
| C6EF | resetMessageQueue | Empties the message queue by copying the head index $9251 into the tail $9252, clears persistentMessageId $92A1 and the message timer zp_72, then falls through to showNextQueuedMessage so the status line is refreshed at once. Called from a dozen places whenever a screen changes or a game phase starts/ends. | D_9251 | D_9252 = D_9251, D_92A1 = 0, zp_72 = 0, status line redrawn | high |
| C6FF | showStatusMessage | A = message id. Plays the sound associated with that id (sub_C557 looks it up in the per-message sound table at $0612; $80 = silent), stores the id in pendingMessageId zp_7B and prints the message text immediately through sub_C5E4, bypassing the queue. Used by the disk-error retry path ($082D), the link-status screens and the comm module. | A = message id; D_0612, D_0500/D_0524 | zp_7B = id, message printed, sound queued | high |
| C705 | showStatusMessageNoSound | Tail/alternate entry of showStatusMessage that skips the sound lookup: stores A in pendingMessageId zp_7B and prints that message. Reached by JMP from showWorkingMessage ($C858). | A = message id | zp_7B = id, message printed | high |
| C70A | handlePauseKey | RUN/STOP pause handling, called every frame from updateGameFrame ($40BF/$413D). Returns at once unless gamePhase zp_B1 == 0 and a game is running ($0BA8 == 0). In solo/film mode ($0B9B bit 7) RUN/STOP toggles commPauseFlags zp_BE; while paused persistentMessageId is $19 ('TIMEOUT. RUN/STOP TO RESUME.'), only handleMenuSessionPacket is serviced and two PLAs discard the caller's return address so the rest of the frame update is skipped. In live play the first RUN/STOP shows message 1 and sends command $8C reason 3; once zp_BE is set it saves the sprite-enable mask, clears the screen windows (unless currentScreen == 3), then either performs a voice pause (zp_BE bit 7 -> voicePauseReconnect) or spins calling serviceCommandExchange until zp_BE clears, sending $8C reason 4 (and showWaitingForOpponent) on each further RUN/STOP. On resume it redraws the overview map and viewport on screen 0 and restores the sprites. | zp_B1, D_0BA8, D_0B9B, zp_BE, D_9022, D_90FB, D_0B7D | zp_BE, D_92A1, D_9022 restored via enableSprites, commands $8C/3 and $8C/4 queued, screen redrawn; may return to the caller's caller | high |
| C7A3 | sendPauseCommand | A = reason code. Stores it in cmdParam0 zp_73 and queues the 2-byte network command $8C via queueCmd2 ($561F). Reason 3 = request pause, 4 = request resume (reason 2 = ready for new game and 5 = voice pause/disconnect are sent elsewhere). | A = reason | zp_73, outgoing command ring | high |
| C7AA | consumeRunStopKey | Edge test for the RUN/STOP key: if lastKeyCode $9248 holds $93 it arms a 60-frame input lockout ($0B7D), clears the key, plays click sound 2 and returns A = 0 (Z = 1); otherwise it returns with Z = 0. All three call sites are inside handlePauseKey. | D_9248 | A/Z, D_9248 = $FF, D_0B7D = $3C, sound 2 | high |
| C7C3 | showWaitingForOpponent | A = commPauseFlags. When the comm build flag $0BA5 bit 7 is clear and A bit 0 is set, sets persistentMessageId to $0B ('WAITING FOR OPPONENT...') and queues it with sub_C566. Also called from overlay A ($7C00). | A, D_0BA5 | D_92A1 = $0B, message queued | high |
| C7D5 | handleTextEntryKey | One-key line editor shared by the in-game chat (called from handleChatSend at $1387, inside the raster IRQ, with A = $A3) and the SAVE GAME FILM name field ($8029, A = $10). A = maximum length with bit 7 = 'scroll left when full', Y = cursor position in the 36-byte buffer $9278. It consumes newKeyEvent $90EA: RETURN ($8D) returns with bit 7 set in Y, DEL ($88) backspaces, codes $20-$5F are validated through the ASCII-to-glyph table ($985E,x; entry $3B means 'no glyph', which rejects backslash, caret and underscore) and stored, and the buffer is re-terminated with $9E. Overflow either scrolls the buffer one byte left or beeps (sound 3); an accepted key clicks (sound 2) and clears $90EA and $9248. Returns Z = 1 when nothing was accepted, otherwise A = the accepted key code. | A = max length/flags, Y = cursor position, D_90EA, D_985E table | A = key code or $FF, Z flag, Y advanced (bit 7 on RETURN), D_9278 buffer, D_90EA = $FF, D_9248 = $FF, sound; self-modifies $C813, $C817, $C84F | high |
| C853 | showWorkingMessage | Sets persistentMessageId $92A1 to 1 and shows message 1 ('WORKING...') immediately through showStatusMessageNoSound. Called before slow operations (disk access, game start, pause request). | none | D_92A1 = 1, zp_7B = 1, message printed | high |
| C85B | promptPressButtonWhenReady | Prints 'PRESS BUTTON WHEN READY.' ($CBBE) on the status line, then loops stirring the game RNG (nextGameRandom) until checkFirePressed reports a fresh fire-button click, and finally restores/blanks the status line with sub_C39F. Only caller is waitForGameDiskInserted ($10E8), so the wait also randomises the map seed. | fire button (zp_5F/zp_79) | returns after a click; RNG advanced, status line redrawn | high |
| C86D | exitIfGameEnded | If gameEndFlag $92C9 has bit 7 set the routine never returns: it jumps to returnToMainMenu ($0AA3) when gamePhase zp_B1 is non-zero (setup/menu/film) or to endGameToMainMenu ($1ADC) during battle. Otherwise it simply returns. Called at the top of updateGameFrame ($40B2) and from the command-exchange loop ($4E41). | D_92C9, zp_B1 | returns, or transfers control to the menu (stack is rebuilt there) | high |
| C89A | playSound | Public sound entry used by ~50 call sites: A = sound id 0-28, calls requestSound and restores X and Y afterwards through two self-modified operands ($C8A4, $C8A6). Because the save slots are static this entry is not re-entrant; the IRQ-side callers ($1370, $140E) deliberately call requestSound directly instead. | A = sound id | sound request queued; X and Y preserved | high |
| C8A8 | requestSound | Core of the sound request queue. A = 0 silences everything: slots $92E7-$92E9 become $FF and slot $92EA becomes 0, so the IRQ dequeues sound id 0 and stops all voices. Otherwise the class byte from soundClassTable $C87D[id] decides: a negative class forces the request into slot (class AND 3) - $80/$81/$82 are voices 0/1/2 and $FF is the multi-voice slot 3 - while a positive class (0, 1 or 2) is a plain voice number that is only taken when that slot is idle ($FF) and the sound currently playing on that voice (sndVoiceSoundId $67C8,x) is either silent or has a non-negative (low priority) class. Ids >= $1D are discarded. | A = sound id 0-28; D_C87D, D_92E7-D_92EA, D_67C8 | one of D_92E7-D_92EA set to the id (or all cleared); self-modifies $C8DC; A/X/Y clobbered | high |
| C8E9 | dequeueSoundRequest | Scans the four request slots from 3 down to 0 for a pending id; the first non-negative one is returned in X and its slot is reset to $FF. X = $FF when all four are empty. Called once per frame from the raster IRQ at $1283, which then hands X to startSound ($6728). | D_92E7-D_92EA | X = sound id or $FF, that slot cleared; A/Y clobbered | high |
| C8FA | updateComcenMoveSound | Keeps the own command centre's engine sound in step with its movement. Runs only during live battle (gamePhase zp_B1 == 0 and playModeFlags $0B9B == 0). If the own comcen ($9236) is moving (unit idle flag $FB54 bit 7 clear) and SID voice 2 is silent it requests sound $14; if the comcen has become idle while voice 2 is still playing $14 or $16 it requests the stop sound $1C. Called for every unit step from processUnitStep ($42A0). | zp_B1, D_0B9B, D_9236, D_FB54, D_67CA (voice 2 current sound) | sound $14 or $1C requested | medium |
| C924 | setMessageSlot | X = message id, A/Y = string address. Writes the run-time message pointer tables: low byte into $0500,x and high byte into $0524,x. Used for the dynamic slots $1E, $20 and $21 by the boot code ($0B1D/$0B26/$0C8E/$0C97), overlay A ($7BAC/$81C2/$821C/$86B5/$8724) and the comm-module tail ($EED6/$EEFF). | A = string low byte, Y = string high byte, X = message id | D_0500[x], D_0524[x]; A = high byte on exit | high |
| CD0A | showUnitPictureSprites | X = unit type 0-4 (GRUNT/RIDER/BOOMER/SPY/COMCEN). Fetches that type's RLE block address from unitPictureLoTable $CD05,x / unitPictureHiTable $CD00,x, unpacks it into $0200-$03FF with unpackRleTo0200 and copies the first 192 bytes to $8B40, which is the shape area of sprites 5, 6 and 7 (pointers $2D-$2F). Then sets those three sprite colours to white, X positions to 236/268/300 (the two MSBs via $9020 /= $C0), Y position to 162 and enables sprites 5-7 with mask $E0. Only caller is showUnitInfoPanel ($22F1), whose LDX operand at $22EF is patched with the unit type. | X = unit type 0-4 | $0200-$03FF overwritten, $8B40-$8BFF = three 24x21 sprite frames, sprite shadows $900D-$900F, $9015-$9017, $901D-$901F, $9020, $9022; zp_B2/B3/1A/1C clobbered | high |

Data:

- `$C000` **bitPairHighTable** (byteTable, 4 bytes): $00,$40,$80,$C0 - a two-bit value shifted into bit positions 7-6. No reference in the disassembly (the label bootEntry on it belongs to the boot loader that used to live here).
- `$C004` **solidColourByteTable** (byteTable, 4 bytes): $00,$55,$AA,$FF - the byte that fills a whole multicolour bitmap row with colour 0-3. No reference in the disassembly.
- `$C008` **bitMaskTable_C008** (byteTable, 8 bytes): $80,$40,$20,$10,$08,$04,$02,$01 - single bit masks, read as L_C008,x by the sprite/bit plotting code at $30B9 (ORA, set bit) and $3217 (LDA, test/EOR).
- `$C010` **pixelPairMaskTable** (byteTable, 4 bytes): $C0,$30,$0C,$03 - the four multicolour pixel-pair masks of a bitmap byte. No reference in the disassembly.
- `$C014` **pixelPairClearMaskTable** (byteTable, 4 bytes): $3F,$CF,$F3,$FC - complements of pixelPairMaskTable. No reference in the disassembly.
- `$C018` **unusedC018** (unknown, 2 bytes): $A9,$8D - two bytes between the mask tables and printChar; no reference, meaning unknown.
- `$C23B` **pixelDoubleTable** (byteTable, 16 bytes): $00,$03,$0C,$0F,$30,$33,$3C,$3F,$C0,$C3,$CC,$CF,$F0,$F3,$FC,$FF - expands the four bits of a nibble into eight pixels (each bit doubled), used by expandGlyphNibbles for the 2x glyph magnifier. The stale labels bootShowTitlePicture ($C23F) and L_C249 sit inside this table.
- `$C44E` **decimalDigits** (byteTable, 3 bytes): Three RAM bytes inside the code holding the hundreds/tens/ones digits produced by splitDecimalDigits; read by the drone fuel ($6FD6/$6FDE/$6FE6) and missile counter ($7AD5/$7ADD) displays.
- `$C630` **messageReviewIndex** (byteTable, 1 bytes): Single state byte for the message-history scrollback, stored between routines (bit 7 = active, low 7 bits = saved queue read index).
- `$C87D` **soundClassTable** (byteTable, 29 bytes): Class/priority byte per sound id 0-28 used by requestSound. $00/$01/$02 = normal-priority request for SID voice 0/1/2 (only accepted if that voice is free or busy with another normal sound); $80/$81/$82 = force onto voice 0/1/2; $FF = force onto the whole-chip slot 3 (three-voice fanfares such as ids 13, 14 and 25). Contents: FF 00 00 00 00 00 FF 80 81 01 01 01 80 FF FF 82 82 82 82 82 02 FF 82 82 82 FF 80 81 82.
- `$C92C` **groupNameTable** (text, 56 bytes): Seven 8-byte group names printed with sub_C100 (index = unitIdleFlags bits 2-4): ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER. Used by printUnitStatusLines ($23B1), runJoinGroupMenu ($6162) and runGroupOptionsMenu ($634E). An index of 7 would fall into unitTypeNameTable and print GRUNT.
- `$C964` **unitTypeNameTable** (text, 40 bytes): Five 8-byte unit type names indexed by unitTypeTable bits 0-2: GRUNT, RIDER, BOOMER, SPY, COMCEN. Printed by showUnitInfoPanel ($22C9).
- `$C98C` **unitStatusNameTable** (text, 70 bytes): Nine 8-byte status words: 0 READY, 1 RELOAD, 2 FIRED, 3 ' -   ', 4 REPAIR, 5 MOVE, 6 PINNED, 7 STUN'D, 8 DIGGIN. The WEAPON: line indexes it from $C98C, the ACTION: line from entry 3 ($C9A4) so entries 3-8 become ' -   '/REPAIR/MOVE/PINNED/STUN'D/DIGGIN. The last entry is only 6 bytes long because the next block starts right after it.
- `$C9D2` **unitOptionMenuText** (text, 186 bytes): Twelve $8D-terminated menu lines for the UNIT/GROUP OPTIONS menus, printed in sequence with sub_C133/sub_C137 from $5FCA and $6387: FORMATION, MOVE TO GOAL, CLOAKING ON, NO CLOAKING, CLEAR TARGET, SET TARGET, MEMBERSHIP, JOIN GROUP, DIRECT UNIT, SELF DESTRCT, SET BLITZ, BLITZ OFF.
- `$CA8C` **formationMenuText** (text, 45 bytes): Formation sub-menu, one string with embedded $0D line breaks: 'SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT)'.
- `$CAB9` **consoleMiscMenuText** (text, 48 bytes): Six $8D-terminated lines used by the command-console panels: ' ENERGY', ' BATTLE', ' RADAR', '  DRONE', '  DIG OUT', 'DIG IN'.
- `$CAE9` **unitInfoLabelText** (text, 33 bytes): Label column of the unit info panel, one string with $0D breaks and a $BA terminator: 'GROUP:' + three blank lines + 'ENERGY:' 'WEAPON:' 'ACTION:'. Printed by showUnitInfoPanel ($22E0).
- `$CB0A` **consoleHeadingText** (text, 21 bytes): Two headings: '    UNIT' ($CB0A) and '   OPTIONS' + $0D ($CB13).
- `$CB1F` **uiPromptStrings** (text, 218 bytes): Pool of short bit-7-terminated prompts and status words used by the unit/group menus, the command console tabs and the info panel: GROUP ($CB1F), WHICH GROUP: ($CB25), '  TOO FAR!' ($CB31), ADDED ($CB3B), REMOVED ($CB40), FINISHED ($CB47), SETTING ($CB4F), DESTINATION ($CB56), TARGET ($CB61), FUEL: ($CB67), MISC ($CB6C), STATS ($CB70), REPAIR ($CB75), CLOAKED ($CB7B), BLITZ ($CB82), DUG IN ($CB87), NOT IN GROUP ($CB8D), RE-POSITIONED ($CB99), NEW POSITION ($CBA6), ' EXIT ' ($CBB2), ENERGY ($CBB8), 'PRESS BUTTON WHEN READY.' ($CBBE, used by promptPressButtonWhenReady), 'REPAIR / WHICH:' ($CBD6), 'ATTEMPTING REPAIR...' ($CBE5). MISC/STATS/REPAIR are the three command-console tab labels drawn around $1655-$1675.
- `$CBF9` **gameHeaderText** (text, 24 bytes): Status header fragments: ' PLAY' ($CBF9), '1ST' ($CBFE), '2ND' ($CC01) and '\r  SCENARIO\r' ($CC04).
- `$CC11` **rulesNameTable** (text, 14 bytes): Two 8-byte-stride entries selected with sub_C100 at $15C9: STANDARD ($CC11) and CUSTOM ($CC19, only 6 bytes because the next block follows).
- `$CC1F` **mainMenuItemText** (text, 122 bytes): The six main-menu lines printed in sequence: 'COMPETE WITH MODEM OPPONENT', 'PRACTICE WITH SOLO TRAINER', 'WATCH GAME FILM', 'SAVE GAME FILM' ($CC1F block), 'LOAD GAME FILM' ($CC75) and 'DISCONNECT/SET VOICE' ($CC84).
- `$CC99` **inGameMenuText** (text, 36 bytes): ENEMY ($CC99), MISSILES ($CC9E), 'ABORT GAME' ($CCA6) and 'VOICE PAUSE' ($CCB1) - the items of the in-game MISC options menu plus two panel captions.
- `$CCBD` **strGameTypeLabel** (text, 11 bytes): 'GAME TYPE:' header printed before the scenario name at $15C5.
- `$CCC8` **scenarioNameTable** (text, 56 bytes): Seven 8-byte scenario names indexed by gameTypeOptions $0BA3 bits 0-2 (printed with sub_C100 at $15D9): SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER.
- `$CD00` **unitPictureHiTable** (byteTable, 5 bytes): High bytes of the five unit-picture RLE block addresses ($CD, $CD, $CE, $CE, $CF). Also probed as a build hook: $0C0D tests whether $CD00 holds $4C (JMP) and, if so, disables VIC IRQs and jumps there - in the shipped overlay it holds table data, so the hook is inert; showUnitInfoPanel ($22EA) only tests that $CD00 is negative (pictures present).
- `$CD05` **unitPictureLoTable** (byteTable, 5 bytes): Low bytes of the five unit-picture RLE block addresses ($52, $D3, $3B, $CD, $3A) giving $CD52, $CDD3, $CE3B, $CECD and $CF3A.
- `$CD52` **unitPictureGrunt** (sprite, 129 bytes): RLE block (escape marker $05) for unit type 0 = GRUNT; the first 192 unpacked bytes are three 24x21 sprite frames (a helmeted figure, a running figure and a burst).
- `$CDD3` **unitPictureRider** (sprite, 104 bytes): RLE block (escape marker $07) for unit type 1 = RIDER; three 24x21 sprite frames.
- `$CE3B` **unitPictureBoomer** (sprite, 146 bytes): RLE block (escape marker $08) for unit type 2 = BOOMER; three 24x21 sprite frames (launcher, missile in flight, emplacement).
- `$CECD` **unitPictureSpy** (sprite, 109 bytes): RLE block (escape marker $05) for unit type 3 = SPY; three 24x21 sprite frames (antenna/aerial shapes).
- `$CF3A` **unitPictureComcen** (sprite, 163 bytes): RLE block (escape marker $0A) for unit type 4 = COMCEN; three 24x21 sprite frames of the command centre building. Its 192nd unpacked byte (the unused pad of sprite 7) is fetched from $CFDD, which is already inside the leftover code fragment at the end of the page.

Insights:

- [game_ovl_C000_C000] This chunk is the game's text/glyph engine and status-message system, not a loader: the file still carries the boot loader's labels (bootEntry, bootLoadSectors, bootSetDest, bootReadOneSector, bootReceiveSectorData, bootDestLo, bootMain, rtiVector, bootShowTitlePicture, bootModemInit, rs232Params, strHayesInit, strBlockExecute, strGameChar, bootSavedCpuPort, bootDiskIdTable, bootDone) because boot/loader_C000.s occupies the same addresses. In the overlay every one of those labels lands in the middle of an unrelated instruction or table; only the comments already marked 'stale' warn about it. XREF.txt merges the two units as well, so caller lists for $C0xx-$C3xx contain both boot-loader and overlay addresses.
- [game_ovl_C000_C000] Text API (matches and extends the notes in game_main_0800_163A): window = zp_39 top, zp_3A bottom, zp_3B left, zp_3C width; cursor = zp_3F column (relative to zp_3B) and zp_40 row (absolute); zp_3D = colour byte, zp_3E = blank glyph, zp_42 = pad colour, zp_43 bit 7 = raw glyph mode, zp_44 bit 7 = double-size characters. The whole block zp_39-zp_44 is saved/restored to $92B6 by $C41B/$C40E - a single slot, so the pair must not be nested.
- [game_ovl_C000_C000] Character rendering: printChar translates ASCII $20-$5F through the 64-byte table at $987E into glyph indices 0-$3B; glyph 0 is blank. Two 60-glyph fonts of 8 bytes each hold the same shapes: $98BE-$9A95 for multicolour mode (every pixel is bit pair 11, so the colour byte zp_3D goes to colour RAM $D800) and $9A96 for hires mode (colour byte goes to screen RAM $8C00). updateCursorPointers ($C099) is what turns the screen pointer into a colour-RAM pointer by adding $4C to zp_B7 when $D016 shadow D_9001 bit 4 is set. This font is distinct from the 114-glyph map font at $9426 used by plotGlyph ($2AC8).
- [game_ovl_C000_C000] The glyph copy loop at $C227-$C231 reads the existing bitmap byte first and its second opcode ($C22B) is patched by setBitmapDrawMode ($2A41) to LDA/ORA/EOR/CMP (zp),Y, so text can be drawn opaque, ORed, EORed (highlight) or transparently. The matching colour instruction at $C233 ('lda (zp_B6),y' followed by 'lda zp_3D') is never patched, so the colour byte is always replaced - the load at $C233 is dead.
- [game_ovl_C000_C000] $C24B is a general 2x2 magnifier used both by the double-size text mode and by magnifyWindow2x ($2E0F): it expands one glyph into four cells via the nibble-doubling table at $C23B, with zp_BA masking off quadrants (bit 0 top, bit 1 bottom, bit 2 left, bit 3 right) so odd-sized zoom rectangles keep half-size edges. Note that zp_BA is only ever written by magnifyWindow2x, so double-size text depends on whatever mask that routine left behind.
- [game_ovl_C000_C000] Status bar: setMessageLineWindow ($C3AB) defines it as one row, columns 2-37 (left 2, width $24), row 23 normally and row 22 when currentScreenId $90FB >= 3; the colour is 0 in multicolour mode and 7 in hires. showMessageString ($C3D8) centres a string there and restores the caller's window; clearMessageLine ($C39F) blanks it.
- [game_ovl_C000_C000] Message system: ids 0-$23 index the pointer tables $0500 (lo) / $0524 (hi) and the sound table $0612 ($80 = silent). queueMessage ($C566) keeps an 8-entry ring at $9249 (write index $9251, read index $9252) sorted so higher ids win; updateStatusMessage ($C4EF), called from every polling loop, shows the pending id for $3C ticks (zp_72), plays its sound, and when nothing is queued either shows the persistent message $92A1 or refreshes the clock/score line at $C6B6 whenever gameClock $91CB changes. zp_7A = pending id, zp_7B = displayed id, bit 7 set means 'none'.
- [game_ovl_C000_C000] Chat integration: while typing (chatState $929C bit 7) the status line shows the local line buffer $9278 with a $11-coloured cursor block; holding key $8B peeks at the received chat line (message 0, text at $0548) and releasing it restores the typed line. serviceChatDisplay pops two bytes off the stack in that case so updateStatusMessage returns to its caller without touching the queue.
- [game_ovl_C000_C000] checkUiContinue ($C451) is the common 'may this modal UI keep running' test used by the menu selector, the info panel and the group editor: it returns 0 (and arms the 60-frame input lockout $0B7D) on game end, on a pending screen change (zp_66 >= 0), on SPACE ($9248 = $A0) or when the setup-phase test at $C470 says stop.
- [game_ovl_C000_C000] Leftovers worth knowing: the newline routine compares the row against the window bottom and branches to the next instruction either way ($C04D-$C053), so windows never scroll; splitDecimalDigits has a dead leading-zero test at $C444-$C448; advanceFillPointer keeps a 16-bit counter in zp_4E/zp_4F that nothing reads; three unreferenced mask tables sit at $C000/$C004/$C010/$C014 and three unreferenced bytes 'lda #$04 / rts' at $C0BD; updateStatusMessage starts with a NOP that no code patches.
- [game_ovl_C000_C000] Register conventions: printChar preserves A/X/Y using the zp_54/zp_55/zp_56 triple ($C0E8 save, $C030 restore); the same triple is borrowed by the viewport drawing code at $5C81-$5CA1. updateCursorPointers and plotGlyphDoubleSize preserve X/Y through self-modified immediate operands rather than the stack.
- [game_ovl_C000_C5EE] Status-line message system (all of it lives in this overlay): 36 message slots with pointer tables at $0500 (lo) / $0524 (hi) and a per-message sound id at $0612 ($80 = silent); slot 0's text is $0548, the chat line ($9E = blank, $1E = show the incoming chat text mirrored at $0549). sub_C566 inserts an id priority-sorted into the 8-entry ring $9249-$9250 (head $9251, tail $9252); showNextQueuedMessage ($C5EE) pops it into zp_7A; sub_C4EF prints it and reloads messageTimer zp_72 with $3C; persistentMessageId $92A1 is the fallback when the ring is empty (0 = blank line, negative = draw the score line instead); resetMessageQueue ($C6EF) flushes everything.
- [game_ovl_C000_C5EE] Key codes decoded from keyMatrixCodeTable ($0D67, row-major PA0..PA7 x PB0..PB7): $88 INST/DEL, $8B CRSR up/down, $8C HOME, $8D RETURN, $8E CRSR left/right, $93 RUN/STOP, $A0 SPACE, $9B left-arrow, $83 pound, $FE modifier, $FF CTRL. In this chunk CRSR up/down scrolls back through the message history, CRSR left/right pops up the CLICKS/US/THEM score line during battle, and RUN/STOP is the pause key.
- [game_ovl_C000_C5EE] Pause protocol: RUN/STOP sets commPauseFlags zp_BE. Live play sends command $8C with reason 3 (pause) and reason 4 (resume) through sendPauseCommand ($C7A3 -> queueCmd2); reason 5 is the voice pause sent by runInGameOptionsMenu, and reason 2 is 'ready for new game'. While paused, handlePauseKey ($C70A) owns the frame: it clears the screen windows, spins on serviceCommandExchange, shows message $0B 'WAITING FOR OPPONENT...' on each further RUN/STOP, and on release redraws the overview map and viewport and restores the sprite-enable mask. In solo/film mode it instead pops the caller's return address so the rest of updateGameFrame is skipped, with message $19 'TIMEOUT. RUN/STOP TO RESUME.' on screen. (The knowledge base credits $C768 to sub_C86D; it is actually inside sub_C70A.)
- [game_ovl_C000_C5EE] Sound architecture detail: soundClassTable $C87D (29 entries) gives each sound id a class byte. Low two bits = target slot; $00/$01/$02 are 'normal priority on voice 0/1/2' and are only accepted when that request slot is idle and the voice is silent or playing another normal-priority sound, while $80/$81/$82 force voices 0-2 and $FF forces slot 3, the whole-chip slot used by the three-voice fanfares (13, 14, 25). requestSound(0) is the 'silence' request: it sets slots 0-2 to $FF and slot 3 to 0 so the IRQ dequeues id 0 and stops the chip. playSound ($C89A) preserves X/Y via self-modified operands and is therefore NOT re-entrant; the IRQ-side callers ($1370 in handleChatSend and $140E) call requestSound ($C8A8) directly to avoid corrupting it - but handleTextEntryKey, which also runs in the IRQ, does call $C89A, so a click or beep raised from the IRQ can clobber the X/Y save slots of a main-thread $C89A call.
- [game_ovl_C000_C5EE] Unit pictures: five RLE blocks at $CD52/$CDD3/$CE3B/$CECD/$CF3A (pointer halves at $CD05 lo / $CD00 hi) hold three 24x21 frames each for GRUNT, RIDER, BOOMER, SPY and COMCEN, in unit-type order. showUnitPictureSprites ($CD0A) unpacks one into $0200, copies 192 bytes to $8B40 (sprite pointers $2D-$2F = sprites 5/6/7), colours them white and places them at X = 236/268/300, Y = 162, i.e. across the info panel. Each block's stream ends exactly where the next begins; only the first 192 of the 512 unpacked bytes are meaningful.
- [game_ovl_C000_C5EE] The string pool uses deliberate table overlap to save bytes: the 9-entry unitStatusNameTable at $C98C is indexed from $C98C for the WEAPON: line and from entry 3 ($C9A4) for the ACTION: line; the last entry (DIGGIN) and the CUSTOM entry of rulesNameTable are truncated to 6 bytes because the following block starts immediately; and group index 7 of groupNameTable ($C92C) would read GRUNT out of the adjacent unitTypeNameTable.
- [game_ovl_C000_C5EE] $CD00 is probed by $0C0D as an optional build hook: if it holds $4C (a JMP opcode) the game turns off VIC IRQs and jumps to $CD00. In the shipped overlay it holds the unit-picture high-byte table ($CD,$CD,$CE,$CE,$CF), so the hook never fires; showUnitInfoPanel only checks that $CD00 is negative to decide that unit pictures are available.
- [game_ovl_C000_C5EE] $0B9F is the OPPONENT's side index, not the local player's: drawStatusScoreLine prints D_0B9F EOR 1 under 'US:' and D_0B9F under 'THEM:', and computeGameResult chooses 'WE LOST ON POINTS!' ($11) / 'WE WERE KNOCKED OUT!' ($14) exactly when the winner index equals $0B9F. The knowledge base entry for $28F1 states the opposite.
- [game_ovl_C000_C5EE] handleTextEntryKey ($C7D5) is the shared line editor for the in-game chat (called from the IRQ at $1387 with A=$A3: 35 chars, scroll when full) and the SAVE GAME FILM name field ($8029, A=$10: 16 chars, no scroll). Character validation goes through the ASCII-to-glyph table indexed directly by character code as $985E,x (= $987E - $20); the three codes whose glyph entry is $3B (backslash, caret, underscore) are rejected with a beep. The scroll loop at $C81D copies 36 bytes from $9279 to $9278 and therefore reads one byte past the buffer, pulling chatState $929C into $929B.
- [game_ovl_C000_C5EE] The overlay's last 35 bytes ($CFDD-$CFFF) are an unreferenced, truncated copy of the timer-decrement tail of updateFrameTimers ($12DD) - build leftover, never executed.
- [game_ovl_C000_C5EE] Mis-traced labels: XREF.txt attributes two boot-loader call sites to this overlay ('sub_CBED call:C3D0', 'sub_CBF2 call:C3D9'). Those JSRs live in boot/loader_C000; in the runtime overlay $C3D0 is the operand byte of LDY #$00 and $C3D9 the operand of LDX #$24. The result is two spurious routine labels, sub_CBED and sub_CBF2, planted in the middle of the text string 'ATTEMPTING REPAIR...' at $CBE5 - they are data, not code.

## game/tileRules0200

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| 0300 | initGameSystems | One-shot start-up routine that lives inside the block loaded from track 18 sectors 15-16. Called exactly once, from mainStart at $0A87 immediately after loadSetupBlock ($10BB). It calls resetGameVariables ($0C1C), resetSpritesAndVicBank ($035E), invertTextFonts ($031B), seeds the 24-bit game RNG with $49/$19/$02, calls buildMapRowAddressTable ($0339) and finally JMPs to initBitmapScreen ($2D41), which sets up the hires bitmap display and returns to mainStart. | none (runs with $01=$35, ROMs out, I/O in) | zero page and $9008-$92FF cleared by resetGameVariables; zp_57/58/59 = $49/$19/$02 (RNG seed); all sprite registers and $9022 cleared; VIC bank 2 selected; fonts at $98BE-$9C6D inverted; map row tables at $BFB0/$BFD8 built; bitmap screen initialised; zp_48-zp_4B and zp_4E/4F clobbered | high |
| 031B | invertTextFonts | Inverts (EOR #$FF) the 944 bytes at $98BE-$9C6D in place, walking the source pointer with advanceFillPointer ($C3F9) using zp_48/49 = $98BE and the limit zp_4A/4B = $9C6D. That range is exactly the two 59-glyph 8x8 character sets used by the $C000 overlay's text plotter: $98BE (multicolour / double-width glyphs) and $9A96 = $98BE+$1D8 (hires glyphs), selected at $C20B by bit 4 of the $D016 shadow $9001. The glyphs are stored positive on disk (1 bits = strokes) and are negatives after this call, so the plotter's ORA/EOR/CMP draw modes (set via setBitmapDrawMode, which patches $C22B) operate on inverted art. | none; hardcoded range $98BE..$9C6D | $98BE-$9C6D EORed with $FF; zp_48/49 left at $9C6E, zp_4A/4B = $9C6D, zp_4E/4F incremented 944 times (never read) | high |
| 0339 | buildMapRowAddressTable | Builds the 40-entry row address table of the 40x40 battlefield map: for row 0..39 it stores the high byte of $F000+40*row at $BFD8+row and the low byte at $BFB0+row. These tables are what readMapCell ($25FA) and every other map accessor use to turn (column,row) into a pointer, so this must run before any map access. Uses zp_48/zp_49 as the running address. | none; base address $F000 and stride $28 are hardcoded | $BFB0-$BFD7 = row address low bytes, $BFD8-$BFFF = high bytes; zp_48/zp_49 left at $F640 (one row past the map) | high |
| 035E | resetSpritesAndVicBank | Clears all sprite state and picks the VIC bank. Zeroes $D000-$D010 (all eight sprite X/Y pairs plus the X-MSB register), $D015 (sprite enable) and its shadow $9022, $D017 (Y expand) and $D01A-$D01F (raster IRQ mask, sprite priority, sprite multicolour, X expand and the two collision latches, which the write clears). Then makes CIA2 port A bits 0-1 outputs and writes $05: VIC bank 2 ($8000-$BFFF, matching bitmap $A000 / screen $8C00) with the user-port RS-232 TXD latch left high. Ends with JMP clearAllSpriteData ($2FA5), which zeroes the sprite shape area $8A00-$8BFF. | none | $D000-$D010, $D015, $D017, $D01A-$D01F = 0; $9022 = 0; CIA2_DDRA /= $03; CIA2_PRA = $05 (VIC bank 2); $8A00-$8BFF cleared; A = 0 | high |

Data:

- `$0200` **contourRuleTable** (byteTable, 256 bytes): Terrain-contour rule table of the random map generator, read as contourRuleTable,Y at $7484 in overlay A. Y is an 8-bit mask of which of the eight neighbours of the current cell are higher terrain, bit order bit0=NW, bit1=N, bit2=NE, bit3=W, bit4=E, bit5=SW, bit6=S, bit7=SE (verified from the edge-clamp masks at $7463-$7481: $D6 at column 0, $6B at column 39, $F8 at row 0, $1F at row 39). Entry semantics: $00 (index 0 only) do nothing; $41-$51 a contour tile code to place, from which the generator subtracts contourTileOffset ($0324, 0 for the summit ring and $20 for the lower-hill ring) - $51 north edge, $4D south, $4B west, $49 east, $45 N+W corner, $46 N+E, $47 W+S, $48 E+S, $44/$43/$42/$41 the isolated NW/NE/SW/SE diagonals; $80+n (105 entries) randomly erase neighbour n instead; $91/$95/$97 (indexes $05, $A0, $84) erase that neighbour or else fill; $FF (93 entries) fill the cell with the interior tile. Loaded from track 18 sector 15 by loadSetupBlock ($10BB) / loadMapAndSetup_10AB and never written back to disk. Only valid between $0B35 and $7AE9, after which the trainer playbook (track 29 s6-8) overwrites the page.
- `$0200` **contourRuleTable** (byteTable, 256 bytes): Terrain-contour rule table of the random map generator (overlay A, read at $7484 as contourRuleTable,Y). Y is an 8-bit mask of which of the eight neighbours of the current cell are higher terrain; the bit order is verified from the edge-clamp masks at $7463-$7481 ($D6 at column 0, $6B at column 39, $F8 at row 0, $1F at row 39): bit0=NW, bit1=N, bit2=NE, bit3=W, bit4=E, bit5=SW, bit6=S, bit7=SE. Entry semantics, confirmed against the actual bytes: $00 (index 0 only) = do nothing; $41-$51 = contour tile code to place (the generator subtracts contourTileOffset $0324, 0 for the summit ring, $20 for the lower-hill ring) - $51 north edge, $4D south edge, $4B west edge, $49 east edge, $45 N+W corner, $46 N+E, $47 W+S, $48 E+S, $44/$43/$42/$41 the isolated NW/NE/SW/SE diagonals; $80+n (105 entries, n = neighbour bit index) = randomly erase neighbour n instead; $91/$95/$97 (indexes $05 NW+NE, $A0 SW+SE, $84 NE+SE) = erase that neighbour or else fill; $FF (93 entries, including index $FF) = fill the cell with the interior tile. Loaded from track 18 sector 15 by loadSetupBlock/loadMapAndSetup; never written back to disk.
- `$0389` **setupBlockPadding** (unknown, 119 bytes): $0389-$03FF, all zero on disk: the tail of track 18 sector 16 after the one-shot start-up code at $0300-$0388. Not referenced as code or data. At run time the page is reused for the film directory, the RLE sprite-shape buffer, overlay A's map-generator variables and the trainer playbook.
- `$0389` **setupBlockPadding** (unknown, 119 bytes): $0389-$03FF, all zero on disk. Tail of track 18 sector 16 after the start-up code; not referenced as code or data by anything. At run time the page is later reused for other things (film directory, sprite shape buffer, map-generator variables).

Insights:

- [data_setup_0200] The block that ships in track 18 sectors 15-16 is 256 bytes of pure data (contourRuleTable) followed by 137 bytes of one-shot start-up code ($0300-$0388, surveyed in game_setup_0200_0200.json) and 119 zero bytes; there are no strings in it.
- [data_setup_0200] Neighbour-mask bit order for the whole map generator: bit0=NW, bit1=N, bit2=NE, bit3=W, bit4=E, bit5=SW, bit6=S, bit7=SE (reading order, centre skipped).
- [data_setup_0200] XREF.txt and the include lists of other files carry boot-era names for addresses in this block: $0300 appears as basicVectorHijack and $02B8/$02EC as eaLoadGame/eaFileName, all from boot/ea.s which loaded to $02A8-$030B before the game started.
- [game_setup_0200_0200] The block at $0200-$03FF is not really a 'saved setup block': $0200-$02FF is the map generator's 256-entry contour rule table and $0300-$0388 is a one-shot start-up routine, both read from track 18 sectors 15-16 and never written back. writeSectors ($0843) has exactly two callers, $7E85 (film directory -> track 18 s2-3) and $8194 (film pages), so nothing in the game rewrites T18 s15-16.
- [game_setup_0200_0200] The $0200-$03FF page is time-multiplexed between at least five owners: (1) contourRuleTable + this start-up code, reloaded by loadSetupBlock ($10BB, once at boot) and loadMapAndSetup_10AB ($10AB, from $0B35 before every game session); (2) BAM buffer for checkDiskId ($0FB1, uses $02A2/$02A3/$02AB); (3) the RLE sprite-shape buffer written by unpackRleTo0200 and read at $CD15 / by loadDirectionShapeSprite; (4) overlay A's map-generator scratch variables at $0300-$032E (hillSiteCount, riverCol, mapStyleFlags, ...), which overwrite the start-up code; (5) the film directory $0300-$04FF (track 18 s2-3) and, at game start, the unit-deployment tables that sub_EC91 (called from $7AE9) loads from track 29 sectors 6-8 into $0200-$04FF. The contour table is therefore only valid between $0B35 and $7AE9.
- [game_setup_0200_0200] Neighbour-mask bit order for the whole map generator, derived from the edge clamps at $7463-$7481: bit0=NW, bit1=N, bit2=NE, bit3=W, bit4=E, bit5=SW, bit6=S, bit7=SE (reading order, centre skipped). Column 0 ANDs with $D6, column 39 with $6B, row 0 with $F8, row 39 with $1F.
- [game_setup_0200_0200] $BFB0-$BFFF holds the 40+40 byte row address table of the $F000 map ($F000+40*row), built here at boot and used by readMapCell ($25FA) and every other map accessor. It sits in the 192 bytes of the VIC bank left over above the bitmap ($A000-$BF3F), so anything that clears the bitmap must stop at $BF40.
- [game_setup_0200_0200] There are two independent font systems. plotGlyph ($2AC8, main program) uses gameFont at $9426 (114 glyphs) with fontColorTable at $93B4. The $C000 overlay's plotter ($C1F0/$C24B) uses a 59-glyph set that exists twice: multicolour/double-width at $98BE and hires at $9A96 ($98BE+$1D8), selected by bit 4 of the $D016 shadow $9001. Only the second pair is inverted at start-up by $031B - on disk both are positives (1 = ink), at run time both are negatives.
- [game_setup_0200_0200] VIC bank 2 is selected here, not by the boot loader's title code: $035E sets CIA2_DDRA |= $03 and CIA2_PRA = $05 (bank 2 = $8000-$BFFF, plus the RS-232 TXD latch high), which is what makes bitmap $A000 / screen $8C00 / sprite data $8A00 work.
- [game_setup_0200_0200] The game RNG ($57-$59, stepped by nextGameRandom $FD5D) is seeded deterministically with $49/$19/$02 at every cold start, so an unmodified boot always produces the same first random sequence until the joystick/keyboard reseeding kicks in.
- [game_setup_0200_0200] Label hygiene: XREF.txt and the include lists of main_0800.s / ovl_6F00_A.s carry stale names for addresses in this unit - $0300 is called basicVectorHijack and $02B8/$02EC eaLoadGame/eaFileName (all from boot/ea.s, which loaded to $02A8-$030B during booting), and $0313 is called commandDispatchLoop (from the 1541 fast loader at $0300-$05FF in drive RAM). None of those meanings apply once the game is running; proposed names here are initGameSystems for $0300 and contourRuleTable for the $0200 page.

## game/trainerAiE000

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| E000 | commRequestEntry | Jump-table slot 0 of the communications module (JMP handleCommRequest). The game calls it every raster IRQ with X=0 ($1129), with X=1 to transmit the outgoing packet (sendOutgoingPacket $56AE), with X=2 to acknowledge a completed exchange ($4EFF/$526F) and with X=3 to cancel a pending send. In this build (T35 = SOLO TRAINER) there is no link at all: X=1 runs one turn of the computer opponent's AI and leaves its orders in the receive buffer. | X = request code 0..3 | see handleCommRequest ($E043) | high |
| E003 | commSetLinkStateEntry | Jump-table slot 1 (JMP setCommLinkState). Called by initCommModule ($0F46, X=1) and by hangUpModemSetState ($1B56/$1B70, X=3) to bring the pretend link up (X=0) or down (X>=1). | X = 0 open, >=1 close | isLinkActive/commCarrierStatus/commLinkReady ($E03B/$E03C/$E03D) | high |
| E006 | commGetReceivedByteEntry | Jump-table slot 2 (JMP reportNoReceivedByte). Called by receiveChatCharacter ($1399) to fetch one byte of the opponent's chat line. In the trainer build it always returns C=1 (nothing received), so the chat receiver never produces text. | none | C=1 always (no byte) | high |
| E009 | commSendByteEntry | Jump-table slot 3 (JMP commNoOpClc). Jumped to from $1396 to transmit one chat character. In the trainer build the byte is discarded and C=0 (accepted) is returned. | A = byte to send (ignored) | C=0 | high |
| E00C | commFrameTickEntry | Jump-table slot 4 (JMP commNoOpClc). Called once per raster IRQ from $112C; in the modem build this services the serial line, in the trainer build it does nothing. | none | C=0 | high |
| E012 | commKeyboardScanVector | JMP whose operand bytes $E013/$E014 are the module's keyboard-scan vector. initCommModule ($0F4B/$0F50) patches them to the game's scanKeyboard ($0DB7); until then the vector points at commNoOpClc so no key is ever reported. | none | whatever scanKeyboard returns (A=key, Y/X = shift/C= flags) | high |
| E015 | commReadKeyEntry | Jump-table slot 5 (JMP commKeyboardScanVector). Called from pollKeyboardEvent ($0E56) every frame; it is the only route by which the game reads the keyboard, so the comm module can steal the keyboard while it owns the CPU. | none | key code in A, shift/C= flags in Y/X | high |
| E043 | handleCommRequest | Body of commRequestEntry. X=1 jumps to buildTrainerReplyPacket ($E082) which runs the computer opponent. X=3 clears outgoingPacketLength ($E01F). Every other value (X=0 per-frame poll, X=2 packet acknowledge) sets packetExchangeStatus ($E01D) = $C0 - 'own packet sent AND opponent packet received' - and copies commSuspendRequest ($E039) into commSuspendAck ($E03A). Because $E01D is always $C0 the lock-step exchange in processCommandExchange never waits, and suspend/resume around disk access ($0F5F/$0F71) succeeds immediately. | X = request code | $E01D = $C0, $E03A = $E039, or $E01F = 0 | high |
| E060 | setCommLinkState | Body of commSetLinkStateEntry. X=0 stores $FF into commLinkReady ($E03D), commCarrierStatus ($E03C) and isLinkActive ($E03B); X>=1 stores 0 in all three and also clears outgoingPacketLength ($E01F). Since $E03C is $FF the game's carrier tests (bit 6 in pollMenuSession, bit 7 in hangUpModemSetState) always see a healthy link. | X | $E03B, $E03C, $E03D, $E01F | high |
| E075 | reportNoReceivedByte | SEC + RTS: 'no character available' answer of commGetReceivedByteEntry in the trainer build. | none | C=1 | high |
| E077 | commNoOpClc | CLC + RTS shared no-op used as the body of commSendByteEntry, commFrameTickEntry and the unpatched keyboard vector. $E078 is the bare RTS. | none | C=0 | high |
| E079 | runTrainerRoundHook | Once-per-round hook called from finishRound ($4654) after the game clock has been decremented. Unless the drone sub-overlay is resident (trainerDroneTimer $8977 bit 7 set) it runs planTrainerRound ($E170), the top-level 'think' routine of the computer opponent. | D_8977 | see planTrainerRound | high |
| E082 | buildTrainerReplyPacket | Reached with X=1 from sendOutgoingPacket, i.e. once per lock-step exchange right after the human's packet was handed over. Clears receivedPacketLength ($E01E) and returns at once during the setup phase (zp_B1), film playback ($0B9B bit 7), after the game ended ($0BA8) or while paused (zp_BE). Otherwise it manages the swappable $EE00 sub-overlay: while the opponent's drone is in the air (droneStateFlags $920D bit 0) it loads track 29 s4-5 to $EE00 (the drone-flight AI) and calls $EE00 with $8977 = $FF; when the drone is gone it reloads track 35 s14-15 to $EE00 (the native tail), sets $8976 = $FF and $8977 = 3. In the normal case it counts $8977 down and calls flushTrainerCommandQueue ($EA7B), which moves up to 7 bytes of queued AI orders into the received-packet buffer $E020. | zp_B1, $0B9B, $0BA8, zp_BE, $920D, $8977 | $E01E/$E020 (the 'received' packet), $8976, $8977, $EE00-$EFFF possibly reloaded from disk | high |
| E0E8 | trainerUnitStepHook | Per-unit hook called at the end of processUnitStep ($4526) for currentUnit ($90FA). Dead units are skipped. For one of the AI's own units (index >= 50) that has scored both territory zones (unitTypeTable bits 3 and 4) it sets bit 7 of trainerUnitPenetrated ($8864 + ordinal) so the planner stops counting it as 'still at home'; for the AI comcen (unit 99) it additionally samples bit 1 of unitLocalFlags[99] ($FBB7, set by recordUnitHit) into trainerComcenHit ($897B) and the one-round-sticky trainerComcenHitRecent ($897C). All other units fall through to trainerUnitSpottedHook ($EACF), which tracks what the AI has seen. | $90FA, $F76C, $FBB7, $8977 | $8864+ordinal bit 7, $897B, $897C; otherwise calls $EACF | medium |
| E128 | setSubOverlayLoadDest | Helper: X=$00, Y=$EE, JMP setLoadDest ($085C) - points the fast loader at $EE00 before the two sub-overlay loads in buildTrainerReplyPacket. | none | loader destination = $EE00 | high |
| E12F | pickTrainerRallyPoint | Chooses the AI's current objective: a random cell near the human comcen (unitCol[49] = $F671, unitRow[49] = $F6D5) via randomiseCoordinate, stored in trainerRallyCol/Row ($8969/$896A). Called every six rounds (or immediately once the human comcen has been spotted) from planTrainerRound, and once at game start by the trainer setup overlay ($EC47). | $F671, $F6D5, RNG | $8969, $896A | high |
| E14A | randomiseCoordinate | Returns A = base coordinate plus a random offset, clamped to 0..39. The base is stored into the immediate operand at $E163+1 and the offset comes from randomCoordinateOffsetTable ($E142) indexed by nextGameRandom AND 7. The offset is used raw when the human comcen is visible (bit 7 of unitEnergy[49] = $F9F5, the 'spotted' bit), doubled when trainerEnemyComcenSeen ($896C) bit 7 is set and quadrupled otherwise - so the AI aims accurately at a comcen it can see and wanders when it cannot. $E14D is the continuation that reuses the last base. | A = base coordinate, $F9F5, $896C, RNG | A = coordinate 0..39; self-modifies $E164 | high |
| E170 | planTrainerRound | Top-level planner of the solo trainer, run once per game round. Returns at once if the game is over ($0BA8). Resets trainerOrdersThisRound ($8968), refreshes the rally point every 6 rounds (or immediately when the human comcen is visible), sets trainerActionBudget ($8965) from the AI comcen's energy ($FA27 AND $3F) and trainerComcenDamage ($8971) from its damage nibble ($F9C3 AND $0F). With the DAMAGE option off ($0BA3 bit 4 clear) the budget is a flat 50; a fully damaged comcen ($0F) makes the AI do nothing this round; 1-7 damage costs 16 budget points. A budget below 20 diverts to the repair/energy routine ($EB67). Otherwise it evaluates the threat around its own comcen with scanEnemyThreat ($EBEF) into trainerBestDir/trainerEnemyProximity/trainerThreatScore ($8960/$8961/$8962), sets trainerDefendMode ($8963) to $FF (defend) when enemies are close and either no assault group exists or the comcen was just hit, else 0 (attack, and then considers launching a drone via $EEE0). It then runs scanTrainerUnitsForOrders ($E2BD), and while fewer than 5 orders have been issued picks/refreshes the assault group ($E83C) and finishes with pushTrainerComcenForward ($E21F, attack) or retreatTrainerComcen ($E266, defend). | $0BA8, $F9F5, $FA27, $F9C3, $0BA3, $91CB, RNG, group tables at $8800-$897C | $8965, $8968, $896B, $896C, $8960-$8963, orders queued at $88A0, plus everything the callees change | medium |
| E21F | pushTrainerComcenForward | Attack-mode tail of planTrainerRound (branch target, not a JSR entry). If an assault group exists ($895D), the comcen is undamaged and its own replan clock ($8831) has come due, it queues a move order for the AI comcen (ordinal 49) one step further along trainerBestDir ($8960) from the assault group's average position ($895E/$895F), and re-arms $8831 to gameClock - 3. This is how the trainer walks its comcen up behind its attack. | $895D, $8971, $8831, $91CB, $8960, $8961, $895E, $895F | $8831, one $80/$81 order queued through queueTrainerMoveOrder ($E919) | medium |
| E266 | retreatTrainerComcen | Defend-mode tail of planTrainerRound (branch target). Records the current clock in trainerAlertClock ($896E), and unless the comcen is damaged queues a move order that walks the AI comcen away along trainerBestDir ($8960) by trainerEnemyProximity ($8961) cells. If the threat score ($8962) then still exceeds trainerRecallThreshold ($897A) - or the comcen was damaged - it recomputes the assault group's statistics and jumps to aimGroupAtOwnComcen ($E339) to pull the group back home. | $8971, $8960, $8961, $8962, $897A, $895D, $F6A3, $F707 | $896E, $895A = 0, one comcen move order, possibly a full group recall | medium |
| E2BD | scanTrainerUnitsForOrders | Walks the AI's 50 unit slots from ordinal 49 down to 0 using the embedded loop counter at $E2BC, stopping as soon as 4 orders have been queued this round ($8968). A dead slot ($F79E bit 7) gets its replan clock zeroed and its group set to $FF. A slot whose replan clock ($8800+ordinal) has been reached by the down-counting game clock ($91CB) is handed to decideTrainerGroupAction. The default replan urgency ($896D) is reloaded with 5 before each unit. | $8968, $91CB, $F79E, $8800, $8832 | $E2BC, $8800, $8832, $896D; orders queued by the callee | high |
| E2F1 | decideTrainerGroupAction | Decides what the AI group owning unit ordinal Y should do. Group id 0 is the comcen's own group: it is only handled when the comcen is undamaged and no assault group is selected, in which case the group target ($88D2/$88DE entry 0) is set to the comcen's own destination. For any group it caches the group flags ($893E) in the scratch byte $8966, masks them down to the low nibble, and calls computeTrainerGroupStats; an empty group aborts. It then branches into the four behaviours: head for the AI comcen (home group $8964, or the assault group when defending), advance toward the rally point ($8969/$896A) when the group has already penetrated, fall back to row 29 of the AI's half when it has not, or - for group 0 - run the free direction search from the comcen position. | Y = AI unit ordinal, $8832, $8971, $895D, $F833, $F897, $8964, $8966, zp_1D | $88D2/$88DE, $893E, $896D, $8955-$8963 group scratch; ends in one of the order-issuing tails | medium |
| E339 | aimGroupAtOwnComcen | Internal tail (JMP target from $E2B3 and fall-through from $E336): sets the line-stepper target zp_95/zp_94 to the AI comcen's cell ($F6A3/$F707), copies the group centre (zp_93/zp_92) into the search origin zp_27/zp_28 and the target into zp_22/zp_24, then falls into issueTrainerGroupOrders. Used to recall a group to defend the comcen. | $F6A3, $F707, zp_92/zp_93 (group centre from computeTrainerGroupStats) | zp_22/zp_24 target cell, zp_27/zp_28 origin | high |
| E3D0 | aimGroupAtHomeLine | Internal branch target used when the group still has members that have not reached the enemy end zone (zp_1D non-zero) and the group's 'committed' flag (bit 6 of $893E) is clear: the target column stays where the group is and the target row becomes 29 plus the group's row extents, i.e. a defensive line inside the AI's own half. Once the alert clock ($896E) has been set, or the group is weak, the group's combat value ($8956/$8957) is capped at 9 before the search continues at $E400. | zp_93, $891A, $8932, $896E, $91CB, $8956/$8957 | zp_94/zp_95 target, $8956/$8957 possibly capped | medium |
| E42A | findTrainerAdvanceCell | Threat-aware path search. First builds the per-unit threat map (buildThreatWeights $E773) into the $0400/$0464 scratch pages. Then, for each of the 8 direction ranks in the preference list at $8896 (built by buildDirectionPreference $E7E3), it steps outward from (zp_27,zp_28) in steps of 2 cells, testing every candidate with evaluateCellSafety ($E69C); it keeps extending while the cell is safe, up to the desired advance distance ($895B) or the per-rank cap in maxAdvanceByRankTable ($E664), and moves to the next rank when a cell is rejected at distance 0. If nothing works the group stays put (direction 0, distance 0). The winning direction/distance are converted into the absolute target cell zp_22/zp_24 through directionColDeltaTable/directionRowDeltaTable ($E68A/$E693) and addStepsClamped ($EA1B). | zp_27/zp_28 origin, $8896 direction preference, $895B, $E664, $8956/$8957 group strength | zp_22/zp_24 = chosen target cell, zp_23 = distance, zp_26 = direction, $0400/$0464 threat map | medium |
| E49E | issueTrainerGroupOrders | Issues the move orders for the current group ($8955). Marks every member's replan clock with the current game clock, clamps the target with clampGroupTargetToMap (which also yields the delta zp_2A/zp_2B from the group's average destination), derives the replan delay zp_29 from the distance class ($E82A, doubled when the group contains slow units - $895A) with a floor of 4 and a cap of the urgency $896D, and then walks all 50 slots: every member of the group gets a move order to its own destination plus the delta through queueTrainerMoveOrder ($E919) and a new replan clock of gameClock - zp_29. Members that also belong to a real in-game group (unitGroupByte bit 6) propagate that clock to all their in-game group mates so the AI does not re-order the same formation twice. | $8955, zp_22/zp_24 target, $91CB, $895A, $896D, $8832, $F802/$F866, $F73A | $8800 replan clocks, $88D2/$88DE group target, orders appended to the queue at $88A0, zp_29/zp_2A/zp_2B | medium |
| E531 | computeTrainerGroupStats | Gathers the statistics of AI group Y (stored in $8955). Scans the 50 AI slots: sums the per-type combat weights from unitCombatValueTable ($E2B7) into the 16-bit trainerGroupValue ($8956/$8957), sets trainerGroupHasSlowUnits ($895A) when a GRUNT (type 0) or BOOMER (type 2) is present, counts members that have not yet penetrated into zp_1D, and builds the bounding box of the members' ordered destinations in zp_22/zp_24 (columns) and zp_23/zp_25 (rows). Then averageGroupPosition ($E882) gives the member count and the average current and ordered positions, which are stored as group centre ($88EA/$88F6), group size ($8902) and the four box offsets ($890E left, $8926 right, $891A top, $8932 bottom). Returns C=1 for an empty group. | Y = group id, $8832, $F79E, $F802, $F866, $8864, $895D, $8895 | $8955, $8956/$8957, $895A, $8902, $88EA/$88F6, $890E/$891A/$8926/$8932, zp_1D, zp_92/zp_93 = group centre, zp_1E/zp_1F = average destination; C=1 if empty | high |
| E5FF | clampGroupTargetToMap | Clamps the group target in zp_22/zp_24 so that the whole formation box ($890E/$8926 columns, $891A/$8932 rows) still fits inside the 40x40 map, stores the clamped target as the group's target cell ($88D2/$88DE) and returns the movement delta relative to the group's average ordered position (zp_1E/zp_1F) in zp_2A (columns, also copied to $8958) and zp_2B (rows). Also called by the trainer setup overlay at $EF00. | zp_22/zp_24 target, zp_1E/zp_1F average destination, $8955, box offsets | zp_22/zp_24 clamped, zp_2A/zp_2B delta, $8958, $88D2/$88DE | high |
| E66C | offsetPointInDirection | Computes the cell that lies Y steps in compass direction X from the point (zp_27 column, zp_28 row), using directionColDeltaTable/directionRowDeltaTable ($E68A/$E693) and addStepsClamped ($EA1B). Returns C=1 when the column leaves the map (the row is then not computed); otherwise zp_18 = column, zp_19 = row. Used by the path search ($E44D), by evaluateCellSafety ($E69C) and by the drone target scan ($EF9F). Self-modifies the LDX operand at $E67D. | X = direction 0..7 (0 = north, clockwise), Y = number of steps, zp_27/zp_28 = origin | zp_18/zp_19 = target cell, C=1 if off the map | high |
| E69C | isGroupDestinationTooRisky | Risk test for a candidate destination of the AI group in currentAiGroup ($8955). Computes the cell 'distance Y in direction X' from (zp_27,zp_28) with computeCellInDirection, expands it by the group's bounding box (groupSpanLeft/Up/Right/Down) into a rectangle clamped to 0..39, then walks all 100 units and sums unitThreatWeight ($0464) of every unit whose square influence area (unitThreatRadius $0400) overlaps that rectangle: plus for the human player's units 0-49, minus for the AI's own units 50-99 (own group members were already zeroed by buildUnitThreatTables). The tail at $E743 rejects a negative or zero total, otherwise subtracts the group's own strength ($8956/$8957) and returns carry. | X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row, $8955 currentAiGroup, $8954 aiGroupCount, group span tables $890E/$891A/$8926/$8932, $0400/$0464 threat tables (built by buildUnitThreatTables), $8956/$8957 group strength, $896E, $91CB | C=1 -> reject (off map, or enemy threat >= group strength; exact ties broken with nextGameRandom); C=0 -> acceptable. zp_18/zp_19 = candidate column/row, zp_1A/zp_1B = signed 16-bit threat total, zp_1C clobbered; self-modifies the operands at $E704,$E70B,$E715,$E71C | medium |
| E773 | buildUnitThreatTables | Rebuilds the two 100-entry influence-map tables used by isGroupDestinationTooRisky. For each unit 99..0 it stores an influence radius in $0400 and a strength weight in $0464. Dead units, spies/comcens (type >= 3) and AI units belonging to the group currently being moved get radius 0 and weight 0; grunts/riders get base 3, boomers base 6, each plus terrainSightBonusTable of the terrain the unit stands on. The weight comes from unitTypeStrengthTable ($E2B7) = 2,2,3,0,0. | $8955 currentAiGroup, aiUnitGroup table ($8800+unit), unitType $F76C, unitUnderCell $FAF0, terrainClassTable $077E, terrainSightBonusTable $35BB, unitTypeStrengthTable $E2B7 | $0400[0..99] = threat radius, $0464[0..99] = threat weight; zp_18, zp_19 clobbered | high |
| E7B9 | multiplyAbyY | 8-bit multiply by repeated addition: returns A = A * Y (mod 256), with A = 0 when Y = 0. The multiplicand is stashed in the self-modified operand at $E7CB and Y is restored from the operand at $E7D0 before returning. Only used by offsetCoordinateClamped, where A is a step delta of 0, +1 or $FF and Y a cell distance. | A = multiplicand, Y = count | A = A*Y; Y preserved; X, flags clobbered; self-modifies $E7CB and $E7D0 | high |
| E7D3 | buildDirectionFanTowardTarget | Works out the compass direction from the line start (zp_93,zp_92) to the line end (zp_95,zp_94) with getStepDirection, saves it in zp_19 and, when a direction was found, falls into buildDirectionFan to lay out the eight candidate move directions around it. Called from the group-movement code at $E416. | zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column | zp_19 = heading 0-7 ($FF and no fan built when start == target); $8896-$889D = candidate directions; clobbers A/X/Y, zp_18, $8C-$97 via initLineStepper/stepLine | medium |
| E7E3 | buildDirectionFan | Fills the eight-byte scratch buffer $8896-$889D with candidate move directions: entry i = (dirFanOffsetTable[i] + heading + 8) AND 7, where the offset is mirrored through dirMirrorTable ($E7DB, (8-x) AND 7) when bit 0 of the group's groupFlags is set, so left-handed and right-handed groups fan out the opposite way. The list is consumed by the move search at $E431-$E47E, whose per-slot minimum-distance thresholds are in $E664. | zp_19 = heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable | $8896-$889D = 8 candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered | medium |
| E80E | getStepDirection | Runs initLineStepper + one stepLine between (zp_93,zp_92) and (zp_95,zp_94) and converts the resulting per-step deltas ($90F5 column, $90F4 row) into one of the eight compass directions by searching dirToColDelta/dirToRowDelta backwards. | zp_92/zp_93 start row/col, zp_94/zp_95 end row/col | Y = direction 0-7, or $FF when the deltas match no entry (start == end); A = last compared delta; line-stepper state $8C-$97, $90F4/$90F5 updated | high |
| E82A | cellDistance | Integer distance between two map cells: calls distanceSquared ($3870) for /dcol/^2+/drow/^2 and converts it back with squaresTable, returning the largest n with n*n <= dsq. Saturated squared distances return 16. | zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row | A = X = distance 0-16 | high |
| E83C | updateFocusGroupCentre | Keeps focusGroupId ($895D) and its centre ($895E/$895F) up to date; the AI comcen escorts this group. When the id has bit 7 set the centre falls back to the AI comcen's own cell and the group is re-enabled if its groupFlags bit 7 has cleared; an enabled group whose groupFlags bit 7 is set is disabled by ORing $80 into the id; otherwise the centroid of the group's members 50-95 is recomputed with computeGroupCentroid and the group is dropped ($895D = $FF) when it has no members left. | $895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707 | $895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered | medium |
| E882 | computeGroupCentroid | Scans AI units (index 50+Y down to 50) and averages the position and the ordered destination of every living member whose aiUnitGroup entry equals A. Four 16-bit sums are accumulated in zp_18-zp_21 and divided by the member count with divideSigned16by8. | A = group id (patched into the compare at $E89D), Y = highest AI-relative unit index to scan (usually $31 or $2D); $8832 aiUnitGroup, $F79E unitType, $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow | A = member count (Z set when none), X = average column, Y = average row, zp_1E = average destination column, zp_1F = average destination row, zp_1C = member count; zp_18-zp_21 clobbered; self-modifies $E89D, $E913, $E915 | high |
| E919 | orderAiUnitToCell | Central 'issue a move order' helper of the trainer AI. Returns immediately when the unit's ordered destination ($F802/$F866 masked to 6 bits) already is the requested cell; otherwise it mirrors both coordinates with mirrorMapCoordinate (because the engine treats them as an opponent packet and mirrors them back) and queues command $80 MOVE, or $81 MOVE GROUP when the unit's unitFlags bit 6 marks it as a group member. | X = target column, A = target row, Y = AI-relative unit index 0-49 (absolute unit = Y+50) | $8896 = $80/$81, $8897 = absolute unit index, $8898/$8899 = mirrored column/row, then queueAiCommand; nothing when the order is redundant | high |
| E937 | queueMirroredMoveOrder | Second half of orderAiUnitToCell, only reached by falling through in this build (the XREF entries $E917/$EA01 belong to the T34 build): mirrors $8898/$8899, chooses command $80 or $81 from unitFlags bit 6, writes the unit index and tail-jumps into queueAiCommand. | $8898/$8899 = target column/row, Y = AI-relative unit index, $F73A unitFlags | command staged in $8896-$8899 and queued | high |
| E95D | isPathToPlayerComcenClear | Walks a Bresenham line from the current position (zp_93,zp_92) to the human player's comcen (unitCol[49]/unitRow[49]) and reports whether any living enemy combat unit (index < 50, type 0-2) stands on it. Used at $E3B1 to decide whether the group can make a straight run at the comcen. Both exits leave (zp_93,zp_92) restored to the group's centre through loadGroupCentreIntoLinePos. | zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player comcen position, map at $F000 | C=1 = line reaches the comcen unobstructed, C=0 = blocked or invalid group; zp_92/zp_93 = group centre; zp_48/zp_49 and the line-stepper state clobbered | medium |
| E997 | loadGroupCentreIntoLinePos | Copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6) into the line-walker position zp_93/zp_92 and returns with C=1. Used as both a subroutine and the C=1 exit path of isPathToPlayerComcenClear. | $8955 currentAiGroup, $88EA/$88F6 | zp_93 = column, zp_92 = row, C=1, A/Y clobbered | high |
| E9A6 | scoreTargetBlock | Scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment target: every unit standing in the four cells contributes targetValueByUnitType (grunt 1, rider 2, boomer 4, spy 1, comcen 4), positive for the human player's units and negative for the AI's own. A block with a negative total is rejected; otherwise, if the total beats the running best in zp_1C, the corner is stored in zp_95/zp_94 and the highest player unit index seen in the block (which favours the comcen, unit 49) becomes the chosen target in zp_1D. Called from the missile/drone target search at $EFA8. | X = column, Y = row of the top-left cell, zp_1C = best score so far (threshold), zp_1D = best unit so far, map at $F000, $F76C unitType | zp_1A = block score, zp_1B = highest player unit index in the block; on improvement zp_1C, zp_1D, zp_94, zp_95 updated; zp_48/zp_49 = map pointer; self-modifies $E9E4 and $E9E8 | medium |
| E9F6 | addUnitTargetValue | Helper of scoreTargetBlock: takes a map cell byte holding $80/unitIndex, ignores dead units, and adds targetValueByUnitType[type] to zp_1A, negated for the AI's own units 50-99. For a player unit it also keeps the largest index seen in zp_1B. | A = map cell byte with bit 7 set, $F76C unitType, $E9F1 targetValueByUnitType | zp_1A += weight, zp_1B = max player unit index; X, Y clobbered | high |
| EA1B | offsetCoordinateClamped | Returns clamp(A + X*Y, 0..39) - the base coordinate A displaced by Y steps of the signed delta X - and reports through the carry whether the unclamped result left the 0..39 map range. | A = base coordinate, X = signed step (0, +1 or $FF), Y = number of steps | A = clamped coordinate 0-39, C=1 when the raw value was outside 0..39, Y preserved; self-modifies $EA27 and $EA36 | high |
| EA38 | mirrorBlockCornerForOpponent | Converts the top-left corner (X,Y) of a 2x2 object into the coordinates that the engine's mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn back into (X,Y): X = 39-(X+1), Y = 39-(Y+1). Used when the AI writes a drone position into the fake incoming packet at $EEB1. | X = column, Y = row of the block's top-left cell | X, Y = mirrored coordinates; A clobbered | high |
| EA46 | queueAiCommand | Appends the command staged in $8896.. to the AI's outgoing queue at $88A0. Bumps aiCommandsThisRound ($8968), looks the command length up with getCommandLength, records a high-water mark in aiCmdQueuePeak ($EA45) and, if the queue would grow to 50 bytes or more, drops the command and sets the border-colour shadow $9003 to 6 as a visible overflow warning instead. | $8896-$8899 = staged command, $8952 aiCmdQueueLength, commandLengthTable via getCommandLength | $88A0 queue extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the copy counter), $EA45 peak; on overflow $9003 = 6 | high |
| EA7B | injectAiCommandsIntoPacket | The heart of the solo trainer: instead of receiving a packet over the modem, the AI copies whole commands out of its own queue at $88A0 straight into the comm module's received-packet buffer $E020/$E01E, so the lock-step engine executes them as if the opponent had sent them. It first calls sub_ED0B (drone/missile logic), stops if that set bit 7 of $8966, and then moves as many complete commands as fit while the packet stays below 8 bytes, shifting the remainder of the queue down each time. Called every exchange from $E0E4. | $8952 aiCmdQueueLength, $88A0 queue, $E01E current packet length, getCommandLength | $E020.. receive buffer filled, $E01E updated, queue compacted, $8952 reduced, $8966 = 0 (then possibly set by sub_ED0B), $92EE clobbered | high |
| EACF | updateAiUnitTracking | Per-unit hook run from sub_E0E8 after the engine steps each unit. For a human unit (index < 50) it latches the first grunt/rider/boomer whose chosen fire target ($91F0) is the AI comcen (unit 99) into comcenAttackerUnit ($8972). For an AI unit it flags the unit's group as having contact with the player's comcen (groupFlags bit 6, plus a game-clock stamp) when that comcen has been spotted and the unit is within 8 cells of it; finally, for the spy units 96-98 that have reached the opponent's end zone and have no group yet, it allocates a fresh group id ($8953) and seeds that group's target from the unit's own destination. | $90FA currentUnit, $F76C unitType, $91F0 chosenTargetUnit, $F9F5 player comcen energy/spotted bits, $F671/$F6D5 player comcen position, $F640/$F6A4 positions, $8800 aiUnitOrderRound, $8832 aiUnitGroup, $8864 aiUnitDeepFlag, $91CB roundsRemaining, $8954, $8964 | $8972, $893E groupFlags, $8953 lastGroupId, aiUnitGroup entry, $87CE+unit round stamp (= aiUnitOrderRound), $88D2/$88DE group target | medium |
| EB67 | runAiComcenActions | Spends the AI comcen's remaining command budget for the round (3 minus aiCommandsThisRound). When the comcen is stunned it needs two slots: it halts the comcen and, with a 1-in-8 chance, queues command $9E (comcen repaired). Otherwise it halts the comcen once and then calls maybeQueueComcenEnergyRepair for every remaining slot. Entered from $E1CD and $E21B. | $8968 aiCommandsThisRound, $8971 comcenStunCounter | $8967 comcenActionBudget, commands queued through queueAiCommand; returns silently when the budget is exhausted | medium |
| EBA5 | maybeQueueComcenEnergyRepair | With 50 percent probability, and only while the AI comcen's energy is below 45 of 50, queues command $9A (ADD ENERGY) for unit 99 with an amount of 1 + (50-energy)/8, occasionally bumped by one so that the running total stays aligned to 4. This is how the solo trainer heals its own command centre; the projected energy is accumulated in $8965. | $FA27 = unitEnergy[99] (bits 0-5), $8965 projected comcen energy, nextGameRandom | $8896 = $9A, $8897 = $63, $8898 = amount, command queued; $8965 increased | medium |
| EBDD | queueComcenCommand | Tail entry shared by runAiComcenActions and maybeQueueComcenEnergyRepair: stores A as the command byte in $8896 and jumps into queueAiCommand, leaving $8897/$8898 as previously prepared. | A = command byte, $8897/$8898 = parameters already staged | command appended to the AI queue | high |
| EBE4 | haltAiComcen | Orders the AI comcen (unit 99) to move to the cell it already occupies, which orderAiUnitToCell turns into a MOVE command only when the comcen currently has a different destination - i.e. it stops the comcen so that it can repair or launch. Called before the repair and drone actions in runAiComcenActions. | $F6A3 unitCol[99], $F707 unitRow[99] | possibly a $80/$81 command queued; nothing when the comcen is already halted | medium |
| EBEF | assessThreatAroundAiComcen | Builds a directional threat map around the AI comcen and picks an escape direction. For each of the human player's units 0-45 within 20 cells it adds a weight (1 for being close, 4 more when the comcen is inside the unit's weapon range of 5, or 9 for boomers, plus 1 when the comcen is standing still) into one of eight direction buckets in $8896-$889D. The tail (past the end of this chunk, $EC82-$ED0A) copies the buckets to $0400, smooths them circularly as (2*b[i]+b[i-1]+b[i+1])/4 and picks the centre of the least threatening three-direction window, breaking ties on the RNG state. | zp_27/zp_28 = AI comcen column/row, $F76C/$F640/$F6A4 player unit records, $897C comcenIsMoving, zp_57 RNG state | A = (total threat - threat count)/2, X = deepest weapon-range penetration (0 = nothing in range), Y = safest direction 0-7 or 8 when no threats; $8896-$889D and $0400-$0407 = direction buckets, zp_18-zp_23 clobbered. The caller at $E1DB stores them into $8962/$8961/$8960 | medium |
| EC82 | rankThreatDirections | Second half of sub_EBEF (the routine starts at $EBEF, outside this chunk). sub_EBEF has just scanned the human player's mobile units 0-45 and accumulated a saturating 'enemy pressure' weight per compass direction into the 8 bytes $8896-$889D, the total into zp_1F, the number of threatening units into zp_20 and the largest weapon-range surplus into zp_21. This tail copies the 8-direction ring to the $0400 scratch page, smooths it (sub-loop at $EC91), searches the quietest 3-direction arc (sub-loop at $ECD4) and returns the summary. If zp_20 was 0 the caller path at $EC7C returns A=0, X=0, Y=8 instead (index 8 of the direction tables $E68A/$E693 is the null delta 0,0). | zp_1F total threat weight, zp_20 threatening unit count, zp_21 max range surplus, $8896-$889D per-direction weights, zp_57 (game RNG low byte) for the tie break | A = (zp_1F - zp_20) / 2 = overall threat level; Y = zp_18 = index 0-7 of the safest (lowest-pressure) compass direction, or 8 when nothing threatens; X = zp_21; $0400-$0407 and $8896-$889D overwritten; zp_18, zp_19, zp_1A, zp_23 clobbered | high |
| EC91 | smoothThreatRing | Inner loop of rankThreatDirections (branch target of the DEC zp_23 / BPL at $ECCA, not a JSR target in this build). For each direction i = 7..0 it computes (2*ring[i] + ring[i-1] + ring[i+1]) / 4 with a 2-bit carry kept in zp_19, reading the unsmoothed copy from $0400,i and writing the result back to $8896,i. Indices wrap modulo 8, so it is a circular 1-2-1 blur of the 8-direction threat ring. | $0400-$0407 = unsmoothed direction weights, zp_23 = current direction index | $8896-$889D = smoothed weights; zp_19 carry scratch; zp_23 counts down to $FF | high |
| ECD4 | findQuietestDirectionArc | Second inner loop of rankThreatDirections (branch target of $ECFE). For each y = 7..0 it sums the smoothed weights of the three adjacent directions y, y-1, y-2 (wrapping) and keeps the smallest sum in zp_19 with the centre direction (y-1) in zp_18, so the result is the middle of the calmest 135-degree arc. Ties are broken pseudo-randomly by BIT zp_57 / BMI at $ECF3 (zp_57 is the low byte of the lock-step game RNG state $57-$59). The three-term sum is 8-bit and can wrap if all weights are large. | $8896-$889D smoothed weights, zp_57 | zp_18 = quietest direction 0-7, zp_19 = its arc sum; zp_1A scratch | high |
| ED0B | runAiMissileTurn | The computer opponent's missile/drone decision, called once per command exchange from sub_EA7B ($EA80), which is itself reached from the packet-transmit entry sub_E000 (X=1) at $E082. It first offers to launch the AI's own drone (sub_EE85), then, if the human player currently has a drone in the air ($920D bit 6) and the AI still has missiles ($92B1), decides whether to shoot at that drone: the drone cell is $92F6+6 / $92F7+5, the aim point goes into zp_22/zp_24 and a reluctance score is built in zp_23 from the range to the AI comcen, the comcen's stun state ($F9C3 & $0F), its energy bits and a missile-conservation test against the human's remaining drones ($92AE). A shot fires either as a clean kill (command $92 written at $E020, i.e. 'drone finished') or as a near miss whose impact point is scattered by +/-(zp_23/2+1) cells with sub_EE55. It then falls through into the code at $EDD5/$EE00 that fires at an ordinary spotted unit. | $8966 action budget, $920D droneStateFlags, $92B1 AI missiles left, $92AE human drones left, $92F6/$92F7 drone view origin, $F6A3/$F707 AI comcen col/row, $F9C3, $FA27, $8970 reload timer, $920B/$896F drone heading, game RNG | may append command $92 and/or $90 col,row to the fabricated incoming packet ($E020/$E01E), $8970 reload timer, $8966 decremented, $8968 += 2, zp_22/zp_23/zp_24/zp_29/zp_2B/zp_18 clobbered | medium |
| EDD5 | fireMissileAtSpottedUnit | Continuation of runAiMissileTurn (branch/JMP target only). When no player drone is being steered, the AI still has more missiles than twice the human's remaining drones, its reload timer $8970 has expired and its action budget $8966 is not spent, it fires a missile at the unit index held in $8972 (or, falling through at $EE00, at the human comcen unit $31 when that comcen is currently visible, $F9F5 bit 7). sub_EE6A checks that the target sits inside the AI comcen's 21x17 radar grid; the reload timer is then set to half the range and sub_EE2D queues the $90 strike. Ends by remembering the current drone heading in $896F and ticking $8970 down. | $920D, $92AE, $92B1, $8970, $8966, $8972 current missile target, $F76C unit type/alive flags, $F9F5, $920B | $8972 (set to $FF when no shot), $8970, $896F, packet bytes via sub_EE2D | medium |
| EE00 | targetEnemyComcenWithMissile | Fall-through label inside fireMissileAtSpottedUnit: if the human comcen (unit $31) is currently visible ($F9F5 bit 7) it becomes the missile target. IMPORTANT: although XREF lists 'sub_EE00 call:E0A5', the JSR at $E0A5 does NOT reach this code - $EE00-$EFFF is a swappable sub-overlay slot and $E0A5 is only executed while the track 29 s4-5 variant (disassembly/game/ovl_EE00_T29.s, the AI's drone-flying code) is loaded there. In this T35 build $EE00 is never entered from outside. | $F9F5 = energy/visibility byte of unit 49 (the human comcen) | X = $31 when the comcen is visible, otherwise branches away to $EE19 | high |
| EE2D | queueMissileStrikeCommand | Appends a 3-byte command $90 col,row (missile strike, handler cmdMissileStrike $53C0) to the fabricated incoming packet at $E020, length $E01E. Both coordinates go through mirrorMapCoordinate ($3FA3, 39-v) because the packet is presented to the engine as if it had arrived from the remote player, and the command dispatcher un-mirrors remote parameters. Also spends one AI action ($8966--) and charges two units of packet cost ($8968 += 2). | zp_22 = strike column, zp_24 = strike row, $E01E current packet length | $E020+ packet bytes, $E01E advanced by 3, $8966 decremented, $8968 incremented twice, X clobbered | high |
| EE55 | randomlySignValue | Returns A or -A with equal probability, using bit 0 of nextGameRandom ($FD5D). Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss the player's drone. | A = magnitude | A = zp_18 = +/-A (two's complement), game RNG advanced | high |
| EE6A | isUnitOnAiRadar | Loads unit X's map position into zp_22/zp_24 and tests whether that cell lies inside the 21x17 radar grid centred on the AI's comcen: it temporarily forces comcenUnit $9236 to $63 (unit 99 = side 1 comcen), calls overlay B's mapCellToRadarGrid ($7A13) and restores $9236 to $31 (unit 49 = the local player's comcen). The unconditional restore to $31 shows that this module always assumes the local player is side 0 and the computer opponent is side 1. | X = unit index, $F640/$F6A4 unit column/row arrays | zp_22 = column, zp_24 = row, C = 0 when the cell is on the AI's radar grid, C = 1 otherwise; $9236 left at $31; X/Y clobbered | high |
| EE85 | launchAiDrone | Called first thing by runAiMissileTurn. When the launch delay $8977 has run out, a drone target unit has been chosen ($8976 >= 0) and no drone is airborne ($920D bit 7 clear), it fabricates the opponent's drone launch directly into the incoming packet: $E020 = $97 (launch), $E021 = $8D (drone move) with heading, column and row parameters, $E01E = 5. The launch point is the AI comcen's cell converted with sub_EA38 (v+1 then mirrored = 38-v, pre-compensating the mirror-and-decrement the $8D handler applies to remote commands). The heading byte is whatever sub_E80E leaves in A, which is the row component of the direction step (-1/0/+1); cmdDroneMove turns it into sprite shape (heading+4)&7 = 3/4/5, i.e. the drone is always drawn flying towards the player - possibly deliberate, possibly a slip for the direction index that sub_E80E returns in Y. | $8977 launch delay, $8976 target unit, $920D, $F6A3/$F707 AI comcen position, $F640/$F6A4 target position | $E020-$E024 = $97,$8D,heading,col,row; $E01E = 5; $8974/$8975 = launch column/row; $8973 = $64 (100 drone steps); $8977 = 3; $8966 decremented; zp_92-zp_95 line-stepper state clobbered | medium |
| EEE0 | chooseAiDroneTarget | Called once per AI round from $E1FF. Decides what the computer opponent should send its next drone at and stores the chosen unit index in $8976. Returns immediately if the AI has no drones left ($92AF = 0) or a target is already pending. In game type 2 (THE BOMB) the only target is the human comcen, once it is visible or on the AI's radar. Otherwise, if the human comcen is visible it sums the weights $8902[i] of every objective flagged $20 (doubled) or $40 in $893E[i]; when that sum reaches 8 the AI commits to an all-out attack: it sets bit 6 in every objective flag, re-stamps the order clock of all 46 mobile AI units (the 46-byte array at $8800, indexed relative to unit 50) with the game clock $91CB, sets the launch delay $8977 = $15 and targets the comcen. If the comcen is not visible it falls into pickDroneTargetAlongThreatAxis. | $92AF AI drones left, $8976, $0BA3 game type, $F9F5 comcen visibility, $8902/$893E objective tables, $8953/$8954 objective count, $91CB game clock | $8976 = target unit index ($31 for the comcen), $893E[] bit 6 set, $8800-$882D = $91CB, $8977 = $15; zp_18, zp_19 clobbered | medium |
| EF59 | pickDroneTargetAlongThreatAxis | Tail of chooseAiDroneTarget, reached when the human comcen is not a viable drone target. It takes the AI's current objective ($895D) - its map cell from $88EA/$88F6 and its threat threshold from $8902, or the AI comcen and threshold 3 when no objective is active - and runs the threat scan sub_EBEF around it. If the measured pressure reaches the threshold and at least one enemy unit is in weapon range, it walks a ray in the direction opposite the safest one ((Y+4) & 7, i.e. straight into the enemy) at radii 4..14 with sub_E66C and scores each 2x2 block of cells with sub_E9A6 (unit values 1/2/4/1/4 for GRUNT/RIDER/BOOMER/SPY/COMCEN). The best cluster's unit index becomes the drone target $8976, and the acceptance threshold $8979 is reduced to 75% of its value so the AI grows less picky over the game. | $895D current objective, $88EA/$88F6 objective column/row, $8902 objective weights, $8979 target value threshold, $F6A3/$F707 AI comcen position | $8976 target unit index, $8979 scaled down, $8956 threshold copy, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius, zp_22/zp_24/zp_27/zp_28 clobbered | medium |

Data:

- `$E00F` **commRtsRtiStubs** (byteTable, 2 bytes): $60 (RTS) and $40 (RTI). $E010 is the module's default NMI handler, which is what the initial value $E010 of commNmiVector ($E031/$E032) points at.
- `$E018` **unusedJumpSlotE018** (unknown, 5 bytes): Five $00 bytes. The modem build (T34) has a sixth jump-table entry here; in the trainer build nothing calls $E018 and executing it would BRK.
- `$E01D` **commInterfaceBlock** (byteTable, 38 bytes): $E01D-$E042: the fixed interface block shared by both comm-module builds - packetExchangeStatus, receivedPacketLength/Buffer, outgoingPacketLength/Buffer, commBuildId, commNmiVector, cia2IcrMask, the suspend handshake and the link flags. See the variables list for the individual bytes; $E034-$E038 and $E03E-$E042 are only used by the modem build.
- `$E142` **randomCoordinateOffsetTable** (byteTable, 8 bytes): $01,$FF,$01,$01,$FF,$FF,$02,$FE - signed cell offsets picked with nextGameRandom AND 7 by randomiseCoordinate; scaled by 1, 2 or 4 depending on whether the human comcen has been spotted.
- `$E2B7` **unitCombatValueTable** (byteTable, 5 bytes): $02,$02,$03,$00,$00 - combat weight per unit type (0 GRUNT 2, 1 RIDER 2, 2 BOOMER 3, 3 SPY 0, 4 COMCEN 0). Summed into trainerGroupValue by computeTrainerGroupStats and copied into the per-unit weight page $0464 by buildThreatWeights ($E7AF).
- `$E2BC` **trainerUnitScanIndex** (byteTable, 1 bytes): Loop counter of scanTrainerUnitsForOrders, kept as a byte inside the code stream (written at $E2BF/$E2EB, read at $E2CE).
- `$E664` **maxAdvanceByRankTable** (byteTable, 8 bytes): $05,$05,$05,$03,$03,$01,$01,$01 - maximum number of cells the path search may advance for direction-preference rank 0..7: the three best headings may be followed up to 5 cells, the worst three only 1.
- `$E68A` **dirToColDelta** (byteTable, 9 bytes): Column delta of the 8 compass directions numbered clockwise from north: 0,+1,+1,+1,0,-1,-1,-1. The 9th entry ($E692) is 0 and pairs with dirToRowDelta[8] to form a harmless 'no direction' entry used when assessThreatAroundAiComcen finds no threats.
- `$E693` **dirToRowDelta** (byteTable, 9 bytes): Row delta of the same 8 directions: -1,-1,0,+1,+1,+1,0,-1, plus the 9th zero entry. Direction 0 = north, 2 = east, 4 = south, 6 = west.
- `$E7DB` **dirMirrorTable** (byteTable, 8 bytes): 00,07,06,05,04,03,02,01 = (8 - d) AND 7: mirrors a direction across the north-south axis. Used by buildDirectionFan to flip the candidate fan for groups whose groupFlags bit 0 is set. (The label L_E7E0 in the middle of the table is a T34 branch target, not a separate block.)
- `$E9F1` **targetValueByUnitType** (byteTable, 5 bytes): 01,02,04,01,04 - bombardment value of a unit by type (grunt 1, rider 2, boomer 4, spy 1, comcen 4), used by addUnitTargetValue.
- `$EA45` **aiCmdQueuePeak** (byteTable, 1 bytes): One byte of data embedded in the code stream between mirrorBlockCornerForOpponent and queueAiCommand: the high-water mark of the AI command queue length. Written and compared only inside queueAiCommand; no other code reads it.
- `$EFCD` **strayTextTail** (text, 3 bytes): $43,$45,$AE = 'CE' plus '.'|$80. Stale tail of the string 'INSERT DATA DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the T34 build of this overlay slot; here the same bytes are preceded by code, so only the last three characters survive. Unreferenced.
- `$EFD0` **msgDatagamecustomOff2** (text, 18 bytes): 'DATAGAMECUSTOM OFF' with bit 7 on the final F. Byte-identical to the same block in ovl_E000_T34, ovl_EC00_T29 and ovl_EE00_T29 - mastering-tool filler at the tail of every $EC00/$EE00 overlay build. No reference anywhere in the disassembly.
- `$EFE2` **msgDestroyOff2** (text, 11 bytes): 'DESTROY OFF' with bit 7 on the final F. Same shared filler block; unreferenced.
- `$EFED` **msgAccepted2** (text, 8 bytes): 'ACCEPTED' with bit 7 on the final D. Same shared filler block; unreferenced.
- `$EFF5` **overlayTailFiller** (unknown, 11 bytes): $C0,$DF then $00,$FF repeated to the end of the page. Identical in all four builds of the $EC00/$EE00 overlay slot except for the very last byte ($FF here and in the track 29 variants, $00 in ovl_E000_T34). Looks like leftover buffer content from the mastering system; unreferenced.

Insights:

- [game_ovl_E000_T35_E000] Comm-module build 1 (track 35, selected when linkTypeFlag $0BA5 bit 7 is set) is NOT a null-modem variant - it is the SOLO TRAINER's computer opponent. handleMainMenuChoice sets $0BA5 = $FF for 'PRACTICE WITH SOLO TRAINER' and $0BA5 = 0 for 'COMPETE WITH MODEM OPPONENT' (build 2 = track 34). This corrects the guess recorded in knowledgeBase.md line 787.
- [game_ovl_E000_T35_E000] The trainer fakes the whole link: $E006 always answers 'no byte', $E009 discards the byte, $E00C does nothing, $E003 sets/clears $E03B/$E03C/$E03D, and every $E000 call other than X=1/X=3 stores $C0 into packetExchangeStatus and mirrors $E039 into $E03A. Consequently processCommandExchange never waits, the suspend/resume handshake around disk access always succeeds instantly, and chat is dead in solo mode.
- [game_ovl_E000_T35_E000] The AI is driven entirely from three game hooks: $E000 with X=1 (once per lock-step exchange, from sendOutgoingPacket) builds the 'received' packet; sub_E079 (from finishRound $4654) runs the once-per-round planner; sub_E0E8 (from processUnitStep $4526) is the per-unit observer. The AI's orders are ordinary command bytes ($80/$81 move, $8D/$8F/$92/$97 drone, $90 missile, $9A comcen energy, $9E comcen repair) placed straight into receivedPacketBuffer $E020 - the rest of the game cannot tell them from a modem opponent's.
- [game_ovl_E000_T35_E000] AI work RAM lives in the $8800 message-strings overlay, which has been consumed by the time the battle starts: $8800-$8831 replan clock per AI unit ordinal, $8832-$8863 AI group id, $8864-$8895 'has scored both zones' flags, $8896-$889F command staging / 8-way direction scratch, $88A0-$88D1 the 50-byte outgoing order queue, then twelve-entry per-group arrays at $88D2 (target col), $88DE (target row), $88EA/$88F6 (centre), $8902 (size), $890E/$891A/$8926/$8932 (formation box) and $893E (flags), and the scalar state $8952-$897C. The AI addresses its own units by ordinal 0-49 = absolute unit 50-99; queueTrainerMoveOrder ($E919) adds 50 and mirrors both coordinates with mirrorMapCoordinate so the command arrives in the same form a real opponent would send.
- [game_ovl_E000_T35_E000] Planner outline (sub_E170, once per round): budget = AI comcen energy ($FA27 AND $3F, or a flat 50 when the DAMAGE option $0BA3 bit 4 is off); a fully damaged comcen ($F9C3 AND $0F == $0F) skips the round entirely; a budget below 20 only issues repair ($9E) and energy ($9A) commands via $EB67. Otherwise scanEnemyThreat ($EBEF) around the AI comcen yields a best heading, an enemy-proximity value and a threat score, which decide attack ($8963 = 0, and consider launching a drone) versus defend ($8963 = $FF, comcen retreats and the assault group is recalled). Each group is then re-planned when its replan clock catches the down-counting game clock $91CB, at most four or five command bytes per round.
- [game_ovl_E000_T35_E000] Group movement uses a threat-aware search: buildThreatWeights ($E773) writes a per-unit reach into page $0400 and a per-unit combat weight into $0464 (both scratch pages the game also clears in finishRound); evaluateCellSafety ($E69C) expands the group's formation box around a candidate cell and accepts it only while (enemy weight - own weight) inside that box stays below the group's own combat value; findTrainerAdvanceCell ($E42A) walks outward in steps of two cells along the direction ranks in $8896, limited by maxAdvanceByRankTable ($E664); issueTrainerGroupOrders ($E49E) then moves every member by the same delta so the formation is preserved.
- [game_ovl_E000_T35_E000] The $EE00-$EFFF quarter of the module is hot-swapped at run time: buildTrainerReplyPacket loads track 29 sectors 4-5 there (the drone-flight AI, disassembled as game/ovl_EE00_T29) as soon as droneStateFlags $920D bit 0 goes up, and reloads track 35 sectors 14-15 (the native tail) when the drone is gone. $8977 bit 7 records which one is resident. Separately, $EC00-$EFFF holds three different overlays over the life of a trainer game: overlay A loads track 29 s0-3 ($EC0F, which initialises all the $88xx AI tables) during startGameSession, and main_0800 at $0B4F reloads track 35 s12-15 immediately afterwards so the AI's own tail is back in place before the battle starts.
- [game_ovl_E000_T35_E000] Possible original bug: buildDirectionPreference ($E7E3) reads its eight compass offsets from $894A, but no traced code ever writes that table - it still holds the already-consumed side-1 game-type defaults from the $8800 strings overlay, i.e. 02 02 02 01 01 02 02 02. All eight 'preference ranks' therefore resolve to base+2 or base+1, so the trainer's direction search effectively tries only two headings instead of spiralling out from the intended one.
- [game_ovl_E000_T35_E68A] The T35 build of the $E000 module is not a link variant at all: it is the SOLO TRAINER's computer opponent. Chunk $E68A-$EC7F contains its movement, risk-assessment and command-generation core. The AI plays side 1 (units 50-99, comcen 99) against the human's side 0 (units 0-49, comcen 49).
- [game_ovl_E000_T35_E68A] How the AI reaches the game engine: injectAiCommandsIntoPacket ($EA7B, called every exchange from $E0E4) copies whole commands out of its own queue at $88A0 directly into the comm module's received-packet buffer $E020/$E01E, so processCommandExchange executes them exactly as if a modem opponent had sent them. Commands are staged in $8896-$8899, appended by queueAiCommand ($EA46) and the packet is capped at 7 bytes. Because they travel as 'remote' commands, orderAiUnitToCell ($E919) pre-mirrors both coordinates with mirrorMapCoordinate (and mirrorBlockCornerForOpponent, $EA38, additionally pre-decrements for 2x2 objects) so that the engine's mirrorCmdCoordsIfRemote(Dec) recovers the intended cell. Unit indices are NOT mirrored.
- [game_ovl_E000_T35_E68A] AI data model in the $8800 block (the modem message strings loaded there are dead once the trainer runs): three 50-byte per-AI-unit tables - $8800 last-order round stamp, $8832 group id, $8864 'reached the enemy end zone' flag - each also addressed with the absolute unit index through the bases $87CE/$8800/$8832; twelve-entry per-group tables at $88D2 targetCol, $88DE targetRow, $88EA/$88F6 centre, $8902 member count, $890E/$891A/$8926/$8932 formation spans left/up/right/down, $893E flags; then scalars $8952-$897C. The 50-byte command queue is at $88A0.
- [game_ovl_E000_T35_E68A] Direction encoding used throughout the module: 0-7 clockwise from north via dirToColDelta ($E68A) / dirToRowDelta ($E693), with a deliberate 9th (0,0) entry so that 'no direction' = 8 is safe. getStepDirection ($E80E) converts a Bresenham step from the shared line walker ($FBB8/$FBFA, deltas in $90F4/$90F5) into that encoding; offsetCoordinateClamped ($EA1B) and computeCellInDirection ($E66C) turn (direction, distance) back into a clamped 0..39 cell and report leaving the map through the carry.
- [game_ovl_E000_T35_E68A] Movement risk model: buildUnitThreatTables ($E773) builds an influence map - per unit a radius (3 grunt/rider, 6 boomer, 0 spy/comcen, plus terrainSightBonusTable of the cell it stands on) in $0400 and a weight (2,2,3,0,0 by type) in $0464, with the moving group's own members zeroed. isGroupDestinationTooRisky ($E69C) then sums the weights of all enemy units whose influence square overlaps the group's footprint at a candidate cell, subtracts friendly ones, and rejects the cell when the result reaches the group's own strength ($8956/$8957); exact ties are decided by nextGameRandom, so the AI is deliberately non-deterministic here.
- [game_ovl_E000_T35_E68A] Comcen behaviour: assessThreatAroundAiComcen ($EBEF) buckets the player's units 0-45 by direction around the AI comcen, smooths the 8 buckets circularly as (2b[i]+b[i-1]+b[i+1])/4 and returns the centre of the LEAST threatening 3-direction window as the escape direction (minimum search at $ECED, random tie-break on the RNG state zp_57). runAiComcenActions ($EB67) then spends the round's leftover command slots: haltAiComcen ($EBE4) stops it, maybeQueueComcenEnergyRepair ($EBA5) queues command $9A ADD ENERGY for unit 99 (1 + (50-energy)/8 points, 50 percent of the time, only below 45/50 energy) and a stunned comcen has a 1-in-8 chance per round of queuing $9E COMCEN REPAIRED. The trainer therefore heals its own command centre - a built-in handicap for the human.
- [game_ovl_E000_T35_E68A] Two bytes this module reads but never writes: $894A (the 8-entry direction-fan offset table for buildDirectionFan) and $897A (the comcen threat threshold). $897A is initialised to 5 by the track-29 $EC00 setup overlay ($EC44), but nothing anywhere in the disassembly writes $894A, so at run time it contains the bytes the $8800 message-string overlay left there ($02,$02,$02,$01,$01,$02,$02,$02) and the eight 'candidate directions' collapse to just heading+1 and heading+2. This looks like a genuine bug.
- [game_ovl_E000_T35_E68A] queueAiCommand sets the VIC border-colour shadow $9003 to 6 and drops the command when the queue would reach 50 bytes, and keeps an otherwise unused high-water mark in $EA45 - both are debugging leftovers that survived into the shipped build.
- [game_ovl_E000_T35_E68A] WARNING for other annotators: disassembly/XREF.txt merges the cross-references of ovl_E000_T34 and ovl_E000_T35 for every $E000-$EFFF address (duplicated entries such as 'call:E917,E917' are the give-away). Several caller lists attributed to T35 labels here (sub_E937 call:E917/$EA01, sub_E9A6 call:EE37/EE73/EE7D/EE88/EE92, sub_EA38 call:EEC1/EF8F, D_8976 w:EC33) are T34 addresses; in T35 sub_E937 is only fall-through, sub_E9A6 is called just from $EFA8 and sub_EA38 just from $EEB1. Similarly, several D_/L_ labels inside this chunk ($EAA0-$EAC6, $EAEB, $EAFF, $EB01, $EB03, $EBFE, $EC05, $EC0A) are branch targets of the T34 build sitting in the middle of T35 routines.
- [game_ovl_E000_T35_E68A] The AI's group plan is not computed at run time: the track-29 $EC00 setup overlay ($EC0F) picks one of five canned plays by game type, loads a 768-byte playbook from track 29 sectors 6-8 into $0200-$04FF and copies a 150-byte slice of it into aiUnitGroup ($8832), unitDestCol ($F802) and unitDestRow ($F866), setting aiGroupCount ($8954) to 11 (plays 0-1) or 5 (plays 2-4) and picking a random home-defence group id in $8964. This chunk only executes that plan.
- [game_ovl_E000_T35_EC82] This build of the $E000 module is the SOLO TRAINER, not a modem driver. loadCommModule ($0F16) picks build 1 (track 35) when $0BA5 bit 7 is set, and everywhere the main program tests that flag it skips the modem behaviour ('PICK UP PHONE THEN PRESS SPACE.' at $1B64, 'WAITING FOR CONNECTION...' at $1B77, carrier handling). The jump-table entry sub_E000 with X=1, which sendOutgoingPacket ($562B) uses to transmit, lands at $E082 and runs a complete computer opponent that fabricates the reply packet in $E020/$E01E. So $E020/$E01E = incoming packet buffer + length, $E027/$E01F = outgoing packet buffer + length.
- [game_ovl_E000_T35_EC82] The AI always plays side 1: it works on unit 99 ($F6A3/$F707/$FA27/$F9C3) as its own comcen and unit 49 ($F9F5) as the enemy comcen, scans enemy units 0-45 in the threat pass, and isUnitOnAiRadar restores comcenUnit $9236 to $31 unconditionally. Every coordinate it puts in a packet goes through mirrorMapCoordinate ($3FA3) - and sub_EA38 additionally adds 1 first, pre-compensating the mirror-and-decrement that cmdDroneMove applies to remote $8D parameters.
- [game_ovl_E000_T35_EC82] $EE00-$EFFF is a third overlay layer inside the module. The default content is track 35 s14-15 (the missile/target code in this chunk); $E0CD-$E0D9 swaps in track 29 s4-5 (disassembly/game/ovl_EE00_T29.s, the drone-flying AI) while the AI's drone is airborne and $E0AB-$E0B6 reloads track 35 s14-15 afterwards. $8977 = $FF marks the drone overlay being resident. The 'jsr sub_EE00' at $E0A5 therefore never enters the code at $EE00 in this file - that address is only a fall-through label of runAiMissileTurn here.
- [game_ovl_E000_T35_EC82] disassembly/XREF.txt merges references from ALL builds that share an address range, so almost every caller it lists for $EC82-$EFFD is wrong for this file. sub_EC91/sub_ECD4/sub_ECF3/sub_ED43/sub_EE4A/sub_EEB9/sub_EEDD and the labels msgBeginnerDisplaySet($EF28)/msgMapType($EF77)/D_EFB6 come from ovl_E000_T34 or ovl_EC00_T29 (the $EC00 slot the setup overlay A works with); in ovl_E000_T35 those addresses are mid-instruction or plain loop heads. The only genuine entries into this chunk are $ED0B (from $EA80), $EE2D, $EE55, $EE6A, $EE85 (all internal) and $EEE0 (from $E1FF); ovl_6F00_B and main_0800 never call into $EC00-$EFFF.
- [game_ovl_E000_T35_EC82] Threat model used by the AI (sub_EBEF, whose tail is this chunk): for every enemy unit within 20 cells of a reference point it adds a weight to the compass direction the unit lies in - +1 base (unless $897C bit 7), +1 if closer than 10 cells, +4 if the unit's weapon range (5, or 9 for types with bit 1 set, i.e. BOOMER/SPY) covers the point. The 8-direction ring is then blurred 1-2-1 circularly and the calmest 3-direction arc is picked. Return values: A = overall threat, X = largest in-range surplus, Y = safest direction (8 = nothing there, which is exactly the null entry of the direction delta tables $E68A/$E693). Callers use (Y+4)&7 to get the direction the enemy is coming from.
- [game_ovl_E000_T35_EC82] Direction encoding used throughout the module: index 0-7 clockwise from north, column delta table at $E68A = 0,+1,+1,+1,0,-1,-1,-1 and row delta table at $E693 = -1,-1,0,+1,+1,+1,0,-1, both with a 9th entry 0 so index 8 means 'nowhere'. sub_E80E returns this index in Y (A holds the row delta) and sub_E66C(X = direction, Y = distance) returns the clipped cell in zp_18/zp_19 with C = 1 when it fell off the 40x40 map.
- [game_ovl_E000_T35_EC82] The AI's per-unit bookkeeping lives in the tail of the $8800 message block, which is dead once a battle starts: $8800-$8831 = order time stamp (game clock) per AI unit, indexed by unitIndex-50 (also addressed as $87CE,absoluteUnitIndex), and $8832-$8863 = objective id per AI unit (also addressed as $8800,absoluteUnitIndex). $8896-$889D is the 8-direction threat ring, and the AI parameter block $8952-$897C is initialised by the track 29 $EC00 sub-overlay ($EC1C-$ECD2 there) before the real $EC00 code is loaded over it at $0B4F.
- [game_ovl_E000_T35_EC82] Missile behaviour is deliberately imperfect: an anti-drone shot compares nextGameRandom against ((reluctance+1)<<5) + $40 if the drone changed heading; on failure the impact point is scattered by randomlySignValue, on success the AI also queues command $92 so the engine treats the drone as destroyed. Missiles are conserved with an explicit rule - the AI will not shoot unless missilesLeft exceeds 2x (or 4x, for the anti-drone case) the number of drones the human still has.

## game/trainerSetupEC00

| addr | name | summary | in | out | conf |
|---|---|---|---|---|---|
| EC0F | initTrainerAi | Only entry point of this sub-overlay; called once from overlay A at $7B30 right after the overlay was loaded (T29 s0-3) and just before $7B33 restores the comm tail. It builds the whole opening plan of the solo-trainer computer opponent (side 1, units 50-99): clears the AI work area in the $8800 block, picks an objective near the human comcen (sub_E12F), selects a plan index 0-4 from the game type ($0BA3 & 7), loads the 5 x 150 byte deployment template from track 29 s6-8 into $0200-$04FF, copies the plan's 50 group ids to $8832 and its 50 destination cells to unitDestCol+50/unitDestRow+50, gives every AI group a random destination, then calls rebuildAiGroupTables and computes the phase-change clock $896E. | $0BA3 game type bits 0-2, $91CB game clock (set by setGameClockForType at $7B1A), $92AC zone-score setting, unit arrays $F640/$F6A4/$F708/$F76C/$F7D0/$F834, tables at $EC00/$EC05/$EC0A, disk (loadSectors/setLoadDest), nextGameRandom | $0200-$04FF overwritten with the deployment template; AI work area $8800-$8979 initialised; unitDestCol/unitDestRow of units 50-99 set; unitFlagsTable[99]=$21 and unitStatusFlags[99]=$20 for plans 4; $F7D0[99] bits 6-7 cleared for THE BOMB; $E01E=0; one $A5 00 $14 setup command queued for DEFENDER; $91CB restored to $FF; zp_18-zp_2B and zp_48-zp_4D clobbered | high |
| ED8E | rebuildAiGroupTables | Recomputes the per-group bookkeeping of the AI's 12 groups. First calls buildAiHeadingSearchOrder, then zeroes the 12-entry group arrays ($88D2/$88DE/$88EA/$88F6/$8902 and their high-byte halves at $0464/$0478/$048C/$04A0) and gives every group a random start delay 0-15 in $893E. It then walks the 50 AI units, drops dead ones ($8832=$FF), accumulates each unit's destination and current cell into its group's 16-bit sums, sets the per-unit zone flag $8864, stores the unit's activation clock in $8800 (game clock minus the group delay, except group 0 and the guard group), and marks a unit that already stands on its destination by setting bit 7 of unitDestCol. Finally it divides all four sums by the member count so $88D2/$88DE hold the group's average destination and $88EA/$88F6 its average position. | $8832 group ids, $8864, $8953 highest group, $8964 guard group, $8966 plan index, $91CB clock, $92AC, unit arrays $F672/$F6D6 (col/row of units 50-99), $F802/$F866 (dest col/row), $F79E (types) | $0464/$0478/$048C/$04A0 (12 bytes each), $88D2/$88DE/$88EA/$88F6/$8902/$893E, $8800-$8831 activation clocks, $8864-$8895 flags, bit 7 of $F802 entries; zp_18/zp_1A/zp_1B/zp_1C/zp_22/zp_24/zp_27/zp_28 clobbered | high |
| EEC3 | buildAiHeadingSearchOrder | Fills the 8-byte table at $894A with heading offsets 0, +/-1, +/-1, +/-2, +/-2, +/-3, +/-3, +/-4 taken mod 8, the sign of each pair being chosen at random. The resident AI walks this table to try headings in order of increasing deviation from the wanted direction, with a random left/right bias per game. | nextGameRandom | $894A-$8951; zp_18 scratch, A/Y clobbered | medium |
| EEED | orderAiGroupToPoint | Orders AI group Y to move so that its formation centre ends up at column X, row A. Patches the two immediate operands at $EEF9/$EEFD with X and A, calls sub_E531 to measure the group (returns C=1 and does nothing when the group is empty), stores the point in zp_22/zp_24 and falls into shiftAiGroupDestinations. Called from initTrainerAi to make group 0 follow the main group and to send the guard group to the AI comcen. | X = target column, A = target row, Y = group index, $8832 group ids | self-modifies $EEF9/$EEFD; group destination arrays and unitDestCol/unitDestRow of the group's units updated; C=1 (untouched) when the group has no live unit | high |
| EF00 | shiftAiGroupDestinations | Tail of orderAiGroupToPoint, also called directly at $ED3F. Calls sub_E5FF, which clips the wanted point (zp_22/zp_24) so the group's bounding box stays on the 40x40 map, stores the result as the group destination ($88D2/$88DE) and returns the delta from the group's current centre in zp_2A/zp_2B. It then adds that delta to unitDestCol/unitDestRow of every unit whose group id matches $8955, so the whole group translates while keeping its formation (the AND #$3F also clears the idle/mirror bits). | zp_22/zp_24 wanted centre, $8955 current group (set by sub_E531), $8832, $F802/$F866 | $88D2/$88DE for the group, unitDestCol+50/unitDestRow+50 of its members, zp_2A/zp_2B | high |
| EF27 | stepAiDroneAndQueueMove | Drone-steering step of the trainer AI, shown as .byte in this file. Turns the drone heading $896F by X steps (carry set forces -1), looks the heading up in headingToDirectionTable ($7316 in the $6F00 overlay) and converts it through directionDeltaTable ($20F0) / diagonalDirectionTable ($1FB2) into dx/dy, advances the drone position $8974/$8975 by one cell, then fabricates a 4-byte command $8D (drone move: heading+4 mod 8, column, row) in the comm module's INCOMING packet buffer $E020-$E023 and sets $E01E = 4, i.e. the trainer feeds the engine a packet as if the remote player had sent it. Coordinates are mirrored with sub_EA38 first. These bytes are identical to $EF27 in game/ovl_EE00_T29, where the routine is live; in this overlay nothing reaches it, so here it is a dead copy. | X = heading change, C = 1 forces -1, $896F heading, $8974/$8975 drone column/row | $896F, $8974/$8975 advanced, $E020=$8D, $E021=heading+4, $E022/$E023 = mirrored column/row, $E01E=4 | medium |
| EF9E | measureDroneDistanceToTarget | Copies the drone position $8974/$8975 into zp_27/zp_28 and jumps into sub_E82A, which returns the distance bucket (0-$10, via distanceSquared and squaresTable) between the cell in zp_22/zp_24 and the drone. Shown as .byte here; live in game/ovl_EE00_T29 where it is called from $EE26 and $EEFC. | zp_22/zp_24 target cell, $8974/$8975 drone position | A = distance bucket, zp_27/zp_28 set, X clobbered | high |
| EFAB | decayDroneScoreThreshold | 34-byte code fragment (shown as .byte) shared byte for byte with the $EE00/$EC00 tails of the other builds: increments zp_1F, loops back to $EF9B while it is below $0F, then stores zp_1D into $8976 and reduces $8979 to three quarters of itself. In the T35 comm build the loop entry $EF9B is a real instruction; with this file's instruction alignment the BCC at $EFB1 lands inside the operand of the STA $E01E at $EF9A, so the fragment is stale code left over from the shared source module and is not usable here. | zp_1D, zp_1F, $8979 | $8976, $8979 (x 3/4), zp_18 | low |

Data:

- `$EC00` **mainGroupByPlanTable** (byteTable, 5 bytes): Per AI plan 0-4: index of the main/spearhead group ($FF, $FF, 4, 4, 4) stored in $895D.
- `$EC05` **groupCountByPlanTable** (byteTable, 5 bytes): Per AI plan 0-4: highest group index ($0A, $0A, 4, 4, 4) stored in $8953; $8954 gets this value + 1.
- `$EC0A` **aiPlanParamTable** (byteTable, 5 bytes): Per AI plan 0-4: drone score threshold ($FF, $FF, 8, $0C, $0C) copied to both $8978 and $8979.
- `$EFCD` **msgSpaceTailFragment** (text, 3 bytes): 'CE' + '.'|$80 - the tail of the string 'AND PRESS SPACE.' that the T34 comm tail keeps at this address; only the last three bytes survive in this variant, which is why the shared block starts here.
- `$EFD0` **msgDataGameCustomOff** (text, 18 bytes): 'DATAGAMECUSTOM OF'+'F'|$80; almost certainly addressed at two offsets ('DATAGAME' at $EFD0 and 'CUSTOM OFF' at $EFD8), only the final F carries the bit-7 terminator.
- `$EFE2` **msgDestroyOff** (text, 11 bytes): 'DESTROY OF'+'F'|$80.
- `$EFED` **msgAccepted** (text, 8 bytes): 'ACCEPTE'+'D'|$80.
- `$EFF5` **overlayTailPadding_EFF5** (byteTable, 11 bytes): $C0,$DF then four $00,$FF pairs and a trailing byte ($FF here, $00 in the T34 tail). Purpose unknown; the same bytes end every $EC00/$EE00 overlay variant.

Insights:

- [game_ovl_EC00_T29_EC00] This overlay is the opening-plan generator of the solo trainer. Overlay A runs it only when linkTypeFlag $0BA5 bit 7 is set (PRACTICE WITH SOLO TRAINER): $7B24 loads track 29 s0-3 to $EC00, $7B30 calls $EC0F once, $7B33 immediately restores the build-2 comm tail from track 34 s12-15. Everything in this file therefore executes exactly once, and only the $EC0F call graph ($EC0F, $ED8E, $EEC3, $EEED, $EF00 plus helpers in $E000-$EBFF) is live; $EF27-$EFCC is a dead copy of code that belongs to the $EE00 drone sub-overlay.
- [game_ovl_EC00_T29_EC00] The trainer AI plays side 1 (units 50-99, the human is forced to side 0 by cmdChooseSide) and keeps all its state in the $8800-$89FF page pair, i.e. it overwrites the message strings that were loaded there from track 29 s9-10 and the 5x5 game-type default tables at $892C/$8945 that applyGameTypeDefaults ($7C2E) reads earlier. Map of the work area: $8800 activation clock per AI unit (50), $8832 group id per AI unit (50, addressed as $8800+absoluteUnitIndex by the resident AI), $8864 'zone done' flag per unit (50), $8896 command scratch, $88A0 command queue (50), then twelve 12-byte per-group arrays $88D2 destCol, $88DE destRow, $88EA centreCol, $88F6 centreRow, $8902 unitCount, $890E/$891A/$8926/$8932 bounding box, $893E start delay, $894A heading search order (8), and the scalars $8952-$8979.
- [game_ovl_EC00_T29_EC00] The 16-bit halves of the four per-group sums live at $0464/$0478/$048C/$04A0 - inside the $0400 page that held the copy-protection answer table and the scan lists, which is free once the game is running. The existing labels D_0464/D_0478/D_048C/writeSectorToDisk for those addresses are address collisions from the table_0400 unit and mean nothing here.
- [game_ovl_EC00_T29_EC00] AI plan selection from the game type ($0BA3 & 7): 0 SCRIMAGE -> plan 0, 1 QB SNEAK -> plan 1 (no guard group), 2 THE BOMB -> plan 0 but the deployment template is skipped entirely (only units 49 and 99 exist), 3 FACE-OFF -> plan 2, 4 SLUGGERS -> plan 3, 5 FULL WAR -> plan 4, 6 DEFENDER -> plan 4 plus a queued $A5,$00,$14 setup-option command (20 extra points). Plans 0/1 use 11 groups, plans 2-4 use 5 groups. Plans 4 also force unitFlagsTable[99] = $21 and unitStatusFlags[99] = $20 for the AI comcen.
- [game_ovl_EC00_T29_EC00] Track 29 sectors 6-8 are the AI deployment template, not just a 'default setup block': $ECA6-$ECB3 loads them to $0200-$04FF and treats them as five 150-byte records (one per plan, base = $0200 + 150*plan), each holding 50 group ids, 50 destination columns and 50 destination rows for units 50-99. The tail of the third record runs into $04xx, which is why the group sum high bytes were placed at $0464+.
- [game_ovl_EC00_T29_EC00] How the trainer fakes an opponent: the AI never goes through the modem. It assembles command bytes in $8896, queues them in $88A0 with sub_EA46 and later copies them into the comm module's INCOMING packet buffer $E020.. while setting $E01E to the packet length, so mergeIncomingPacket ($4F05) executes them as if the remote player had sent them. The drone code copied at $EF27 does the same directly, writing a 4-byte $8D heading,col,row packet after mirroring the coordinates with sub_EA38.
- [game_ovl_EC00_T29_EC00] Group geometry helpers used from this chunk live in the resident comm build 1: sub_E531(Y=group) measures a group (member count, strength sum via the per-type weight table $E2B7 = 2,2,3,0,0 for GRUNT/RIDER/BOOMER/SPY/COMCEN, bounding box, centre in zp_1E/zp_1F), sub_E5FF clips a wanted centre so the whole box stays on the 40x40 map and returns the shift in zp_2A/zp_2B, sub_E82A converts a cell pair into a distance bucket 0-$10, sub_E12F re-picks the AI objective near the human comcen, sub_EA46 queues a command.
- [game_ovl_EC00_T29_EC00] Timing: rebuildAiGroupTables staggers the AI - each group gets a random 0-15 round delay ($893E), every unit's activation clock is gameClock - groupDelay ($8800), and group 0 plus the guard group start at once. initTrainerAi also computes $896E = gameClock - random(32..95) for plans >= 2, the round at which the resident AI switches behaviour ($E3E2/$E762 compare it with $91CB). $91CB itself is left at $FF because the real clock is only started later at $7B86.
- [game_ovl_EC00_T29_EC00] Label warning for this file: because labels are keyed by address across all overlays, several symbols here come from the T35/T34 comm tails and are not routine starts in this variant - sub_EC00/L_EC05/D_EC0A are three data tables, and sub_EC54, sub_EC91, sub_ECD4, sub_ECF3, sub_EE00, sub_EE4A, sub_EE55, sub_EEB9, sub_EEE0 are all mid-routine fall-through points of $EC0F/$ED8E. Conversely $EC58/$EC91/$EC1C called by overlay A at $7AE1-$7AEC are the T35 tail's map-rotation routines, not this file's code.
- [game_ovl_EC00_T29_EC00] $EFCD-$EFFE is byte-identical in all three $EC00 tail builds (T29 s0-3, T34 s12-15, T35 s12-15) and $EF27-$EFFF is identical between the two track-29 sub-overlays, so the message strings DATAGAME/CUSTOM OFF/DESTROY OFF/ACCEPTED sit at a fixed address that survives every swap of the $EC00-$EFFF window.

