{
 "chunk": "game_mainProgram0800_0800",
 "unit": "game/mainProgram0800",
 "unitLabels": {
  "game/mainProgram0800": {
   "0A47": "msgIdentifyMapLeadSpace",
   "0C8A": "comcenUnitIndexBySide"
  }
 },
 "dataTypes": {
  "0803": [
   "byte",
   1
  ],
  "08E8": [
   "byte",
   1
  ],
  "08E9": [
   "byte",
   2
  ],
  "0993": [
   "text",
   15
  ],
  "09A2": [
   "byte",
   1
  ],
  "09A3": [
   "byte",
   2
  ],
  "09A5": [
   "byte",
   1
  ],
  "0A47": [
   "byte",
   1
  ],
  "0A48": [
   "text",
   36
  ],
  "0C8A": [
   "byte",
   2
  ],
  "0CE4": [
   "text",
   4
  ],
  "0D67": [
   "grid",
   64,
   8
  ],
  "0DA7": [
   "grid",
   16,
   8
  ]
 },
 "zpComments": {
  "66": "last key delivered by pollKeyboardEvent: $FF = none, 0-3 = F1/F3/F5/F7 = the screen the player asked for (the only values switchToRequestedScreen acts on), any other value = the PETSCII|$80 code of the key just typed, which reads as 'no screen change wanted' and so cancels a stale F-key request",
  "6C": "joystick auto-repeat countdown armed by setJoystickRepeatDelay ($0D49): 16 frames before the first repeat after the stick leaves centre, 6 frames between repeats afterwards",
  "79": "fire-button edge latch: set when checkFirePressed/isFireHeld see the button released, cleared when a press is accepted, so one press produces exactly one click",
  "80": "never written: BIT $80 is used purely as a 3-cycle delay in the fast-loader receive loops ($096F here, $C10E in the boot loader)"
 },
 "notes": {
  "0800": {
   "routine": [
    "gameEntry - the game's front door.  The boot loader does not return to its caller: it rebuilt the stack so that its final RTS lands in verifyProgramChecksum ($0461), and that routine ends with JMP $0800.  This is also where the setup block jumps back to after a reset of the game state.",
    "In:  nothing; $01 = $35 (I/O visible, no ROMs), interrupts pointing at the RTI in the boot loader.",
    "Out: never returns - control passes to mainStart.",
    "Called from: boot/eaBootFile $02D5, and game/scratchAndMessages0400 $04AB (the tail of verifyProgramChecksum)."
   ],
   "line": "the three bytes at $0800 are the published entry point, so the real start-up code can live anywhere",
   "unit": "game/mainProgram0800"
  },
  "0803": {
   "block": [
    "loaderStatus - sticky error flag of the fast loader.  Bit 7 means 'the DISK ERROR message is currently on",
    "the status line'; the low bits keep the drive's error code from that failure ($02 header not found, $03 no",
    "sync, $05 checksum, $08 write protected / wrong disk id).  Only loadSectors reads or writes it."
   ],
   "line": "$00 = no error outstanding",
   "unit": "game/mainProgram0800"
  },
  "0804": {
   "routine": [
    "loadSectors - the game's own copy of the fast loader.  Reads A consecutive sectors of track Y, starting at sector X, into the address last given to setLoadDest, one sector per page.  Every overlay, the battlefield map, the setup block and the trainer's playbook arrive through here.  A drive error is not fatal: 'DISK ERROR.' goes up on the status line and the same sector is retried forever, so the player can close the drive door or put the right disk back in and the load simply continues.",
    "In:  A = number of sectors, Y = track, X = first sector, loadDestLo/loadDestHi = destination page.",
    "Out: the sectors in memory, loadDestHi advanced one page per sector, C=0, the caller's I flag restored; ldTrack/ldSector/ldCount clobbered.",
    "Called from: $0B58, $0F10, $0F38, $0F41, $0F5C in this file, game/mapGenerator6F00 $7B2D and game/trainerAiE000 $E0B4/$E0D6/$ECB3."
   ],
   "line": "remember the caller's interrupt flag - the transfer itself is cycle counted",
   "unit": "game/mainProgram0800"
  },
  "0805": {
   "line": "no interrupts from here to the RTS: a raster IRQ in the middle of a sector would lose bytes",
   "unit": "game/mainProgram0800"
  },
  "0806": {
   "line": "sectors still to transfer",
   "unit": "game/mainProgram0800"
  },
  "0808": {
   "block": [
    "One pass per sector: ask the drive for it, retry until it answers 'status 0', then move the destination on",
    "one page and step to the next sector of the same track."
   ],
   "line": "fetch one sector into (loadDestLo/Hi); returns C=1 and the drive's error code in A on failure",
   "unit": "game/mainProgram0800"
  },
  "080B": {
   "line": "branch when the drive reported no error",
   "unit": "game/mainProgram0800"
  },
  "080D": {
   "line": "keep the error code but set bit 7 = 'the error message is on the screen'",
   "unit": "game/mainProgram0800"
  },
  "080F": {
   "line": "remember that so the message can be taken down after a successful retry",
   "unit": "game/mainProgram0800"
  },
  "0812": {
   "line": "low byte of msgDiskError ($0FA2 = 'DISK ERROR.')",
   "unit": "game/mainProgram0800"
  },
  "0814": {
   "line": "high byte of msgDiskError",
   "unit": "game/mainProgram0800"
  },
  "0816": {
   "line": "print it centred on the status line (showMessageString $C3D8)",
   "unit": "game/mainProgram0800"
  },
  "0819": {
   "line": "reload the track of the sector that failed...",
   "unit": "game/mainProgram0800"
  },
  "081B": {
   "line": "...and its sector number",
   "unit": "game/mainProgram0800"
  },
  "081D": {
   "line": "retry the same sector forever - the loader never gives up",
   "unit": "game/mainProgram0800"
  },
  "0820": {
   "line": "did any earlier attempt put DISK ERROR on the screen?",
   "unit": "game/mainProgram0800",
   "block": [
    "A sector came in cleanly.  If an earlier attempt had put DISK ERROR on the status line, take it down",
    "again and put the message the game wants there back."
   ]
  },
  "0823": {
   "line": "no message to take down - go straight on",
   "unit": "game/mainProgram0800"
  },
  "0825": {
   "line": "clear the 'message showing' bit, leaving the last error code behind for a post mortem",
   "unit": "game/mainProgram0800"
  },
  "0827": {
   "line": "store the cleared flag",
   "unit": "game/mainProgram0800"
  },
  "082A": {
   "line": "persistentMessageId = the message the status line should fall back to",
   "unit": "game/mainProgram0800"
  },
  "082D": {
   "line": "reprint it over the DISK ERROR text (showStatusMessage $C6FF)",
   "unit": "game/mainProgram0800"
  },
  "0830": {
   "line": "next sector of this track",
   "unit": "game/mainProgram0800",
   "block": [
    "Advance to the next sector of the run: one sector forward on the disk, one page forward in memory."
   ]
  },
  "0832": {
   "line": "next destination page - one sector is exactly 256 bytes",
   "unit": "game/mainProgram0800"
  },
  "0835": {
   "line": "sector number for the next request",
   "unit": "game/mainProgram0800"
  },
  "0837": {
   "line": "track number for the next request (never changes inside one call)",
   "unit": "game/mainProgram0800"
  },
  "0839": {
   "line": "one sector fewer to go",
   "unit": "game/mainProgram0800"
  },
  "083B": {
   "line": "loop until the whole run has been read",
   "unit": "game/mainProgram0800"
  },
  "083D": {
   "line": "restore the caller's interrupt flag",
   "unit": "game/mainProgram0800"
  },
  "083E": {
   "line": "C=0: a read can only ever end successfully, it retries instead of failing",
   "unit": "game/mainProgram0800"
  },
  "083F": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0840": {
   "block": [
    "Failure exit of writeSectors: the drive refused a sector (write protected, wrong disk id, or it could not",
    "find the header).  Unlike a read this is not retried - the caller shows a message and gives up."
   ],
   "line": "restore the caller's interrupt flag",
   "unit": "game/mainProgram0800"
  },
  "0841": {
   "line": "C=1 = the save failed",
   "unit": "game/mainProgram0800"
  },
  "0842": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0843": {
   "routine": [
    "writeSectors - the write half of the fast loader: sends A consecutive sectors from the address set by setLoadDest to track Y starting at sector X, one page per sector.  The drive encrypts each block with the same rolling XOR that a read decrypts with (cryptSectorBuffer $0531 in the drive code).  This is the only path that ever sends command $60, and it is used solely to save a game film and the setup block to the save slots on track 18.",
    "In:  A = number of sectors, Y = track, X = first sector, loadDestLo/loadDestHi = source page.",
    "Out: C=0 when every sector went out, C=1 on the first refusal; loadDestHi advanced.",
    "Called from: game/mapGenerator6F00 $7E85 (save game film) and $8194 (save setup)."
   ],
   "line": "remember the caller's interrupt flag",
   "unit": "game/mainProgram0800"
  },
  "0844": {
   "line": "interrupts off for the whole transfer",
   "unit": "game/mainProgram0800"
  },
  "0845": {
   "line": "sectors still to write",
   "unit": "game/mainProgram0800"
  },
  "0847": {
   "line": "write one sector; C=1 if the drive refused it",
   "unit": "game/mainProgram0800"
  },
  "084A": {
   "line": "give up on the first error instead of retrying",
   "unit": "game/mainProgram0800"
  },
  "084C": {
   "line": "next sector of the track",
   "unit": "game/mainProgram0800"
  },
  "084E": {
   "line": "next source page",
   "unit": "game/mainProgram0800"
  },
  "0851": {
   "line": "sector number for the next request",
   "unit": "game/mainProgram0800"
  },
  "0853": {
   "line": "track number for the next request",
   "unit": "game/mainProgram0800"
  },
  "0855": {
   "line": "one sector fewer to go",
   "unit": "game/mainProgram0800"
  },
  "0857": {
   "line": "loop until the whole run has been written",
   "unit": "game/mainProgram0800"
  },
  "0859": {
   "line": "restore the caller's interrupt flag",
   "unit": "game/mainProgram0800"
  },
  "085A": {
   "line": "C=0 = everything was written",
   "unit": "game/mainProgram0800"
  },
  "085B": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "085C": {
   "routine": [
    "setLoadDest - names the memory the next transfer works on.  It does not use a variable: it patches the operand bytes loadDestLo ($09A2) and loadDestHi ($09A3), which readSectorBody and writeOneSector then copy into the zero-page pointers the byte loops actually use.  Only the page matters in practice, since every transfer is a whole number of 256-byte sectors.",
    "In:  X = address low byte, Y = address high byte.",
    "Out: loadDestLo/loadDestHi patched.",
    "Called from: every overlay load in this file ($0B4F, $0F07, $0F1A, $0FC8, $104B, $1064, $1074, $108D, $10AF, $10BF), game/mapGenerator6F00 ($7B24, $7E7D, $7E8F, $7F7B, $818B) and the trainer ($E12C, $ECAA)."
   ],
   "line": "low byte of the destination (or source) address",
   "unit": "game/mainProgram0800"
  },
  "085F": {
   "line": "high byte; loadSectors/writeSectors increment this one as they advance page by page",
   "unit": "game/mainProgram0800"
  },
  "0862": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0863": {
   "routine": [
    "writeOneSector - writes a single sector: command $60, then the track and the sector number, then the 256 bytes. CIA1 timer A is stopped across the transfer, exactly as on the read path, and started again afterwards - on this side it is only belt and braces, since the CIA interrupts were masked at start-up.",
    "In:  Y = track, X = sector, loadDestLo/loadDestHi = the page to write.",
    "Out: C=1 when the drive's status byte is non-zero (through finishTransfer); ldTrack/ldSector set; wrSrcPtr points at the source page.",
    "Called from: writeSectors $0847."
   ],
   "line": "no interrupts - the handshake with the drive must not be broken",
   "unit": "game/mainProgram0800"
  },
  "0864": {
   "line": "keep the track where the caller's loop can find it again",
   "unit": "game/mainProgram0800"
  },
  "0866": {
   "line": "same for the sector number",
   "unit": "game/mainProgram0800"
  },
  "0868": {
   "line": "the page setLoadDest patched in...",
   "unit": "game/mainProgram0800"
  },
  "086B": {
   "line": "...becomes the low byte of the zero-page source pointer",
   "unit": "game/mainProgram0800"
  },
  "086D": {
   "line": "high byte of the source page",
   "unit": "game/mainProgram0800"
  },
  "0870": {
   "line": "sendSectorData reads the 256 bytes through (wrSrcPtr),y",
   "unit": "game/mainProgram0800"
  },
  "0872": {
   "line": "$60 = the drive's 'write a sector' command (handleWriteCommand $0490 in the drive code)",
   "unit": "game/mainProgram0800"
  },
  "0874": {
   "line": "send the command byte",
   "unit": "game/mainProgram0800"
  },
  "0877": {
   "line": "the track to write to",
   "unit": "game/mainProgram0800"
  },
  "0879": {
   "line": "send it",
   "unit": "game/mainProgram0800"
  },
  "087C": {
   "line": "the sector to write to",
   "unit": "game/mainProgram0800"
  },
  "087E": {
   "line": "send it",
   "unit": "game/mainProgram0800"
  },
  "0881": {
   "line": "CRA bit 0 = 0...",
   "unit": "game/mainProgram0800"
  },
  "0883": {
   "line": "...stops CIA1 timer A, the 60 Hz tick the KERNAL left running, so its underflow cannot disturb the exchange",
   "unit": "game/mainProgram0800"
  },
  "0886": {
   "line": "hand over the 256 bytes and collect the status byte",
   "unit": "game/mainProgram0800"
  },
  "0889": {
   "line": "CRA = $01: timer A on, continuous mode...",
   "unit": "game/mainProgram0800"
  },
  "088B": {
   "line": "...restart it now that the drive is done",
   "unit": "game/mainProgram0800"
  },
  "088E": {
   "line": "C still holds finishTransfer's verdict",
   "unit": "game/mainProgram0800"
  },
  "088F": {
   "routine": [
    "sendSectorData - pushes the 256 bytes at (wrSrcPtr) to the drive one handshaked byte at a time, then falls into finishTransfer to read back the status byte.  The drive receives them the slow way too (writeSectorToDisk $04A0), so no cycle counting is needed on this side.",
    "In:  wrSrcPtr/wrSrcPtrHi = the page to send.",
    "Out: 256 bytes sent; C=1 if the drive's status byte is non-zero.",
    "Called from: writeOneSector $0886."
   ],
   "line": "start at the first byte of the page",
   "unit": "game/mainProgram0800"
  },
  "0891": {
   "line": "next byte of the sector",
   "unit": "game/mainProgram0800"
  },
  "0893": {
   "line": "clock it out (the byte coming back is ignored)",
   "unit": "game/mainProgram0800"
  },
  "0896": {
   "line": "advance through the page",
   "unit": "game/mainProgram0800"
  },
  "0897": {
   "line": "loop until Y wraps, i.e. all 256 bytes are gone",
   "unit": "game/mainProgram0800"
  },
  "0899": {
   "line": "read the drive's verdict and return it in C",
   "unit": "game/mainProgram0800"
  },
  "089C": {
   "routine": [
    "sendByteToDrive - the slow, fully handshaked, full-duplex byte exchange with the drive; the other end is exchangeByteWithHost ($034B) in the drive code.  Eight times over it samples the drive's bit off DATA IN, pulls CLK to acknowledge, puts its own bit on DATA OUT, releases CLK and waits for the drive to pull CLK back - one bit each way per handshake, so neither side has to be cycle accurate.  Every command, track and sector byte goes out this way, and the status byte is collected by exchanging a dummy byte.",
    "Both shift registers work LSB first (LSR sendByteShift / ROR recvByteShift); the phase-1 note saying 'MSB first' was wrong.",
    "In:  A = the byte to send.",
    "Out: A = the byte received from the drive, C=0, X=0, sendByteShift = 0, recvByteShift = the received byte.",
    "Called from: writeOneSector ($0874/$0879/$087E), sendSectorData ($0893), readSectorBody ($0900/$0905/$090A) and finishTransfer ($0949); also from the modem driver and the trainer at $EA1D/$EA29."
   ],
   "block": [
    "CIA2 port A ($DD00) is the serial bus: bit 4 = CLK OUT, bit 5 = DATA OUT, bit 6 = CLK IN, bit 7 = DATA IN,",
    "and bits 0-1 are the VIC bank select, which must survive every write (the game runs in bank 2, so they read",
    "%01).  The C64's outputs are inverted by the bus drivers - writing a 1 pulls the line low - while its inputs",
    "read the true line level, so a line that somebody is pulling reads back as 0.  In the drive it is the other",
    "way round, which is why the two sides look mirrored."
   ],
   "line": "the byte to send goes into the output shift register",
   "unit": "game/mainProgram0800"
  },
  "089F": {
   "line": "eight bits, one per pass of the loop",
   "unit": "game/mainProgram0800"
  },
  "08A1": {
   "line": "poll the serial port; V = bit 6 = CLK IN",
   "unit": "game/mainProgram0800"
  },
  "08A4": {
   "line": "wait until the drive releases CLK - by then its bit is sitting on DATA",
   "unit": "game/mainProgram0800"
  },
  "08A6": {
   "line": "read the port so the VIC bank bits can be written back unchanged",
   "unit": "game/mainProgram0800"
  },
  "08A9": {
   "line": "clear bit 5: release our DATA OUT so the line shows only what the drive drives",
   "unit": "game/mainProgram0800"
  },
  "08AB": {
   "line": "let go of DATA",
   "unit": "game/mainProgram0800"
  },
  "08AE": {
   "line": "sample the port with the drive's bit on DATA IN",
   "unit": "game/mainProgram0800"
  },
  "08B1": {
   "line": "C = bit 7 = the drive's data bit (line high = a 1 bit)",
   "unit": "game/mainProgram0800"
  },
  "08B3": {
   "line": "shift it in from the top; after eight rounds the first bit has reached bit 0, i.e. LSB first",
   "unit": "game/mainProgram0800"
  },
  "08B6": {
   "line": "set bit 4 = pull CLK low: 'I have taken your bit'",
   "unit": "game/mainProgram0800"
  },
  "08B8": {
   "line": "assert CLK",
   "unit": "game/mainProgram0800"
  },
  "08BB": {
   "line": "clear bit 6 in the copy: that is what the port reads back now that CLK is low",
   "unit": "game/mainProgram0800"
  },
  "08BD": {
   "line": "compare the live port against that snapshot",
   "unit": "game/mainProgram0800"
  },
  "08C0": {
   "line": "wait until it differs - the drive toggles DATA as its acknowledgement ($0364 in the drive code)",
   "unit": "game/mainProgram0800"
  },
  "08C2": {
   "line": "shift the next outgoing bit (LSB first) into the carry",
   "unit": "game/mainProgram0800"
  },
  "08C5": {
   "line": "a 0 bit: leave DATA OUT clear so the line stays high",
   "unit": "game/mainProgram0800"
  },
  "08C7": {
   "line": "branch when the bit to send is 0",
   "unit": "game/mainProgram0800"
  },
  "08C9": {
   "line": "a 1 bit: bit 5 = DATA OUT, which pulls the line low; the drive reads its inputs inverted and sees a 1",
   "unit": "game/mainProgram0800"
  },
  "08CB": {
   "line": "merge with the live port so the bank bits and CLK OUT survive",
   "unit": "game/mainProgram0800"
  },
  "08CE": {
   "line": "put the bit on the DATA line",
   "unit": "game/mainProgram0800"
  },
  "08D1": {
   "line": "clear bit 4 = release CLK: 'the bit is on the line, take it'",
   "unit": "game/mainProgram0800"
  },
  "08D3": {
   "line": "release CLK",
   "unit": "game/mainProgram0800"
  },
  "08D6": {
   "line": "poll the port again; V = CLK IN",
   "unit": "game/mainProgram0800"
  },
  "08D9": {
   "line": "wait until the drive pulls CLK low as its acknowledgement ($037F in the drive code)",
   "unit": "game/mainProgram0800"
  },
  "08DB": {
   "line": "toggle our own DATA OUT bit - the handshake the drive waits for at $0389",
   "unit": "game/mainProgram0800"
  },
  "08DD": {
   "line": "write the toggled value",
   "unit": "game/mainProgram0800"
  },
  "08E0": {
   "line": "one bit done",
   "unit": "game/mainProgram0800"
  },
  "08E1": {
   "line": "round again until all eight bits have been exchanged",
   "unit": "game/mainProgram0800"
  },
  "08E3": {
   "line": "return whatever the drive sent back (the status byte, when that is what we came for)",
   "unit": "game/mainProgram0800"
  },
  "08E6": {
   "line": "C=0 - readSectorBody depends on this: its ADC #$2F must add exactly $2F",
   "unit": "game/mainProgram0800"
  },
  "08E7": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "08E8": {
   "block": [
    "sendByteShift - the outgoing shift register of sendByteToDrive; LSR pushes bit 0 out first, so it holds 0",
    "when the byte has gone."
   ],
   "line": "outgoing bit buffer",
   "unit": "game/mainProgram0800"
  },
  "08E9": {
   "block": [
    "recvByteShift - the incoming shift register of sendByteToDrive; ROR feeds bits in at the top, so after eight",
    "of them the first bit received sits in bit 0.  The second byte, $08EA, is never referenced - a pad byte.  The",
    "$BE the register holds in the disk image is just what the assembler left behind."
   ],
   "line": "incoming bit buffer; the $BE it holds in the disk image is just whatever the assembler left behind",
   "unit": "game/mainProgram0800"
  },
  "08EB": {
   "routine": [
    "readOneSector - reads one sector into the load destination.  It stops CIA1 timer A for the duration (the 60 Hz tick the KERNAL left running) so that absolutely nothing can interfere with the cycle-counted receive loop, and restarts it afterwards.",
    "In:  Y = track, X = sector, loadDestLo/loadDestHi = destination page.",
    "Out: 256 bytes in memory, ldTrack/ldSector set, C=1 and A = the error code when the drive reports a problem.",
    "Called from: loadSectors $0808."
   ],
   "line": "interrupts off - from here on the transfer is counted in cycles",
   "unit": "game/mainProgram0800"
  },
  "08EC": {
   "line": "keep the track for the retry logic in loadSectors",
   "unit": "game/mainProgram0800"
  },
  "08EE": {
   "line": "and the sector number",
   "unit": "game/mainProgram0800"
  },
  "08F0": {
   "line": "CRA bit 0 = 0...",
   "unit": "game/mainProgram0800"
  },
  "08F2": {
   "line": "...stop CIA1 timer A while the sector comes in",
   "unit": "game/mainProgram0800"
  },
  "08F5": {
   "line": "do the actual request and receive",
   "unit": "game/mainProgram0800"
  },
  "08F8": {
   "line": "CRA = $01: timer A running, continuous mode...",
   "unit": "game/mainProgram0800"
  },
  "08FA": {
   "line": "...start it again",
   "unit": "game/mainProgram0800"
  },
  "08FD": {
   "line": "C and A carry the drive's verdict back to loadSectors",
   "unit": "game/mainProgram0800"
  },
  "08FE": {
   "routine": [
    "readSectorBody - the body of a sector read.  It sends command $80 with the track and sector, parks the sprites, works out the raster constant the receive loop uses to dodge bad lines, waits for the drive to announce that the sector is decoded, and then runs the fast 2-bits-per-port-read receive loop.",
    "In:  ldTrack, ldSector, loadDestLo/loadDestHi.",
    "Out: 256 bytes at the destination, savedSpriteEnable = the sprite enable register as it was, ldCia2Bits = the VIC bank bits, the operand at $095F patched, C=1 on a drive error.",
    "Called from: readOneSector $08F5."
   ],
   "line": "$80 = the drive's 'read a sector' command (anything that is not $C0 or $60 in commandDispatchLoop $0313)",
   "unit": "game/mainProgram0800"
  },
  "0900": {
   "line": "send the command byte",
   "unit": "game/mainProgram0800"
  },
  "0903": {
   "line": "the track to read",
   "unit": "game/mainProgram0800"
  },
  "0905": {
   "line": "send it",
   "unit": "game/mainProgram0800"
  },
  "0908": {
   "line": "the sector to read",
   "unit": "game/mainProgram0800"
  },
  "090A": {
   "line": "send it",
   "unit": "game/mainProgram0800"
  },
  "090D": {
   "line": "the sprite enable register as the game left it...",
   "unit": "game/mainProgram0800"
  },
  "0910": {
   "line": "...saved so the display can be put back together afterwards",
   "unit": "game/mainProgram0800"
  },
  "0913": {
   "line": "read CIA2 port A...",
   "unit": "game/mainProgram0800"
  },
  "0916": {
   "line": "...and keep only bits 0-1, the VIC bank select (%01 = bank 2, where the bitmap and screen live)",
   "unit": "game/mainProgram0800"
  },
  "0918": {
   "line": "the receive loop rewrites the port from this on every strobe, so the bank must never move",
   "unit": "game/mainProgram0800"
  },
  "091A": {
   "line": "add bit 5 = DATA OUT: pull DATA low, which tells the drive 'not ready for a byte yet'",
   "unit": "game/mainProgram0800"
  },
  "091C": {
   "line": "this also releases CLK OUT and the ATN and RS-232 lines",
   "unit": "game/mainProgram0800"
  },
  "091F": {
   "line": "$D011 holds the vertical fine scroll in bits 0-2...",
   "unit": "game/mainProgram0800"
  },
  "0922": {
   "line": "...isolate it: a bad line happens whenever (raster AND 7) equals that value",
   "unit": "game/mainProgram0800"
  },
  "0924": {
   "line": "$2F = one line before the first display line ($30); C is 0 here because sendByteToDrive ended with CLC",
   "unit": "game/mainProgram0800"
  },
  "0926": {
   "line": "patch the SBC operand of the receive loop at $095F, so the loop can test 'is the next line a bad line?'",
   "unit": "game/mainProgram0800"
  },
  "0929": {
   "line": "poll the serial port; V = CLK IN",
   "unit": "game/mainProgram0800"
  },
  "092C": {
   "line": "wait until the drive pulls CLK low = 'the sector is read, decrypted and ready to go' ($044D in the drive code)",
   "unit": "game/mainProgram0800"
  },
  "092E": {
   "line": "no sprites at all...",
   "unit": "game/mainProgram0800"
  },
  "0930": {
   "line": "...so the VIC cannot steal cycles with sprite DMA while the sector comes in",
   "unit": "game/mainProgram0800"
  },
  "0933": {
   "line": "copy the patched destination address...",
   "unit": "game/mainProgram0800"
  },
  "0936": {
   "line": "...into the zero-page pointer the receive loop stores through",
   "unit": "game/mainProgram0800"
  },
  "0938": {
   "line": "high byte of the destination page",
   "unit": "game/mainProgram0800"
  },
  "093B": {
   "line": "(ldDestPtr),y then walks the whole 256-byte sector",
   "unit": "game/mainProgram0800"
  },
  "093D": {
   "line": "take the 256 bytes",
   "unit": "game/mainProgram0800"
  },
  "0940": {
   "line": "put the sprites back and read the drive's status byte",
   "unit": "game/mainProgram0800"
  },
  "0943": {
   "routine": [
    "finishTransfer - shared tail of a read and a write: puts the sprite enable register back the way it was and collects the drive's status byte, which arrives on the ordinary handshaked path.  Status 0 means success; anything else is a DOS error code ($02 header not found, $03 no sync, $05 data checksum, $08 write protected or wrong disk id).",
    "In:  savedSpriteEnable.",
    "Out: A = the status byte, C=1 when it is non-zero, sprites restored.",
    "Called from: readSectorBody $0940 and sendSectorData $0899, both by JMP, so its RTS returns to their callers."
   ],
   "line": "the sprite enable register as it was before the transfer...",
   "unit": "game/mainProgram0800"
  },
  "0946": {
   "line": "...restored, so the cursor and marker sprites come back",
   "unit": "game/mainProgram0800"
  },
  "0949": {
   "line": "exchange a dummy byte (the sprite mask happens to be in A) to collect the status byte",
   "unit": "game/mainProgram0800"
  },
  "094C": {
   "line": "status 0 = no error, leave C=0",
   "unit": "game/mainProgram0800"
  },
  "094E": {
   "line": "C=1 tells the caller the sector failed",
   "unit": "game/mainProgram0800"
  },
  "094F": {
   "line": "A still holds the status byte",
   "unit": "game/mainProgram0800"
  },
  "0950": {
   "routine": [
    "receiveSectorData - the fast half of the protocol and the only cycle-counted code in the game.  The drive does not handshake per bit here: it writes four port values per byte at fixed intervals (sendSectorToHost $044B), each carrying two data bits on CLK and DATA, and the only synchronisation is per byte - the host releases DATA to ask for the next one and then samples four times at fixed cycle offsets.  That is why sprites are off, CIA1 timer A is stopped, interrupts are disabled and each byte only starts on a raster line that is not immediately before a bad line.",
    "The four samples are folded together with LSR/EOR so that the two bits of sample 1 end up in bits 0-1 and those of sample 4 in bits 6-7; the VIC bank bits shift out of the bottom on their own, except in the last sample, which is why they are EORed out explicitly at $0984.",
    "In:  ldDestPtr/ldDestPtrHi = destination page, ldCia2Bits = the VIC bank bits, the SBC operand at $095F.",
    "Out: 256 bytes stored at (ldDestPtr); the bus left with DATA pulled low.",
    "Called from: readSectorBody $093D."
   ],
   "line": "poll the serial port; V = CLK IN",
   "unit": "game/mainProgram0800"
  },
  "0953": {
   "line": "wait until the drive releases CLK again - it does that just before every byte ($0466 in the drive code)",
   "unit": "game/mainProgram0800"
  },
  "0955": {
   "line": "index of the first byte of the sector",
   "unit": "game/mainProgram0800"
  },
  "0957": {
   "block": [
    "One pass per byte of the sector.  The pass first waits for a safe raster line, then releases DATA to ask for",
    "the byte and samples the port four times, two bits at a time."
   ],
   "line": "SBC below must subtract exactly the patched constant",
   "unit": "game/mainProgram0800"
  },
  "0958": {
   "line": "the bank bits with CLK OUT and DATA OUT clear: writing this releases both lines",
   "unit": "game/mainProgram0800"
  },
  "095A": {
   "line": "one cycle of padding",
   "unit": "game/mainProgram0800"
  },
  "095B": {
   "line": "where is the raster right now?",
   "unit": "game/mainProgram0800"
  },
  "095E": {
   "line": "operand patched at $0926 with (yscroll + $2F): subtracts the top of the display window",
   "unit": "game/mainProgram0800"
  },
  "0960": {
   "line": "above the display window there are no bad lines - go straight ahead",
   "unit": "game/mainProgram0800"
  },
  "0962": {
   "line": "how far the raster is from the next bad line",
   "unit": "game/mainProgram0800"
  },
  "0964": {
   "line": "wait while the next line would be a bad line: the VIC would steal 40 cycles in the middle of the byte",
   "unit": "game/mainProgram0800"
  },
  "0966": {
   "line": "release CLK OUT and DATA OUT: 'send me the next byte' - the drive waits for exactly this at $046C",
   "unit": "game/mainProgram0800"
  },
  "0969": {
   "line": "take a copy of the bank bits...",
   "unit": "game/mainProgram0800"
  },
  "096A": {
   "line": "...with bit 5 set, i.e. DATA OUT asserted, ready to say 'got it' after the byte",
   "unit": "game/mainProgram0800"
  },
  "096C": {
   "line": "keep it in X until then",
   "unit": "game/mainProgram0800"
  },
  "096D": {
   "line": "padding so the first sample lands on the drive's first port write",
   "unit": "game/mainProgram0800"
  },
  "096E": {
   "line": "padding",
   "unit": "game/mainProgram0800"
  },
  "096F": {
   "line": "BIT on an unused zero-page byte: three cycles of padding, nothing is tested",
   "unit": "game/mainProgram0800"
  },
  "0971": {
   "line": "sample 1: CLK IN and DATA IN carry data bits 0 and 1",
   "unit": "game/mainProgram0800"
  },
  "0974": {
   "line": "shift them down two places...",
   "unit": "game/mainProgram0800"
  },
  "0975": {
   "line": "...so the next sample can be merged in above them (the bank bits fall off the bottom)",
   "unit": "game/mainProgram0800"
  },
  "0976": {
   "line": "padding to hit the drive's second write",
   "unit": "game/mainProgram0800"
  },
  "0977": {
   "line": "sample 2: data bits 2 and 3",
   "unit": "game/mainProgram0800"
  },
  "097A": {
   "line": "make room for the next pair...",
   "unit": "game/mainProgram0800"
  },
  "097B": {
   "line": "...again dropping the bank bits of the older sample",
   "unit": "game/mainProgram0800"
  },
  "097C": {
   "line": "padding",
   "unit": "game/mainProgram0800"
  },
  "097D": {
   "line": "padding",
   "unit": "game/mainProgram0800"
  },
  "097E": {
   "line": "padding to hit the drive's third write",
   "unit": "game/mainProgram0800"
  },
  "097F": {
   "line": "sample 3: data bits 4 and 5",
   "unit": "game/mainProgram0800"
  },
  "0982": {
   "line": "shift the four bits gathered so far down...",
   "unit": "game/mainProgram0800"
  },
  "0983": {
   "line": "...into their final positions",
   "unit": "game/mainProgram0800"
  },
  "0984": {
   "line": "cancel the bank bits of the last sample in advance - they are not shifted out any more",
   "unit": "game/mainProgram0800"
  },
  "0986": {
   "line": "sample 4: data bits 6 and 7; the byte is now complete",
   "unit": "game/mainProgram0800"
  },
  "0989": {
   "line": "pull DATA low again: 'byte taken, do not send yet'",
   "unit": "game/mainProgram0800"
  },
  "098C": {
   "line": "padding so the store cannot overlap the drive's last write",
   "unit": "game/mainProgram0800"
  },
  "098D": {
   "line": "put the byte in the sector buffer",
   "unit": "game/mainProgram0800"
  },
  "098F": {
   "line": "next byte",
   "unit": "game/mainProgram0800"
  },
  "0990": {
   "line": "loop until Y wraps: 256 bytes received",
   "unit": "game/mainProgram0800"
  },
  "0992": {
   "line": "back to readSectorBody, which reads the status byte",
   "unit": "game/mainProgram0800"
  },
  "0993": {
   "block": [
    "strBlockExecuteLeftover - the DOS command strings 'B-E 2 0 1 17' and '#' that the boot loader ($C32C) sends",
    "to start the drive code, left in this copy of the loader.  The game never opens a KERNAL channel (the ROMs",
    "are banked out by the time $0800 runs), and nothing references these bytes: dead weight carried over when",
    "the loader was copied into the main program."
   ],
   "line": "'B-E 2 0 1 17' = block-execute track 1 sector 17 in the drive, then a 0 terminator, the filename '#' and another 0",
   "unit": "game/mainProgram0800"
  },
  "09A2": {
   "block": [
    "loadDestLo - operand byte of the LDA at $0868/$0933: the low byte of the transfer address, patched by setLoadDest."
   ],
   "line": "low byte of the next transfer address",
   "unit": "game/mainProgram0800"
  },
  "09A3": {
   "block": [
    "loadDestHi - operand byte of the LDA at $086D/$0938: the high byte of the transfer address.  loadSectors and",
    "writeSectors increment it after every sector, so it walks the destination page by page.  $09A4 is a pad byte",
    "nothing touches."
   ],
   "line": "high byte of the next transfer address",
   "unit": "game/mainProgram0800"
  },
  "09A5": {
   "block": [
    "savedSpriteEnable - VIC_SPR_ENA as it was when a sector transfer started; the sprites are blanked so that sprite DMA cannot steal cycles, and this puts them back."
   ],
   "line": "sprite enable register saved across one sector transfer",
   "unit": "game/mainProgram0800"
  },
  "09A6": {
   "routine": [
    "manualLookupCheck - the manual look-up copy protection, and it is disabled in this build: the JSR that called it at $0A9B was assembled as BIT ($2C instead of $20), so it is never entered.  It picks one of the 32 map seeds in the table at $0401 using the boot-time random byte at $09C3, generates and draws that battlefield, prints 'IDENTIFY THIS MAP (SEE MANUAL)' and lets the player type the five-letter map name (the printable form of the 24-bit seed) three times.  A wrong answer three times over wipes memory downwards from $C000 and hangs the machine.  The current map seed and map name are saved on the stack and put back on success, so a real game is unaffected.",
    "In:  the answer table mapNameSeedTable ($0401), the random byte at $09C3, the player's typing through enterMapName ($7717), which packs the five characters into scenarioSeed0-2 ($5A-$5C).",
    "Out: mapSeed and mapNameText restored, textColour/textPadColour = 0, the info panel cleared; on failure it never returns.",
    "Called from: nobody - the only reference is the disabled JSR at $0A9B."
   ],
   "line": "byte 0 of the map seed of the game in progress...",
   "unit": "game/mainProgram0800"
  },
  "09A9": {
   "line": "...onto the stack",
   "unit": "game/mainProgram0800"
  },
  "09AA": {
   "line": "byte 1 of the map seed",
   "unit": "game/mainProgram0800"
  },
  "09AD": {
   "line": "saved",
   "unit": "game/mainProgram0800"
  },
  "09AE": {
   "line": "byte 2 of the map seed",
   "unit": "game/mainProgram0800"
  },
  "09B1": {
   "line": "saved",
   "unit": "game/mainProgram0800"
  },
  "09B2": {
   "line": "start at the first character of the map name",
   "unit": "game/mainProgram0800"
  },
  "09B4": {
   "line": "the printable map name plus its terminator ($0BA9-$0BAE)",
   "unit": "game/mainProgram0800"
  },
  "09B7": {
   "line": "push all six bytes; they come back off in reverse at $0A2B",
   "unit": "game/mainProgram0800"
  },
  "09B8": {
   "line": "next character",
   "unit": "game/mainProgram0800"
  },
  "09B9": {
   "line": "five characters and the terminator byte",
   "unit": "game/mainProgram0800"
  },
  "09BB": {
   "line": "loop until all six are on the stack",
   "unit": "game/mainProgram0800"
  },
  "09BD": {
   "line": "three attempts...",
   "unit": "game/mainProgram0800"
  },
  "09BF": {
   "line": "...counted down in manualCheckTries",
   "unit": "game/mainProgram0800"
  },
  "09C2": {
   "line": "the operand byte $09C3 is the random seed the boot loader took from the SID noise generator ($C215) and verifyProgramChecksum preserves ($0461)",
   "unit": "game/mainProgram0800"
  },
  "09C4": {
   "line": "32 questions in the manual, so keep 5 bits",
   "unit": "game/mainProgram0800"
  },
  "09C6": {
   "line": "hold the question number",
   "unit": "game/mainProgram0800"
  },
  "09C8": {
   "line": "x2...",
   "unit": "game/mainProgram0800"
  },
  "09C9": {
   "line": "...plus one more = x3, the stride of the 3-byte answer table",
   "unit": "game/mainProgram0800"
  },
  "09CB": {
   "line": "index the table with it",
   "unit": "game/mainProgram0800"
  },
  "09CC": {
   "line": "byte 0 of that map's seed",
   "unit": "game/mainProgram0800"
  },
  "09CF": {
   "line": "make it the current map seed",
   "unit": "game/mainProgram0800"
  },
  "09D2": {
   "line": "next byte of the entry",
   "unit": "game/mainProgram0800"
  },
  "09D3": {
   "line": "byte 1",
   "unit": "game/mainProgram0800"
  },
  "09D6": {
   "line": "stored",
   "unit": "game/mainProgram0800"
  },
  "09D9": {
   "line": "last byte of the entry",
   "unit": "game/mainProgram0800"
  },
  "09DA": {
   "line": "byte 2",
   "unit": "game/mainProgram0800"
  },
  "09DD": {
   "line": "stored",
   "unit": "game/mainProgram0800"
  },
  "09E0": {
   "line": "build the 40x40 battlefield from that seed (map generator overlay, $6F04)",
   "unit": "game/mainProgram0800"
  },
  "09E3": {
   "line": "draw it in the overview window so the player can compare it with the manual",
   "unit": "game/mainProgram0800"
  },
  "09E6": {
   "line": "colour byte $02 for the prompt cells: with the inverted font the strokes come out red on black",
   "unit": "game/mainProgram0800"
  },
  "09E8": {
   "line": "colour of the printed characters",
   "unit": "game/mainProgram0800"
  },
  "09EA": {
   "line": "colour of the blanks that pad a centred line",
   "unit": "game/mainProgram0800"
  },
  "09EC": {
   "line": "clear the info panel to that colour",
   "unit": "game/mainProgram0800"
  },
  "09EF": {
   "line": "low byte of the prompt - $0A47, one character before msgIdentifyMap, so the text starts with an extra space",
   "unit": "game/mainProgram0800"
  },
  "09F1": {
   "line": "high byte of the prompt",
   "unit": "game/mainProgram0800"
  },
  "09F3": {
   "line": "print 'IDENTIFY THIS MAP (SEE MANUAL)' (it contains two $0D line breaks)",
   "unit": "game/mainProgram0800"
  },
  "09F6": {
   "line": "blank the five-character name field ($7836 in the overlay)",
   "unit": "game/mainProgram0800"
  },
  "09F9": {
   "line": "let the player type a map name; it is packed 5 bits per letter into scenarioSeed0-2",
   "unit": "game/mainProgram0800"
  },
  "09FC": {
   "line": "byte 0 of what was typed",
   "unit": "game/mainProgram0800"
  },
  "09FE": {
   "line": "against byte 0 of the seed the map was built from",
   "unit": "game/mainProgram0800"
  },
  "0A01": {
   "line": "wrong - no need to look further",
   "unit": "game/mainProgram0800"
  },
  "0A03": {
   "line": "byte 1 typed",
   "unit": "game/mainProgram0800"
  },
  "0A05": {
   "line": "against byte 1 of the seed",
   "unit": "game/mainProgram0800"
  },
  "0A08": {
   "line": "wrong",
   "unit": "game/mainProgram0800"
  },
  "0A0A": {
   "line": "byte 2 typed",
   "unit": "game/mainProgram0800"
  },
  "0A0C": {
   "line": "against byte 2 of the seed",
   "unit": "game/mainProgram0800"
  },
  "0A0F": {
   "line": "all three match: the player owns the manual",
   "unit": "game/mainProgram0800"
  },
  "0A11": {
   "line": "sound 3 = the error beep",
   "unit": "game/mainProgram0800"
  },
  "0A13": {
   "line": "play it",
   "unit": "game/mainProgram0800"
  },
  "0A16": {
   "line": "one attempt gone",
   "unit": "game/mainProgram0800"
  },
  "0A19": {
   "line": "ask again while attempts are left",
   "unit": "game/mainProgram0800"
  },
  "0A1B": {
   "line": "index 0...",
   "unit": "game/mainProgram0800"
  },
  "0A1D": {
   "line": "...and A = 0: the byte written everywhere",
   "unit": "game/mainProgram0800"
  },
  "0A1E": {
   "block": [
    "Three wrong answers: erase the machine.  The loop zeroes one page at a time, walking downwards from $C000",
    "by decrementing the high byte of its own STA operand, and stops when that byte reaches 0.  Long before then",
    "it wipes page $0A - its own code - so the CPU runs into zeroed memory and the machine has to be switched off.",
    "The tables above $C0FF survive, which is of no use to anybody."
   ],
   "line": "zero one byte; the operand high byte at $0A20 is what makes this walk down memory",
   "unit": "game/mainProgram0800"
  },
  "0A21": {
   "line": "next byte of the page",
   "unit": "game/mainProgram0800"
  },
  "0A22": {
   "line": "256 bytes per pass",
   "unit": "game/mainProgram0800"
  },
  "0A24": {
   "line": "step the STA operand down to the previous page",
   "unit": "game/mainProgram0800"
  },
  "0A27": {
   "line": "keep going until the operand high byte wraps to 0 - by then this code is gone too",
   "unit": "game/mainProgram0800"
  },
  "0A29": {
   "line": "restore the six saved map-name bytes, last pushed first",
   "unit": "game/mainProgram0800"
  },
  "0A2B": {
   "line": "pull one byte",
   "unit": "game/mainProgram0800"
  },
  "0A2C": {
   "line": "back into the map name",
   "unit": "game/mainProgram0800"
  },
  "0A2F": {
   "line": "work backwards",
   "unit": "game/mainProgram0800"
  },
  "0A30": {
   "line": "until all six are back",
   "unit": "game/mainProgram0800"
  },
  "0A32": {
   "line": "byte 2 of the saved map seed",
   "unit": "game/mainProgram0800"
  },
  "0A33": {
   "line": "restored",
   "unit": "game/mainProgram0800"
  },
  "0A36": {
   "line": "byte 1",
   "unit": "game/mainProgram0800"
  },
  "0A37": {
   "line": "restored",
   "unit": "game/mainProgram0800"
  },
  "0A3A": {
   "line": "byte 0",
   "unit": "game/mainProgram0800"
  },
  "0A3B": {
   "line": "restored - the game in progress never notices the detour",
   "unit": "game/mainProgram0800"
  },
  "0A3E": {
   "line": "back to colour 0...",
   "unit": "game/mainProgram0800"
  },
  "0A40": {
   "line": "...for printed characters",
   "unit": "game/mainProgram0800"
  },
  "0A42": {
   "line": "...and for the padding",
   "unit": "game/mainProgram0800"
  },
  "0A44": {
   "line": "clear the info panel again and return through it",
   "unit": "game/mainProgram0800"
  },
  "0A47": {
   "line": "a plain space: printString is called with $0A47, so this is really the first character of the prompt below",
   "unit": "game/mainProgram0800"
  },
  "0A48": {
   "line": "'IDENTIFY THIS MAP (SEE MANUAL)' - the $0D bytes are line breaks and the ')' carries bit 7 as the terminator",
   "unit": "game/mainProgram0800"
  },
  "0A6C": {
   "routine": [
    "mainStart - the cold start of the game, run once.  It silences the CIAs, parks both hardware vectors on an RTI, pulls in the runtime overlays (text engine, setup overlay, message strings and the $E000 opponent module), loads the saved setup block and runs the little start-up routine at $0300 that came with it, installs the raster IRQ, paints the empty battlefield screen and drops into the main menu.  The manual look-up check that used to run here is disabled in this build.",
    "In:  nothing but the memory image the boot loader left behind.",
    "Out: everything initialised: overlays resident, IRQ running, currentScreen = 0, playerSide = $FE (no side chosen); falls straight into returnToMainMenu.",
    "Called from: gameEntry $0800."
   ],
   "line": "no interrupts until the vectors are safe",
   "unit": "game/mainProgram0800"
  },
  "0A6D": {
   "line": "mask and acknowledge both CIAs so no timer or serial interrupt can arrive",
   "unit": "game/mainProgram0800"
  },
  "0A70": {
   "line": "low byte of irqRti ($1298)...",
   "unit": "game/mainProgram0800"
  },
  "0A72": {
   "line": "...into the NMI vector - the ROMs are banked out, so these RAM vectors are the real ones",
   "unit": "game/mainProgram0800"
  },
  "0A75": {
   "line": "and into the IRQ vector, until installRasterIrq puts the real handler there",
   "unit": "game/mainProgram0800"
  },
  "0A78": {
   "line": "high byte of irqRti",
   "unit": "game/mainProgram0800"
  },
  "0A7A": {
   "line": "NMI vector high byte",
   "unit": "game/mainProgram0800"
  },
  "0A7D": {
   "line": "IRQ vector high byte - any interrupt now just RTIs",
   "unit": "game/mainProgram0800"
  },
  "0A80": {
   "line": "interrupts on again; nothing can happen except an RTI",
   "unit": "game/mainProgram0800"
  },
  "0A81": {
   "line": "pull in $C000 (text engine), the $6F00 setup overlay, the message strings and the $E000 opponent module",
   "unit": "game/mainProgram0800"
  },
  "0A84": {
   "line": "load the saved setup block to $0200-$03FF (track 18 sectors 15-16)",
   "unit": "game/mainProgram0800"
  },
  "0A87": {
   "line": "run initGameSystems ($0300), the start-up code that came inside that block: RNG seed, fonts, map row tables, bitmap screen",
   "unit": "game/mainProgram0800"
  },
  "0A8A": {
   "line": "install the raster IRQ and clear the input state",
   "unit": "game/mainProgram0800"
  },
  "0A8D": {
   "line": "point the NMI vector and the comm module's copy of it at the RTI, and arm the BRK exit",
   "unit": "game/mainProgram0800"
  },
  "0A90": {
   "line": "screen 0...",
   "unit": "game/mainProgram0800"
  },
  "0A92": {
   "line": "...is the battlefield map screen",
   "unit": "game/mainProgram0800"
  },
  "0A95": {
   "line": "clear the bitmap and draw the empty game screen",
   "unit": "game/mainProgram0800"
  },
  "0A98": {
   "line": "blank the 20x20 overview window in it",
   "unit": "game/mainProgram0800"
  },
  "0A9B": {
   "line": "assembled as BIT, not JSR: the manual look-up copy protection is switched off in this build",
   "unit": "game/mainProgram0800"
  },
  "0A9E": {
   "line": "$FE = no side chosen yet...",
   "unit": "game/mainProgram0800"
  },
  "0AA0": {
   "line": "...the setup screens or the link decide who plays side 0 and who side 1",
   "unit": "game/mainProgram0800"
  },
  "0AA3": {
   "routine": [
    "returnToMainMenu - the panic exit back to the main menu, used whenever the game has to abandon whatever it was doing: the end of a game, a lost link, the SHIFT + C= + F7 abort in the raster IRQ, and the fall-through from mainStart.  It cannot return to its caller, so it throws the stack away and rebuilds it.",
    "In:  nothing.",
    "Out: VIC interrupts and all sprites off, stack pointer = $FD, input state reset, message queue empty, diskLoadDepth = 0, currentScreen = 3 (the menu screen); continues into the menu flow at $1A86.",
    "Called from: $126B (the abort hot key in the raster IRQ), $5A3F, game/textEngineC000 $C876, game/modemDriverE000 $EE0C, and by falling out of mainStart."
   ],
   "line": "0 into both VIC registers below",
   "unit": "game/mainProgram0800"
  },
  "0AA5": {
   "line": "no more raster interrupts while the screen is rebuilt",
   "unit": "game/mainProgram0800"
  },
  "0AA8": {
   "line": "all eight sprites off - the cursor and marker sprites belong to the game screens",
   "unit": "game/mainProgram0800"
  },
  "0AAB": {
   "line": "allow interrupts again (the handler is still installed)",
   "unit": "game/mainProgram0800"
  },
  "0AAC": {
   "line": "throw away whatever return addresses the abandoned code left...",
   "unit": "game/mainProgram0800"
  },
  "0AAE": {
   "line": "...by resetting the stack pointer to $FD, which is where the game keeps it",
   "unit": "game/mainProgram0800"
  },
  "0AAF": {
   "line": "clear the bitmap, screen and colour RAM to grey and set up the display registers",
   "unit": "game/mainProgram0800"
  },
  "0AB2": {
   "line": "silence the sound, re-install the raster IRQ, lock the input out for 158 frames",
   "unit": "game/mainProgram0800"
  },
  "0AB5": {
   "line": "drop every pending status message",
   "unit": "game/mainProgram0800"
  },
  "0AB8": {
   "line": "no disk load is in progress...",
   "unit": "game/mainProgram0800"
  },
  "0ABA": {
   "line": "...so the IRQ may pass keypresses to the game again",
   "unit": "game/mainProgram0800"
  },
  "0ABD": {
   "line": "screen 3...",
   "unit": "game/mainProgram0800"
  },
  "0ABF": {
   "line": "...is the main menu / console screen",
   "unit": "game/mainProgram0800"
  },
  "0AC2": {
   "line": "draw the title, credits and menu frame",
   "unit": "game/mainProgram0800"
  },
  "0AC5": {
   "line": "into the menu's link-status flow, which ends up in mainMenuLoop",
   "unit": "game/mainProgram0800"
  },
  "0AC8": {
   "routine": [
    "startGameFromSetup - the hand-over from the setup screens to the battle, entered by JMP from the setup overlay ($7E3F) once both sides have agreed on a game.  It resets the game state, makes sure the game disk is in the drive, swaps the $E000 opponent module if the player changed between modem and solo trainer, asks for ANSWER or ORIGINATE when a modem game is starting, loads the battlefield map and the setup block, negotiates the sides and the map over the link, brings overlay B in and finally jumps into the battlefield loop.",
    "In:  setupPlayerSide, filmPlaybackMode, linkSessionState, isSoloTrainer / prevSoloTrainerFlag, the resident build id $E030, overlayVariantFlag $87FF.",
    "Out: the battle running - never returns; jumps to runMapMainLoop ($1FD0).",
    "Called from: game/mapGenerator6F00 $7E3F (JMP)."
   ],
   "line": "0 into VIC_IRQ_MASK below",
   "unit": "game/mainProgram0800"
  },
  "0ACA": {
   "line": "no raster interrupts while the game state is torn down and rebuilt",
   "unit": "game/mainProgram0800"
  },
  "0ACD": {
   "line": "the side chosen in the setup screens...",
   "unit": "game/mainProgram0800"
  },
  "0AD0": {
   "line": "...becomes the side this machine plays (0 owns units 0-49, 1 owns units 50-99)",
   "unit": "game/mainProgram0800"
  },
  "0AD3": {
   "line": "clear the zero page, the variable block and the message queue",
   "unit": "game/mainProgram0800"
  },
  "0AD6": {
   "line": "sound off, raster IRQ back on, input locked out for a moment",
   "unit": "game/mainProgram0800"
  },
  "0AD9": {
   "line": "write the BCC/BCS opcode that decides 'is this unit mine?' into the two map renderers",
   "unit": "game/mainProgram0800"
  },
  "0ADC": {
   "line": "block until the game disk is in the drive (its BAM says OZ)",
   "unit": "game/mainProgram0800"
  },
  "0ADF": {
   "line": "watching a game film?",
   "unit": "game/mainProgram0800"
  },
  "0AE2": {
   "line": "then there is no opponent module to arrange",
   "unit": "game/mainProgram0800"
  },
  "0AE4": {
   "line": "the opponent the player picked: bit 7 set = solo trainer, clear = modem",
   "unit": "game/mainProgram0800"
  },
  "0AE7": {
   "line": "remembered for the whole game (the flag itself is overwritten below)",
   "unit": "game/mainProgram0800"
  },
  "0AEA": {
   "line": "compare it with the build id of the module actually resident ($FF trainer, $00 modem)",
   "unit": "game/mainProgram0800"
  },
  "0AED": {
   "line": "only bit 7 says which build it is",
   "unit": "game/mainProgram0800"
  },
  "0AEF": {
   "line": "same kind of opponent as last time - the module can stay",
   "unit": "game/mainProgram0800"
  },
  "0AF1": {
   "line": "mark a disk load in progress so the IRQ swallows keypresses",
   "unit": "game/mainProgram0800"
  },
  "0AF4": {
   "line": "load the other $E000 module (track 35 for the trainer, track 18 s7 + track 34 for the modem)",
   "unit": "game/mainProgram0800"
  },
  "0AF7": {
   "line": "is a link session already running?",
   "unit": "game/mainProgram0800"
  },
  "0AFA": {
   "line": "yes - do not re-initialise the module underneath it",
   "unit": "game/mainProgram0800"
  },
  "0AFC": {
   "line": "cold-initialise the module and point its keyboard vector at scanKeyboard",
   "unit": "game/mainProgram0800"
  },
  "0AFF": {
   "line": "link already up?",
   "unit": "game/mainProgram0800"
  },
  "0B02": {
   "line": "then the modem has long since been set up",
   "unit": "game/mainProgram0800"
  },
  "0B04": {
   "line": "watching a film?",
   "unit": "game/mainProgram0800"
  },
  "0B07": {
   "line": "then no modem prompt either",
   "unit": "game/mainProgram0800"
  },
  "0B09": {
   "line": "what kind of opponent the previous game had",
   "unit": "game/mainProgram0800"
  },
  "0B0C": {
   "line": "and what this one has",
   "unit": "game/mainProgram0800"
  },
  "0B0F": {
   "line": "remember it for next time",
   "unit": "game/mainProgram0800"
  },
  "0B12": {
   "line": "solo trainer now: nothing to dial",
   "unit": "game/mainProgram0800"
  },
  "0B15": {
   "line": "the previous game was a modem game too: the modem is already set",
   "unit": "game/mainProgram0800"
  },
  "0B17": {
   "block": [
    "Switching from the solo trainer to a modem game: put the two-line prompt up and wait for A or O.",
    "Message slots $20 and $21 are the two run-time slots whose text pointers setMessageSlot patches."
   ],
   "line": "message slot $20...",
   "unit": "game/mainProgram0800"
  },
  "0B19": {
   "line": "...low byte of msgGetOpponentOnPhoneAnd ($8800 = 'GET OPPONENT ON PHONE AND...')",
   "unit": "game/mainProgram0800"
  },
  "0B1B": {
   "line": "...high byte",
   "unit": "game/mainProgram0800"
  },
  "0B1D": {
   "line": "patch slot $20 to point at it",
   "unit": "game/mainProgram0800"
  },
  "0B20": {
   "line": "message slot $21...",
   "unit": "game/mainProgram0800"
  },
  "0B22": {
   "line": "...low byte of msgPressAOrOAndSetModem ($881C = 'PRESS A OR O AND SET MODEM.')",
   "unit": "game/mainProgram0800"
  },
  "0B24": {
   "line": "...high byte",
   "unit": "game/mainProgram0800"
  },
  "0B26": {
   "line": "patch slot $21",
   "unit": "game/mainProgram0800"
  },
  "0B29": {
   "line": "show them and wait until the player presses A (answer) or O (originate)",
   "unit": "game/mainProgram0800"
  },
  "0B2C": {
   "line": "the build id of the module that is really resident...",
   "unit": "game/mainProgram0800"
  },
  "0B2F": {
   "line": "...written back over the wanted flag, so $0BA5 always describes what is in memory ($FF trainer / $00 modem)",
   "unit": "game/mainProgram0800"
  },
  "0B32": {
   "line": "disk load starting: swallow keypresses",
   "unit": "game/mainProgram0800"
  },
  "0B35": {
   "line": "load the unit start-position templates to $F000 and the setup block to $0200",
   "unit": "game/mainProgram0800"
  },
  "0B38": {
   "line": "0 into diskLoadDepth",
   "unit": "game/mainProgram0800"
  },
  "0B3A": {
   "line": "the disk is done with for now",
   "unit": "game/mainProgram0800"
  },
  "0B3D": {
   "line": "let the comm module run again and clear the loading message",
   "unit": "game/mainProgram0800"
  },
  "0B40": {
   "line": "agree the sides and the map with the opponent and set up all the battle state",
   "unit": "game/mainProgram0800"
  },
  "0B43": {
   "line": "one more disk load coming",
   "unit": "game/mainProgram0800"
  },
  "0B46": {
   "line": "solo trainer?",
   "unit": "game/mainProgram0800"
  },
  "0B49": {
   "line": "no - the modem build's $EC00 tail was loaded with the module itself",
   "unit": "game/mainProgram0800"
  },
  "0B4F": {
   "routine": [
    "loadCommTailBuild1 - not a routine of its own: the tail of startGameFromSetup, entered with X/Y already set at $0B4B.  It pulls the solo trainer's $EC00-$EFFF sub-overlay (the code that picks the opening play) off track 35 sectors 12-15.  The modem build's tail comes from track 34 sectors 12-15 through loadCommTailBuild2 ($1070) instead.",
    "In:  X = $00, Y = $EC (the destination page).",
    "Out: $EC00-$EFFF loaded.",
    "Called from: falls in from $0B4D."
   ],
   "line": "destination $EC00",
   "unit": "game/mainProgram0800"
  },
  "0B52": {
   "line": "track 35 = the solo trainer's track",
   "unit": "game/mainProgram0800"
  },
  "0B54": {
   "line": "first sector 12",
   "unit": "game/mainProgram0800"
  },
  "0B56": {
   "line": "four sectors = $EC00-$EFFF",
   "unit": "game/mainProgram0800"
  },
  "0B58": {
   "line": "read them",
   "unit": "game/mainProgram0800"
  },
  "0B5B": {
   "line": "0 into gamePhase",
   "unit": "game/mainProgram0800"
  },
  "0B5D": {
   "line": "gamePhase 0 means the battle itself is running (non-zero = setup, film or menu)",
   "unit": "game/mainProgram0800"
  },
  "0B5F": {
   "line": "bring the comcen missile/drone screens into $6F00 if the setup overlay is still there",
   "unit": "game/mainProgram0800"
  },
  "0B62": {
   "line": "0 into the two bytes below",
   "unit": "game/mainProgram0800"
  },
  "0B64": {
   "line": "no disk load in progress: the IRQ may deliver keys again",
   "unit": "game/mainProgram0800"
  },
  "0B67": {
   "line": "the manual-check attempt counter is not needed during a game",
   "unit": "game/mainProgram0800"
  },
  "0B6A": {
   "line": "empty the message queue before the battle messages start",
   "unit": "game/mainProgram0800"
  },
  "0B6D": {
   "line": "watching a film?",
   "unit": "game/mainProgram0800"
  },
  "0B70": {
   "line": "then do not announce the battle",
   "unit": "game/mainProgram0800"
  },
  "0B72": {
   "line": "message $1C = 'THE BATTLE BEGINS!'",
   "unit": "game/mainProgram0800"
  },
  "0B74": {
   "line": "queue it",
   "unit": "game/mainProgram0800"
  },
  "0B77": {
   "line": "build the '<us> <them>' name line shown on the menu panel",
   "unit": "game/mainProgram0800"
  },
  "0B7A": {
   "line": "into the battlefield screen's main loop - this never returns",
   "unit": "game/mainProgram0800"
  },
  "0BB5": {
   "routine": [
    "initIrqAndInputState - brings the interrupt and input state back to a known point.  Called at cold start, on every return to the main menu and at the start of a game.  It silences the sound, installs the raster IRQ, re-arms the one-shot 'SPOTTED RECYCLER' check, blanks the chat line and locks the keyboard and joystick out for 158 frames so that the keypress or click that got here is not read again by the screen the player is about to land in.",
    "In:  nothing.",
    "Out: raster IRQ installed, inputLockoutTimer = $9E, chat line blank, gameEndCountdown / shiftHeldFlag / comcenStunStatus = 0, requestedScreenKey = $FF, the NOP restored at $3F57.",
    "Called from: mainStart $0A8A, returnToMainMenu $0AB2 and startGameFromSetup $0AD6."
   ],
   "line": "silence all four sound slots",
   "unit": "game/mainProgram0800"
  },
  "0BB8": {
   "line": "point $FFFE at rasterIrqHandler and enable the raster interrupt",
   "unit": "game/mainProgram0800"
  },
  "0BBB": {
   "line": "$EA = NOP...",
   "unit": "game/mainProgram0800"
  },
  "0BBD": {
   "line": "...restored over the first byte of checkEnemyRecyclerSpotted, which patches itself to RTS ($60) once it has fired, so the 'SPOTTED RECYCLER!' message can happen again in the next game",
   "unit": "game/mainProgram0800"
  },
  "0BC0": {
   "line": "$9E is used twice below: as a frame count and as a string terminator",
   "unit": "game/mainProgram0800"
  },
  "0BC2": {
   "line": "158 frames (about 2.6 seconds) during which the IRQ ignores the keyboard and the joystick",
   "unit": "game/mainProgram0800"
  },
  "0BC5": {
   "line": "and as the second byte of the chat line, where $9E is a bit-7 terminator that prints nothing - i.e. blank the incoming chat line",
   "unit": "game/mainProgram0800"
  },
  "0BC8": {
   "line": "0 into the three flags below",
   "unit": "game/mainProgram0800"
  },
  "0BCA": {
   "line": "the end-of-game countdown is not running",
   "unit": "game/mainProgram0800"
  },
  "0BCD": {
   "line": "no SHIFT held as far as the game is concerned",
   "unit": "game/mainProgram0800"
  },
  "0BD0": {
   "line": "the comcen is not stunned",
   "unit": "game/mainProgram0800"
  },
  "0BD3": {
   "line": "$FF = no key",
   "unit": "game/mainProgram0800"
  },
  "0BD5": {
   "line": "forget any pending F-key screen request",
   "unit": "game/mainProgram0800"
  },
  "0BD7": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0BD8": {
   "routine": [
    "disableCiaInterrupts - masks every interrupt source in both CIAs and acknowledges whatever was already pending.  Writing $7F to an interrupt control register clears all five mask bits (bit 7 = 0 means 'clear the bits I name'), and reading the register clears the latched flags.",
    "In:  nothing.",
    "Out: CIA1 and CIA2 will not raise IRQ or NMI; their pending flags cleared.",
    "Called from: mainStart $0A6D and loadCommModule $0F22 (before the trainer build is read in)."
   ],
   "line": "$7F = clear all five interrupt mask bits (bit 7 clear = 'clear the bits set below')",
   "unit": "game/mainProgram0800"
  },
  "0BDA": {
   "line": "CIA1 raises the IRQ line - no more timer or keyboard interrupts",
   "unit": "game/mainProgram0800"
  },
  "0BDD": {
   "line": "CIA2 raises the NMI line - no more serial or timer NMIs",
   "unit": "game/mainProgram0800"
  },
  "0BE0": {
   "line": "reading the register clears any interrupt already latched in CIA1",
   "unit": "game/mainProgram0800"
  },
  "0BE3": {
   "line": "and in CIA2",
   "unit": "game/mainProgram0800"
  },
  "0BE6": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0BE7": {
   "routine": [
    "initInterruptVectors - finishes the interrupt set-up: it points the BRK escape of the raster handler at the normal IRQ exit and parks both the hardware NMI vector and the comm module's private copy of it on the bare RTI at $1298.  The comm module later replaces its own copy with the modem's NMI routine when the UART is running.",
    "In:  nothing.",
    "Out: the JMP operand at $10FF/$1100 = irqExit ($1293); $E031/$E032 and $FFFA/$FFFB = irqRti ($1298).",
    "Called from: mainStart $0A8D."
   ],
   "line": "low byte of irqExit ($1293)",
   "unit": "game/mainProgram0800"
  },
  "0BE9": {
   "line": "operand of the JMP at $10FE, which rasterIrqHandler takes when the pushed status byte says BRK - so a stray BRK just restores the registers and RTIs",
   "unit": "game/mainProgram0800"
  },
  "0BEC": {
   "line": "high byte of irqExit",
   "unit": "game/mainProgram0800"
  },
  "0BEE": {
   "line": "second operand byte of that JMP",
   "unit": "game/mainProgram0800"
  },
  "0BF1": {
   "line": "low byte of irqRti ($1298)",
   "unit": "game/mainProgram0800"
  },
  "0BF3": {
   "line": "the comm module's copy of the NMI vector, which its own handler chains through",
   "unit": "game/mainProgram0800"
  },
  "0BF6": {
   "line": "and the hardware NMI vector itself",
   "unit": "game/mainProgram0800"
  },
  "0BF9": {
   "line": "high byte of irqRti",
   "unit": "game/mainProgram0800"
  },
  "0BFB": {
   "line": "comm module copy, high byte",
   "unit": "game/mainProgram0800"
  },
  "0BFE": {
   "line": "hardware NMI vector, high byte - a RESTORE keypress now does nothing",
   "unit": "game/mainProgram0800"
  },
  "0C01": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0C02": {
   "routine": [
    "setNmiVectorRti - points the hardware NMI vector at the bare RTI at $1298.  The solo trainer never uses an NMI, so loadCommModule jumps here after reading the trainer build in, to undo whatever the modem build had left in the vector.",
    "In:  nothing.",
    "Out: $FFFA/$FFFB = $1298.",
    "Called from: loadCommModule $0F2E (JMP)."
   ],
   "line": "low byte of irqRti",
   "unit": "game/mainProgram0800"
  },
  "0C04": {
   "line": "NMI vector low",
   "unit": "game/mainProgram0800"
  },
  "0C07": {
   "line": "high byte of irqRti",
   "unit": "game/mainProgram0800"
  },
  "0C09": {
   "line": "NMI vector high - an NMI now returns immediately",
   "unit": "game/mainProgram0800"
  },
  "0C0C": {
   "line": "shared RTS, also used by jumpToCd00IfPatched below",
   "unit": "game/mainProgram0800"
  },
  "0C0D": {
   "routine": [
    "jumpToCd00IfPatched - dead code.  It looks at $CD00 and, if it finds a JMP opcode there, turns the VIC interrupt off and jumps into it.  Nothing calls it, and at run time $CD00 is unitPictureHiTable inside the $C000 overlay, i.e. ordinary data, so the test would practically never pass.  It reads like a development hook for jumping into a monitor or a test routine loaded over the overlay.",
    "In:  $CD00.",
    "Out: normally just an RTS; if the byte were $4C it would disable the raster interrupt and jump there.",
    "Called from: nothing in any traced code."
   ],
   "line": "first byte of the $C000 overlay's unit-picture table",
   "unit": "game/mainProgram0800"
  },
  "0C10": {
   "line": "$4C = JMP absolute: has somebody planted a jump there?",
   "unit": "game/mainProgram0800"
  },
  "0C12": {
   "line": "no - return through the RTS at $0C0C",
   "unit": "game/mainProgram0800"
  },
  "0C14": {
   "line": "0 into VIC_IRQ_MASK",
   "unit": "game/mainProgram0800"
  },
  "0C16": {
   "line": "no raster interrupts, so the planted code owns the machine",
   "unit": "game/mainProgram0800"
  },
  "0C19": {
   "line": "and run it",
   "unit": "game/mainProgram0800"
  },
  "0C1C": {
   "routine": [
    "resetGameVariables - wipes the game's variables back to a cold state: zero page $15-$7F and $8C-$FF, the whole variable block $9008-$92FF, the eight-entry message queue and the last-key bytes.  It deliberately leaves $00-$14 (the 6510 port and the loader's own scratch) and $80-$8B alone, and starts the variable block at $9008 so the VIC shadow registers at $9000-$9007 survive.  Clearing the RNG state $57-$59 is intentional too: stepRandomSeed re-seeds it from the joystick as soon as it sees all three bytes zero.",
    "In:  nothing.",
    "Out: zero page and $9008-$92FF cleared, messageQueue and lastTypedKey = $FF, newKeyEvent = $FF, gamePhase = $FF, filmPtr rewound to $D000, currentScreen = 3, the chat line blanked.",
    "Called from: startGameFromSetup $0AD3 and the setup block's initGameSystems ($0300)."
   ],
   "line": "start at $0015 - $00-$14 belong to the 6510 port and the loader",
   "unit": "game/mainProgram0800"
  },
  "0C1E": {
   "line": "the value written everywhere",
   "unit": "game/mainProgram0800"
  },
  "0C20": {
   "line": "absolute,Y so it can reach the whole of page zero",
   "unit": "game/mainProgram0800"
  },
  "0C23": {
   "line": "next byte",
   "unit": "game/mainProgram0800"
  },
  "0C24": {
   "line": "stop at $80, i.e. clear $15-$7F",
   "unit": "game/mainProgram0800"
  },
  "0C26": {
   "line": "second run: $8C upwards ($80-$8B are left alone)",
   "unit": "game/mainProgram0800"
  },
  "0C28": {
   "line": "clear it",
   "unit": "game/mainProgram0800"
  },
  "0C2B": {
   "line": "next byte",
   "unit": "game/mainProgram0800"
  },
  "0C2C": {
   "line": "until Y wraps at the end of page zero",
   "unit": "game/mainProgram0800"
  },
  "0C2E": {
   "line": "$9008 is the first variable after the VIC shadow registers...",
   "unit": "game/mainProgram0800"
  },
  "0C30": {
   "line": "...low byte of the fill pointer",
   "unit": "game/mainProgram0800"
  },
  "0C32": {
   "line": "high byte...",
   "unit": "game/mainProgram0800"
  },
  "0C34": {
   "line": "...so the fill starts at $9008",
   "unit": "game/mainProgram0800"
  },
  "0C36": {
   "line": "$92FF is the last variable...",
   "unit": "game/mainProgram0800"
  },
  "0C38": {
   "line": "...low byte of the limit",
   "unit": "game/mainProgram0800"
  },
  "0C3A": {
   "line": "high byte...",
   "unit": "game/mainProgram0800"
  },
  "0C3C": {
   "line": "...so the fill ends at $92FF",
   "unit": "game/mainProgram0800"
  },
  "0C3E": {
   "line": "the indirect store always uses offset 0",
   "unit": "game/mainProgram0800"
  },
  "0C40": {
   "line": "zero",
   "unit": "game/mainProgram0800"
  },
  "0C42": {
   "line": "clear one variable byte",
   "unit": "game/mainProgram0800"
  },
  "0C44": {
   "line": "step the pointer and set C when the limit is reached",
   "unit": "game/mainProgram0800"
  },
  "0C47": {
   "line": "loop until the whole block is clear",
   "unit": "game/mainProgram0800"
  },
  "0C49": {
   "line": "eight message queue slots...",
   "unit": "game/mainProgram0800"
  },
  "0C4B": {
   "line": "...filled with $FF = empty",
   "unit": "game/mainProgram0800"
  },
  "0C4D": {
   "line": "clear queue slot Y",
   "unit": "game/mainProgram0800"
  },
  "0C50": {
   "line": "next slot",
   "unit": "game/mainProgram0800"
  },
  "0C51": {
   "line": "until all eight are done",
   "unit": "game/mainProgram0800"
  },
  "0C53": {
   "line": "$FF = nothing typed",
   "unit": "game/mainProgram0800"
  },
  "0C56": {
   "line": "$FF = no key event pending",
   "unit": "game/mainProgram0800"
  },
  "0C59": {
   "line": "$FF = not in the battle phase",
   "unit": "game/mainProgram0800"
  },
  "0C5B": {
   "line": "rewind the game-film pointer to the start of the buffer under the I/O area ($D000)",
   "unit": "game/mainProgram0800"
  },
  "0C5E": {
   "line": "screen 3...",
   "unit": "game/mainProgram0800"
  },
  "0C60": {
   "line": "...the menu/console screen is where the game goes next",
   "unit": "game/mainProgram0800"
  },
  "0C63": {
   "line": "blank the chat status line and return through it",
   "unit": "game/mainProgram0800"
  },
  "0C66": {
   "routine": [
    "installRasterIrq - installs rasterIrqHandler as the interrupt routine.  There is no KERNAL to go through: with $01 = $35 the hardware vector at $FFFE is the real one, and the handler rewrites it from the zero-page shadow on every pass, so a corrupted vector repairs itself.  The interrupt is armed for raster line $FA (250), in the lower border below the 200 display lines, so all the per-frame work happens off screen.",
    "In:  nothing.",
    "Out: $FFFE/$FFFF and zp_AB/zp_AC = $10F1, rasterIrqLine = $FA, irqInProgress = 0, VIC raster interrupt acknowledged and enabled.",
    "Called from: initIrqAndInputState $0BB8."
   ],
   "line": "no interrupts while the vector is half written",
   "unit": "game/mainProgram0800"
  },
  "0C67": {
   "line": "low byte of rasterIrqHandler ($10F1)",
   "unit": "game/mainProgram0800"
  },
  "0C69": {
   "line": "the shadow the handler writes back to $FFFE on every interrupt",
   "unit": "game/mainProgram0800"
  },
  "0C6B": {
   "line": "and the live hardware vector",
   "unit": "game/mainProgram0800"
  },
  "0C6E": {
   "line": "high byte of rasterIrqHandler",
   "unit": "game/mainProgram0800"
  },
  "0C70": {
   "line": "shadow high byte",
   "unit": "game/mainProgram0800"
  },
  "0C72": {
   "line": "hardware vector high byte",
   "unit": "game/mainProgram0800"
  },
  "0C75": {
   "line": "raster line 250, in the lower border",
   "unit": "game/mainProgram0800"
  },
  "0C77": {
   "line": "the handler re-arms VIC_RASTER from this every time",
   "unit": "game/mainProgram0800"
  },
  "0C79": {
   "line": "arm the raster compare now",
   "unit": "game/mainProgram0800"
  },
  "0C7C": {
   "line": "0 into irqInProgress",
   "unit": "game/mainProgram0800"
  },
  "0C7E": {
   "line": "the handler's re-entrancy guard starts clear",
   "unit": "game/mainProgram0800"
  },
  "0C80": {
   "line": "interrupts on",
   "unit": "game/mainProgram0800"
  },
  "0C81": {
   "line": "bit 0 = the raster interrupt",
   "unit": "game/mainProgram0800"
  },
  "0C83": {
   "line": "acknowledge anything already latched",
   "unit": "game/mainProgram0800"
  },
  "0C86": {
   "line": "and enable raster interrupts",
   "unit": "game/mainProgram0800"
  },
  "0C89": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0C8A": {
   "block": [
    "comcenUnitIndexBySide - the unit index of each side's COMCEN: 49 ($31) for side 0, 99 ($63) for side 1,",
    "the last unit of each side's 50.  Indexed by the side number, so the code can reach either command centre's",
    "row in the unit arrays (energy, stun, position) without a multiply.  Phase 1 called this perSideValueTable;",
    "$7BF2 in the setup overlay copies the entry straight into comcenUnit ($9236)."
   ],
   "line": "side 0's comcen is unit 49, side 1's is unit 99",
   "unit": "game/mainProgram0800"
  },
  "0C8C": {
   "routine": [
    "showModemSetupPrompt - puts a caller-supplied line above 'PRESS SPACE WHEN READY.' and waits for the space bar.  The setup overlay uses it for the modem dialogue ('PICK UP PHONE THEN PRESS SPACE.' and friends).",
    "In:  A/Y = the string to show in message slot $20.",
    "Out: message slots $20 and $21 patched, both messages shown, then it tail-jumps into waitForSpaceKey ($7DB8) in the overlay, so it never returns here.",
    "Called from: game/mapGenerator6F00 $79E3."
   ],
   "line": "message slot $20 = the run-time slot for the caller's line",
   "unit": "game/mainProgram0800"
  },
  "0C8E": {
   "line": "patch its text pointer to A/Y",
   "unit": "game/mainProgram0800"
  },
  "0C91": {
   "line": "message slot $21 = the second run-time slot",
   "unit": "game/mainProgram0800"
  },
  "0C93": {
   "line": "low byte of msgPressSpaceWhenReady ($8837)",
   "unit": "game/mainProgram0800"
  },
  "0C95": {
   "line": "high byte",
   "unit": "game/mainProgram0800"
  },
  "0C97": {
   "line": "patch slot $21 to it",
   "unit": "game/mainProgram0800"
  },
  "0C9A": {
   "line": "show slot $20, beep twice and arm slot $21 as the fallback line",
   "unit": "game/mainProgram0800"
  },
  "0C9D": {
   "line": "wait for the space bar in the setup overlay; that routine returns to our caller",
   "unit": "game/mainProgram0800"
  },
  "0CA0": {
   "routine": [
    "resetMessagesAndBeep - clears the message queue, shows message slot $20 immediately, beeps twice a third of a second apart and makes slot $21 the line the status bar falls back to once slot $20 has had its time.  The double beep is what tells the player across the room that the game wants something.",
    "In:  message slots $20 and $21 already patched by the caller.",
    "Out: queue empty, slot $20 displayed, persistentMessageId = $21.",
    "Called from: showModemSetupPrompt $0C9A and waitForAnswerOriginateKey $0CBD."
   ],
   "line": "drop anything already queued",
   "unit": "game/mainProgram0800"
  },
  "0CA3": {
   "line": "message id $20 = the first run-time slot",
   "unit": "game/mainProgram0800"
  },
  "0CA5": {
   "line": "put it on the status line",
   "unit": "game/mainProgram0800"
  },
  "0CA8": {
   "line": "sound 3 = the attention beep",
   "unit": "game/mainProgram0800"
  },
  "0CAA": {
   "line": "play it",
   "unit": "game/mainProgram0800"
  },
  "0CAD": {
   "line": "20 frames = a third of a second",
   "unit": "game/mainProgram0800"
  },
  "0CAF": {
   "line": "wait, so the two beeps are heard as two",
   "unit": "game/mainProgram0800"
  },
  "0CB2": {
   "line": "sound 3 again",
   "unit": "game/mainProgram0800"
  },
  "0CB4": {
   "line": "play it",
   "unit": "game/mainProgram0800"
  },
  "0CB7": {
   "line": "message id $21 = the second run-time slot",
   "unit": "game/mainProgram0800"
  },
  "0CB9": {
   "line": "it becomes the line the status bar returns to when the queue drains",
   "unit": "game/mainProgram0800"
  },
  "0CBC": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0CBD": {
   "routine": [
    "waitForAnswerOriginateKey - the ANSWER/ORIGINATE question at the start of a modem game.  It shows the two prompt lines, then spins until the player types A or O, and tells the comm module which of the two Hayes command strings to use ($E011 indexes the modem's init-string table).  The spin also stirs the game RNG, so the moment the player answers seeds the map.",
    "In:  message slots $20/$21 already patched by the caller; lastTypedKey fed by the raster IRQ.",
    "Out: $E011 = 0 (answer) or 1 (originate), inputLockoutTimer = 0, message queue emptied.",
    "Called from: startGameFromSetup $0B29."
   ],
   "line": "show the prompt and beep twice",
   "unit": "game/mainProgram0800"
  },
  "0CC0": {
   "line": "$FF = nothing typed",
   "unit": "game/mainProgram0800"
  },
  "0CC2": {
   "line": "forget any key that was typed before the prompt went up",
   "unit": "game/mainProgram0800"
  },
  "0CC5": {
   "line": "0 into inputLockoutTimer",
   "unit": "game/mainProgram0800"
  },
  "0CC7": {
   "line": "cancel the 158-frame lockout so the IRQ passes the keystroke through at once",
   "unit": "game/mainProgram0800"
  },
  "0CCA": {
   "line": "keep the status line alive while waiting",
   "unit": "game/mainProgram0800"
  },
  "0CCD": {
   "line": "stir the 24-bit game RNG: the human reaction time is the entropy source",
   "unit": "game/mainProgram0800"
  },
  "0CD0": {
   "line": "X = 0 = ANSWER",
   "unit": "game/mainProgram0800"
  },
  "0CD2": {
   "line": "the last key the IRQ recorded",
   "unit": "game/mainProgram0800"
  },
  "0CD5": {
   "line": "$C1 = 'A' (PETSCII with bit 7 set) = answer the call",
   "unit": "game/mainProgram0800"
  },
  "0CD7": {
   "line": "got it",
   "unit": "game/mainProgram0800"
  },
  "0CD9": {
   "line": "X = 1 = ORIGINATE",
   "unit": "game/mainProgram0800"
  },
  "0CDA": {
   "line": "$CF = 'O' = place the call",
   "unit": "game/mainProgram0800"
  },
  "0CDC": {
   "line": "neither - keep waiting",
   "unit": "game/mainProgram0800"
  },
  "0CDE": {
   "line": "tell the modem driver which mode it is in; it indexes its init-string table with this",
   "unit": "game/mainProgram0800"
  },
  "0CE1": {
   "line": "clear the prompt away and return through resetMessageQueue",
   "unit": "game/mainProgram0800"
  },
  "0CE4": {
   "block": [
    "strOurs - the four characters 'OURS', copied over the owner-name field when the disk carries no name.",
    "It has no bit-7 terminator because it is never printed directly: buildOwnerNameString copies it into",
    "ownPlayerName and the terminator is added to the assembled line afterwards."
   ],
   "line": "default owner name for an unpersonalised disk",
   "unit": "game/mainProgram0800"
  },
  "0CE8": {
   "routine": [
    "buildOwnerNameString - assembles the 21-character line that names the two players on the menu panel, out of the two 10-byte name fields at the top of memory: ownPlayerName ($FFDE), copied out of the disk's BAM by checkDiskId, and opponentPlayerName ($FFE8), which the modem driver fills in from the other machine.  The first name is packed to the right of the first ten columns and the second to the left of the last ten, so the two meet either side of the single space in the middle.  Trailing blanks of the first name and leading blanks of the second are dropped ($FFDE holds shifted spaces, $A0, which the AND #$7F turns into ordinary spaces).",
    "In:  ownPlayerName and opponentPlayerName, 10 bytes each.",
    "Out: ownerNameString ($0B7F-$0B93) filled and terminated with bit 7 set on its last byte; ownPlayerName replaced by 'OURS' when it was blank.",
    "Called from: startGameFromSetup $0B77."
   ],
   "line": "21 characters, $0B7F-$0B93",
   "unit": "game/mainProgram0800"
  },
  "0CEA": {
   "line": "a plain space",
   "unit": "game/mainProgram0800"
  },
  "0CEC": {
   "line": "blank the whole line first",
   "unit": "game/mainProgram0800"
  },
  "0CEF": {
   "line": "next character",
   "unit": "game/mainProgram0800"
  },
  "0CF0": {
   "line": "until all 21 are blank",
   "unit": "game/mainProgram0800"
  },
  "0CF2": {
   "line": "first character of the owner name out of the disk BAM",
   "unit": "game/mainProgram0800"
  },
  "0CF5": {
   "line": "$A0 = shifted space = the field was never personalised",
   "unit": "game/mainProgram0800"
  },
  "0CF7": {
   "line": "the disk has a name - use it",
   "unit": "game/mainProgram0800"
  },
  "0CF9": {
   "line": "four characters of 'OURS'",
   "unit": "game/mainProgram0800"
  },
  "0CFB": {
   "line": "copy them...",
   "unit": "game/mainProgram0800"
  },
  "0CFE": {
   "line": "...over the first four bytes of the owner-name field",
   "unit": "game/mainProgram0800"
  },
  "0D01": {
   "line": "next character",
   "unit": "game/mainProgram0800"
  },
  "0D02": {
   "line": "until all four are in",
   "unit": "game/mainProgram0800"
  },
  "0D04": {
   "block": [
    "First name, right aligned: walk it backwards into columns 9..0 of the line.  scratch18 stays 0 until the",
    "first real character has been placed and is negative afterwards, so blanks at the end of the field are",
    "skipped while blanks inside the name are kept."
   ],
   "line": "0 into the 'seen a character yet' flag",
   "unit": "game/mainProgram0800"
  },
  "0D06": {
   "line": "scratch18 = 0 means nothing placed so far",
   "unit": "game/mainProgram0800"
  },
  "0D08": {
   "line": "last character of the 10-byte field",
   "unit": "game/mainProgram0800"
  },
  "0D0A": {
   "line": "column 9 = the rightmost of the first half of the line",
   "unit": "game/mainProgram0800"
  },
  "0D0C": {
   "line": "one character of the owner name",
   "unit": "game/mainProgram0800"
  },
  "0D0F": {
   "line": "clear bit 7: shifted characters ($A0 etc.) become plain ones",
   "unit": "game/mainProgram0800"
  },
  "0D11": {
   "line": "is it a space?",
   "unit": "game/mainProgram0800"
  },
  "0D13": {
   "line": "no - place it",
   "unit": "game/mainProgram0800"
  },
  "0D15": {
   "line": "have any characters been placed yet?",
   "unit": "game/mainProgram0800"
  },
  "0D17": {
   "line": "no - this is a trailing blank, drop it",
   "unit": "game/mainProgram0800"
  },
  "0D19": {
   "line": "place the character in the line",
   "unit": "game/mainProgram0800"
  },
  "0D1C": {
   "line": "one column to the left",
   "unit": "game/mainProgram0800"
  },
  "0D1D": {
   "line": "make scratch18 negative: from now on blanks count as part of the name",
   "unit": "game/mainProgram0800"
  },
  "0D1F": {
   "line": "previous character of the field",
   "unit": "game/mainProgram0800"
  },
  "0D20": {
   "line": "until the whole field has been scanned",
   "unit": "game/mainProgram0800"
  },
  "0D22": {
   "block": [
    "Second name, left aligned: columns 11..20, forwards through the field, with the same blank-squeezing rule -",
    "so this drops the leading blanks.  Column 10 stays a space and separates the two names."
   ],
   "line": "column 11, just past the separating space",
   "unit": "game/mainProgram0800"
  },
  "0D24": {
   "line": "first character of the opponent's name",
   "unit": "game/mainProgram0800"
  },
  "0D26": {
   "line": "nothing placed yet",
   "unit": "game/mainProgram0800"
  },
  "0D28": {
   "line": "one character of the opponent name",
   "unit": "game/mainProgram0800"
  },
  "0D2B": {
   "line": "clear bit 7",
   "unit": "game/mainProgram0800"
  },
  "0D2D": {
   "line": "is it a space?",
   "unit": "game/mainProgram0800"
  },
  "0D2F": {
   "line": "no - place it",
   "unit": "game/mainProgram0800"
  },
  "0D31": {
   "line": "have any characters been placed yet?",
   "unit": "game/mainProgram0800"
  },
  "0D33": {
   "line": "no - this is a leading blank, drop it",
   "unit": "game/mainProgram0800"
  },
  "0D35": {
   "line": "place the character",
   "unit": "game/mainProgram0800"
  },
  "0D38": {
   "line": "one column to the right",
   "unit": "game/mainProgram0800"
  },
  "0D39": {
   "line": "make scratch18 negative so inner blanks survive",
   "unit": "game/mainProgram0800"
  },
  "0D3B": {
   "line": "next character of the field",
   "unit": "game/mainProgram0800"
  },
  "0D3C": {
   "line": "ten characters in the field",
   "unit": "game/mainProgram0800"
  },
  "0D3E": {
   "line": "loop until they are all scanned",
   "unit": "game/mainProgram0800"
  },
  "0D40": {
   "line": "the last byte of the line ($0B93)",
   "unit": "game/mainProgram0800"
  },
  "0D43": {
   "line": "set bit 7 on it...",
   "unit": "game/mainProgram0800"
  },
  "0D45": {
   "line": "...which is how every string in this game ends",
   "unit": "game/mainProgram0800"
  },
  "0D48": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0D49": {
   "routine": [
    "setJoystickRepeatDelay - the joystick auto-repeat used by every menu and by the map scrolling.  The caller sets a direction going and calls this to arm the delay before it happens again: 16 frames for the first repeat, 6 frames afterwards, so a nudge moves one step and a held stick runs.  The raster IRQ clears joyRepeatMask when the stick returns to centre, which forces joyHoldCounter back to 0 and so restores the long first delay.  A is preserved so callers can keep a value in it across the call.",
    "In:  joyRepeatMask ($91C9), joyHoldCounter ($91CA).",
    "Out: joyRepeatTimer (zp_6C) = 16 or 6, joyHoldCounter stepped, joyRepeatMask = $7F, A unchanged.",
    "Called from: $20E7, $2230 and $60AF in this file and $8090/$849E in the setup overlay."
   ],
   "line": "16 frames: the pause before a held direction starts repeating",
   "unit": "game/mainProgram0800"
  },
  "0D4B": {
   "line": "the caller's A must survive",
   "unit": "game/mainProgram0800"
  },
  "0D4C": {
   "line": "count this step",
   "unit": "game/mainProgram0800"
  },
  "0D4F": {
   "line": "read the counter back",
   "unit": "game/mainProgram0800"
  },
  "0D52": {
   "line": "the IRQ clears this mask to 0 while the stick is centred, which zeroes the counter",
   "unit": "game/mainProgram0800"
  },
  "0D55": {
   "line": "so the counter only grows while the stick is held",
   "unit": "game/mainProgram0800"
  },
  "0D58": {
   "line": "was this the first step after centring?",
   "unit": "game/mainProgram0800"
  },
  "0D5A": {
   "line": "yes - keep the long 16-frame delay",
   "unit": "game/mainProgram0800"
  },
  "0D5C": {
   "line": "no - 6 frames between repeats",
   "unit": "game/mainProgram0800"
  },
  "0D5E": {
   "line": "arm the countdown; the raster IRQ decrements it every frame",
   "unit": "game/mainProgram0800"
  },
  "0D60": {
   "line": "$7F lets the counter run again...",
   "unit": "game/mainProgram0800"
  },
  "0D62": {
   "line": "...until the IRQ sees the stick centred and clears the mask",
   "unit": "game/mainProgram0800"
  },
  "0D65": {
   "line": "give the caller its A back",
   "unit": "game/mainProgram0800"
  },
  "0D66": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0D67": {
   "block": [
    "keyMatrixCodeTable - the whole C64 keyboard as 64 bytes, indexed by (port A row) * 8 + (port B column bit),",
    "which is exactly the order scanKeyboard finds a key in.  Ordinary keys are their ASCII code with bit 7 set",
    "(so 'A' is $C1 and '3' is $B3), the four function keys are 0-3 because the game uses them as screen numbers",
    "(F1 = 0 map, F3 = 1 missile, F5 = 2 drone, F7 = 3 menu), and the control keys are ASCII control codes with",
    "bit 7 set: DEL $88, RETURN $8D, CRSR DOWN $8B, HOME $8C, CRSR RIGHT $8E, RUN/STOP $93, left arrow $9B,",
    "pound $83.  Two magic entries: $FE marks the three modifier cells (both SHIFTs and the Commodore key), which",
    "the scanner tests separately and skips here, and $FF marks CTRL, which makes the scan report 'no key' - the",
    "game has no use for CTRL and treats holding it as a way to block the keyboard."
   ],
   "line": "row 0 (PA0): DEL, RETURN, CRSR RIGHT, F7=3, F1=0, F3=1, F5=2, CRSR DOWN",
   "unit": "game/mainProgram0800"
  },
  "0D6F": {
   "line": "row 1 (PA1): 3 W A 4 Z S E, then LEFT SHIFT ($FE = handled separately)",
   "unit": "game/mainProgram0800"
  },
  "0D77": {
   "line": "row 2 (PA2): 5 R D 6 C F T X",
   "unit": "game/mainProgram0800"
  },
  "0D7F": {
   "line": "row 3 (PA3): 7 Y G 8 B H U V",
   "unit": "game/mainProgram0800"
  },
  "0D87": {
   "line": "row 4 (PA4): 9 I J 0 M K O N",
   "unit": "game/mainProgram0800"
  },
  "0D8F": {
   "line": "row 5 (PA5): + P L - . : @ ,",
   "unit": "game/mainProgram0800"
  },
  "0D97": {
   "line": "row 6 (PA6): pound, *, ;, HOME, RIGHT SHIFT ($FE), =, up arrow, /",
   "unit": "game/mainProgram0800"
  },
  "0D9F": {
   "line": "row 7 (PA7): 1, left arrow, CTRL ($FF = report no key), 2, SPACE, C= ($FE), Q, RUN/STOP",
   "unit": "game/mainProgram0800"
  },
  "0DA7": {
   "block": [
    "shiftedKeyTable - what a key produces when SHIFT is held, for the sixteen codes $AC-$BB (',' '-' '.' '/',",
    "the digits '0'-'9', ':' and ';'); pollKeyboardEvent indexes it with keycode - $AC.  The digits give the",
    "usual C64 symbols !\"#$%&'() and shifted '0' stays '0', ',' '.' '/' become '<' '>' '?' and '-' stays '-'.",
    "The last two entries repeat those of '8' and '9', so shifted ':' and ';' come out as '(' and ')' instead of",
    "the brackets a C64 would give - the game's font has no square brackets.  Letters are not in the table: SHIFT",
    "does not change them, the font has only one case."
   ],
   "line": "shifted , - . / 0 1 2 3  ->  < - > ? 0 ! \" #",
   "unit": "game/mainProgram0800"
  },
  "0DAF": {
   "line": "shifted 4 5 6 7 8 9 : ;  ->  $ % & ' ( ) ( )",
   "unit": "game/mainProgram0800"
  },
  "0DB7": {
   "routine": [
    "scanKeyboard - the game's own keyboard matrix scanner; there is no KERNAL to ask, since the ROMs are banked out.  It is not called directly: initCommModule patches its address into the comm module's key vector ($E013/$E014), and pollKeyboardEvent reaches it once a frame through the module's $E015 entry, which lets the modem driver steal keys for its Commodore-key hot keys first.",
    "The scan first makes sure no joystick is confusing the matrix (a stick in port 1 pulls column lines low, one in port 2 pulls row lines low, and either looks exactly like a key), then tests the two SHIFT keys and the Commodore key as modifiers, and finally walks rows 7..0 and columns 7..0 and translates the first key it finds through keyMatrixCodeTable.  Only one key is ever reported - the game needs no rollover.",
    "In:  CIA1 ports; the row masks at D_2F8D ($2F8D) and the column bit masks at spriteBitMasks ($2F85).",
    "Out: A = key code or $FF for none, Y = $80 when a SHIFT is held (else 0), X = $80 when the Commodore key is held (else 0).  It self-modifies the operands at $0E2B, $0E3A, $0E41 and $0E43, and leaves CIA1_DDRA = $FF, CIA1_DDRB = $00 and the last row mask in CIA1_PRA - the raster IRQ relies on that to read joystick 2 immediately afterwards.",
    "Called from: game/modemDriverE000 and game/trainerAiE000 through the patched vector at $E013 ($E012/$E015)."
   ],
   "line": "0 into both result operands below",
   "unit": "game/mainProgram0800"
  },
  "0DB9": {
   "line": "the LDY operand at $0E41: assume no SHIFT is held",
   "unit": "game/mainProgram0800"
  },
  "0DBC": {
   "line": "the LDX operand at $0E43: assume the Commodore key is not held",
   "unit": "game/mainProgram0800"
  },
  "0DBF": {
   "line": "port B all inputs - it reads the eight key columns",
   "unit": "game/mainProgram0800"
  },
  "0DC2": {
   "line": "$FF into port A and its direction register",
   "unit": "game/mainProgram0800"
  },
  "0DC4": {
   "line": "port A all outputs - it selects the key rows (a 0 bit selects a row)",
   "unit": "game/mainProgram0800"
  },
  "0DC7": {
   "line": "no row selected at all",
   "unit": "game/mainProgram0800"
  },
  "0DCA": {
   "line": "with no row selected every column should read high",
   "unit": "game/mainProgram0800"
  },
  "0DCD": {
   "line": "it does - the matrix is undisturbed",
   "unit": "game/mainProgram0800"
  },
  "0DCF": {
   "line": "a column is being pulled low with no row selected: that is a joystick in port 1, so report 'no key'",
   "unit": "game/mainProgram0800"
  },
  "0DD2": {
   "line": "port A should also read back the $FF it is driving",
   "unit": "game/mainProgram0800"
  },
  "0DD5": {
   "line": "it does not: a joystick in port 2 is pulling a row line low, so the scan would lie - report 'no key'",
   "unit": "game/mainProgram0800"
  },
  "0DD7": {
   "line": "X is still 0: select all eight rows at once",
   "unit": "game/mainProgram0800"
  },
  "0DDA": {
   "line": "if any key at all is down, some column now reads low",
   "unit": "game/mainProgram0800"
  },
  "0DDD": {
   "line": "nothing is down - report 'no key' without scanning",
   "unit": "game/mainProgram0800"
  },
  "0DDF": {
   "block": [
    "The three modifier keys are wired into the matrix like any other key, so they have to be tested by name",
    "before the general scan, which skips their cells ($FE in keyMatrixCodeTable)."
   ],
   "line": "row mask $BF = PA6 low (the row with RIGHT SHIFT)",
   "unit": "game/mainProgram0800"
  },
  "0DE1": {
   "line": "column bit 4 = RIGHT SHIFT",
   "unit": "game/mainProgram0800"
  },
  "0DE3": {
   "line": "is it down?",
   "unit": "game/mainProgram0800"
  },
  "0DE6": {
   "line": "yes - record a shift",
   "unit": "game/mainProgram0800"
  },
  "0DE8": {
   "line": "row mask $FD = PA1 low (the row with LEFT SHIFT)",
   "unit": "game/mainProgram0800"
  },
  "0DEA": {
   "line": "column bit 7 = LEFT SHIFT",
   "unit": "game/mainProgram0800"
  },
  "0DEC": {
   "line": "is that one down?",
   "unit": "game/mainProgram0800"
  },
  "0DEF": {
   "line": "neither shift is down",
   "unit": "game/mainProgram0800"
  },
  "0DF1": {
   "line": "$80 = shift held",
   "unit": "game/mainProgram0800"
  },
  "0DF3": {
   "line": "patch it into the LDY at $0E40, which returns the shift flag in Y",
   "unit": "game/mainProgram0800"
  },
  "0DF6": {
   "line": "row mask $7F = PA7 low (the row with the Commodore key)",
   "unit": "game/mainProgram0800"
  },
  "0DF8": {
   "line": "column bit 5 = the Commodore key",
   "unit": "game/mainProgram0800"
  },
  "0DFA": {
   "line": "is it down?",
   "unit": "game/mainProgram0800"
  },
  "0DFD": {
   "line": "no",
   "unit": "game/mainProgram0800"
  },
  "0DFF": {
   "line": "$80 = Commodore key held",
   "unit": "game/mainProgram0800"
  },
  "0E01": {
   "line": "patch it into the LDX at $0E42, which returns that flag in X",
   "unit": "game/mainProgram0800"
  },
  "0E04": {
   "block": [
    "The scan proper: eight rows, eight columns each, from the top down, stopping at the first key that is down.",
    "So when two keys are held the one with the higher row (and within a row the higher column) wins."
   ],
   "line": "start at row 7",
   "unit": "game/mainProgram0800"
  },
  "0E06": {
   "line": "start at column 7",
   "unit": "game/mainProgram0800"
  },
  "0E08": {
   "line": "the row select pattern for this row (one bit low)",
   "unit": "game/mainProgram0800"
  },
  "0E0B": {
   "line": "select it on port A",
   "unit": "game/mainProgram0800"
  },
  "0E0E": {
   "line": "read the columns...",
   "unit": "game/mainProgram0800"
  },
  "0E11": {
   "line": "...and read them again",
   "unit": "game/mainProgram0800"
  },
  "0E14": {
   "line": "until two reads agree - cheap debounce against key bounce and the raster's noise",
   "unit": "game/mainProgram0800"
  },
  "0E16": {
   "line": "test the single column bit for this column",
   "unit": "game/mainProgram0800"
  },
  "0E19": {
   "line": "zero means that key is down",
   "unit": "game/mainProgram0800"
  },
  "0E1B": {
   "line": "next column",
   "unit": "game/mainProgram0800"
  },
  "0E1C": {
   "line": "until all eight of this row are done",
   "unit": "game/mainProgram0800"
  },
  "0E1E": {
   "line": "next row",
   "unit": "game/mainProgram0800"
  },
  "0E1F": {
   "line": "until all eight rows are done",
   "unit": "game/mainProgram0800"
  },
  "0E21": {
   "line": "nothing found anywhere - report 'no key'",
   "unit": "game/mainProgram0800"
  },
  "0E23": {
   "line": "the row number...",
   "unit": "game/mainProgram0800"
  },
  "0E24": {
   "line": "...times two...",
   "unit": "game/mainProgram0800"
  },
  "0E25": {
   "line": "...times four...",
   "unit": "game/mainProgram0800"
  },
  "0E26": {
   "line": "...times eight = the start of that row in the code table",
   "unit": "game/mainProgram0800"
  },
  "0E27": {
   "line": "patch the column number into the ORA operand at $0E2B",
   "unit": "game/mainProgram0800"
  },
  "0E2A": {
   "line": "operand patched just above: row * 8 + column = the index into keyMatrixCodeTable",
   "unit": "game/mainProgram0800"
  },
  "0E2C": {
   "line": "save the row number in the LDX operand at $0E3A, because X is about to be reused as the table index",
   "unit": "game/mainProgram0800"
  },
  "0E2F": {
   "line": "index the table",
   "unit": "game/mainProgram0800"
  },
  "0E30": {
   "line": "the key code for the key that is down",
   "unit": "game/mainProgram0800"
  },
  "0E33": {
   "line": "$FE and $FF are the two magic entries",
   "unit": "game/mainProgram0800"
  },
  "0E35": {
   "line": "a real key code - return it",
   "unit": "game/mainProgram0800"
  },
  "0E37": {
   "line": "$FF = CTRL: pretend nothing is pressed",
   "unit": "game/mainProgram0800"
  },
  "0E39": {
   "line": "$FE = one of the modifier cells: restore the row number (operand patched at $0E2C)...",
   "unit": "game/mainProgram0800"
  },
  "0E3B": {
   "line": "...and carry on scanning from the next column",
   "unit": "game/mainProgram0800"
  },
  "0E3E": {
   "line": "$FF = no key",
   "unit": "game/mainProgram0800"
  },
  "0E40": {
   "line": "operand patched at $0DB9/$0DF3: Y = $80 when a shift is held, 0 otherwise",
   "unit": "game/mainProgram0800"
  },
  "0E42": {
   "line": "operand patched at $0DBC/$0E01: X = $80 when the Commodore key is held, 0 otherwise",
   "unit": "game/mainProgram0800"
  },
  "0E44": {
   "line": "back to pollKeyboardEvent (through the comm module)",
   "unit": "game/mainProgram0800"
  },
  "0E45": {
   "routine": [
    "testKeyMask - tests one named key for scanKeyboard: it selects the row given in Y, reads the columns twice until two reads agree, and masks out the single column bit the caller passed in A.  The mask is not kept in a register but patched into the AND operand at $0E54, so both registers stay free.",
    "In:  Y = the port A row select pattern (one bit low), A = the column bit mask.",
    "Out: Z = 1 when that key is down; A = the masked port B reading; the operand at $0E54 patched.",
    "Called from: scanKeyboard $0DE3 (right shift), $0DEC (left shift) and $0DFA (Commodore key)."
   ],
   "line": "patch the caller's column mask into the AND at $0E53",
   "unit": "game/mainProgram0800"
  },
  "0E48": {
   "line": "select the row",
   "unit": "game/mainProgram0800"
  },
  "0E4B": {
   "line": "read the columns...",
   "unit": "game/mainProgram0800"
  },
  "0E4E": {
   "line": "...and read them again",
   "unit": "game/mainProgram0800"
  },
  "0E51": {
   "line": "until two reads agree",
   "unit": "game/mainProgram0800"
  },
  "0E53": {
   "line": "keep only the column of the key we are asking about (operand patched at $0E45)",
   "unit": "game/mainProgram0800"
  },
  "0E55": {
   "line": "Z = 1 means the key is down",
   "unit": "game/mainProgram0800"
  },
  "0E56": {
   "routine": [
    "pollKeyboardEvent - the keyboard front end, run once per frame from the raster IRQ (and only from there).  It asks the comm module for a key - which reaches scanKeyboard, after the modem driver has had its chance to swallow Commodore-key hot keys - and turns the raw 'this key is down' answer into events: a key that is the same as last frame is ignored until the 20-frame repeat timer runs out, so a held key repeats about three times a second.",
    "The four function keys (codes 0-3) are screen requests: they go only into requestedScreenKey (zp_66) and explicitly clear newKeyEvent, because nothing may type them into a text field.  Every other key (all codes have bit 7 set) becomes a newKeyEvent for the line editors, a lastTypedKey for the menus, and is also written into requestedScreenKey, which cancels any stale F-key request.  When a shift is held the sixteen punctuation and digit codes $AC-$BB are replaced from shiftedKeyTable.",
    "In:  the comm module key entry $E015 -> scanKeyboard (A = code, Y = shift, X = Commodore); prevKeyCode, keyRepeatTimer.",
    "Out: rawKeyCode/shiftHeldFlag/commodoreHeldFlag ($92A0/$92E4/$92E5) = the raw scan, newKeyEvent ($90EA), prevKeyCode ($90E9), lastTypedKey ($9248), requestedScreenKey (zp_66) and keyRepeatTimer (zp_70).",
    "Called from: rasterIrqHandler $11E7 (only when the input lockout timer has expired)."
   ],
   "line": "read a key through the comm module, which vectors on to scanKeyboard",
   "unit": "game/mainProgram0800"
  },
  "0E59": {
   "line": "keep the raw code the scanner returned",
   "unit": "game/mainProgram0800"
  },
  "0E5C": {
   "line": "Y = $80 when a shift was held during that scan",
   "unit": "game/mainProgram0800"
  },
  "0E5F": {
   "line": "X = $80 when the Commodore key was held",
   "unit": "game/mainProgram0800"
  },
  "0E62": {
   "line": "$FF = no key is down",
   "unit": "game/mainProgram0800"
  },
  "0E64": {
   "line": "nothing pressed - clear the pending state",
   "unit": "game/mainProgram0800"
  },
  "0E66": {
   "line": "the same key as the previous frame?",
   "unit": "game/mainProgram0800"
  },
  "0E69": {
   "line": "no - a genuinely new keypress",
   "unit": "game/mainProgram0800"
  },
  "0E6B": {
   "line": "the repeat countdown the IRQ decrements every frame",
   "unit": "game/mainProgram0800"
  },
  "0E6D": {
   "line": "still running: the key is being held, do not report it again yet",
   "unit": "game/mainProgram0800"
  },
  "0E6F": {
   "line": "20 frames until a held key repeats",
   "unit": "game/mainProgram0800"
  },
  "0E71": {
   "line": "arm the repeat timer",
   "unit": "game/mainProgram0800"
  },
  "0E73": {
   "line": "publish the key as this frame's event",
   "unit": "game/mainProgram0800"
  },
  "0E76": {
   "line": "remember it so the next frame can tell 'held' from 'pressed again'",
   "unit": "game/mainProgram0800"
  },
  "0E79": {
   "line": "codes below $80 are only the four function keys",
   "unit": "game/mainProgram0800"
  },
  "0E7B": {
   "line": "F1/F3/F5/F7 are handled separately below",
   "unit": "game/mainProgram0800"
  },
  "0E7D": {
   "line": "everything else is a typed character: the menus read it here",
   "unit": "game/mainProgram0800"
  },
  "0E80": {
   "line": "and into the screen-request byte, where a printable code reads as 'no screen change wanted' and so cancels a stale F-key request",
   "unit": "game/mainProgram0800"
  },
  "0E82": {
   "line": "was a shift held?",
   "unit": "game/mainProgram0800"
  },
  "0E85": {
   "line": "no - the code stands as it is",
   "unit": "game/mainProgram0800"
  },
  "0E87": {
   "line": "prepare an exact subtraction",
   "unit": "game/mainProgram0800"
  },
  "0E88": {
   "line": "$AC = ',' is the first code that has a shifted form",
   "unit": "game/mainProgram0800"
  },
  "0E8A": {
   "line": "below the table - letters and control codes are unaffected",
   "unit": "game/mainProgram0800"
  },
  "0E8C": {
   "line": "sixteen entries, $AC-$BB",
   "unit": "game/mainProgram0800"
  },
  "0E8E": {
   "line": "above the table - unaffected",
   "unit": "game/mainProgram0800"
  },
  "0E90": {
   "line": "the difference is the table index",
   "unit": "game/mainProgram0800"
  },
  "0E91": {
   "line": "the shifted form of that key",
   "unit": "game/mainProgram0800"
  },
  "0E94": {
   "line": "replace this frame's event...",
   "unit": "game/mainProgram0800"
  },
  "0E97": {
   "line": "...and the typed character with it",
   "unit": "game/mainProgram0800"
  },
  "0E9A": {
   "line": "back to the IRQ",
   "unit": "game/mainProgram0800"
  },
  "0E9B": {
   "line": "hold the function key number (0-3) in Y",
   "unit": "game/mainProgram0800"
  },
  "0E9C": {
   "line": "$FF = no character event...",
   "unit": "game/mainProgram0800"
  },
  "0E9E": {
   "line": "...so a screen key can never be typed into a text field",
   "unit": "game/mainProgram0800"
  },
  "0EA1": {
   "line": "the screen the player asked for: 0 map, 1 missile, 2 drone, 3 menu",
   "unit": "game/mainProgram0800"
  },
  "0EA3": {
   "line": "back to the IRQ",
   "unit": "game/mainProgram0800"
  },
  "0EA4": {
   "line": "$FF = none",
   "unit": "game/mainProgram0800"
  },
  "0EA6": {
   "line": "no screen change pending",
   "unit": "game/mainProgram0800"
  },
  "0EA8": {
   "line": "and no previous key, so the very next press counts as new",
   "unit": "game/mainProgram0800"
  },
  "0EAB": {
   "line": "$FF = nothing to report",
   "unit": "game/mainProgram0800"
  },
  "0EAD": {
   "line": "clear this frame's key event",
   "unit": "game/mainProgram0800"
  },
  "0EB0": {
   "line": "back to the IRQ",
   "unit": "game/mainProgram0800"
  },
  "0EB1": {
   "routine": [
    "checkFirePressed - the game's 'has the player clicked?' primitive; almost every interactive loop spins on it with BNE.  It reports a click only on the edge: the button must have been seen released since the last accepted click (fireButtonArmed) and must be down now.  An accepted click arms a 10-frame debounce and plays the click sound.  It also refuses clicks when the outgoing command ring has less than five bytes free, which throttles the player instead of overflowing the link.",
    "In:  joyFireUp (zp_5F, 0 = button down), fireButtonArmed (zp_79), fireDebounceTimer (zp_6D), the command ring indices through isCmdRingNearlyFull.",
    "Out: A = 0 (Z=1) for a fresh click, A = 1 otherwise; fireDebounceTimer = 10 and sound 1 queued on a click; fireButtonArmed maintained.",
    "Called from: about a dozen loops - $0ED5, $1470, $21E7, $5E68, $608E, $6226, $63FB here, $7751/$806F/$8127/$84A4/$878D in the overlays and $C865 in the text engine."
   ],
   "line": "C=1 when fewer than five bytes are free in the outgoing command ring",
   "unit": "game/mainProgram0800"
  },
  "0EB4": {
   "line": "the link cannot take another order - swallow the click",
   "unit": "game/mainProgram0800"
  },
  "0EB6": {
   "line": "frames left of the debounce after the last accepted click",
   "unit": "game/mainProgram0800"
  },
  "0EB8": {
   "line": "still counting down - ignore the button",
   "unit": "game/mainProgram0800"
  },
  "0EBA": {
   "line": "0 = the button is down right now",
   "unit": "game/mainProgram0800"
  },
  "0EBC": {
   "line": "the latch that says the button has been released since the last click",
   "unit": "game/mainProgram0800"
  },
  "0EBE": {
   "line": "either of them non-zero means there is something to look at",
   "unit": "game/mainProgram0800"
  },
  "0EC0": {
   "line": "A = 1: no click this time",
   "unit": "game/mainProgram0800"
  },
  "0EC2": {
   "line": "callers loop on BNE, so this keeps them waiting",
   "unit": "game/mainProgram0800"
  },
  "0EC3": {
   "line": "is the button actually down?",
   "unit": "game/mainProgram0800"
  },
  "0EC5": {
   "line": "no - it has just been released, so re-arm the latch",
   "unit": "game/mainProgram0800"
  },
  "0EC7": {
   "line": "10 frames...",
   "unit": "game/mainProgram0800"
  },
  "0EC9": {
   "line": "...during which further clicks are ignored (contact bounce and human double taps)",
   "unit": "game/mainProgram0800"
  },
  "0ECB": {
   "line": "sound 1 = the UI click",
   "unit": "game/mainProgram0800"
  },
  "0ECD": {
   "line": "play it so the player hears the press",
   "unit": "game/mainProgram0800"
  },
  "0ED0": {
   "line": "A = 0: a fresh click, and 0 also disarms the latch below",
   "unit": "game/mainProgram0800"
  },
  "0ED2": {
   "line": "store 0 (click taken) or the button-up state (latch armed)",
   "unit": "game/mainProgram0800"
  },
  "0ED4": {
   "line": "A tells the caller which of the two happened",
   "unit": "game/mainProgram0800"
  },
  "0ED5": {
   "routine": [
    "checkFirePressedInteractive - checkFirePressed plus a 'may the player act at all?' gate, used by the loops that turn a click into an order.  It reports a click only when a game film is not being watched (filmPlaybackMode bit 7) and the game has not already ended (gameOverFlags bit 6, which is set when a game is over or aborted).  Phase 1 called this checkFirePressedNotSolo, which is wrong: the solo trainer leaves filmPlaybackMode clear and its player clicks normally.",
    "In:  the same joystick state as checkFirePressed, plus filmPlaybackMode ($0B9B) and gameOverFlags ($923F).",
    "Out: A = 0 only for an accepted click during live play; non-zero otherwise.",
    "Called from: $2076 here and $75FA/$8043 in the $6F00 overlays."
   ],
   "line": "the ordinary edge-detected click",
   "unit": "game/mainProgram0800"
  },
  "0ED8": {
   "line": "no click - return non-zero without looking further",
   "unit": "game/mainProgram0800"
  },
  "0EDA": {
   "line": "watching a game film?",
   "unit": "game/mainProgram0800",
   "block": [
    "Shared entry: isPlayerInputAllowed ($0EE5) jumps in here with A = 0 to ask the same question without",
    "testing the button."
   ]
  },
  "0EDD": {
   "line": "then the player is a spectator: the click does not count (A is negative, i.e. non-zero)",
   "unit": "game/mainProgram0800"
  },
  "0EDF": {
   "line": "the flags set when the game has ended or been aborted",
   "unit": "game/mainProgram0800"
  },
  "0EE2": {
   "line": "bit 6 blocks all further player commands; clear = the click is accepted (A = 0)",
   "unit": "game/mainProgram0800"
  },
  "0EE4": {
   "line": "Z=1 only for an accepted click in a live game",
   "unit": "game/mainProgram0800"
  },
  "0EE5": {
   "routine": [
    "isPlayerInputAllowed - the gate half of checkFirePressedInteractive without the button test: it jumps into that routine at $0EDA with A = 0, so it answers 'may the local player still act?' - Z=1 when no film is being watched and the game has not ended.  Phase 1 named it getSoloOrFlag923F and thought it was unreferenced; the REPAIR tab uses it to decide whether the selected system may be repaired.",
    "In:  filmPlaybackMode ($0B9B), gameOverFlags ($923F).",
    "Out: Z=1 (A = 0) when the player may act.",
    "Called from: $658B, the REPAIR tab menu loop."
   ],
   "line": "A = 0 stands in for 'the button was pressed'",
   "unit": "game/mainProgram0800"
  },
  "0EE7": {
   "line": "run only the film/game-over test of checkFirePressedInteractive",
   "unit": "game/mainProgram0800"
  },
  "0EEA": {
   "routine": [
    "isFireHeld - the level-triggered version of checkFirePressed: no ring-buffer check, no debounce reload and no click sound, so it keeps answering 0 for as long as the button is held (once it has been seen released).  Used by the two loops where an action has to run while the button is down - the map screen's group box at $2376 and the console at $407A.",
    "In:  joyFireUp (zp_5F), fireButtonArmed (zp_79), fireDebounceTimer (zp_6D).",
    "Out: A = 0 while the button is held, A = 1 otherwise; fireButtonArmed re-armed when the button is up.",
    "Called from: $2376 and $407A."
   ],
   "line": "still inside the debounce of an earlier click?",
   "unit": "game/mainProgram0800"
  },
  "0EEC": {
   "line": "then say 'not held'",
   "unit": "game/mainProgram0800"
  },
  "0EEE": {
   "line": "0 = the button is down",
   "unit": "game/mainProgram0800"
  },
  "0EF0": {
   "line": "the released-since-last-time latch",
   "unit": "game/mainProgram0800"
  },
  "0EF2": {
   "line": "something to look at",
   "unit": "game/mainProgram0800"
  },
  "0EF4": {
   "line": "A = 1: the button is not held (or has never been released)",
   "unit": "game/mainProgram0800"
  },
  "0EF6": {
   "line": "back to the caller",
   "unit": "game/mainProgram0800"
  },
  "0B14": {
   "unit": "game/mainProgram0800",
   "line": "the kind of opponent the previous game had, back into A"
  },
  "0B4B": {
   "unit": "game/mainProgram0800",
   "line": "destination low byte $00..."
  },
  "0B4D": {
   "unit": "game/mainProgram0800",
   "line": "...high byte $EC: the trainer tail loads at $EC00"
  },
  "0B81": {
   "unit": "game/mainProgram0800",
   "line": "more of the 21 blanks the owner-name line starts life as"
  },
  "0B87": {
   "unit": "game/mainProgram0800",
   "line": "more blanks of the owner-name line"
  },
  "0B89": {
   "unit": "game/mainProgram0800",
   "line": "more blanks of the owner-name line"
  },
  "0B91": {
   "unit": "game/mainProgram0800",
   "line": "the last blanks before the terminator position"
  },
  "0BAA": {
   "unit": "game/mainProgram0800",
   "line": "characters 2-5 of the printable map name"
  },
  "0BB0": {
   "unit": "game/mainProgram0800",
   "line": "the rest of the saved copy of the map name and its terminator"
  }
 }
}