{
 "_comment": "generated by tools/mergeSurvey.py from survey/*.json - do not edit by hand",
 "notes": {
  "1DA2": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXStartIndex: byteTable, 3 bytes. per horizontal direction (0=-1,1=0,2=+1): start Y index of the bitmap byte copy ($5F,$00,$00)"
   ]
  },
  "2BDD": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "windowTopTable: byteTable, 27 bytes. Top screen row of windows 0-26"
   ]
  },
  "2C13": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "windowHeightTable: byteTable, 27 bytes. Height in cells of windows 0-26"
   ]
  },
  "2C49": {
   "unit": "game/mainProgram0800",
   "block": [
    "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)"
   ]
  },
  "5A59": {
   "unit": "game/mainProgram0800",
   "block": [
    "overviewPatternLo: byteTable, 33 bytes. low bytes of the 4-byte quadrant pattern for terrain indices 0-32 (patterns at $5A9B-$5AD6)"
   ]
  },
  "5A7A": {
   "unit": "game/mainProgram0800",
   "block": [
    "overviewPatternHi: byteTable, 33 bytes. high bytes (all $5A) of the pattern pointers; the listing merges this with the patterns that follow"
   ]
  },
  "5A9B": {
   "unit": "game/mainProgram0800",
   "block": [
    "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"
   ]
  },
  "6782": {
   "unit": "game/mainProgram0800",
   "block": [
    "sidVoiceRegOffsetTable: byteTable, 5 bytes. SID register offset per voice, indexed by voice*2: $00, (pad), $07, (pad), $0E"
   ]
  },
  "6787": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "sndDriverVars: byteTable, 6 bytes. work RAM: sndVolume, sndLastByte, sndScratch, sndInitLock, sndCurrentSound, sndStartVoiceIndex"
   ]
  },
  "67C8": {
   "unit": "game/mainProgram0800",
   "block": [
    "sndVoiceSoundId: byteTable, 3 bytes. work RAM: sound id currently playing on each of the three voices (0 = free)"
   ]
  },
  "6831": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "padding6EDD: unknown, 35 bytes. zero filler to the end of the main program at $6EFF"
   ]
  },
  "0800": {
   "routine": [
    "gameEntry - Entry point jumped to by the boot code at $02B8; just JMPs to mainStart.",
    "In:  none",
    "Out: none"
   ],
   "unit": "game/mainProgram0800"
  },
  "0804": {
   "routine": [
    "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.",
    "In:  A=sector count, Y=track, X=first sector, loadDestLo/Hi",
    "Out: C=0, memory filled, loadDestHi advanced, ldTrack/ldSector/ldCount"
   ],
   "unit": "game/mainProgram0800"
  },
  "0843": {
   "routine": [
    "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).",
    "In:  A=count, Y=track, X=first sector, loadDestLo/Hi",
    "Out: C=1 on failure, C=0 on success"
   ],
   "unit": "game/mainProgram0800"
  },
  "085C": {
   "routine": [
    "setLoadDest - Stores X/Y as the low/high byte of the next transfer address (self-modifying operands loadDestLo/loadDestHi).",
    "In:  X=lo, Y=hi",
    "Out: loadDestLo, loadDestHi"
   ],
   "unit": "game/mainProgram0800"
  },
  "0863": {
   "routine": [
    "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.",
    "In:  Y=track, X=sector, loadDestLo/Hi",
    "Out: C=1 on error (from finishTransfer)"
   ],
   "unit": "game/mainProgram0800"
  },
  "088F": {
   "routine": [
    "sendSectorData - Sends the 256 bytes at (wrSrcPtr) to the drive one byte at a time, then falls into finishTransfer to read the status.",
    "In:  wrSrcPtr",
    "Out: C=1 on error"
   ],
   "unit": "game/mainProgram0800"
  },
  "089C": {
   "routine": [
    "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).",
    "In:  A=byte",
    "Out: A=byte received from the drive, C=0, X=0"
   ],
   "unit": "game/mainProgram0800"
  },
  "08EB": {
   "routine": [
    "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.",
    "In:  Y=track, X=sector, loadDestLo/Hi",
    "Out: C=1 on error, ldTrack/ldSector"
   ],
   "unit": "game/mainProgram0800"
  },
  "08FE": {
   "routine": [
    "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.",
    "In:  ldTrack, ldSector, loadDestLo/Hi",
    "Out: C=1 on error, savedSpriteEnable, ldCia2Bits"
   ],
   "unit": "game/mainProgram0800"
  },
  "0943": {
   "routine": [
    "finishTransfer - Restores the sprite enable register and reads the drive status byte; C=1 if the status is nonzero.",
    "In:  savedSpriteEnable",
    "Out: C, A=status"
   ],
   "unit": "game/mainProgram0800"
  },
  "0950": {
   "routine": [
    "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.",
    "In:  ldDestPtr, ldCia2Bits",
    "Out: 256 bytes at (ldDestPtr)"
   ],
   "unit": "game/mainProgram0800"
  },
  "09A6": {
   "routine": [
    "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.",
    "In:  $0401 table, $5A-$5C player input",
    "Out: $0B95-$0B97 and $0BA9-$0BAE restored, zp_3D/zp_42=0"
   ],
   "unit": "game/mainProgram0800"
  },
  "0A6C": {
   "routine": [
    "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.",
    "In:  none",
    "Out: whole system initialised"
   ],
   "unit": "game/mainProgram0800"
  },
  "0AA3": {
   "routine": [
    "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).",
    "In:  none",
    "Out: currentScreen=3, loadingDepth=0, stack reset",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0AC8": {
   "routine": [
    "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).",
    "In:  $0B9B, $0B9C, $0BA5, $0BA6, $87FF, $E030",
    "Out: map at $F000, setup at $0200, comm module configured, loadingDepth=0",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0B4F": {
   "routine": [
    "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).",
    "In:  X=0,Y=$EC",
    "Out: $EC00-$EFFF"
   ],
   "unit": "game/mainProgram0800"
  },
  "0BB5": {
   "routine": [
    "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.",
    "In:  none",
    "Out: IRQ installed, inputLockoutTimer, currentKey",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0BD8": {
   "routine": [
    "disableCiaInterrupts - Writes $7F to both CIA ICRs and reads them back to acknowledge pending interrupts.",
    "In:  none",
    "Out: CIA interrupts disabled"
   ],
   "unit": "game/mainProgram0800"
  },
  "0BE7": {
   "routine": [
    "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).",
    "In:  none",
    "Out: $10FF/$1100, $E031/$E032, $FFFA/$FFFB",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0C02": {
   "routine": [
    "setNmiVectorRti - Sets the NMI vector to irqRti ($1298); jumped to after loading comm build 1.",
    "In:  none",
    "Out: $FFFA/$FFFB"
   ],
   "unit": "game/mainProgram0800"
  },
  "0C0D": {
   "routine": [
    "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.",
    "In:  $CD00",
    "Out: VIC_IRQ_MASK=0 when taken",
    "(confidence: low)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0C1C": {
   "routine": [
    "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.",
    "In:  none",
    "Out: variables cleared",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0C66": {
   "routine": [
    "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.",
    "In:  none",
    "Out: $FFFE/$FFFF, zp_AA-zp_AC, VIC_RASTER, VIC_IRQ_MASK"
   ],
   "unit": "game/mainProgram0800"
  },
  "0C8C": {
   "routine": [
    "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).",
    "In:  A/Y=string address",
    "Out: message slots $20/$21",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0CA0": {
   "routine": [
    "resetMessagesAndBeep - Resets the message queue, queues message $20, plays sound 3 twice with a 20-frame pause and sets messageState ($92A1) to $21.",
    "In:  none",
    "Out: $92A1=$21",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0CBD": {
   "routine": [
    "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.",
    "In:  $9248 key from IRQ",
    "Out: $E011 answer/originate, $0B7D=0"
   ],
   "unit": "game/mainProgram0800"
  },
  "0CE8": {
   "routine": [
    "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'.",
    "In:  $FFDE-$FFF1",
    "Out: $0B7F-$0B93",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0D49": {
   "routine": [
    "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.",
    "In:  $91C9, $91CA",
    "Out: zp_6C, $91CA, $91C9=$7F",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0DB7": {
   "routine": [
    "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'.",
    "In:  CIA1_PRA/PRB/DDRA/DDRB; keyMatrixCodeTable ($0D67); row masks D_2F8D ($2F8D); column masks spriteBitMasks ($2F85)",
    "Out: 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."
   ],
   "unit": "game/mainProgram0800"
  },
  "0E45": {
   "routine": [
    "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.",
    "In:  Y = row select pattern for CIA1_PRA, A = column bit mask",
    "Out: A = masked port B value, Z=1 if the key is down. Self-modifies $0E54."
   ],
   "unit": "game/mainProgram0800"
  },
  "0E56": {
   "routine": [
    "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.",
    "In:  comm module entry $E015 -> scanKeyboard results; zp_70; $90E9",
    "Out: zp_66 currentKey, zp_70 repeat timer, $90E9 lastKeyCode, $90EA newKeyEvent, $9248 lastTypedKey, $92A0/$92E4/$92E5 raw scan results"
   ],
   "unit": "game/mainProgram0800"
  },
  "0EB1": {
   "routine": [
    "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.",
    "In:  zp_5F fire state (0 = pressed), zp_79 armed flag, zp_6D debounce timer, $908D/$908E via isCmdRingNearlyFull",
    "Out: A/Z (0 = fresh press), zp_79, zp_6D, sound 1 queued"
   ],
   "unit": "game/mainProgram0800"
  },
  "0ED5": {
   "routine": [
    "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.",
    "In:  zp_5F/zp_79/zp_6D, $0B9B, $923F",
    "Out: A/Z (0 = press accepted)",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0EE5": {
   "routine": [
    "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.)",
    "In:  $0B9B, $923F",
    "Out: A/Z (Z=1 = the player may act)",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0EEA": {
   "routine": [
    "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.",
    "In:  zp_5F, zp_79, zp_6D",
    "Out: A/Z, zp_79"
   ],
   "unit": "game/mainProgram0800"
  },
  "0EFE": {
   "routine": [
    "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.",
    "In:  none",
    "Out: $92E7-$92EA reset, all SID voices released"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F03": {
   "routine": [
    "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.",
    "In:  $0BA5 (read further down the chain)",
    "Out: $C000-$CFFF, $6F00-$87FF (variant A), $8800-$89FF, $EC00-$EFFF, $E000-$EBFF, comm module initialised"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F16": {
   "routine": [
    "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.",
    "In:  $0BA5 bit 7",
    "Out: $E000-$EBFF, comm module initialised, NMI vector (build 1)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F31": {
   "routine": [
    "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.",
    "In:  load destination already set to $E000",
    "Out: $E000-$EBFF"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F41": {
   "routine": [
    "loadCommFinish - Shared tail of the two comm-module load paths: performs the final loadSectors with the registers the caller prepared and falls into initCommModule.",
    "In:  A = sector count, Y = track, X = first sector",
    "Out: comm module loaded and initialised"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F44": {
   "routine": [
    "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.",
    "In:  comm module resident at $E000",
    "Out: $E013/$E014 = $0DB7, I flag cleared"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F55": {
   "routine": [
    "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.",
    "In:  A = sector count, Y = track, X = first sector, load destination from setLoadDest",
    "Out: as loadSectors (memory filled, C = error)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F5F": {
   "routine": [
    "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.",
    "In:  $E03B link-active flag",
    "Out: $E039 = $C0, blocks until $E03A bit 7 set",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0F71": {
   "routine": [
    "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.",
    "In:  $E03B",
    "Out: $E039 = 0, blocks until $E03A bit 7 clear",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0FB1": {
   "routine": [
    "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!'.",
    "In:  A = silent flag (bit 7), X = 0 or 2 index into diskIdTable",
    "Out: C=0 ok / C=1 wrong disk or drive error, $92F0, $FFDE-$FFE7, $0200-$02FF clobbered by the BAM, self-modifies $0FDD/$0FE4/$0FFF"
   ],
   "unit": "game/mainProgram0800"
  },
  "1018": {
   "routine": [
    "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).",
    "In:  none",
    "Out: C=1"
   ],
   "unit": "game/mainProgram0800"
  },
  "101C": {
   "routine": [
    "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.",
    "In:  A = message address low byte, Y = high byte",
    "Out: C=1, message area restored"
   ],
   "unit": "game/mainProgram0800"
  },
  "1030": {
   "routine": [
    "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.",
    "In:  $87FF bit 7",
    "Out: overlay/strings reloaded, comm module suspended and resumed",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1047": {
   "routine": [
    "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.",
    "In:  none",
    "Out: $6F00-$87FF = variant A, plus $8800-$89FF and $EC00-$EFFF from the fall-through"
   ],
   "unit": "game/mainProgram0800"
  },
  "1060": {
   "routine": [
    "loadStringsAndCommTail - Loads the message-string block from track 29 sectors 9-10 into $8800-$89FF, then falls into loadCommTailBuild2.",
    "In:  none",
    "Out: $8800-$89FF, $EC00-$EFFF"
   ],
   "unit": "game/mainProgram0800"
  },
  "1070": {
   "routine": [
    "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.",
    "In:  none",
    "Out: $EC00-$EFFF"
   ],
   "unit": "game/mainProgram0800"
  },
  "1080": {
   "routine": [
    "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.",
    "In:  $87FF bit 7",
    "Out: see loadOverlayB"
   ],
   "unit": "game/mainProgram0800"
  },
  "1086": {
   "routine": [
    "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.",
    "In:  $206B",
    "Out: $6F00-$87FF = variant B, $750A patched"
   ],
   "unit": "game/mainProgram0800"
  },
  "10AB": {
   "routine": [
    "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.",
    "In:  none",
    "Out: $F000-$F6FF map, $0200-$03FF setup block"
   ],
   "unit": "game/mainProgram0800"
  },
  "10BB": {
   "routine": [
    "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).",
    "In:  none",
    "Out: $0200-$03FF"
   ],
   "unit": "game/mainProgram0800"
  },
  "10CB": {
   "routine": [
    "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.",
    "In:  none",
    "Out: comm module paused, $92A1 = 1",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "10D1": {
   "routine": [
    "resumeCommAfterDiskAccess - Epilogue for disk access: resumeCommModule, clear messageState ($92A1) and clear/redraw the message area through $C39F.",
    "In:  none",
    "Out: comm module running, $92A1 = 0",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "10DC": {
   "routine": [
    "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).",
    "In:  none",
    "Out: blocks until C=0 from checkDiskId; comm module left suspended for the caller"
   ],
   "unit": "game/mainProgram0800"
  },
  "10F1": {
   "routine": [
    "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.",
    "In:  VIC/CIA1 registers, shadows $9000-$9024, $0B7D, $0BA7, $92C8/$92F4/$92F5, comm module entries $E000/$E00C",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "1293": {
   "routine": [
    "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.",
    "In:  stack layout pushed by rasterIrqHandler",
    "Out: registers restored, RTI"
   ],
   "unit": "game/mainProgram0800"
  },
  "1298": {
   "routine": [
    "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.",
    "In:  none",
    "Out: none"
   ],
   "unit": "game/mainProgram0800"
  },
  "129C": {
   "routine": [
    "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.",
    "In:  zp_45 frame counter, zp_B1 game phase",
    "Out: $92F8, $92FB, $9223, $92C9, zp_7F, zp_72, $9163, $90FC, $91C8, $9234, $923E"
   ],
   "unit": "game/mainProgram0800"
  },
  "130A": {
   "routine": [
    "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).",
    "In:  zp_57-zp_59, zp_5D",
    "Out: zp_57-zp_59 advanced, A = the new random byte"
   ],
   "unit": "game/mainProgram0800"
  },
  "131B": {
   "routine": [
    "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.",
    "In:  $131A, $9008",
    "Out: $131A, $9008, $900A"
   ],
   "unit": "game/mainProgram0800"
  },
  "1331": {
   "routine": [
    "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.",
    "In:  $E03B, $E02E, $92E6, $929C, $90EA, $9248",
    "Out: $929C, $9278.. line buffer, $90EA/$9248 cleared, one byte queued to $E009, sounds 2",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1399": {
   "routine": [
    "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.",
    "In:  comm module receive ring via $E006, $9277",
    "Out: $9253-$9276 buffer, $9277, zp_72, sound 5"
   ],
   "unit": "game/mainProgram0800"
  },
  "13C4": {
   "routine": [
    "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).",
    "In:  A = character, $9277, $9253 buffer",
    "Out: $9253-$9276, $9277, $0548-$056C status line, zp_72 = 0, sound 5",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1414": {
   "routine": [
    "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.",
    "In:  none",
    "Out: menuSelection",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "141D": {
   "routine": [
    "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.",
    "In:  $922A",
    "Out: menuSelection, screen",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1439": {
   "routine": [
    "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).",
    "In:  zp_5D, zp_6C, $90F9",
    "Out: $90F9 menuSelection, screen",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "14F0": {
   "routine": [
    "snapshotSetupId - Copies $9241-$9243 to $92D7-$92D9 and map-table byte $F9C4[$9236] & $3F to $92D6 - remembers the identity of the current setup.",
    "In:  $9241-$9243, $9236",
    "Out: $92D6-$92D9",
    "(confidence: low)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1507": {
   "routine": [
    "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.",
    "In:  $0BA8, $922A, $9241-$9243, $92D6-$92D9",
    "Out: $90F9",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "153B": {
   "routine": [
    "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.",
    "In:  none",
    "Out: screen",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1565": {
   "routine": [
    "drawMenuFrame - Draws two frame elements (sub_2C7F with X=$18 and $19) then falls into clearMenuTextWindow.",
    "In:  none",
    "Out: screen",
    "(confidence: low)"
   ],
   "unit": "game/mainProgram0800"
  },
  "156F": {
   "routine": [
    "clearMenuTextWindow - Sets the menu text attribute and window bounds and clears the window ($C06A).",
    "In:  $922A, $90F9",
    "Out: zp_39-zp_40, zp_3D",
    "(confidence: low)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1575": {
   "routine": [
    "setMenuTextWindow - Sets the text attribute from link state (sub_16EB) then the menu window bounds.",
    "In:  $922A",
    "Out: zp_3D, zp_39-zp_40",
    "(confidence: low)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1578": {
   "routine": [
    "setMenuWindowBounds - Text window for the menu panel: cursor col 0, top row 7, cursor row 7, left column 2, bottom 22, right column 36.",
    "In:  none",
    "Out: zp_39=7, zp_3A=$16, zp_3B=2, zp_3C=$24, zp_3F=0, zp_40=7",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "158F": {
   "routine": [
    "clearMenuWindowPlain - Attribute 0, menu window bounds, clear window; jumped to from $2E0A.",
    "In:  none",
    "Out: zp_3D=0, screen",
    "(confidence: low)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1599": {
   "routine": [
    "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.",
    "In:  $0BA3, $0BA1, $0B7F",
    "Out: screen, zp_3B/zp_3D/zp_3F",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "0803": {
   "unit": "game/mainProgram0800",
   "block": [
    "loaderStatus: byteTable, 1 bytes. bit 7 = a read error occurred"
   ]
  },
  "08E8": {
   "unit": "game/mainProgram0800",
   "block": [
    "sendByteShift: byteTable, 1 bytes. shift register for the byte being sent"
   ]
  },
  "08E9": {
   "unit": "game/mainProgram0800",
   "block": [
    "recvByteShift: byteTable, 2 bytes. shift register for the byte received during a send; $08EA unused"
   ]
  },
  "0993": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "loadDestLo: byteTable, 1 bytes. transfer address low byte (self-modified)"
   ]
  },
  "09A3": {
   "unit": "game/mainProgram0800",
   "block": [
    "loadDestHi: byteTable, 2 bytes. transfer address high byte; $09A4 unused"
   ]
  },
  "09A5": {
   "unit": "game/mainProgram0800",
   "block": [
    "savedSpriteEnable: byteTable, 1 bytes. sprite enable saved during a sector transfer"
   ]
  },
  "0A47": {
   "unit": "game/mainProgram0800",
   "block": [
    "padA47: unknown, 1 bytes. single $20 byte before msgIdentifyMap"
   ]
  },
  "0A48": {
   "unit": "game/mainProgram0800",
   "block": [
    "msgIdentifyMap: text, 36 bytes. copy-protection prompt"
   ]
  },
  "0B7D": {
   "unit": "game/mainProgram0800",
   "block": [
    "inputLockoutTimer: byteTable, 2 bytes. $0B7D lockout timer, $0B7E settings byte"
   ]
  },
  "0B7F": {
   "unit": "game/mainProgram0800",
   "block": [
    "ownerNameString: text, 21 bytes. owner name string built at run time"
   ]
  },
  "0B94": {
   "unit": "game/mainProgram0800",
   "block": [
    "gameSettingsBlock: byteTable, 33 bytes. $0B94-$0BB4: game settings/state bytes (mapSeed $0B95-97, flags $0B9B-$0BA8, two 6-byte text fields $0BA9/$0BAF)"
   ]
  },
  "0C8A": {
   "unit": "game/mainProgram0800",
   "block": [
    "perSideValueTable: byteTable, 2 bytes. $31,$63 indexed by player side"
   ]
  },
  "0CE4": {
   "unit": "game/mainProgram0800",
   "block": [
    "strOurs: text, 4 bytes. 'OURS' default owner name"
   ]
  },
  "0D67": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "shiftedKeyTable: byteTable, 16 bytes. shifted replacements for key codes $AC-$BB (',' '-' '.' '/' '0'-'9' ':' ';')"
   ]
  },
  "0F81": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "msgInsertGameDisk: text, 17 bytes. 'INSERT GAME DISK.' - shown when the 'OZ' game disk is missing."
   ]
  },
  "0FA2": {
   "unit": "game/mainProgram0800",
   "block": [
    "msgDiskError: text, 11 bytes. 'DISK ERROR.' - shown by diskErrorExit when the BAM read fails."
   ]
  },
  "0FAD": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "padIrq1299: unknown, 3 bytes. Three unreferenced bytes $00,$09,$13 between irqRti and updateFrameTimers - alignment padding or an assembler leftover."
   ]
  },
  "131A": {
   "unit": "game/mainProgram0800",
   "block": [
    "cursorBlinkTimer: byteTable, 1 bytes. 30-frame countdown used by blinkCursorColor; stored immediately before the routine that owns it."
   ]
  },
  "1412": {
   "unit": "game/mainProgram0800",
   "block": [
    "menuFlag1412: byteTable, 2 bytes. $1412 menu flag, $1413 link-state copy"
   ]
  },
  "1535": {
   "unit": "game/mainProgram0800",
   "block": [
    "menuItemHandlerTable: addrTable, 6 bytes. 3 handler addresses for fire on menu item 0-2: $158E (RTS), $17DA, $1A49"
   ]
  },
  "163A": {
   "unit": "game/mainProgram0800",
   "block": [
    "tabColumnTable: byteTable, 3 bytes. screen column of tab 0..2 boxes ($0B,$13,$1B)"
   ]
  },
  "163D": {
   "unit": "game/mainProgram0800",
   "block": [
    "tabColourTable: byteTable, 3 bytes. normal colour byte of tabs (green $05, blue $06, red $02)"
   ]
  },
  "1640": {
   "unit": "game/mainProgram0800",
   "block": [
    "tabHighlightColourTable: byteTable, 3 bytes. bright colour byte of tabs ($0D,$0E,$0A)"
   ]
  },
  "1643": {
   "routine": [
    "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.",
    "In:  none",
    "Out: screen updated; zp_21 used as loop counter; zp_39-zp_40, zp_3D, zp_3F clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "168C": {
   "routine": [
    "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.",
    "In:  X = tab index 0..2",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "1691": {
   "routine": [
    "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.",
    "In:  X = tab index 0..2",
    "Out: screen colour RAM updated; D_2A2D/D_2A2E set"
   ],
   "unit": "game/mainProgram0800"
  },
  "16B7": {
   "routine": [
    "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.",
    "In:  zp_6E countdown, D_90F9 selected tab, D_1889",
    "Out: zp_6E=10, D_1889 toggled, tab recoloured"
   ],
   "unit": "game/mainProgram0800"
  },
  "16D6": {
   "routine": [
    "printEnergyLabel - Prints the string 'ENERGY' ($CBB8) at column $1C, row 8 of the REPAIR tab using the menu text colour.",
    "In:  none",
    "Out: zp_3D, zp_3F, zp_40 set; text printed"
   ],
   "unit": "game/mainProgram0800"
  },
  "16EB": {
   "routine": [
    "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).",
    "In:  D_922A, D_90F9",
    "Out: zp_3D"
   ],
   "unit": "game/mainProgram0800"
  },
  "16FF": {
   "routine": [
    "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.",
    "In:  D_9236 comcen unit index, D_F9C4 energy table, D_16FE",
    "Out: D_16FE updated, D_9241 (energy status) updated, text printed; saves/restores the text window (sub_C41B/sub_C40E)"
   ],
   "unit": "game/mainProgram0800"
  },
  "175A": {
   "routine": [
    "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.",
    "In:  D_922A stun status, D_9236, D_F9C4",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "17DA": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "188B": {
   "routine": [
    "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.",
    "In:  D_91D5 selected system 0..3, blinkPhase $1889",
    "Out: blinkPhase toggled; falls through to drawSystemMarker or jumps to eraseSystemMarker (X=$16)"
   ],
   "unit": "game/mainProgram0800"
  },
  "189D": {
   "routine": [
    "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.",
    "In:  Y = system index 0..3, D_9241 comcenSystemStatus, showOkMarkers $188A, systemMarkerColumnTable/systemMarkerRowTable",
    "Out: screen RAM colour cells at $8C00; D_2A2A fill colour, D_2A2D row, D_2A2E column; self-modifies $18A4; energy readout refreshed"
   ],
   "unit": "game/mainProgram0800"
  },
  "1919": {
   "routine": [
    "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.",
    "In:  Y = system index 0..3, X = colour byte (always $16)",
    "Out: D_2A2D/D_2A2E set, 3x3 colour rectangle filled via fillScreenColourRect"
   ],
   "unit": "game/mainProgram0800"
  },
  "192D": {
   "routine": [
    "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.",
    "In:  D_9236 comcenUnit, repairAmount $17D9",
    "Out: zp_74 = comcen unit, zp_75 = amount; command $9A queued, pendingOwnCmdCount $929F incremented"
   ],
   "unit": "game/mainProgram0800"
  },
  "193C": {
   "routine": [
    "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.",
    "In:  D_91D5 selected system, D_9241 comcenSystemStatus, comcenEnergySnapshot $17D8, D_9236, D_F9C4 unitEnergyTable, nextGameRandom",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "19B0": {
   "routine": [
    "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.",
    "In:  D_91D5, D_92C9 gameEndFlag",
    "Out: zp_6F counted down by the IRQ, blinkPhase toggled, marker drawn/erased, battle advanced"
   ],
   "unit": "game/mainProgram0800"
  },
  "19C9": {
   "routine": [
    "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.",
    "In:  none",
    "Out: text drawn into the bitmap at column 10, row 20; text window restored from $92B6"
   ],
   "unit": "game/mainProgram0800"
  },
  "19D6": {
   "routine": [
    "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.",
    "In:  zp_3E fill character (0 while the repair menus are up)",
    "Out: 20 characters of the message line blanked; text window restored"
   ],
   "unit": "game/mainProgram0800"
  },
  "19E1": {
   "routine": [
    "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.",
    "In:  D_922A, D_90F9 (through setMenuTextColour)",
    "Out: zp_39-zp_40 window, zp_3D colour, cursor pointer recomputed; previous window saved at $92B6"
   ],
   "unit": "game/mainProgram0800"
  },
  "19F3": {
   "routine": [
    "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.",
    "In:  D_0BA8, D_92C9, D_0B9B, D_0BA5, D_E03B",
    "Out: text printed; zp_39-zp_3F set"
   ],
   "unit": "game/mainProgram0800"
  },
  "1A49": {
   "routine": [
    "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.",
    "In:  D_92C9, D_0BA8, D_0B9B, D_0BA5, D_E03B, D_0B9C, D_E030, D_92A1",
    "Out: D_91D4/D_91D5/D_91D6 menu state, D_92A1, zp_73, D_9163, zp_7F; may end the game via endGameToMainMenu",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1ADC": {
   "routine": [
    "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.",
    "In:  D_67C8, D_E030, D_E03B, zp_B1, D_0B9B, D_0B9C",
    "Out: D_92C9=0, zp_BE=0, D_908D/D_908E=0, D_92FD, D_0BA8, D_0B9C, D_E03B",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1B33": {
   "routine": [
    "hangUpModem - Entry that hangs up with a resulting commSessionState of 0 (falls into hangUpModemSetState with A=0).",
    "In:  none",
    "Out: see hangUpModemSetState"
   ],
   "unit": "game/mainProgram0800"
  },
  "1B35": {
   "routine": [
    "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.",
    "In:  A = new value for D_0B9C, D_E03B, D_E030, D_E03C",
    "Out: D_0B9C, comm flags cleared by sub_E003, zp_7F; self-modifies L_1B59"
   ],
   "unit": "game/mainProgram0800"
  },
  "1B5F": {
   "routine": [
    "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.",
    "In:  D_E03B, D_0BA5",
    "Out: comm link flags set"
   ],
   "unit": "game/mainProgram0800"
  },
  "1B74": {
   "routine": [
    "openLinkAndWaitForConnection - openCommLink followed by waitForConnection.",
    "In:  none",
    "Out: see callees"
   ],
   "unit": "game/mainProgram0800"
  },
  "1B77": {
   "routine": [
    "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).",
    "In:  D_0BA5",
    "Out: D_92A1, zp_18, zp_BE"
   ],
   "unit": "game/mainProgram0800"
  },
  "1B9A": {
   "routine": [
    "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.",
    "In:  zp_B1, D_E03C, D_E03D, zp_18",
    "Out: zp_18, sound off",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1BC1": {
   "routine": [
    "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.",
    "In:  A = message id",
    "Out: D_92A1, D_9277, D_929C, D_0B7D, D_9248"
   ],
   "unit": "game/mainProgram0800"
  },
  "1BF7": {
   "routine": [
    "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).",
    "In:  D_0B9C",
    "Out: zp_7B, D_91D5",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1C08": {
   "routine": [
    "hangUpAndPause - hangUpModemSetState with A = current $0B9C (or 0 if it is negative), then waits 250 frames (sub_C37F).",
    "In:  D_0B9C",
    "Out: D_0B9C"
   ],
   "unit": "game/mainProgram0800"
  },
  "1C17": {
   "routine": [
    "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.",
    "In:  D_0B9C",
    "Out: zp_73=5, command queued, link closed, D_0B9C=0",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1C2C": {
   "routine": [
    "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.",
    "In:  D_0B9C, D_92FD, zp_3B",
    "Out: D_91D4, D_91D5, D_91D6, D_90F8=$F0, zp_B1=$F0, zp_3D/zp_42=$60, D_0BA7",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1C7B": {
   "routine": [
    "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.",
    "In:  D_0BA3",
    "Out: Z flag, A = type",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1C87": {
   "routine": [
    "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.",
    "In:  D_0B9B, D_0BA4, D_0BAF.., D_0B98.., D_0BA2, D_0B9E",
    "Out: D_0B9B=0, D_0BA9.., D_0B95.., D_0BA1, D_0B9D, D_0BA3",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1CC1": {
   "routine": [
    "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).",
    "In:  Y = row delta, zp_B1",
    "Out: zp_F8, Y adjusted",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1CD3": {
   "routine": [
    "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.",
    "In:  X = dx, Y = dy, zp_A5, zp_A6, zp_B1, D_920A, D_922E-D_9231",
    "Out: zp_A5, zp_A6, zp_F0, zp_F1, zp_F8, D_91CC=0; screen scrolled"
   ],
   "unit": "game/mainProgram0800"
  },
  "1D4E": {
   "routine": [
    "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).",
    "In:  A = ticks per step (0 = immediate), X = unit index, zp_92/zp_93 current cell",
    "Out: D_1D4C, D_1D4D, zp_6E, zp_92-zp_97, zp_8C-zp_91, zp_A5/zp_A6, screen"
   ],
   "unit": "game/mainProgram0800"
  },
  "1D97": {
   "routine": [
    "setLineTargetToUnit - Loads the Bresenham target from the unit position tables: zp_95 = unitXTable[X], zp_94 = unitYTable[X].",
    "In:  X = unit index",
    "Out: zp_94, zp_95"
   ],
   "unit": "game/mainProgram0800"
  },
  "1DE4": {
   "routine": [
    "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.",
    "In:  zp_F0, zp_F1, zp_A5, zp_A6",
    "Out: zp_F2-zp_F7, zp_54-zp_56, bitmap/screen/colour RAM; self-modifies L_1E81-L_1EBF and D_1E89/D_1EBE opcodes"
   ],
   "unit": "game/mainProgram0800"
  },
  "1F4E": {
   "routine": [
    "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.",
    "In:  zp_A5, zp_A6, D_922E-D_9231, zp_B1",
    "Out: zp_A5, zp_A6, zp_18, zp_19, zp_F8; view redrawn"
   ],
   "unit": "game/mainProgram0800"
  },
  "16FE": {
   "unit": "game/mainProgram0800",
   "block": [
    "lastEnergyShown: byteTable, 1 bytes. energy value last printed ($FF forces a reprint)"
   ]
  },
  "17D8": {
   "unit": "game/mainProgram0800",
   "block": [
    "comcenEnergySnapshot: byteTable, 1 bytes. comcen energy (0..50) when the REPAIR tab was drawn / after a repair"
   ]
  },
  "17D9": {
   "unit": "game/mainProgram0800",
   "block": [
    "repairAmount: byteTable, 1 bytes. energy added by the last repair attempt"
   ]
  },
  "1881": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "systemMarkerRowTable: byteTable, 4 bytes. Screen row of the four comcen system markers: $0E, $0A, $0A, $0C."
   ]
  },
  "1889": {
   "unit": "game/mainProgram0800",
   "block": [
    "blinkPhase: byteTable, 1 bytes. Shared 0/1 blink phase for the menu tab blink and the comcen system marker blink."
   ]
  },
  "188A": {
   "unit": "game/mainProgram0800",
   "block": [
    "showOkMarkers: byteTable, 1 bytes. Flag: draw markers for undamaged systems as well (set only while the REPAIR WHICH menu is open)."
   ]
  },
  "1BC0": {
   "unit": "game/mainProgram0800",
   "block": [
    "orphanRts1BC0: unknown, 1 bytes. unreachable $60 (RTS) between waitForCarrier and showMessageWaitForSpace"
   ]
  },
  "1D4C": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollTargetUnit: byteTable, 1 bytes. unit index parameter of scrollViewToUnit"
   ]
  },
  "1D4D": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollStepDelay: byteTable, 1 bytes. ticks per step parameter of scrollViewToUnit (0 = immediate)"
   ]
  },
  "1DA5": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXEndIndex: byteTable, 3 bytes. end Y index of the bitmap byte copy ($FF,$70,$60)"
   ]
  },
  "1DA8": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXStepOpcode: byteTable, 3 bytes. DEY ($88) or INY ($C8) opcode patched into both copy loops"
   ]
  },
  "1DAB": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXBitmapSrcOffset: byteTable, 3 bytes. byte offset added to the bitmap source address (0,0,$10)"
   ]
  },
  "1DAE": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXBitmapDstOffset: byteTable, 3 bytes. byte offset added to the bitmap destination address ($10,0,0)"
   ]
  },
  "1DB1": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYBitmapSrcLo: byteTable, 3 bytes. per vertical direction: bitmap source address lo ($AAC8,$A208,$A488)"
   ]
  },
  "1DB4": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYBitmapSrcHi: byteTable, 3 bytes. bitmap source address hi"
   ]
  },
  "1DB7": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYBitmapDstLo: byteTable, 3 bytes. bitmap destination address lo ($AD48,$A208,$A208)"
   ]
  },
  "1DBA": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYBitmapDstHi: byteTable, 3 bytes. bitmap destination address hi"
   ]
  },
  "1DBD": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYBitmapStrideLo: byteTable, 3 bytes. bitmap row stride lo ($FEC0 = -320 when scrolling up, $0140 = +320 otherwise)"
   ]
  },
  "1DC0": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYBitmapStrideHi: byteTable, 3 bytes. bitmap row stride hi"
   ]
  },
  "1DC3": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYRowCount: byteTable, 3 bytes. character rows to copy (8,10,8)"
   ]
  },
  "1DC6": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXScreenStartIndex: byteTable, 3 bytes. start Y index of the screen/colour copy ($0B,0,0)"
   ]
  },
  "1DC9": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXScreenEndIndex: byteTable, 3 bytes. end Y index of the screen/colour copy ($FF,$0E,$0C)"
   ]
  },
  "1DCC": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXScreenSrcOffset: byteTable, 3 bytes. column offset of the screen source (0,0,2)"
   ]
  },
  "1DCF": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollXScreenDstOffset: byteTable, 3 bytes. column offset of the screen destination (2,0,0)"
   ]
  },
  "1DD2": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYScreenSrcLo: byteTable, 3 bytes. screen RAM source address lo ($8D59 row 8, $8C41 row 1, $8C91 row 3; column 25)"
   ]
  },
  "1DD5": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYScreenSrcHi: byteTable, 3 bytes. screen RAM source address hi (colour RAM = +$4C00)"
   ]
  },
  "1DD8": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYScreenDstLo: byteTable, 3 bytes. screen RAM destination address lo ($8DA9, $8C41, $8C41)"
   ]
  },
  "1DDB": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYScreenDstHi: byteTable, 3 bytes. screen RAM destination address hi"
   ]
  },
  "1DDE": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYScreenStrideLo: byteTable, 3 bytes. screen row stride lo ($FFD8 = -40 up, $0028 = +40)"
   ]
  },
  "1DE1": {
   "unit": "game/mainProgram0800",
   "block": [
    "scrollYScreenStrideHi: byteTable, 3 bytes. screen row stride hi"
   ]
  },
  "1FC3": {
   "routine": [
    "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.",
    "In:  D_92C3/D_92C4 saved viewport origin",
    "Out: zp_A5/zp_A6 viewport origin; screen redrawn; does not return (falls into sub_1FD0)"
   ],
   "unit": "game/mainProgram0800"
  },
  "1FD0": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "20F4": {
   "routine": [
    "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).",
    "In:  A = direction code 0-7, zp_6C, zp_BB, zp_A5/zp_A6, D_90F8, D_FAF0 terrain under unit",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2185": {
   "routine": [
    "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.",
    "In:  zp_BB target mode, D_9209, D_90F8, zp_B1, D_90FC, D_90EE, zp_5D, zp_6C",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2264": {
   "routine": [
    "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.",
    "In:  D_90F8, zp_A5/zp_A6, D_F640/D_F6A4, D_F76C",
    "Out: zp_94/zp_95 target, sprite 1 moved/hidden"
   ],
   "unit": "game/mainProgram0800"
  },
  "22A2": {
   "routine": [
    "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.",
    "In:  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",
    "Out: D_91D7 (unit shown, $FF on exit), D_90EF = 0, zp_3D, D_920E cleared, D_9022 bits 5-7 cleared, text window reset"
   ],
   "unit": "game/mainProgram0800"
  },
  "2367": {
   "routine": [
    "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.",
    "In:  zp_66, zp_5D, zp_5F/zp_79",
    "Out: C flag, D_0B7D = $1E when interrupted"
   ],
   "unit": "game/mainProgram0800"
  },
  "2384": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "24E4": {
   "routine": [
    "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.",
    "In:  D_90F8, D_F7D0, D_F834",
    "Out: zp_94/zp_95, sprite 1"
   ],
   "unit": "game/mainProgram0800"
  },
  "24F3": {
   "routine": [
    "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).",
    "In:  D_90EE, zp_6C, D_24E1/D_24E2/D_24E3, zp_94/zp_95 target",
    "Out: zp_92/zp_93, D_24E2/D_24E3, zp_6C = 4, sprite 1 position/enable (D_9011/D_9019/D_9022)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2540": {
   "routine": [
    "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).",
    "In:  D_90F8, D_920A",
    "Out: D_91CC = 0, sprite 1 image/colour ($8A40, D_9009), D_24E1-D_24E3"
   ],
   "unit": "game/mainProgram0800"
  },
  "256F": {
   "routine": [
    "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.",
    "In:  D_90F8, D_F640, D_F6A4",
    "Out: D_24E1 = 5, D_24E2/D_24E3, sprite 1 position"
   ],
   "unit": "game/mainProgram0800"
  },
  "2585": {
   "routine": [
    "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.",
    "In:  X = map column, Y = map row",
    "Out: D_9011 (sprite 1 X), D_9019 (sprite 1 Y)"
   ],
   "unit": "game/mainProgram0800"
  },
  "258F": {
   "routine": [
    "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.",
    "In:  D_90F8, zp_A5/zp_A6",
    "Out: zp_92/zp_93 (start point), viewport scrolled"
   ],
   "unit": "game/mainProgram0800"
  },
  "25B7": {
   "routine": [
    "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.",
    "In:  D_9248",
    "Out: D_9248",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "25C4": {
   "routine": [
    "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.",
    "In:  zp_B1 must be nonzero (setup phase)",
    "Out: C flag from sub_8658"
   ],
   "unit": "game/mainProgram0800"
  },
  "25C7": {
   "routine": [
    "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.",
    "In:  zp_A5, zp_A6, D_BFB0/D_BFD8 map row tables",
    "Out: zp_48/zp_49, A = map byte (only meaningful when in range), Y = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "25E3": {
   "routine": [
    "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.",
    "In:  Y = map row, zp_A5",
    "Out: zp_48/zp_49, A = map byte, Y = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "25FA": {
   "routine": [
    "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.",
    "In:  X = map column 0-39, Y = map row 0-39, D_BFB0/D_BFD8 row address tables (built at $0345/$034A)",
    "Out: A = map byte (bit 7 set = unit, low 7 bits = unit index; else terrain code, $40 = blank), zp_48/zp_49 pointer, Y = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "260D": {
   "routine": [
    "readCellUnderCursor - Reads the map cell at the cursor, which is always the centre of the 7x5 viewport: (zp_A5+3, zp_A6+2).",
    "In:  zp_A5, zp_A6",
    "Out: A = map byte, X/Y = cursor column/0, zp_48/zp_49"
   ],
   "unit": "game/mainProgram0800"
  },
  "261C": {
   "routine": [
    "clearInfoPanel - Sets the info-panel text window (sub_2622) and clears it with the text engine's window clear (sub_C06A).",
    "In:  zp_3D text colour, zp_3E fill character",
    "Out: zp_39-zp_3C window, zp_3F/zp_40 cursor, panel area cleared"
   ],
   "unit": "game/mainProgram0800"
  },
  "2622": {
   "routine": [
    "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).",
    "In:  -",
    "Out: zp_39 = $0C, zp_3A = $15, zp_3B = $19, zp_3C = $0E"
   ],
   "unit": "game/mainProgram0800"
  },
  "2636": {
   "routine": [
    "clearScreenAndRedrawGameScreen - Clears the bitmap screen to grey (sub_2D41) and falls into redrawGameScreen. Called at start-up ($0A95) and from overlay A.",
    "In:  -",
    "Out: whole screen"
   ],
   "unit": "game/mainProgram0800"
  },
  "2639": {
   "routine": [
    "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).",
    "In:  zp_B1, D_90FB, map, zp_7B",
    "Out: D_9001 = $D8, D_9004 = 7, D_9226 = 0, zp_3D, D_2A29-D_2A2C = 0, screen"
   ],
   "unit": "game/mainProgram0800"
  },
  "268F": {
   "routine": [
    "drawMiniMapFrame - Draws panel 0: the 22x22 character frame at column 0, row 0 around the strategic mini-map, in colour $0C.",
    "In:  -",
    "Out: zp_3D = $0C, X = 0 -> sub_2C7F"
   ],
   "unit": "game/mainProgram0800"
  },
  "2698": {
   "routine": [
    "drawViewportFrame - Draws panel 1: the 16x12 frame at column 24, row 0 around the tactical viewport, then falls into clearViewportTextArea.",
    "In:  -",
    "Out: zp_3D, frame drawn, viewport interior cleared"
   ],
   "unit": "game/mainProgram0800"
  },
  "26A1": {
   "routine": [
    "clearViewportTextArea - Defines the viewport interior as a text window (rows 1-10, column 25, width 14, colour 0) and clears it.",
    "In:  -",
    "Out: zp_39 = 1, zp_3A = $0B, zp_3B = $19, zp_3C = $0E, zp_3D = 0, area cleared"
   ],
   "unit": "game/mainProgram0800"
  },
  "26B8": {
   "routine": [
    "drawInfoPanelFrame - Draws panel 2: the 16x11 frame at column 24, row 11 around the unit info panel.",
    "In:  -",
    "Out: zp_3D = $0C, X = 2 -> sub_2C7F"
   ],
   "unit": "game/mainProgram0800"
  },
  "26C1": {
   "routine": [
    "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).",
    "In:  zp_A5/zp_A6 viewport origin, map at $F000, unit arrays",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "1FA6": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "pathMarkerState: byteTable, 3 bytes. Variables embedded in code: repeat counter (initial 5), marker X, marker Y of the animated mini-map path marker."
   ]
  },
  "2772": {
   "routine": [
    "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).",
    "In:  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'",
    "Out: $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"
   ],
   "unit": "game/mainProgram0800"
  },
  "28E1": {
   "routine": [
    "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).",
    "In:  D_90FB",
    "Out: zp_3D; window frame on screen; D_2A2D/D_2A2E/D_9221/D_9222 set by sub_2C7F",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "28F1": {
   "routine": [
    "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.",
    "In:  Y (preserved); D_0BA8, D_0B9F, D_92A2-D_92A5",
    "Out: 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",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "29B3": {
   "routine": [
    "setOtherPlayerPoints - Helper of computeGameResult: stores Y as the 16-bit point total of the player other than X (X ^ 1), high byte 0.",
    "In:  X = player index, Y = value",
    "Out: D_92A2[X^1] = Y, D_92A4[X^1] = 0; X = X^1"
   ],
   "unit": "game/mainProgram0800"
  },
  "29C1": {
   "routine": [
    "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.",
    "In:  D_92C8, D_9000, D_9001, PRNG state zp_57-59",
    "Out: D_9000, D_9001, D_92F4, D_92F5; self-modified operands $29D6,$29E6,$29F3,$2A03"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A08": {
   "routine": [
    "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).",
    "In:  D_0B9B bit 0, opcode byte at $2840",
    "Out: opcode bytes at $2846 and $5BFE"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A1A": {
   "routine": [
    "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.",
    "In:  A = word index (0-2)",
    "Out: text printed at the current text cursor; A/Y = string pointer passed to sub_C133"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A38": {
   "routine": [
    "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.",
    "In:  Y = mode 0-3, A preserved",
    "Out: opcode at $2AF3 and $C22B; operand at $2A45"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A47": {
   "routine": [
    "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.",
    "In:  Y = mode 0-3, A preserved",
    "Out: opcode at $2B00 and $2A85; operand at $2A54"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A56": {
   "routine": [
    "resetDrawModes - Sets both the bitmap and the colour draw modes back to 0 (replace). Y is preserved.",
    "In:  Y preserved",
    "Out: opcodes at $2AF3, $C22B, $2B00, $2A85 = LDA"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A64": {
   "routine": [
    "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.",
    "In:  A = colour byte, X = width, Y = height, D_2A2D/D_2A2E origin",
    "Out: screen RAM cells; D_2A2A = A; zp_B6/B7, zp_1B, zp_1C clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "2A9F": {
   "routine": [
    "calcCellPointers - Computes the bitmap address and screen RAM address of the character cell at row zp_18, column zp_19 (falls into calcCellPointersXY).",
    "In:  zp_18 = row 0-24, zp_19 = column 0-39",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2AA3": {
   "routine": [
    "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.",
    "In:  Y = row, X = column",
    "Out: zp_B4/B5 bitmap pointer, zp_B6/B7 screen RAM pointer"
   ],
   "unit": "game/mainProgram0800"
  },
  "2AC3": {
   "routine": [
    "drawCharAt - Draws character A at row zp_18, column zp_19: computes the cell pointers and falls into plotGlyph.",
    "In:  A = character code, zp_18/zp_19",
    "Out: see plotGlyph"
   ],
   "unit": "game/mainProgram0800"
  },
  "2AC8": {
   "routine": [
    "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).",
    "In:  A = character code (0-113); zp_B4/B5, zp_B6/B7 pointers; D_2A29, D_2A2A, zp_3D, D_9001",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2B18": {
   "routine": [
    "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).",
    "In:  A/Y = pointer to block data; D_2A2D, D_2A2E, D_2A2B, D_2A2C",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2B75": {
   "routine": [
    "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.",
    "In:  D_2A2D, D_2A2E, D_9221, D_9222",
    "Out: 4 characters drawn; zp_18/19/1A/1B clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2BB7": {
   "routine": [
    "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.",
    "In:  A = symbol index, zp_18/zp_19",
    "Out: two characters drawn"
   ],
   "unit": "game/mainProgram0800"
  },
  "2BCE": {
   "routine": [
    "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.",
    "In:  A = colour byte, D_2A2D/D_2A2E",
    "Out: 4 screen RAM cells toggled; draw modes reset; D_2A2A = A",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2762": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "textDrawParams: byteTable, 7 bytes. parameter block of the glyph plotters: useFixedColourFlag, drawColourByte, blockFillFlag, blockFillChar, windowRow, windowCol, one filler byte"
   ]
  },
  "2A30": {
   "unit": "game/mainProgram0800",
   "block": [
    "bitmapDrawOpcodes: byteTable, 4 bytes. opcodes LDA/ORA/EOR/CMP (zp),Y ($B1,$11,$51,$D1) for the bitmap draw modes 0-3"
   ]
  },
  "2A34": {
   "unit": "game/mainProgram0800",
   "block": [
    "colourDrawOpcodes: byteTable, 4 bytes. opcodes LDA/ORA/EOR/CMP abs ($AD,$0D,$4D,$CD) for the colour draw modes 0-3"
   ]
  },
  "2C7F": {
   "routine": [
    "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).",
    "In:  X = window index 0-26",
    "Out: fillColumn, fillRow, windowWidth, windowHeight set; frame drawn; windowWidth/windowHeight left decremented by drawFrameBox; A/X/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "2CA1": {
   "routine": [
    "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.",
    "In:  A/Y = lo/hi pointer to the 9-byte tile set; fillRow, fillColumn, windowWidth, windowHeight; current colour state (colourOverride/fillColourValue/charColourTable)",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2D1D": {
   "routine": [
    "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.",
    "In:  Y = tile offset in the set, zp_B8/zp_B9 = tile set, zp_18 = first row, zp_19 = column, zp_1B = stop row",
    "Out: zp_18 = zp_1B; zp_1C = tile code; cells drawn"
   ],
   "unit": "game/mainProgram0800"
  },
  "2D2F": {
   "routine": [
    "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.",
    "In:  Y = tile offset, zp_B8/zp_B9 = tile set, zp_18 = row, zp_19 = first column, zp_1A = stop column",
    "Out: zp_19 = zp_1A; zp_1C = tile code; cells drawn"
   ],
   "unit": "game/mainProgram0800"
  },
  "2D41": {
   "routine": [
    "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).",
    "In:  none",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2DBD": {
   "routine": [
    "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.",
    "In:  A = terrain tile index (map byte - $40)",
    "Out: zp_B2/zp_B3 = tile pointer, colourOverride ($2A29) = 0, Y = 0",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2DDB": {
   "routine": [
    "clearMessageLineAndScreen - Clears the message line via $C39F and falls into clearCurrentScreenWindows. Called from the screen-switch code at $070C.",
    "In:  currentScreen ($90FB)",
    "Out: see clearCurrentScreenWindows",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2DDE": {
   "routine": [
    "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.",
    "In:  currentScreen ($90FB), commPauseFlags (zp_BE)",
    "Out: spriteEnableShadow = 0 (or 2 for screen 2), zp_3D = 0 for the battlefield; tail-calls the per-screen routine",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2E0F": {
   "routine": [
    "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.",
    "In:  fillRow, fillColumn, windowWidth, windowHeight",
    "Out: 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",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2F34": {
   "routine": [
    "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.",
    "In:  zp_30 = source row offset, zp_31 = source column offset, fillRow, fillColumn, zp_3F/zp_40 = destination origin",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "2F6A": {
   "routine": [
    "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.",
    "In:  none",
    "Out: spriteEnableShadow = 0, $8FF8-$8FFF set, zp_48/zp_49 = $8FF8, A/X/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "2FA5": {
   "routine": [
    "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).",
    "In:  none",
    "Out: $8A00-$8BFF = 0, X = 0, A = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "2FB2": {
   "routine": [
    "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).",
    "In:  A = sprite number 0-7",
    "Out: sprite shape cleared; A = 0, Y = $FF"
   ],
   "unit": "game/mainProgram0800"
  },
  "2FCF": {
   "routine": [
    "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).",
    "In:  none",
    "Out: sprite 0 shape and colour, spriteEnableShadow bit 0 set",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2FF3": {
   "routine": [
    "setCursorShapeCross - Same as setCursorShapeSquare but with the thick cross/plus shape at $9E80 (used when zp_BB is positive, see $2195).",
    "In:  none",
    "Out: sprite 0 shape = cross, colour 0, sprite 0 enabled",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "2FFA": {
   "routine": [
    "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).",
    "In:  cursorSpriteIndex ($91CC) = sprite number",
    "Out: sprite shape rewritten, spriteColorShadow[n] = $0C for n != 0",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3023": {
   "routine": [
    "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.",
    "In:  X = map column, Y = map row, groupBoxActive, $922F, $9231",
    "Out: X = sprite X, Y = sprite Y",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "303A": {
   "routine": [
    "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).",
    "In:  X = map column 0-39, Y = map row 0-39",
    "Out: X = sprite X coordinate, Y = sprite Y coordinate"
   ],
   "unit": "game/mainProgram0800"
  },
  "3049": {
   "routine": [
    "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.",
    "In:  zp_A5, zp_A6, cursorSpriteIndex, groupBoxActive and offsets",
    "Out: spriteXShadow[n], spriteYShadow[n], spriteEnableShadow bit n"
   ],
   "unit": "game/mainProgram0800"
  },
  "306C": {
   "routine": [
    "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.",
    "In:  A = 0 solid / $80 dotted; zp_92-zp_95 line endpoints",
    "Out: pixels set in sprite 3; line stepper state consumed",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "308A": {
   "routine": [
    "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).",
    "In:  zp_92 = row, zp_93 = column",
    "Out: 4 pixels set in sprite 3; zp_93 = column + 1",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "30A1": {
   "routine": [
    "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).",
    "In:  zp_92 = row 0-20, zp_93 = column 0-23",
    "Out: one bit set in $8AC0-$8AFF; A/X/Y clobbered, operand at $30B4 patched"
   ],
   "unit": "game/mainProgram0800"
  },
  "30C0": {
   "routine": [
    "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.",
    "In:  selectedGroupId ($9206), selectedUnit ($90F8), unit arrays $F76C/$FB54/$F7D0/$F834",
    "Out: sprite 0 shape, spriteBuildBuffer, $922E-$9231, spriteXExpandShadow/spriteYExpandShadow bits 0-1, groupBoxActive = $FF, spritePlotMode = $40, view origin possibly nudged",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "31A3": {
   "routine": [
    "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.",
    "In:  A = sprite bit mask",
    "Out: spriteEnableShadow updated (or unchanged); A = combined mask"
   ],
   "unit": "game/mainProgram0800"
  },
  "31AF": {
   "routine": [
    "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.",
    "In:  A = sprite number (bit 7 = use $9025 buffer), X = column, Y = row, spritePlotMode",
    "Out: one pixel modified; zp_98/zp_99 = sprite row pointer, spritePlotTemp clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "31CA": {
   "routine": [
    "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).",
    "In:  A = sprite number, X = column, Y = row",
    "Out: pixel set"
   ],
   "unit": "game/mainProgram0800"
  },
  "3239": {
   "routine": [
    "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).",
    "In:  A = sprite number (stored to cursorSpriteIndex $91CC), X = radius index 1-10, spritePlotMode",
    "Out: circle plotted; $91CC = A, circleStepsLeft ($91F2) = -1, zp_AD-zp_B0 = table pointers"
   ],
   "unit": "game/mainProgram0800"
  },
  "3266": {
   "routine": [
    "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).",
    "In:  Y = step index, zp_AD/zp_AE and zp_AF/zp_B0 = dx/dy table pointers",
    "Out: $32DA-$32E9 filled"
   ],
   "unit": "game/mainProgram0800"
  },
  "32BF": {
   "routine": [
    "plotCircleOctantPoints - Plots the 8 points prepared by computeCircleOctantPoints into sprite cursorSpriteIndex via plotSpritePixel, iterating circlePointIndex ($3238) from 7 down to 0.",
    "In:  $32DA-$32E9, cursorSpriteIndex ($91CC), spritePlotMode",
    "Out: 8 pixels plotted; circlePointIndex = $FF"
   ],
   "unit": "game/mainProgram0800"
  },
  "2E0D": {
   "unit": "game/mainProgram0800",
   "block": [
    "magnifyOddFlags: byteTable, 2 bytes. magnifyWidthOdd and magnifyHeightOdd work variables"
   ]
  },
  "2F85": {
   "unit": "game/mainProgram0800",
   "block": [
    "spriteBitMasks: byteTable, 16 bytes. $01,$02,...,$80 (set masks for sprite n) followed by $FE,$FD,...,$7F (clear masks, used by $337D)"
   ]
  },
  "2F95": {
   "unit": "game/mainProgram0800",
   "block": [
    "spriteOffsetLoTable: byteTable, 8 bytes. Low byte of n*64 for sprite 0-7 ($00,$40,$80,$C0 repeating); added to $8A00"
   ]
  },
  "2F9D": {
   "unit": "game/mainProgram0800",
   "block": [
    "spriteOffsetHiTable: byteTable, 8 bytes. High byte of n*64 for sprite 0-7 (0,0,0,0,1,1,1,1)"
   ]
  },
  "3237": {
   "unit": "game/mainProgram0800",
   "block": [
    "spritePlotWork: byteTable, 2 bytes. spritePlotTemp and circlePointIndex work bytes"
   ]
  },
  "32EA": {
   "routine": [
    "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).",
    "In:  spriteEnableShadow $9022",
    "Out: $9022 and $D015 with bit 1 cleared; A clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "3302": {
   "routine": [
    "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).",
    "In:  none",
    "Out: sprite 2 data, $900A=0, $9023/$9024 bit 2 set, $9012=0, $9020 bit 2 set, $901A=$4D, sprite 2 enabled via sub_31A3",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3306": {
   "routine": [
    "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.",
    "In:  none",
    "Out: sprite 2 data and VIC shadow registers as for showCrosshairSprite",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3340": {
   "routine": [
    "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).",
    "In:  A = shape index 0-7, $91CC = target sprite (0 or 5)",
    "Out: $8A00 or $8B40 sprite data, sprite enabled; zp_98/zp_99 = source pointer (clobbered), X/Y clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3383": {
   "routine": [
    "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.",
    "In:  none",
    "Out: sprite 2 data/colour/position, zp_67=10, zp_68=8, sprite 2 enabled",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "33B1": {
   "routine": [
    "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.",
    "In:  X = grid column, Y = grid row",
    "Out: X = sprite X, Y = sprite Y"
   ],
   "unit": "game/mainProgram0800"
  },
  "340B": {
   "routine": [
    "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.",
    "In:  X = map column, Y = map row (passed to sub_7A13)",
    "Out: C = 0 and X/Y = sprite coordinates of the cell centre, or C = 1",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "341F": {
   "routine": [
    "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.",
    "In:  $923E, zp_A5/zp_A6 view origin, patched operands $3425/$3437",
    "Out: $9014/$901C sprite 4 position, sprite 4 enable bit set or cleared",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3456": {
   "routine": [
    "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.",
    "In:  none",
    "Out: sprite 4 data, $900C=$12, $91CC=4, $91F3=$40; self-modifies the loop counter at $346E",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "347F": {
   "routine": [
    "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).",
    "In:  $90F8 selected unit, zp_A5/zp_A6 view origin",
    "Out: zp_73/74/75 command parameters, $FB54[unit] bits 6/7, 4 bytes queued in the outgoing command ring, C=0",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "34A4": {
   "routine": [
    "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.",
    "In:  zp_73 = unit, zp_74 = destination column, zp_75 = destination row",
    "Out: $FB54[unit] bit 7; self-modifies the ORA operand at $34C1",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3568": {
   "routine": [
    "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.",
    "In:  none",
    "Out: $F9C4[0..99] |= $C0; Y = $FF"
   ],
   "unit": "game/mainProgram0800"
  },
  "3576": {
   "routine": [
    "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.",
    "In:  $0BA8, $F9C4[]",
    "Out: $F9C4[] visibility bits, $37FF/$3800 scan filter"
   ],
   "unit": "game/mainProgram0800"
  },
  "35C3": {
   "routine": [
    "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.",
    "In:  X = mode (bit 7: 0 spotting, 1 attack targets), Y = scanning unit; unit tables, map, $077E terrain classes",
    "Out: $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",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3746": {
   "routine": [
    "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.",
    "In:  X = terrain class of the next cell, zp_1E = previous height, zp_1F = $80 until the line went uphill",
    "Out: C = 1 blocked / 0 clear, zp_1E updated, zp_1F = rise amount"
   ],
   "unit": "game/mainProgram0800"
  },
  "375D": {
   "routine": [
    "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.",
    "In:  X = start unit, Y = end unit",
    "Out: see initLineFromUnit"
   ],
   "unit": "game/mainProgram0800"
  },
  "3767": {
   "routine": [
    "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.",
    "In:  X = unit, zp_94/zp_95 = target row/column",
    "Out: A = direction code 0-3, line walker state zp_8C-zp_97"
   ],
   "unit": "game/mainProgram0800"
  },
  "378B": {
   "routine": [
    "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.",
    "In:  Y = unit index",
    "Out: $37FF, $3800 patched; A clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "37AA": {
   "routine": [
    "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).",
    "In:  A = radius, X = centre column, Y = centre row, $0B9F, map via sub_25FA",
    "Out: $0400/$0432 lists, X = zp_29 = count-1, zp_22-zp_29 scratch, zp_48/zp_49 map pointer; self-modifies $385A"
   ],
   "unit": "game/mainProgram0800"
  },
  "3870": {
   "routine": [
    "distanceSquared - Returns |zp_22-zp_27|^2 + |zp_24-zp_28|^2 using squaresTable (each delta capped at 15, sum saturating at $FF).",
    "In:  zp_22/zp_24 = cell column/row, zp_27/zp_28 = centre column/row",
    "Out: A = squared distance (max $FF), X clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "38C2": {
   "routine": [
    "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.",
    "In:  X = attacker unit, Y = target unit; unit tables, $077E terrain classes, $90FB, $922A",
    "Out: $38BB-$38C1 scratch, $923D attacker type, target energy/status updated, sprites 3 and zoom-window line, sound; self-modifies $3A4E, $393F, $3971",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3A5A": {
   "routine": [
    "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.",
    "In:  zp_92-zp_95 = attacker row/col and target row/col (map cells), $923D",
    "Out: sprite 3 data ($8AC0), $9013/$901B, $900B=1, $9022 bit 3, $91C8=8; zp_48 used as step counter"
   ],
   "unit": "game/mainProgram0800"
  },
  "3B22": {
   "routine": [
    "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.",
    "In:  A = damage, Y = target unit",
    "Out: $9240 = applied damage, $F9C4/$FA28/$FA8C/$FB54/$F76C of the target, $9208, $92A2-$92A5, $92C8, $0BA8, sounds/messages, unit redraw via sub_48A2",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3BDF": {
   "routine": [
    "reduceUnitEnergyBy - Stores A into hitDamage $9240 and falls into reduceUnitEnergy. Used by resolveAttack for the first-strike energy sacrifice and by $3EF8 (1 point).",
    "In:  A = amount, Y = unit",
    "Out: see reduceUnitEnergy"
   ],
   "unit": "game/mainProgram0800"
  },
  "3BE2": {
   "routine": [
    "reduceUnitEnergy - Subtracts hitDamage $9240 from the low 6 bits of unitEnergy[Y] with a floor of 0, preserving the visibility bits 6-7.",
    "In:  Y = unit, $9240",
    "Out: $F9C4[Y]; self-modifies the ORA operand at $3BF7"
   ],
   "unit": "game/mainProgram0800"
  },
  "3BFC": {
   "routine": [
    "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.",
    "In:  A = damage, Y = unit",
    "Out: $FA8C[Y], $FA28[Y], $FB54[Y] bit 1; self-modifies $3C0F",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3C20": {
   "routine": [
    "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.",
    "In:  zp_A5/zp_A6, $90F8 selected unit, map, $F708",
    "Out: zp_74/zp_75, 3 command bytes queued, or sound 3",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "32F6": {
   "routine": [
    "hideSprite0Dead - Unreferenced copy of hideSprite1 that clears bit 0 (sprite 0) instead; rendered as .byte in the listing.",
    "In:  none",
    "Out: would clear $9022/$D015 bit 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "33C8": {
   "routine": [
    "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.",
    "In:  A = index 0/1, X/Y = cell",
    "Out: dead code",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "34C6": {
   "routine": [
    "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.",
    "In:  zp_73/zp_74/zp_75",
    "Out: dead code",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "355D": {
   "routine": [
    "clampMapCoordOld - Helper of applyMoveOrderOld: negative values become 0, values >= 40 become 39.",
    "In:  A = coordinate",
    "Out: A clamped to 0..39"
   ],
   "unit": "game/mainProgram0800"
  },
  "379E": {
   "routine": [
    "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.",
    "In:  A = radius, X/Y = centre",
    "Out: dead code",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3C50": {
   "routine": [
    "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).",
    "In:  zp_73 = boomer unit index, zp_75 = target unit index (bits 0-6)",
    "Out: D_91DC[boomer ordinal] = target | $80; Y = ordinal; X = unit index"
   ],
   "unit": "game/mainProgram0800"
  },
  "32DA": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "terrainSightBonusTable: byteTable, 8 bytes. extra range by terrain class 0-7 (0,0,0,1,1,2,2,0), doubled for spotting"
   ]
  },
  "3860": {
   "unit": "game/mainProgram0800",
   "block": [
    "squaresTable: byteTable, 16 bytes. n*n for n = 0..15 (also used by overlay A $7831 and the comm module $E832)"
   ]
  },
  "38A4": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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)"
   ]
  },
  "3C5D": {
   "routine": [
    "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.",
    "In:  X = unit index (exclusive upper limit)",
    "Out: Y = number of boomers with index < X; X = input X; A = last type byte; C = 1"
   ],
   "unit": "game/mainProgram0800"
  },
  "3C74": {
   "routine": [
    "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.",
    "In:  X = unit index",
    "Out: C = 1 and A = boomerTargetTable[Y], Y = boomer ordinal (boomer); C = 0 otherwise"
   ],
   "unit": "game/mainProgram0800"
  },
  "3C85": {
   "routine": [
    "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.",
    "In:  Y = boomer unit index; D_0400/D_0432/D_0464 lists produced by sub_35C3; zp_2F = list count-1",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "3CF2": {
   "routine": [
    "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).",
    "In:  A = number of radius steps (patched into the CPX at $3D25); sprite 1 position already set in $9011/$9019",
    "Out: sprite 1 shape and enable bit modified, D_3CF1 loop counter, D_91F3 plot mode (left 0), zp_7D = 0 (never read), sound $17",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3D57": {
   "routine": [
    "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).",
    "In:  D_9240 = damage amount of the hit; D_0B9B, D_0BA3, D_0BA8",
    "Out: D_9241[1..3] may become $80; D_3D56 loop index; A/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "3D8A": {
   "routine": [
    "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.",
    "In:  unit start positions in unitCol/unitRow, unitTypeTable",
    "Out: all per-unit arrays reset, map cells marked with $80|unit, D_91F4-D_9203 = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "3DA0": {
   "routine": [
    "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.",
    "In:  zp_18 = X = unit index",
    "Out: F898/F8FC/F7D0/F834/F76C/F960/F708 of the unit; continues in placeUnitOnMap"
   ],
   "unit": "game/mainProgram0800"
  },
  "3DDA": {
   "routine": [
    "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).",
    "In:  zp_18 = X = unit index; unitCol/unitRow",
    "Out: F960/FA8C/FA28/F9C4/FB54/FAF0 of the unit, map cell at (col,row), zp_48/49 = map pointer"
   ],
   "unit": "game/mainProgram0800"
  },
  "3E23": {
   "routine": [
    "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).",
    "In:  D_0BA8 (no game -> return), D_92A8/D_92AA recycler coordinates, D_92A6 recycler mode, D_0C8A last unit index per side, unit arrays",
    "Out: unit arrays and map updated, D_923B, message 5; zp_18/19/1A/1B/1C/1D scratch"
   ],
   "unit": "game/mainProgram0800"
  },
  "3EBD": {
   "routine": [
    "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.",
    "In:  X = unit index, unitUnderCell[X], unitCol/unitRow",
    "Out: map cell restored, FAF0[X] = $FF, FA8C[X] = 0, zp_48/49"
   ],
   "unit": "game/mainProgram0800"
  },
  "3EC2": {
   "routine": [
    "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.",
    "In:  A = new map cell value, X = unit index",
    "Out: map cell, FAF0[X] = $FF, FA8C[X] = 0; Y = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "3EE2": {
   "routine": [
    "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.",
    "In:  Y = unit index; zp_B1, D_0B9B, D_9236, D_67CA",
    "Out: F708[Y] bit 7, F9C4[Y] energy-1 (via sub_3BDF, D_9240 = 1), FA28[Y] += 25",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3F27": {
   "routine": [
    "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.",
    "In:  Y = side index (0/1); D_92A8/D_92AA",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "3F57": {
   "routine": [
    "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).",
    "In:  D_0B9F playerSide, D_0C8A, D_92A8/D_92AA of the opponent",
    "Out: map cell, message 8, self-patch at $3F57; candidate lists at $0400/$0432"
   ],
   "unit": "game/mainProgram0800"
  },
  "3F97": {
   "routine": [
    "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).",
    "In:  X = unit index; opcode at $3F9E",
    "Out: A = 0 or 1"
   ],
   "unit": "game/mainProgram0800"
  },
  "3FA3": {
   "routine": [
    "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).",
    "In:  A = coordinate 0..39",
    "Out: A = 39 - A"
   ],
   "unit": "game/mainProgram0800"
  },
  "3FAC": {
   "routine": [
    "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.",
    "In:  Y = unit index; D_0BA3, D_9236, D_92FE",
    "Out: F898/F7D0/F8FC/F834/F708/FB54 of the unit",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "3FE8": {
   "routine": [
    "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).",
    "In:  D_90FB currentScreen",
    "Out: FB54/F898/F8FC/F9C4 of all units, D_9242-D_9244 = 0, screen redraw"
   ],
   "unit": "game/mainProgram0800"
  },
  "4027": {
   "routine": [
    "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.",
    "In:  D_0BA8 end reason, scores D_92A2-D_92A5 (via sub_28F1)",
    "Out: D_920D = 0, D_92F9 = 0, D_92C9 = $78, all units halted"
   ],
   "unit": "game/mainProgram0800"
  },
  "3CF1": {
   "unit": "game/mainProgram0800",
   "block": [
    "ringAnimStep: byteTable, 1 bytes. loop counter of animateRingBurst"
   ]
  },
  "3D56": {
   "unit": "game/mainProgram0800",
   "block": [
    "comcenSystemIndex: byteTable, 1 bytes. loop counter of rollComcenSystemDamage"
   ]
  },
  "403B": {
   "routine": [
    "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.",
    "In:  X = 0 own side / nonzero opponent side; playerSide $0B9F; comcenIndexBySide $0C8A",
    "Out: comcenRevealTimer[side] = 6; unitEnergyTable[comcen] |= $C0; pendingRedrawUnit = comcen index when X<>0; A/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "406F": {
   "routine": [
    "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.",
    "In:  A = number of frames; Y = value to return in Y (stored into the operand at $4092)",
    "Out: Y restored to the entry value; A/X clobbered; game state advanced by the ticks that ran"
   ],
   "unit": "game/mainProgram0800"
  },
  "4094": {
   "routine": [
    "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.",
    "In:  A = frame count; Y preserved",
    "Out: Y = entry value; A/X clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "40B2": {
   "routine": [
    "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).",
    "In:  $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",
    "Out: 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)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4213": {
   "routine": [
    "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.",
    "In:  X = unit index, $9162 position in the round order; unit record arrays $F640-$FBB7; zp_B1 gamePhase; $0BA8 noGameInProgress",
    "Out: 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",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4559": {
   "routine": [
    "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.",
    "In:  $92/$93 current row/col, $94/$95 target row/col (from initLineStepper inputs), $90FA currentUnit, unit arrays $F76C/$F898/$F708/$FA28/$FAF0, $91F4 groupMoveTable",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4751": {
   "routine": [
    "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.",
    "In:  $90FA currentUnit, $93/$92 destination col/row, $19 destination cell byte, zp_48 -> destination cell, $9228 currentUnitType, $92AC scoreValueBySide",
    "Out: map cells, $F640/$F6A4/$FAF0/$F76C of the unit, $92A2/$92A4 scores, $9208, $406E = 1, $92C2 = 1; zp_48 low byte clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "47ED": {
   "routine": [
    "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.",
    "In:  $9164 unitBaseOrder, RNG state $5A-$5C",
    "Out: two entries of $9164 exchanged; A/X/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "4805": {
   "routine": [
    "randomUnitIndex - Returns a uniformly distributed unit index 0-99: takes nextScenarioRandom ($FD4A) AND $7F and retries while the value is >= 100.",
    "In:  RNG state $5A-$5C",
    "Out: A = 0..99; RNG advanced"
   ],
   "unit": "game/mainProgram0800"
  },
  "480F": {
   "routine": [
    "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.",
    "In:  $90FB currentScreenId, $91D7 trackedUnit, $90FA currentUnit, zp_A5/zp_A6 view origin, $F640/$F6A4",
    "Out: $91DA/$91DB = cursor cell, sprite 0 position/enable, view scrolled ($92/$93, zp_A5/zp_A6)",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4860": {
   "routine": [
    "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.",
    "In:  $FA8C unitHitFlashTimer, $F76C unitTypeTable, $FAF0 unitSavedTerrain",
    "Out: $FA8C updated, map/screen cells redrawn; zp_18 used as loop index"
   ],
   "unit": "game/mainProgram0800"
  },
  "48A2": {
   "routine": [
    "drawUnitAtPosition - Draws unit Y on the map display: A = Y|$80 (the map byte of a unit) and falls into drawValueAtUnit.",
    "In:  Y = unit index",
    "Out: see drawValueAtUnit"
   ],
   "unit": "game/mainProgram0800"
  },
  "48A5": {
   "routine": [
    "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.",
    "In:  A = cell byte to draw, Y = unit index",
    "Out: screen updated; A/X/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "48B1": {
   "routine": [
    "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.",
    "In:  $90FA currentUnit, $F640/$F6A4/$F7D0/$F834",
    "Out: A = new $F7D0 byte (bit 7 = stationary), Y = currentUnit, $F7D0 updated"
   ],
   "unit": "game/mainProgram0800"
  },
  "48DD": {
   "routine": [
    "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.",
    "In:  unit arrays $F76C/$F708/$FAF0, $92B4/$92B5 comcen speed classes, $9236 ownComcenUnit, tables $405E/$4066/$077E",
    "Out: $91F4-$9203 rewritten; $0400-$043F scratch; zp_18/zp_19/zp_1A clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "45E6": {
   "routine": [
    "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.",
    "In:  $91CB, $92EF, $92C2, zp_B1, $0BA8, $92B2/$92B3, unit arrays, $9164 unitBaseOrder",
    "Out: $90FD unitProcessOrder rebuilt, $9162 = 0, $90FC = 0, $FA28 movement points, $91F4, visibility bits, messages; zp_1A/zp_1B used as stack/list indices"
   ],
   "unit": "game/mainProgram0800"
  },
  "405E": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "unitMovedFlag: byteTable, 1 bytes. 1-byte variable (not a table): set when moveUnitToCell moved the current unit"
   ]
  },
  "4987": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4A02": {
   "routine": [
    "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).",
    "In:  Y = unit index, zp_75 = group key, $F640/$F6A4 unit position",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4A35": {
   "routine": [
    "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.",
    "In:  D_90F8 selectedUnit, zp_A5/zp_A6 view origin",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4A52": {
   "routine": [
    "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.",
    "In:  zp_73 = leader unit index, zp_74/zp_75 = ordered column/row (already mirrored for remote commands by mirrorCmdCoordsIfRemote $4FE4)",
    "Out: 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)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4AA3": {
   "routine": [
    "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.",
    "In:  D_9206 selectedGroupId (group number bits 2-4 plus side bit 5), D_90F8 selectedUnit",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4AD4": {
   "routine": [
    "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.",
    "In:  zp_74 = unit index, zp_75 = group key (both from readCmdArgs2)",
    "Out: $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)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4AF2": {
   "routine": [
    "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.",
    "In:  D_90F8 selectedUnit",
    "Out: $FB54[unit] &= $BF, zp_73 = unit, 2 bytes queued by queueCmd2 ($561F)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4B04": {
   "routine": [
    "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.",
    "In:  A = group key, Y = unit index or $80; D_91D8/D_91D9 when Y = $80",
    "Out: C=0 within range, C=1 too far; zp_18-zp_1D clobbered (box and mode); self-modifies the LDY operand at $4B0F"
   ],
   "unit": "game/mainProgram0800"
  },
  "4B5E": {
   "routine": [
    "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.",
    "In:  A = group key, Y = mode (bit 7 set = use destinations); $F76C, $FB54, $F640/$F6A4 or $F7D0/$F834",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4BBF": {
   "routine": [
    "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.",
    "In:  zp_3D text colour, zp_3E fill character",
    "Out: info-panel window selected and cleared, sound 3 queued, message printed (tail call to printString)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4BD3": {
   "routine": [
    "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.",
    "In:  D_90F8 selectedUnit, D_9206 selectedGroupId, D_91D8/D_91D9 picked cell, checkUiContinue ($C451)",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "4C59": {
   "routine": [
    "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.",
    "In:  zp_73 = unit index, zp_74/zp_75 = column/row (mirrored for remote commands), D_929D",
    "Out: unitDestCol/unitDestRow updated via sub_34D5, D_929E decremented when the group matches"
   ],
   "unit": "game/mainProgram0800"
  },
  "4C6B": {
   "routine": [
    "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.",
    "In:  X = unit index",
    "Out: $FB54[X] = (old & $83) | (F708[X] & $7C); self-modifies the ORA operand at $4C79"
   ],
   "unit": "game/mainProgram0800"
  },
  "4C84": {
   "routine": [
    "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.",
    "In:  zp_74 = leader unit index, zp_75 = rotation mode 0-2, zp_76 bit 7 = command came from the opponent, zp_B1 gamePhase",
    "Out: 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)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4D9F": {
   "routine": [
    "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).",
    "In:  A = command byte ($80-$A6)",
    "Out: A = D_92EE = length, X = command index (A & $3F)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4DA9": {
   "routine": [
    "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.",
    "In:  D_0B9B bit 7 film playback, D_9163 throttle",
    "Out: see processCommandExchange"
   ],
   "unit": "game/mainProgram0800"
  },
  "4DB7": {
   "routine": [
    "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.",
    "In:  D_0B9B, D_0B9C, D_0BA8, D_E03B, D_E01D, D_0B9F, film pointer zp_BC/zp_BD, D_923C, D_91CB",
    "Out: 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)"
   ],
   "unit": "game/mainProgram0800"
  },
  "4E91": {
   "routine": [
    "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).",
    "In:  D_908F buffer, D_92EB count, D_92EC player-1 start, D_0B9F local player, D_0B9B film flag",
    "Out: game state changed by the handlers; D_90B7, D_92ED, zp_76, D_92C2, D_929F updated; next packet sent"
   ],
   "unit": "game/mainProgram0800"
  },
  "4F05": {
   "routine": [
    "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.",
    "In:  D_E01E, D_E020-D_E026, zp_69",
    "Out: D_908F[zp_69..] filled, zp_69 advanced, zp_6A scratch, zp_76 = $80, D_92F9 possibly changed"
   ],
   "unit": "game/mainProgram0800"
  },
  "4F5E": {
   "routine": [
    "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.",
    "In:  A = command byte, Y = its index in the packet",
    "Out: A preserved, zp_6A = next index, D_92EE = length"
   ],
   "unit": "game/mainProgram0800"
  },
  "4F6B": {
   "routine": [
    "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.",
    "In:  D_E01F, D_E027-D_E02D, zp_69",
    "Out: D_908F filled, zp_69 advanced, zp_6A scratch, zp_76 = 0, zp_A5/zp_A6 or D_929F possibly changed"
   ],
   "unit": "game/mainProgram0800"
  },
  "4C7E": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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": {
   "unit": "game/mainProgram0800",
   "block": [
    "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."
   ]
  },
  "4FC4": {
   "routine": [
    "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.",
    "In:  D_90B7 = offset of the command byte in D_908F; D_908F merged command buffer",
    "Out: zp_73/zp_74/zp_75 = the three parameters; D_90B7 += 4; A/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "4FD0": {
   "routine": [
    "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).",
    "In:  D_90B7, D_908F",
    "Out: zp_74, zp_75; D_90B7 += 3; A/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "4FE4": {
   "routine": [
    "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.",
    "In:  zp_76 bit 7, zp_74 = column, zp_75 = row",
    "Out: zp_74/zp_75 replaced by 39-x/39-y for a remote command, untouched for an own command; A clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "4FF7": {
   "routine": [
    "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.",
    "In:  D_90B7, D_908F",
    "Out: zp_73; D_90B7 += 2; A/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "5005": {
   "routine": [
    "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.",
    "In:  zp_76, zp_74, zp_75",
    "Out: zp_74/zp_75 = 38-x, 38-y when remote; A clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "5011": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: unit destination/waypoint tables via $34C6"
   ],
   "unit": "game/mainProgram0800"
  },
  "501A": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: destinations of all group members via $4A52"
   ],
   "unit": "game/mainProgram0800"
  },
  "5023": {
   "routine": [
    "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.",
    "In:  D_908F, D_90B7, F76C unit type",
    "Out: D_F898[unit] |= $C0, D_F8FC[unit] &= $3F, D_923B and D_9245 via the tail; self-modifies $502D"
   ],
   "unit": "game/mainProgram0800"
  },
  "504B": {
   "routine": [
    "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.",
    "In:  zp_73 after readCmdArg1; zp_76",
    "Out: D_F898[unit] &= $3F; D_923B = unit for an own command; D_9245"
   ],
   "unit": "game/mainProgram0800"
  },
  "5062": {
   "routine": [
    "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).",
    "In:  D_908F/D_90B7",
    "Out: D_F8FC[unit] &= $3F; zp_73 = unit; Y = unit"
   ],
   "unit": "game/mainProgram0800"
  },
  "5070": {
   "routine": [
    "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.",
    "In:  D_908F, D_90B7, F76C unit type",
    "Out: D_F8FC[unit] |= $C0, D_F898[unit] &= $3F, energy/movement via $3EE2; self-modifies $507A"
   ],
   "unit": "game/mainProgram0800"
  },
  "5098": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7; group id byte carries bits 2-5 (bits 2-4 group index, bit 5 side)",
    "Out: D_F898 of every living non-SPY member |= $C0, D_F8FC &= $3F, D_91F4[g] |= $80, D_923A, D_9245"
   ],
   "unit": "game/mainProgram0800"
  },
  "50AA": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F8FC of every living non-SPY member |= $C0, per-member blitz cost, D_91F4[g]"
   ],
   "unit": "game/mainProgram0800"
  },
  "50BE": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F8FC of members &= $3F, D_91F4[g] &= $BF"
   ],
   "unit": "game/mainProgram0800"
  },
  "50D2": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F898 of members &= $3F, D_91F4[g] &= $7F, D_923A for an own command, D_9245"
   ],
   "unit": "game/mainProgram0800"
  },
  "5151": {
   "routine": [
    "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.",
    "In:  D_9236 own comcen index, D_F898; no arguments",
    "Out: D_9245 = D_F898[comcen] & $80; A/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "515D": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_91DC[ordinal] = 0"
   ],
   "unit": "game/mainProgram0800"
  },
  "516B": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_91DC[ordinal] = target | $80 via $3C50; zp_73/zp_75 rewritten"
   ],
   "unit": "game/mainProgram0800"
  },
  "5185": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F708[unit] = (old & 3) | group | $40 and D_FB54 via $4AD4"
   ],
   "unit": "game/mainProgram0800"
  },
  "519B": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F708[unit] &= $BF, D_FB54[unit]"
   ],
   "unit": "game/mainProgram0800"
  },
  "51AB": {
   "routine": [
    "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).",
    "In:  D_908F/D_90B7, zp_76",
    "Out: D_F640, D_F6A4, D_F7D0, D_F834, D_F898, D_F8FC, D_FB54 of the unit; self-modifies $51C9/$51E5"
   ],
   "unit": "game/mainProgram0800"
  },
  "51EA": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_0B9D = 1, then everything cmdSetMapSeed does",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "51EE": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_0B9D = 0, D_0B95-D_0B97 = seed, zp_B1 &= $7F"
   ],
   "unit": "game/mainProgram0800"
  },
  "520C": {
   "routine": [
    "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.",
    "In:  zp_B1, zp_76, D_0B9F playerSide, perSideValueTable $0C8A",
    "Out: battle: D_F960[comcen] &= $F0. Setup: zp_B1 &= mask, D_0B7D = $20, D_90EA = $20, D_929C = 0. D_90B7 += 1 in both cases"
   ],
   "unit": "game/mainProgram0800"
  },
  "523E": {
   "routine": [
    "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).",
    "In:  A = reason byte, X/Y = the merge loop registers, zp_76, D_0B9B, D_0BA8",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "52AE": {
   "routine": [
    "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.'.",
    "In:  X = current pause/phase byte, D_0BA5, zp_76",
    "Out: A = AND mask $FC/$FD/$FE; may queue message 4; Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "52C8": {
   "routine": [
    "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.",
    "In:  Y = index in D_E020, zp_76, D_92F9, D_90FB current screen",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "532F": {
   "routine": [
    "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.",
    "In:  X, Y",
    "Out: self-modifies $532B and $532D"
   ],
   "unit": "game/mainProgram0800"
  },
  "5336": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, zp_76, D_92F9, D_0B9B",
    "Out: D_92F9, D_920D, mergedCmdBuffer command byte, sound, blast damage and sprite"
   ],
   "unit": "game/mainProgram0800"
  },
  "537B": {
   "routine": [
    "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.",
    "In:  zp_76, D_90B7",
    "Out: D_920D &= $C0 when remote",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "538B": {
   "routine": [
    "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.",
    "In:  zp_76, D_920D, D_92F9",
    "Out: D_920D &= $3F; D_92F9 |= $10 for an own command"
   ],
   "unit": "game/mainProgram0800"
  },
  "53A3": {
   "routine": [
    "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.",
    "In:  zp_76, D_92AE",
    "Out: D_92F9 = 0, D_92AE[side] decremented, comcen reveal timer and visibility bits via $403B"
   ],
   "unit": "game/mainProgram0800"
  },
  "53C0": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, zp_B1, zp_76, D_0BA8, map $F000",
    "Out: D_92AE/D_92B0[side], unit energy/score via $3B22, sprite 4 blast ring, sound"
   ],
   "unit": "game/mainProgram0800"
  },
  "542E": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_F708",
    "Out: D_F76C entries set to $FF; self-modifies $544E; zp_73 used as the loop counter"
   ],
   "unit": "game/mainProgram0800"
  },
  "5459": {
   "routine": [
    "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.",
    "In:  zp_73 = unit index",
    "Out: D_F76C[unit] = $FF, explosion/sound/score side effects of $3B52; Y = unit"
   ],
   "unit": "game/mainProgram0800"
  },
  "5470": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: destination tables, D_929E"
   ],
   "unit": "game/mainProgram0800"
  },
  "5479": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_0BA8",
    "Out: D_F9C4[unit]; self-modifies $5499"
   ],
   "unit": "game/mainProgram0800"
  },
  "549E": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_F708, D_F76C",
    "Out: D_F7D0, D_F834, D_F898, D_F8FC, D_FB54 of every member; self-modifies $54D0"
   ],
   "unit": "game/mainProgram0800"
  },
  "54E1": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_0B9F, zp_76",
    "Out: D_92A6/D_92A8/D_92AA indexed by side; map redraw; self-modifies $550C"
   ],
   "unit": "game/mainProgram0800"
  },
  "5511": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_0BA5, zp_76, D_90F8",
    "Out: D_0B9F playerSide, zp_B1 = $DF"
   ],
   "unit": "game/mainProgram0800"
  },
  "553B": {
   "routine": [
    "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.",
    "In:  D_0B9F",
    "Out: zp_73, D_90F8, two bytes appended to the outgoing command ring"
   ],
   "unit": "game/mainProgram0800"
  },
  "5548": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F960[unit] = (old & $1F) | $20"
   ],
   "unit": "game/mainProgram0800"
  },
  "554C": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_F960[unit] &= $1F, D_F76C[unit] &= $BF"
   ],
   "unit": "game/mainProgram0800"
  },
  "555A": {
   "routine": [
    "setUnitDigBits - Shared prologue of cmdDigIn/cmdDigOut: patches the ORA operand at $5568 with A, reads the unit argument and falls into applyDigBitsToUnit.",
    "In:  A = new bits 5-7 ($20 or $00)",
    "Out: self-modifies $5568; zp_73 and Y = unit; D_F960[unit]"
   ],
   "unit": "game/mainProgram0800"
  },
  "556D": {
   "routine": [
    "cmdGroupDigIn - Handler of command $A1 (2 bytes: group id byte): patches the dig bits to $20 and runs the group loop at $5573.",
    "In:  D_908F/D_90B7",
    "Out: D_F960 of every living member"
   ],
   "unit": "game/mainProgram0800"
  },
  "5571": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_F708, D_F76C",
    "Out: D_F960 of every member; self-modifies $5568"
   ],
   "unit": "game/mainProgram0800"
  },
  "5596": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: destinations of the group members"
   ],
   "unit": "game/mainProgram0800"
  },
  "559C": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7",
    "Out: D_0BA3"
   ],
   "unit": "game/mainProgram0800"
  },
  "55A5": {
   "routine": [
    "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.",
    "In:  D_908F/D_90B7, D_0B9F, zp_76",
    "Out: D_92A2/D_92A4, D_92AC, D_92B4; self-modifies $55C2"
   ],
   "unit": "game/mainProgram0800"
  },
  "55EC": {
   "routine": [
    "queueCmdBytePending - Queues the single command byte in A (1-byte commands such as $92/$97/$9E/$8F) and increments pendingOwnCmdCount ($929F).",
    "In:  A",
    "Out: ring buffer, $929F"
   ],
   "unit": "game/mainProgram0800"
  },
  "55EF": {
   "routine": [
    "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).",
    "In:  A, $908D, $0B9B",
    "Out: $9065[], $908D"
   ],
   "unit": "game/mainProgram0800"
  },
  "560A": {
   "routine": [
    "queueCmd4 - Queues a 4-byte command: A, cmdParam0, cmdParam1, cmdParam2 (e.g. $80 move, $81, $98, $8D) and counts it once in pendingOwnCmdCount.",
    "In:  A = code, $73/$74/$75",
    "Out: ring buffer, $929F"
   ],
   "unit": "game/mainProgram0800"
  },
  "560F": {
   "routine": [
    "queueCmd3 - Queues a 3-byte command: A, cmdParam1, cmdParam2 (e.g. $87, $88, $90, $8E, $A3, $A5) and increments pendingOwnCmdCount.",
    "In:  A = code, $74/$75",
    "Out: ring buffer, $929F"
   ],
   "unit": "game/mainProgram0800"
  },
  "561F": {
   "routine": [
    "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.",
    "In:  A = code, $73",
    "Out: ring buffer, $929F"
   ],
   "unit": "game/mainProgram0800"
  },
  "562B": {
   "routine": [
    "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.",
    "In:  $9065/$908D/$908E, $920D, $92F9, $BE",
    "Out: $E027.., $E01F, $908E, $562A"
   ],
   "unit": "game/mainProgram0800"
  },
  "56B1": {
   "routine": [
    "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.",
    "In:  $E03C",
    "Out: -"
   ],
   "unit": "game/mainProgram0800"
  },
  "56BB": {
   "routine": [
    "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.",
    "In:  $E01D, $E01E, $E020/$E021, $46",
    "Out: message $1F, falls into completeExchange"
   ],
   "unit": "game/mainProgram0800"
  },
  "56F1": {
   "routine": [
    "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.",
    "In:  $E01D, $0B9C",
    "Out: $E01F, $7F, comm module state"
   ],
   "unit": "game/mainProgram0800"
  },
  "572B": {
   "routine": [
    "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).",
    "In:  $E01D/$E01E/$E020/$E021",
    "Out: $0B9C = 2, $92A1"
   ],
   "unit": "game/mainProgram0800"
  },
  "577C": {
   "routine": [
    "buildReadyPacket - Fills the outgoing packet buffer with $8C,$02 and sets $E01F = 2.",
    "In:  -",
    "Out: $E027/$E028/$E01F"
   ],
   "unit": "game/mainProgram0800"
  },
  "578A": {
   "routine": [
    "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.",
    "In:  $908F, $92EB, $92EC, $BC/$BD, $92F1/$92F2",
    "Out: film bytes, $BC/$BD"
   ],
   "unit": "game/mainProgram0800"
  },
  "57BE": {
   "routine": [
    "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.",
    "In:  Y = offset, $91CB, $923C",
    "Out: 1-2 film bytes, Y advanced"
   ],
   "unit": "game/mainProgram0800"
  },
  "57E7": {
   "routine": [
    "advanceFilmPtr - Adds Y to the 16-bit filmPtr ($BC/$BD).",
    "In:  Y",
    "Out: $BC/$BD"
   ],
   "unit": "game/mainProgram0800"
  },
  "57F2": {
   "routine": [
    "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.",
    "In:  $0BA8, $BC/$BD, $91CB, $92A2-$92A5, $0B94, $0B9F",
    "Out: film header $D000-$D033, $92F1/$92F2"
   ],
   "unit": "game/mainProgram0800"
  },
  "585C": {
   "routine": [
    "setFilmPtrToEnd - filmPtr ($BC/$BD) = filmEndPtr ($92F1/$92F2).",
    "In:  $92F1/$92F2",
    "Out: $BC/$BD"
   ],
   "unit": "game/mainProgram0800"
  },
  "5867": {
   "routine": [
    "saveFilmEndAndRewind - filmEndPtr = filmPtr, then filmPtr = $D000 (rewindFilmPtr).",
    "In:  $BC/$BD",
    "Out: $92F1/$92F2, $BC/$BD"
   ],
   "unit": "game/mainProgram0800"
  },
  "5874": {
   "routine": [
    "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.",
    "In:  Y = offset, $BC/$BD",
    "Out: A = byte"
   ],
   "unit": "game/mainProgram0800"
  },
  "5893": {
   "routine": [
    "writeFilmByte - Stores A at (filmPtr),Y in the RAM under I/O with the same interrupt/banking protocol as readFilmByte. X preserved.",
    "In:  A, Y, $BC/$BD",
    "Out: RAM under I/O"
   ],
   "unit": "game/mainProgram0800"
  },
  "58B2": {
   "routine": [
    "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.",
    "In:  patched operands",
    "Out: 256 bytes copied"
   ],
   "unit": "game/mainProgram0800"
  },
  "58D5": {
   "routine": [
    "rewindFilmPtr - filmPtr ($BC/$BD) = $D000, the start of the film/saved-game buffer.",
    "In:  -",
    "Out: $BC/$BD"
   ],
   "unit": "game/mainProgram0800"
  },
  "58DE": {
   "routine": [
    "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.",
    "In:  film header",
    "Out: C, A = $D004"
   ],
   "unit": "game/mainProgram0800"
  },
  "5900": {
   "routine": [
    "computeFilmHeaderChecksum - Sums the 51 header bytes $D000-$D032 with carry chaining into zp_18 (rewindFilmPtr first).",
    "In:  film header",
    "Out: $18 = checksum, $BC/$BD = $D000"
   ],
   "unit": "game/mainProgram0800"
  },
  "5915": {
   "routine": [
    "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.",
    "In:  $9223",
    "Out: -"
   ],
   "unit": "game/mainProgram0800"
  },
  "591E": {
   "routine": [
    "compareFilmPtrToEnd - Compares filmPtr with filmEndPtr: C=1 when filmPtr >= filmEndPtr (buffer full while recording, end reached while playing).",
    "In:  $BC/$BD, $92F1/$92F2",
    "Out: C"
   ],
   "unit": "game/mainProgram0800"
  },
  "5929": {
   "routine": [
    "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.",
    "In:  $92A2.., $F640.., $91CB",
    "Out: $D030-$D367"
   ],
   "unit": "game/mainProgram0800"
  },
  "5983": {
   "routine": [
    "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).",
    "In:  -",
    "Out: A/X",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "5988": {
   "routine": [
    "getFilmSnapshotSrcAddr - Returns A=$60, X=$F8: the address $F860 (last page source of the unit snapshot).",
    "In:  -",
    "Out: A/X",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "598D": {
   "routine": [
    "setFilmLimitToBufferEnd - filmEndPtr = $DFF0, the recording limit of the film buffer. Called from overlay A $78D1 when recording starts.",
    "In:  -",
    "Out: $92F1/$92F2"
   ],
   "unit": "game/mainProgram0800"
  },
  "5998": {
   "routine": [
    "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.",
    "In:  -",
    "Out: $D002/$D003, $BC/$BD"
   ],
   "unit": "game/mainProgram0800"
  },
  "59B3": {
   "routine": [
    "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.",
    "In:  $908D, $908E",
    "Out: C"
   ],
   "unit": "game/mainProgram0800"
  },
  "59D0": {
   "routine": [
    "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.",
    "In:  film header, $0B9F",
    "Out: $91CB, $92A2-$92A5, $0BA8, $923F, $92A1, $0548, $92C9"
   ],
   "unit": "game/mainProgram0800"
  },
  "5A14": {
   "routine": [
    "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.",
    "In:  $9223, $B1, $67C8",
    "Out: C, $9223, $0B9C, $0BA6"
   ],
   "unit": "game/mainProgram0800"
  },
  "5AD7": {
   "routine": [
    "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.",
    "In:  $F000 map, unit tables",
    "Out: bitmap/screen/colour RAM, $9004 = 7"
   ],
   "unit": "game/mainProgram0800"
  },
  "5B6C": {
   "routine": [
    "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.",
    "In:  Y = character row, $90B8",
    "Out: $90B8[0]",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "5BAD": {
   "routine": [
    "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.",
    "In:  A = map cell, X = quadrant, Y preserved",
    "Out: $32, $90B8 bytes"
   ],
   "unit": "game/mainProgram0800"
  },
  "5BD4": {
   "routine": [
    "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.",
    "In:  A = cell, X = quadrant, Y = return value",
    "Out: $90B8, X/Y restored"
   ],
   "unit": "game/mainProgram0800"
  },
  "5C7C": {
   "routine": [
    "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).",
    "In:  A/X/Y, $90FB, $A5/$A6",
    "Out: bitmap updates; A/X/Y restored"
   ],
   "unit": "game/mainProgram0800"
  },
  "5D1A": {
   "routine": [
    "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).",
    "In:  $54/$55/$56, $A5/$A6",
    "Out: bitmap, $90F6/$90F7 scratch"
   ],
   "unit": "game/mainProgram0800"
  },
  "5D7F": {
   "routine": [
    "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.",
    "In:  $54-$56, $A5/$A6, $9236",
    "Out: bitmap/colour RAM via overlay B, $9237-$9239",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "5E03": {
   "routine": [
    "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.",
    "In:  -",
    "Out: $39-$3C, window cleared"
   ],
   "unit": "game/mainProgram0800"
  },
  "562A": {
   "unit": "game/mainProgram0800",
   "block": [
    "outPacketFill: byteTable, 1 bytes. work variable of sendOutgoingPacket (bytes placed in the outgoing packet)"
   ]
  },
  "5B64": {
   "unit": "game/mainProgram0800",
   "block": [
    "quadrantPixelMask: byteTable, 4 bytes. AND mask for quadrant 0-3 of an overview character: $F0 left half, $0F right half"
   ]
  },
  "5B68": {
   "unit": "game/mainProgram0800",
   "block": [
    "quadrantRowOffset: byteTable, 4 bytes. first scanline of quadrant 0-3: 0,0,4,4 (4 rows each)"
   ]
  },
  "5E17": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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)"
   ],
   "unit": "game/mainProgram0800"
  },
  "5E24": {
   "routine": [
    "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.",
    "In:  D_90F8, unit arrays, D_0BA3 bit 3, zp_5D, D_9236, zp_B1",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "5FA6": {
   "routine": [
    "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.",
    "In:  -",
    "Out: D_9209 = 0, D_91D7 = $FF"
   ],
   "unit": "game/mainProgram0800"
  },
  "5FB1": {
   "routine": [
    "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.",
    "In:  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",
    "Out: D_91CE[n] = id, D_91D4 incremented, text printed; A/X/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "5FD1": {
   "routine": [
    "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.",
    "In:  C flag, menu state D_91D4/D_91D5/D_91D6",
    "Out: see runMenuLoopWatchUnit",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "5FD7": {
   "routine": [
    "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.",
    "In:  menu state D_91D4/D_91D5/D_91D6, D_90F8, D_612E",
    "Out: A = D_91CE[D_91D5] item id, Y = D_91D5 selection"
   ],
   "unit": "game/mainProgram0800"
  },
  "5FDC": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "60E5": {
   "routine": [
    "unhighlightMenuRow - Removes the highlight from the selected menu row if it is currently highlighted (D_92C5 == 0); otherwise returns. Falls into toggleMenuRowHighlight.",
    "In:  D_92C5, D_91D5, D_91D6, zp_3C, zp_3D",
    "Out: row colours restored, D_92C5 = 1"
   ],
   "unit": "game/mainProgram0800"
  },
  "60EA": {
   "routine": [
    "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).",
    "In:  D_91D5, D_91D6, zp_3C, zp_3D, D_90F8, D_922A, D_92C5",
    "Out: zp_6E = 20, colour bytes toggled, D_92C5 ^= 1, zp_B6/zp_B7 and text cursor changed"
   ],
   "unit": "game/mainProgram0800"
  },
  "612F": {
   "routine": [
    "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.",
    "In:  D_90F8, D_FB54, D_F76C",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "61CF": {
   "routine": [
    "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.",
    "In:  D_90F8, D_FB54, D_F898, D_F8FC, D_F960, zp_5D, D_0BA3 bit 3",
    "Out: D_9206, D_9205, D_9204 = $FF, D_9209 = 2, D_612E = 1, commands queued with zp_73 = group key, menu state"
   ],
   "unit": "game/mainProgram0800"
  },
  "633C": {
   "routine": [
    "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.",
    "In:  D_9206",
    "Out: text printed, zp_3F = 4"
   ],
   "unit": "game/mainProgram0800"
  },
  "6358": {
   "routine": [
    "printGroupTitle - Sets the text column to 4 and prints the string GROUP ($CB1F, ends with CR).",
    "In:  text window",
    "Out: text printed, zp_3F = 4"
   ],
   "unit": "game/mainProgram0800"
  },
  "6366": {
   "routine": [
    "clearGroupHighlight - Clears the viewport group highlight (D_9205 = $FF) and redraws the viewport (sub_26C1).",
    "In:  -",
    "Out: D_9205 = $FF, viewport redrawn"
   ],
   "unit": "game/mainProgram0800"
  },
  "636E": {
   "routine": [
    "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.",
    "In:  zp_BB",
    "Out: zp_3D = zp_42 = 4, text printed"
   ],
   "unit": "game/mainProgram0800"
  },
  "6387": {
   "routine": [
    "printFormationCentred - Prints the word FORMATION ($C9D2) centred in the text window (sub_C121). Also called by the formation sub-menu sub_65FF.",
    "In:  text window",
    "Out: text printed"
   ],
   "unit": "game/mainProgram0800"
  },
  "638E": {
   "routine": [
    "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.",
    "In:  D_9206 group key, zp_BB mode, zp_5D, zp_6C, D_90EE, D_9236, D_FB54, D_63E2 own-side branch",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "64DA": {
   "routine": [
    "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).",
    "In:  D_9206",
    "Out: zp_73 = D_9206, two bytes queued via sub_561F"
   ],
   "unit": "game/mainProgram0800"
  },
  "64E4": {
   "routine": [
    "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.",
    "In:  D_9209",
    "Out: zp_3D = zp_42 = colour, text printed"
   ],
   "unit": "game/mainProgram0800"
  },
  "64F6": {
   "routine": [
    "printSettingWord - Prints SETTING ($CB4F) centred in the text window followed by a newline.",
    "In:  text window",
    "Out: text printed"
   ],
   "unit": "game/mainProgram0800"
  },
  "6500": {
   "routine": [
    "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.",
    "In:  D_90F8, D_9209, D_9206",
    "Out: zp_BB = 0, move order queued by sub_2185"
   ],
   "unit": "game/mainProgram0800"
  },
  "650E": {
   "routine": [
    "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.",
    "In:  A = group key (group number * 4 | side bit $20)",
    "Out: A = zp_18 = count, zp_19 = key | $40, Y = $FF"
   ],
   "unit": "game/mainProgram0800"
  },
  "5E14": {
   "unit": "game/mainProgram0800",
   "block": [
    "uiLevelTextColourTable: byteTable, 3 bytes. Info-panel text colour per uiLevel: $09 (browsing), $05 (unit selected), $04 (group). Read at $22AE (showUnitInfoPanel) and $64E7."
   ]
  },
  "612E": {
   "unit": "game/mainProgram0800",
   "block": [
    "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)."
   ]
  },
  "652E": {
   "routine": [
    "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.",
    "In:  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",
    "Out: 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"
   ],
   "unit": "game/mainProgram0800"
  },
  "65A0": {
   "routine": [
    "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.",
    "In:  D_91D4 item count and D_91D6 first row prepared by the caller, D_0B9B, D_0BA5, D_E03C, zp_73",
    "Out: Y = D_91D5 selected index (0 = ABORT GAME, last = (EXIT)), D_90F8=$FE, zp_3D=$60, command $8C/5 queued, highlight removed"
   ],
   "unit": "game/mainProgram0800"
  },
  "65E7": {
   "routine": [
    "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.",
    "In:  D_9206, D_929D, D_929E",
    "Out: none (may unwind one stack level)",
    "(confidence: medium)"
   ],
   "unit": "game/mainProgram0800"
  },
  "65FF": {
   "routine": [
    "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.",
    "In:  D_90F8 selected unit (or group leader)",
    "Out: Y = D_91D5, zp_74/zp_75 command parameters, command $A3 queued, D_91D4=5"
   ],
   "unit": "game/mainProgram0800"
  },
  "6637": {
   "routine": [
    "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.",
    "In:  $67C5, $67C2, per-voice state at $6792-$67C1, script pointers zp_D8-zp_DD",
    "Out: SID registers $D400-$D418, voice state, $67C8-$67CA"
   ],
   "unit": "game/mainProgram0800"
  },
  "6651": {
   "routine": [
    "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].",
    "In:  X = 0/2/4, voice state arrays $6792-$67C1",
    "Out: SID voice registers, $67C8+voice cleared on end, X preserved"
   ],
   "unit": "game/mainProgram0800"
  },
  "66C7": {
   "routine": [
    "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.",
    "In:  X = voice*2, script at (zp_D8,x), $67C3 current byte",
    "Out: voice state $6792-$67C1 updated, script pointer advanced, $67C4 scratch"
   ],
   "unit": "game/mainProgram0800"
  },
  "671C": {
   "routine": [
    "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.",
    "In:  X = voice*2, zp_D8/zp_D9 + X",
    "Out: A = byte, $67C3 = byte, pointer incremented"
   ],
   "unit": "game/mainProgram0800"
  },
  "6728": {
   "routine": [
    "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).",
    "In:  A = sound id, soundPointerTable $6831",
    "Out: $67C2-$67C7, $67C8-$67CA, zp_D8-zp_DD, voice state; X/Y clobbered"
   ],
   "unit": "game/mainProgram0800"
  },
  "67CB": {
   "routine": [
    "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.",
    "In:  A/Y = source block address",
    "Out: $0200-$03FF filled, D_9246 = marker, D_9247 = 0, zp_B2/zp_B3/zp_1A/zp_1C clobbered, sub_681A operand patched"
   ],
   "unit": "game/mainProgram0800"
  },
  "681A": {
   "routine": [
    "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).",
    "In:  patched operand $681B/$681C",
    "Out: A = X = byte, operand incremented"
   ],
   "unit": "game/mainProgram0800"
  },
  "50EB": {
   "routine": [
    "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.",
    "In:  patched operands at $512D/$5134/$5139/$513B/$514B/$514D; zp_73 group byte; D_F708, D_F76C",
    "Out: cloak or blitz bits of all matching units, D_91F4[group], D_923A, D_9245"
   ],
   "unit": "game/mainProgram0800"
  },
  "5562": {
   "routine": [
    "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.",
    "In:  Y = unit index, patched ORA operand at $5568",
    "Out: D_F960[Y]"
   ],
   "unit": "game/mainProgram0800"
  }
 }
}