{
 "chunk": "game_mainProgram0800_0EF7",
 "unit": "game/mainProgram0800",
 "unitLabels": {
  "game/mainProgram0800": {
   "0F03": "loadStartupOverlays",
   "1030": "ensureOverlayAResident",
   "1080": "loadOverlayBIfNotResident",
   "10AB": "loadUnitTemplatesAndSetup",
   "131B": "blinkCursorColour",
   "1412": "repairLoopCounter",
   "1413": "repairTabStunCopy",
   "1414": "resumeMainMenuScreen",
   "1507": "selectDefaultTab",
   "1535": "tabActionTable",
   "14F0": "snapshotComcenStatus",
   "0EF7": "fireArmOrHeld",
   "1101": "irqNotBrk",
   "1134": "irqBailOut",
   "1137": "irqPhase2",
   "1174": "irqUseScrollOffsets",
   "1187": "irqSpriteLoopSetup",
   "1191": "irqSpriteLoop",
   "11AC": "irqWriteSpriteColour",
   "11B1": "irqSpritePosition",
   "11DB": "irqAfterSprites",
   "11E7": "irqPollInput",
   "1205": "irqStoreJoystick",
   "1218": "irqIdleCounters",
   "123D": "irqTimerLoop",
   "124F": "irqCheckAbortHotkey",
   "126E": "irqPerFrameWork",
   "12B4": "bumpLinkWatchdog",
   "13EF": "chatLineWriteLoop",
   "1493": "menuJoyLeftRight",
   "14CB": "drawSelectedTab"
  }
 },
 "notes": {
  "0EF7": {
   "unit": "game/mainProgram0800",
   "block": [
    "Tail of isFireHeld ($0EEA): reached when the button is up, or is down and has already been seen",
    "released once (fireButtonArmed set).  A=0 on return means 'the player is holding fire'."
   ],
   "line": "re-read the latched fire state: 0 = button down, 1 = button up"
  },
  "0EF9": {
   "unit": "game/mainProgram0800",
   "line": "button still down (and armed) -> return A=0 = held"
  },
  "0EFB": {
   "unit": "game/mainProgram0800",
   "line": "button is up: arm the latch so the next press counts, and return A=1 = not held"
  },
  "0EFD": {
   "unit": "game/mainProgram0800",
   "line": "back to the caller; Z=1 means fire is being held"
  },
  "0EFE": {
   "unit": "game/mainProgram0800",
   "routine": [
    "stopAllSound - Silences every SID voice by requesting sound id 0, which requestSound turns into",
    "$FF/$FF/$FF/$00 in the four sound-request slots $92E7-$92EA so the IRQ dequeues 'sound 0' and the",
    "player releases all three voices.",
    "In:  none",
    "Out: $92E7-$92EA reset, all voices silenced",
    "Called from: initIrqAndInputState ($0BB5) and $1AF7 (end of game)"
   ],
   "line": "sound id 0 = the 'silence everything' request"
  },
  "0F00": {
   "unit": "game/mainProgram0800",
   "line": "tail-call the $C000 sound entry (playSound preserves X/Y and returns to our caller)"
  },
  "0F03": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadStartupOverlays - One-shot start-up loader: fast-loads track 30 sectors 0-15 into $C000-$CFFF",
    "(the text/glyph engine, which replaces the boot loader that lived there), then loadOverlayA - which",
    "chains the message strings and the comm tail - and falls straight into loadCommModule.",
    "In:  isSoloTrainer ($0BA5), read further down the chain",
    "Out: $C000-$CFFF, $6F00-$87FF variant A, $8800-$89FF, $EC00-$EFFF, $E000-$EBFF, comm module initialised",
    "Called from: mainStart ($0A81), once"
   ],
   "line": "destination low byte $00 ..."
  },
  "0F05": {
   "unit": "game/mainProgram0800",
   "line": "... and high byte $C0: the transfer lands at $C000"
  },
  "0F07": {
   "unit": "game/mainProgram0800",
   "line": "store both into the loader's self-modifying destination operands"
  },
  "0F0A": {
   "unit": "game/mainProgram0800",
   "line": "track $1E = 30, the text/glyph engine overlay"
  },
  "0F0C": {
   "unit": "game/mainProgram0800",
   "line": "$10 = 16 sectors = 4 KB -> $C000-$CFFF"
  },
  "0F0E": {
   "unit": "game/mainProgram0800",
   "line": "starting at sector 0"
  },
  "0F10": {
   "unit": "game/mainProgram0800",
   "line": "pull it in over the boot loader that is still sitting at $C000"
  },
  "0F13": {
   "unit": "game/mainProgram0800",
   "line": "then the $6F00 overlay variant A, the $8800 strings and the $EC00 comm tail"
  },
  "0F16": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadCommModule - Loads the opponent module behind the $E000 jump table and initialises it.  With",
    "isSoloTrainer ($0BA5) bit 7 set it disables the CIA interrupts and reads track 35 sectors 0-11 (the",
    "solo trainer), then points the NMI vector at the bare RTI because the trainer never uses NMIs;",
    "otherwise it falls into loadCommBuild2 for the real modem driver.",
    "In:  isSoloTrainer ($0BA5) bit 7",
    "Out: $E000-$EBFF filled, comm module initialised, (build 1) NMI vector = irqRti",
    "Called from: $0AF4 in startGameFromSetup, and by fall-through from loadStartupOverlays"
   ],
   "line": "destination low byte $00 ..."
  },
  "0F18": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $E0: the module always loads at $E000"
  },
  "0F1A": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader's destination"
  },
  "0F1D": {
   "unit": "game/mainProgram0800",
   "line": "which opponent did the player pick?"
  },
  "0F20": {
   "unit": "game/mainProgram0800",
   "line": "bit 7 clear = modem game -> load build 2 (the software modem)"
  },
  "0F22": {
   "unit": "game/mainProgram0800",
   "line": "solo trainer: no CIA interrupts at all while the fast loader runs"
  },
  "0F25": {
   "unit": "game/mainProgram0800",
   "line": "track $23 = 35, the solo trainer"
  },
  "0F27": {
   "unit": "game/mainProgram0800",
   "line": "$0C = 12 sectors = 3 KB -> $E000-$EBFF"
  },
  "0F29": {
   "unit": "game/mainProgram0800",
   "line": "from sector 0"
  },
  "0F2B": {
   "unit": "game/mainProgram0800",
   "line": "load it and run the module's init entry"
  },
  "0F2E": {
   "unit": "game/mainProgram0800",
   "line": "the trainer has no UART, so send NMIs straight to an RTI and return"
  },
  "0F31": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadCommBuild2 - Loads the modem-driver build of the comm module with interrupts off.  Its first",
    "page comes from track 18 sector 7 (on the directory track, so it is not encrypted and holds the",
    "module's jump table and variable block); track 34 sectors 1-11 supply the rest.  Falls into",
    "loadCommFinish.",
    "In:  load destination already set to $E000",
    "Out: $E000-$EBFF = the modem driver",
    "Called from: loadCommModule ($0F20) when isSoloTrainer bit 7 is clear"
   ],
   "line": "no interrupts during the fast-loader handshake"
  },
  "0F32": {
   "unit": "game/mainProgram0800",
   "line": "track $12 = 18, the directory track"
  },
  "0F34": {
   "unit": "game/mainProgram0800",
   "line": "sector 7 holds the driver's first page"
  },
  "0F36": {
   "unit": "game/mainProgram0800",
   "line": "just that one sector -> $E000-$E0FF"
  },
  "0F38": {
   "unit": "game/mainProgram0800",
   "line": "read it (loadSectors leaves the destination advanced to $E100)"
  },
  "0F3B": {
   "unit": "game/mainProgram0800",
   "line": "track $22 = 34, the rest of the modem driver"
  },
  "0F3D": {
   "unit": "game/mainProgram0800",
   "line": "sectors 1..11 (sector 0 is filler)"
  },
  "0F3F": {
   "unit": "game/mainProgram0800",
   "line": "$0B = 11 sectors -> $E100-$EBFF"
  },
  "0F41": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadCommFinish - Shared tail of the two comm-module load paths: does the final loadSectors with the",
    "registers the caller set up and falls into initCommModule.",
    "In:  A = sector count, Y = track, X = first sector, destination from setLoadDest",
    "Out: comm module loaded and initialised",
    "Called from: loadCommModule ($0F2B); reached by fall-through from loadCommBuild2"
   ],
   "line": "final transfer of the module"
  },
  "0F44": {
   "unit": "game/mainProgram0800",
   "routine": [
    "initCommModule - Cold-initialises the freshly loaded comm module and grafts the game's own keyboard",
    "scanner into it: the module's keyboard vector at $E013/$E014 is patched to scanKeyboard ($0DB7), so",
    "the module's $E012/$E015 entries scan the matrix with the game's routine (the KERNAL is banked out).",
    "In:  comm module resident at $E000",
    "Out: $E013/$E014 = $0DB7, interrupts enabled",
    "Called from: loadCommFinish (fall-through) and directly from $0AFC"
   ],
   "line": "function code 1 = cold initialise the module"
  },
  "0F46": {
   "unit": "game/mainProgram0800",
   "line": "call the module's life-cycle entry $E003"
  },
  "0F49": {
   "unit": "game/mainProgram0800",
   "line": "low byte of scanKeyboard ($0DB7) ..."
  },
  "0F4B": {
   "unit": "game/mainProgram0800",
   "line": "... into the operand of the module's keyboard JMP"
  },
  "0F4E": {
   "unit": "game/mainProgram0800",
   "line": "high byte $0D ..."
  },
  "0F50": {
   "unit": "game/mainProgram0800",
   "line": "... completing the vector: $E012/$E015 now scan the game's own key matrix"
  },
  "0F53": {
   "unit": "game/mainProgram0800",
   "line": "the module is live: let the raster IRQ run again"
  },
  "0F54": {
   "unit": "game/mainProgram0800",
   "line": "done"
  },
  "0F55": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadSectorsDelayed - Wrapper around loadSectors that first burns 8 raster frames through the $C000",
    "frame delay, giving the drive time to settle (and the previous fast-load handshake time to finish)",
    "before the next request.  Every overlay, map and setup load in this file goes through it.",
    "In:  A = sector count, Y = track, X = first sector, destination from setLoadDest",
    "Out: as loadSectors (memory filled, C=1 on error)",
    "Called from: $0FD1, $1054, $105D, $106D, $1096, $109F, $10B8, $107D, $10C8 and overlay A ($7E97, $7F84)"
   ],
   "line": "the sector count must survive the delay"
  },
  "0F56": {
   "unit": "game/mainProgram0800",
   "line": "8 frames ..."
  },
  "0F58": {
   "unit": "game/mainProgram0800",
   "line": "... of raster-timed idling (Y and X are preserved by waitFrames)"
  },
  "0F5B": {
   "unit": "game/mainProgram0800",
   "line": "get the sector count back"
  },
  "0F5C": {
   "unit": "game/mainProgram0800",
   "line": "now do the transfer and return to our caller"
  },
  "0F5F": {
   "unit": "game/mainProgram0800",
   "routine": [
    "suspendCommModule - Asks the comm module to pause before the game touches the disk: the fast loader",
    "needs interrupts off and exact cycle timing, which a UART NMI would ruin.  When the link is up it",
    "writes request code $C0 into the module's suspend-request byte $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 is set",
    "Called from: suspendCommForDiskAccess ($10CB)"
   ],
   "line": "the caller's A must survive"
  },
  "0F60": {
   "unit": "game/mainProgram0800",
   "line": "is the link actually up?"
  },
  "0F63": {
   "unit": "game/mainProgram0800",
   "line": "no link (or the solo trainer) - nothing to suspend"
  },
  "0F65": {
   "unit": "game/mainProgram0800",
   "line": "$C0 = 'disable every CIA2 NMI source' request code"
  },
  "0F67": {
   "unit": "game/mainProgram0800",
   "line": "hand it to the module's request byte $E039"
  },
  "0F6A": {
   "unit": "game/mainProgram0800",
   "line": "poll the acknowledge byte $E03A ..."
  },
  "0F6D": {
   "unit": "game/mainProgram0800",
   "line": "... until bit 7 says the module has parked itself"
  },
  "0F6F": {
   "unit": "game/mainProgram0800",
   "line": "restore the caller's A"
  },
  "0F70": {
   "unit": "game/mainProgram0800",
   "line": "the disk may now be touched"
  },
  "0F71": {
   "unit": "game/mainProgram0800",
   "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 the acknowledge byte $E03A, i.e. until the UART",
    "NMIs are running again.",
    "In:  $E03B (link active flag)",
    "Out: $E039 = 0; blocks until $E03A bit 7 is clear",
    "Called from: resumeCommAfterDiskAccess ($10D1)"
   ],
   "line": "is the link up?"
  },
  "0F74": {
   "unit": "game/mainProgram0800",
   "line": "no link - nothing to resume"
  },
  "0F76": {
   "unit": "game/mainProgram0800",
   "line": "0 = 'no suspend request'"
  },
  "0F78": {
   "unit": "game/mainProgram0800",
   "line": "release the module"
  },
  "0F7B": {
   "unit": "game/mainProgram0800",
   "line": "poll the acknowledge byte ..."
  },
  "0F7E": {
   "unit": "game/mainProgram0800",
   "line": "... until the module reports it is running again"
  },
  "0F80": {
   "unit": "game/mainProgram0800",
   "line": "back to normal service"
  },
  "0F81": {
   "unit": "game/mainProgram0800",
   "block": [
    "Three status-line strings used by the disk routines below, each terminated by a character with",
    "bit 7 set.  showDiskMessageAndFail is called with A = low byte, Y = high byte ($0F)."
   ],
   "line": "shown when the disk in the drive is not the film/data disk ('EA')"
  },
  "0F91": {
   "unit": "game/mainProgram0800",
   "line": "shown when the disk in the drive is not the game disk ('OZ')"
  },
  "0FA2": {
   "unit": "game/mainProgram0800",
   "line": "shown when the BAM read itself failed (drive error or no disk)"
  },
  "0FAD": {
   "unit": "game/mainProgram0800",
   "block": [
    "diskIdTable: two 2-character disk ids indexed by X (X=0 -> 'OZ' = the game disk, X=2 -> 'EA' = the",
    "film/data disk).  checkDiskId reads the first character with diskIdTable,X and the second with",
    "$0FAE,X and compares them against the id field of the BAM at $02A2/$02A3."
   ],
   "line": "first character of the game-disk id 'OZ' = Ozark Softscape"
  },
  "0FAE": {
   "unit": "game/mainProgram0800",
   "line": "'Z' completes 'OZ'; 'E','A' is the data-disk id used for game films"
  },
  "0FB1": {
   "unit": "game/mainProgram0800",
   "routine": [
    "checkDiskId - Verifies which disk is in the drive.  Reads the BAM (track 18 sector 0, which is not",
    "encrypted) to $0200 and compares the disk id at $02A2/$02A3 with the two characters of",
    "diskIdTable[X]; the two expected characters and the 'silent' flag are stuffed into instruction",
    "operands rather than variables.  For the game disk (X=0), outside film playback, it also copies the",
    "ten bytes at BAM offset $AB - the owner name the game writes onto each player's disk - into",
    "ownPlayerName ($FFDE), which buildOwnerNameString turns into the printed owner line and which is",
    "exchanged with the opponent.  NOTE: the read clobbers $0200-$02FF, so callers reload the setup block.",
    "In:  A = silent flag (bit 7 set = fail without a message), X = 0 ('OZ') or 2 ('EA')",
    "Out: C=0 ok / C=1 wrong disk or drive error, diskIdIndex ($92F0), $FFDE-$FFE7, $0200-$02FF clobbered",
    "Called from: waitForGameDisk ($10E3) and the film save/load code in overlay A ($7E53, $7E5C)"
   ],
   "line": "self-modifying: the caller's silent flag becomes the operand of the LDA #$FF at $0FFE"
  },
  "0FB4": {
   "unit": "game/mainProgram0800",
   "line": "remember which id we are looking for (X is about to be destroyed)"
  },
  "0FB7": {
   "unit": "game/mainProgram0800",
   "line": "Y = expected first id character ('O' or 'E')"
  },
  "0FBA": {
   "unit": "game/mainProgram0800",
   "line": "A = expected second id character ('Z' or 'A'), from diskIdTable+1"
  },
  "0FBD": {
   "unit": "game/mainProgram0800",
   "line": "park it in X so both can be written into the compare operands"
  },
  "0FBE": {
   "unit": "game/mainProgram0800",
   "line": "self-modifying: patch the CMP #$FF at $0FDC with the expected first character"
  },
  "0FC1": {
   "unit": "game/mainProgram0800",
   "line": "self-modifying: patch the CMP #$FF at $0FE3 with the expected second character"
  },
  "0FC4": {
   "unit": "game/mainProgram0800",
   "line": "BAM destination low byte $00 ..."
  },
  "0FC6": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $02: the sector lands at $0200 (over the terrain rule table)"
  },
  "0FC8": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "0FCB": {
   "unit": "game/mainProgram0800",
   "line": "one sector"
  },
  "0FCD": {
   "unit": "game/mainProgram0800",
   "line": "track $12 = 18, the directory track"
  },
  "0FCF": {
   "unit": "game/mainProgram0800",
   "line": "sector 0 = the BAM"
  },
  "0FD1": {
   "unit": "game/mainProgram0800",
   "line": "read it with the usual 8-frame settling delay"
  },
  "0FD4": {
   "unit": "game/mainProgram0800",
   "line": "C=0: the BAM is in memory"
  },
  "0FD6": {
   "unit": "game/mainProgram0800",
   "line": "drive error / no disk -> 'DISK ERROR.' and C=1"
  },
  "0FD9": {
   "unit": "game/mainProgram0800",
   "line": "first character of the disk id in the BAM"
  },
  "0FDC": {
   "unit": "game/mainProgram0800",
   "line": "compare against the expected first character (operand patched at $0FBE)"
  },
  "0FDE": {
   "unit": "game/mainProgram0800",
   "line": "wrong disk"
  },
  "0FE0": {
   "unit": "game/mainProgram0800",
   "line": "second character of the disk id"
  },
  "0FE3": {
   "unit": "game/mainProgram0800",
   "line": "compare against the expected second character (operand patched at $0FC1)"
  },
  "0FE5": {
   "unit": "game/mainProgram0800",
   "line": "wrong disk"
  },
  "0FE7": {
   "unit": "game/mainProgram0800",
   "line": "which id were we checking?"
  },
  "0FEA": {
   "unit": "game/mainProgram0800",
   "line": "non-zero = the 'EA' data disk: nothing else to do, report success"
  },
  "0FEC": {
   "unit": "game/mainProgram0800",
   "line": "game disk: is a film being replayed?"
  },
  "0FEF": {
   "unit": "game/mainProgram0800",
   "line": "during playback the film supplies the names, so leave $FFDE alone"
  },
  "0FF1": {
   "unit": "game/mainProgram0800",
   "line": "copy 10 bytes, index 9 down to 0"
  },
  "0FF3": {
   "unit": "game/mainProgram0800",
   "line": "BAM offset $AB = the owner name EA writes into the unused tail of the BAM"
  },
  "0FF6": {
   "unit": "game/mainProgram0800",
   "line": "-> ownPlayerName ($FFDE-$FFE7), used by buildOwnerNameString and the link handshake"
  },
  "0FF9": {
   "unit": "game/mainProgram0800",
   "line": "next byte"
  },
  "0FFA": {
   "unit": "game/mainProgram0800",
   "line": "loop until index 0 has been copied"
  },
  "0FFC": {
   "unit": "game/mainProgram0800",
   "line": "always taken (X went to $FF): report success"
  },
  "0FFE": {
   "unit": "game/mainProgram0800",
   "line": "operand patched at $0FB1 with the caller's silent flag"
  },
  "1000": {
   "unit": "game/mainProgram0800",
   "line": "bit 7 set = caller wants a silent failure -> just return C=1"
  },
  "1002": {
   "unit": "game/mainProgram0800",
   "line": "low byte of msgInsertGameDisk ($0F91) ..."
  },
  "1004": {
   "unit": "game/mainProgram0800",
   "line": "... and its high byte"
  },
  "1006": {
   "unit": "game/mainProgram0800",
   "line": "which id did we expect (the patched compare operand)?"
  },
  "1009": {
   "unit": "game/mainProgram0800",
   "line": "'E' means we wanted the data disk 'EA'"
  },
  "100B": {
   "unit": "game/mainProgram0800",
   "line": "wanted the game disk -> keep 'INSERT GAME DISK.'"
  },
  "100D": {
   "unit": "game/mainProgram0800",
   "line": "low byte of msgNotADataDisk ($0F81) - overlay A jumps straight in here at $7E71"
  },
  "100F": {
   "unit": "game/mainProgram0800",
   "line": "... and its high byte"
  },
  "1011": {
   "unit": "game/mainProgram0800",
   "line": "show it, beep, pause and return C=1"
  },
  "1014": {
   "unit": "game/mainProgram0800",
   "line": "C=0 = the expected disk is in the drive"
  },
  "1015": {
   "unit": "game/mainProgram0800",
   "line": "success return"
  },
  "1016": {
   "unit": "game/mainProgram0800",
   "line": "C=1 = wrong disk"
  },
  "1017": {
   "unit": "game/mainProgram0800",
   "line": "silent failure return"
  },
  "1018": {
   "unit": "game/mainProgram0800",
   "routine": [
    "diskErrorExit - Loads the address of msgDiskError ('DISK ERROR.') and falls into",
    "showDiskMessageAndFail.",
    "In:  none",
    "Out: message shown, C=1",
    "Called from: checkDiskId ($0FD6) and overlay A ($7E9D)"
   ],
   "line": "low byte of msgDiskError ($0FA2) ..."
  },
  "101A": {
   "unit": "game/mainProgram0800",
   "line": "... and its high byte"
  },
  "101C": {
   "unit": "game/mainProgram0800",
   "routine": [
    "showDiskMessageAndFail - Common failure exit of the disk routines: error beep, put the message on",
    "the centred status line for about two seconds, blank the line again and return C=1.",
    "In:  A = message address low byte, Y = high byte (bit-7 terminated string)",
    "Out: C=1, status line restored",
    "Called from: checkDiskId ($1011) and diskErrorExit by fall-through"
   ],
   "line": "the message pointer's low byte has to survive the beep"
  },
  "101D": {
   "unit": "game/mainProgram0800",
   "line": "sound 3 = the error buzz"
  },
  "101F": {
   "unit": "game/mainProgram0800",
   "line": "queue it (playSound preserves Y, which still holds the message page)"
  },
  "1022": {
   "unit": "game/mainProgram0800",
   "line": "get the low byte back"
  },
  "1023": {
   "unit": "game/mainProgram0800",
   "line": "print the string centred on the 36-column status line"
  },
  "1026": {
   "unit": "game/mainProgram0800",
   "line": "$64 = 100 frames, about 2 seconds ..."
  },
  "1028": {
   "unit": "game/mainProgram0800",
   "line": "... so the player can read it"
  },
  "102B": {
   "unit": "game/mainProgram0800",
   "line": "blank the status line again"
  },
  "102E": {
   "unit": "game/mainProgram0800",
   "line": "C=1 = the operation failed"
  },
  "102F": {
   "unit": "game/mainProgram0800",
   "line": "back to the caller"
  },
  "1030": {
   "unit": "game/mainProgram0800",
   "routine": [
    "ensureOverlayAResident - Makes sure the $6F00-$87FF overlay variant A (map generator, setup and menu",
    "code) is in memory before control leaves for handleMainMenuChoice ($7DE3).  overlayVariantFlag",
    "($87FF) is the identity byte carried by whichever overlay is resident: $FF = variant B, so variant A",
    "must be read in full; $00 = variant A is already there and only the $8800 message strings and the",
    "$EC00 comm tail (which sub-overlays borrow) need refreshing.",
    "In:  overlayVariantFlag ($87FF)",
    "Out: variant A resident plus fresh strings/comm tail; comm module suspended and resumed around the disk access",
    "Called from: $1C72, with diskLoadDepth bumped around the call"
   ],
   "line": "park the comm module and put 'WORKING...' on the status line"
  },
  "1033": {
   "unit": "game/mainProgram0800",
   "line": "make sure the OZ disk is really in the drive first"
  },
  "1036": {
   "unit": "game/mainProgram0800",
   "line": "identity byte of the resident $6F00 overlay"
  },
  "1039": {
   "unit": "game/mainProgram0800",
   "line": "$FF = variant B is resident -> the whole of variant A has to be read"
  },
  "103B": {
   "unit": "game/mainProgram0800",
   "line": "variant A is already there: just refresh $8800-$89FF and $EC00-$EFFF"
  },
  "103E": {
   "unit": "game/mainProgram0800",
   "line": "restart the comm module, clear the message and return"
  },
  "1041": {
   "unit": "game/mainProgram0800",
   "line": "read variant A (which chains the strings and the comm tail)"
  },
  "1044": {
   "unit": "game/mainProgram0800",
   "line": "restart the comm module, clear the message and return"
  },
  "1047": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadOverlayA - Reads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00-$7FFF and",
    "track 32 sectors 0-7 to $8000-$87FF.  Variant A is the random map generator, the setup phase and the",
    "film menus.  Falls into loadStringsAndCommTail.",
    "In:  none",
    "Out: $6F00-$87FF = variant A (its last byte $87FF is the identity byte $00), plus $8800-$89FF and $EC00-$EFFF",
    "Called from: loadStartupOverlays ($0F13) and ensureOverlayAResident ($1041)"
   ],
   "line": "destination low byte $00 ..."
  },
  "1049": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $6F: the overlay starts at $6F00"
  },
  "104B": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "104E": {
   "unit": "game/mainProgram0800",
   "line": "track $1F = 31"
  },
  "1050": {
   "unit": "game/mainProgram0800",
   "line": "from sector 0"
  },
  "1052": {
   "unit": "game/mainProgram0800",
   "line": "$11 = 17 sectors -> $6F00-$7FFF"
  },
  "1054": {
   "unit": "game/mainProgram0800",
   "line": "read them"
  },
  "1057": {
   "unit": "game/mainProgram0800",
   "line": "track $20 = 32"
  },
  "1059": {
   "unit": "game/mainProgram0800",
   "line": "from sector 0"
  },
  "105B": {
   "unit": "game/mainProgram0800",
   "line": "8 sectors -> $8000-$87FF, ending with the identity byte at $87FF"
  },
  "105D": {
   "unit": "game/mainProgram0800",
   "line": "read them"
  },
  "1060": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadStringsAndCommTail - Reads the modem status-message string block (track 29 sectors 9-10) into",
    "$8800-$89FF, then falls into loadCommTailBuild2.  In a solo game the trainer overwrites this block",
    "with its own per-unit tables, so it has to be reloaded whenever the strings are wanted again.",
    "In:  none",
    "Out: $8800-$89FF strings, $EC00-$EFFF comm tail",
    "Called from: ensureOverlayAResident ($103B); reached by fall-through from loadOverlayA"
   ],
   "line": "destination low byte $00 ..."
  },
  "1062": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $88"
  },
  "1064": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "1067": {
   "unit": "game/mainProgram0800",
   "line": "track $1D = 29"
  },
  "1069": {
   "unit": "game/mainProgram0800",
   "line": "from sector 9"
  },
  "106B": {
   "unit": "game/mainProgram0800",
   "line": "2 sectors -> $8800-$89FF"
  },
  "106D": {
   "unit": "game/mainProgram0800",
   "line": "read them"
  },
  "1070": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadCommTailBuild2 - Reads the last page group of the modem driver (track 34 sectors 12-15) into",
    "$EC00-$EFFF.  Overlay A also calls it to put the comm tail back after the $EC00 sub-overlay from",
    "track 29 has temporarily occupied that space.",
    "In:  none",
    "Out: $EC00-$EFFF = the modem driver tail",
    "Called from: overlay A ($7B33); reached by fall-through from loadStringsAndCommTail"
   ],
   "line": "destination low byte $00 ..."
  },
  "1072": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $EC"
  },
  "1074": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "1077": {
   "unit": "game/mainProgram0800",
   "line": "track $22 = 34"
  },
  "1079": {
   "unit": "game/mainProgram0800",
   "line": "from sector $0C = 12"
  },
  "107B": {
   "unit": "game/mainProgram0800",
   "line": "4 sectors -> $EC00-$EFFF"
  },
  "107D": {
   "unit": "game/mainProgram0800",
   "line": "read them and return straight to our caller"
  },
  "1080": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadOverlayBIfNotResident - Loads the $6F00 overlay variant B only when it is not already in memory.",
    "overlayVariantFlag ($87FF) is $00 while variant A is resident and $FF while B is, so a positive",
    "value means the swap has to happen.",
    "In:  overlayVariantFlag ($87FF)",
    "Out: see loadOverlayB, or nothing at all",
    "Called from: startGameFromSetup ($0B5F)"
   ],
   "line": "identity byte of the resident $6F00 overlay"
  },
  "1083": {
   "unit": "game/mainProgram0800",
   "line": "$00 = variant A is resident -> swap variant B in"
  },
  "1085": {
   "unit": "game/mainProgram0800",
   "line": "variant B is already there, nothing to do"
  },
  "1086": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadOverlayB - Reads the $6F00-$87FF overlay variant B (the comcen missile and drone screens): track",
    "33 sectors 0-16 to $6F00-$7FFF and track 32 sectors 8-15 to $8000-$87FF.  Afterwards it copies the",
    "byte at $206B into $750A: both are the opcode byte of a BCC/BCS that follows a 'is this unit index",
    "below 50' test, i.e. the side-dependent branch the main program has already patched for this",
    "player's side, propagated into the freshly loaded overlay.",
    "In:  $206B (the patched branch opcode), overlayVariantFlag",
    "Out: $6F00-$87FF = variant B, $750A patched, comm module suspended and resumed",
    "Called from: loadOverlayBIfNotResident ($1083)"
   ],
   "line": "park the comm module and show 'WORKING...' - the fast loader needs the interrupts"
  },
  "1089": {
   "unit": "game/mainProgram0800",
   "line": "destination low byte $00 ..."
  },
  "108B": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $6F"
  },
  "108D": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "1090": {
   "unit": "game/mainProgram0800",
   "line": "track $21 = 33"
  },
  "1092": {
   "unit": "game/mainProgram0800",
   "line": "$11 = 17 sectors -> $6F00-$7FFF"
  },
  "1094": {
   "unit": "game/mainProgram0800",
   "line": "from sector 0"
  },
  "1096": {
   "unit": "game/mainProgram0800",
   "line": "read them"
  },
  "1099": {
   "unit": "game/mainProgram0800",
   "line": "track $20 = 32"
  },
  "109B": {
   "unit": "game/mainProgram0800",
   "line": "from sector 8 (sectors 0-7 of that track belong to variant A)"
  },
  "109D": {
   "unit": "game/mainProgram0800",
   "line": "8 sectors -> $8000-$87FF"
  },
  "109F": {
   "unit": "game/mainProgram0800",
   "line": "read them"
  },
  "10A2": {
   "unit": "game/mainProgram0800",
   "line": "the branch opcode ($90 BCC / $B0 BCS) that patchOwnSideBranches wrote at $206B for this player's side"
  },
  "10A5": {
   "unit": "game/mainProgram0800",
   "line": "patch the matching 'cpy #$32 / branch' in the overlay at $7508-$750A"
  },
  "10A8": {
   "unit": "game/mainProgram0800",
   "line": "restart the comm module, clear the message and return"
  },
  "10AB": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadUnitTemplatesAndSetup - Reads track 18 sectors 8-14 into $F000-$F6FF, then falls into",
    "loadSetupBlock.  What is on the disk there is not a battlefield: it is four 400-byte unit",
    "start-position templates (columns, rows, flags and types for 100 units each) which the setup code",
    "picks from and copies into the live unit arrays; the 40x40 map is generated, not loaded.",
    "In:  none",
    "Out: $F000-$F6FF = the start-position templates, $0200-$03FF = the setup block",
    "Called from: startGameFromSetup ($0B35), with diskLoadDepth bumped so the IRQ drops keypresses"
   ],
   "line": "destination low byte $00 ..."
  },
  "10AD": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $F0"
  },
  "10AF": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "10B2": {
   "unit": "game/mainProgram0800",
   "line": "track $12 = 18, the directory track"
  },
  "10B4": {
   "unit": "game/mainProgram0800",
   "line": "from sector 8"
  },
  "10B6": {
   "unit": "game/mainProgram0800",
   "line": "7 sectors -> $F000-$F6FF (four 400-byte templates plus 192 unused bytes)"
  },
  "10B8": {
   "unit": "game/mainProgram0800",
   "line": "read them"
  },
  "10BB": {
   "unit": "game/mainProgram0800",
   "routine": [
    "loadSetupBlock - Reads the saved setup block (track 18 sectors 15-16) into $0200-$03FF: the terrain",
    "tile rule table at $0200 and the persisted game settings plus the little start-up routine at $0300.",
    "In:  none",
    "Out: $0200-$03FF",
    "Called from: mainStart ($0A84); reached by fall-through from loadUnitTemplatesAndSetup"
   ],
   "line": "destination low byte $00 ..."
  },
  "10BD": {
   "unit": "game/mainProgram0800",
   "line": "... high byte $02"
  },
  "10BF": {
   "unit": "game/mainProgram0800",
   "line": "arm the loader"
  },
  "10C2": {
   "unit": "game/mainProgram0800",
   "line": "track $12 = 18"
  },
  "10C4": {
   "unit": "game/mainProgram0800",
   "line": "from sector $0F = 15"
  },
  "10C6": {
   "unit": "game/mainProgram0800",
   "line": "2 sectors -> $0200-$03FF"
  },
  "10C8": {
   "unit": "game/mainProgram0800",
   "line": "read them and return straight to our caller"
  },
  "10CB": {
   "unit": "game/mainProgram0800",
   "routine": [
    "suspendCommForDiskAccess - Standard prologue for disk access: pause the comm module, then set",
    "persistentMessageId to 1 and paint message 1 ('WORKING...') on the status line so the player knows",
    "the drive is busy.",
    "In:  none",
    "Out: comm module paused, persistentMessageId ($92A1) = 1",
    "Called from: ensureOverlayAResident ($1030), loadOverlayB ($1086), waitForGameDiskInserted ($10DC) and overlay A ($7B7D, $7DF7, $7F57, $815E)"
   ],
   "line": "stop the UART NMIs so the fast loader can keep its timing"
  },
  "10CE": {
   "unit": "game/mainProgram0800",
   "line": "show the 'WORKING...' notice and return to our caller"
  },
  "10D1": {
   "unit": "game/mainProgram0800",
   "routine": [
    "resumeCommAfterDiskAccess - Epilogue for disk access: restart the comm module, forget the persistent",
    "'WORKING...' message and blank the status line.",
    "In:  none",
    "Out: comm module running, persistentMessageId ($92A1) = 0, status line cleared",
    "Called from: $0B3D, $103E, $1044, $10A8 and overlay A ($7DFE, $7F95, $81A2, $81AF)"
   ],
   "line": "let the module's NMIs run again"
  },
  "10D4": {
   "unit": "game/mainProgram0800",
   "line": "no message is being held on the line any more"
  },
  "10D6": {
   "unit": "game/mainProgram0800",
   "line": "clear the persistent message id"
  },
  "10D9": {
   "unit": "game/mainProgram0800",
   "line": "blank the status line and return to our caller"
  },
  "10DC": {
   "unit": "game/mainProgram0800",
   "routine": [
    "waitForGameDiskInserted - Blocks until the OZ game disk is in the drive.  Suspends the comm module",
    "(and leaves it suspended for the caller), then loops: check the disk id verbosely, and on failure",
    "print 'PRESS BUTTON WHEN READY.' and wait for a fire click before trying again.  Because that prompt",
    "stirs the RNG while it waits, the delay also randomises the map seed.",
    "In:  none",
    "Out: returns only with C=0 from checkDiskId; comm module left suspended",
    "Called from: mainStart ($0ADC), ensureOverlayAResident ($1033) and overlay A ($81AC)"
   ],
   "line": "pause the comm module and show 'WORKING...'"
  },
  "10DF": {
   "unit": "game/mainProgram0800",
   "block": [
    "Retry loop: ask for the game disk until it is there."
   ],
   "line": "id index 0 = the game disk 'OZ'"
  },
  "10E1": {
   "unit": "game/mainProgram0800",
   "line": "A bit 7 clear = report a mismatch on the status line"
  },
  "10E3": {
   "unit": "game/mainProgram0800",
   "line": "read the BAM and compare the id"
  },
  "10E6": {
   "unit": "game/mainProgram0800",
   "line": "C=0: the right disk is in the drive"
  },
  "10E8": {
   "unit": "game/mainProgram0800",
   "line": "wrong disk: prompt and wait for the fire button"
  },
  "10EB": {
   "unit": "game/mainProgram0800",
   "line": "then try again"
  },
  "10EE": {
   "unit": "game/mainProgram0800",
   "line": "the game disk is present"
  },
  "10EF": {
   "unit": "game/mainProgram0800",
   "block": [
    "spriteEnableScratch: written by the raster IRQ at $11CB with the sprite enable mask it is about to",
    "put into VIC_SPR_ENA.  Nothing ever reads it back (a dead store); $10F0 is unused padding before",
    "the interrupt handler."
   ],
   "line": "copy of the last sprite enable mask written by the IRQ; never read"
  },
  "10F1": {
   "unit": "game/mainProgram0800",
   "routine": [
    "rasterIrqHandler - The game's only interrupt handler, entered through the RAM vector $FFFE at raster",
    "line $FA (250), installed by installRasterIrq ($0C66).  A BRK is recognised from the B flag in the",
    "pushed status byte and thrown away through the patchable JMP operand at $10FF/$1100 (a debug hook",
    "that initInterruptVectors points at irqExit).  Phase 1 acknowledges the VIC interrupt, re-arms the",
    "raster line and the $FFFE/$FFFF vector from their zero-page shadows, guards against re-entry with",
    "irqInProgress, copies the two VIC control registers from their shadows and services the comm module",
    "($E000 with X=0 and $E00C); this half also runs while a disk load is in progress.  Phase 2 copies",
    "the remaining VIC shadows and the eight sprite colour/X/Y shadows (colours >= $10 hold two nibbles",
    "that alternate every other frame to make a sprite flash; positions can be biased by the fine-scroll",
    "offsets), bumps the 24-bit frame counter, re-enables interrupts, polls the keyboard and joystick 2,",
    "maintains the idle counter, the RNG and the six frame countdowns, watches for the SHIFT+C=+F7 abort,",
    "blinks the map cursor, ticks the frame timers, runs both halves of the chat and drives the sound.",
    "In:  VIC and CIA1 registers, the shadow block $9000-$9024, inputLockoutTimer ($0B7D), diskLoadDepth",
    "     ($0BA7), screenShakeTimer ($92C8), spriteScrollFineX/Y ($92F4/$92F5), comm entries $E000/$E00C",
    "Out: every VIC register, frameCounter (zp_45-zp_47), joyDirection/joyFireUp and their copies,",
    "     requestedScreenKey, irqInProgress, the timers zp_6C-zp_71, the chat buffers and the sound state",
    "Called from: the hardware IRQ vector $FFFE (and BRK)"
   ],
   "line": "save the registers the handler is about to use"
  },
  "10F2": {
   "unit": "game/mainProgram0800",
   "line": "X ..."
  },
  "10F3": {
   "unit": "game/mainProgram0800",
   "line": "... onto the stack"
  },
  "10F4": {
   "unit": "game/mainProgram0800",
   "line": "Y ..."
  },
  "10F5": {
   "unit": "game/mainProgram0800",
   "line": "... onto the stack"
  },
  "10F6": {
   "unit": "game/mainProgram0800",
   "line": "S now points 6 bytes below the pushed processor status"
  },
  "10F7": {
   "unit": "game/mainProgram0800",
   "line": "$0104+S = the status byte the CPU pushed when it took the interrupt"
  },
  "10FA": {
   "unit": "game/mainProgram0800",
   "line": "bit 4 = the B flag: set means this was a BRK, not a raster IRQ"
  },
  "10FC": {
   "unit": "game/mainProgram0800",
   "line": "a real interrupt - carry on"
  },
  "10FE": {
   "unit": "game/mainProgram0800",
   "line": "BRK debug hook: the operand at $10FF/$1100 is patched by initInterruptVectors to irqExit"
  },
  "1101": {
   "unit": "game/mainProgram0800",
   "block": [
    "Acknowledge the VIC interrupt and re-arm both the raster line and the RAM interrupt vector; the",
    "vector is rewritten every frame because the film code temporarily flips $01 to see the RAM under",
    "the I/O area."
   ],
   "line": "bit 0 = the raster interrupt latch ..."
  },
  "1103": {
   "unit": "game/mainProgram0800",
   "line": "... written back to clear it"
  },
  "1106": {
   "unit": "game/mainProgram0800",
   "line": "the raster line the game wants ($FA = 250, below the visible playfield)"
  },
  "1108": {
   "unit": "game/mainProgram0800",
   "line": "re-arm VIC_RASTER for the next frame"
  },
  "110B": {
   "unit": "game/mainProgram0800",
   "line": "shadow of the IRQ vector low byte ..."
  },
  "110D": {
   "unit": "game/mainProgram0800",
   "line": "... written back into $FFFE"
  },
  "1110": {
   "unit": "game/mainProgram0800",
   "line": "shadow of the high byte ..."
  },
  "1112": {
   "unit": "game/mainProgram0800",
   "line": "... written back into $FFFF"
  },
  "1115": {
   "unit": "game/mainProgram0800",
   "line": "is a previous invocation still running? (the handler re-enables interrupts halfway through)"
  },
  "1117": {
   "unit": "game/mainProgram0800",
   "line": "yes - do nothing this frame"
  },
  "1119": {
   "unit": "game/mainProgram0800",
   "line": "mark the handler as busy"
  },
  "111B": {
   "unit": "game/mainProgram0800",
   "line": "shadow of VIC_CTRL1 (screen mode, raster bit 8, Y scroll)"
  },
  "111E": {
   "unit": "game/mainProgram0800",
   "line": "-> $D011"
  },
  "1121": {
   "unit": "game/mainProgram0800",
   "line": "shadow of VIC_CTRL2 (multicolour, X scroll)"
  },
  "1124": {
   "unit": "game/mainProgram0800",
   "line": "-> $D016"
  },
  "1127": {
   "unit": "game/mainProgram0800",
   "line": "function 0 = the comm module's per-frame service tick"
  },
  "1129": {
   "unit": "game/mainProgram0800",
   "line": "run the UART / trainer bit-and-byte timing"
  },
  "112C": {
   "unit": "game/mainProgram0800",
   "line": "and the suspend handshake plus the carrier-detect sampling"
  },
  "112F": {
   "unit": "game/mainProgram0800",
   "line": "how deeply nested is the current disk load?"
  },
  "1132": {
   "unit": "game/mainProgram0800",
   "line": "always taken in practice - nothing ever makes diskLoadDepth negative, so the bail-out below is effectively dead"
  },
  "1134": {
   "unit": "game/mainProgram0800",
   "line": "bail out with only the comm module serviced"
  },
  "1137": {
   "unit": "game/mainProgram0800",
   "block": [
    "Phase 2: push the rest of the VIC shadow block ($9002-$9006, $9020, $9023, $9024) into the chip.",
    "Keeping shadows and copying them once per frame means the game never has to worry about a partly",
    "updated register being displayed."
   ],
   "line": "shadow of VIC_MEM_PTR (bitmap at $A000, video matrix at $8C00 within VIC bank 2)"
  },
  "113A": {
   "unit": "game/mainProgram0800",
   "line": "-> $D018"
  },
  "113D": {
   "unit": "game/mainProgram0800",
   "line": "border colour shadow"
  },
  "1140": {
   "unit": "game/mainProgram0800",
   "line": "-> $D020"
  },
  "1143": {
   "unit": "game/mainProgram0800",
   "line": "background colour 0 shadow"
  },
  "1146": {
   "unit": "game/mainProgram0800",
   "line": "-> $D021"
  },
  "1149": {
   "unit": "game/mainProgram0800",
   "line": "multicolour background 1 shadow"
  },
  "114C": {
   "unit": "game/mainProgram0800",
   "line": "-> $D022"
  },
  "114F": {
   "unit": "game/mainProgram0800",
   "line": "multicolour background 2 shadow"
  },
  "1152": {
   "unit": "game/mainProgram0800",
   "line": "-> $D023"
  },
  "1155": {
   "unit": "game/mainProgram0800",
   "line": "sprite X bit-8 mask shadow"
  },
  "1158": {
   "unit": "game/mainProgram0800",
   "line": "-> $D010"
  },
  "115B": {
   "unit": "game/mainProgram0800",
   "line": "sprite X expand mask shadow"
  },
  "115E": {
   "unit": "game/mainProgram0800",
   "line": "-> $D01D"
  },
  "1161": {
   "unit": "game/mainProgram0800",
   "line": "sprite Y expand mask shadow"
  },
  "1164": {
   "unit": "game/mainProgram0800",
   "line": "-> $D017"
  },
  "1167": {
   "unit": "game/mainProgram0800",
   "block": [
    "Sprite position bias: while screenShakeTimer is running the sprites are shifted by the current fine",
    "scroll offsets so they stay glued to the smooth-scrolled bitmap; otherwise both biases are zero.",
    "The two offsets are written into the ADC operands at $11B6 and $11BF."
   ],
   "line": "is the screen being shaken / fine-scrolled?"
  },
  "116A": {
   "unit": "game/mainProgram0800",
   "line": "yes - derive the offsets from the scroll position"
  },
  "116C": {
   "unit": "game/mainProgram0800",
   "line": "no bias: patch the X adder at $11B5 with 0"
  },
  "116F": {
   "unit": "game/mainProgram0800",
   "line": "and the Y adder at $11BE with 0"
  },
  "1172": {
   "unit": "game/mainProgram0800",
   "line": "always taken (A is still 0)"
  },
  "1174": {
   "unit": "game/mainProgram0800",
   "line": "vertical fine-scroll position"
  },
  "1177": {
   "unit": "game/mainProgram0800",
   "line": "only the 0-7 pixel offset"
  },
  "1179": {
   "unit": "game/mainProgram0800",
   "line": "prepare a borrow-free subtraction"
  },
  "117A": {
   "unit": "game/mainProgram0800",
   "line": "centre it: -3..+4 pixels"
  },
  "117C": {
   "unit": "game/mainProgram0800",
   "line": "patch the Y adder operand at $11BE"
  },
  "117F": {
   "unit": "game/mainProgram0800",
   "line": "horizontal fine-scroll position"
  },
  "1182": {
   "unit": "game/mainProgram0800",
   "line": "only the 0-7 pixel offset"
  },
  "1184": {
   "unit": "game/mainProgram0800",
   "line": "patch the X adder operand at $11B5"
  },
  "1187": {
   "unit": "game/mainProgram0800",
   "block": [
    "Copy the eight sprite shadows into the VIC.  Y counts the sprite index 7..0 and X the register",
    "offset 2*index, so $D000+X / $D001+X are that sprite's X and Y registers.  A colour shadow below",
    "$10 is a plain colour; $10 and above packs two nibbles which are swapped every other frame to make",
    "the sprite flash between two colours."
   ],
   "line": "start with sprite 7"
  },
  "1189": {
   "unit": "game/mainProgram0800",
   "line": "$0E = 7*2, the X/Y register offset of sprite 7"
  },
  "118B": {
   "unit": "game/mainProgram0800",
   "line": "free-running frame counter ..."
  },
  "118D": {
   "unit": "game/mainProgram0800",
   "line": "... rotated right twice ..."
  },
  "118E": {
   "unit": "game/mainProgram0800",
   "line": "... so that bit 7 of the result is bit 0 of the frame counter"
  },
  "118F": {
   "unit": "game/mainProgram0800",
   "line": "the flash phase: negative on every other frame"
  },
  "1191": {
   "unit": "game/mainProgram0800",
   "line": "colour shadow of sprite Y ($9008+Y)"
  },
  "1194": {
   "unit": "game/mainProgram0800",
   "line": "values 0-$0F are ordinary colours"
  },
  "1196": {
   "unit": "game/mainProgram0800",
   "line": "plain colour - write it straight out"
  },
  "1198": {
   "unit": "game/mainProgram0800",
   "line": "two packed nibbles: only swap them on the flashing frames"
  },
  "119A": {
   "unit": "game/mainProgram0800",
   "line": "off-phase frame - leave the colour register untouched"
  },
  "119C": {
   "unit": "game/mainProgram0800",
   "line": "compare the shadow with what the register currently holds"
  },
  "119F": {
   "unit": "game/mainProgram0800",
   "line": "look at the low nibble of the difference"
  },
  "11A1": {
   "unit": "game/mainProgram0800",
   "line": "C=1 when the register does not already show the shadow's low nibble"
  },
  "11A3": {
   "unit": "game/mainProgram0800",
   "line": "reload the packed shadow byte"
  },
  "11A6": {
   "unit": "game/mainProgram0800",
   "line": "register differs -> show the low nibble this time"
  },
  "11A8": {
   "unit": "game/mainProgram0800",
   "line": "otherwise shift the high nibble down ..."
  },
  "11A9": {
   "unit": "game/mainProgram0800",
   "line": "... (4 shifts) ..."
  },
  "11AA": {
   "unit": "game/mainProgram0800",
   "line": "... so the sprite alternates ..."
  },
  "11AB": {
   "unit": "game/mainProgram0800",
   "line": "... between the two packed colours"
  },
  "11AC": {
   "unit": "game/mainProgram0800",
   "line": "only the colour bits"
  },
  "11AE": {
   "unit": "game/mainProgram0800",
   "line": "-> $D027+Y, the sprite's colour register"
  },
  "11B1": {
   "unit": "game/mainProgram0800",
   "line": "X position shadow of sprite Y ($9010+Y)"
  },
  "11B4": {
   "unit": "game/mainProgram0800",
   "line": "clear carry for the bias add"
  },
  "11B5": {
   "unit": "game/mainProgram0800",
   "line": "add the horizontal bias (operand patched at $116C or $1184)"
  },
  "11B7": {
   "unit": "game/mainProgram0800",
   "line": "-> $D000+X, the sprite's X register"
  },
  "11BA": {
   "unit": "game/mainProgram0800",
   "line": "Y position shadow of sprite Y ($9018+Y)"
  },
  "11BD": {
   "unit": "game/mainProgram0800",
   "line": "clear carry for the bias add"
  },
  "11BE": {
   "unit": "game/mainProgram0800",
   "line": "add the vertical bias (operand patched at $116F or $117C)"
  },
  "11C0": {
   "unit": "game/mainProgram0800",
   "line": "-> $D001+X, the sprite's Y register"
  },
  "11C3": {
   "unit": "game/mainProgram0800",
   "line": "step the register offset ..."
  },
  "11C4": {
   "unit": "game/mainProgram0800",
   "line": "... down by two"
  },
  "11C5": {
   "unit": "game/mainProgram0800",
   "line": "next sprite"
  },
  "11C6": {
   "unit": "game/mainProgram0800",
   "line": "loop until sprite 0 has been done"
  },
  "11C8": {
   "unit": "game/mainProgram0800",
   "line": "sprite enable mask shadow"
  },
  "11CB": {
   "unit": "game/mainProgram0800",
   "line": "keep a copy at $10EF (nothing ever reads it back)"
  },
  "11CE": {
   "unit": "game/mainProgram0800",
   "line": "-> $D015, so sprites appear and disappear in step with the rest of the frame"
  },
  "11D1": {
   "unit": "game/mainProgram0800",
   "block": [
    "Bump the free-running 24-bit frame counter zp_45/zp_46/zp_47."
   ],
   "line": "low byte of the frame counter"
  },
  "11D3": {
   "unit": "game/mainProgram0800",
   "line": "no carry into the middle byte"
  },
  "11D5": {
   "unit": "game/mainProgram0800",
   "line": "middle byte"
  },
  "11D7": {
   "unit": "game/mainProgram0800",
   "line": "no carry into the high byte"
  },
  "11D9": {
   "unit": "game/mainProgram0800",
   "line": "high byte: wraps after about 77 hours"
  },
  "11DB": {
   "unit": "game/mainProgram0800",
   "block": [
    "From here on the handler runs with interrupts enabled - irqInProgress keeps a second raster IRQ",
    "from re-entering it.  inputLockoutTimer counts frames during which the keyboard and joystick are",
    "not sampled at all, so joyDirection and joyFireUp keep whatever they had."
   ],
   "line": "everything time-critical is done; allow interrupts again"
  },
  "11DC": {
   "unit": "game/mainProgram0800",
   "line": "is input still locked out (after a screen change or an abort)?"
  },
  "11DF": {
   "unit": "game/mainProgram0800",
   "line": "no - sample the keyboard and the stick"
  },
  "11E1": {
   "unit": "game/mainProgram0800",
   "line": "count the lockout down one frame ..."
  },
  "11E4": {
   "unit": "game/mainProgram0800",
   "line": "... and skip the whole input poll"
  },
  "11E7": {
   "unit": "game/mainProgram0800",
   "line": "scan the key matrix through the comm module's keyboard entry"
  },
  "11EA": {
   "unit": "game/mainProgram0800",
   "line": "default 'joystick idle' pattern in case a key is pending"
  },
  "11EC": {
   "unit": "game/mainProgram0800",
   "line": "did the poll leave a screen-select key pending (0-3 = F1/F3/F5/F7, $FF = none)?"
  },
  "11EE": {
   "unit": "game/mainProgram0800",
   "line": "yes - do not read the stick this frame"
  },
  "11F0": {
   "unit": "game/mainProgram0800",
   "line": "is a typed key waiting to be consumed?"
  },
  "11F3": {
   "unit": "game/mainProgram0800",
   "line": "$FF = no key"
  },
  "11F5": {
   "unit": "game/mainProgram0800",
   "line": "a key is pending - do not read the stick this frame"
  },
  "11F7": {
   "unit": "game/mainProgram0800",
   "line": "deselect every keyboard row ..."
  },
  "11F9": {
   "unit": "game/mainProgram0800",
   "line": "... so CIA1 port A shows joystick 2 instead of the matrix"
  },
  "11FC": {
   "unit": "game/mainProgram0800",
   "line": "read the port ..."
  },
  "11FF": {
   "unit": "game/mainProgram0800",
   "line": "... let it settle ..."
  },
  "1200": {
   "unit": "game/mainProgram0800",
   "line": "... and read it again"
  },
  "1203": {
   "unit": "game/mainProgram0800",
   "line": "the two reads disagree (contact bounce) - try once more"
  },
  "1205": {
   "unit": "game/mainProgram0800",
   "line": "keep bits 0-4: up, down, left, right, fire (all active low)"
  },
  "1207": {
   "unit": "game/mainProgram0800",
   "line": "assume the button is up"
  },
  "1209": {
   "unit": "game/mainProgram0800",
   "line": "bit 4 = the fire button"
  },
  "120B": {
   "unit": "game/mainProgram0800",
   "line": "bit set = button not pressed - keep Y=1"
  },
  "120D": {
   "unit": "game/mainProgram0800",
   "line": "button down -> Y=0"
  },
  "120E": {
   "unit": "game/mainProgram0800",
   "line": "latch the fire state for the whole game"
  },
  "1210": {
   "unit": "game/mainProgram0800",
   "line": "second copy - nothing ever reads it (dead store)"
  },
  "1212": {
   "unit": "game/mainProgram0800",
   "line": "keep the four direction bits ($0F = centred)"
  },
  "1214": {
   "unit": "game/mainProgram0800",
   "line": "latch the direction for the whole game"
  },
  "1216": {
   "unit": "game/mainProgram0800",
   "line": "second copy - nothing ever reads it (dead store)"
  },
  "1218": {
   "unit": "game/mainProgram0800",
   "block": [
    "inputIdleFrames counts consecutive frames with the stick centred and the button up, saturating at",
    "$FF; anything else resets it to 0.  The pause/attract logic elsewhere uses it."
   ],
   "line": "the latched direction nibble"
  },
  "121A": {
   "unit": "game/mainProgram0800",
   "line": "$0F = all four switches open = centred"
  },
  "121C": {
   "unit": "game/mainProgram0800",
   "line": "the stick is being pushed"
  },
  "121E": {
   "unit": "game/mainProgram0800",
   "line": "stick centred: forget any held direction ..."
  },
  "1220": {
   "unit": "game/mainProgram0800",
   "line": "... by clearing the direction repeat mask"
  },
  "1223": {
   "unit": "game/mainProgram0800",
   "line": "is the fire button also up?"
  },
  "1225": {
   "unit": "game/mainProgram0800",
   "line": "yes - nothing at all is being touched"
  },
  "1227": {
   "unit": "game/mainProgram0800",
   "line": "some input is active ..."
  },
  "1229": {
   "unit": "game/mainProgram0800",
   "line": "... so restart the idle counter"
  },
  "122C": {
   "unit": "game/mainProgram0800",
   "line": "always taken"
  },
  "122E": {
   "unit": "game/mainProgram0800",
   "line": "frames of no input so far"
  },
  "1231": {
   "unit": "game/mainProgram0800",
   "line": "already saturated?"
  },
  "1233": {
   "unit": "game/mainProgram0800",
   "line": "yes - leave it at $FF"
  },
  "1235": {
   "unit": "game/mainProgram0800",
   "line": "another idle frame"
  },
  "1238": {
   "unit": "game/mainProgram0800",
   "line": "stir the 24-bit game RNG once per frame"
  },
  "123B": {
   "unit": "game/mainProgram0800",
   "line": "index 5 down to 0 covers zp_71..zp_6C"
  },
  "123D": {
   "unit": "game/mainProgram0800",
   "block": [
    "Decrement the six frame countdowns zp_6C-zp_71 (joystick repeat, fire debounce, menu blink, marker",
    "blink, key repeat and one spare), each only while it is non-zero."
   ],
   "line": "timer zp_6C+X"
  },
  "123F": {
   "unit": "game/mainProgram0800",
   "line": "already expired - leave it alone"
  },
  "1241": {
   "unit": "game/mainProgram0800",
   "line": "one frame closer to expiry"
  },
  "1243": {
   "unit": "game/mainProgram0800",
   "line": "next timer"
  },
  "1244": {
   "unit": "game/mainProgram0800",
   "line": "loop over all six"
  },
  "1246": {
   "unit": "game/mainProgram0800",
   "line": "is a disk load or overlay swap in progress?"
  },
  "1249": {
   "unit": "game/mainProgram0800",
   "line": "no - function keys may switch screens"
  },
  "124B": {
   "unit": "game/mainProgram0800",
   "line": "$FF = no screen requested ..."
  },
  "124D": {
   "unit": "game/mainProgram0800",
   "line": "... so an F-key pressed during a load is thrown away"
  },
  "124F": {
   "unit": "game/mainProgram0800",
   "block": [
    "The panic hotkey: SHIFT + C= + F7 abandons whatever is happening and returns to the main menu.",
    "returnToMainMenu resets the stack pointer, so this JMP never comes back - the interrupt frame is",
    "simply abandoned (interrupts were already re-enabled at $11DB)."
   ],
   "line": "was SHIFT held during the key scan?"
  },
  "1252": {
   "unit": "game/mainProgram0800",
   "line": "no - not the abort combination"
  },
  "1254": {
   "unit": "game/mainProgram0800",
   "line": "was the Commodore key held too?"
  },
  "1257": {
   "unit": "game/mainProgram0800",
   "line": "no - not the abort combination"
  },
  "1259": {
   "unit": "game/mainProgram0800",
   "line": "which function key is pending?"
  },
  "125B": {
   "unit": "game/mainProgram0800",
   "line": "3 = F7"
  },
  "125D": {
   "unit": "game/mainProgram0800",
   "line": "not F7 - carry on normally"
  },
  "125F": {
   "unit": "game/mainProgram0800",
   "line": "lock the input out ..."
  },
  "1261": {
   "unit": "game/mainProgram0800",
   "line": "... by pretending a disk load is running"
  },
  "1264": {
   "unit": "game/mainProgram0800",
   "line": "$FF ..."
  },
  "1266": {
   "unit": "game/mainProgram0800",
   "line": "... = the longest possible input lockout"
  },
  "1269": {
   "unit": "game/mainProgram0800",
   "line": "... and forget the pending F7"
  },
  "126B": {
   "unit": "game/mainProgram0800",
   "line": "never returns: returnToMainMenu rebuilds the stack from scratch"
  },
  "126E": {
   "unit": "game/mainProgram0800",
   "block": [
    "Per-frame housekeeping that is allowed to run with interrupts on."
   ],
   "line": "which screen is showing?"
  },
  "1271": {
   "unit": "game/mainProgram0800",
   "line": "not the battlefield screen - no map cursor to blink"
  },
  "1273": {
   "unit": "game/mainProgram0800",
   "line": "gamePhase: bit 7 marks the phases in which there is no map cursor to blink"
  },
  "1275": {
   "unit": "game/mainProgram0800",
   "line": "gamePhase negative -> skip the blink"
  },
  "1277": {
   "unit": "game/mainProgram0800",
   "line": "toggle the map cursor sprite colour"
  },
  "127A": {
   "unit": "game/mainProgram0800",
   "line": "count the game's own frame timers down"
  },
  "127D": {
   "unit": "game/mainProgram0800",
   "line": "outgoing chat: pick up typed characters and queue them to the opponent"
  },
  "1280": {
   "unit": "game/mainProgram0800",
   "line": "incoming chat: take one received byte and add it to the message line"
  },
  "1283": {
   "unit": "game/mainProgram0800",
   "line": "take the highest-priority queued sound request into X"
  },
  "1286": {
   "unit": "game/mainProgram0800",
   "line": "X = $FF when nothing is queued"
  },
  "1287": {
   "unit": "game/mainProgram0800",
   "line": "nothing to start"
  },
  "1289": {
   "unit": "game/mainProgram0800",
   "line": "start sound A on its voices"
  },
  "128C": {
   "unit": "game/mainProgram0800",
   "line": "advance the SID sound scripts by one frame"
  },
  "128F": {
   "unit": "game/mainProgram0800",
   "line": "the handler is finished ..."
  },
  "1291": {
   "unit": "game/mainProgram0800",
   "line": "... so a later raster IRQ may run it again"
  },
  "1293": {
   "unit": "game/mainProgram0800",
   "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 that initInterruptVectors patches into the BRK-exit JMP at",
    "$10FF/$1100.",
    "In:  the register frame pushed by rasterIrqHandler",
    "Out: registers restored, RTI",
    "Called from: $10FE (BRK) and $1134 (early bail-out); reached by fall-through at the end of the handler"
   ],
   "line": "Y ..."
  },
  "1294": {
   "unit": "game/mainProgram0800",
   "line": "... restored"
  },
  "1295": {
   "unit": "game/mainProgram0800",
   "line": "X ..."
  },
  "1296": {
   "unit": "game/mainProgram0800",
   "line": "... restored"
  },
  "1297": {
   "unit": "game/mainProgram0800",
   "line": "and A"
  },
  "1298": {
   "unit": "game/mainProgram0800",
   "routine": [
    "irqRti - A bare RTI.  It is the NMI vector target for the whole game: initInterruptVectors and",
    "setNmiVectorRti put $1298 into $FFFA/$FFFB and into the comm module's own NMI chain vector",
    "($E031/$E032), so a stray RS-232 NMI cannot disturb the game when no modem session is running.",
    "In:  none",
    "Out: none",
    "Called from: the NMI vector; reached by fall-through from irqExit"
   ],
   "line": "return from interrupt"
  },
  "1299": {
   "unit": "game/mainProgram0800",
   "block": [
    "Three leftover bytes between irqRti and updateFrameTimers.  Nothing references them and they do not",
    "disassemble into anything sensible - alignment padding or the tail of an edited-out routine."
   ],
   "line": "unreferenced padding"
  },
  "129C": {
   "unit": "game/mainProgram0800",
   "routine": [
    "updateFrameTimers - Per-frame housekeeping called from the raster IRQ.  It bumps the two",
    "free-running animation ticks, then runs three groups of counters: an 8-frame group (the",
    "packet-exchange watchdog, the end-of-game countdown and linkWaitTimer), a 4-frame group (the",
    "status-message hold timer and the link poll delay) and a per-frame group (step delay, shot line,",
    "view redraw and marker sprite).  Every countdown is guarded so it stops at 0.",
    "In:  frameCounter (zp_45), gamePhase (zp_B1)",
    "Out: $92F8, $92FB, $9223, $92C9, zp_7F, zp_72, $9163, $90FC, $91C8, $9234, $923E",
    "Called from: rasterIrqHandler ($127A)"
   ],
   "line": "animation tick used by the missile screen"
  },
  "129F": {
   "unit": "game/mainProgram0800",
   "line": "animation tick used by the radar sweep"
  },
  "12A2": {
   "unit": "game/mainProgram0800",
   "line": "the frame counter drives all the slower groups"
  },
  "12A4": {
   "unit": "game/mainProgram0800",
   "line": "every 8th frame ..."
  },
  "12A6": {
   "unit": "game/mainProgram0800",
   "line": "... otherwise skip straight to the 4-frame group"
  },
  "12A8": {
   "unit": "game/mainProgram0800",
   "line": "which of the two 8-frame slots is this?"
  },
  "12AA": {
   "unit": "game/mainProgram0800",
   "line": "low nibble 0 = one of the two, i.e. every 16 frames"
  },
  "12AC": {
   "unit": "game/mainProgram0800",
   "line": "16-frame slot: always bump the watchdog"
  },
  "12AE": {
   "unit": "game/mainProgram0800",
   "line": "the odd 8-frame slot only counts when gamePhase is not $FF ..."
  },
  "12B0": {
   "unit": "game/mainProgram0800",
   "line": "... so at gamePhase $FF the watchdog runs at half speed (every 16 frames)"
  },
  "12B2": {
   "unit": "game/mainProgram0800",
   "line": "skip the watchdog this time"
  },
  "12B4": {
   "unit": "game/mainProgram0800",
   "block": [
    "linkTimeoutCounter is the packet-exchange watchdog: checkLinkTimeout ($5A14) arms it with bit 7",
    "set, a completed exchange clears it at $4E69, and here it counts up while negative and sticks at",
    "$FF, which is how the game notices that the opponent has stopped answering."
   ],
   "line": "the watchdog value"
  },
  "12B7": {
   "unit": "game/mainProgram0800",
   "line": "positive = not armed, nothing to do"
  },
  "12B9": {
   "unit": "game/mainProgram0800",
   "line": "clear carry for the increment"
  },
  "12BA": {
   "unit": "game/mainProgram0800",
   "line": "one more tick towards the timeout"
  },
  "12BC": {
   "unit": "game/mainProgram0800",
   "line": "already at $FF - saturate instead of wrapping to 0"
  },
  "12BE": {
   "unit": "game/mainProgram0800",
   "line": "store the advanced watchdog"
  },
  "12C1": {
   "unit": "game/mainProgram0800",
   "line": "gameEndCountdown: the delay from 'game over' to the summary screen"
  },
  "12C4": {
   "unit": "game/mainProgram0800",
   "line": "0 = not running"
  },
  "12C6": {
   "unit": "game/mainProgram0800",
   "line": "negative = it has already reached its $80 end stop"
  },
  "12C8": {
   "unit": "game/mainProgram0800",
   "line": "count it up towards $80"
  },
  "12CB": {
   "unit": "game/mainProgram0800",
   "line": "linkWaitTimer: the timeout used while waiting for the comm module"
  },
  "12CD": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "12CF": {
   "unit": "game/mainProgram0800",
   "line": "one 8-frame tick closer to zero"
  },
  "12D1": {
   "unit": "game/mainProgram0800",
   "line": "the frame counter again ..."
  },
  "12D3": {
   "unit": "game/mainProgram0800",
   "line": "... for the every-4-frames group"
  },
  "12D5": {
   "unit": "game/mainProgram0800",
   "line": "not a 4-frame slot"
  },
  "12D7": {
   "unit": "game/mainProgram0800",
   "line": "messageTimer: how long the status-line message stays up"
  },
  "12D9": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "12DB": {
   "unit": "game/mainProgram0800",
   "line": "count it down"
  },
  "12DD": {
   "unit": "game/mainProgram0800",
   "line": "linkPollDelay: pacing of the link status polls"
  },
  "12E0": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "12E2": {
   "unit": "game/mainProgram0800",
   "line": "count it down"
  },
  "12E5": {
   "unit": "game/mainProgram0800",
   "block": [
    "The per-frame group: four independent countdowns, each frozen once it reaches 0."
   ],
   "line": "stepDelayTimer: frames until the next unit step of the round"
  },
  "12E8": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "12EA": {
   "unit": "game/mainProgram0800",
   "line": "count it down"
  },
  "12ED": {
   "unit": "game/mainProgram0800",
   "line": "shotLineTimer: how long a shot line stays drawn in the viewport"
  },
  "12F0": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "12F2": {
   "unit": "game/mainProgram0800",
   "line": "count it down"
  },
  "12F5": {
   "unit": "game/mainProgram0800",
   "line": "viewRedrawTimer: frames until the 7x5 view is redrawn"
  },
  "12F8": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "12FA": {
   "unit": "game/mainProgram0800",
   "line": "$80 is the 'frozen / redraw pending' sentinel ..."
  },
  "12FC": {
   "unit": "game/mainProgram0800",
   "line": "... which must not be counted down"
  },
  "12FE": {
   "unit": "game/mainProgram0800",
   "line": "count it down"
  },
  "1301": {
   "unit": "game/mainProgram0800",
   "line": "markerSpriteTimer: how long the repair/target marker sprites stay visible"
  },
  "1304": {
   "unit": "game/mainProgram0800",
   "line": "already expired"
  },
  "1306": {
   "unit": "game/mainProgram0800",
   "line": "count it down"
  },
  "1309": {
   "unit": "game/mainProgram0800",
   "line": "back to the interrupt handler"
  },
  "130A": {
   "unit": "game/mainProgram0800",
   "routine": [
    "stepRandomSeed - Called from the IRQ every frame.  If the 24-bit game RNG state has collapsed to",
    "all zeroes (which would lock the LFSR up) it re-seeds the low byte from the current joystick",
    "direction, so the generator both recovers and is fed a little entropy from the player, then steps",
    "the LFSR once.",
    "In:  gameRngState0-2 (zp_57-zp_59), joyDirection (zp_5D)",
    "Out: the RNG state advanced, A = the new random byte",
    "Called from: rasterIrqHandler ($1238)"
   ],
   "line": "byte 0 of the RNG state ..."
  },
  "130C": {
   "unit": "game/mainProgram0800",
   "line": "... ORed with byte 1 ..."
  },
  "130E": {
   "unit": "game/mainProgram0800",
   "line": "... and byte 2"
  },
  "1310": {
   "unit": "game/mainProgram0800",
   "line": "any bit set - the state is fine"
  },
  "1312": {
   "unit": "game/mainProgram0800",
   "line": "all zero: take the joystick nibble as fresh entropy ..."
  },
  "1314": {
   "unit": "game/mainProgram0800",
   "line": "... and drop it into byte 0 so the LFSR can run again"
  },
  "1316": {
   "unit": "game/mainProgram0800",
   "line": "advance the 24-bit LFSR in the high-memory block"
  },
  "1319": {
   "unit": "game/mainProgram0800",
   "line": "A holds the new random byte"
  },
  "131A": {
   "unit": "game/mainProgram0800",
   "block": [
    "cursorBlinkTimer: the 30-frame countdown of blinkCursorColour, stored in the code stream just in",
    "front of the routine that owns it."
   ],
   "line": "counts 30..0 and then wraps; bit 7 signals 'time to toggle'"
  },
  "131B": {
   "unit": "game/mainProgram0800",
   "routine": [
    "blinkCursorColour - Blinks the map cursor.  A 30-frame countdown toggles bit 0 of the sprite 0",
    "colour shadow and copies the result to the sprite 2 shadow, so the two cursor sprites alternate",
    "between two adjacent colours.  The IRQ only calls it while currentScreen is 0 (the battlefield",
    "screen) and gamePhase is not negative.",
    "In:  cursorBlinkTimer ($131A), spriteColourShadow ($9008)",
    "Out: $131A, $9008, $900A",
    "Called from: rasterIrqHandler ($1277)"
   ],
   "line": "one frame off the blink countdown"
  },
  "131E": {
   "unit": "game/mainProgram0800",
   "line": "still positive - nothing to do this frame"
  },
  "1320": {
   "unit": "game/mainProgram0800",
   "line": "$1E = 30 frames until the next toggle ..."
  },
  "1322": {
   "unit": "game/mainProgram0800",
   "line": "... reload the countdown"
  },
  "1325": {
   "unit": "game/mainProgram0800",
   "line": "sprite 0 colour shadow"
  },
  "1328": {
   "unit": "game/mainProgram0800",
   "line": "flip the lowest colour bit (e.g. between colours 6 and 7)"
  },
  "132A": {
   "unit": "game/mainProgram0800",
   "line": "back into the sprite 0 shadow"
  },
  "132D": {
   "unit": "game/mainProgram0800",
   "line": "and mirror it into the sprite 2 shadow so both halves of the cursor match"
  },
  "1330": {
   "unit": "game/mainProgram0800",
   "line": "the IRQ picks the shadows up on the next frame"
  },
  "1331": {
   "unit": "game/mainProgram0800",
   "routine": [
    "handleChatSend - The outgoing half of the modem chat ('message to opponent'), run every frame from",
    "the IRQ.  chatState ($929C) is the state byte: 0 = off, $7F = 'open the typing line', and bit 7 set",
    "= typing, with bits 0-5 holding the cursor position in the outgoing line buffer $9278.  Typing is",
    "entered by RETURN (or by another routine setting $7F) provided the module's transmit ring is empty;",
    "after that each frame hands one key to the $C000 line editor and queues the accepted character to",
    "the opponent.  If the transmit ring fills up the line is terminated with RETURN instead.",
    "In:  $E03B link flag, $E02E transmit ring count, filmNameLength ($92E6), chatState ($929C), newKeyEvent ($90EA)",
    "Out: chatState, the line buffer $9278.., newKeyEvent/lastTypedKey cleared, one byte queued through $E009, sounds",
    "Called from: rasterIrqHandler ($127D)"
   ],
   "line": "is the link up?"
  },
  "1334": {
   "unit": "game/mainProgram0800",
   "line": "no link - chat is impossible"
  },
  "1336": {
   "unit": "game/mainProgram0800",
   "line": "is the SAVE GAME FILM name field using the same line buffer?"
  },
  "1339": {
   "unit": "game/mainProgram0800",
   "line": "no - the chat may use it"
  },
  "133B": {
   "unit": "game/mainProgram0800",
   "line": "shut the chat down ..."
  },
  "133D": {
   "unit": "game/mainProgram0800",
   "line": "... by clearing the state byte"
  },
  "1340": {
   "unit": "game/mainProgram0800",
   "line": "nothing more to do this frame"
  },
  "1341": {
   "unit": "game/mainProgram0800",
   "line": "the chat state byte"
  },
  "1344": {
   "unit": "game/mainProgram0800",
   "line": "bit 7 set = already typing -> go and send the next character"
  },
  "1346": {
   "unit": "game/mainProgram0800",
   "line": "$7F = another routine asked for the typing line to be opened"
  },
  "1348": {
   "unit": "game/mainProgram0800",
   "line": "open it"
  },
  "134A": {
   "unit": "game/mainProgram0800",
   "line": "otherwise look at the last key"
  },
  "134D": {
   "unit": "game/mainProgram0800",
   "line": "$8D = RETURN opens the chat line"
  },
  "134F": {
   "unit": "game/mainProgram0800",
   "line": "open it"
  },
  "1351": {
   "unit": "game/mainProgram0800",
   "line": "nothing to do this frame"
  },
  "1352": {
   "unit": "game/mainProgram0800",
   "block": [
    "Enter typing mode - but only if the module has nothing left to transmit."
   ],
   "line": "double-check the link is still up"
  },
  "1355": {
   "unit": "game/mainProgram0800",
   "line": "it dropped - abandon"
  },
  "1357": {
   "unit": "game/mainProgram0800",
   "line": "how many bytes are still queued for transmission?"
  },
  "135A": {
   "unit": "game/mainProgram0800",
   "line": "the ring is not empty - wait until it drains"
  },
  "135C": {
   "unit": "game/mainProgram0800",
   "line": "$C0 = bit 7 (typing) + bit 6, cursor position 0 ..."
  },
  "135E": {
   "unit": "game/mainProgram0800",
   "line": "... becomes the new chat state"
  },
  "1361": {
   "unit": "game/mainProgram0800",
   "line": "$9E = the cursor glyph ..."
  },
  "1363": {
   "unit": "game/mainProgram0800",
   "line": "... put at the head of the outgoing line so the player sees a caret"
  },
  "1366": {
   "unit": "game/mainProgram0800",
   "line": "swallow the key ..."
  },
  "1368": {
   "unit": "game/mainProgram0800",
   "line": "... that opened the line ..."
  },
  "136B": {
   "unit": "game/mainProgram0800",
   "line": "... and the typed-key copy as well"
  },
  "136E": {
   "unit": "game/mainProgram0800",
   "line": "sound 2 = the typing click"
  },
  "1370": {
   "unit": "game/mainProgram0800",
   "line": "requestSound is called directly (playSound is not re-entrant and this is inside the IRQ)"
  },
  "1373": {
   "unit": "game/mainProgram0800",
   "block": [
    "Typing mode: bits 0-5 of chatState are the cursor position.  One key per frame is fed to the",
    "shared line editor and the accepted character is queued to the opponent, so the remote sees the",
    "message being typed letter by letter."
   ],
   "line": "cursor position within the 36-byte line buffer"
  },
  "1375": {
   "unit": "game/mainProgram0800",
   "line": "the editor wants it in Y"
  },
  "1376": {
   "unit": "game/mainProgram0800",
   "line": "how full is the transmit ring?"
  },
  "1379": {
   "unit": "game/mainProgram0800",
   "line": "$10 = 16 bytes = full"
  },
  "137B": {
   "unit": "game/mainProgram0800",
   "line": "room left - edit one more character"
  },
  "137D": {
   "unit": "game/mainProgram0800",
   "line": "ring full: force the line to end ..."
  },
  "137E": {
   "unit": "game/mainProgram0800",
   "line": "... by setting bit 7 of the cursor position (the editor's 'RETURN' marker) ..."
  },
  "1380": {
   "unit": "game/mainProgram0800",
   "line": "... back into Y"
  },
  "1381": {
   "unit": "game/mainProgram0800",
   "line": "and send RETURN instead of a character"
  },
  "1383": {
   "unit": "game/mainProgram0800",
   "line": "always taken"
  },
  "1385": {
   "unit": "game/mainProgram0800",
   "line": "$A3 = max 35 characters with bit 7 = 'scroll the line left when it is full'"
  },
  "1387": {
   "unit": "game/mainProgram0800",
   "line": "let the $C000 line editor consume newKeyEvent into the buffer"
  },
  "138A": {
   "unit": "game/mainProgram0800",
   "line": "nothing was accepted this frame"
  },
  "138C": {
   "unit": "game/mainProgram0800",
   "line": "keep the accepted character"
  },
  "138D": {
   "unit": "game/mainProgram0800",
   "line": "the editor's updated cursor position"
  },
  "138E": {
   "unit": "game/mainProgram0800",
   "line": "toggle bit 7: a normal character leaves it set (still typing), RETURN clears it (line finished)"
  },
  "1390": {
   "unit": "game/mainProgram0800",
   "line": "bit 6 is always set so the state byte is never mistaken for 0"
  },
  "1392": {
   "unit": "game/mainProgram0800",
   "line": "store the new chat state"
  },
  "1395": {
   "unit": "game/mainProgram0800",
   "line": "get the character back"
  },
  "1396": {
   "unit": "game/mainProgram0800",
   "line": "queue it to the opponent and return to the IRQ"
  },
  "1399": {
   "unit": "game/mainProgram0800",
   "routine": [
    "receiveChatCharacter - The incoming half of the chat, called every frame from the IRQ.  It pulls one",
    "byte out of the comm module's receive ring and ignores anything below $80.  RETURN ($8D) marks the",
    "message complete, DEL ($88) backspaces by overwriting the previous character with a space, and",
    "everything else is appended to the incoming message line.",
    "In:  the comm receive ring through $E006 (C=1 = empty), incomingChatLength ($9277)",
    "Out: the $9253 buffer and the $0548 status line, $9277, messageTimer cleared, sound 5",
    "Called from: rasterIrqHandler ($1280)"
   ],
   "line": "fetch one received byte (C=1 = nothing waiting)"
  },
  "139C": {
   "unit": "game/mainProgram0800",
   "line": "nothing received this frame"
  },
  "139E": {
   "unit": "game/mainProgram0800",
   "line": "chat characters always have bit 7 set ..."
  },
  "13A0": {
   "unit": "game/mainProgram0800",
   "line": "... so anything below $80 is not chat text"
  },
  "13A2": {
   "unit": "game/mainProgram0800",
   "line": "$8D = RETURN, the end of the opponent's message"
  },
  "13A4": {
   "unit": "game/mainProgram0800",
   "line": "not RETURN - try DEL"
  },
  "13A6": {
   "unit": "game/mainProgram0800",
   "line": "$80 marks the message complete: bit 7 set, length 0 ..."
  },
  "13A8": {
   "unit": "game/mainProgram0800",
   "line": "... so the next character starts a fresh message"
  },
  "13AB": {
   "unit": "game/mainProgram0800",
   "line": "always taken - go and beep"
  },
  "13AD": {
   "unit": "game/mainProgram0800",
   "line": "$88 = DEL"
  },
  "13AF": {
   "unit": "game/mainProgram0800",
   "line": "an ordinary character - append it"
  },
  "13B1": {
   "unit": "game/mainProgram0800",
   "line": "how long is the message so far?"
  },
  "13B4": {
   "unit": "game/mainProgram0800",
   "line": "empty - nothing to erase"
  },
  "13B6": {
   "unit": "game/mainProgram0800",
   "line": "the previous message was already complete - nothing to erase"
  },
  "13B8": {
   "unit": "game/mainProgram0800",
   "line": "step back over the last character"
  },
  "13BB": {
   "unit": "game/mainProgram0800",
   "line": "a space ..."
  },
  "13BD": {
   "unit": "game/mainProgram0800",
   "line": "... written over it (this re-increments the length)"
  },
  "13C0": {
   "unit": "game/mainProgram0800",
   "line": "step back again so the next character lands on that space"
  },
  "13C3": {
   "unit": "game/mainProgram0800",
   "line": "back to the IRQ"
  },
  "13C4": {
   "unit": "game/mainProgram0800",
   "routine": [
    "appendChatCharacter - Appends character A to the 36-byte incoming message buffer at $9253 and",
    "rewrites the visible status line from it.  A negative incomingChatLength means the previous message",
    "was finished, so the buffer restarts at 0; when the buffer is full at 35 characters it is scrolled",
    "one byte left.  The line is rewritten backwards from the newest character so that only that",
    "character carries bit 7 - the game's string terminator convention - and the printed line therefore",
    "always ends exactly where the message does.",
    "In:  A = character, incomingChatLength ($9277), the $9253 buffer",
    "Out: $9253-$9276, $9277, the status line $0548-$056C, messageTimer = 0, sound 5",
    "Called from: receiveChatCharacter ($13AF as a branch, $13BD as a call)"
   ],
   "line": "current message length = the index of the next free byte"
  },
  "13C7": {
   "unit": "game/mainProgram0800",
   "line": "a message is in progress"
  },
  "13C9": {
   "unit": "game/mainProgram0800",
   "line": "previous message finished: start a new one ..."
  },
  "13CB": {
   "unit": "game/mainProgram0800",
   "line": "... at offset 0"
  },
  "13CE": {
   "unit": "game/mainProgram0800",
   "line": "$23 = 35 characters, the width of the line"
  },
  "13D0": {
   "unit": "game/mainProgram0800",
   "line": "there is still room"
  },
  "13D2": {
   "unit": "game/mainProgram0800",
   "line": "the buffer is full - save the new character"
  },
  "13D3": {
   "unit": "game/mainProgram0800",
   "line": "scroll the whole buffer one byte to the left ..."
  },
  "13D5": {
   "unit": "game/mainProgram0800",
   "line": "... reading from $9254 (buffer+1) ..."
  },
  "13D8": {
   "unit": "game/mainProgram0800",
   "line": "... and writing to $9253"
  },
  "13DB": {
   "unit": "game/mainProgram0800",
   "line": "next byte"
  },
  "13DC": {
   "unit": "game/mainProgram0800",
   "line": "$24 = 36 bytes"
  },
  "13DE": {
   "unit": "game/mainProgram0800",
   "line": "keep scrolling"
  },
  "13E0": {
   "unit": "game/mainProgram0800",
   "line": "the write position moves back one"
  },
  "13E1": {
   "unit": "game/mainProgram0800",
   "line": "and so does the recorded length"
  },
  "13E4": {
   "unit": "game/mainProgram0800",
   "line": "get the new character back"
  },
  "13E5": {
   "unit": "game/mainProgram0800",
   "line": "store it without the terminator bit"
  },
  "13E7": {
   "unit": "game/mainProgram0800",
   "line": "into the buffer at the write position"
  },
  "13EA": {
   "unit": "game/mainProgram0800",
   "line": "the message is one character longer"
  },
  "13ED": {
   "unit": "game/mainProgram0800",
   "line": "the newest character carries the bit-7 terminator"
  },
  "13EF": {
   "unit": "game/mainProgram0800",
   "block": [
    "Rewrite the whole visible line backwards, newest character first, so that exactly one byte (the",
    "last one) has bit 7 set and the string printer stops there."
   ],
   "line": "write into the status line text at $0549+index"
  },
  "13F2": {
   "unit": "game/mainProgram0800",
   "line": "walk backwards through the message"
  },
  "13F3": {
   "unit": "game/mainProgram0800",
   "line": "the whole message has been written"
  },
  "13F5": {
   "unit": "game/mainProgram0800",
   "line": "earlier characters are stored plain (bit 7 clear)"
  },
  "13F8": {
   "unit": "game/mainProgram0800",
   "line": "keep going"
  },
  "13FB": {
   "unit": "game/mainProgram0800",
   "line": "$1E = the leading marker glyph ..."
  },
  "13FD": {
   "unit": "game/mainProgram0800",
   "line": "... at $0548, in front of the message text"
  },
  "1400": {
   "unit": "game/mainProgram0800",
   "line": "leftover test: the message queue head ..."
  },
  "1403": {
   "unit": "game/mainProgram0800",
   "line": "... compared with the tail ..."
  },
  "1406": {
   "unit": "game/mainProgram0800",
   "line": "... but both outcomes continue at $1408, so this compare has no effect (dead code)"
  },
  "1408": {
   "unit": "game/mainProgram0800",
   "line": "0 ..."
  },
  "140A": {
   "unit": "game/mainProgram0800",
   "line": "... clears the message hold timer so the chat line is shown at once"
  },
  "140C": {
   "unit": "game/mainProgram0800",
   "line": "sound 5 = the incoming-character click"
  },
  "140E": {
   "unit": "game/mainProgram0800",
   "line": "requestSound directly - this runs inside the IRQ, where playSound is not re-entrant"
  },
  "1411": {
   "unit": "game/mainProgram0800",
   "line": "back to the IRQ"
  },
  "1412": {
   "unit": "game/mainProgram0800",
   "block": [
    "$1412 is a scratch byte the REPAIR tab uses as a 3..0 loop counter while it erases and redraws the",
    "four comcen system markers ($184B-$187C).  $1413 holds the comcenStunStatus value that was current",
    "when drawRepairTab last ran ($175F), so the console loop can spot a change of stun state.  The",
    "$7F written into $1412 at $141F is never read: the repair code always reloads it first."
   ],
   "line": "REPAIR tab marker loop counter; the $7F stored at $141F is a leftover dead store"
  },
  "1413": {
   "unit": "game/mainProgram0800",
   "line": "copy of comcenStunStatus taken when the REPAIR tab was last drawn"
  },
  "1414": {
   "unit": "game/mainProgram0800",
   "routine": [
    "resumeMainMenuScreen - Re-entry into the console screen (screen 3, the F7 screen with the STATS /",
    "REPAIR / MISC tabs) that does not rebuild the frame: it just re-picks the default tab, clears the",
    "text window and joins the console loop at its 'tab has changed' handler so the panel is redrawn.",
    "Used when the comcen-stunned blackout ($0709) hands control back to whichever screen was showing.",
    "In:  none",
    "Out: menuTabIndex ($90F9) set, the tab panel redrawn; never returns (the console loop takes over)",
    "Called from: $076D, the screen dispatch at the end of runComcenStunnedBlackout"
   ],
   "line": "pick the tab the current game state suggests"
  },
  "1417": {
   "unit": "game/mainProgram0800",
   "line": "blank the panel area under the tab bar"
  },
  "141A": {
   "unit": "game/mainProgram0800",
   "line": "draw the newly selected tab and fall into the console loop"
  },
  "141D": {
   "unit": "game/mainProgram0800",
   "routine": [
    "enterMainMenu - Full entry into the console screen (screen 3): picks the default tab, draws the",
    "whole screen (tab bar and frames) and enters the console loop with that tab's panel drawn.",
    "In:  comcenStunStatus ($922A) and everything selectDefaultTab reads",
    "Out: the console screen drawn, menuTabIndex set; never returns",
    "Called from: $0706, the F7 branch of switchToRequestedScreen"
   ],
   "line": "$7F into the REPAIR tab's scratch byte $1412 ..."
  },
  "141F": {
   "unit": "game/mainProgram0800",
   "line": "... which nothing ever reads: the repair marker loops always reload it first, so this is a leftover dead store"
  },
  "1422": {
   "unit": "game/mainProgram0800",
   "line": "leftover tab pre-selection: default to tab 0 (STATS) ..."
  },
  "1424": {
   "unit": "game/mainProgram0800",
   "line": "... unless the comcen is stunned ..."
  },
  "1427": {
   "unit": "game/mainProgram0800",
   "line": "... (bit 7 of comcenStunStatus) ..."
  },
  "1429": {
   "unit": "game/mainProgram0800",
   "line": "... in which case tab 1 (REPAIR)"
  },
  "142A": {
   "unit": "game/mainProgram0800",
   "line": "store it - but selectDefaultTab three bytes later overwrites it unconditionally, so all of $1422-$142A is dead"
  },
  "142D": {
   "unit": "game/mainProgram0800",
   "line": "really choose the tab"
  },
  "1430": {
   "unit": "game/mainProgram0800",
   "line": "draw the tab bar and both frames (it reads menuTabIndex for the text colour)"
  },
  "1433": {
   "unit": "game/mainProgram0800",
   "line": "choose the tab again - redundant, nothing showMainMenu does changes the inputs"
  },
  "1436": {
   "unit": "game/mainProgram0800",
   "line": "draw that tab's panel and fall into the console loop"
  },
  "1439": {
   "unit": "game/mainProgram0800",
   "routine": [
    "mainMenuLoop - The console screen's main loop (screen 3, tabs STATS / REPAIR / MISC).  Every pass it",
    "runs one frame of the game engine (the battle keeps going while the console is open), blinks the",
    "selected tab, refreshes the REPAIR tab when the comcen stun state changes and the STATS tab when a",
    "command exchange has been applied, and honours the function keys.  The joystick moves the selection",
    "left and right between the three tabs with a 20-frame repeat, and the fire button dispatches through",
    "tabActionTable.  The loop never returns; every path jumps back to $1439.",
    "In:  joyDirection (zp_5D), joyRepeatTimer (zp_6C), menuTabIndex ($90F9), comcenStunStatus ($922A), exchangeAppliedFlag ($920E)",
    "Out: menuTabIndex, the console screen; runs forever",
    "Called from: entered by fall-through/JMP from resumeMainMenuScreen and enterMainMenu"
   ],
   "line": "keep the simulation running while the console is open"
  },
  "143C": {
   "unit": "game/mainProgram0800",
   "line": "toggle the selected tab between its bright and normal colour"
  },
  "143F": {
   "unit": "game/mainProgram0800",
   "line": "which tab is showing?"
  },
  "1442": {
   "unit": "game/mainProgram0800",
   "line": "1 = REPAIR"
  },
  "1444": {
   "unit": "game/mainProgram0800",
   "line": "any other tab - skip the repair refresh"
  },
  "1446": {
   "unit": "game/mainProgram0800",
   "line": "current comcen stun state ..."
  },
  "1449": {
   "unit": "game/mainProgram0800",
   "line": "... against the copy taken when the tab was drawn ..."
  },
  "144C": {
   "unit": "game/mainProgram0800",
   "line": "... looking only at bit 7 (stunned or not)"
  },
  "144E": {
   "unit": "game/mainProgram0800",
   "line": "unchanged - no full redraw needed"
  },
  "1450": {
   "unit": "game/mainProgram0800",
   "line": "the stun state flipped: redraw the whole REPAIR panel in its new colour"
  },
  "1453": {
   "unit": "game/mainProgram0800",
   "line": "refresh the comcen energy percentage (it only prints when the value changed)"
  },
  "1456": {
   "unit": "game/mainProgram0800",
   "line": "has a command exchange been applied since the STATS tab was drawn?"
  },
  "1459": {
   "unit": "game/mainProgram0800",
   "line": "no - nothing to refresh"
  },
  "145B": {
   "unit": "game/mainProgram0800",
   "line": "which tab is showing?"
  },
  "145E": {
   "unit": "game/mainProgram0800",
   "line": "only tab 0 (STATS) shows the numbers that just changed"
  },
  "1460": {
   "unit": "game/mainProgram0800",
   "line": "recount the units and reprint the STATS panel (it clears the flag again)"
  },
  "1463": {
   "unit": "game/mainProgram0800",
   "line": "let F1/F3/F5/F7 switch to another screen (never returns if it does)"
  },
  "1466": {
   "unit": "game/mainProgram0800",
   "block": [
    "Joystick handling: left and right move between the three tabs, everything else falls through to",
    "the fire-button test.  joyRepeatTimer holds the auto-repeat delay so a held stick steps once every",
    "20 frames."
   ],
   "line": "is the joystick auto-repeat still counting down?"
  },
  "1468": {
   "unit": "game/mainProgram0800",
   "line": "yes - ignore the stick, only look at the button"
  },
  "146A": {
   "unit": "game/mainProgram0800",
   "line": "the latched joystick direction"
  },
  "146C": {
   "unit": "game/mainProgram0800",
   "line": "$0F = centred"
  },
  "146E": {
   "unit": "game/mainProgram0800",
   "line": "the stick is being pushed - see which way"
  },
  "1470": {
   "unit": "game/mainProgram0800",
   "line": "edge-detect a fresh fire-button press"
  },
  "1473": {
   "unit": "game/mainProgram0800",
   "line": "no press - go round again"
  },
  "1475": {
   "unit": "game/mainProgram0800",
   "block": [
    "Fire on a tab: light that tab and call its handler through tabActionTable.  The handler address is",
    "copied into the operand of the JSR at $148D, which is why the disassembly shows 'jsr $FFFF' there."
   ],
   "line": "the selected tab"
  },
  "1478": {
   "unit": "game/mainProgram0800",
   "line": "show it in its bright colour while its action runs"
  },
  "147B": {
   "unit": "game/mainProgram0800",
   "line": "tab index ..."
  },
  "147E": {
   "unit": "game/mainProgram0800",
   "line": "... times two ..."
  },
  "147F": {
   "unit": "game/mainProgram0800",
   "line": "... is the offset into the table of 16-bit handler addresses"
  },
  "1480": {
   "unit": "game/mainProgram0800",
   "line": "handler low byte ..."
  },
  "1483": {
   "unit": "game/mainProgram0800",
   "line": "... self-modifying: into the operand of the JSR at $148D"
  },
  "1486": {
   "unit": "game/mainProgram0800",
   "line": "next table byte"
  },
  "1487": {
   "unit": "game/mainProgram0800",
   "line": "handler high byte ..."
  },
  "148A": {
   "unit": "game/mainProgram0800",
   "line": "... into the high half of that operand"
  },
  "148D": {
   "unit": "game/mainProgram0800",
   "line": "call the tab's fire handler (operand patched just above; tab 0 points at the RTS at $158E)"
  },
  "1490": {
   "unit": "game/mainProgram0800",
   "line": "back to the top of the loop"
  },
  "1493": {
   "unit": "game/mainProgram0800",
   "block": [
    "Left/right tab movement.  The direction bits are active low, so after masking bits 2 and 3, $04",
    "means right is pressed and $08 means left; $0C means neither."
   ],
   "line": "keep the left (bit 2) and right (bit 3) switches"
  },
  "1495": {
   "unit": "game/mainProgram0800",
   "line": "$0C = neither of them pressed"
  },
  "1497": {
   "unit": "game/mainProgram0800",
   "line": "up or down only - ignore it"
  },
  "1499": {
   "unit": "game/mainProgram0800",
   "line": "$14 = 20 frames ..."
  },
  "149B": {
   "unit": "game/mainProgram0800",
   "line": "... of auto-repeat delay before the selection may move again"
  },
  "149D": {
   "unit": "game/mainProgram0800",
   "line": "remember the direction"
  },
  "149E": {
   "unit": "game/mainProgram0800",
   "line": "the tab we are leaving ..."
  },
  "14A1": {
   "unit": "game/mainProgram0800",
   "line": "... goes back to its normal colour"
  },
  "14A4": {
   "unit": "game/mainProgram0800",
   "line": "get the direction back"
  },
  "14A5": {
   "unit": "game/mainProgram0800",
   "line": "$04 = right pressed (bit 3 low)"
  },
  "14A7": {
   "unit": "game/mainProgram0800",
   "line": "not right, so it must be left"
  },
  "14A9": {
   "unit": "game/mainProgram0800",
   "line": "the selected tab"
  },
  "14AC": {
   "unit": "game/mainProgram0800",
   "line": "2 = MISC, the rightmost tab"
  },
  "14AE": {
   "unit": "game/mainProgram0800",
   "line": "already at the right edge - ignore"
  },
  "14B0": {
   "unit": "game/mainProgram0800",
   "line": "move one tab to the right"
  },
  "14B3": {
   "unit": "game/mainProgram0800",
   "line": "always taken"
  },
  "14B5": {
   "unit": "game/mainProgram0800",
   "line": "the selected tab"
  },
  "14B8": {
   "unit": "game/mainProgram0800",
   "line": "already at STATS, the left edge - ignore"
  },
  "14BA": {
   "unit": "game/mainProgram0800",
   "line": "move one tab to the left"
  },
  "14BD": {
   "unit": "game/mainProgram0800",
   "line": "sound 2 = the selection click"
  },
  "14BF": {
   "unit": "game/mainProgram0800",
   "line": "play it"
  },
  "14C2": {
   "unit": "game/mainProgram0800",
   "line": "the newly selected tab ..."
  },
  "14C5": {
   "unit": "game/mainProgram0800",
   "line": "... gets its bright colour"
  },
  "14C8": {
   "unit": "game/mainProgram0800",
   "line": "blank the panel under the tab bar ready for the new contents"
  },
  "14CB": {
   "unit": "game/mainProgram0800",
   "block": [
    "Draw the panel of the currently selected tab.  Entered directly by both console entry points as",
    "well as after a tab change."
   ],
   "line": "which tab?"
  },
  "14CE": {
   "unit": "game/mainProgram0800",
   "line": "not tab 0"
  },
  "14D0": {
   "unit": "game/mainProgram0800",
   "line": "STATS: the game type, rules, play order, owner names and map id"
  },
  "14D3": {
   "unit": "game/mainProgram0800",
   "line": "plus the unit counts, drones, missiles, comspeed and terrain points (in overlay B)"
  },
  "14D6": {
   "unit": "game/mainProgram0800",
   "line": "back to the loop"
  },
  "14D9": {
   "unit": "game/mainProgram0800",
   "line": "tab 1?"
  },
  "14DB": {
   "unit": "game/mainProgram0800",
   "line": "no - it must be MISC"
  },
  "14DD": {
   "unit": "game/mainProgram0800",
   "line": "REPAIR: the comcen schematic and its four system markers"
  },
  "14E0": {
   "unit": "game/mainProgram0800",
   "line": "remember the comcen status that is now on screen"
  },
  "14E3": {
   "unit": "game/mainProgram0800",
   "line": "back to the loop"
  },
  "14E6": {
   "unit": "game/mainProgram0800",
   "line": "tab 2?"
  },
  "14E8": {
   "unit": "game/mainProgram0800",
   "line": "menuTabIndex is only ever 0, 1 or 2 (every writer is in this loop), so this branch is never taken"
  },
  "14EA": {
   "unit": "game/mainProgram0800",
   "line": "MISC: print the option list (ABORT GAME, VOICE PAUSE, EXIT ...)"
  },
  "14ED": {
   "unit": "game/mainProgram0800",
   "line": "back to the loop"
  },
  "14F0": {
   "unit": "game/mainProgram0800",
   "routine": [
    "snapshotComcenStatus - Remembers what the REPAIR tab is currently showing: copies the three comcen",
    "system status bytes ($9242-$9244) into the snapshot at $92D7-$92D9 and the comcen's energy (the low",
    "6 bits of its entry in unitEnergyTable) into $92D6.  selectDefaultTab compares the live values",
    "against this snapshot to decide whether the player still has repairs to look at.",
    "In:  comcenSystemStatus ($9241..), comcenUnit ($9236), unitEnergyTable ($F9C4)",
    "Out: comcenStatusSnapshot ($92D6-$92D9)",
    "Called from: mainMenuLoop ($14E0) and the end of repairTabAction ($187E)"
   ],
   "line": "index 3 down to 1: the battle, radar and drone system bytes"
  },
  "14F2": {
   "unit": "game/mainProgram0800",
   "line": "system status byte $9241+X"
  },
  "14F5": {
   "unit": "game/mainProgram0800",
   "line": "into the snapshot at $92D6+X"
  },
  "14F8": {
   "unit": "game/mainProgram0800",
   "line": "next system"
  },
  "14F9": {
   "unit": "game/mainProgram0800",
   "line": "stop at index 1 - slot 0 is filled from the energy below"
  },
  "14FB": {
   "unit": "game/mainProgram0800",
   "line": "this side's comcen (unit 49 or 99)"
  },
  "14FE": {
   "unit": "game/mainProgram0800",
   "line": "its energy byte"
  },
  "1501": {
   "unit": "game/mainProgram0800",
   "line": "the low 6 bits are the energy 0-50 (the top bits are flags)"
  },
  "1503": {
   "unit": "game/mainProgram0800",
   "line": "snapshot slot 0 = the comcen energy"
  },
  "1506": {
   "unit": "game/mainProgram0800",
   "line": "done"
  },
  "1507": {
   "unit": "game/mainProgram0800",
   "routine": [
    "selectDefaultTab - Chooses which console tab should be showing.  If the game is over or has not",
    "started (gameEndReason non-zero) it opens MISC (2), where ABORT/EXIT live.  Otherwise it opens",
    "REPAIR (1) when the comcen is stunned or when anything on the comcen has changed since the last",
    "snapshotComcenStatus, and MISC (2) when nothing needs attention.  STATS (0) is never chosen here.",
    "In:  gameEndReason ($0BA8), comcenStunStatus ($922A), comcenSystemStatus, comcenStatusSnapshot, comcenUnit, unitEnergyTable",
    "Out: menuTabIndex ($90F9)",
    "Called from: resumeMainMenuScreen ($1414) and enterMainMenu ($142D, $1433)"
   ],
   "line": "assume tab 2 = MISC"
  },
  "1509": {
   "unit": "game/mainProgram0800",
   "line": "0 while a game is actually running"
  },
  "150C": {
   "unit": "game/mainProgram0800",
   "line": "no game in progress - MISC it is"
  },
  "150E": {
   "unit": "game/mainProgram0800",
   "line": "a game is running: assume tab 0 = STATS for now"
  },
  "1510": {
   "unit": "game/mainProgram0800",
   "line": "is the comcen stunned?"
  },
  "1513": {
   "unit": "game/mainProgram0800",
   "line": "yes - the player needs the REPAIR tab"
  },
  "1515": {
   "unit": "game/mainProgram0800",
   "line": "compare the three system bytes with the snapshot, index 3 down to 1"
  },
  "1517": {
   "unit": "game/mainProgram0800",
   "line": "live system status $9241+X ..."
  },
  "151A": {
   "unit": "game/mainProgram0800",
   "line": "... against the snapshot $92D6+X"
  },
  "151D": {
   "unit": "game/mainProgram0800",
   "line": "something changed - show REPAIR"
  },
  "151F": {
   "unit": "game/mainProgram0800",
   "line": "next system"
  },
  "1520": {
   "unit": "game/mainProgram0800",
   "line": "stop at index 1"
  },
  "1522": {
   "unit": "game/mainProgram0800",
   "line": "this side's comcen"
  },
  "1525": {
   "unit": "game/mainProgram0800",
   "line": "its energy byte"
  },
  "1528": {
   "unit": "game/mainProgram0800",
   "line": "keep the 0-50 energy value"
  },
  "152A": {
   "unit": "game/mainProgram0800",
   "line": "has the comcen lost energy since the snapshot?"
  },
  "152D": {
   "unit": "game/mainProgram0800",
   "line": "unchanged - nothing to repair, leave Y at 2 = MISC"
  },
  "152F": {
   "unit": "game/mainProgram0800",
   "line": "something needs attention: tab 1 = REPAIR"
  },
  "1531": {
   "unit": "game/mainProgram0800",
   "line": "store the chosen tab"
  },
  "1534": {
   "unit": "game/mainProgram0800",
   "line": "done"
  },
  "1535": {
   "unit": "game/mainProgram0800",
   "block": [
    "tabActionTable: the three fire-button handlers of the console tabs, one 16-bit little-endian",
    "address each, indexed by menuTabIndex*2 and copied into the JSR operand at $148D.",
    "  +0 tab 0 STATS   -> $158E, the RTS at the end of setMenuWindowBounds, i.e. no action at all",
    "  +2 tab 1 REPAIR  -> repairTabAction ($17DA), the 'repair which system' menu",
    "  +4 tab 2 MISC    -> miscTabAction ($1A49), the ABORT GAME / VOICE PAUSE / EXIT option list"
   ],
   "line": "three handler addresses, one per console tab"
  },
  "153B": {
   "unit": "game/mainProgram0800",
   "routine": [
    "showMainMenu - Builds the console screen from scratch: resets the glyph plotter's parameter block,",
    "sets the background colour, draws the STATS/REPAIR/MISC tab bar and the two nested frames, and ends",
    "by repainting the status message line.",
    "In:  none",
    "Out: the whole screen 3 drawn; textDrawParams, vicBg0Shadow ($9004), cellBackgroundColour ($9226), screenBuiltFlag (zp_41)",
    "Called from: returnToMainMenu ($0AC2) and enterMainMenu ($1430)"
   ],
   "line": "0 into the four glyph-plotter parameters below"
  },
  "153D": {
   "unit": "game/mainProgram0800",
   "line": "useFixedColourFlag = 0: take the colour from the text state, not a fixed byte"
  },
  "1540": {
   "unit": "game/mainProgram0800",
   "line": "drawColourByte = 0"
  },
  "1543": {
   "unit": "game/mainProgram0800",
   "line": "blockFillFlag = 0"
  },
  "1546": {
   "unit": "game/mainProgram0800",
   "line": "blockFillChar = 0"
  },
  "1549": {
   "unit": "game/mainProgram0800",
   "line": "colour $0C = medium grey ..."
  },
  "154B": {
   "unit": "game/mainProgram0800",
   "line": "... as the screen background (the IRQ copies the shadow into $D021)"
  },
  "154E": {
   "unit": "game/mainProgram0800",
   "line": "0 ..."
  },
  "1550": {
   "unit": "game/mainProgram0800",
   "line": "... = the cell background colour used by the tile drawing"
  },
  "1553": {
   "unit": "game/mainProgram0800",
   "line": "1 ..."
  },
  "1555": {
   "unit": "game/mainProgram0800",
   "line": "... marks 'a screen has been built' (written by every screen builder, read nowhere)"
  },
  "1557": {
   "unit": "game/mainProgram0800",
   "line": "0 ..."
  },
  "1559": {
   "unit": "game/mainProgram0800",
   "line": "... into blockFillFlag again - redundant, it was already cleared at $1543"
  },
  "155C": {
   "unit": "game/mainProgram0800",
   "line": "print STATS, REPAIR and MISC and colour their three tab boxes"
  },
  "155F": {
   "unit": "game/mainProgram0800",
   "line": "draw the outer and inner frames"
  },
  "1562": {
   "unit": "game/mainProgram0800",
   "line": "repaint whatever message belongs on the status line and return to our caller"
  },
  "1565": {
   "unit": "game/mainProgram0800",
   "routine": [
    "drawMenuFrame - Draws the console's two nested frames from the predefined window table: window $18",
    "is the outer 40x20 box starting at row 5, window $19 the inner 38x18 box at row 6.  Falls into",
    "clearMenuTextWindow.",
    "In:  none",
    "Out: the frames drawn, then the text window set and cleared",
    "Called from: showMainMenu ($155F)"
   ],
   "line": "window $18 = column 0, row 5, 40x20 cells - the outer frame"
  },
  "1567": {
   "unit": "game/mainProgram0800",
   "line": "draw it from its 9-tile frame set"
  },
  "156A": {
   "unit": "game/mainProgram0800",
   "line": "window $19 = column 1, row 6, 38x18 cells - the inner frame"
  },
  "156C": {
   "unit": "game/mainProgram0800",
   "line": "draw it too"
  },
  "156F": {
   "unit": "game/mainProgram0800",
   "routine": [
    "clearMenuTextWindow - Sets the console panel's text colour and window bounds and blanks the window.",
    "In:  comcenStunStatus ($922A), menuTabIndex ($90F9)",
    "Out: textColour (zp_3D), the window bounds zp_39-zp_40, the panel area cleared",
    "Called from: resumeMainMenuScreen ($1417), mainMenuLoop ($14C8), $1ACA, $1AD6 and overlay A ($7EA0)"
   ],
   "line": "colour and window bounds for the panel"
  },
  "1572": {
   "unit": "game/mainProgram0800",
   "line": "blank it and return to our caller"
  },
  "1575": {
   "unit": "game/mainProgram0800",
   "routine": [
    "setMenuTextWindow - Picks the console text colour from the comcen stun state (red on the REPAIR tab",
    "while the comcen is stunned) and falls into setMenuWindowBounds.",
    "In:  comcenStunStatus ($922A), menuTabIndex ($90F9)",
    "Out: textColour (zp_3D) plus the window bounds",
    "Called from: clearMenuTextWindow ($156F), $1866 and $1A80"
   ],
   "line": "$61 normally, $21 on a stunned REPAIR tab"
  },
  "1578": {
   "unit": "game/mainProgram0800",
   "routine": [
    "setMenuWindowBounds - Sets the text window that the console panels print into: rows 7 to 21, left",
    "column 2, 36 characters wide, cursor homed at the top left of the window.  Its final RTS at $158E",
    "doubles as the do-nothing handler of the STATS tab in tabActionTable.",
    "In:  none",
    "Out: textCursorCol=0, textWindowTop=7, textCursorRow=7, textWindowLeft=2, textWindowBottom=$16, textWindowWidth=$24",
    "Called from: setMenuTextWindow by fall-through, clearMenuWindowPlain ($1593) and $19E7"
   ],
   "line": "cursor column 0 ..."
  },
  "157A": {
   "unit": "game/mainProgram0800",
   "line": "... relative to the window's left edge"
  },
  "157C": {
   "unit": "game/mainProgram0800",
   "line": "row 7 = the first line under the tab bar and frame ..."
  },
  "157E": {
   "unit": "game/mainProgram0800",
   "line": "... as the top of the window ..."
  },
  "1580": {
   "unit": "game/mainProgram0800",
   "line": "... and as the starting cursor row"
  },
  "1582": {
   "unit": "game/mainProgram0800",
   "line": "screen column 2 ..."
  },
  "1584": {
   "unit": "game/mainProgram0800",
   "line": "... is the window's left edge"
  },
  "1586": {
   "unit": "game/mainProgram0800",
   "line": "$16 = row 22 is the exclusive bottom limit ..."
  },
  "1588": {
   "unit": "game/mainProgram0800",
   "line": "... so the window covers rows 7-21"
  },
  "158A": {
   "unit": "game/mainProgram0800",
   "line": "$24 = 36 characters ..."
  },
  "158C": {
   "unit": "game/mainProgram0800",
   "line": "... is the window width and the wrap point"
  },
  "158E": {
   "unit": "game/mainProgram0800",
   "line": "also the STATS tab's 'do nothing' fire handler (tabActionTable entry 0)"
  }
 },
 "dataTypes": {
  "0F81": [
   "text",
   16
  ],
  "0F91": [
   "text",
   17
  ],
  "0FA2": [
   "text",
   11
  ],
  "0FAD": [
   "text",
   4
  ],
  "10EF": [
   "byte",
   2
  ],
  "1299": [
   "byte",
   3
  ],
  "131A": [
   "byte",
   1
  ],
  "1412": [
   "byte",
   2
  ],
  "1535": [
   "addr",
   6
  ]
 },
 "zpComments": {}
}