{
 "chunk": "game_modemDriverE000_E736",
 "unit": "game/modemDriverE000",
 "unitLabels": {
  "game/modemDriverE000": {
   "EAB8": "zeroRunLength",
   "EAB9": "rxPayloadRemaining",
   "EABA": "packetByteCounter",
   "EABB": "rxAckControlByte",
   "EABC": "rxDataControlByte",
   "EABD": "rxSeqCounter",
   "EABE": "txSeqCounter",
   "EABF": "resendRequest",
   "EAC0": "ackPending",
   "EAC1": "sendFfPending",
   "EAC2": "sendZeroPending",
   "EAC3": "txRingIndex",
   "EAC4": "txRingTailIndex",
   "EAC5": "txNextFreeIndex",
   "EAC6": "txOldestUnackedIndex",
   "EAEA": "packetLength",
   "EAEC": "packetControlByte",
   "EAED": "packetPayload",
   "EAFE": "packetIndexSave",
   "EAFF": "rxLengthCodeByte",
   "EB01": "modemDelayCounter",
   "EB02": "lastModemChar",
   "E755": "trackZerosReturn",
   "E765": "checkConnectionPhase",
   "E76B": "serviceLinkTick",
   "E775": "storeInFlightFlags",
   "E77B": "checkTerminalMode",
   "E78A": "testCarrierPresent",
   "E78C": "drainHostOutRing",
   "E791": "enterTerminalMode",
   "E799": "serviceModemInput",
   "E7A7": "checkConnectSpeed",
   "E7B2": "switchTo1200Baud",
   "E7B5": "substituteSpaceForCr",
   "E7B7": "rememberModemChar",
   "E7C0": "tickGuardDelay",
   "E7C6": "storeGuardDelay",
   "E7C9": "modemTickReturn",
   "E7E0": "keepReplyTimer",
   "E7E3": "tickModemReplyTimer",
   "E7EA": "countReplyTimerDown",
   "E7FF": "dispatchCommState",
   "E80A": "drainRxRingLoop",
   "E80F": "restartByteSync",
   "E81D": "queueSyncByte",
   "E828": "continueByteSync",
   "E832": "awaitPeerSyncZero",
   "E83D": "answerSyncWithFf",
   "E844": "linkTickReturn",
   "E845": "awaitPeerSyncFf",
   "E860": "servicePacketTransmit",
   "E879": "queueRawSyncByte",
   "E87F": "checkPacketInFlight",
   "E893": "resendOldestMessage",
   "E89E": "buildNextMessage",
   "E8CC": "copyMessageByteLoop",
   "E8D9": "finishMessageBuild",
   "E8DF": "claimOutstandingSlot",
   "E8EB": "storeNextFreeIndex",
   "E8F2": "packetPhaseReturn",
   "E8F3": "sendAckIfPending",
   "E90C": "sendBuiltPacket",
   "E924": "transmitPacketLoop",
   "E93B": "checksumLoop",
   "E95A": "copyRingByteLoop",
   "E960": "storePacketByte",
   "E97B": "advanceTxRingIndex",
   "E982": "storeTxRingIndex",
   "E985": "txRingReturn",
   "E997": "countStalledPoll",
   "E99B": "storeStallCount",
   "E9A2": "unwindAndResync",
   "E9A7": "noteAckRequest",
   "E9C3": "checkForFfSync",
   "E9CC": "lengthCodeSearchLoop",
   "E9D6": "beginPacketBody",
   "E9D9": "awaitWholePacket",
   "E9E9": "copyPacketByteLoop",
   "EA1D": "sendDriveByteLoop",
   "EA29": "sendFinalDriveByte",
   "EA2E": "handleAckOrDataPacket",
   "EA3D": "checkOutstandingSequence",
   "EA5E": "handleDataPacket",
   "EA65": "acceptDataPacket",
   "EA8E": "deliverPayloadLoop",
   "EC5A": "storeMirrorMode",
   "EC6F": "mirrorCellPairLoop",
   "EC83": "stepBackwardPointer",
   "EC89": "decBackwardPointer",
   "ECBE": "mirrorEntryLoop",
   "ECCC": "restoreCoordFlagBits",
   "ECDE": "lookupLowRangeCode",
   "ECE7": "checkTerrainCodeRange",
   "ECEE": "lookupTerrainCode",
   "ECF2": "mirrorTerrainReturn",
   "ED18": "advanceTemplateSource",
   "ED2C": "copyTemplatePages",
   "ED30": "templateCopyLoad",
   "ED33": "templateCopyStore",
   "ED42": "templateAlreadyLoaded",
   "ED4A": "copyTrainerNameLoop",
   "ED54": "handshakeWithPeer",
   "ED6F": "copyEnemyNameLoop",
   "ED78": "sendSettingsMessage",
   "EDD3": "copyOwnNameHalfLoop",
   "EDEA": "restoreNameOffset",
   "EE28": "finaliseRecyclerSideLoop",
   "EE41": "nextRecyclerSide",
   "EE54": "readScoreBlockLoop",
   "EE8C": "copySnapshotPageLoop",
   "EEBE": "beginDisplayToggle",
   "EED4": "queueDisplaySetMessage",
   "EEED": "copyModeNameLoop"
  }
 },
 "notes": {
  "E736": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushArqRxRingThunk - Three-byte trampoline: JMP pushArqRxRing ($E49E).  It gives the packet layer a fixed address for the game-facing inbound ring, the same address in both $E000 modules.",
    "In:  A = byte to hand to the game",
    "Out: byte appended to the 18-byte inbound command ring $E45C, count $E459 bumped",
    "Called from: deliverPacketPayload ($EA91)"
   ],
   "line": "JMP $E49E - deliver one decoded command byte into the game's inbound ring $E45C"
  },
  "E739": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popUartRxRingThunk - Three-byte trampoline: JMP popUartRxRing ($E4D2), the packet layer's only door into the raw 20-byte UART receive ring.",
    "In:  none",
    "Out: A = received byte (A = 0 and C = 1 when the ring is empty), X clobbered",
    "Called from: receiveByteTrackZeros ($E743) and the packet body copier ($E9E9)"
   ],
   "line": "JMP $E4D2 - take the next raw byte the software UART has assembled"
  },
  "E73C": {
   "unit": "game/modemDriverE000",
   "routine": [
    "queueByteForTransmit - Pushes one byte into the 22-byte raw UART transmit ring $E417 and counts it in $E0A5, the number of characters the NMI transmitter still owes.  The NMI decrements $E0A5 as each character finishes ($E6B0) and zeroes it if the ring unexpectedly runs dry ($E634).",
    "In:  A = byte to transmit",
    "Out: $E417 ring, $E414 count, $E416 write index, $E0A5 incremented; A preserved, X clobbered",
    "Called from: sendNextModemCommandChar ($E7D2), beginByteSyncPhase ($E81D), runPacketPhase ($E87C) and sendPacket ($E92A)"
   ],
   "line": "push the byte into the 22-byte UART transmit ring $E417; pushUartTxRing silently drops it when the ring already holds 22"
  },
  "E73F": {
   "unit": "game/modemDriverE000",
   "line": "count one more character owed to the NMI transmitter - note this happens even when the push above overflowed, but the NMI resets $E0A5 to 0 when the ring runs dry"
  },
  "E742": {
   "unit": "game/modemDriverE000",
   "line": "A still holds the byte, so the caller can test what it just sent"
  },
  "E743": {
   "unit": "game/modemDriverE000",
   "routine": [
    "receiveByteTrackZeros - Pops one byte from the raw UART receive ring and maintains zeroRunLength ($EAB8), the count of consecutive $00 bytes on the line.  Any non-$00 byte resets the run; an empty ring leaves it untouched.  The sync and packet layers use the run length to tell a single $00 (the peer's 'resend' nak) from a stream of $00s (a dead or unsynchronised line).",
    "In:  the UART receive ring $E430/$E42D, zeroRunLength $EAB8",
    "Out: A = byte ($00 when the ring was empty), X = the new run length, C = 1 when the ring was empty, $EAB8 updated",
    "Called from: beginByteSyncPhase ($E837, $E84C) and receivePacket ($E9AF)"
   ],
   "line": "pop one byte; popUartRxRing returns A = 0 and C = 1 when the ring is empty"
  },
  "E746": {
   "unit": "game/modemDriverE000",
   "line": "X = the run of consecutive $00 bytes seen so far"
  },
  "E749": {
   "unit": "game/modemDriverE000",
   "line": "ring was empty: leave the run length alone and return with C = 1 and X = the old count"
  },
  "E74B": {
   "unit": "game/modemDriverE000",
   "line": "provisionally extend the zero run by this byte"
  },
  "E74C": {
   "unit": "game/modemDriverE000",
   "line": "EOR #$00 purely to set Z from the byte just popped"
  },
  "E74E": {
   "unit": "game/modemDriverE000",
   "line": "the byte was $00 - keep the extended run length"
  },
  "E750": {
   "unit": "game/modemDriverE000",
   "line": "any non-$00 byte breaks the run"
  },
  "E752": {
   "unit": "game/modemDriverE000",
   "line": "store the new run length; it is also returned in X"
  },
  "E755": {
   "unit": "game/modemDriverE000",
   "line": "return with C still holding the ring-empty flag - none of the instructions above touch it"
  },
  "E756": {
   "unit": "game/modemDriverE000",
   "routine": [
    "runModemStateMachine - The per-frame link service.  It consumes the restart request in $E042, retires a packet once the transmitter has drained, and drives connectionPhase $E040: 0 = idle, 1 = modem command/terminal mode, 2 = byte sync, 3 = packet protocol.  In phase 1 it shuttles characters between the modem and the game's two host character rings, sniffs the modem's verbose result codes so it can switch to 1200 baud on a 'CONNECT 1200', and honours the guard-time delay bytes embedded in the AT strings.  It falls through into sendNextModemCommandChar and from there, once the modem answer timer expires, into runLinkStateMachine.",
    "In:  $E042 restart request, $E040 connection phase, $E03C debounced carrier byte, $E03F modem answer timer, $E048 modem option flags, $E0A4/$E0A5/$E0A6, modemDelayCounter $EB01, lastModemChar $EB02, both host character rings and both UART rings",
    "Out: $E040, $E03F, $E044, $E0A4, $E0A6, $EB01, $EB02, the baud parameters $E056-$E058, the host-input ring $E086 and the UART transmit ring",
    "Called from: serviceCommTick ($E134), i.e. once per raster IRQ through jump-table entry $E000 with X = 0"
   ],
   "block": [
    "Consume the restart request posted by restartUart ($E58B writes 1) or stopCommNmi ($E5C5 writes $FF)."
   ],
   "line": "X = 0, the value used below to clear the request and the connection phase"
  },
  "E758": {
   "unit": "game/modemDriverE000",
   "line": "restart request: 1 = re-evaluate the link, $FF = tear the link down"
  },
  "E75B": {
   "unit": "game/modemDriverE000",
   "line": "no request pending"
  },
  "E75D": {
   "unit": "game/modemDriverE000",
   "line": "consume the request"
  },
  "E760": {
   "unit": "game/modemDriverE000",
   "line": "request was 1: just re-evaluate, keep the current connection phase"
  },
  "E762": {
   "unit": "game/modemDriverE000",
   "line": "request was $FF: force the connection phase back to 0 (link down)"
  },
  "E765": {
   "unit": "game/modemDriverE000",
   "line": "connection phase: 0 idle, 1 modem terminal, 2 byte sync, 3 packet protocol"
  },
  "E768": {
   "unit": "game/modemDriverE000",
   "line": "phase 1-3, or a pending re-evaluation: there is work to do"
  },
  "E76A": {
   "unit": "game/modemDriverE000",
   "line": "idle with nothing requested - leave the link alone"
  },
  "E76B": {
   "unit": "game/modemDriverE000",
   "block": [
    "A message packet counts as 'in flight' until the UART transmitter has emptied its ring."
   ],
   "line": "characters the NMI transmitter still owes"
  },
  "E76E": {
   "unit": "game/modemDriverE000",
   "line": "compare with $E0A4, the 'a message packet is in flight' flag (set to 1 at $E8A8)"
  },
  "E771": {
   "unit": "game/modemDriverE000",
   "line": "transmitter not drained yet - the packet has not left the machine"
  },
  "E773": {
   "unit": "game/modemDriverE000",
   "line": "X = 0",
   "routine": [
    "clearInFlightFlags - Mid-routine entry of runModemStateMachine, also used by the solo trainer build: clears the frame-in-flight flag $E0A4 and the packet-layer send enable $E0A6.  Clearing $E0A4 is what tells runPacketPhase that the packet it queued has physically left the transmitter and a new one may be built.",
    "In:  X = 0",
    "Out: $E0A4 = 0, $E0A6 = 0",
    "Called from: runModemStateMachine by fall-through from $E771; the trainer build of this module also jumps here"
   ]
  },
  "E775": {
   "unit": "game/modemDriverE000",
   "line": "transmitter drained: the packet in flight has physically gone out"
  },
  "E778": {
   "unit": "game/modemDriverE000",
   "line": "and clear the packet-layer send enable $E0A6 - the only write to it in the whole image, which is why the message builder at $E8A8 is unreachable"
  },
  "E77B": {
   "unit": "game/modemDriverE000",
   "block": [
    "Choose between the modem dialogue (terminal mode) and the game link."
   ],
   "line": "1 = the modem command / terminal phase"
  },
  "E77D": {
   "unit": "game/modemDriverE000",
   "line": "compare with the current connection phase"
  },
  "E780": {
   "unit": "game/modemDriverE000",
   "line": "already in terminal mode: go and service the characters flowing to and from the modem"
  },
  "E782": {
   "unit": "game/modemDriverE000",
   "line": "modem answer timer / 'an AT command is in progress' byte ($FF while a command string is queued)"
  },
  "E785": {
   "unit": "game/modemDriverE000",
   "line": "non-zero: a modem dialogue is under way, so make sure we are in terminal mode"
  },
  "E787": {
   "unit": "game/modemDriverE000",
   "line": "test the debounced carrier byte; bit 6 (V) = carrier detect, i.e. CIA2 PB4 read low"
  },
  "E78A": {
   "unit": "game/modemDriverE000",
   "line": "carrier is already up - skip the modem dialogue entirely and run the link state machine"
  },
  "E78C": {
   "unit": "game/modemDriverE000",
   "line": "no carrier: throw away everything the game had queued for the opponent"
  },
  "E78F": {
   "unit": "game/modemDriverE000",
   "line": "loop until the 16-byte host-output ring $E091 is empty"
  },
  "E791": {
   "unit": "game/modemDriverE000",
   "line": "drop anything still queued for the UART transmitter"
  },
  "E794": {
   "unit": "game/modemDriverE000",
   "line": "1 = modem command / terminal mode"
  },
  "E796": {
   "unit": "game/modemDriverE000",
   "line": "enter connection phase 1"
  },
  "E799": {
   "unit": "game/modemDriverE000",
   "block": [
    "Terminal mode, modem to game: forward what the modem says to the chat line and watch for 'CONNECT 1200'."
   ],
   "line": "take the next character the modem sent back"
  },
  "E79C": {
   "unit": "game/modemDriverE000",
   "line": "nothing arrived this tick"
  },
  "E79E": {
   "unit": "game/modemDriverE000",
   "line": "$E048 modem option flags ($CC as shipped): bit 7 (N) = a Hayes modem is attached, bit 6 (V) = auto-detect the connect speed"
  },
  "E7A1": {
   "unit": "game/modemDriverE000",
   "line": "no modem configured - discard whatever arrived"
  },
  "E7A3": {
   "unit": "game/modemDriverE000",
   "line": "CR terminates a verbose result line such as 'CONNECT 1200'"
  },
  "E7A5": {
   "unit": "game/modemDriverE000",
   "line": "an ordinary character: just remember and forward it"
  },
  "E7A7": {
   "unit": "game/modemDriverE000",
   "line": "V = bit 6 of $E048: auto-baud disabled, so do not sniff the result code"
  },
  "E7A9": {
   "unit": "game/modemDriverE000",
   "line": "the character that preceded this CR"
  },
  "E7AC": {
   "unit": "game/modemDriverE000",
   "line": "'0' - the last digit of 'CONNECT 1200'; plain 'CONNECT' (300 baud) ends in 'T' and is ignored"
  },
  "E7AE": {
   "unit": "game/modemDriverE000",
   "line": "not a 1200 baud connect message"
  },
  "E7B0": {
   "unit": "game/modemDriverE000",
   "line": "index 3 = the second three-byte entry of baudParameterTable $E059 = {$53,$03,$02} = $0353 cycles = 1200 baud"
  },
  "E7B2": {
   "unit": "game/modemDriverE000",
   "line": "copy that entry into the live bit period $E056/$E057 and the pacing divisor $E058"
  },
  "E7B5": {
   "unit": "game/modemDriverE000",
   "line": "replace the CR with a space - the chat line cannot print a CR - and reset the last-character memory"
  },
  "E7B7": {
   "unit": "game/modemDriverE000",
   "line": "remember this character for the next auto-baud test"
  },
  "E7BA": {
   "unit": "game/modemDriverE000",
   "line": "test the character"
  },
  "E7BB": {
   "unit": "game/modemDriverE000",
   "line": "never forward a NUL to the game"
  },
  "E7BD": {
   "unit": "game/modemDriverE000",
   "line": "hand the character to the game through the 8-byte host-input ring $E086, which jump-table entry $E006 pops"
  },
  "E7C0": {
   "unit": "game/modemDriverE000",
   "block": [
    "Guard-time delays are held in $EB01 as negative tick counts and block both transmission and the answer timer while they run."
   ],
   "line": "guard-time delay counter ($A0 = -96 and $C0 = -64 come straight out of the AT strings)"
  },
  "E7C3": {
   "unit": "game/modemDriverE000",
   "line": "no delay pending (counter >= 0): send the next byte of the modem command string"
  },
  "E7C5": {
   "unit": "game/modemDriverE000",
   "line": "count the guard time up towards zero"
  },
  "E7C6": {
   "unit": "game/modemDriverE000",
   "line": "store it; this is also where a $80-$FF byte taken out of an AT string is parked as a fresh delay"
  },
  "E7C9": {
   "unit": "game/modemDriverE000",
   "line": "nothing more to do this tick"
  },
  "E7CA": {
   "unit": "game/modemDriverE000",
   "routine": [
    "sendNextModemCommandChar - Tail of the per-frame link service.  It pops the next byte queued for the modem: a byte with bit 7 set is not a character but a guard-time delay and is parked in modemDelayCounter $EB01, anything else goes to the UART transmitter.  It then ages the modem answer timer $E03F.  Sending the CR that ends an AT command arms the timer with $79 (121 frames, about two seconds); while no carrier is present the timer is pinned at 1 so it fires on the first tick after carrier appears.  When it does fire the routine fabricates a CR into the game's chat input and falls straight into runLinkStateMachine - which is how a modem game leaves terminal mode and starts the byte sync.",
    "In:  the 16-byte host-output ring $E091 (via popHostOutRing), $E03F modem answer timer, $E03C bit 6 = carrier detect, $EB01 guard-time counter",
    "Out: byte queued for transmission, $EB01 or $E03F updated, possibly a CR pushed into the host-input ring $E086; falls through into runLinkStateMachine",
    "Called from: runModemStateMachine ($E7C3), its only entry"
   ],
   "line": "next byte queued for the modem: AT-string bytes, or the player's chat text in terminal mode"
  },
  "E7CD": {
   "unit": "game/modemDriverE000",
   "line": "queue empty - nothing to send, just age the answer timer"
  },
  "E7CF": {
   "unit": "game/modemDriverE000",
   "line": "keep a copy of the byte in X"
  },
  "E7D0": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 set: this is a guard-time delay, not a character - park it in $EB01 and stop for this tick"
  },
  "E7D2": {
   "unit": "game/modemDriverE000",
   "line": "hand the character to the UART transmitter (A survives the call)"
  },
  "E7D5": {
   "unit": "game/modemDriverE000",
   "line": "$79 = 121 frames, roughly two seconds, to wait for the modem's answer"
  },
  "E7D7": {
   "unit": "game/modemDriverE000",
   "line": "$E03F with bit 7 set means 'idle, no answer expected' ($FF from queueModemCommandString, $8D from the C=+RETURN hot key)"
  },
  "E7DA": {
   "unit": "game/modemDriverE000",
   "line": "a countdown is already running - just tick it, do not re-arm"
  },
  "E7DC": {
   "unit": "game/modemDriverE000",
   "line": "did we just transmit the CR that terminates an AT command?"
  },
  "E7DE": {
   "unit": "game/modemDriverE000",
   "line": "yes - arm the 121-frame answer timeout"
  },
  "E7E0": {
   "unit": "game/modemDriverE000",
   "line": "nothing was sent: carry on with the timer value already stored"
  },
  "E7E3": {
   "unit": "game/modemDriverE000",
   "line": "bit 6 (V) of the debounced carrier byte = carrier detect"
  },
  "E7E6": {
   "unit": "game/modemDriverE000",
   "line": "carrier present: age the real timer value in X"
  },
  "E7E8": {
   "unit": "game/modemDriverE000",
   "line": "no carrier yet: hold the timer at 1 (2 minus the DEX below) so that it fires on the first tick after carrier appears"
  },
  "E7EA": {
   "unit": "game/modemDriverE000",
   "line": "tick the answer timer down"
  },
  "E7EB": {
   "unit": "game/modemDriverE000",
   "line": "timer was 0 or negative (idle) - nothing to do"
  },
  "E7ED": {
   "unit": "game/modemDriverE000",
   "line": "store the new value"
  },
  "E7F0": {
   "unit": "game/modemDriverE000",
   "line": "still counting"
  },
  "E7F2": {
   "unit": "game/modemDriverE000",
   "line": "timer reached zero: fabricate the CR the modem never sent"
  },
  "E7F4": {
   "unit": "game/modemDriverE000",
   "line": "push it into the host-input ring so the game's chat line unblocks, then fall into the link state machine below"
  },
  "E7F7": {
   "unit": "game/modemDriverE000",
   "routine": [
    "runLinkStateMachine - Second half of the per-frame link service: it dispatches on connectionPhase $E040.  Any error counted by countLinkError ($E403) since the last tick restarts the byte sync whatever the phase; otherwise phase 2 continues the sync, phase 3 (or above) runs the packet protocol, and phases 0 and 1 fall through into a fresh byte sync.",
    "In:  $E040 connection phase, $E047 link error counter",
    "Out: branches into beginByteSyncPhase, the sync continuation at $E828 or runPacketPhase; never returns to its caller directly",
    "Called from: runModemStateMachine ($E78A when carrier is already up) and by fall-through from sendNextModemCommandChar ($E7F4)"
   ],
   "line": "current connection phase"
  },
  "E7FA": {
   "unit": "game/modemDriverE000",
   "line": "link error counter, bumped from the NMI receiver by countLinkError on a bad stop bit or a receive overflow"
  },
  "E7FD": {
   "unit": "game/modemDriverE000",
   "line": "any error since the last tick: throw the connection away and re-synchronise"
  },
  "E7FF": {
   "unit": "game/modemDriverE000",
   "line": "phase 2 = a byte sync is already in progress"
  },
  "E801": {
   "unit": "game/modemDriverE000",
   "line": "continue the sync where it left off"
  },
  "E803": {
   "unit": "game/modemDriverE000",
   "line": "phase 3 or above: run the reliable packet protocol"
  },
  "E805": {
   "unit": "game/modemDriverE000",
   "routine": [
    "beginByteSyncPhase - Connection phase 2, the byte-alignment handshake that runs before any packet may be sent.  It empties both UART rings, clears the sync state and then plays a two-step game with the peer: keep sending $00 until a $00 comes back, then answer $FF and wait for the peer's $FF.  Ten rounds without the peer's $FF restart the whole thing.  On success the peer's request for an acknowledgement is recorded and control drops into runPacketPhase.",
    "In:  the UART rings ($E417 transmit, $E430 receive), syncRoundCounter $EAB7, zeroRunLength $EAB8, $E0A5 pending transmit count",
    "Out: $E040 = 2 then 3, $E047/$EAB7/$EAB8/$EAB9 cleared, $00 and $FF bytes transmitted, $E044 paced, ackPending $EAC0 = $FF on success",
    "Called from: runLinkStateMachine ($E7FD, and by fall-through past $E803) and noteIdlePoll ($E9A4) after sixteen dead service ticks"
   ],
   "line": "2 = the byte-sync phase"
  },
  "E807": {
   "unit": "game/modemDriverE000",
   "line": "enter it"
  },
  "E80A": {
   "unit": "game/modemDriverE000",
   "line": "throw away every byte already sitting in the UART receive ring"
  },
  "E80D": {
   "unit": "game/modemDriverE000",
   "line": "loop until it is empty (C = 1)"
  },
  "E80F": {
   "unit": "game/modemDriverE000",
   "line": "drop anything still queued for transmission; this is also the 'start the sync over' entry"
  },
  "E812": {
   "unit": "game/modemDriverE000",
   "line": "A = 0: clears the sync state and doubles as the first sync byte"
  },
  "E814": {
   "unit": "game/modemDriverE000",
   "line": "forget any link errors"
  },
  "E817": {
   "unit": "game/modemDriverE000",
   "line": "no packet is half received"
  },
  "E81A": {
   "unit": "game/modemDriverE000",
   "line": "syncRoundCounter ($EAB7, the first byte of the packet-protocol state block) = 0 = 'waiting for the peer's $00'"
  },
  "E81D": {
   "unit": "game/modemDriverE000",
   "line": "queue the sync byte held in A ($00 or $FF) for transmission"
  },
  "E820": {
   "unit": "game/modemDriverE000",
   "line": "bit 1 of the transmit pacing counter..."
  },
  "E822": {
   "unit": "game/modemDriverE000",
   "line": "...is forced on..."
  },
  "E825": {
   "unit": "game/modemDriverE000",
   "line": "...so at least two NMI transmit slots separate the sync bytes"
  },
  "E828": {
   "unit": "game/modemDriverE000",
   "line": "sync round counter"
  },
  "E82B": {
   "unit": "game/modemDriverE000",
   "line": "non-zero: we are past the $00 round and are waiting for the peer's $FF"
  },
  "E82D": {
   "unit": "game/modemDriverE000",
   "line": "characters still queued for the transmitter"
  },
  "E830": {
   "unit": "game/modemDriverE000",
   "line": "transmitter idle: send another $00 (A is 0 here)"
  },
  "E832": {
   "unit": "game/modemDriverE000",
   "line": "run of consecutive $00 bytes received from the peer"
  },
  "E835": {
   "unit": "game/modemDriverE000",
   "line": "the peer's $00 has already been seen"
  },
  "E837": {
   "unit": "game/modemDriverE000",
   "line": "otherwise take one more byte off the line and update the zero run"
  },
  "E83A": {
   "unit": "game/modemDriverE000",
   "line": "X = the new run length"
  },
  "E83B": {
   "unit": "game/modemDriverE000",
   "line": "nothing from the peer yet - try again next tick"
  },
  "E83D": {
   "unit": "game/modemDriverE000",
   "line": "the peer answered: move on to round 1"
  },
  "E840": {
   "unit": "game/modemDriverE000",
   "line": "answer with $FF..."
  },
  "E842": {
   "unit": "game/modemDriverE000",
   "line": "...always taken, queue it"
  },
  "E844": {
   "unit": "game/modemDriverE000",
   "line": "shared 'nothing more to do this tick' exit for the whole link service"
  },
  "E845": {
   "unit": "game/modemDriverE000",
   "line": "ten rounds without the peer's $FF?"
  },
  "E847": {
   "unit": "game/modemDriverE000",
   "line": "give up and start the sync from scratch"
  },
  "E849": {
   "unit": "game/modemDriverE000",
   "line": "count this round"
  },
  "E84C": {
   "unit": "game/modemDriverE000",
   "line": "look for the peer's answer"
  },
  "E84F": {
   "unit": "game/modemDriverE000",
   "line": "A = 0 means either a $00 arrived or the ring was empty"
  },
  "E850": {
   "unit": "game/modemDriverE000",
   "line": "the peer has not answered $FF yet - wait for the next tick"
  },
  "E852": {
   "unit": "game/modemDriverE000",
   "line": "was it $FF?"
  },
  "E853": {
   "unit": "game/modemDriverE000",
   "line": "some other byte on the line: the peer is not where we think it is, resync"
  },
  "E855": {
   "unit": "game/modemDriverE000",
   "line": "$FF received, both ends are byte aligned; A = $FF also records that the peer wants an acknowledgement"
  },
  "E858": {
   "unit": "game/modemDriverE000",
   "routine": [
    "runPacketPhase - Connection phase 3, one tick of the reliable packet layer.  It first decodes anything the peer sent, then, if the transmitter has room, emits at most one thing: a deferred raw sync byte ($00 nak or $FF ack request), a retransmission of the oldest unacknowledged message, a freshly built message packet, or a bare acknowledgement.",
    "The message-building branch at $E8A8 is dead in the shipped build: its gate $E0A6 is only ever",
    "written with 0 (at $E778), so outgoing game traffic really leaves through the older frame engine",
    "at $E1DD and this layer only receives, decodes and acknowledges.",
    "In:  $E0A5 pending transmit count, sendZeroPending $EAC2, sendFfPending $EAC1, $E0A4 packet-in-flight, $E0A6 send enable, resendRequest $EABF, the outstanding-message indices $EAC4/$EAC5/$EAC6, ackPending $EAC0",
    "Out: packets clocked out through sendPacket; $EAC0/$EAC1/$EAC2/$EABF cleared, $EAC3/$EAC4/$EAC5/$EAC6/$EABE updated",
    "Called from: runLinkStateMachine ($E803) and by fall-through from the successful end of the byte sync ($E855)"
   ],
   "line": "3 = the packet-protocol phase"
  },
  "E85A": {
   "unit": "game/modemDriverE000",
   "line": "enter (or stay in) it"
  },
  "E85D": {
   "unit": "game/modemDriverE000",
   "line": "decode whatever the peer has sent since the last tick"
  },
  "E860": {
   "unit": "game/modemDriverE000",
   "line": "characters still queued for the UART transmitter"
  },
  "E863": {
   "unit": "game/modemDriverE000",
   "line": "two or more still to go?"
  },
  "E865": {
   "unit": "game/modemDriverE000",
   "line": "the line is busy - do not start anything else this tick"
  },
  "E867": {
   "unit": "game/modemDriverE000",
   "line": "X = 0, the value used to clear the two raw-byte requests"
  },
  "E869": {
   "unit": "game/modemDriverE000",
   "line": "A = 0 = the raw $00 byte that means 'resend'"
  },
  "E86A": {
   "unit": "game/modemDriverE000",
   "line": "sendZeroPending, made negative by receivePacket when a checksum failed"
  },
  "E86D": {
   "unit": "game/modemDriverE000",
   "line": "consume the request"
  },
  "E870": {
   "unit": "game/modemDriverE000",
   "line": "a $00 nak is owed - send it"
  },
  "E872": {
   "unit": "game/modemDriverE000",
   "line": "otherwise A = $FF = the raw byte that means 'acknowledge me'"
  },
  "E874": {
   "unit": "game/modemDriverE000",
   "line": "sendFfPending - nothing in the shipped image ever sets this, so this path is dead"
  },
  "E877": {
   "unit": "game/modemDriverE000",
   "line": "no raw byte owed"
  },
  "E879": {
   "unit": "game/modemDriverE000",
   "line": "consume the $FF request"
  },
  "E87C": {
   "unit": "game/modemDriverE000",
   "line": "put the raw sync byte on the wire"
  },
  "E87F": {
   "unit": "game/modemDriverE000",
   "line": "is a message packet already in flight?"
  },
  "E882": {
   "unit": "game/modemDriverE000",
   "line": "yes - wait until it has been acknowledged"
  },
  "E884": {
   "unit": "game/modemDriverE000",
   "line": "resendRequest: 1 = the peer sent a lone $00, negative = resend the message with this index"
  },
  "E887": {
   "unit": "game/modemDriverE000",
   "line": "nothing to resend"
  },
  "E889": {
   "unit": "game/modemDriverE000",
   "line": "consume the request (A is 0 here)"
  },
  "E88C": {
   "unit": "game/modemDriverE000",
   "line": "N still comes from the LDX above: a plain $00 nak, so resend the oldest unacknowledged message"
  },
  "E88E": {
   "unit": "game/modemDriverE000",
   "line": "targeted resend: does the request still name the oldest outstanding message?"
  },
  "E891": {
   "unit": "game/modemDriverE000",
   "line": "no - it has already been retired, ignore the stale request"
  },
  "E893": {
   "unit": "game/modemDriverE000",
   "line": "index of the oldest unacknowledged message; bit 7 = the slot is in use"
  },
  "E896": {
   "unit": "game/modemDriverE000",
   "line": "nothing outstanding - send a bare acknowledgement instead"
  },
  "E898": {
   "unit": "game/modemDriverE000",
   "line": "strip the in-use flag to get the message ring index"
  },
  "E89A": {
   "unit": "game/modemDriverE000",
   "line": "retransmit that message"
  },
  "E89D": {
   "unit": "game/modemDriverE000",
   "line": "done"
  },
  "E89E": {
   "unit": "game/modemDriverE000",
   "block": [
    "Build a brand new packet out of the game's outbound message ring.  Unreachable in the shipped build: $E0A6 is only ever written with 0."
   ],
   "line": "index of the next free slot in the 35-byte message ring"
  },
  "E8A1": {
   "unit": "game/modemDriverE000",
   "line": "negative = both outstanding slots are busy, so no new message may be started"
  },
  "E8A3": {
   "unit": "game/modemDriverE000",
   "line": "packet-layer send enable - never set in this build, so everything below is unreachable here"
  },
  "E8A6": {
   "unit": "game/modemDriverE000",
   "line": "not enabled: fall through to the acknowledgement"
  },
  "E8A8": {
   "unit": "game/modemDriverE000",
   "line": "mark a message as in flight"
  },
  "E8AB": {
   "unit": "game/modemDriverE000",
   "line": "start writing at the free ring position"
  },
  "E8AE": {
   "unit": "game/modemDriverE000",
   "line": "first byte of the game's queued message; its low nibble is the argument count"
  },
  "E8B1": {
   "unit": "game/modemDriverE000",
   "line": "keep the original command byte"
  },
  "E8B4": {
   "unit": "game/modemDriverE000",
   "line": "low nibble = number of argument bytes (game commands are $80-$A6 with 0-3 arguments)"
  },
  "E8B6": {
   "unit": "game/modemDriverE000",
   "line": "use it as the copy counter"
  },
  "E8B9": {
   "unit": "game/modemDriverE000",
   "line": "store the argument count as the message's length byte"
  },
  "E8BC": {
   "unit": "game/modemDriverE000",
   "line": "advance the transmit sequence number"
  },
  "E8BF": {
   "unit": "game/modemDriverE000",
   "line": "read it back..."
  },
  "E8C2": {
   "unit": "game/modemDriverE000",
   "line": "...and keep two bits of it"
  },
  "E8C4": {
   "unit": "game/modemDriverE000",
   "line": "shift the sequence..."
  },
  "E8C5": {
   "unit": "game/modemDriverE000",
   "line": "...into bits 2-3"
  },
  "E8C6": {
   "unit": "game/modemDriverE000",
   "line": "merge it with the command byte..."
  },
  "E8C9": {
   "unit": "game/modemDriverE000",
   "line": "...and cancel the length nibble again, giving (command AND $F0) OR (sequence << 2)"
  },
  "E8CC": {
   "unit": "game/modemDriverE000",
   "line": "append the control byte, and on every later pass one argument byte"
  },
  "E8CF": {
   "unit": "game/modemDriverE000",
   "line": "one fewer argument to copy"
  },
  "E8D2": {
   "unit": "game/modemDriverE000",
   "line": "all arguments copied"
  },
  "E8D4": {
   "unit": "game/modemDriverE000",
   "line": "next argument byte out of the game's outbound ring $E447"
  },
  "E8D7": {
   "unit": "game/modemDriverE000",
   "line": "got one - store it"
  },
  "E8D9": {
   "unit": "game/modemDriverE000",
   "line": "remember the ring index just past this message"
  },
  "E8DC": {
   "unit": "game/modemDriverE000",
   "line": "index of the message just built"
  },
  "E8DF": {
   "unit": "game/modemDriverE000",
   "line": "save it for the transmit call below"
  },
  "E8E0": {
   "unit": "game/modemDriverE000",
   "line": "mark the slot as used"
  },
  "E8E2": {
   "unit": "game/modemDriverE000",
   "line": "is the oldest-unacknowledged slot already occupied?"
  },
  "E8E5": {
   "unit": "game/modemDriverE000",
   "line": "yes - this message becomes the second outstanding one"
  },
  "E8E7": {
   "unit": "game/modemDriverE000",
   "line": "no - this message becomes the oldest outstanding message..."
  },
  "E8EA": {
   "unit": "game/modemDriverE000",
   "line": "...and the free index moves to the end of the message (X = $EAC4)"
  },
  "E8EB": {
   "unit": "game/modemDriverE000",
   "line": "store the new free / second-outstanding index"
  },
  "E8EE": {
   "unit": "game/modemDriverE000",
   "line": "recover the ring index of the message to send"
  },
  "E8EF": {
   "unit": "game/modemDriverE000",
   "line": "build the packet and clock it out"
  },
  "E8F2": {
   "unit": "game/modemDriverE000",
   "line": "done"
  },
  "E8F3": {
   "unit": "game/modemDriverE000",
   "line": "does the peer still need an acknowledgement?"
  },
  "E8F6": {
   "unit": "game/modemDriverE000",
   "line": "no - nothing at all to send this tick"
  },
  "E8F8": {
   "unit": "game/modemDriverE000",
   "routine": [
    "buildAckPacket - Builds the three-byte bare acknowledgement in packetBuffer: length code $55 (one payload byte) and control byte $EC, then falls into the common send tail at $E90C which ORs in the receive sequence number, appends the check byte and transmits it.",
    "In:  none",
    "Out: packetBuffer = $55,$EC, packetLength = 2; packet transmitted, ackPending $EAC0 cleared",
    "Called from: runPacketPhase ($E896) when a data packet has arrived but no message of our own is outstanding"
   ],
   "line": "$EC = the bare-acknowledgement control byte (any control byte $E0-$EF is an ack)"
  },
  "E8FA": {
   "unit": "game/modemDriverE000",
   "line": "$55 = packetLengthCodeTable entry 1, the code for a one-byte payload"
  },
  "E8FC": {
   "unit": "game/modemDriverE000",
   "line": "packet byte 0 = length code"
  },
  "E8FF": {
   "unit": "game/modemDriverE000",
   "line": "packet byte 1 = control byte"
  },
  "E902": {
   "unit": "game/modemDriverE000",
   "line": "two bytes..."
  },
  "E904": {
   "unit": "game/modemDriverE000",
   "line": "...before the check byte"
  },
  "E907": {
   "unit": "game/modemDriverE000",
   "line": "always taken: skip the message-copy step and go straight to the send tail"
  },
  "E909": {
   "unit": "game/modemDriverE000",
   "routine": [
    "sendPacket - Sends one message from the 35-byte transmit ring: buildPacketFromRing copies it into packetBuffer, the low two bits of rxSeqCounter are piggy-backed into the control byte as the acknowledgement of everything received so far, computePacketChecksum appends the check byte, and every byte is pushed into the UART transmit ring.  Entry $E90C skips the copy step and is used by buildAckPacket.",
    "In:  A = index into txMessageRing $EAC7 (entry $E909 only); rxSeqCounter $EABD; packetBuffer and packetLength for entry $E90C",
    "Out: finished packet in $EAEB.., all of its bytes queued for transmission, ackPending $EAC0 cleared, $EABA and txRingIndex $EAC3 clobbered",
    "Called from: runPacketPhase ($E89A retransmit, $E8EF new message) and by fall-through from buildAckPacket"
   ],
   "line": "copy the message at ring index A into packetBuffer and prefix it with its length code"
  },
  "E90C": {
   "unit": "game/modemDriverE000",
   "line": "the sequence number of the next packet we expect to receive"
  },
  "E90F": {
   "unit": "game/modemDriverE000",
   "line": "keep two bits of it"
  },
  "E911": {
   "unit": "game/modemDriverE000",
   "line": "piggy-back it in the control byte's low two bits..."
  },
  "E914": {
   "unit": "game/modemDriverE000",
   "line": "...so every packet also acknowledges what we have received"
  },
  "E917": {
   "unit": "game/modemDriverE000",
   "line": "checksum packet bytes 0..packetLength-1; X comes back equal to packetLength"
  },
  "E91A": {
   "unit": "game/modemDriverE000",
   "line": "append the check byte right after the payload"
  },
  "E91D": {
   "unit": "game/modemDriverE000",
   "line": "this packet carries the acknowledgement itself..."
  },
  "E91F": {
   "unit": "game/modemDriverE000",
   "line": "...so the pending bare-ack request is satisfied"
  },
  "E922": {
   "unit": "game/modemDriverE000",
   "line": "start at packet byte 0"
  },
  "E924": {
   "unit": "game/modemDriverE000",
   "line": "save the index - queueByteForTransmit clobbers X"
  },
  "E927": {
   "unit": "game/modemDriverE000",
   "line": "next packet byte"
  },
  "E92A": {
   "unit": "game/modemDriverE000",
   "line": "hand it to the UART transmitter"
  },
  "E92D": {
   "unit": "game/modemDriverE000",
   "line": "restore the index"
  },
  "E930": {
   "unit": "game/modemDriverE000",
   "line": "have we just queued the check byte?  the carry is taken before the INX below"
  },
  "E933": {
   "unit": "game/modemDriverE000",
   "line": "advance to the next byte"
  },
  "E934": {
   "unit": "game/modemDriverE000",
   "line": "loop; the check byte at index packetLength is the last one sent"
  },
  "E936": {
   "unit": "game/modemDriverE000",
   "line": "the packet is on the wire"
  },
  "E937": {
   "unit": "game/modemDriverE000",
   "routine": [
    "computePacketChecksum - Rotate-and-add check byte over packetBuffer[0..packetLength-1].  A starts at $EE and each byte contributes ASL / ADC #0 (fold the bit shifted out back in) / ADC byte / ADC #0 (fold the add carry back in), so both the value and the position of every byte matter.",
    "In:  packetLength $EAEA, packetBuffer $EAEB..",
    "Out: A = check byte, X = packetLength, i.e. the index of the check byte slot",
    "Called from: sendPacket ($E917, result appended) and receivePacket ($EA01, result EORed with the received check byte)"
   ],
   "line": "start at packet byte 0"
  },
  "E939": {
   "unit": "game/modemDriverE000",
   "line": "$EE = the checksum seed"
  },
  "E93B": {
   "unit": "game/modemDriverE000",
   "line": "rotate the running sum left..."
  },
  "E93C": {
   "unit": "game/modemDriverE000",
   "line": "...folding the bit that fell out back in at the bottom"
  },
  "E93E": {
   "unit": "game/modemDriverE000",
   "line": "add the packet byte"
  },
  "E941": {
   "unit": "game/modemDriverE000",
   "line": "fold that carry back in too"
  },
  "E943": {
   "unit": "game/modemDriverE000",
   "line": "next byte"
  },
  "E944": {
   "unit": "game/modemDriverE000",
   "line": "stop at packetLength - the check byte slot itself is not included"
  },
  "E947": {
   "unit": "game/modemDriverE000",
   "line": "loop"
  },
  "E949": {
   "unit": "game/modemDriverE000",
   "line": "return A = check byte, X = index of the check byte slot"
  },
  "E94A": {
   "unit": "game/modemDriverE000",
   "routine": [
    "buildPacketFromRing - Copies one queued message out of the 35-byte transmit ring into packetBuffer.  A message is stored as [argument count][control byte][arguments...]; the argument count plus one is the payload length and is turned into one of the sixteen transition-rich code bytes of packetLengthCodeTable, which becomes packet byte 0.",
    "In:  A = ring index of the message's length byte; txMessageRing $EAC7, packetLengthCodeTable $EAA0",
    "Out: packetBuffer filled, packetLength $EAEA = number of bytes before the check byte, txRingIndex $EAC3 left just past the message, $EABA clobbered",
    "Called from: sendPacket ($E909)"
   ],
   "line": "A = ring index of the message's length byte"
  },
  "E94B": {
   "unit": "game/modemDriverE000",
   "line": "read it; the ring index is left pointing at the control byte"
  },
  "E94E": {
   "unit": "game/modemDriverE000",
   "line": "X = argument count"
  },
  "E94F": {
   "unit": "game/modemDriverE000",
   "line": "payload length = arguments + 1, because the control byte counts as payload too"
  },
  "E950": {
   "unit": "game/modemDriverE000",
   "line": "use it as the copy counter"
  },
  "E953": {
   "unit": "game/modemDriverE000",
   "line": "packetLengthCodeTable[length]; the table base $EAA0 is the shared RTS, so entry 0 is never selected"
  },
  "E956": {
   "unit": "game/modemDriverE000",
   "line": "packet byte 0..."
  },
  "E958": {
   "unit": "game/modemDriverE000",
   "line": "...always taken, store the length code there"
  },
  "E95A": {
   "unit": "game/modemDriverE000",
   "line": "next byte of the message out of the ring"
  },
  "E95D": {
   "unit": "game/modemDriverE000",
   "line": "append it at the current packet length"
  },
  "E960": {
   "unit": "game/modemDriverE000",
   "line": "store the packet byte"
  },
  "E963": {
   "unit": "game/modemDriverE000",
   "line": "advance..."
  },
  "E964": {
   "unit": "game/modemDriverE000",
   "line": "...and record the new packet length"
  },
  "E967": {
   "unit": "game/modemDriverE000",
   "line": "one fewer byte to copy"
  },
  "E96A": {
   "unit": "game/modemDriverE000",
   "line": "loop over the control byte and every argument"
  },
  "E96C": {
   "unit": "game/modemDriverE000",
   "line": "packetBuffer now holds [length code][control][arguments]"
  },
  "E96D": {
   "unit": "game/modemDriverE000",
   "routine": [
    "writeTxRingByte - Appends A to the 35-byte transmit message ring $EAC7 at txRingIndex and advances the index modulo 35.",
    "In:  A = byte, txRingIndex $EAC3",
    "Out: ring updated, $EAC3 advanced (wrapping at $23), X = the new index",
    "Called from: the message builder in runPacketPhase ($E8B9, $E8CC)"
   ],
   "line": "current write position in the 35-byte message ring"
  },
  "E970": {
   "unit": "game/modemDriverE000",
   "line": "store the byte"
  },
  "E973": {
   "unit": "game/modemDriverE000",
   "line": "always taken (the index is 0..34): go and advance it"
  },
  "E975": {
   "unit": "game/modemDriverE000",
   "routine": [
    "readTxRingByte - Reads the transmit-ring byte at txRingIndex and advances the index modulo 35.",
    "In:  txRingIndex $EAC3",
    "Out: A = byte, $EAC3 advanced, X = the new index",
    "Called from: buildPacketFromRing ($E95A)"
   ],
   "line": "current read position in the message ring"
  },
  "E978": {
   "unit": "game/modemDriverE000",
   "routine": [
    "readTxRingByteAt - Reads the transmit-ring byte at an arbitrary index X and leaves txRingIndex pointing one past it.  Used to start a retransmission and to inspect the header of a message the peer has acknowledged.",
    "In:  X = ring index 0..34",
    "Out: A = byte, txRingIndex $EAC3 = X+1 modulo 35, X = the new index",
    "Called from: buildPacketFromRing ($E94B) and the acknowledgement handler ($EA40, $EA43)"
   ],
   "line": "read txMessageRing[X]"
  },
  "E97B": {
   "unit": "game/modemDriverE000",
   "line": "advance the index"
  },
  "E97C": {
   "unit": "game/modemDriverE000",
   "line": "$23 = 35 = the ring size"
  },
  "E97E": {
   "unit": "game/modemDriverE000",
   "line": "no wrap needed"
  },
  "E980": {
   "unit": "game/modemDriverE000",
   "line": "wrap back to the start of the ring"
  },
  "E982": {
   "unit": "game/modemDriverE000",
   "line": "save the new index"
  },
  "E985": {
   "unit": "game/modemDriverE000",
   "line": "return A = byte, X = new index"
  },
  "E986": {
   "unit": "game/modemDriverE000",
   "routine": [
    "noteIdlePoll - Called when the receiver found nothing on the line.  It only counts a stall when a message really is outstanding and we are not still transmitting it; otherwise silence is normal and stallCounter $EB00 is reset.  The NMI receiver also zeroes $EB00 (at $E716) on every character that arrives, so the counter really measures consecutive dead service ticks.  Sixteen of them drop two return addresses off the stack and restart the byte sync.",
    "In:  the outstanding-message indices $EAC5/$EAC6, $E0A4 packet-in-flight, stallCounter $EB00",
    "Out: $EB00 updated; on overflow the stack is unwound and control jumps to beginByteSyncPhase, so the caller never returns",
    "Called from: receivePacket ($E9B2 when the ring is empty); entry $E997 also from $E9DC when a packet is only half received"
   ],
   "line": "X = 0 = 'not stalled'"
  },
  "E988": {
   "unit": "game/modemDriverE000",
   "line": "oldest unacknowledged message slot"
  },
  "E98B": {
   "unit": "game/modemDriverE000",
   "line": "positive = nothing outstanding, so an idle line is perfectly normal - reset the counter"
  },
  "E98D": {
   "unit": "game/modemDriverE000",
   "line": "the second outstanding slot"
  },
  "E990": {
   "unit": "game/modemDriverE000",
   "line": "negative = the send window is full, so we really are stalled"
  },
  "E992": {
   "unit": "game/modemDriverE000",
   "line": "is a packet still being clocked out?"
  },
  "E995": {
   "unit": "game/modemDriverE000",
   "line": "yes - progress is being made, reset the counter"
  },
  "E997": {
   "unit": "game/modemDriverE000",
   "line": "count one more fruitless service tick"
  },
  "E99A": {
   "unit": "game/modemDriverE000",
   "line": "bump it"
  },
  "E99B": {
   "unit": "game/modemDriverE000",
   "line": "store the counter"
  },
  "E99E": {
   "unit": "game/modemDriverE000",
   "line": "sixteen consecutive dead ticks?"
  },
  "E9A0": {
   "unit": "game/modemDriverE000",
   "line": "not yet - return normally"
  },
  "E9A2": {
   "unit": "game/modemDriverE000",
   "line": "discard receivePacket's return address..."
  },
  "E9A3": {
   "unit": "game/modemDriverE000",
   "line": "...and its caller's..."
  },
  "E9A4": {
   "unit": "game/modemDriverE000",
   "line": "...so the whole link restarts from the byte-sync handshake"
  },
  "E9A7": {
   "unit": "game/modemDriverE000",
   "block": [
    "Entered from $E9C5: a raw $FF from the peer means 'acknowledge me'.  Record it and fall into the top of receivePacket to look at the next byte."
   ],
   "line": "remember that the peer is asking for an acknowledgement, then loop back into the scanner below"
  },
  "E9AA": {
   "unit": "game/modemDriverE000",
   "routine": [
    "receivePacket - Assembles one incoming packet from the UART receive ring.  The inter-packet bytes are handled first: a lone $00 is the peer's 'resend' nak, two or more in a row mean the line has lost sync, and $FF asks for an acknowledgement.  The first other byte is looked up in packetLengthCodeTable to learn the payload length; the routine then waits (across ticks, remembering its state in rxPayloadRemaining) until payload and check byte are both queued, copies them into packetBuffer and verifies the checksum.  A bad checksum arranges a $00 nak for the next tick; a good packet falls into dispatchReceivedPacket.",
    "In:  the UART receive ring $E430/$E42D, zeroRunLength $EAB8, rxPayloadRemaining $EAB9, packetLengthCodeTable $EAA0",
    "Out: $EAB9, rxLengthCodeByte $EAFF, packetLength $EAEA, packetBuffer, resendRequest $EABF, ackPending $EAC0, sendZeroPending $EAC2; may resync through $E9A2",
    "Called from: runPacketPhase ($E85D), once per frame while the link is in phase 3"
   ],
   "line": "bytes still expected for a packet whose header has already arrived"
  },
  "E9AD": {
   "unit": "game/modemDriverE000",
   "line": "resume that packet"
  },
  "E9AF": {
   "unit": "game/modemDriverE000",
   "line": "take the next byte and update the zero-run counter"
  },
  "E9B2": {
   "unit": "game/modemDriverE000",
   "line": "ring empty - count an idle poll (which may force a resync) and return"
  },
  "E9B4": {
   "unit": "game/modemDriverE000",
   "line": "set Z from the byte"
  },
  "E9B6": {
   "unit": "game/modemDriverE000",
   "line": "not a $00 inter-packet byte"
  },
  "E9B8": {
   "unit": "game/modemDriverE000",
   "line": "X = the zero-run length..."
  },
  "E9B9": {
   "unit": "game/modemDriverE000",
   "line": "...minus two"
  },
  "E9BA": {
   "unit": "game/modemDriverE000",
   "line": "two or more $00 in a row: the peer is not framing any more, resync the link"
  },
  "E9BC": {
   "unit": "game/modemDriverE000",
   "line": "a single $00 is the peer's 'resend the last message' nak..."
  },
  "E9BE": {
   "unit": "game/modemDriverE000",
   "line": "...record it for runPacketPhase"
  },
  "E9C1": {
   "unit": "game/modemDriverE000",
   "line": "always taken: go back and look at the next byte"
  },
  "E9C3": {
   "unit": "game/modemDriverE000",
   "line": "$FF is the peer's 'acknowledge me' inter-packet byte"
  },
  "E9C5": {
   "unit": "game/modemDriverE000",
   "line": "record it and keep scanning"
  },
  "E9C7": {
   "unit": "game/modemDriverE000",
   "line": "anything else must be a packet header: remember the length code byte"
  },
  "E9CA": {
   "unit": "game/modemDriverE000",
   "line": "search packetLengthCodeTable from entry 1 upwards"
  },
  "E9CC": {
   "unit": "game/modemDriverE000",
   "line": "next table entry"
  },
  "E9CD": {
   "unit": "game/modemDriverE000",
   "line": "compare; the table is sorted ascending and ends in an $FF sentinel, so the search always terminates"
  },
  "E9D0": {
   "unit": "game/modemDriverE000",
   "line": "found: X = payload length 1..16"
  },
  "E9D2": {
   "unit": "game/modemDriverE000",
   "line": "the received byte is still above this entry - keep searching"
  },
  "E9D4": {
   "unit": "game/modemDriverE000",
   "line": "it fell between two entries, so it is not a valid length code: resync"
  },
  "E9D6": {
   "unit": "game/modemDriverE000",
   "line": "remember how many payload bytes are still to come"
  },
  "E9D9": {
   "unit": "game/modemDriverE000",
   "line": "are the payload and its check byte all in the receive ring yet?  we need X+1 bytes"
  },
  "E9DC": {
   "unit": "game/modemDriverE000",
   "line": "not yet - count the wait and come back next tick with the state preserved"
  },
  "E9DE": {
   "unit": "game/modemDriverE000",
   "line": "re-fetch the length code byte"
  },
  "E9E1": {
   "unit": "game/modemDriverE000",
   "line": "it is packet byte 0"
  },
  "E9E4": {
   "unit": "game/modemDriverE000",
   "line": "start filling..."
  },
  "E9E6": {
   "unit": "game/modemDriverE000",
   "line": "...at packet byte 1"
  },
  "E9E9": {
   "unit": "game/modemDriverE000",
   "line": "next byte off the wire"
  },
  "E9EC": {
   "unit": "game/modemDriverE000",
   "line": "one more byte in the packet..."
  },
  "E9EF": {
   "unit": "game/modemDriverE000",
   "line": "...index it..."
  },
  "E9F2": {
   "unit": "game/modemDriverE000",
   "line": "...and store it"
  },
  "E9F5": {
   "unit": "game/modemDriverE000",
   "line": "one fewer byte to read"
  },
  "E9F8": {
   "unit": "game/modemDriverE000",
   "line": "loop over the payload and the check byte"
  },
  "E9FA": {
   "unit": "game/modemDriverE000",
   "line": "back to 0: no packet is half received any more"
  },
  "E9FD": {
   "unit": "game/modemDriverE000",
   "line": "the check byte should never be $00 - that is an inter-packet byte"
  },
  "E9FF": {
   "unit": "game/modemDriverE000",
   "line": "it was - treat the packet as garbage and resync"
  },
  "EA01": {
   "unit": "game/modemDriverE000",
   "line": "recompute the checksum over bytes 0..packetLength-1"
  },
  "EA04": {
   "unit": "game/modemDriverE000",
   "line": "compare it with the received check byte at index packetLength"
  },
  "EA07": {
   "unit": "game/modemDriverE000",
   "line": "they match - act on the packet"
  },
  "EA09": {
   "unit": "game/modemDriverE000",
   "line": "bad checksum: make the next tick put a raw $00 nak on the wire"
  },
  "EA0C": {
   "unit": "game/modemDriverE000",
   "line": "drop the packet"
  },
  "EA0D": {
   "unit": "game/modemDriverE000",
   "routine": [
    "dispatchReceivedPacket - Acts on a packet whose checksum was good.  A control byte of $F0 or above is a 1541 pass-through: the bytes are clocked straight to the drive with interrupts off.  $E0-$EF is a pure acknowledgement, which retires the outstanding message(s) by comparing the sequence field of the acknowledged message with the ack byte.  Anything below $E0 is a data packet: it is accepted only when its 2-bit sequence field is the one expected next and the game's inbound command ring is empty, and the game's original command byte is then rebuilt from the control byte and the payload length.",
    "The pass-through byte count comes from the control byte itself (256 minus its value), not from the",
    "packet length, so a control byte that disagrees with the payload length would read past the packet",
    "buffer.  Together with remoteCodeBuffer at $EB03 this gives the peer two ways to reach into this",
    "machine.",
    "In:  packetBuffer/packetControlByte, packetLength $EAEA, rxSeqCounter $EABD, $E459 inbound ring count, the outstanding-message indices $EAC4/$EAC5/$EAC6",
    "Out: $EABB/$EABC/$EABD/$EABF/$EAC0/$EAC5/$EAC6 updated; decoded command bytes pushed into the inbound ring $E45C",
    "Called from: receivePacket ($EA07), by fall-through, on every packet that passes the checksum"
   ],
   "line": "the control byte, i.e. packet byte 1"
  },
  "EA10": {
   "unit": "game/modemDriverE000",
   "line": "$F0 and above is the 1541 pass-through channel"
  },
  "EA12": {
   "unit": "game/modemDriverE000",
   "line": "below: an ordinary acknowledgement or data packet"
  },
  "EA14": {
   "unit": "game/modemDriverE000",
   "line": "save the interrupt state"
  },
  "EA15": {
   "unit": "game/modemDriverE000",
   "line": "the drive handshake is bit-banged, so the UART NMI must not interrupt it"
  },
  "EA16": {
   "unit": "game/modemDriverE000",
   "line": "Z still comes from the CPX: a control byte of exactly $F0 sends a single byte"
  },
  "EA18": {
   "unit": "game/modemDriverE000",
   "line": "A is 0 here (the checksum matched), so Y = 0 = the first payload byte"
  },
  "EA19": {
   "unit": "game/modemDriverE000",
   "line": "the first byte handed to the drive is the control byte itself"
  },
  "EA1A": {
   "unit": "game/modemDriverE000",
   "line": "$EAFE counts up to zero, so 256 minus the control byte bytes are sent in total"
  },
  "EA1D": {
   "unit": "game/modemDriverE000",
   "line": "clock one byte out to the 1541 over CIA2 port A"
  },
  "EA20": {
   "unit": "game/modemDriverE000",
   "line": "next payload byte ($EAED = packet byte 2)"
  },
  "EA23": {
   "unit": "game/modemDriverE000",
   "line": "advance the payload index"
  },
  "EA24": {
   "unit": "game/modemDriverE000",
   "line": "count the byte"
  },
  "EA27": {
   "unit": "game/modemDriverE000",
   "line": "loop until the counter wraps to zero"
  },
  "EA29": {
   "unit": "game/modemDriverE000",
   "line": "send the last byte"
  },
  "EA2C": {
   "unit": "game/modemDriverE000",
   "line": "restore the interrupt state"
  },
  "EA2D": {
   "unit": "game/modemDriverE000",
   "line": "done with the pass-through"
  },
  "EA2E": {
   "unit": "game/modemDriverE000",
   "line": "remember the received control byte for the sequence comparison"
  },
  "EA31": {
   "unit": "game/modemDriverE000",
   "line": "is any message of ours outstanding?"
  },
  "EA34": {
   "unit": "game/modemDriverE000",
   "line": "no - there is nothing to retire"
  },
  "EA36": {
   "unit": "game/modemDriverE000",
   "line": "control bytes $EC and above also ask for the outstanding message to be resent"
  },
  "EA38": {
   "unit": "game/modemDriverE000",
   "line": "below $EC: a plain acknowledgement"
  },
  "EA3A": {
   "unit": "game/modemDriverE000",
   "line": "ask runPacketPhase to resend the message named by $EAC6 (A is that negative index)"
  },
  "EA3D": {
   "unit": "game/modemDriverE000",
   "line": "strip the in-use bit to get the ring index"
  },
  "EA3F": {
   "unit": "game/modemDriverE000",
   "line": "index the ring with it"
  },
  "EA40": {
   "unit": "game/modemDriverE000",
   "line": "skip the message's argument-count byte"
  },
  "EA43": {
   "unit": "game/modemDriverE000",
   "line": "read its control byte"
  },
  "EA46": {
   "unit": "game/modemDriverE000",
   "line": "the message's own sequence number sits in bits 2-3..."
  },
  "EA47": {
   "unit": "game/modemDriverE000",
   "line": "...so shift it down to bits 0-1"
  },
  "EA48": {
   "unit": "game/modemDriverE000",
   "line": "CLC before SBC subtracts one extra..."
  },
  "EA49": {
   "unit": "game/modemDriverE000",
   "line": "...from the sequence carried in the ack byte's low two bits"
  },
  "EA4C": {
   "unit": "game/modemDriverE000",
   "line": "bit 1 of the difference separates 'covered by this ack' from 'not yet acknowledged'"
  },
  "EA4E": {
   "unit": "game/modemDriverE000",
   "line": "acknowledged - the send window is clear again"
  },
  "EA50": {
   "unit": "game/modemDriverE000",
   "line": "not acknowledged: shuffle the outstanding slots down one place..."
  },
  "EA53": {
   "unit": "game/modemDriverE000",
   "line": "...moving the newest index..."
  },
  "EA56": {
   "unit": "game/modemDriverE000",
   "line": "...into the free slot..."
  },
  "EA59": {
   "unit": "game/modemDriverE000",
   "line": "...and the old free slot into the oldest-unacknowledged slot"
  },
  "EA5C": {
   "unit": "game/modemDriverE000",
   "line": "another message is still outstanding - test that one too"
  },
  "EA5E": {
   "unit": "game/modemDriverE000",
   "line": "the control byte again"
  },
  "EA61": {
   "unit": "game/modemDriverE000",
   "line": "$E0-$EF are pure acknowledgements"
  },
  "EA63": {
   "unit": "game/modemDriverE000",
   "line": "an ack carries no payload, so there is nothing left to do"
  },
  "EA65": {
   "unit": "game/modemDriverE000",
   "line": "a data packet, so we now owe the peer an acknowledgement"
  },
  "EA68": {
   "unit": "game/modemDriverE000",
   "line": "remember the control byte"
  },
  "EA6B": {
   "unit": "game/modemDriverE000",
   "line": "its 2-bit sequence number sits in bits 2-3..."
  },
  "EA6C": {
   "unit": "game/modemDriverE000",
   "line": "...shift it down"
  },
  "EA6D": {
   "unit": "game/modemDriverE000",
   "line": "CLC before SBC subtracts one extra, so a difference of 0 means 'the very next packet'"
  },
  "EA6E": {
   "unit": "game/modemDriverE000",
   "line": "compare with the sequence we expect"
  },
  "EA71": {
   "unit": "game/modemDriverE000",
   "line": "only the two sequence bits matter"
  },
  "EA73": {
   "unit": "game/modemDriverE000",
   "line": "and the game's inbound command ring must be empty before we hand it anything"
  },
  "EA76": {
   "unit": "game/modemDriverE000",
   "line": "out of sequence or the game is still busy: drop the packet, the peer will resend it"
  },
  "EA78": {
   "unit": "game/modemDriverE000",
   "line": "accept it: the next packet should carry the following sequence number"
  },
  "EA7B": {
   "unit": "game/modemDriverE000",
   "routine": [
    "deliverPacketPayload - Hands an accepted data packet to the game.  The command byte is rebuilt as (control byte AND $F0) OR (argument count AND $0F) - the low nibble the sender replaced with the sequence number is restored from the packet length - and it and its arguments are pushed one at a time into the game's 18-byte inbound command ring $E45C.",
    "In:  packetLength $EAEA, packetBuffer, packetControlByte $EAEC",
    "Out: command byte plus arguments pushed into $E45C, $E459 incremented, $EAEA and $EAFE clobbered",
    "Called from: dispatchReceivedPacket, by fall-through from $EA78"
   ],
   "line": "total packet length, i.e. the index of the check byte"
  },
  "EA7E": {
   "unit": "game/modemDriverE000",
   "line": "minus the length code..."
  },
  "EA7F": {
   "unit": "game/modemDriverE000",
   "line": "...and the control byte..."
  },
  "EA81": {
   "unit": "game/modemDriverE000",
   "line": "...leaves the number of argument bytes, which is also the loop counter"
  },
  "EA84": {
   "unit": "game/modemDriverE000",
   "line": "classic bit merge: A EOR control byte..."
  },
  "EA87": {
   "unit": "game/modemDriverE000",
   "line": "...masked to the low nibble..."
  },
  "EA89": {
   "unit": "game/modemDriverE000",
   "line": "...EOR the control byte again gives (ctrl AND $F0) OR (argcount AND $0F) = the game's original command byte"
  },
  "EA8C": {
   "unit": "game/modemDriverE000",
   "line": "packet byte 1 is the control-byte slot; the first argument follows at byte 2"
  },
  "EA8E": {
   "unit": "game/modemDriverE000",
   "line": "save the buffer index - the push call clobbers X"
  },
  "EA91": {
   "unit": "game/modemDriverE000",
   "line": "push the byte into the game's inbound command ring $E45C"
  },
  "EA94": {
   "unit": "game/modemDriverE000",
   "line": "restore the index..."
  },
  "EA97": {
   "unit": "game/modemDriverE000",
   "line": "...and step to the next packet byte"
  },
  "EA98": {
   "unit": "game/modemDriverE000",
   "line": "load the argument byte"
  },
  "EA9B": {
   "unit": "game/modemDriverE000",
   "line": "one fewer argument to deliver"
  },
  "EA9E": {
   "unit": "game/modemDriverE000",
   "line": "loop, then fall into the shared RTS below"
  },
  "EAA0": {
   "unit": "game/modemDriverE000",
   "routine": [
    "packetReturn - A single RTS used as the common exit of the packet layer.  The same $60 byte is also read as packetLengthCodeTable entry 0, which can never be selected because a packet always carries at least one payload byte.",
    "In:  none",
    "Out: returns to the caller",
    "Called from: branch target from $EA63 and $EA76, fall-through from deliverPacketPayload; read as table entry 0 at $E953 and $E9CD"
   ],
   "line": "shared exit of the packet layer; also table entry 0 of packetLengthCodeTable, which is never matched"
  },
  "EAA1": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetLengthCodeTable - one byte per payload length 1..16, indexed as $EAA0,X.  Every code is a",
    "byte whose four bit pairs are each 01 or 10, so it can never be confused with the inter-packet bytes",
    "$00 and $FF and always carries plenty of edges for the receiver's bit sampler.  The table must stay",
    "sorted ascending: receivePacket searches it with CMP/BCS and gives up as soon as an entry is larger."
   ],
   "line": "codes for payload lengths 1 and 2"
  },
  "EAA3": {
   "unit": "game/modemDriverE000",
   "line": "codes for payload lengths 3 to 10"
  },
  "EAAB": {
   "unit": "game/modemDriverE000",
   "line": "codes for payload lengths 11 to 16, then the $FF sentinel that ends the search"
  },
  "EAB2": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearPacketState - Zeroes the entire 76-byte packet-protocol state block $EAB7-$EB02 in one call.  clearInlineVarBlock pops the return address to build a self-modified STA abs,X over the bytes that follow the JSR, so this routine returns two levels up, to the caller of initCommModule.",
    "In:  none",
    "Out: $EAB7-$EB02 = 0; returns to its caller's caller",
    "Called from: clearArqVars ($E2BD), i.e. jump-table entry $E003 with X = 1 right after the module is loaded"
   ],
   "line": "$4C = 76 bytes to clear"
  },
  "EAB4": {
   "unit": "game/modemDriverE000",
   "line": "zero the 76 bytes that follow this JSR; the helper consumes the return address, so the RTS lands in initCommModule's caller"
  },
  "EAB7": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetProtocolState - the whole state of the packet layer, $EAB7-$EB02, cleared in one go by the",
    "call above.  It is data, not code: the disassembler renders the first byte as BRK but nothing ever",
    "executes it.  Layout: sixteen scalars ($EAB7-$EAC6), the 35-byte transmit message ring ($EAC7),",
    "the packet length and assembly buffer ($EAEA-$EAFD) and five more scalars ($EAFE-$EB02)."
   ],
   "line": "syncRoundCounter: 0 = still waiting for the peer's $00, 1-9 = waiting for its $FF, 10 = give up and restart the sync"
  },
  "EAB8": {
   "unit": "game/modemDriverE000",
   "line": "zeroRunLength: consecutive $00 bytes received; 1 = a resend nak, 2 or more = the link has lost sync"
  },
  "EAB9": {
   "unit": "game/modemDriverE000",
   "line": "rxPayloadRemaining: payload bytes still to be read for the packet in progress; 0 = no packet in progress"
  },
  "EABA": {
   "unit": "game/modemDriverE000",
   "line": "packetByteCounter: scratch loop counter and index, saved across calls that clobber X"
  },
  "EABB": {
   "unit": "game/modemDriverE000",
   "line": "rxAckControlByte: control byte of the acknowledgement being processed"
  },
  "EABC": {
   "unit": "game/modemDriverE000",
   "line": "rxDataControlByte: control byte of the data packet being processed (written at $EA68, never read back)"
  },
  "EABD": {
   "unit": "game/modemDriverE000",
   "line": "rxSeqCounter: sequence number of the next data packet expected; its low two bits ride out in every packet we send"
  },
  "EABE": {
   "unit": "game/modemDriverE000",
   "line": "txSeqCounter: sequence number given to the next message we build"
  },
  "EABF": {
   "unit": "game/modemDriverE000",
   "line": "resendRequest: 0 = none, 1 = the peer sent a lone $00, negative = resend the message with this index"
  },
  "EAC0": {
   "unit": "game/modemDriverE000",
   "line": "ackPending: non-zero when the peer is owed an acknowledgement"
  },
  "EAC1": {
   "unit": "game/modemDriverE000",
   "line": "sendFfPending: request to put a raw $FF on the wire - nothing in the shipped image ever sets it"
  },
  "EAC2": {
   "unit": "game/modemDriverE000",
   "line": "sendZeroPending: made negative by a checksum failure so that a raw $00 nak goes out next tick"
  },
  "EAC3": {
   "unit": "game/modemDriverE000",
   "line": "txRingIndex: current read/write position in the 35-byte message ring"
  },
  "EAC4": {
   "unit": "game/modemDriverE000",
   "line": "txRingTailIndex: ring index just past the message most recently built"
  },
  "EAC5": {
   "unit": "game/modemDriverE000",
   "line": "txNextFreeIndex: where the next message may be written; negative means both outstanding slots are busy"
  },
  "EAC6": {
   "unit": "game/modemDriverE000",
   "line": "txOldestUnackedIndex: bit 7 = slot in use, bits 0-6 = ring index of the oldest unacknowledged message"
  },
  "EAC7": {
   "unit": "game/modemDriverE000",
   "block": [
    "txMessageRing - 35-byte circular buffer of queued outgoing messages.  Each entry is",
    "[argument count][control byte = (command AND $F0) OR (sequence << 2)][arguments...]; indices wrap",
    "at $23 and live in $EAC3-$EAC6.  Empty in the shipped image, because the only code that fills it",
    "is gated by $E0A6, which is never set."
   ],
   "line": "start of the 35-byte transmit message ring"
  },
  "EAEA": {
   "unit": "game/modemDriverE000",
   "line": "packetLength: number of packet bytes before the check byte, i.e. the index of the check byte slot"
  },
  "EAEB": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetBuffer - one packet under assembly or reception: [length code][control byte][payload",
    "0..14][check byte].  A payload is at most 16 bytes, so the buffer occupies $EAEB-$EAFD."
   ],
   "line": "packet byte 0 = the length code taken from packetLengthCodeTable"
  },
  "EAEC": {
   "unit": "game/modemDriverE000",
   "line": "packet byte 1 = the control byte: command nibble in bits 4-7, sequence in bits 2-3, piggy-backed ack in bits 0-1"
  },
  "EAED": {
   "unit": "game/modemDriverE000",
   "line": "packet byte 2 = the first payload/argument byte"
  },
  "EAFE": {
   "unit": "game/modemDriverE000",
   "line": "packetIndexSave: buffer index parked across calls that clobber X; also the drive pass-through byte counter"
  },
  "EAFF": {
   "unit": "game/modemDriverE000",
   "line": "rxLengthCodeByte: the length code of the packet being received, kept while waiting for its body"
  },
  "EB00": {
   "unit": "game/modemDriverE000",
   "line": "stallCounter: consecutive service ticks with no progress; the NMI zeroes it at $E716 on every character received and sixteen of them force a resync.  It is also the untouched base of the self-modified LDA $EBxx,X at $E3A1 that loads the AT strings"
  },
  "EB01": {
   "unit": "game/modemDriverE000",
   "line": "modemDelayCounter: guard-time delay, held as a negative tick count taken from the AT strings"
  },
  "EB02": {
   "unit": "game/modemDriverE000",
   "line": "lastModemChar: the previous character the modem sent, used to spot the '0' of 'CONNECT 1200'"
  },
  "EB03": {
   "unit": "game/modemDriverE000",
   "routine": [
    "remoteCodeBuffer - Landing area for 6502 code sent by the peer.  The message engine at $E196 appends the payload of every received message whose command byte is $70 or above here (self-modified index at $E1AE) and calls JSR $EB03 when such a message arrives with an empty payload.  The shipped first byte is $60 (RTS), so the call does nothing until the opponent has actually downloaded something.",
    "In:  code previously written by $E1B3",
    "Out: whatever the downloaded code does",
    "Called from: $E1A2 (JSR $EB03); written by $E1B3"
   ],
   "line": "shipped as a bare RTS, so the peer's 'run downloaded code' message is a no-op until code has been uploaded"
  },
  "EB04": {
   "unit": "game/modemDriverE000",
   "line": "$EB04-$EBD5: dead filler.  These 210 bytes are a fragment of the SOLO TRAINER build's code - they read the unit arrays at $F6A4/$F7D0/$F834 and the trainer work RAM at $88xx/$89xx - that the mastering system left in the sector.  The modem driver only ever uses this area as a write buffer, so the bytes are never executed"
  },
  "EBD6": {
   "unit": "game/modemDriverE000",
   "block": [
    "The three canned Hayes command strings, stored BACK TO FRONT: queueModemCommandString ($E393)",
    "copies them forwards into the host-output ring at $E092 and sets the read index to the string",
    "length, and popHostOutRing walks that index downwards, so they leave the ring in reverse order.",
    "A byte with bit 7 set is never transmitted: runModemStateMachine parks it in modemDelayCounter",
    "$EB01 as a negative tick count, which is how the Hayes escape guard times are produced."
   ],
   "line": "modemHangUpString, emitted from $EBE1 downwards: delay $A0 (96 ticks), '+','+','+', delay $A0, CR, delay $C0 (64 ticks), 'A','T','H','0', CR - the Hayes escape sequence with its guard times followed by the hang-up command"
  },
  "EBDD": {
   "unit": "game/modemDriverE000",
   "line": "the two guard-time delay bytes and the '+++' escape; the $00 at $EBE2 terminates the copy"
  },
  "EBE3": {
   "unit": "game/modemDriverE000",
   "line": "modemAnswerInitString, emitted backwards as delay $C0, 'A','T','Q','0','V','1','X','1','A', CR - quiet off, verbose on, extended result codes, answer.  Selected by the C=+A hot key ($E37A)"
  },
  "EBE4": {
   "unit": "game/modemDriverE000",
   "line": "the string body, stored in reverse order"
  },
  "EBEF": {
   "unit": "game/modemDriverE000",
   "line": "the $00 terminator"
  },
  "EBF0": {
   "unit": "game/modemDriverE000",
   "line": "modemDialInitString, emitted backwards as delay $C0, 'A','T','Q','0','V','1','X','1','D', CR - the same setup but 'D' = dial.  Selected by the C=+O hot key ($E37E)"
  },
  "EBFB": {
   "unit": "game/modemDriverE000",
   "line": "leading CR and the $00 terminator"
  },
  "EBFD": {
   "unit": "game/modemDriverE000",
   "line": "hangUpStringPtrLo: low byte of modemHangUpString, read by hangUpModem ($E390); the high byte $EB is the un-patched half of the self-modified LDA at $E3A1"
  },
  "EBFE": {
   "unit": "game/modemDriverE000",
   "line": "modemInitStringPtrLoTable: low bytes of the answer ($EBE3) and dial ($EBF0) strings, indexed by isOriginateMode $E011 at $E2E0"
  },
  "EC00": {
   "unit": "game/modemDriverE000",
   "block": [
    "==========================================================================",
    "$EC00-$EFFF is the 'comm tail', loaded separately from track 34 sectors 12-15 by",
    "loadCommTailBuild2 ($1070).  Despite living inside the modem driver it is not comm code at all:",
    "it holds battlefield-map and unit helpers, the start-of-session handshake, the film-snapshot",
    "loader and a block of message text, all called by overlay A.  The solo trainer has its own,",
    "completely different, tail at the same addresses.",
    "=========================================================================="
   ],
   "routine": [
    "clearBattlefieldMap - Fills all 1600 cells of the 40x40 battlefield map $F000-$F63F with $40, the empty-terrain code, using the shared pointer-advance helper advanceFillPointer ($C3F9).",
    "In:  none",
    "Out: $F000-$F63F = $40; zp_48/zp_49 left at $F640, zp_4A/zp_4B = $F63F, the zp_4E/zp_4F counter advanced by 1600",
    "Called from: overlay A: generateBattlefieldMap ($6F30) and the map editor ($76F2)"
   ],
   "line": "low byte of the first map cell..."
  },
  "EC02": {
   "unit": "game/modemDriverE000",
   "line": "...into the fill pointer zp_48/zp_49"
  },
  "EC04": {
   "unit": "game/modemDriverE000",
   "line": "map base page"
  },
  "EC06": {
   "unit": "game/modemDriverE000",
   "line": "fill pointer = $F000 = map cell (0,0)"
  },
  "EC08": {
   "unit": "game/modemDriverE000",
   "line": "low byte of the last map cell..."
  },
  "EC0A": {
   "unit": "game/modemDriverE000",
   "line": "...into the fill limit zp_4A/zp_4B"
  },
  "EC0C": {
   "unit": "game/modemDriverE000",
   "line": "limit page"
  },
  "EC0E": {
   "unit": "game/modemDriverE000",
   "line": "limit = $F63F = map cell (39,39); 40*40 = 1600 cells"
  },
  "EC10": {
   "unit": "game/modemDriverE000",
   "line": "always index 0 - the pointer itself walks"
  },
  "EC12": {
   "unit": "game/modemDriverE000",
   "line": "$40 = the empty terrain code (the map alphabet runs $40-$58)"
  },
  "EC14": {
   "unit": "game/modemDriverE000",
   "line": "clear this cell"
  },
  "EC16": {
   "unit": "game/modemDriverE000",
   "line": "C = 1 once the pointer has reached the limit, then step it on one cell"
  },
  "EC19": {
   "unit": "game/modemDriverE000",
   "line": "loop over all 1600 cells"
  },
  "EC1B": {
   "unit": "game/modemDriverE000",
   "line": "the map is blank"
  },
  "EC1C": {
   "unit": "game/modemDriverE000",
   "routine": [
    "setViewOriginToHome - Puts the 7x5 tactical view back at the middle of the player's own end of the battlefield: origin column 17, row 28, so the cursor cell (origin+3, origin+2) lands on map (20,30).  Both the live zero-page pair and the saved copy at $92C3/$92C4 are set.",
    "In:  none",
    "Out: zp_A5 = $11, zp_A6 = $1C, $92C3 = $11, $92C4 = $1C",
    "Called from: overlay A at $7AEC (battle start), $7D51 (film playback) and $83EF (recycler placement)"
   ],
   "line": "column 17: the view is 7 cells wide, so the cursor sits on column 20, the middle of the 40-cell map"
  },
  "EC1E": {
   "unit": "game/modemDriverE000",
   "line": "live view origin column"
  },
  "EC20": {
   "unit": "game/modemDriverE000",
   "line": "and the copy that survives a screen change"
  },
  "EC23": {
   "unit": "game/modemDriverE000",
   "line": "row 28: the view is 5 cells tall, so the cursor sits on row 30, well inside the player's own half"
  },
  "EC25": {
   "unit": "game/modemDriverE000",
   "line": "live view origin row"
  },
  "EC27": {
   "unit": "game/modemDriverE000",
   "line": "and its saved copy"
  },
  "EC2A": {
   "unit": "game/modemDriverE000",
   "line": "done"
  },
  "EC2B": {
   "unit": "game/modemDriverE000",
   "routine": [
    "loadMapSeedIntoScenarioRng - Copies the 24-bit map seed out of the game settings block into the scenario random-number state, so that both machines run the procedural map generator from the identical seed and get the identical battlefield.  That seed written in base 32 is exactly the map name the two players compare before a game.",
    "In:  mapSeed $0B95/$0B96/$0B97",
    "Out: zp_5A/zp_5B/zp_5C = the seed; A, Y and X left holding the three bytes",
    "Called from: overlay A: generateBattlefieldMap ($6F30-$6F33) and $7BB2"
   ],
   "line": "seed byte 0 from the settings block"
  },
  "EC2E": {
   "unit": "game/modemDriverE000",
   "line": "into the scenario RNG state"
  },
  "EC30": {
   "unit": "game/modemDriverE000",
   "line": "seed byte 1"
  },
  "EC33": {
   "unit": "game/modemDriverE000",
   "line": "into the RNG state"
  },
  "EC35": {
   "unit": "game/modemDriverE000",
   "line": "seed byte 2"
  },
  "EC38": {
   "unit": "game/modemDriverE000",
   "line": "into the RNG state - both players now generate the same map"
  },
  "EC3A": {
   "unit": "game/modemDriverE000",
   "line": "done"
  },
  "EC3B": {
   "unit": "game/modemDriverE000",
   "block": [
    "mirroredTerrainCodeTable - 25 entries indexed by terrainCode-$40, giving the code that must",
    "replace it when the battlefield is turned 180 degrees.  $40 (empty) maps to itself; the",
    "direction-dependent codes pair up A<->D, B<->C, E<->H, F<->G, I<->K, J<->L, M<->T, N<->Q, O<->S,",
    "P<->R, U<->X and V<->W.  mirrorTerrainCode also uses the table with a -$20 shift for the second",
    "code range $20-$3F."
   ],
   "line": "index 0 = code $40 (empty terrain), which maps to itself"
  },
  "EC3C": {
   "unit": "game/modemDriverE000",
   "line": "codes $41-$48 ('A'-'H') mirror to 'D','C','B','A','H','G','F','E'"
  },
  "EC44": {
   "unit": "game/modemDriverE000",
   "line": "codes $49-$4A ('I','J') mirror to 'K','L'"
  },
  "EC46": {
   "unit": "game/modemDriverE000",
   "line": "codes $4B-$52 ('K'-'R') mirror to 'I','J','T','Q','S','R','N','P'"
  },
  "EC4E": {
   "unit": "game/modemDriverE000",
   "line": "codes $53-$58 ('S'-'X') mirror to 'O','M','X','W','V','U'"
  },
  "EC54": {
   "unit": "game/modemDriverE000",
   "routine": [
    "makeMapPointSymmetric - Entered with mirrorMode zp_18 = $FF: walks a forward pointer up from $F000 and a backward pointer down from $F63F and writes mirrorTerrainCode(map[i]) into map[1599-i] without copying anything back the other way.  The net effect is that the second half of the map becomes the point-mirrored image of the first half, which is the map generator's fairness step.",
    "In:  the battlefield map $F000-$F63F",
    "Out: the map made point symmetric; zp_18 = $FF, zp_48/zp_49, zp_4A/zp_4B and zp_4E/zp_4F clobbered",
    "Called from: overlay A at $6F77 (generateBattlefieldMap)"
   ],
   "line": "$FF = mirror one way only, do not copy the far half back"
  },
  "EC56": {
   "unit": "game/modemDriverE000",
   "line": "always taken"
  },
  "EC58": {
   "unit": "game/modemDriverE000",
   "routine": [
    "rotateMapAndMirrorCodes - Entered with mirrorMode zp_18 = 0: exchanges map[i] and map[1599-i] over the whole map, passing both bytes through mirrorTerrainCode, i.e. rotates the battlefield 180 degrees and flips the direction-dependent terrain codes with it.  Together with mirrorAllUnitCoordinates this is what lets both players always see their own side at the bottom of the screen.",
    "In:  the battlefield map $F000-$F63F",
    "Out: the map rotated in place; zp_18 = 0, zp_48/zp_49, zp_4A/zp_4B and zp_4E/zp_4F clobbered",
    "Called from: overlay A at $76B5, $7749, $7AE1 and $7D2E"
   ],
   "line": "0 = swap in both directions, a true 180 degree rotation"
  },
  "EC5A": {
   "unit": "game/modemDriverE000",
   "line": "remember which of the two modes we are in"
  },
  "EC5C": {
   "unit": "game/modemDriverE000",
   "line": "low byte of the first map cell"
  },
  "EC5E": {
   "unit": "game/modemDriverE000",
   "line": "forward pointer zp_48/zp_49 low = $00"
  },
  "EC60": {
   "unit": "game/modemDriverE000",
   "line": "clear carry for the address arithmetic below"
  },
  "EC61": {
   "unit": "game/modemDriverE000",
   "line": "$00 + $3F..."
  },
  "EC63": {
   "unit": "game/modemDriverE000",
   "line": "...backward pointer zp_4A/zp_4B low = $3F"
  },
  "EC65": {
   "unit": "game/modemDriverE000",
   "line": "map base page"
  },
  "EC67": {
   "unit": "game/modemDriverE000",
   "line": "forward pointer = $F000 = map cell (0,0)"
  },
  "EC69": {
   "unit": "game/modemDriverE000",
   "line": "$F0 + $06..."
  },
  "EC6B": {
   "unit": "game/modemDriverE000",
   "line": "...backward pointer = $F63F = map cell (39,39)"
  },
  "EC6D": {
   "unit": "game/modemDriverE000",
   "line": "always index 0 - both pointers walk"
  },
  "EC6F": {
   "unit": "game/modemDriverE000",
   "block": [
    "Loop over the 800 cell pairs: the forward pointer climbs from $F000 and the backward pointer",
    "falls from $F63F until the two meet in the middle.  advanceFillPointer compares them and returns",
    "C = 1 once they have."
   ],
   "line": "the cell at the far end of the map"
  },
  "EC71": {
   "unit": "game/modemDriverE000",
   "line": "keep it on the stack"
  },
  "EC72": {
   "unit": "game/modemDriverE000",
   "line": "the cell at the near end"
  },
  "EC74": {
   "unit": "game/modemDriverE000",
   "line": "convert it to its 180-degree equivalent"
  },
  "EC77": {
   "unit": "game/modemDriverE000",
   "line": "and write it into the far cell"
  },
  "EC79": {
   "unit": "game/modemDriverE000",
   "line": "recover the far cell's original byte"
  },
  "EC7A": {
   "unit": "game/modemDriverE000",
   "line": "which mirror mode?"
  },
  "EC7C": {
   "unit": "game/modemDriverE000",
   "line": "$FF = symmetry pass, so do not write anything back into the near cell"
  },
  "EC7E": {
   "unit": "game/modemDriverE000",
   "line": "rotation pass: mirror the far cell too..."
  },
  "EC81": {
   "unit": "game/modemDriverE000",
   "line": "...and store it in the near cell, completing the swap"
  },
  "EC83": {
   "unit": "game/modemDriverE000",
   "line": "step the backward pointer down one cell - low byte first"
  },
  "EC85": {
   "unit": "game/modemDriverE000",
   "line": "no page underflow"
  },
  "EC87": {
   "unit": "game/modemDriverE000",
   "line": "borrow into the high byte"
  },
  "EC89": {
   "unit": "game/modemDriverE000",
   "line": "decrement the low byte"
  },
  "EC8B": {
   "unit": "game/modemDriverE000",
   "line": "step the forward pointer up and set C when the two pointers have met"
  },
  "EC8E": {
   "unit": "game/modemDriverE000",
   "line": "loop over all 800 pairs"
  },
  "EC90": {
   "unit": "game/modemDriverE000",
   "line": "the map is now rotated (or symmetric)"
  },
  "EC91": {
   "unit": "game/modemDriverE000",
   "routine": [
    "mirrorAllUnitCoordinates - Mirrors the six 100-entry unit coordinate arrays that follow the map, so that unit positions match a battlefield rotateMapAndMirrorCodes has just turned 180 degrees.  The arrays are $64 apart from $F640: unitCol $F640, unitRow $F6A4, unitDestCol $F7D0, unitDestRow $F834, unitPathCol $F898 and unitPathRow $F8FC.  The flags array $F708 and the type array $F76C hold no coordinates and are skipped.",
    "In:  the six coordinate arrays",
    "Out: every entry replaced by 39 minus its coordinate with the flag bits preserved; zp_48/zp_49 clobbered",
    "Called from: overlay A at $7AE9, only when the local player is side 1"
   ],
   "line": "unitColTable low byte..."
  },
  "EC93": {
   "unit": "game/modemDriverE000",
   "line": "...page: array 0 at $F640, the current column of each of the 100 units"
  },
  "EC95": {
   "unit": "game/modemDriverE000",
   "line": "mirror it"
  },
  "EC98": {
   "unit": "game/modemDriverE000",
   "line": "unitRowTable low byte..."
  },
  "EC9A": {
   "unit": "game/modemDriverE000",
   "line": "...page: array 1 at $F6A4, $64 further on, the current row"
  },
  "EC9C": {
   "unit": "game/modemDriverE000",
   "line": "mirror it"
  },
  "EC9F": {
   "unit": "game/modemDriverE000",
   "line": "unitDestColTable low byte..."
  },
  "ECA1": {
   "unit": "game/modemDriverE000",
   "line": "...page: array 4 at $F7D0, the ordered destination column"
  },
  "ECA3": {
   "unit": "game/modemDriverE000",
   "line": "mirror it"
  },
  "ECA6": {
   "unit": "game/modemDriverE000",
   "line": "unitDestRowTable low byte..."
  },
  "ECA8": {
   "unit": "game/modemDriverE000",
   "line": "...page: array 5 at $F834, the destination row"
  },
  "ECAA": {
   "unit": "game/modemDriverE000",
   "line": "mirror it"
  },
  "ECAD": {
   "unit": "game/modemDriverE000",
   "line": "unitPathColTable low byte..."
  },
  "ECAF": {
   "unit": "game/modemDriverE000",
   "line": "...page: array 6 at $F898, the way-point column"
  },
  "ECB1": {
   "unit": "game/modemDriverE000",
   "line": "mirror it"
  },
  "ECB4": {
   "unit": "game/modemDriverE000",
   "line": "unitPathRowTable low byte..."
  },
  "ECB6": {
   "unit": "game/modemDriverE000",
   "line": "...page: array 7 at $F8FC, the way-point row; fall straight into the mirror routine"
  },
  "ECB8": {
   "unit": "game/modemDriverE000",
   "routine": [
    "mirrorCoordinateArray - Replaces the coordinate in each of the 100 entries of the array at A/Y with 39 minus itself, leaving the two top flag bits of every entry untouched (they carry per-unit markers).",
    "In:  A = array low byte, Y = array high byte; mirrorMapCoordinate $3FA3",
    "Out: all 100 entries mirrored; zp_48/zp_49 = the array pointer; the ORA immediate operand at $ECCD is self-modified",
    "Called from: mirrorAllUnitCoordinates ($EC95, $EC9C, $ECA3, $ECAA, $ECB1 and by fall-through from $ECB6)"
   ],
   "line": "array pointer low"
  },
  "ECBA": {
   "unit": "game/modemDriverE000",
   "line": "array pointer high"
  },
  "ECBC": {
   "unit": "game/modemDriverE000",
   "line": "$63 = 99 = the last of the 100 unit slots"
  },
  "ECBE": {
   "unit": "game/modemDriverE000",
   "line": "the entry for this unit"
  },
  "ECC0": {
   "unit": "game/modemDriverE000",
   "line": "keep a copy"
  },
  "ECC1": {
   "unit": "game/modemDriverE000",
   "line": "isolate the two flag bits (bits 6-7)"
  },
  "ECC3": {
   "unit": "game/modemDriverE000",
   "line": "patch them into the ORA immediate operand at $ECCD - this routine's only state is self-modifying code"
  },
  "ECC6": {
   "unit": "game/modemDriverE000",
   "line": "back to the whole byte"
  },
  "ECC7": {
   "unit": "game/modemDriverE000",
   "line": "the coordinate lives in bits 0-5 (0..39 needs six bits)"
  },
  "ECC9": {
   "unit": "game/modemDriverE000",
   "line": "39 minus the coordinate"
  },
  "ECCC": {
   "unit": "game/modemDriverE000",
   "line": "operand patched at $ECC3: put the flag bits back"
  },
  "ECCE": {
   "unit": "game/modemDriverE000",
   "line": "store the mirrored entry"
  },
  "ECD0": {
   "unit": "game/modemDriverE000",
   "line": "previous unit"
  },
  "ECD1": {
   "unit": "game/modemDriverE000",
   "line": "loop over units 99 down to 0"
  },
  "ECD3": {
   "unit": "game/modemDriverE000",
   "line": "the array is mirrored"
  },
  "ECD4": {
   "unit": "game/modemDriverE000",
   "routine": [
    "mirrorTerrainCode - Maps one map cell byte to its 180-degree-rotated equivalent.  A cell with bit 7 set holds a unit index rather than terrain and is returned unchanged; codes $40-$58 are looked up directly in mirroredTerrainCodeTable, codes $20-$3F go through the same table with a -$20 shift, and anything else is passed through untouched.",
    "In:  A = map cell byte",
    "Out: A = the mirrored cell byte, X clobbered",
    "Called from: rotateMapAndMirrorCodes / makeMapPointSymmetric ($EC74 and $EC7E)"
   ],
   "line": "keep the original byte in X"
  },
  "ECD5": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 set: a unit stands here and the terrain under it is kept in the per-unit array - leave the cell alone"
  },
  "ECD7": {
   "unit": "game/modemDriverE000",
   "line": "no borrow"
  },
  "ECD8": {
   "unit": "game/modemDriverE000",
   "line": "convert to a table index by removing the $40 base of the terrain alphabet"
  },
  "ECDA": {
   "unit": "game/modemDriverE000",
   "line": "code was $40 or above - the normal range"
  },
  "ECDC": {
   "unit": "game/modemDriverE000",
   "line": "code was below $40: C is clear, so this adds $20-1 and maps the $20-$3F range onto indices $00-$1F"
  },
  "ECDE": {
   "unit": "game/modemDriverE000",
   "line": "index the table"
  },
  "ECDF": {
   "unit": "game/modemDriverE000",
   "line": "look up the mirrored code"
  },
  "ECE2": {
   "unit": "game/modemDriverE000",
   "line": "no borrow"
  },
  "ECE3": {
   "unit": "game/modemDriverE000",
   "line": "shift the answer back down into the $20-$3F range"
  },
  "ECE5": {
   "unit": "game/modemDriverE000",
   "line": "effectively unconditional: the table only holds $40-$58, so the result can never be zero"
  },
  "ECE7": {
   "unit": "game/modemDriverE000",
   "line": "$19 = 25 = the number of table entries"
  },
  "ECE9": {
   "unit": "game/modemDriverE000",
   "line": "index 0-24 is a valid terrain code"
  },
  "ECEB": {
   "unit": "game/modemDriverE000",
   "line": "out of range - hand back the original byte"
  },
  "ECEC": {
   "unit": "game/modemDriverE000",
   "line": "taken unless the cell byte was $00, which the map never contains"
  },
  "ECEE": {
   "unit": "game/modemDriverE000",
   "line": "index the table"
  },
  "ECEF": {
   "unit": "game/modemDriverE000",
   "line": "look up the mirrored code"
  },
  "ECF2": {
   "unit": "game/modemDriverE000",
   "line": "return the (possibly unchanged) cell byte"
  },
  "ECF3": {
   "unit": "game/modemDriverE000",
   "routine": [
    "loadUnitStartTemplate - Installs the unit start positions for the selected game type.  Track 18 sectors 8-14 are loaded to $F000 and hold four 400-byte templates rather than a battlefield; unitLayoutByGameType ($78B8) says which one this game type uses, and it is copied to $F640 where the live unit arrays begin.  Bit 7 of $F640 marks 'the template data is still present', so a second call does nothing.",
    "It copies two whole pages (512 bytes) although a template is only 400 bytes, so the 112 bytes",
    "after the type array - the start of unitDestCol $F7D0 - are filled with the beginning of the next",
    "template.  Harmless, because destinations are initialised later.",
    "In:  $F640 bit 7, gameTypeOptions $0BA3 bits 0-2, unitLayoutByGameType $78B8, template data at $F000",
    "Out: $F640-$F83F overwritten with the template (unitCol, unitRow, flags, type); the source and destination operands at $ED31/$ED32 and $ED34/$ED35 are self-modified",
    "Called from: overlay A at $79E6 and $82FD, before the map is generated"
   ],
   "line": "first byte of the live unit column array; while the templates are still resident it has bit 7 set"
  },
  "ECF6": {
   "unit": "game/modemDriverE000",
   "line": "the template has already been consumed - nothing to do"
  },
  "ECF8": {
   "unit": "game/modemDriverE000",
   "line": "the game options byte"
  },
  "ECFB": {
   "unit": "game/modemDriverE000",
   "line": "bits 0-2 = game type 0-7 (SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER)"
  },
  "ECFD": {
   "unit": "game/modemDriverE000",
   "line": "index the layout table with it"
  },
  "ECFE": {
   "unit": "game/modemDriverE000",
   "line": "template area low byte..."
  },
  "ED00": {
   "unit": "game/modemDriverE000",
   "line": "...patched into the LDA operand at $ED31"
  },
  "ED03": {
   "unit": "game/modemDriverE000",
   "line": "template area page..."
  },
  "ED05": {
   "unit": "game/modemDriverE000",
   "line": "...patched into the LDA operand at $ED32, so the copy reads from $F000"
  },
  "ED08": {
   "unit": "game/modemDriverE000",
   "line": "unit array low byte..."
  },
  "ED0A": {
   "unit": "game/modemDriverE000",
   "line": "...patched into the STA operand at $ED34"
  },
  "ED0D": {
   "unit": "game/modemDriverE000",
   "line": "unit array page..."
  },
  "ED0F": {
   "unit": "game/modemDriverE000",
   "line": "...patched into the STA operand at $ED35, so the copy writes to $F640"
  },
  "ED12": {
   "unit": "game/modemDriverE000",
   "line": "which of the four templates does this game type use?"
  },
  "ED15": {
   "unit": "game/modemDriverE000",
   "line": "template 0 - the source already points at it"
  },
  "ED17": {
   "unit": "game/modemDriverE000",
   "line": "use the template number as a repeat count"
  },
  "ED18": {
   "unit": "game/modemDriverE000",
   "line": "advance the patched source address..."
  },
  "ED1B": {
   "unit": "game/modemDriverE000",
   "line": "...clear carry..."
  },
  "ED1C": {
   "unit": "game/modemDriverE000",
   "line": "...by $190 = 400 bytes, the size of one template..."
  },
  "ED1E": {
   "unit": "game/modemDriverE000",
   "line": "...low byte..."
  },
  "ED21": {
   "unit": "game/modemDriverE000",
   "line": "...and..."
  },
  "ED24": {
   "unit": "game/modemDriverE000",
   "line": "...the..."
  },
  "ED26": {
   "unit": "game/modemDriverE000",
   "line": "...high byte"
  },
  "ED29": {
   "unit": "game/modemDriverE000",
   "line": "one template further"
  },
  "ED2A": {
   "unit": "game/modemDriverE000",
   "line": "repeat until the source points at the chosen template"
  },
  "ED2C": {
   "unit": "game/modemDriverE000",
   "line": "start at offset 0 of the page"
  },
  "ED2E": {
   "unit": "game/modemDriverE000",
   "line": "two pages = 512 bytes"
  },
  "ED30": {
   "unit": "game/modemDriverE000",
   "line": "operand patched at $ED00/$ED05 - reads the template ($FFFF is just what the shipped bytes happen to say)"
  },
  "ED33": {
   "unit": "game/modemDriverE000",
   "line": "operand patched at $ED0A/$ED0F - writes the live unit arrays"
  },
  "ED36": {
   "unit": "game/modemDriverE000",
   "line": "next byte"
  },
  "ED37": {
   "unit": "game/modemDriverE000",
   "line": "256 bytes per page"
  },
  "ED39": {
   "unit": "game/modemDriverE000",
   "line": "next source page"
  },
  "ED3C": {
   "unit": "game/modemDriverE000",
   "line": "next destination page"
  },
  "ED3F": {
   "unit": "game/modemDriverE000",
   "line": "one page done"
  },
  "ED40": {
   "unit": "game/modemDriverE000",
   "line": "copy the second page"
  },
  "ED42": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "ED43": {
   "unit": "game/modemDriverE000",
   "routine": [
    "exchangePlayerNamesAndSettings - The start-of-session handshake.  In a solo game it simply copies 'TRAINER   ' into the opponent name and returns.  In a modem game it waits for the link, swaps the ten-character owner name (read from the disk BAM into $FFDE) with the peer in two five-character messages, substitutes 'ENEMY' when the peer's name is blank, and then swaps a four-byte settings message carrying the comm build id, the game version byte and the program checksum.  The EOR of the peer's last two bytes is left in $90F8, which the caller uses as a 'same build?' test.  Any length or magic mismatch drops the player back to the main menu.",
    "In:  isSoloTrainer $0BA5 bit 7, ownPlayerName $FFDE, commBuildId $E030, gameVersionByte $0B94, programChecksum $0B7E, the comm API $E000 and the message buffers $E01D-$E02A",
    "Out: opponentPlayerName $FFE8-$FFF1, the peer's build id in $E02F, the peer's version signature in $90F8; link messages sent and consumed",
    "Called from: overlay A at $79D1, which pre-loads $90F8 with $DB and shows 'OPPONENT USING NON-STANDARD GAME.' ($895E) if it comes back different"
   ],
   "line": "bit 7 set = practice with the solo trainer, so there is no link to talk to"
  },
  "ED46": {
   "unit": "game/modemDriverE000",
   "line": "modem game - do the real handshake"
  },
  "ED48": {
   "unit": "game/modemDriverE000",
   "line": "the opponent name is ten characters"
  },
  "ED4A": {
   "unit": "game/modemDriverE000",
   "line": "'TRAINER   ' at $884E"
  },
  "ED4D": {
   "unit": "game/modemDriverE000",
   "line": "into the opponent name at $FFE8"
  },
  "ED50": {
   "unit": "game/modemDriverE000",
   "line": "previous character"
  },
  "ED51": {
   "unit": "game/modemDriverE000",
   "line": "copy all ten"
  },
  "ED53": {
   "unit": "game/modemDriverE000",
   "line": "done - a solo game has no peer"
  },
  "ED54": {
   "unit": "game/modemDriverE000",
   "line": "wait until the comm module reports a completed exchange"
  },
  "ED57": {
   "unit": "game/modemDriverE000",
   "line": "X = 2 = release the received message"
  },
  "ED59": {
   "unit": "game/modemDriverE000",
   "line": "call jump-table entry $E000 to free the receive buffer"
  },
  "ED5C": {
   "unit": "game/modemDriverE000",
   "line": "X = 0 = name characters 0-4"
  },
  "ED5E": {
   "unit": "game/modemDriverE000",
   "line": "swap the first half of the name"
  },
  "ED61": {
   "unit": "game/modemDriverE000",
   "line": "X = 5..."
  },
  "ED63": {
   "unit": "game/modemDriverE000",
   "line": "...swap name characters 5-9"
  },
  "ED66": {
   "unit": "game/modemDriverE000",
   "line": "first character of the name the peer sent"
  },
  "ED69": {
   "unit": "game/modemDriverE000",
   "line": "$A0 = shifted space: the peer's disk carries no owner name"
  },
  "ED6B": {
   "unit": "game/modemDriverE000",
   "line": "the peer has a name - keep it"
  },
  "ED6D": {
   "unit": "game/modemDriverE000",
   "line": "'ENEMY' is five characters"
  },
  "ED6F": {
   "unit": "game/modemDriverE000",
   "line": "'ENEMY' at $CC99"
  },
  "ED72": {
   "unit": "game/modemDriverE000",
   "line": "overwrite the blank name"
  },
  "ED75": {
   "unit": "game/modemDriverE000",
   "line": "previous character"
  },
  "ED76": {
   "unit": "game/modemDriverE000",
   "line": "copy all five"
  },
  "ED78": {
   "unit": "game/modemDriverE000",
   "line": "$FF..."
  },
  "ED7A": {
   "unit": "game/modemDriverE000",
   "line": "...is the magic first byte of every handshake message"
  },
  "ED7D": {
   "unit": "game/modemDriverE000",
   "line": "commBuildId ($E030): 0 for the modem build, bit 7 set for the trainer build"
  },
  "ED80": {
   "unit": "game/modemDriverE000",
   "line": "message byte 1"
  },
  "ED83": {
   "unit": "game/modemDriverE000",
   "line": "the game version byte"
  },
  "ED86": {
   "unit": "game/modemDriverE000",
   "line": "message byte 2"
  },
  "ED89": {
   "unit": "game/modemDriverE000",
   "line": "the program checksum computed at boot"
  },
  "ED8C": {
   "unit": "game/modemDriverE000",
   "line": "message byte 3"
  },
  "ED8F": {
   "unit": "game/modemDriverE000",
   "line": "four bytes..."
  },
  "ED91": {
   "unit": "game/modemDriverE000",
   "line": "...is the outgoing message length"
  },
  "ED94": {
   "unit": "game/modemDriverE000",
   "line": "X = 1 = transmit the outgoing message"
  },
  "ED96": {
   "unit": "game/modemDriverE000",
   "line": "send it"
  },
  "ED99": {
   "unit": "game/modemDriverE000",
   "line": "wait for the peer's reply"
  },
  "ED9C": {
   "unit": "game/modemDriverE000",
   "line": "length of the message that came back"
  },
  "ED9F": {
   "unit": "game/modemDriverE000",
   "line": "it must also be four bytes"
  },
  "EDA1": {
   "unit": "game/modemDriverE000",
   "line": "protocol error - bail out to the main menu"
  },
  "EDA3": {
   "unit": "game/modemDriverE000",
   "line": "first byte of the reply"
  },
  "EDA6": {
   "unit": "game/modemDriverE000",
   "line": "it must be the $FF magic"
  },
  "EDA8": {
   "unit": "game/modemDriverE000",
   "line": "protocol error - bail out"
  },
  "EDAA": {
   "unit": "game/modemDriverE000",
   "line": "the peer's comm build id"
  },
  "EDAD": {
   "unit": "game/modemDriverE000",
   "line": "remember it in $E02F"
  },
  "EDB0": {
   "unit": "game/modemDriverE000",
   "line": "the peer's program checksum..."
  },
  "EDB3": {
   "unit": "game/modemDriverE000",
   "line": "...EOR its version byte..."
  },
  "EDB6": {
   "unit": "game/modemDriverE000",
   "line": "...parked in $90F8, which the caller pre-loaded with $DB and compares afterwards to warn about a different build"
  },
  "EDB9": {
   "unit": "game/modemDriverE000",
   "line": "X = 2 = release the received message"
  },
  "EDBB": {
   "unit": "game/modemDriverE000",
   "line": "free the receive buffer, then fall into sendEmptyMessage"
  },
  "EDBE": {
   "unit": "game/modemDriverE000",
   "routine": [
    "sendEmptyMessage - Sets the outgoing message length to zero and asks the comm module to transmit it, which closes the handshake and, on the error path, unblocks the peer.",
    "In:  none",
    "Out: $E01F = 0 and jump-table entry $E000 called with X = 1",
    "Called from: the tail of exchangePlayerNamesAndSettings (fall-through from $EDBB) and abortLinkAndReturnToMenu ($EE09)"
   ],
   "line": "zero length..."
  },
  "EDC0": {
   "unit": "game/modemDriverE000",
   "line": "...outgoing message"
  },
  "EDC3": {
   "unit": "game/modemDriverE000",
   "line": "X = 1 = transmit"
  },
  "EDC5": {
   "unit": "game/modemDriverE000",
   "line": "send the empty message"
  },
  "EDC8": {
   "unit": "game/modemDriverE000",
   "line": "the handshake is finished"
  },
  "EDC9": {
   "unit": "game/modemDriverE000",
   "routine": [
    "exchangeNameHalf - Sends one half of the local player's name - a six-byte message of $FF followed by ownPlayerName[X..X+4] - waits for the peer's reply and checks that it is six bytes long and starts with the same $FF magic.  X is carried across the blocking wait in a self-modified LDX immediate.  Any mismatch falls into abortLinkAndReturnToMenu.",
    "In:  X = 0 or 5, the offset into the ten-byte name; ownPlayerName $FFDE",
    "Out: opponentPlayerName $FFE8+X..+4 filled in; the LDX immediate operand at $EDEB is self-modified",
    "Called from: exchangePlayerNamesAndSettings ($ED5E with X = 0 and $ED63 with X = 5)"
   ],
   "line": "park the name offset in the LDX immediate at $EDEB so it survives the blocking wait below"
  },
  "EDCC": {
   "unit": "game/modemDriverE000",
   "line": "$FF..."
  },
  "EDCE": {
   "unit": "game/modemDriverE000",
   "line": "...the handshake magic goes in message byte 0"
  },
  "EDD1": {
   "unit": "game/modemDriverE000",
   "line": "message bytes 1-5 hold the name characters"
  },
  "EDD3": {
   "unit": "game/modemDriverE000",
   "line": "character X of our own name"
  },
  "EDD6": {
   "unit": "game/modemDriverE000",
   "line": "into the outgoing message"
  },
  "EDD9": {
   "unit": "game/modemDriverE000",
   "line": "next name character"
  },
  "EDDA": {
   "unit": "game/modemDriverE000",
   "line": "next message byte"
  },
  "EDDB": {
   "unit": "game/modemDriverE000",
   "line": "six bytes in all"
  },
  "EDDD": {
   "unit": "game/modemDriverE000",
   "line": "copy five characters"
  },
  "EDDF": {
   "unit": "game/modemDriverE000",
   "line": "Y = 6 = the outgoing message length"
  },
  "EDE2": {
   "unit": "game/modemDriverE000",
   "line": "X = 1 = transmit"
  },
  "EDE4": {
   "unit": "game/modemDriverE000",
   "line": "send our half of the name"
  },
  "EDE7": {
   "unit": "game/modemDriverE000",
   "line": "block until the peer's half arrives"
  },
  "EDEA": {
   "unit": "game/modemDriverE000",
   "line": "operand patched at $EDC9: recover the name offset"
  },
  "EDEC": {
   "unit": "game/modemDriverE000",
   "line": "length of the message that came back"
  },
  "EDEF": {
   "unit": "game/modemDriverE000",
   "line": "it must be six bytes"
  },
  "EDF1": {
   "unit": "game/modemDriverE000",
   "line": "protocol error - bail out to the main menu"
  },
  "EDF3": {
   "unit": "game/modemDriverE000",
   "line": "the payload starts at message byte 1"
  },
  "EDF5": {
   "unit": "game/modemDriverE000",
   "line": "message byte 0"
  },
  "EDF8": {
   "unit": "game/modemDriverE000",
   "line": "it must be the $FF magic"
  },
  "EDFA": {
   "unit": "game/modemDriverE000",
   "line": "good - copy the peer's five characters"
  },
  "EDFC": {
   "unit": "game/modemDriverE000",
   "routine": [
    "abortLinkAndReturnToMenu - The handshake failure path: show message/sound 3, wait a second, let the engine finish the exchange it is in the middle of, release the peer with an empty message and jump back to the main menu.  It never returns to its caller.",
    "In:  none",
    "Out: the game is back at the main menu; this routine does not return",
    "Called from: exchangeNameHalf ($EDF1 and by fall-through past $EDFA) and exchangePlayerNamesAndSettings ($EDA1, $EDA8)"
   ],
   "line": "message/sound id 3 - the error beep"
  },
  "EDFE": {
   "unit": "game/modemDriverE000",
   "line": "show it and play its sound immediately, bypassing the message queue"
  },
  "EE01": {
   "unit": "game/modemDriverE000",
   "line": "$3C = 60 frames..."
  },
  "EE03": {
   "unit": "game/modemDriverE000",
   "line": "...one second, so the player can read it"
  },
  "EE06": {
   "unit": "game/modemDriverE000",
   "line": "let the engine finish the exchange it was in the middle of"
  },
  "EE09": {
   "unit": "game/modemDriverE000",
   "line": "release the peer with a zero-length message"
  },
  "EE0C": {
   "unit": "game/modemDriverE000",
   "line": "back to the main menu"
  },
  "EE0F": {
   "unit": "game/modemDriverE000",
   "routine": [
    "storeOpponentNameHalf - Success tail of exchangeNameHalf: copies the five payload bytes of the received message into the opponent name and releases the receive buffer.",
    "In:  X = the name offset, Y = 1, the received message at $E020",
    "Out: opponentPlayerName $FFE8+X..+4 written; the comm module's receive buffer released (jump-table entry $E000 with X = 2)",
    "Called from: exchangeNameHalf ($EDFA)"
   ],
   "line": "payload byte of the received message"
  },
  "EE12": {
   "unit": "game/modemDriverE000",
   "line": "into the opponent's name"
  },
  "EE15": {
   "unit": "game/modemDriverE000",
   "line": "next name character"
  },
  "EE16": {
   "unit": "game/modemDriverE000",
   "line": "next message byte"
  },
  "EE17": {
   "unit": "game/modemDriverE000",
   "line": "six bytes in the message"
  },
  "EE19": {
   "unit": "game/modemDriverE000",
   "line": "copy all five characters"
  },
  "EE1B": {
   "unit": "game/modemDriverE000",
   "line": "X = 2 = release the received message"
  },
  "EE1D": {
   "unit": "game/modemDriverE000",
   "line": "free the receive buffer and return through the comm module"
  },
  "EE20": {
   "unit": "game/modemDriverE000",
   "routine": [
    "waitForLinkMessage - Busy-waits until bit 6 of the comm status byte $E01D is set, i.e. until the module reports that an exchange has completed: a message has been received, or the pending transmit has gone out.",
    "In:  $E01D exchange flags, kept up to date by the raster IRQ",
    "Out: returns when bit 6 is set; no registers changed",
    "Called from: exchangePlayerNamesAndSettings ($ED54, $ED99) and exchangeNameHalf ($EDE7)"
   ],
   "line": "test the exchange flag byte; bit 6 (V) = an exchange has completed"
  },
  "EE23": {
   "unit": "game/modemDriverE000",
   "line": "spin until the raster IRQ sets it"
  },
  "EE25": {
   "unit": "game/modemDriverE000",
   "line": "a message is waiting"
  },
  "EE26": {
   "unit": "game/modemDriverE000",
   "routine": [
    "finaliseRecyclerSetup - Last step of the setup phase.  For each side, a recyclerMode of 2 means 'this side has no recycler': its coordinates are marked $FF, and if it is the opponent's side the 'SPOTTED RECYCLER!' scan checkEnemyRecyclerSpotted ($3F57) is disabled by patching an RTS over its first byte.  Finally the local player's recycler is drawn onto the map.",
    "In:  recyclerMode $92A6[side], playerSide $0B9F",
    "Out: $92A8/$92AA set to $FF for a side without a recycler, $3F57 possibly patched to $60, and the local recycler drawn into the map",
    "Called from: overlay A at $7BAF, by JMP"
   ],
   "line": "start with side 1, then side 0"
  },
  "EE28": {
   "unit": "game/modemDriverE000",
   "line": "how this side's recycler was configured"
  },
  "EE2B": {
   "unit": "game/modemDriverE000",
   "line": "2 = 'none', the setup menu's third choice"
  },
  "EE2D": {
   "unit": "game/modemDriverE000",
   "line": "this side does have a recycler - leave it alone"
  },
  "EE2F": {
   "unit": "game/modemDriverE000",
   "line": "$FF = 'no position'..."
  },
  "EE31": {
   "unit": "game/modemDriverE000",
   "line": "...marks the recycler column..."
  },
  "EE34": {
   "unit": "game/modemDriverE000",
   "line": "...and the row as unset"
  },
  "EE37": {
   "unit": "game/modemDriverE000",
   "line": "is this the local player's own side?"
  },
  "EE3A": {
   "unit": "game/modemDriverE000",
   "line": "yes - keep the spotted-recycler scan alive"
  },
  "EE3C": {
   "unit": "game/modemDriverE000",
   "line": "$60 = the RTS opcode"
  },
  "EE3E": {
   "unit": "game/modemDriverE000",
   "line": "patch it over the first byte of checkEnemyRecyclerSpotted so the 'SPOTTED RECYCLER!' scan never runs"
  },
  "EE41": {
   "unit": "game/modemDriverE000",
   "line": "next side"
  },
  "EE42": {
   "unit": "game/modemDriverE000",
   "line": "loop over sides 1 and 0"
  },
  "EE44": {
   "unit": "game/modemDriverE000",
   "line": "Y = the local player's side"
  },
  "EE47": {
   "unit": "game/modemDriverE000",
   "line": "draw his recycler onto the battlefield map and return through it"
  },
  "EE4A": {
   "unit": "game/modemDriverE000",
   "routine": [
    "loadFilmStartSnapshot - Restores the game state a 'game film' was recorded from.  The film buffer lives in the RAM under the I/O area at $D000, so every access goes through readFilmByte/copyPageUnderIo which bank $01 to $34 with the CIA2 interrupts held off.  Twenty bytes of scores and options are read back, four pages of unit arrays are copied into place, and the film pointer is then set to the first recorded command.",
    "In:  the film buffer at $D000+, readFilmByte $5874, copyPageUnderIo $58B2, rewindFilmPtr $58D5, getFilmSnapshotDestAddr $5983, getFilmSnapshotSrcAddr $5988",
    "Out: $92A2-$92B5 (20 bytes), the unit arrays from $F640 up, gameClock $91CB, filmPtr zp_BC/zp_BD; the copy operands $58C3/$58C4/$58C6/$58C7 and zp_19 clobbered",
    "Called from: overlay A at $7D41, at the start of film playback"
   ],
   "line": "film header offset $34..."
  },
  "EE4C": {
   "unit": "game/modemDriverE000",
   "line": "...low byte of the film pointer"
  },
  "EE4E": {
   "unit": "game/modemDriverE000",
   "line": "the film buffer lives under the I/O area..."
  },
  "EE50": {
   "unit": "game/modemDriverE000",
   "line": "...so the pointer is $D034, just past the film directory entry"
  },
  "EE52": {
   "unit": "game/modemDriverE000",
   "line": "start of the score/option block"
  },
  "EE54": {
   "unit": "game/modemDriverE000",
   "line": "read one byte from under the I/O area"
  },
  "EE57": {
   "unit": "game/modemDriverE000",
   "line": "into the score and option block at $92A2"
  },
  "EE5A": {
   "unit": "game/modemDriverE000",
   "line": "next byte"
  },
  "EE5B": {
   "unit": "game/modemDriverE000",
   "line": "$14 = 20 bytes"
  },
  "EE5D": {
   "unit": "game/modemDriverE000",
   "line": "loop"
  },
  "EE5F": {
   "unit": "game/modemDriverE000",
   "line": "source low byte..."
  },
  "EE61": {
   "unit": "game/modemDriverE000",
   "line": "...into copyPageUnderIo's source operand"
  },
  "EE64": {
   "unit": "game/modemDriverE000",
   "line": "source page..."
  },
  "EE66": {
   "unit": "game/modemDriverE000",
   "line": "...so the first page comes from $D048"
  },
  "EE69": {
   "unit": "game/modemDriverE000",
   "line": "destination low byte..."
  },
  "EE6B": {
   "unit": "game/modemDriverE000",
   "line": "...into copyPageUnderIo's destination operand"
  },
  "EE6E": {
   "unit": "game/modemDriverE000",
   "line": "destination page..."
  },
  "EE70": {
   "unit": "game/modemDriverE000",
   "line": "...so it lands on the unit arrays at $F640"
  },
  "EE73": {
   "unit": "game/modemDriverE000",
   "line": "copy 256 bytes with the I/O area banked out"
  },
  "EE76": {
   "unit": "game/modemDriverE000",
   "line": "A/X = $D268, the last page of the snapshot in the film buffer"
  },
  "EE79": {
   "unit": "game/modemDriverE000",
   "line": "into the source operand low..."
  },
  "EE7C": {
   "unit": "game/modemDriverE000",
   "line": "...and high"
  },
  "EE7F": {
   "unit": "game/modemDriverE000",
   "line": "A/X = $F860, the last page of the unit arrays"
  },
  "EE82": {
   "unit": "game/modemDriverE000",
   "line": "into the destination operand low..."
  },
  "EE85": {
   "unit": "game/modemDriverE000",
   "line": "...and high"
  },
  "EE88": {
   "unit": "game/modemDriverE000",
   "line": "three more pages, counted 2, 1, 0..."
  },
  "EE8A": {
   "unit": "game/modemDriverE000",
   "line": "...in zp_19"
  },
  "EE8C": {
   "unit": "game/modemDriverE000",
   "line": "copy one page"
  },
  "EE8F": {
   "unit": "game/modemDriverE000",
   "line": "walk the source down one page"
  },
  "EE92": {
   "unit": "game/modemDriverE000",
   "line": "and the destination down one page"
  },
  "EE95": {
   "unit": "game/modemDriverE000",
   "line": "one page fewer"
  },
  "EE97": {
   "unit": "game/modemDriverE000",
   "line": "loop for three pages"
  },
  "EE99": {
   "unit": "game/modemDriverE000",
   "line": "film pointer back to $D000"
  },
  "EE9C": {
   "unit": "game/modemDriverE000",
   "line": "header offset $30"
  },
  "EE9E": {
   "unit": "game/modemDriverE000",
   "line": "read the recorded clock value"
  },
  "EEA1": {
   "unit": "game/modemDriverE000",
   "line": "restore the game clock"
  },
  "EEA4": {
   "unit": "game/modemDriverE000",
   "line": "header offset 3 = high byte of the first-command offset"
  },
  "EEA6": {
   "unit": "game/modemDriverE000",
   "line": "read it"
  },
  "EEA9": {
   "unit": "game/modemDriverE000",
   "line": "keep it in X"
  },
  "EEAA": {
   "unit": "game/modemDriverE000",
   "line": "header offset 2 = the low byte"
  },
  "EEAB": {
   "unit": "game/modemDriverE000",
   "line": "read it"
  },
  "EEAE": {
   "unit": "game/modemDriverE000",
   "line": "no carry into the addition"
  },
  "EEAF": {
   "unit": "game/modemDriverE000",
   "line": "the offset is relative to the film buffer, whose low byte is 0"
  },
  "EEB1": {
   "unit": "game/modemDriverE000",
   "line": "film pointer low"
  },
  "EEB3": {
   "unit": "game/modemDriverE000",
   "line": "the high half of the offset..."
  },
  "EEB4": {
   "unit": "game/modemDriverE000",
   "line": "...plus the film buffer page $D0"
  },
  "EEB6": {
   "unit": "game/modemDriverE000",
   "line": "film pointer high - playback now starts at the first recorded command"
  },
  "EEB8": {
   "unit": "game/modemDriverE000",
   "line": "done"
  },
  "EEB9": {
   "unit": "game/modemDriverE000",
   "routine": [
    "handleDisplayToggleKey - The solo trainer's F1 handler, polled every frame from the main idle loop at $600A but only while $0BA5 bit 7 (practice mode) is set.  It fires when the pending screen key is 0 (F1), sets a 60-frame input lockout and falls into announceAndToggleDisplayMode, whose first store also puts $3C into zp_66 so the key cannot retrigger.",
    "In:  requestedScreenKey zp_66 (0 = F1, $FF = none)",
    "Out: inputLockoutTimer $0B7D = $3C, zp_66 = $3C, the display mode toggled and two messages queued",
    "Called from: the main idle loop at $600A, in trainer mode only"
   ],
   "line": "the function key the IRQ poll last saw: 0 = F1, 1 = F3, 2 = F5, 3 = F7, $FF = none"
  },
  "EEBB": {
   "unit": "game/modemDriverE000",
   "line": "F1 - toggle the battle display"
  },
  "EEBD": {
   "unit": "game/modemDriverE000",
   "line": "any other key is not ours"
  },
  "EEBE": {
   "unit": "game/modemDriverE000",
   "line": "$3C = 60 frames..."
  },
  "EEC0": {
   "unit": "game/modemDriverE000",
   "line": "...of input lockout, so a held key does not repeat"
  },
  "EEC3": {
   "unit": "game/modemDriverE000",
   "routine": [
    "announceAndToggleDisplayMode - Queues message slot $21 with either 'BEGINNER DISPLAY SET.' or 'STANDARD DISPLAY SET.'.  The flag still holds the old value here, so the message names the mode that is about to be entered.  It then falls into toggleBattleDisplayMode, which performs the switch.",
    "In:  A = $3C from the caller, revealAllUnits $92FC",
    "Out: zp_66 = $3C so F1 does not retrigger; message slot $21 pointed at the right string and queued",
    "Called from: handleDisplayToggleKey, by fall-through from $EEC0"
   ],
   "line": "$3C is not a valid function-key code, so the key stops repeating"
  },
  "EEC5": {
   "unit": "game/modemDriverE000",
   "line": "'BEGINNER DISPLAY SET.' low byte..."
  },
  "EEC7": {
   "unit": "game/modemDriverE000",
   "line": "...page: the string at $EF28"
  },
  "EEC9": {
   "unit": "game/modemDriverE000",
   "line": "revealAllUnits: bit 7 set = beginner display, in which every enemy unit is drawn"
  },
  "EECC": {
   "unit": "game/modemDriverE000",
   "line": "currently standard, so beginner is what is about to be switched on"
  },
  "EECE": {
   "unit": "game/modemDriverE000",
   "line": "clear carry..."
  },
  "EECF": {
   "unit": "game/modemDriverE000",
   "line": "...+$15 = 21 bytes moves on to 'STANDARD DISPLAY SET.' at $EF3D"
  },
  "EED1": {
   "unit": "game/modemDriverE000",
   "line": "no page crossing"
  },
  "EED3": {
   "unit": "game/modemDriverE000",
   "line": "carry into the page"
  },
  "EED4": {
   "unit": "game/modemDriverE000",
   "line": "message slot $21"
  },
  "EED6": {
   "unit": "game/modemDriverE000",
   "line": "point that slot at the chosen string"
  },
  "EED9": {
   "unit": "game/modemDriverE000",
   "line": "A = the slot number (setMessageSlot leaves X untouched)"
  },
  "EEDA": {
   "unit": "game/modemDriverE000",
   "line": "queue it for display, then fall into the toggle itself"
  },
  "EEDD": {
   "unit": "game/modemDriverE000",
   "routine": [
    "toggleBattleDisplayMode - Flips revealAllUnits $92FC (bit 7 set = beginner display, in which renderMapTile at $284F draws every enemy unit), patches the eight-character hole at $EF10 with the name of the mode F1 will switch to NEXT, and queues the resulting prompt 'F 1 FOR xxxxxxxx BATTLE DISPLAY.' as message slot $20.",
    "In:  revealAllUnits $92FC",
    "Out: $92FC toggled, displayModeNameSlot $EF10-$EF17 patched, message slot $20 registered and queued",
    "Called from: announceAndToggleDisplayMode by fall-through, and overlay A at $824B when a trainer battle starts"
   ],
   "line": "Y = 0 selects 'BEGINNER' at $EF28"
  },
  "EEDF": {
   "unit": "game/modemDriverE000",
   "line": "the current display mode"
  },
  "EEE2": {
   "unit": "game/modemDriverE000",
   "line": "flip every bit..."
  },
  "EEE4": {
   "unit": "game/modemDriverE000",
   "line": "...so $00 becomes $FF and back"
  },
  "EEE7": {
   "unit": "game/modemDriverE000",
   "line": "now standard: the prompt should offer BEGINNER, so keep Y = 0"
  },
  "EEE9": {
   "unit": "game/modemDriverE000",
   "line": "now beginner: Y = $15 = 21 selects 'STANDARD' at $EF3D instead"
  },
  "EEEB": {
   "unit": "game/modemDriverE000",
   "line": "start of the eight-character hole in the prompt"
  },
  "EEED": {
   "unit": "game/modemDriverE000",
   "line": "one character of 'BEGINNER' or 'STANDARD' - the first eight characters of the two 'xxx DISPLAY SET.' strings"
  },
  "EEF0": {
   "unit": "game/modemDriverE000",
   "line": "patch it into the prompt at $EF10"
  },
  "EEF3": {
   "unit": "game/modemDriverE000",
   "line": "next source character"
  },
  "EEF4": {
   "unit": "game/modemDriverE000",
   "line": "next destination character"
  },
  "EEF5": {
   "unit": "game/modemDriverE000",
   "line": "eight characters"
  },
  "EEF7": {
   "unit": "game/modemDriverE000",
   "line": "copy them all"
  },
  "EEF9": {
   "unit": "game/modemDriverE000",
   "line": "message slot $20"
  },
  "EEFB": {
   "unit": "game/modemDriverE000",
   "line": "prompt string low byte..."
  },
  "EEFD": {
   "unit": "game/modemDriverE000",
   "line": "...page: 'F 1 FOR ' at $EF08"
  },
  "EEFF": {
   "unit": "game/modemDriverE000",
   "line": "point slot $20 at it"
  },
  "EF02": {
   "unit": "game/modemDriverE000",
   "line": "dead instruction - A is overwritten by the very next LDA.  Left over from a version that took the slot number from X, as $EED9 still does"
  },
  "EF03": {
   "unit": "game/modemDriverE000",
   "line": "message slot $20"
  },
  "EF05": {
   "unit": "game/modemDriverE000",
   "line": "queue the prompt and return through queueMessage"
  },
  "EACE": {
   "unit": "game/modemDriverE000",
   "line": "message ring byte 7"
  },
  "EACF": {
   "unit": "game/modemDriverE000",
   "line": "message ring bytes 8-15"
  },
  "EAD7": {
   "unit": "game/modemDriverE000",
   "line": "message ring bytes 16-23"
  },
  "EADF": {
   "unit": "game/modemDriverE000",
   "line": "message ring bytes 24-31"
  },
  "EAE7": {
   "unit": "game/modemDriverE000",
   "line": "message ring bytes 32-34, the last of the 35 slots"
  },
  "EAEE": {
   "unit": "game/modemDriverE000",
   "line": "packet bytes 3-5"
  },
  "EAF1": {
   "unit": "game/modemDriverE000",
   "line": "packet bytes 6-13"
  },
  "EAF9": {
   "unit": "game/modemDriverE000",
   "line": "packet bytes 14-18; a full 16-byte payload plus length code and check byte uses up to byte 17"
  },
  "EB05": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB0D": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB14": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB1C": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB24": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB2C": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB34": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB3C": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB44": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB4C": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB54": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB5C": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB64": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB66": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB67": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB6F": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB77": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB7F": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB87": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB8F": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB96": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB9C": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EB9F": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBA5": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBAD": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBB5": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBBD": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBC5": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBCD": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBD5": {
   "unit": "game/modemDriverE000",
   "line": "dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here"
  },
  "EBEC": {
   "unit": "game/modemDriverE000",
   "line": "the last three bytes of the reversed answer string: 'A', the $C0 guard delay and the leading CR"
  }
 },
 "dataTypes": {
  "EAA1": [
   "byte",
   17
  ],
  "EAB7": [
   "byte",
   76
  ],
  "EAC7": [
   "byte",
   35
  ],
  "EAEB": [
   "byte",
   19
  ],
  "EBD6": [
   "text",
   13
  ],
  "EBE3": [
   "text",
   13
  ],
  "EBF0": [
   "text",
   13
  ],
  "EBFD": [
   "byte",
   1
  ],
  "EBFE": [
   "byte",
   2
  ],
  "EC3B": [
   "byte",
   25
  ]
 }
}