; ============================================================================
; $E000-$EFFF - modem driver, SwiftLink build: 6551 ACIA, Hayes control and the packet protocol
; ============================================================================
; Loaded by $0F31 for a modem game ($0BA5 bit 7 clear). Three layers: the UART, a Hayes AT command
; layer with a small terminal mode, and the packet layer that exchanges game commands and chat text
; with the opponent. Same jump table at $E000 as the solo trainer, so the rest of the game does not
; care which one is loaded.
;
; This is the SwiftLink build. Only the bottom layer differs from the shipped module: instead of a
; software UART bit-banged out of the CIA2 NMI on the user port, it drives a 6551 ACIA on a SwiftLink
; cartridge at $DE00 (or $DF00 - the base is probed at open time). Everything above the seam is
; untouched, so at the same line rate this module is byte-for-byte compatible on the wire with the
; stock driver: same $00/$FF byte-sync handshake, same length/control/sequence/checksum frame and the
; same retransmit rules. 300 and 1200 baud are where a SwiftLink player and a stock user-port player
; can meet; C=+2, C=+4, C=+9, C=+0 and C=+8 add 2400 up to 38400 for two SwiftLinks.
;
; THE LINE SETTINGS ARE THE USER'S, NOT THE DRIVER'S. The driver comes up in adopt mode - baudIndex
; $E055 ships as 21, the last entry of baudEntryTable - which means it reads the 6551's control
; register before it touches anything (aciaProbeAdopted $E655) and uses whatever rate, stop bits and
; clock source it finds there, so a C64 Ultimate, a real SwiftLink or anything else that has already
; been configured is played at its own setting rather than overridden. C=+U comes back to that mode
; at any time; the seven speed hot keys still impose a fixed rate for anyone who wants one.
; ONE THING IS NOT NEGOTIABLE: EIGHT DATA BITS. The frame layer sends $55 length codes, raw sequence
; bytes and rotate-and-add checksums, so it is not 7-bit clean, and adopt mode corrects the word
; length - and only the word length - of whatever it finds ($E667). Parity and stop bits are free.
; An unconfigured chip reads $00 in that register, which selects external clocks the cartridge does
; not have, so an unusable setting falls back to 300 baud 8N1 - which is also what the module used to
; impose, and which carries the roughly 30 bytes a second the game generates.
;
; Everything the rest of the game reaches into keeps its original address: $E000-$E017, $E01D-$E047
; and $EC00-$EFFF come out byte-identical to the shipped module, and checkAbi.py enforces that on
; every build. Internal helpers have moved twice to make room in a block that has never had more than
; a handful of spare bytes in it - ADDRESS_CHANGES.md is the old-to-new map. CIA2 is never touched
; except to keep its interrupt mask at $7F.
;
; One SwiftLink caveat worth knowing: some modern rebuilds use a WDC W65C51N, whose TDRE bit reads as
; permanently set. This driver is interrupt driven and never polls TDRE in a loop, but on such a board
; the transmit interrupt fires continuously and characters would be overrun; those boards need a timed
; delay per character instead. A genuine 6551 (and VICE's emulation) behaves.

        .setcpu "6502"
        .include "c64.inc"
        .include "zeropage.inc"

; ---- references to code/data outside this file ----
sendByteToDrive          := $089C
returnToMainMenu         := $0AA3
inputLockoutTimer        := $0B7D
programChecksum          := $0B7E
gameVersionByte          := $0B94
mapSeed                  := $0B95
mapSeed1                 := $0B96
mapSeed2                 := $0B97
playerSide               := $0B9F
gameTypeOptions          := $0BA3
isSoloTrainer            := $0BA5
D_114C                   := $114C
drawRecyclerOnMap        := $3F27
checkEnemyRecyclerSpotted := $3F57
mirrorMapCoordinate      := $3FA3
D_4CE0                   := $4CE0
D_4CE1                   := $4CE1
D_4CE3                   := $4CE3
completeExchange         := $56F1
readFilmByte             := $5874
copyPageUnderIo          := $58B2
D_58C3                   := $58C3
D_58C4                   := $58C4
D_58C6                   := $58C6
D_58C7                   := $58C7
rewindFilmPtr            := $58D5
getFilmSnapshotDestAddr  := $5983
getFilmSnapshotSrcAddr   := $5988
D_60E3                   := $60E3
unitLayoutByGameType     := $78B8
msgTrainerName           := $884E
selectedUnit             := $90F8
gameClock                := $91CB
sideScoreLo              := $92A2
recyclerMode             := $92A6
recyclerCol              := $92A8
recyclerRow              := $92AA
savedViewOriginCol       := $92C3
savedViewOriginRow       := $92C4
revealAllUnits           := $92FC
D_A94C                   := $A94C
D_B24C                   := $B24C
waitFrames               := $C37F
advanceFillPointer       := $C3F9
queueMessage             := $C566
showStatusMessage        := $C6FF
setMessageSlot           := $C924
D_CB4C                   := $CB4C
inGameMenuText           := $CC99
unitColTable             := $F640
ownPlayerName            := $FFDE
opponentPlayerName       := $FFE8
nmiVector                := $FFFA
nmiVectorHi              := $FFFB
irqVectorHi              := $FFFF

; Contents
; --------
;    $E000  commJumpTable                commRequest - entry 0 of the seven-entry jump table that every opponent module
;                                        must present.
;    $E003  commLinkControl              jump-table entry 1, life cycle of the link.
;    $E006  getLinkByte                  jump-table entry 2: take one byte that has arrived from the opponent (or was
;                                        echoed back by the modem in terminal mode) out of the 8-byte hostInRing.
;    $E009  putLinkByte                  jump-table entry 3: hand one byte to the module for transmission to the
;                                        opponent (the chat channel).
;    $E00C  pollLinkStatus               jump-table entry 4, called once per raster IRQ: runs the NMI suspend/resume
;                                        handshake the disk loader needs and samples plus debounces the modem's carrier-detect line.
;    $E012  keyboardScanHook             jump-table entry 5, the module's keyboard scanner vector.
;    $E015  commKeyEntry                 jump-table entry 6 and the game's only keyboard read.
;    $E018  clearPacketVars              zeroes the 19-byte packet-exchange block $E01D-$E02F (exchange flags, both
;                                        packet lengths, both 7-byte packet buffers, the host-output count and the peer build id) by calling
;                                        clearInlineVarBlock with the block sitting immediately after the JSR.
;    $E034  clearLinkVars                zeroes the 15-byte link-state block $E039-$E047 (suspend handshake, link-active
;                                        flag, carrier state, modem timers, connection phase, error counter) through clearInlineVarBlock.
;    $E04C  aciaSetControlIdle           program the ACIA control register and then clear txCharActive, so a rate change
;                                        cannot leave the driver waiting for the TDRE of a byte the new rate stranded.  In the dead user-port constants.
;    $E04F  clearTxCharActive            forget the byte the driver believed was in the ACIA's transmit holding register.
;    $E057  healStuckTransmitter         the once-per-frame heal: txCharActive set with the transmit interrupt disarmed is
;                                        unrecoverable, so clear it on sight.  Returns the answer startNextTxChar's first test used to compute.
;    $E067  clearInlineVarBlock          shared 'zero the X bytes that follow my JSR' helper, used six times in this
;                                        module.
;    $E078  clearProtocolVars            zeroes the 43-byte protocol block $E07D-$E0A7: the completed-exchange counter,
;                                        the receive and transmit packet state machines, both host character rings with their counts and indices, the in-flight
;                                        counters and the service lock.
;    $E0A9  popHostInRing                implementation of the $E006 entry.
;    $E0C3  popHostOutRing               pops the next byte out of the 16-byte hostOutRing.
;    $E0DD  pushHostInRing               pushes one byte into the 8-byte hostInRing, the queue the game reads with
;                                        $E006.
;    $E0F6  pushHostOutRing              implementation of the $E009 entry: store A EOR $80 in the 16-byte hostOutRing
;                                        and bump the count, or do nothing at all when the ring already holds 16 bytes.
;    $E111  commRequestDispatch          the dispatcher behind $E000.
;    $E128  requestPacketSend            $E000 with X=1.
;    $E12F  serviceCommTick              $E000 with X=0, called from the raster IRQ once per frame.
;    $E14D  receivePacketFsm             the packet-layer receiver, a resumable state machine held in rxPacketState (0 =
;                                        expecting a header, 1/2 = collecting a command payload, $80 = collecting chat bytes).
;    $E170  beginReceivePacket           the 'a packet is starting' step of receivePacketFsm: clear the received length
;                                        and add 2 to exchangeFlags to mark 'reception in progress', then fall into the payload loop.
;    $E1DD  sendPacketFsm                the packet-layer transmitter, a resumable state machine in txPacketState (bit 0
;                                        = 'header sent, payload in progress', bit 1 = 'frame handed over, waiting for it to drain', bit 7 = 'that frame was
;                                        chat, not a game packet').
;    $E283  countCompletedExchange       bumps the 16-bit exchangeCounter and then sets bit 6 of exchangeFlags
;                                        ('exchange complete').
;    $E28D  setExchangeFlags             ORs A into exchangeFlags and checks for a completed exchange: when bits 7 and 5
;                                        are both set but bit 6 is not (top three bits = %101) the round trip is done, so it drops into countCompletedExchange
;                                        which sets bit 6 as well.
;    $E29A  commLinkControlDispatch      the dispatcher behind $E003.
;    $E2B1  initCommModule_E2B1          initCommModule (the $E003 X=1 case) - cold start, called once immediately after
;                                        the overlay has been loaded.
;    $E2BD  clearArqVars                 tail of initCommModule: zero the 76-byte frame/ARQ state block $EAB7-$EB02
;                                        (sync counters, sequence numbers, the retransmit ring, the packet assembly buffer and the modem delay bytes).
;    $E2C1  openCommLink_E2C1            openCommLink (the $E003 X=0 case) - bring the link up.
;    $E2EC  readKeyAndHandleModemHotkeys the implementation of $E015.
;    $E353  loadBaudParameters           hand the 6551 the control byte for the speed selected by X and keep it in
;                                        bitPeriodLo. The rates are SwiftLink rates, i.e. twice the 6551 data sheet's, because the cartridge fits a 3.6864 MHz crystal.
;    $E38D  hangUpModem_E38D             hangUpModem - drop DTR and then queue the canned Hayes hang-up string, which
;                                        the terminal-mode transmitter emits in reverse as [guard] '+++' [guard] CR [guard] 'ATH0' CR.
;    $E393  queueModemCommandString      load a canned modem command string into the host-output ring.
;    $E3B2  returnNoKey                  the shared 'no key / key handled' return: A = $FF.
;    $E3B6  sendLinkByte                 indirect 'give one byte to the link'.
;    $E3B9  receiveLinkByte              indirect 'take one byte from the link'.
;    $E3BC  reportLinkError              indirect link-error hook, called by the NMI receiver on a framing error, a bad
;                                        stop bit or a receive-ring overflow.
;    $E3CB  pollCarrierState             implementation of $E00C.
;    $E3F3  selectFramedLinkVectors      rewrites the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD so
;                                        that sendLinkByte, receiveLinkByte and reportLinkError use the framed (ARQ) rings and the error counter instead of the
;                                        raw UART rings and the no-op error stub.
;    $E408  dropDtrLine                  clear the whole ACIA command register: DTR down, receiver and all interrupts
;                                        off.  (It moved here from $E574; aciaSetCommandIdle, which used to be here, is gone - see $E685.)
;    $E40F  clearUartVars                zeroes the 90-byte ring-buffer block $E414-$E46D: all four descending circular
;                                        buffers (UART transmit 22, UART receive 20, ARQ transmit 18, ARQ receive 18) with their counts and read/write indices.
;    $E46E  popArqRxRing                 pop the oldest byte out of the 18-byte ARQ receive ring, the queue the frame
;                                        layer fills with checksum-verified payload.
;    $E486  popArqTxRing                 pop the oldest byte out of the 18-byte ARQ transmit ring, i.e.
;    $E49E  pushArqRxRing                push one validated payload byte into the 18-byte ARQ receive ring; the byte is
;                                        dropped when the ring already holds 18.
;    $E4B7  pushArqTxRing                push one byte into the 18-byte ARQ transmit ring; the byte is dropped when the
;                                        ring is full, and the caller sees C=1 because the count compare left it set.
;    $E4D0  returnRingEmpty              the shared 'nothing in that ring' exit (SEC / RTS) used by the four ring pop
;                                        routines.
;    $E4D2  popUartRxRing                pop one received character out of the 20-byte raw UART receive ring the NMI
;                                        fills.
;    $E4EA  popUartTxRing                pop the next character to transmit out of the 22-byte raw UART transmit ring.
;    $E502  pushUartRxRing               push one fully received character into the 20-byte raw UART receive ring.
;    $E51B  pushUartTxRing               push one character into the 22-byte raw UART transmit ring; the byte is dropped
;                                        when the ring is full.
;    $E534  flushUartTxRing              throw away everything still queued for transmission, then set the pending-byte
;                                        count to 1 if a character is still being shifted out or 0 if not.
;    $E540  configureUserPortLines       open the ACIA: control register from the selected speed, command register $09
;                                        (DTR on, RTS low, receiver IRQ on, transmit IRQ off), then drain the receiver and read the status register.
;    $E551  aciaSetCommand               write the ACIA command register and keep a shadow of it.
;    $E558  aciaGetStatus                read the ACIA status register - which is also the interrupt acknowledge - into
;                                        aciaStatusSave.
;    $E55F  aciaGetData                  read the ACIA data register.
;    $E563  aciaPutData                  write the ACIA data register.
;    $E567  aciaSetControl               write the ACIA control register.
;    $E56B  aciaGetControl               read the ACIA control register back; aciaSetControl falls into it, so a write
;                                        and its read-back are one call.
;    $E56F  silenceCia2                  pin ciaIcrShadow and CIA2_ICR at $7F and read CIA2_ICR, so CIA2 stops raising
;                                        the shared /NMI line and lets go of one it is already holding down.  (It moved here from $E727.)
;    $E57B  installCommNmiVector         point the RAM NMI vector at $FFFA/$FFFB at commNmiHandler.
;    $E586  restartUart                  (re)start the link: drain the receiver, ask the modem state machine to
;                                        re-evaluate, prime the carrier sample timer, and fall into setCiaNmiMask with the 'interrupt on' mask.
;    $E593  setCiaNmiMask                arm or silence the module's interrupt: call silenceCia2 and then set the ACIA
;                                        command register to $09 (receiver IRQ on) or $0B (off, with DTR still up).
;    $E5B1  aciaResumeReceiver           forget the byte in the transmitter, drain the receiver and re-read the status.
;                                        (It moved here from $E658; clearUartState, which used to be at $E5B6, is gone - clearTxCharActive $E04F does its work.)
;    $E5C1  suspendUartIfRunning         two-byte entry between the UART state block and stopCommNmi: the disk loader
;                                        has asked for a suspend, so silence the ACIA unless the acknowledge byte already says it is silent.
;    $E5C3  stopCommNmi                  tear the link down: ask the modem state machine to fall back to phase 0, disable
;                                        the ACIA's receiver interrupt (which also sets both suspend bytes to $80), and forget the character in the transmitter.
;    $E5D6  serviceCarrierAndSuspendRequest first half of the $E00C service.
;    $E5FF  rearmCarrierTimer            the 'no character has arrived recently' tail of the carrier sampler: park the
;                                        sample in carrierSampleTimer (it is negative, so the timer stays negative until the NMI zeroes it on the next good
;                                        character) and strip bit 7 from the value that goes into linkStatusSample, marking the link as quiet.
;    $E607  startNextTxChar              kick the transmitter: arm the ACIA's transmit interrupt when there is something
;                                        in the ring, DTR is up and, outside terminal mode, carrier is present.
;    $E644  aciaDetect                   settle once which page the cartridge answers on - $DE00 probed with two
;                                        patterns, $DF00 taken on trust - and never probe again until the next cold start.
;    $E654  aciaDetectDone               the shared bare RTS the detect path and the transmit path both exit through
;                                        (it was setBitPeriodFull, a dead stub of the software UART; setBitPeriod, the other one, is gone).
;    $E655  aciaProbeAdopted             aim the accessors at page A, remember the line settings that page was already
;                                        configured for, and make it read two different control-register patterns back before believing there is a 6551 on it.
;    $E685  commNmiHandler               the module's NMI handler, installed at $FFFA/$FFFB by installCommNmiVector. It
;                                        banks the I/O area in itself, services RDRF and TDRE, and chains a foreign NMI through $E031.
;    $E6F9  aciaSetPage                  record the cartridge's base page by patching the six accessors' operands and
;                                        publishing it in aciaPageLatch $E5BF.  (It moved here from $E663.)
;    $E70F  baudEntryTable               eight three-byte entries {hot-key code, ACIA control byte, spare}: seven fixed
;                                        rates, C=+3/1/2/4/9/0/8 for 300 to 38400, and entry 21 = adopt, C=+U, whose control byte is whatever the user's own
;                                        hardware was already configured for.  (It moved here from $E706.)
;    $E726  queueAndKickTx               the body of queueByteForTransmit.  (It moved here from $E71B.)
;    $E733  popArqTxRingThunk            the fixed entry the frame layer uses to fetch the next payload byte the packet
;                                        layer queued.
;    $E736  pushArqRxRingThunk           Three-byte trampoline: JMP pushArqRxRing ($E49E).
;    $E739  popUartRxRingThunk           Three-byte trampoline: JMP popUartRxRing ($E4D2), the packet layer's only door
;                                        into the raw 20-byte UART receive ring.
;    $E73C  queueByteForTransmit         Pushes one byte into the 22-byte raw UART transmit ring $E417, counts it in
;                                        $E0A5, and starts the transmitter if it was idle.
;    $E73F  countLinkError               the framed link-error hook: count one bad character.
;    $E743  receiveByteTrackZeros        Pops one byte from the raw UART receive ring and maintains zeroRunLength
;                                        ($EAB8), the count of consecutive $00 bytes on the line.
;    $E756  runModemStateMachine         The per-frame link service.
;    $E773  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.
;    $E7CA  sendNextModemCommandChar     Tail of the per-frame link service.
;    $E7F7  runLinkStateMachine          Second half of the per-frame link service: it dispatches on connectionPhase
;                                        $E040.
;    $E805  beginByteSyncPhase           Connection phase 2, the byte-alignment handshake that runs before any packet
;                                        may be sent.
;    $E858  runPacketPhase               Connection phase 3, one tick of the reliable packet layer.
;    $E8F8  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.
;    $E909  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.
;    $E937  computePacketChecksum        Rotate-and-add check byte over packetBuffer[0..packetLength-1].
;    $E94A  buildPacketFromRing          Copies one queued message out of the 35-byte transmit ring into packetBuffer.
;    $E96D  writeTxRingByte              Appends A to the 35-byte transmit message ring $EAC7 at txRingIndex and
;                                        advances the index modulo 35.
;    $E975  readTxRingByte               Reads the transmit-ring byte at txRingIndex and advances the index modulo 35.
;    $E978  readTxRingByteAt             Reads the transmit-ring byte at an arbitrary index X and leaves txRingIndex
;                                        pointing one past it.
;    $E986  noteIdlePoll                 Called when the receiver found nothing on the line.
;    $E9AA  receivePacket                Assembles one incoming packet from the UART receive ring.
;    $EA0D  dispatchReceivedPacket       Acts on a packet whose checksum was good.
;    $EA7B  deliverPacketPayload         Hands an accepted data packet to the game.
;    $EAA0  packetReturn                 A single RTS used as the common exit of the packet layer.
;    $EAB2  clearPacketState             Zeroes the entire 76-byte packet-protocol state block $EAB7-$EB02 in one call.
;    $EB03  remoteCodeBuffer             Landing area for 6502 code sent by the peer.
;    $EC00  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).
;    $EC1C  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).
;    $EC2B  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.
;    $EC54  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.
;    $EC58  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.
;    $EC91  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.
;    $ECB8  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).
;    $ECD4  mirrorTerrainCode            Maps one map cell byte to its 180-degree-rotated equivalent.
;    $ECF3  loadUnitStartTemplate        Installs the unit start positions for the selected game type.
;    $ED43  exchangePlayerNamesAndSettings The start-of-session handshake.
;    $EDBE  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.
;    $EDC9  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.
;    $EDFC  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.
;    $EE0F  storeOpponentNameHalf        Success tail of exchangeNameHalf: copies the five payload bytes of the received
;                                        message into the opponent name and releases the receive buffer.
;    $EE20  waitForLinkMessage           Busy-waits until bit 6 of the comm status byte $E01D is set, i.e.
;    $EE26  finaliseRecyclerSetup        Last step of the setup phase.
;    $EE4A  loadFilmStartSnapshot        Restores the game state a 'game film' was recorded from.
;    $EEB9  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.
;    $EEC3  announceAndToggleDisplayMode Queues message slot $21 with either 'BEGINNER DISPLAY SET.' or 'STANDARD
;                                        DISPLAY SET.'.
;    $EEDD  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.

        .org $E000


; ----------------------------------------------------------------------
; commRequest - entry 0 of the seven-entry jump table that every opponent module must present. X picks
; the service: 0 = per-frame protocol tick, 1 = 'transmit the packet I have prepared in
; txPacketBuffer', 2 or more = acknowledge and throw away a finished exchange.
; In:  X = function code
; Out: falls through to commRequestDispatch; updates exchangeFlags ($E01D)
; Called from: raster IRQ $1129 (X=0), buildOutgoingPacket $56AE (X=1), completeExchange $570F/$5727,
;     $4EFF, $526F, $573B/$5767/$5776, and the handshake in this file
;     ($ED59/$ED96/$EDBB/$EDC5/$EDE4/$EE1D)
; commJumpTable: addrTable, 24 bytes. Seven JMP entries at $E000/$E003/$E006/$E009/$E00C/$E012/$E015,
; with a two-byte constant pool at $E00F ($60 = RTS, $E010 = $40 = RTI, the file's default
; nmiChainVector target) and the isOriginateMode flag at $E011 wedged between them.
; ----------------------------------------------------------------------
commJumpTable:
        jmp     commRequestDispatch     ; E000  entry 0: per-frame tick / send request / acknowledge, selected by X


; ----------------------------------------------------------------------
; commLinkControl - jump-table entry 1, life cycle of the link. X=0 open, X=1 cold-initialise the
; module, X=2 shut the UART down, X>=3 hang the modem up and then shut down.
; In:  X = function code
; Out: see commLinkControlDispatch
; Called from: initCommModule $0F46 (X=1, right after the overlay is loaded), openCommLink $1B70
;     (X=0), hangUpModem $1B56 (X=3)
; ----------------------------------------------------------------------
commLinkControl:
        jmp     commLinkControlDispatch ; E003  entry 1: open, initialise, close or hang up the link, selected by X


; ----------------------------------------------------------------------
; getLinkByte - jump-table entry 2: take one byte that has arrived from the opponent (or was echoed
; back by the modem in terminal mode) out of the 8-byte hostInRing.
; In:  none
; Out: A = byte with bit 7 flipped back, C=0 when a byte was returned, C=1 when the ring was empty
; Called from: receiveChatCharacter $1399
; ----------------------------------------------------------------------
getLinkByte:
        jmp     popHostInRing           ; E006  entry 2: take one byte the opponent sent out of the host input ring


; ----------------------------------------------------------------------
; putLinkByte - jump-table entry 3: hand one byte to the module for transmission to the opponent (the
; chat channel). Stored EOR $80 in the 16-byte hostOutRing.
; In:  A = byte to send
; Out: hostOutRing/hostOutCount updated; the byte is dropped when the ring already holds 16
; Called from: handleChatSend $1396 (JMP)
; ----------------------------------------------------------------------
putLinkByte:
        jmp     pushHostOutRing         ; E009  entry 3: hand one chat byte over for transmission


; ----------------------------------------------------------------------
; pollLinkStatus - jump-table entry 4, called once per raster IRQ: runs the NMI suspend/resume
; handshake the disk loader needs and samples plus debounces the modem's carrier-detect line.
; In:  nmiSuspendRequest/nmiSuspendAck, isLinkActive, CIA2_PRB bit 4
; Out: linkStatus (debounced), linkStatusSample, carrierDebounceCount
; Called from: raster IRQ $112C
; ----------------------------------------------------------------------
pollLinkStatus:
        jmp     pollCarrierState        ; E00C  entry 4: per-frame carrier poll and NMI suspend handshake

commRtsStub:
        rts                             ; E00F  spare RTS stub sitting in the jump table's constant pool

commRtiStub:
        .byte   $40                     ; E010  spare RTI stub - the value nmiChainVector points at until the game patches it
isOriginateMode:
        .byte   $00                     ; E011  0 = answer mode (ATQ0V1X1A), 1 = originate/dial mode (ATQ0V1X1D); set by $0CDE from the A/O menu key and by the C=+A / C=+O hot keys

; ----------------------------------------------------------------------
; keyboardScanHook - jump-table entry 5, the module's keyboard scanner vector. On disk it points at
; returnNoKey (always 'no key'); the main program overwrites the operand bytes $E013/$E014 at
; $0F4B/$0F50 with $0DB7 = scanKeyboard, so at run time this is the game's keyboard matrix scan.
; In:  none
; Out: A = key code ($FF = none), Y bit 7 = SHIFT held, X bit 7 = Commodore key held
; Called from: readKeyAndHandleModemHotkeys $E2EC only
; ----------------------------------------------------------------------
keyboardScanHook:
        jmp     returnNoKey             ; E012  entry 5: keyboard scan - operand patched to $0DB7 (scanKeyboard) by $0F4B/$0F50


; ----------------------------------------------------------------------
; commKeyEntry - jump-table entry 6 and the game's only keyboard read. Routing every key through the
; comm module lets the Commodore-key modem hot keys be intercepted before the game sees them.
; In:  none
; Out: A = key code, or $FF when the key was swallowed as a modem hot key
; Called from: pollKeyboardEvent $0E56 (every frame)
; ----------------------------------------------------------------------
commKeyEntry:
        jmp     readKeyAndHandleModemHotkeys; E015  entry 6: read a key, filtering out the Commodore-key modem hot keys


; ----------------------------------------------------------------------
; clearPacketVars - zeroes the 19-byte packet-exchange block $E01D-$E02F (exchange flags, both packet
; lengths, both 7-byte packet buffers, the host-output count and the peer build id) by calling
; clearInlineVarBlock with the block sitting immediately after the JSR. Because clearInlineVarBlock
; eats the return address, the RTS lands in this routine's caller.
; In:  none
; Out: $E01D-$E02F = 0; returns to the caller of clearPacketVars
; Called from: initCommModule $E2B4
; ----------------------------------------------------------------------
clearPacketVars:
        ldx     #$13                    ; E018  19 bytes to clear: exchangeFlags through peerCommBuildId
        jsr     clearInlineVarBlock     ; E01A  clear the inline block that follows and return to our caller's caller

; ----------------------------------------------------------------------
; Packet-exchange variables, $E01D-$E02F. This is the inline block clearPacketVars hands to
; clearInlineVarBlock, so the JSR above is followed by 19 bytes of data, not code - the BRK the
; disassembler shows is never executed. The game reads and writes most of these directly:
; $E01D flags, $E01E/$E01F lengths, $E020 and $E027 the two 7-byte command packets.
; ----------------------------------------------------------------------
exchangeFlags:
        brk                             ; E01D  exchangeFlags: bit 0 send requested, bit 1 receive in progress, bit 3 idle marker sent, bit 5 own packet sent, bit 6 exchange complete, bit 7 opponent packet received (rendered as BRK - it is data, never executed)
rxPacketLength:
        .byte   $00                     ; E01E  rxPacketLength: number of bytes of the opponent's packet held in rxPacketBuffer
txPacketLength:
        .byte   $00                     ; E01F  txPacketLength: number of bytes the game put in txPacketBuffer (0-4 in practice)
rxPacketBuffer:
        .byte   $00                     ; E020  .  rxPacketBuffer: the command byte of the packet just received (7 bytes, $E020-$E026)
rxPacketArg1:
        .byte   $00                     ; E021  first argument byte of the received command
rxPacketArg2:
        .byte   $00                     ; E022  second argument byte of the received command
rxPacketArg3:
        .byte   $00                     ; E023  third argument byte of the received command
rxPacketArg4:
        .byte   $00                     ; E024  the remaining three bytes of the 7-byte receive buffer
        .byte   $00,$00                 ; E025  ..
txPacketBuffer:
        .byte   $00                     ; E027  .  txPacketBuffer: command byte to send (7 bytes, $E027-$E02D)
txPacketArg1:
        .byte   $00                     ; E028  first argument byte of the command being sent
txPacketArg2:
        .byte   $00                     ; E029  second argument byte of the command being sent
txPacketArg3:
        .byte   $00                     ; E02A  the remaining four bytes of the 7-byte transmit buffer
        .byte   $00,$00,$00             ; E02B  ...
hostOutCount:
        .byte   $00                     ; E02E  hostOutCount: characters waiting in hostOutRing (chat text, or a queued AT command string)
peerCommBuildId:
        .byte   $00                     ; E02F  peerCommBuildId: the opponent's commBuildId, stored by the handshake at $EDAD

; Four bytes that survive every clear: the build id the two machines swap during the handshake, the
; vector this module chains non-CIA2 NMIs through, and the shadow of the CIA2 interrupt mask.
commBuildId:
        .byte   $00                     ; E030  commBuildId: $00 = this modem build; the trainer overlay has bit 7 set here. Read by main at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D and sent to the peer during the handshake
nmiChainVector:
        .byte   $10                     ; E031  nmiChainVector low byte - $E010 (the RTI stub) on disk, patched to $1298 by initInterruptVectors $0BF3
nmiChainVectorHi:
        .byte   $E0                     ; E032  nmiChainVector high byte - patched to $12 by $0BFB
ciaIcrShadow:
        .byte   $90                     ; E033  ciaIcrShadow: the CIA2 interrupt sources this module wants. setCiaNmiMask pins it at $7F = none, because the main program writes this byte back into CIA2_ICR after every film-buffer access and this driver's interrupt comes from the cartridge

; ----------------------------------------------------------------------
; clearLinkVars - zeroes the 15-byte link-state block $E039-$E047 (suspend handshake, link-active
; flag, carrier state, modem timers, connection phase, error counter) through clearInlineVarBlock.
; In:  none
; Out: $E039-$E047 = 0; returns to the caller of the routine that jumped here
; Called from: commLinkControlDispatch $E2AE (JMP - the tail of the 'close the link' path)
; ----------------------------------------------------------------------
clearLinkVars:
        ldx     #$0F                    ; E034  15 bytes to clear: nmiSuspendRequest through linkErrorCount
L_E036:
        jsr     clearInlineVarBlock     ; E036  clear the inline block that follows; the RTS lands in the caller of whoever jumped here

; ----------------------------------------------------------------------
; Link-state variables, $E039-$E047: the inline block clearLinkVars zeroes. $E039/$E03A are the
; two-way suspend handshake with the disk loader, $E03B-$E03D the link and carrier state, and the rest
; timers and counters used by the modem state machine. The 'DTH' at $E045-$E047 is leftover content
; from the sector image (this page comes from track 18 sector 7, outside the encrypted area).
; ----------------------------------------------------------------------
nmiSuspendRequest:
        brk                             ; E039  nmiSuspendRequest: the game writes $C0 here before a disk load and 0 afterwards (rendered as BRK - data, never executed)
nmiSuspendAck:
        .byte   $00                     ; E03A  nmiSuspendAck: this module answers $80 when the UART NMI is really off, 0 when it is running
isLinkActive:
        .byte   $00                     ; E03B  isLinkActive: non-zero once openCommLink has started the UART
linkStatus:
        .byte   $00                     ; E03C  linkStatus, debounced: bit 6 = carrier present, bit 7 = characters still arriving
linkStatusSample:
        .byte   $00                     ; E03D  linkStatusSample ($E03D) and an unused byte ($E03E): the raw carrier sample taken each frame
        .byte   $00                     ; E03E  .
modemReplyTimer:
        .byte   $00                     ; E03F  modemReplyTimer: counts down while waiting for the modem's answer; negative means a command string is still going out
connectionPhase:
        .byte   $00                     ; E040  connectionPhase: 0 idle, 1 modem-command/terminal mode, 2 byte sync, 3 packet protocol
skipModemInitString:
        .byte   $00                     ; E041  skipModemInitString: non-zero after C=+RETURN so openCommLink re-opens without re-sending the AT string
uartRestartRequest:
        .byte   $00                     ; E042  uartRestartRequest: 1 = re-evaluate the connection, $FF = force back to phase 0
carrierDebounceCount:
        .byte   $00                     ; E043  carrierDebounceCount: consecutive samples that disagree with linkStatus
txPaceCounter:
        .byte   $00                     ; E044  txPaceCounter: dead, but beginByteSyncPhase $E820 still writes it, so the byte has to stay
carrierSampleTimer:
        .byte   $44                     ; E045  carrierSampleTimer: zeroed by the NMI on every byte that arrives without an error, so a negative value means the line is quiet ('D' here is leftover from the sector image)
lastHotkeyCode:
        .byte   $54                     ; E046  lastHotkeyCode: the previous hot-key code, used to ignore auto-repeat ('T' is leftover)
linkErrorCount:
        .byte   $48                     ; E047  linkErrorCount: bumped by countLinkError; non-zero makes the link state machine restart byte sync ('H' is leftover)

; Modem/UART constants, $E048-$E066. None of them is inside a cleared block, so they keep their disk
; values for the whole session: option flags, the CIA2 port bit masks, the two baud parameter sets and
; the border-colour table for the link status indicator.
modemOptionFlags:
        .byte   $CC                     ; E048  modemOptionFlags $CC (bit 7 = a modem is attached, bit 6 = watch the result codes for CONNECT 1200) and an unreferenced $08
        .byte   $08                     ; E049  .
carrierOverrideFlags:
        .byte   $00                     ; E04A  carrierOverrideFlags: bit 6 = pretend carrier is present (C=+C toggle, for a direct null-modem cable)
carrierOverrideToggleMask:
        .byte   $40                     ; E04B  $40 = the carrier-override bit the C=+C hot key toggles
; ----------------------------------------------------------------------
; aciaSetControlIdle / clearTxCharActive - two thirds of the transmitter-deadlock fix, living in the
; nine bytes the stock user-port constants left behind. These were dtrToggleMask $24 ($E04C),
; userPortDdrbValue $26 ($E04D), userPortDtrDropped $22 ($E04E), userPortIdleTable $26 $26 ($E04F),
; serialShiftPatternTable $27 $2F $3F ($E051) and serialShiftPatternAnswer1200 $37 ($E054) - nine
; bytes of CIA2 bit-banger constants that nothing has read since the software UART was replaced. The
; .res gaps left in the UART layer itself are all five bytes or fewer except the one at $E72A, and
; the fix needs three separate places to be able to clear one flag, so the shared parts sit here.
;
; The flag is txCharActive $E5BB, and it exists for exactly one reason: to stop a second byte being
; pushed into the ACIA's transmit holding register while the first is still in it. Nothing else reads
; meaning into it. That makes it safe to drop the moment the transmit interrupt goes off, and it makes
; it fatal to leave set when the transmit interrupt goes off: startNextTxChar $E607 returns while the
; flag is set, and the NMI's transmit half ignores TDRE while the command register says the transmit
; interrupt is disarmed, so nothing is left that could ever clear it. A machine in that state stops
; transmitting for good - measured at 2400 baud, six times, and cured every time by writing a single
; $00 over $E5BB from the monitor.
; Clearing it does not push a byte in on top of another, either. The route back to the chip is
; startNextTxChar, which touches nothing unless the command shadow says the transmitter is idle and
; DTR is up, and each place that clears the flag only clears it where the holding register really is
; free: the NMI's disarm site has just seen TDRE, and the once-per-frame heal only fires when the
; transmit interrupt is off. The one case where a byte can still be in flight with the interrupt off
; is a disk-load suspend, command $0B - and startNextTxChar refuses to transmit while nmiSuspendAck
; is negative, so nothing is written there either.
; In:  A = the ACIA control byte (aciaSetControlIdle only)
; Out: the control register programmed (aciaSetControlIdle only), txCharActive = 0, A = 0, Z = 1;
;     X and Y unchanged
; Called from: loadBaudParameters $E35C (aciaSetControlIdle); healStuckTransmitter $E05C and
;     aciaResumeReceiver $E5B1 (clearTxCharActive)
; ----------------------------------------------------------------------
aciaSetControlIdle:
        jsr     aciaSetControl          ; E04C  program the new line rate first, then fall into the clear
clearTxCharActive:
        lda     #$00                    ; E04F  the driver no longer believes a byte is in the ACIA
        sta     txCharActive            ; E051  ...so the next tick may hand it a fresh one
        rts                             ; E054  A = 0 and Z = 1, which is what startNextTxChar wants
        .assert * = $E055, error, "the txCharActive helpers must end at $E054"

baudIndex:
        .byte   $15                     ; E055  baudIndex: the three-byte index into baudEntryTable $E70F - 0 = 300, 3 = 1200, 6 = 2400, 9 = 4800, 12 = 9600, 15 = 19200, 18 = 38400, 21 = adopt. The disk value is 21, so the driver comes up using whatever the user's port is already configured for; it was $00 (300 baud), which is what adopt falls back to when the chip holds nothing usable
bitPeriodLo:
        .byte   $50                     ; E056  the live ACIA control byte, written by loadBaudParameters. The $50 stored here is the stock 300-baud timer byte and is never used: openCommLink loads a real one before the ACIA is opened
; ----------------------------------------------------------------------
; healStuckTransmitter - the self-healing half of the deadlock fix, in the eight dead bytes that were
; bitPeriodHi $0D ($E057), txPaceReload $01 ($E058) and the stock baudParameterTable $50 $0D $01 $53
; $03 $02 ($E059-$E05E). All eight belonged to the software UART's timing and have been unread since
; this build replaced it; the SwiftLink speed table is baudEntryTable $E70F.
; The rule it enforces once a frame: txCharActive set while command bit 2 is clear cannot be true for
; any healthy reason. Bit 2 clear means the transmit interrupt is disarmed, which means no interrupt
; will ever come back to clear the flag, and startNextTxChar will not run while the flag is set. So
; the combination is unrecoverable by definition and can be cleared on sight, whatever produced it -
; the known NMI re-entrancy path, a rate change that landed on a byte in flight, or something not yet
; identified. Clearing a flag that is already clear costs nothing, so it is not worth testing first.
; The answer it returns is also the answer startNextTxChar wants from its own first test, which is
; why it can replace that test outright: A = 0 (go ahead, nothing of ours is in the chip) when the
; transmit interrupt is off, and A = $04 (leave) when it is on - and when it is on, startNextTxChar
; would have refused at $E61B anyway, because it only arms a transmitter it finds idle.
; In:  aciaCommandShadow, txCharActive
; Out: txCharActive = 0 when the transmit interrupt is disarmed; A = 0 / Z = 1 in that case, A = $04 /
;     Z = 0 when it is armed; X and Y unchanged
; Called from: startNextTxChar $E607, which the once-per-frame link service falls into at $E604
; ----------------------------------------------------------------------
healStuckTransmitter:
        lda     aciaCommandShadow       ; E057  the last byte we handed the command register
        and     #$04                    ; E05A  bit 2 = transmit interrupt armed; A = 0 when it is not
        beq     clearTxCharActive       ; E05C  disarmed - drop the flag and return A = 0
        rts                             ; E05E  armed - the NMI still owns the transmitter
        .assert * = $E05F, error, "healStuckTransmitter must end at $E05E"

; linkStatusBorderTable: 8 entries indexed by connectionPhase*2 + carrier. Each byte is the border
; colour shifted left one place with bit 0 set when the colour should be shown even if the Commodore
; key is not held. Phase 1 (talking to the modem) is the only always-visible pair.
linkStatusBorderTable:
        .byte   $00                     ; E05F  .  linkStatusBorderTable entry 0 (phase 0, no carrier): $00 = black, low bit clear so it is only painted while the Commodore key is held
linkStatusBorderPhase0Carrier:
        .byte   $0E                     ; E060  linkStatusBorderTable entries 1-7: value = colour*2 + 'always paint' flag. $0E yellow, $1D light blue + always, $0D blue + always, $14 light red, $04 red, $1A light green, $0A green
        .byte   $1D,$0D,$14,$04,$1A,$0A ; E061  ......

; ----------------------------------------------------------------------
; clearInlineVarBlock - shared 'zero the X bytes that follow my JSR' helper, used six times in this
; module. It pulls the return address off the stack, plants it in the absolute operand of the STA at
; clearBlockStore, and stores 0 through it with X counting down. The return address is gone, so the
; closing RTS returns two levels up, to the caller of the routine that made the JSR.
; In:  X = number of bytes to clear (1..90); the JSR's return address points one byte before the
;     inline block
; Out: the inline block is zeroed, A=0, X=0, and control returns to the caller's caller
; Called from: clearPacketVars $E01A, clearLinkVars $E036, clearProtocolVars $E07A, clearUartVars
;     $E411 and clearPacketState $EAB4
; ----------------------------------------------------------------------
clearInlineVarBlock:
        pla                             ; E067  take the low byte of the return address, which points at the byte before the inline block
        sta     clearBlockStore+1       ; E068  plant it in the address operand of the STA below
L_E06B:
        pla                             ; E06B  take the high byte of the return address as well - the RTS will now go one level further up
        sta     clearBlockStore+2       ; E06C  plant the high byte too
        lda     #$00                    ; E06F  the block is cleared with zeroes
clearBlockStore:
        sta     L_E036+2,x              ; E071  operand patched above; with X counting down this covers block+X-1 .. block, the X bytes right after the JSR
        dex                             ; E074  next byte down
L_E075:
        bne     clearBlockStore         ; E075  keep going until X reaches 0 (index 0 would hit the byte before the block and is skipped)
L_E077:
        rts                             ; E077  return to the caller of the routine that called us


; ----------------------------------------------------------------------
; clearProtocolVars - zeroes the 43-byte protocol block $E07D-$E0A7: the completed-exchange counter,
; the receive and transmit packet state machines, both host character rings with their counts and
; indices, the in-flight counters and the service lock. The constant inFlightSlotIndex at $E0A8 is
; deliberately left out of the count.
; In:  none
; Out: $E07D-$E0A7 = 0; returns to the caller of clearProtocolVars
; Called from: initCommModule $E2B1
; ----------------------------------------------------------------------
clearProtocolVars:
        ldx     #$2B                    ; E078  43 bytes to clear: exchangeCounter through serviceLock (inFlightSlotIndex at $E0A8 is left alone)
        jsr     clearInlineVarBlock     ; E07A  clear the inline block that follows and return to our caller's caller

; ----------------------------------------------------------------------
; Protocol variables, $E07D-$E0A7, the inline block clearProtocolVars zeroes, plus the constant
; inFlightSlotIndex at $E0A8 which is deliberately left out of the count. It holds the receive and
; transmit packet state machines and the two character rings that connect the module to the game.
; ----------------------------------------------------------------------
exchangeCounterLo:
        brk                             ; E07D  exchangeCounterLo - low byte of the completed-exchange counter; nothing in the image ever reads it (rendered as BRK - data, never executed)
exchangeCounterHi:
        .byte   $00                     ; E07E  exchangeCounterHi - high byte of the same unused statistic
txHeaderExtraBits:
        .byte   $00                     ; E07F  txHeaderExtraBits: ORed into every outgoing packet header, but never written anywhere, so headers carry only the length
rxHeaderByte:
        .byte   $00                     ; E080  rxHeaderByte: the header byte of the packet currently being received
rxPacketState:
        .byte   $00                     ; E081  rxPacketState: 0 = expect a header, 1/2 = collecting a command payload, $80 = collecting chat bytes
rxBytesRemaining:
        .byte   $00                     ; E082  rxBytesRemaining: payload bytes still to be read for this packet
txPacketState:
        .byte   $00                     ; E083  txPacketState: bit 0 = payload in progress, bit 1 = frame handed over, bit 7 = the frame was chat
txPayloadIndex:
        .byte   $00                     ; E084  txPayloadIndex: index of the next payload byte to send (and the byte count of a chat frame)
sendRequestCount:
        .byte   $00                     ; E085  sendRequestCount: outstanding $E000/X=1 send requests

; hostInRing: 8-byte descending circular buffer of characters for the game, filled by the packet
; receiver and by the terminal-mode echo, drained through the $E006 entry.
hostInRing:
        .byte   $00,$00,$00,$00,$00,$00,$00,$00; E086  ........  hostInRing: 8-byte descending ring of bytes the game reads with $E006
chatFrameCounter:
        .byte   $00                     ; E08E  chatFrameCounter: bytes still to pop while a chat frame is going out
hostOutReadIndex:
        .byte   $00                     ; E08F  hostOutReadIndex: next slot popHostOutRing will read (counts down, wraps 0 -> 15)
hostOutWriteIndex:
        .byte   $00                     ; E090  hostOutWriteIndex: next slot pushHostOutRing will fill

; hostOutRing: 16-byte descending circular buffer of characters to send. The game pushes chat text
; through $E009; queueModemCommandString instead loads a whole AT string into slots 1..n, which is why
; the strings in page $EB are stored back to front.
hostOutRing:
        .byte   $00                     ; E091  .  hostOutRing: 16-byte descending ring of characters to send (slot 0; the AT-string loader starts at slot 1)
modemCommandChars:
        .byte   $00                     ; E092  modemCommandChars = hostOutRing+1: queueModemCommandString writes an AT string here forwards, and the descending read index sends it back to front
        .byte   $00,$00,$00,$00,$00,$00,$00; E093  .......
        .byte   $00                     ; E09A  the rest of the 16-byte host output ring
        .byte   $00,$00,$00,$00,$00,$00 ; E09B  ......
hostInCount:
        .byte   $00                     ; E0A1  hostInCount: bytes waiting in hostInRing (maximum 8)
hostInReadIndex:
        .byte   $00                     ; E0A2  hostInReadIndex: next slot popHostInRing will read
hostInWriteIndex:
        .byte   $00                     ; E0A3  hostInWriteIndex: next slot pushHostInRing will fill
frameInFlightFlag:
        .byte   $00                     ; E0A4  frameInFlightFlag: the uartPendingCount the frame layer saw when it queued a frame
uartPendingCount:
        .byte   $00                     ; E0A5  uartPendingCount: characters queued for the UART that have not finished transmitting
packetInFlightCount:
        .byte   $00                     ; E0A6  packetInFlightCount: bytes of the packet the packet layer just handed over, cleared once they have drained
serviceLock:
        .byte   $00                     ; E0A7  serviceLock: rests at 1; serviceCommTick decrements it so a re-entrant call does nothing
inFlightSlotIndex:
        .byte   $01                     ; E0A8  inFlightSlotIndex = 1: selects packetInFlightCount ($E0A5+1) as the counter the packet layer waits on; 0 would make it wait on the raw UART count instead

; ----------------------------------------------------------------------
; popHostInRing - implementation of the $E006 entry. Pops the oldest byte out of the 8-byte hostInRing
; and undoes the EOR $80 the producers applied, so the game sees the character the opponent typed.
; In:  hostInCount, hostInReadIndex, hostInRing
; Out: A = byte, C=0; C=1 and A undefined when the ring is empty; count and read index updated
; Called from: the $E006 jump-table entry
; ----------------------------------------------------------------------
popHostInRing:
        lda     hostInCount             ; E0A9  any bytes waiting in the host input ring?
        beq     returnHostRingEmpty     ; E0AC  empty - return C=1
        ldx     hostInReadIndex         ; E0AE  oldest slot
        lda     hostInRing,x            ; E0B1  fetch the character
        eor     #$80                    ; E0B4  undo the EOR $80 the producers applied
        dex                             ; E0B6  walk the read index down
        bpl     L_E0BB                  ; E0B7  still inside the ring?
        ldx     #$07                    ; E0B9  wrapped past slot 0 - go back to the top slot of the 8-byte ring
L_E0BB:
        stx     hostInReadIndex         ; E0BB  store the new read index
        dec     hostInCount             ; E0BE  one byte fewer waiting
L_E0C1:
        clc                             ; E0C1  C=0 says 'here is a byte'
        rts                             ; E0C2  back to the game


; ----------------------------------------------------------------------
; popHostOutRing - pops the next byte out of the 16-byte hostOutRing. Feeds the packet transmitter
; with chat characters, drains the ring when the link drops, and clocks canned modem command strings
; out in terminal mode.
; In:  hostOutCount, hostOutReadIndex, hostOutRing
; Out: A = byte, C=0, or C=1 when the ring is empty
; Called from: sendPacketFsm $E253, runModemStateMachine $E78C, sendNextModemCommandChar $E7CA
; ----------------------------------------------------------------------
popHostOutRing:
        lda     hostOutCount            ; E0C3  any characters queued for transmission?
        beq     returnHostRingEmpty     ; E0C6  empty - return C=1
        ldx     hostOutReadIndex        ; E0C8  oldest slot
        lda     hostOutRing,x           ; E0CB  fetch the character
        dex                             ; E0CE  walk the read index down
        bpl     L_E0D3                  ; E0CF  still inside the ring?
        ldx     #$0F                    ; E0D1  wrapped - go back to the top slot of the 16-byte ring
L_E0D3:
        stx     hostOutReadIndex        ; E0D3  store the new read index
        dec     hostOutCount            ; E0D6  one character fewer waiting
        clc                             ; E0D9  C=0 says 'here is a byte'
        rts                             ; E0DA  return

returnHostRingEmpty:
        sec                             ; E0DB  shared 'that ring is empty' exit for both host-ring pops
L_E0DC:
        rts                             ; E0DC  return


; ----------------------------------------------------------------------
; pushHostInRing - pushes one byte into the 8-byte hostInRing, the queue the game reads with $E006.
; Silently drops the byte when the ring already holds 8.
; In:  A = byte
; Out: hostInRing, hostInCount, hostInWriteIndex
; Called from: receivePacketFsm $E1D3 (chat payload), runModemStateMachine $E7BD (echo of the modem's
;     answer), sendNextModemCommandChar $E7F4 (fabricated CR when the modem does not answer)
; ----------------------------------------------------------------------
pushHostInRing:
        ldx     hostInCount             ; E0DD  how full is the host input ring?
        cpx     #$08                    ; E0E0  the ring holds 8 bytes
        bcs     L_E0F5                  ; E0E2  full - drop the byte on the floor
L_E0E4:
        ldx     hostInWriteIndex        ; E0E4  next free slot
L_E0E7:
        sta     hostInRing,x            ; E0E7  store the byte
        dex                             ; E0EA  walk the write index down
        bpl     L_E0EF                  ; E0EB  still inside the ring?
        ldx     #$07                    ; E0ED  wrapped - back to the top slot
L_E0EF:
        stx     hostInWriteIndex        ; E0EF  store the new write index
        inc     hostInCount             ; E0F2  one more byte waiting for the game
L_E0F5:
        rts                             ; E0F5  return


; ----------------------------------------------------------------------
; pushHostOutRing - implementation of the $E009 entry: store A EOR $80 in the 16-byte hostOutRing and
; bump the count, or do nothing at all when the ring already holds 16 bytes.
; In:  A = byte to send
; Out: hostOutRing, hostOutWriteIndex, hostOutCount
; Called from: the $E009 jump-table entry
; ----------------------------------------------------------------------
pushHostOutRing:
        ldx     hostOutCount            ; E0F6  how full is the host output ring?
        cpx     #$10                    ; E0F9  the ring holds 16 characters
        bcs     L_E110                  ; E0FB  full - drop the character silently
        ldx     hostOutWriteIndex       ; E0FD  next free slot
        eor     #$80                    ; E100  characters travel with bit 7 inverted
        sta     hostOutRing,x           ; E102  store it
        dex                             ; E105  walk the write index down
        bpl     L_E10A                  ; E106  still inside the ring?
        ldx     #$0F                    ; E108  wrapped - back to the top slot
L_E10A:
        stx     hostOutWriteIndex       ; E10A  store the new write index
        inc     hostOutCount            ; E10D  one more character to transmit
L_E110:
        rts                             ; E110  return


; ----------------------------------------------------------------------
; commRequestDispatch - the dispatcher behind $E000. X=0 runs serviceCommTick, X=1 runs
; requestPacketSend, X>=2 acknowledges a finished exchange: if bit 6 of exchangeFlags is set both
; packet lengths and the whole flag byte are cleared, which releases rxPacketBuffer for the next
; packet.
; In:  X = function code, exchangeFlags
; Out: one of the three cases above
; Called from: the $E000 jump-table entry
; ----------------------------------------------------------------------
commRequestDispatch:
        dex                             ; E111  X=0 -> $FF, X=1 -> 0, X>=2 -> positive
        bmi     serviceCommTick         ; E112  X was 0: run the per-frame service tick
        beq     requestPacketSend       ; E114  X was 1: the game wants its packet transmitted
        bit     exchangeFlags           ; E116  X was 2 or more: is the exchange finished (bit 6)?
L_E119:
        bvs     L_E11C                  ; E119  yes - consume it
        rts                             ; E11B  not finished yet, leave everything alone

L_E11C:
        lda     #$00                    ; E11C  clear both packet lengths and all the flags
        sta     rxPacketLength          ; E11E  the received packet has been read out
        sta     txPacketLength          ; E121  the transmitted packet is done with
        sta     exchangeFlags           ; E124  clearing the flags releases the buffers for the next exchange
L_E127:
        rts                             ; E127  return


; ----------------------------------------------------------------------
; requestPacketSend - $E000 with X=1. The game has filled txPacketBuffer and set txPacketLength; this
; bumps bit 0 of exchangeFlags ('a send is pending') and the outstanding-request count, and
; sendPacketFsm picks the request up on the next service tick.
; In:  none (txPacketLength/txPacketBuffer already filled by the caller)
; Out: exchangeFlags +1, sendRequestCount +1
; Called from: commRequestDispatch $E114
; ----------------------------------------------------------------------
requestPacketSend:
        inc     exchangeFlags           ; E128  set bit 0 of exchangeFlags: a send is pending
        inc     sendRequestCount        ; E12B  count the request so sendPacketFsm knows the game is waiting
        rts                             ; E12E  return


; ----------------------------------------------------------------------
; serviceCommTick - $E000 with X=0, called from the raster IRQ once per frame. The DEC/INC pair around
; the body is a re-entrancy lock (serviceLock rests at 1, so only the outermost call runs). It always
; drives the modem/connection state machine, and only once the connection has reached phase 3 with the
; link up does it also run the packet receive and packet transmit state machines.
; In:  serviceLock, connectionPhase, isLinkActive
; Out: drives the whole stack; serviceLock restored to 1
; Called from: commRequestDispatch $E112
; ----------------------------------------------------------------------
serviceCommTick:
        dec     serviceLock             ; E12F  take the re-entrancy lock (rests at 1, so this reaches 0 only in the outermost call)
        bne     L_E149                  ; E132  already inside the service - do nothing this frame
        jsr     runModemStateMachine    ; E134  always drive the modem / connection state machine
        lda     connectionPhase         ; E137  how far has the connection got?
        cmp     #$03                    ; E13A  phase 3 = the packet protocol is running
        bcc     L_E149                  ; E13C  not connected yet - no packet traffic
        ldx     isLinkActive            ; E13E  is the UART actually up?
        beq     L_E149                  ; E141  link down - skip the packet layer
        jsr     receivePacketFsm        ; E143  collect anything the opponent has sent
        jsr     sendPacketFsm           ; E146  and push out whatever we have to say
L_E149:
        inc     serviceLock             ; E149  release the lock
        rts                             ; E14C  return


; ----------------------------------------------------------------------
; receivePacketFsm - the packet-layer receiver, a resumable state machine held in rxPacketState (0 =
; expecting a header, 1/2 = collecting a command payload, $80 = collecting chat bytes). The header
; byte's low three bits are the payload length; bit 7 means the payload is chat text that goes
; straight into hostInRing, otherwise it is a game command packet collected in rxPacketBuffer with the
; length in rxPacketLength. Headers $70-$7F are a third case: the payload is appended to the
; downloadable-code buffer at $EB03, and a $7x header with no payload calls it. A finished command
; packet sets bit 7 of exchangeFlags.
; In:  rxPacketState, rxBytesRemaining, rxHeaderByte, bytes from receiveLinkByte
; Out: rxPacketBuffer, rxPacketLength, exchangeFlags bit 7, hostInRing, the $EB03 buffer
; Called from: serviceCommTick $E143
; ----------------------------------------------------------------------
receivePacketFsm:
        ldx     rxPacketState           ; E14D  where did we leave off?
        bmi     L_E1CC                  ; E150  state $80: we are in the middle of a chat payload
        dex                             ; E152  state 0 -> $FF, 1 -> 0, 2 -> 1
        beq     L_E16B                  ; E153  state 1: the header is in, start the packet
        bpl     L_E183                  ; E155  state 2 or more: keep collecting payload bytes

; ----------------------------------------------------------------------
; State 0: read the header byte and work out what kind of frame is arriving.
; ----------------------------------------------------------------------
        jsr     receiveLinkByte         ; E157  state 0: try to read a header byte
        bcs     L_E1CB                  ; E15A  nothing has arrived yet - come back next frame
        tax                             ; E15C  keep the header in X
        sta     rxHeaderByte            ; E15D  remember it for the $70-$7F test later
        and     #$07                    ; E160  the low three bits are the payload length
L_E162:
        sta     rxBytesRemaining        ; E162  that many payload bytes to collect
        txa                             ; E165  look at the header again
        bmi     L_E1CC                  ; E166  bit 7 set: the payload is chat text for the game's message line
        inc     rxPacketState           ; E168  command packet: advance to state 1
L_E16B:
        bit     exchangeFlags           ; E16B  has the game read the previous packet yet (bit 7 of exchangeFlags)?
        bmi     L_E1CB                  ; E16E  no - leave the payload in the ring and wait

; ----------------------------------------------------------------------
; beginReceivePacket - the 'a packet is starting' step of receivePacketFsm: clear the received length
; and add 2 to exchangeFlags to mark 'reception in progress', then fall into the payload loop. Reached
; by fall-through here; the solo-trainer build calls it as a subroutine from $E07E.
; In:  rxBytesRemaining = payload length
; Out: rxPacketLength = 0, exchangeFlags +2
; Called from: fall-through from $E16E in this build
; ----------------------------------------------------------------------
beginReceivePacket:
        lda     #$00                    ; E170  start a fresh packet
        sta     rxPacketLength          ; E172  nothing stored yet
        inc     exchangeFlags           ; E175  add 2 to exchangeFlags: reception in progress
        inc     exchangeFlags           ; E178  (two INCs because there is no ADC-style flag setter here)
        lda     rxBytesRemaining        ; E17B  how long is the payload?
        beq     L_E196                  ; E17E  a header-only packet - go straight to the delivery step
        inc     rxPacketState           ; E180  advance to state 2: collecting the payload

; ----------------------------------------------------------------------
; Payload loop: read the remaining bytes of a command packet into rxPacketBuffer, resuming from state
; 2 on a later frame whenever the link runs dry.
; ----------------------------------------------------------------------
L_E183:
        jsr     receiveLinkByte         ; E183  read the next payload byte
        bcs     L_E1CB                  ; E186  nothing yet - resume from state 2 next frame
L_E188:
        ldx     rxPacketLength          ; E188  append it to the receive buffer
        sta     rxPacketBuffer,x        ; E18B  rxPacketBuffer holds the command byte and up to 6 arguments
        inc     rxPacketLength          ; E18E  one more byte stored
        dec     rxBytesRemaining        ; E191  one fewer to go
        bne     L_E183                  ; E194  keep reading until the payload is complete

; ----------------------------------------------------------------------
; The packet is complete. Headers $70-$7F carry 6502 code for the downloadable-code buffer at
; $EB03; everything else is a game command packet, which is announced by setting bit 7 of
; exchangeFlags so the main program can pick it up.
; ----------------------------------------------------------------------
L_E196:
        lda     rxHeaderByte            ; E196  look at the header again
        cmp     #$70                    ; E199  headers $70-$7F address the downloadable-code buffer
        bcc     L_E1C1                  ; E19B  an ordinary game command packet - just flag it
        lda     rxPacketLength          ; E19D  did this $7x message carry a payload?
        bne     L_E1A9                  ; E1A0  yes - append it to the code buffer
        jsr     remoteCodeBuffer        ; E1A2  no payload: call whatever the peer has downloaded ($EB03 holds a bare RTS in the shipped image)
        ldx     #$00                    ; E1A5  and reset the buffer write offset
        beq     L_E1BE                  ; E1A7  always taken (Z was set by the LDX)
L_E1A9:
        tya                             ; E1A9  Y is in use by the caller
        pha                             ; E1AA  so save it
        ldy     #$00                    ; E1AB  read the payload from the start
remoteCodeWriteIndex:
        ldx     #$00                    ; E1AD  operand patched at $E1BE: where in the code buffer to continue writing
L_E1AF:
        lda     rxPacketBuffer,y        ; E1AF  next payload byte
        iny                             ; E1B2  advance through the received payload
        sta     remoteCodeBuffer,x      ; E1B3  append it to the downloadable-code buffer at $EB03
        inx                             ; E1B6  advance the buffer offset
        dec     rxPacketLength          ; E1B7  count the byte off
        bne     L_E1AF                  ; E1BA  until the whole payload has been copied
        pla                             ; E1BC  restore the caller's Y
        tay                             ; E1BD  restore the caller's Y
L_E1BE:
        stx     remoteCodeWriteIndex+1  ; E1BE  self-modify: remember the buffer offset for the next $7x message
L_E1C1:
        lda     #$80                    ; E1C1  bit 7 = 'a packet from the opponent is waiting'
        jsr     setExchangeFlags        ; E1C3  flag it (and complete the exchange if our own packet has gone out)
L_E1C6:
        ldx     #$00                    ; E1C6  back to state 0, expecting the next header
L_E1C8:
        stx     rxPacketState           ; E1C8  store the new receive state
L_E1CB:
        rts                             ; E1CB  return


; ----------------------------------------------------------------------
; Chat payload: the header had bit 7 set, so its 0-3 bytes are text that goes straight into the ring
; the game reads with $E006 - it never touches the command packet buffer.
; ----------------------------------------------------------------------
L_E1CC:
        jsr     receiveLinkByte         ; E1CC  chat payload: read one character
        ldx     #$80                    ; E1CF  the state to save if nothing has arrived: stay in 'chat payload'
L_E1D1:
        bcs     L_E1C8                  ; E1D1  nothing yet - save state $80 and resume next frame
        jsr     pushHostInRing          ; E1D3  hand the character to the game's message line
        dec     rxBytesRemaining        ; E1D6  one fewer chat byte to go
        bne     L_E1CC                  ; E1D9  keep going while the frame has more
        beq     L_E1C6                  ; E1DB  frame complete - back to state 0 (always taken)

; ----------------------------------------------------------------------
; sendPacketFsm - the packet-layer transmitter, a resumable state machine in txPacketState (bit 0 =
; 'header sent, payload in progress', bit 1 = 'frame handed over, waiting for it to drain', bit 7 =
; 'that frame was chat, not a game packet'). With nothing in flight it prefers the pending game
; command packet (header = txPacketLength, then that many bytes from txPacketBuffer); otherwise it
; emits a chat frame with header $80|count and up to three characters from hostOutRing, which doubles
; as the keep-alive when both sides are idle. When the frame has drained it sets bit 5 of
; exchangeFlags and clears the state.
; In:  txPacketState, sendRequestCount, hostOutCount, txPacketLength, txPacketBuffer,
;     packetInFlightCount
; Out: bytes handed to sendLinkByte, packetInFlightCount = bytes+1, exchangeFlags bits 3/5,
;     txPayloadIndex, chatFrameCounter
; Called from: serviceCommTick $E146
; ----------------------------------------------------------------------
sendPacketFsm:
        lda     txPacketState           ; E1DD  what is the transmitter doing?
        lsr     a                       ; E1E0  bit 0 into carry
        bcs     L_E222                  ; E1E1  a payload is half way out - carry on where we stopped
        lsr     a                       ; E1E3  bit 1 into carry
        bcc     L_E1E9                  ; E1E4  nothing in flight - decide what to send
        jmp     L_E26B                  ; E1E6  a frame has been handed over - just watch for it to drain


; ----------------------------------------------------------------------
; Nothing is in flight: decide between the game's command packet, a chat frame, and the bare
; keep-alive frame that keeps the exchange ticking over when neither side has anything to say.
; ----------------------------------------------------------------------
L_E1E9:
        ldx     inFlightSlotIndex       ; E1E9  1: the packet layer waits on packetInFlightCount
        lda     uartPendingCount,x      ; E1EC  is the previous frame still on its way out?
        bne     L_E1CB                  ; E1EF  yes - nothing to do this tick
        cmp     sendRequestCount        ; E1F1  A is 0 here, so C=1 means no send has been requested
        lda     hostOutCount            ; E1F4  any chat characters queued?
        beq     L_E20A                  ; E1F7  none - only a command packet could go now
L_E1F9:
        bcs     L_E23A                  ; E1F9  chat waiting and no packet requested - send a chat frame
        lda     txPacketLength          ; E1FB  a packet was requested: is there anything in either packet buffer?
        ora     rxPacketLength          ; E1FE  and the length of the packet just received
        bne     L_E20A                  ; E201  yes - send the command packet
        lda     #$08                    ; E203  $08 = the idle keep-alive marker
        bit     exchangeFlags           ; E205  has that marker already been set in exchangeFlags?
        beq     L_E237                  ; E208  not yet - set it and send one bare frame to keep the link ticking over
L_E20A:
        bcs     L_E1CB                  ; E20A  no send requested (C=1) - nothing to do

; ----------------------------------------------------------------------
; Send a game command packet: header = payload length, then the bytes the game left in txPacketBuffer.
; Any full ring simply stops the loop; the state byte makes it resume here.
; ----------------------------------------------------------------------
        lda     txPacketLength          ; E20C  the header is simply the payload length
        ora     txHeaderExtraBits       ; E20F  plus txHeaderExtraBits, which is always 0 in this build
        jsr     sendLinkByte            ; E212  hand the header to the link
        bcs     L_E282                  ; E215  the link ring is full - try again next tick
        lda     #$00                    ; E217  0 into both counters below
        sta     txPayloadIndex          ; E219  start the payload at byte 0
        sta     sendRequestCount        ; E21C  the request has been taken
L_E21F:
        inc     txPacketState           ; E21F  set bit 0: payload in progress
L_E222:
        ldx     txPayloadIndex          ; E222  how many payload bytes have gone?
        cpx     txPacketLength          ; E225  all of them?
        beq     L_E260                  ; E228  yes - the frame is complete
        lda     txPacketBuffer,x        ; E22A  next byte of the command the game built
        jsr     sendLinkByte            ; E22D  hand it to the link
        bcs     L_E282                  ; E230  ring full - resume from this byte next tick
        inc     txPayloadIndex          ; E232  one more byte away
        bne     L_E222                  ; E235  keep going (always taken)
L_E237:
        jsr     setExchangeFlags        ; E237  A=$08: remember that the idle keep-alive has been sent

; ----------------------------------------------------------------------
; Send a chat frame: header $80 | count, then up to three characters from hostOutRing. With the count
; at zero this is the one-byte $80 keep-alive.
; ----------------------------------------------------------------------
L_E23A:
        lda     hostOutCount            ; E23A  how many chat characters are waiting?
        cmp     #$03                    ; E23D  at most three travel in one frame
        bcc     L_E243                  ; E23F  fewer than three - send them all
        lda     #$03                    ; E241  clamp to three
L_E243:
        sta     txPayloadIndex          ; E243  that many payload bytes
        sta     chatFrameCounter        ; E246  and that many pops to do
        ora     #$80                    ; E249  chat frames are marked by bit 7 of the header
L_E24B:
        jsr     sendLinkByte            ; E24B  send the header, then each character in turn
        dec     chatFrameCounter        ; E24E  one fewer to pop
        bmi     L_E258                  ; E251  all sent
        jsr     popHostOutRing          ; E253  take the next character out of the host output ring
        bcc     L_E24B                  ; E256  and send it (C=1 would mean the ring lied about its count)
L_E258:
        lda     #$81                    ; E258  state $81: a chat frame is on its way (bit 7 keeps it out of the exchange flags)
        sta     txPacketState           ; E25A  remember it
        ldx     txPayloadIndex          ; E25D  the payload byte count
L_E260:
        inx                             ; E260  plus the header byte
        txa                             ; E261  into A for the in-flight counter
        ldx     inFlightSlotIndex       ; E262  1: the in-flight counter lives at $E0A6
L_E265:
        sta     uartPendingCount,x      ; E265  tell the frame layer how many bytes belong to this packet
        inc     txPacketState           ; E268  state -> 2 (or $82 for chat): waiting for the frame to drain

; ----------------------------------------------------------------------
; Both paths end here: wait until the frame layer says the bytes have really gone, then flag 'own
; packet sent' (chat frames are excluded) and go idle.
; ----------------------------------------------------------------------
L_E26B:
        ldx     inFlightSlotIndex       ; E26B  1: read the in-flight counter
        lda     uartPendingCount,x      ; E26E  has the frame left the machine?
        bne     L_E282                  ; E271  not yet - keep waiting
        bit     txPacketState           ; E273  was it a chat frame (bit 7)?
        bmi     L_E27D                  ; E276  yes - chat does not complete a game exchange
        lda     #$20                    ; E278  bit 5 = 'our own packet has gone out'
        jsr     setExchangeFlags        ; E27A  flag it (and complete the exchange if the opponent's packet has arrived)
L_E27D:
        lda     #$00                    ; E27D  transmitter idle again
        sta     txPacketState           ; E27F  clear the transmit state
L_E282:
        rts                             ; E282  return


; ----------------------------------------------------------------------
; countCompletedExchange - bumps the 16-bit exchangeCounter and then sets bit 6 of exchangeFlags
; ('exchange complete'). Reached from setExchangeFlags exactly once per finished send+receive pair.
; Nothing in the whole image ever reads exchangeCounter, so it looks like a leftover debug statistic.
; In:  exchangeCounterLo/Hi
; Out: exchangeCounter +1, falls into setExchangeFlags with A=$40
; Called from: setExchangeFlags $E297
; ----------------------------------------------------------------------
countCompletedExchange:
        inc     exchangeCounterLo       ; E283  count this completed exchange (low byte)
        bne     L_E28B                  ; E286  no carry into the high byte
        inc     exchangeCounterHi       ; E288  16-bit counter that nothing ever reads
L_E28B:
        lda     #$40                    ; E28B  bit 6 = 'exchange complete', which is what the game waits for

; ----------------------------------------------------------------------
; setExchangeFlags - ORs A into exchangeFlags and checks for a completed exchange: when bits 7 and 5
; are both set but bit 6 is not (top three bits = %101) the round trip is done, so it drops into
; countCompletedExchange which sets bit 6 as well. A=$80 marks 'opponent packet received', $20 'own
; packet sent', $40 'complete', $08 the idle keep-alive marker.
; In:  A = flag bits to add
; Out: exchangeFlags updated, possibly exchangeCounter
; Called from: receivePacketFsm $E1C3, sendPacketFsm $E237/$E27A
; ----------------------------------------------------------------------
setExchangeFlags:
        ora     exchangeFlags           ; E28D  add the new flag bits
        sta     exchangeFlags           ; E290  store the updated flags
        and     #$E0                    ; E293  look at bits 7, 6 and 5 only
        eor     #$A0                    ; E295  received + sent but not yet marked complete?
        beq     countCompletedExchange  ; E297  yes - the round trip is finished
        rts                             ; E299  otherwise just return with the flag added


; ----------------------------------------------------------------------
; commLinkControlDispatch - the dispatcher behind $E003. X=0 opens the link, X=1 cold-initialises the
; module, X=2 stops the NMI UART and clears the link variables, X>=3 first sends the Hayes hang-up
; string, spins until it has gone out, and then does the same shutdown.
; In:  X = function code
; Out: see the four cases; every path except X=0/X=1 ends by zeroing $E039-$E047
; Called from: the $E003 jump-table entry
; ----------------------------------------------------------------------
commLinkControlDispatch:
        dex                             ; E29A  X=0 -> $FF, 1 -> 0, 2 -> 1, 3 -> 2
        bmi     openCommLink_E2C1       ; E29B  X was 0: open the link
        beq     initCommModule_E2B1     ; E29D  X was 1: cold-initialise the module
L_E29F:
        dex                             ; E29F  X=2 -> $FF, X=3 -> 0
        beq     L_E2AB                  ; E2A0  X was 2: shut the UART down without hanging up
        jsr     hangUpModem_E38D        ; E2A2  X>=3: queue '+++' / ATH0 first
L_E2A5:
        lda     modemReplyTimer         ; E2A5  wait for the hang-up string to be delivered
        lsr     a                       ; E2A8  modemReplyTimer 0 or 1 means the modem has answered / given up
        bne     L_E2A5                  ; E2A9  spin until it does
L_E2AB:
        jsr     stopCommNmi             ; E2AB  stop the NMI UART
        jmp     clearLinkVars           ; E2AE  and zero the link variables (its RTS returns to the $E003 caller)


; ----------------------------------------------------------------------
; initCommModule (the $E003 X=1 case) - cold start, called once immediately after the overlay has been
; loaded. Clears the protocol block, the packet block, all four ring buffers and the ARQ/frame state
; block, and points the three indirect link I/O jumps at their framed implementations.
; In:  none
; Out: $E07D-$E0A7, $E01D-$E02F, $E414-$E46D and $EAB7-$EB02 zeroed; the operands at $E3B7/$E3BA/$E3BD
;     patched
; Called from: commLinkControlDispatch $E29D (reached from initCommModule $0F46 with X=1)
; ----------------------------------------------------------------------
initCommModule_E2B1:
        jsr     clearProtocolVars       ; E2B1  wipe the protocol block $E07D-$E0A7
        jsr     clearPacketVars         ; E2B4  wipe the packet block $E01D-$E02F
D_E2B7:
        jsr     clearUartVars           ; E2B7  wipe all four ring buffers $E414-$E46D
        jsr     selectFramedLinkVectors ; E2BA  route link I/O through the framed (ARQ) rings

; ----------------------------------------------------------------------
; clearArqVars - tail of initCommModule: zero the 76-byte frame/ARQ state block $EAB7-$EB02 (sync
; counters, sequence numbers, the retransmit ring, the packet assembly buffer and the modem delay
; bytes).
; In:  none
; Out: $EAB7-$EB02 = 0
; Called from: fall-through from initCommModule $E2BA
; ----------------------------------------------------------------------
clearArqVars:
        jsr     clearPacketState        ; E2BD  wipe the frame/ARQ state block $EAB7-$EB02
        rts                             ; E2C0  return


; ----------------------------------------------------------------------
; openCommLink (the $E003 X=0 case) - bring the link up. If it is not already up it loads the baud
; parameters for the selected speed, installs this module's NMI vector, starts the software UART and
; marks the link active. It then always re-programs the user-port lines and, unless the C=+RETURN hot
; key asked for a silent re-open, queues the Hayes init string for answer or originate mode. Finally
; it releases the per-frame service lock.
; In:  isLinkActive, baudIndex, skipModemInitString, isOriginateMode
; Out: isLinkActive = 1, NMI vector at $FFFA/$FFFB, CIA2 configured, an AT string queued, serviceLock
;     = 1
; Called from: commLinkControlDispatch $E29B, and the C=+RETURN hot key at $E369
; ----------------------------------------------------------------------
openCommLink_E2C1:
        lda     isLinkActive            ; E2C1  is the UART already running?
        bne     L_E2D5                  ; E2C4  yes - only refresh the port lines and the AT string
        ldx     baudIndex               ; E2C6  0 = 300 baud, 3 = 1200 baud
        jsr     loadBaudParameters      ; E2C9  load that speed's timer values
        jsr     installCommNmiVector    ; E2CC  point $FFFA/$FFFB at commNmiHandler
        jsr     restartUart             ; E2CF  clear the bit-level state and arm the CIA2 NMI sources
        inc     isLinkActive            ; E2D2  the link is now up
L_E2D5:
        jsr     configureUserPortLines  ; E2D5  set RTS/DTR, DDRB and the idle TXD level
        lda     skipModemInitString     ; E2D8  did C=+RETURN ask for a silent re-open?
        bne     L_E2E6                  ; E2DB  yes - do not send the AT string again
        ldx     isOriginateMode         ; E2DD  0 = answer, 1 = originate
L_E2E0:
        lda     modemInitStringPtrLoTable,x; E2E0  low byte of 'ATQ0V1X1A' ($E3) or 'ATQ0V1X1D' ($F0), both stored back to front
        jsr     queueModemCommandString ; E2E3  load it into the host output ring for the terminal-mode transmitter
L_E2E6:
        lda     #$01                    ; E2E6  the per-frame service lock rests at 1
        sta     serviceLock             ; E2E8  release it, so serviceCommTick may run
L_E2EB:
        rts                             ; E2EB  return


; ----------------------------------------------------------------------
; readKeyAndHandleModemHotkeys - the implementation of $E015. It scans the keyboard through the
; patched hook, optionally repaints the border with the link-status colour, and then interprets a
; Commodore-key combination as a modem hot key: H toggles DTR, C toggles the carrier override, U goes
; back to the port's own line settings, 3/1/2/4/9/0/8 impose 300, 1200, 2400, 4800, 9600, 19200 or
; 38400 baud instead, RETURN re-opens the link without re-sending the init string, P hangs up, A
; selects answer mode and O selects originate mode. A hot key is swallowed by returning $FF so the
; game never sees it.
; In:  the keyboard hook result (A = key code, X bit 7 = Commodore key), isLinkActive, linkStatus,
;     connectionPhase, lastHotkeyCode
; Out: A = key code or $FF; may write VIC_BORDER, lastHotkeyCode, isOriginateMode, baudIndex,
;     carrierOverrideFlags, CIA2_PRB, or queue a modem command string
; Called from: the $E015 jump-table entry
; ----------------------------------------------------------------------
readKeyAndHandleModemHotkeys:
        jsr     keyboardScanHook        ; E2EC  scan the keyboard (the game patched this vector to $0DB7)
        pha                             ; E2EF  keep the key code

; ----------------------------------------------------------------------
; Link status indicator: while the link is up the border shows the connection phase and carrier state.
; Phase 1 (terminal mode) paints unconditionally, the other phases only while the Commodore key is
; held down, which is also when the hot keys below are active.
; ----------------------------------------------------------------------
L_E2F0:
        lda     isLinkActive            ; E2F0  is there a link to report on?
        beq     L_E30F                  ; E2F3  no - leave the border alone
        stx     hotkeyCommodoreFlag+1   ; E2F5  self-modify: stash the Commodore-key flag, because the border code needs X
        lda     linkStatus              ; E2F8  debounced link status
        asl     a                       ; E2FB  shift bit 7 (data flowing) out
        asl     a                       ; E2FC  now carry holds bit 6 = carrier present
        lda     connectionPhase         ; E2FD  connection phase 0-3
        rol     a                       ; E300  index = phase*2 + carrier
        tax                             ; E301  into X to index the colour table
        lda     linkStatusBorderTable,x ; E302  colour*2 + 'always paint' flag
        lsr     a                       ; E305  carry = always-paint flag, A = the colour
hotkeyCommodoreFlag:
        ldx     #$00                    ; E306  operand patched at $E2F5: $80 while the Commodore key is held
        bmi     L_E30C                  ; E308  Commodore key held - show the link status whatever the phase
        bcc     L_E30F                  ; E30A  otherwise only phases 1 (terminal mode) paint on their own
L_E30C:
        sta     VIC_BORDER              ; E30C  paint the border with the link status colour
L_E30F:
        pla                             ; E30F  get the key code back
        bpl     L_E31B                  ; E310  codes below $80 are ordinary keys, never hot keys
        cmp     lastHotkeyCode          ; E312  the same code as last time?
        bne     L_E31C                  ; E315  no - a fresh key press
        cpx     #$80                    ; E317  same key: is the Commodore key still down?
L_E319:
        bcs     L_E33F                  ; E319  yes - swallow the repeat
L_E31B:
        rts                             ; E31B  hand the key to the game


; ----------------------------------------------------------------------
; Modem hot keys - a new key code with bit 7 set while the Commodore key is held. Every one of them is
; swallowed by returning $FF so the game never sees it.
; ----------------------------------------------------------------------
L_E31C:
        sta     lastHotkeyCode          ; E31C  remember the code so it is not handled twice
        cpx     #$80                    ; E31F  was the Commodore key held?
        bcc     L_E31B                  ; E321  no - it is an ordinary key for the game
        cmp     #$C8                    ; E323  C=+H
        bne     L_E332                  ; E325  not that key - try the next
        lda     #$01                    ; E327  command register bit 0 is DTR
        eor     aciaCommandShadow       ; E329  flip it in the shadow, since the register cannot be read back on every clone
        jsr     aciaSetCommand          ; E32C  and hand the new value to the ACIA
        bcs     L_E33F                  ; E32F  carry is still set from the CPX at $E31F - swallow the key
        .assert * <= $E332, error, "C=+H hot key overran $E331"
        .res    $E332 - *, $EA          ; E331  was the last byte of 'sta CIA2_PRB'
L_E332:
        cmp     #$C3                    ; E332  C=+C
        bne     L_E342                  ; E334  not that key - try the next
        lda     carrierOverrideToggleMask; E336  $40 = the carrier-override bit
L_E339:
        eor     carrierOverrideFlags    ; E339  flip 'pretend the carrier is present'
        sta     carrierOverrideFlags    ; E33C  (lets two machines play over a direct cable with no DCD)
L_E33F:
        lda     #$FF                    ; E33F  $FF tells the game 'no key'
        rts                             ; E341  return

; ----------------------------------------------------------------------
; Baud hot keys. The stock driver knew two speeds and tested for them one key at a time; the
; SwiftLink knows seven, so the same fourteen bytes now walk baudEntryTable backwards looking for the
; key code. Entries are still three bytes apart and still in the stock order, so index 0 is 300 baud
; and index 3 is 1200 baud - which is what openCommLink's disk-default baudIndex and the CONNECT 1200
; sniffer at $E7B0 both assume. C=+3 and C=+1 therefore keep their old meanings, and C=+2, C=+4,
; C=+9, C=+0 add 2400, 4800, 9600 and 19200. Anything else falls through to $E362.
; C=+8 selects 38400 again. Its hot-key byte was blanked when 38400 wedged the machine, which hid the
; defect instead of fixing it: wedgeDiagnosis.md then traced the wedge to unbounded NMI re-entrancy in
; commNmiHandler, and the guard at $E68F bounds it at one nested frame whatever the rate, so the entry
; is reachable from the keyboard once more.
; The walk now starts at 21, not 18, because there is an eighth entry: ADOPT. C=+U selects it and it
; means 'stop imposing a rate - use the control register the user's hardware already holds'. It is
; also the disk default, so a link opened without pressing anything comes up on the port's own
; settings. Pressing any of the seven speed keys stores that entry's index instead and takes the
; driver out of adopt mode until C=+U is pressed again.
; ----------------------------------------------------------------------
L_E342:
        ldx     #BAUD_INDEX_ADOPT       ; E342  start at the last entry, 21 = adopt: C=+U goes back to whatever the user's own hardware is configured for, and it costs nothing to search for it because the walk had to start somewhere. 18 = 38400 is the entry below it and its hot key, C=+8, works again - the wedge it used to cause was an NMI re-entrancy fault in commNmiHandler, not a property of the rate, and the guard at $E68F fixes it (wedgeDiagnosis.md)
L_E344:
        cmp     baudEntryTable,x        ; E344  does the key code match this entry?
        beq     L_E350                  ; E347  yes - this is the speed the player asked for
        dex                             ; E349  three bytes per entry, the same stride the stock ...
        dex                             ; E34A  ... baudParameterTable used, so that every ...
        dex                             ; E34B  ... existing caller's index still means the same speed
        bpl     L_E344                  ; E34C  keep looking
        bmi     L_E362                  ; E34E  ran off the bottom - not a baud key, try the rest
L_E350:
        stx     baudIndex               ; E350  remember the speed for the next open

; ----------------------------------------------------------------------
; loadBaudParameters - hand the 6551 the control byte the entry selected by X carries. For the seven
; fixed rates that byte is $10 (8 data bits, one stop bit, internal baud rate generator) OR the rate
; bits; a SwiftLink clocks its 6551 from a 3.6864 MHz crystal, twice what the chip expects, so every
; rate in the data sheet's table comes out doubled and $15 is 300 baud rather than 150.
; For X = 21 - adopt, the disk default - the byte is not a constant at all: aciaProbeAdopted $E655
; put the user's own control register there on the cold start, so what this routine writes back is the
; rate, stop bits and clock source the hardware was already configured for. Either way the routine
; itself does not care: it reads the table and writes what it finds, and the only thing it insists on
; comes from the entry, not from here.
; The byte is parked in bitPeriodLo, where the software UART's bit period used to live, so the live
; setting still sits in the module's own constants and can be read from a monitor.
; Returns A = $FF, which is what the hot-key path needs in order to swallow the key.
; In:  X = 0, 3, 6, 9, 12, 15, 18 (fixed rates) or 21 (adopt) - a three-byte index into baudEntryTable
; Out: the ACIA control register and bitPeriodLo programmed, A = $FF, X unchanged, Y clobbered by
;     aciaDetect on the one call per cold start that probes
; Called from: openCommLink $E2C9, the baud hot keys (fall-through from $E350),
;     runModemStateMachine $E7B2 when it reads a 'CONNECT 1200' result code
; ----------------------------------------------------------------------
loadBaudParameters:
        jsr     aciaDetect              ; E353  make sure we know which page the cartridge answers on. On every call but the first this is five bytes and an RTS: aciaPageLatch $E5BF already holds the answer, so a baud hot key on a live link re-programs the chip without re-probing for it and without writing a probe pattern into whatever is at $DE00
        lda     baudEntryTable+1,x      ; E356  the control byte for this speed
        sta     bitPeriodLo             ; E359  keep the live value where the bit period used to be
        jsr     aciaSetControlIdle      ; E35C  8N1 with the internal baud rate generator, then forget any byte in the transmitter - was 'jsr aciaSetControl' ($20 $67 $E5). A rate change is the one thing that can strand a character half way out of a 6551: the control register is rewritten under it and what the chip then does with the byte in its shift register is undefined, so its TDRE may never come back and txCharActive would stay set for ever. Clearing it here lets the next tick re-arm from a known state
        lda     #$FF                    ; E35F  swallow the hot key
        rts                             ; E361  return
        .assert * <= $E362, error, "loadBaudParameters overran $E361"

L_E362:
        cmp     #$8D                    ; E362  C=+RETURN
        bne     L_E374                  ; E364  not that key - try the next
        dec     skipModemInitString     ; E366  0 -> $FF: re-open without re-sending the AT string
        jsr     openCommLink_E2C1       ; E369  bring the link back up
        lda     #$8D                    ; E36C  a negative reply timer means 'not waiting for an answer'
        sta     modemReplyTimer         ; E36E  so the state machine does not time the modem out
        ldx     #$00                    ; E371  clear the Commodore-key flag the caller sees
        rts                             ; E373  and pass RETURN on to the game

L_E374:
        cmp     #$D0                    ; E374  C=+P
        beq     hangUpModem_E38D        ; E376  hang up (its tail returns $FF, so the key is swallowed)
        ldx     #$00                    ; E378  default: answer mode
L_E37A:
        cmp     #$C1                    ; E37A  C=+A
        beq     L_E384                  ; E37C  answer mode wanted
        cmp     #$CF                    ; E37E  C=+O
        bne     L_E38A                  ; E380  some other Commodore-key combination - just swallow it
        ldx     #$01                    ; E382  originate mode
L_E384:
        stx     isOriginateMode         ; E384  remember which AT string to send
        jsr     configureUserPortLines  ; E387  the port lines depend on answer/originate
L_E38A:
        lda     #$FF                    ; E38A  swallow the hot key
        rts                             ; E38C  return


; ----------------------------------------------------------------------
; hangUpModem - drop DTR and then queue the canned Hayes hang-up string, which the terminal-mode
; transmitter emits in reverse as [guard] '+++' [guard] CR [guard] 'ATH0' CR.
; In:  hangUpStringPtrLo ($EBFD = $D6), modemOptionFlags bit 7
; Out: CIA2_PRB with DTR low, hostOutRing loaded with the escape/hang-up string, modemReplyTimer = $FF
; Called from: commLinkControlDispatch $E2A2 (X>=3), the C=+P hot key at $E376
; ----------------------------------------------------------------------
hangUpModem_E38D:
        jsr     dropDtrLine             ; E38D  drop DTR, which by itself makes most modems hang up
        lda     hangUpStringPtrLo       ; E390  $D6 = low byte of the '+++ ... ATH0' string at $EBD6

; ----------------------------------------------------------------------
; queueModemCommandString - load a canned modem command string into the host-output ring. A is the low
; byte of an address in page $EB; the routine plants it in the LDA operand at modemStringLoad, copies
; bytes (including the terminating 0) forwards into the ring at modemCommandChars, then sets the write
; index to 0 and both the read index and the count to the number of characters. Since the ring pops
; with a decrementing index the string leaves the machine back to front, which is exactly why the AT
; strings at $EBD6-$EBFC are stored reversed. Does nothing but return A=$FF when bit 7 of
; modemOptionFlags says no modem is configured.
; In:  A = low byte of the string address in page $EB, modemOptionFlags
; Out: modemCommandChars.., hostOutReadIndex, hostOutWriteIndex, hostOutCount, modemReplyTimer = $FF,
;     A = $FF
; Called from: openCommLink $E2E3, hangUpModem via fall-through from $E390
; ----------------------------------------------------------------------
queueModemCommandString:
        ldx     modemOptionFlags        ; E393  is a modem configured at all (bit 7)?
        bpl     returnNoKey             ; E396  no modem - just return $FF
        sta     modemStringLoad+1       ; E398  self-modify the LDA below with the string's low byte
        ldx     #$FF                    ; E39B  start one before the first character
        stx     modemReplyTimer         ; E39D  negative reply timer: a command string is going out

; ----------------------------------------------------------------------
; Copy loop: the string is copied forwards into the ring, terminator included, and the read index is
; then set to the last character so the descending pop order sends it in the right order.
; ----------------------------------------------------------------------
L_E3A0:
        inx                             ; E3A0  next character
modemStringLoad:
        lda     D_EB00,x                ; E3A1  operand low byte patched at $E398; the page is fixed at $EB
        sta     modemCommandChars,x     ; E3A4  copy it into the host output ring from slot 1 upwards
        bne     L_E3A0                  ; E3A7  keep going until the terminating 0 has been copied
        sta     hostOutWriteIndex       ; E3A9  A is 0: the ring is written from the bottom again
        stx     hostOutReadIndex        ; E3AC  read index = the character count, so the string pops out back to front
        stx     hostOutCount            ; E3AF  that many characters to send (the terminator is never popped)

; ----------------------------------------------------------------------
; returnNoKey - the shared 'no key / key handled' return: A = $FF. It is the disk-image target of the
; $E012 keyboard hook and the early exit of queueModemCommandString when no modem is configured.
; In:  none
; Out: A = $FF
; Called from: keyboardScanHook $E012 (before the game patches it), queueModemCommandString $E396, and
;     by fall-through from $E3AF
; ----------------------------------------------------------------------
returnNoKey:
        lda     #$FF                    ; E3B2  $FF = 'no key' / 'handled'
        rts                             ; E3B4  return


; padByteE3B5: unknown, 1 bytes. Stray $10 between returnNoKey and the indirect link I/O jumps; not
; referenced.
padByteE3B5:
        .byte   $10                     ; E3B5  unreferenced pad byte between returnNoKey and the indirect jumps

; ----------------------------------------------------------------------
; sendLinkByte - indirect 'give one byte to the link'. The operand byte at $E3B7 selects which half of
; linkIoVectorTable is used: $BF = raw (straight into the UART transmit ring) or $C1 = framed (into
; the ARQ transmit ring). initCommModule always selects the framed one, so the raw target is dead in
; the shipped game.
; In:  A = byte
; Out: as the selected ring push; C=1 when that ring is full
; Called from: sendPacketFsm $E212/$E22D/$E24B
; ----------------------------------------------------------------------
sendLinkByte:
        jmp     (linkIoVectorTable)     ; E3B6  operand low byte is $BF (raw UART ring) or $C1 (framed ARQ ring)


; ----------------------------------------------------------------------
; receiveLinkByte - indirect 'take one byte from the link'. The operand at $E3BA selects $C3 = raw
; (pop the UART receive ring) or $C5 = framed (pop the ARQ receive ring, i.e. only payload the frame
; layer has already checksum-verified).
; In:  none
; Out: A = byte with C=0, or C=1 when nothing is queued
; Called from: receivePacketFsm $E157/$E183/$E1CC
; ----------------------------------------------------------------------
receiveLinkByte:
        jmp     ($E3C3)                 ; E3B9  operand low byte is $C3 (raw UART ring) or $C5 (framed ARQ ring)


; ----------------------------------------------------------------------
; reportLinkError - indirect link-error hook, called by the NMI receiver on a framing error, a bad
; stop bit or a receive-ring overflow. The operand at $E3BD selects $C7 = the no-op stub or $C9 = the
; counting stub; initCommModule selects the counter, whose value makes the link state machine restart
; byte sync.
; In:  none
; Out: linkErrorCount +1 in the framed configuration
; Called from: commNmiHandler $E6B3
; ----------------------------------------------------------------------
reportLinkError:
        jmp     ($E3C7)                 ; E3BC  operand low byte is $C7 (ignore) or $C9 (count the error)


; linkIoVectorTable: three pairs of alternative targets for the indirect jumps above. The first of
; each pair is the raw UART implementation, the second the framed (ARQ) one that
; selectFramedLinkVectors installs. Only the operand bytes of the JMPs are patched, never this table.
linkIoVectorTable:
        .addr   pushUartTxRing          ; E3BF  send, raw: straight into the UART transmit ring (never selected)
        .byte   $B7                     ; E3C1  send, framed: into the ARQ transmit ring - the one initCommModule selects
        .byte   $E4                     ; E3C2  .
        .byte   $D2                     ; E3C3  receive, raw: pop the UART receive ring (never selected)
        .byte   $E4                     ; E3C4  .
        .byte   $6E                     ; E3C5  receive, framed: pop the checksum-verified ARQ receive ring
        .byte   $E4                     ; E3C6  .
        .byte   $3F                     ; E3C7  error, raw: was the no-op stub $E408, which is dead code the module no longer contains; both halves of the pair now point at countLinkError so neither can land on something that is not an error hook (never selected)
L_E3C8:
        .byte   $E7                     ; E3C8  .
        .byte   $3F                     ; E3C9  error, framed: count it in linkErrorCount - countLinkError moved from $E403 to $E73F so that selectFramedLinkVectors could grow the five bytes that clear aciaPageLatch
        .byte   $E7                     ; E3CA  .

; ----------------------------------------------------------------------
; pollCarrierState - implementation of $E00C. Takes a fresh carrier sample (which also services the
; NMI suspend handshake), then debounces it: while the sample disagrees with the accepted linkStatus
; it counts up, and only after 240 consecutive disagreeing samples does it accept the new value. In
; between it merges just bit 7 of the sample, the 'characters are still arriving' bit, which the game
; watches before hanging up. Bit 6 of linkStatus is the debounced 'carrier present' flag.
; In:  linkStatusSample, linkStatus, carrierDebounceCount
; Out: linkStatus, carrierDebounceCount
; Called from: the $E00C jump-table entry
; ----------------------------------------------------------------------
pollCarrierState:
        jsr     serviceCarrierAndSuspendRequest; E3CB  run the suspend handshake and take a fresh carrier sample
        lda     linkStatusSample        ; E3CE  the new sample
        cmp     linkStatus              ; E3D1  does it agree with the accepted status?
        beq     L_E3E3                  ; E3D4  yes - reset the debounce counter
        inc     carrierDebounceCount    ; E3D6  no - count one disagreeing sample
        ldx     carrierDebounceCount    ; E3D9  how long has it disagreed?
        cpx     #$F0                    ; E3DC  240 frames of disagreement (about 4 seconds)
        bcc     L_E3EA                  ; E3DE  not yet - only take the activity bit for now
        sta     linkStatus              ; E3E0  accept the new carrier state
L_E3E3:
        ldx     #$00                    ; E3E3  restart the debounce count
        stx     carrierDebounceCount    ; E3E5  and start counting again from zero
        beq     L_E3F2                  ; E3E8  always taken
L_E3EA:
        and     #$80                    ; E3EA  bit 7 only: 'characters are still arriving'
        ora     linkStatus              ; E3EC  merge it into the status without touching the carrier bit
        sta     linkStatus              ; E3EF  store the merged status
L_E3F2:
        rts                             ; E3F2  return


; ----------------------------------------------------------------------
; selectFramedLinkVectors - rewrites the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD
; so that sendLinkByte, receiveLinkByte and reportLinkError use the framed (ARQ) rings and the error
; counter instead of the raw UART rings and the no-op error stub. Note it patches the pointers to the
; vectors, not the vectors themselves.
; It is also where the ACIA page latch is cleared, and the only place that clears it. This is the one
; routine in the module that runs exactly once per cold start and nowhere else: initCommModule $E2BA
; is its only caller, and initCommModule is the $E003 X=1 entry the game makes immediately after the
; overlay is loaded. Clearing the latch here is therefore 'clear it on a cold start and never again',
; which is exactly what aciaDetect needs - a link open or a baud hot key must not re-decide which page
; the cartridge is on. The five bytes it takes came from countLinkError, which moved to $E73F.
; In:  none
; Out: $E3B7 = $C1, $E3BA = $C5, $E3BD = $C9, aciaPageLatch = 0
; Called from: initCommModule $E2BA (the only caller, so the raw configuration never runs)
; ----------------------------------------------------------------------
selectFramedLinkVectors:
        lda     #$C1                    ; E3F3  $C1 = pushArqTxRing
        sta     sendLinkByte+1          ; E3F5  patch the operand of the JMP ($xxxx) at sendLinkByte
        lda     #$C5                    ; E3F8  $C5 = popArqRxRing
        sta     receiveLinkByte+1       ; E3FA  patch the operand at receiveLinkByte
        lda     #$C9                    ; E3FD  $C9 = countLinkError
        sta     reportLinkError+1       ; E3FF  patch the operand at reportLinkError
        lda     #$00                    ; E402  cold start: forget which page the cartridge was on...
        sta     aciaPageLatch           ; E404  ...so the next link open probes for it once more
        rts                             ; E407  return


; ----------------------------------------------------------------------
; dropDtrLine - clear the whole command register. Bit 0 is DTR, and dropping it is what makes a Hayes
; modem let go of the line; on a 6551 clearing it also shuts the receiver and every ACIA interrupt
; down, which is exactly what hanging up wants. commLinkControlDispatch follows this with stopCommNmi.
; It moved here from $E574, into the bytes aciaSetCommandIdle used to occupy, so that silenceCia2
; could leave the NMI block. aciaSetCommandIdle - 'write the command register and then forget the byte
; the driver believed was in the transmitter' - is gone because the re-entrancy guard makes its second
; half provably unnecessary: the NMI's disarm site at $E6E3 is reached only after the transmit half
; has already cleared txCharActive (or found it clear), and no nested pass can run the body of the
; handler any more, which is the one path that used to be able to set the flag again in between.
; healStuckTransmitter $E057 still repairs the flag once a frame if anything else ever strands it.
; In:  none
; Out: the ACIA command register and aciaCommandShadow = 0
; Called from: hangUpModem $E38D (the only caller)
; ----------------------------------------------------------------------
dropDtrLine:
        lda     #$00                    ; E408  no DTR, no RTS, no interrupts
        jmp     aciaSetCommand          ; E40A  the modem drops the call
        .assert * <= $E40F, error, "dropDtrLine overran $E40E"
        .res    $E40F - *, $EA          ; E40D  spare

; ----------------------------------------------------------------------
; clearUartVars - zeroes the 90-byte ring-buffer block $E414-$E46D: all four descending circular
; buffers (UART transmit 22, UART receive 20, ARQ transmit 18, ARQ receive 18) with their counts and
; read/write indices.
; In:  none
; Out: $E414-$E46D = 0; returns to the caller of clearUartVars
; Called from: initCommModule $E2B7
; ----------------------------------------------------------------------
clearUartVars:
        ldx     #$5A                    ; E40F  90 bytes to clear: all four rings with their counts and indices
        jsr     clearInlineVarBlock     ; E411  clear the inline block that follows and return to our caller's caller

; ----------------------------------------------------------------------
; Ring buffers, $E414-$E46D, the inline block clearUartVars zeroes. Four descending circular buffers,
; each with a count byte and separate read and write indices: UART transmit (22), UART receive (20),
; frame-layer transmit (18) and frame-layer receive (18).
; ----------------------------------------------------------------------
uartTxCount:
        brk                             ; E414  uartTxCount: characters in the 22-byte UART transmit ring (rendered as BRK - data, never executed)
uartTxReadIndex:
        .byte   $00                     ; E415  uartTxReadIndex
uartTxWriteIndex:
        .byte   $00                     ; E416  uartTxWriteIndex
uartTxRing:
        .byte   $00,$00,$00,$00,$00,$00,$00,$00; E417  ........  uartTxRing: 22 bytes, $E417-$E42C
        .byte   $00                     ; E41F  uartTxRing continued
        .byte   $00,$00,$00,$00,$00,$00,$00; E420  .......
        .byte   $00                     ; E427  uartTxRing continued
        .byte   $00,$00                 ; E428  ..
L_E42A:
        .byte   $00                     ; E42A  the last three bytes of the 22-byte transmit ring
        .byte   $00,$00                 ; E42B  ..
uartRxCount:
        .byte   $00                     ; E42D  uartRxCount: characters in the 20-byte UART receive ring
uartRxReadIndex:
        .byte   $00                     ; E42E  uartRxReadIndex
uartRxWriteIndex:
        .byte   $00                     ; E42F  uartRxWriteIndex
uartRxRing:
        .byte   $00                     ; E430  .  uartRxRing: 20 bytes, $E430-$E443
L_E431:
        .byte   $00                     ; E431  uartRxRing continued
        .byte   $00,$00,$00             ; E432  ...
L_E435:
        .byte   $00                     ; E435  uartRxRing continued
        .byte   $00,$00,$00,$00,$00,$00,$00; E436  .......
        .byte   $00                     ; E43D  the last seven bytes of the 20-byte receive ring
        .byte   $00,$00,$00,$00,$00,$00 ; E43E  ......
arqTxCount:
        .byte   $00                     ; E444  arqTxCount: bytes in the 18-byte frame-layer transmit ring
arqTxReadIndex:
        .byte   $00                     ; E445  arqTxReadIndex
arqTxWriteIndex:
        .byte   $00                     ; E446  arqTxWriteIndex
arqTxRing:
        .byte   $00,$00,$00,$00,$00,$00,$00,$00; E447  ........  arqTxRing: 18 bytes, $E447-$E458 - packet layer to frame layer
        .byte   $00                     ; E44F  arqTxRing continued
        .byte   $00,$00,$00,$00,$00,$00,$00; E450  .......
        .byte   $00                     ; E457  the last two bytes of the 18-byte frame transmit ring
        .byte   $00                     ; E458  .
arqRxCount:
        .byte   $00                     ; E459  arqRxCount: bytes in the 18-byte frame-layer receive ring
arqRxReadIndex:
        .byte   $00                     ; E45A  arqRxReadIndex
arqRxWriteIndex:
        .byte   $00                     ; E45B  arqRxWriteIndex
arqRxRing:
        .byte   $00,$00,$00,$00,$00,$00,$00,$00; E45C  ........  arqRxRing: 18 bytes, $E45C-$E46D - frame layer to packet layer
L_E464:
        .byte   $00                     ; E464  arqRxRing continued
        .byte   $00,$00,$00,$00,$00,$00,$00; E465  .......
        .byte   $00                     ; E46C  the last two bytes of the ring sit immediately before popArqRxRing
        .byte   $00                     ; E46D  .

; ----------------------------------------------------------------------
; popArqRxRing - pop the oldest byte out of the 18-byte ARQ receive ring, the queue the frame layer
; fills with checksum-verified payload. This is the framed target of receiveLinkByte, i.e. how the
; packet layer gets its bytes.
; In:  arqRxCount, arqRxReadIndex, arqRxRing
; Out: A = byte with C=0, or C=1 when empty
; Called from: receiveLinkByte $E3B9 through vector $E3C5
; ----------------------------------------------------------------------
popArqRxRing:
        lda     arqRxCount              ; E46E  anything in the frame-layer receive ring?
        beq     returnRingEmpty         ; E471  empty - C=1
        ldx     arqRxReadIndex          ; E473  oldest slot
        lda     arqRxRing,x             ; E476  fetch the byte
        dex                             ; E479  walk the read index down
        bpl     L_E47E                  ; E47A  still inside the ring?
        ldx     #$11                    ; E47C  wrapped - back to slot 17 of the 18-byte ring
L_E47E:
        stx     arqRxReadIndex          ; E47E  store the new read index
        dec     arqRxCount              ; E481  one byte fewer
        clc                             ; E484  C=0: here is a byte
        rts                             ; E485  return


; ----------------------------------------------------------------------
; popArqTxRing - pop the oldest byte out of the 18-byte ARQ transmit ring, i.e. take the bytes the
; packet layer queued so the frame builder can wrap them in a checksummed packet.
; In:  arqTxCount, arqTxReadIndex, arqTxRing
; Out: A = byte with C=0, or C=1 when empty
; Called from: popArqTxRingThunk $E733 (used by the frame builder at $E8AE/$E8D4)
; ----------------------------------------------------------------------
popArqTxRing:
        lda     arqTxCount              ; E486  anything queued for the frame builder?
        beq     returnRingEmpty         ; E489  empty - C=1
        ldx     arqTxReadIndex          ; E48B  oldest slot
        lda     arqTxRing,x             ; E48E  fetch the byte
        dex                             ; E491  walk the read index down
        bpl     L_E496                  ; E492  still inside the ring?
        ldx     #$11                    ; E494  wrapped - back to slot 17
L_E496:
        stx     arqTxReadIndex          ; E496  store the new read index
        dec     arqTxCount              ; E499  one byte fewer
        clc                             ; E49C  C=0: here is a byte
        rts                             ; E49D  return


; ----------------------------------------------------------------------
; pushArqRxRing - push one validated payload byte into the 18-byte ARQ receive ring; the byte is
; dropped when the ring already holds 18.
; In:  A = byte
; Out: arqRxRing, arqRxCount, arqRxWriteIndex
; Called from: pushArqRxRingThunk $E736 (used by the frame receiver at $EA91)
; ----------------------------------------------------------------------
pushArqRxRing:
        ldx     arqRxCount              ; E49E  how full is the frame-layer receive ring?
        cpx     #$12                    ; E4A1  it holds 18 bytes
        bcs     L_E4B6                  ; E4A3  full - drop the byte
        ldx     arqRxWriteIndex         ; E4A5  next free slot
        sta     arqRxRing,x             ; E4A8  store the validated payload byte
        dex                             ; E4AB  walk the write index down
        bpl     L_E4B0                  ; E4AC  still inside the ring?
L_E4AE:
        ldx     #$11                    ; E4AE  wrapped - back to slot 17
L_E4B0:
        stx     arqRxWriteIndex         ; E4B0  store the new write index
        inc     arqRxCount              ; E4B3  one more byte for the packet layer
L_E4B6:
        rts                             ; E4B6  return


; ----------------------------------------------------------------------
; pushArqTxRing - push one byte into the 18-byte ARQ transmit ring; the byte is dropped when the ring
; is full, and the caller sees C=1 because the count compare left it set. This is the framed target of
; sendLinkByte.
; In:  A = byte
; Out: arqTxRing, arqTxCount, arqTxWriteIndex
; Called from: sendLinkByte $E3B6 through vector $E3C1
; ----------------------------------------------------------------------
pushArqTxRing:
        ldx     arqTxCount              ; E4B7  how full is the frame-layer transmit ring?
        cpx     #$12                    ; E4BA  it holds 18 bytes
        bcs     L_E4CF                  ; E4BC  full - return with C=1 so the packet layer retries
        ldx     arqTxWriteIndex         ; E4BE  next free slot
        sta     arqTxRing,x             ; E4C1  store the byte
        dex                             ; E4C4  walk the write index down
        bpl     L_E4C9                  ; E4C5  still inside the ring?
L_E4C7:
        ldx     #$11                    ; E4C7  wrapped - back to slot 17
L_E4C9:
        stx     arqTxWriteIndex         ; E4C9  store the new write index
        inc     arqTxCount              ; E4CC  one more byte for the frame builder
L_E4CF:
        rts                             ; E4CF  return


; ----------------------------------------------------------------------
; returnRingEmpty - the shared 'nothing in that ring' exit (SEC / RTS) used by the four ring pop
; routines.
; In:  none
; Out: C = 1
; Called from: popArqRxRing $E471, popArqTxRing $E489, popUartRxRing $E4D5, popUartTxRing $E4ED
; ----------------------------------------------------------------------
returnRingEmpty:
        sec                             ; E4D0  C=1 means 'that ring was empty'
        rts                             ; E4D1  return


; ----------------------------------------------------------------------
; popUartRxRing - pop one received character out of the 20-byte raw UART receive ring the NMI fills.
; In:  uartRxCount, uartRxReadIndex, uartRxRing
; Out: A = character with C=0, or C=1 when empty
; Called from: runModemStateMachine $E799 (terminal mode), beginByteSyncPhase $E80A,
;     popUartRxRingThunk $E739, and receiveLinkByte through vector $E3C3 in the never-selected raw
;     configuration
; ----------------------------------------------------------------------
popUartRxRing:
        lda     uartRxCount             ; E4D2  any received characters?
        beq     returnRingEmpty         ; E4D5  none - C=1
        ldx     uartRxReadIndex         ; E4D7  oldest slot
        lda     uartRxRing,x            ; E4DA  fetch the character
        dex                             ; E4DD  walk the read index down
        bpl     L_E4E2                  ; E4DE  still inside the ring?
        ldx     #$13                    ; E4E0  wrapped - back to slot 19 of the 20-byte ring
L_E4E2:
        stx     uartRxReadIndex         ; E4E2  store the new read index
        dec     uartRxCount             ; E4E5  one character fewer
        clc                             ; E4E8  C=0: here is a character
        rts                             ; E4E9  return


; ----------------------------------------------------------------------
; popUartTxRing - pop the next character to transmit out of the 22-byte raw UART transmit ring.
; In:  uartTxCount, uartTxReadIndex, uartTxRing
; Out: A = character with C=0, or C=1 when empty
; Called from: commNmiHandler $E6D1, flushUartTxRing $E534
; ----------------------------------------------------------------------
popUartTxRing:
        lda     uartTxCount             ; E4EA  anything left to transmit?
        beq     returnRingEmpty         ; E4ED  no - C=1
        ldx     uartTxReadIndex         ; E4EF  oldest slot
        lda     uartTxRing,x            ; E4F2  fetch the character
        dex                             ; E4F5  walk the read index down
        bpl     L_E4FA                  ; E4F6  still inside the ring?
        ldx     #$15                    ; E4F8  wrapped - back to slot 21 of the 22-byte ring
L_E4FA:
        stx     uartTxReadIndex         ; E4FA  store the new read index
        dec     uartTxCount             ; E4FD  one character fewer
        clc                             ; E500  C=0: here is a character
        rts                             ; E501  return


; ----------------------------------------------------------------------
; pushUartRxRing - push one fully received character into the 20-byte raw UART receive ring. When the
; ring is full it leaves everything alone and returns with C=1 from the count compare, which the NMI
; treats as a receive overflow.
; In:  A = received character
; Out: uartRxRing, uartRxCount, uartRxWriteIndex; C=1 on overflow
; Called from: commNmiHandler $E6AE (the only caller)
; ----------------------------------------------------------------------
pushUartRxRing:
        ldx     uartRxCount             ; E502  how full is the receive ring?
        cpx     #$14                    ; E505  it holds 20 characters
        bcs     L_E51A                  ; E507  full - leave it alone and return with C=1, which the NMI treats as an overflow error
        ldx     uartRxWriteIndex        ; E509  next free slot
        sta     uartRxRing,x            ; E50C  store the character the UART just assembled
        dex                             ; E50F  walk the write index down
        bpl     L_E514                  ; E510  still inside the ring?
        ldx     #$13                    ; E512  wrapped - back to slot 19
L_E514:
        stx     uartRxWriteIndex        ; E514  store the new write index
        inc     uartRxCount             ; E517  one more character for the protocol
L_E51A:
        rts                             ; E51A  return


; ----------------------------------------------------------------------
; pushUartTxRing - push one character into the 22-byte raw UART transmit ring; the byte is dropped
; when the ring is full.
; In:  A = character
; Out: uartTxRing, uartTxCount, uartTxWriteIndex
; Called from: queueByteForTransmit $E73C, and sendLinkByte through vector $E3BF in the never-selected
;     raw configuration
; ----------------------------------------------------------------------
pushUartTxRing:
        ldx     uartTxCount             ; E51B  how full is the transmit ring?
        cpx     #$16                    ; E51E  it holds 22 characters
        bcs     L_E533                  ; E520  full - drop the character
        ldx     uartTxWriteIndex        ; E522  next free slot
        sta     uartTxRing,x            ; E525  store it
        dex                             ; E528  walk the write index down
L_E529:
        bpl     L_E52D                  ; E529  still inside the ring?
        ldx     #$15                    ; E52B  wrapped - back to slot 21
L_E52D:
        stx     uartTxWriteIndex        ; E52D  store the new write index
L_E530:
        inc     uartTxCount             ; E530  one more character to shift out
L_E533:
        rts                             ; E533  return


; ----------------------------------------------------------------------
; flushUartTxRing - throw away everything still queued for transmission, then set the pending-byte
; count to 1 if a character is still being shifted out or 0 if not. Used whenever the connection state
; changes so stale bytes cannot reach the peer.
; In:  the UART transmit ring, txCharActive
; Out: transmit ring emptied, uartPendingCount resynchronised
; Called from: runModemStateMachine $E791, beginByteSyncPhase $E80F
; ----------------------------------------------------------------------
flushUartTxRing:
        jsr     popUartTxRing           ; E534  throw the next queued character away
        bcc     flushUartTxRing         ; E537  until the ring is empty
        lda     txCharActive            ; E539  1 if a character is still being shifted out
        sta     uartPendingCount        ; E53C  resynchronise the pending count with reality
        rts                             ; E53F  return


; ----------------------------------------------------------------------
; configureUserPortLines - on a SwiftLink there is no user port to configure, so this is now 'open
; the ACIA'. It re-derives the control byte from the speed the player last chose - openCommLink skips
; loadBaudParameters when the link is already up, so this is the only place a re-open gets its baud
; rate - then writes the command register with DTR asserted, RTS low, the receiver interrupt on and
; the transmit interrupt off. Finally it drains the receiver and reads the status register, which
; clears the error and IRQ flags and seeds the DCD shadow the carrier sampler reads.
; The bytes the CIA2 programming used to occupy now hold the six one-instruction accessors that every
; other routine uses to reach the ACIA. Their operand high bytes are the only record of the
; cartridge's base page, and aciaSetPage patches all six at once.
; In:  baudIndex
; Out: the ACIA control and command registers programmed, aciaStatusSave seeded
; Called from: openCommLink $E2D5, the C=+A / C=+O hot keys at $E387
; ----------------------------------------------------------------------
configureUserPortLines:
        ldx     baudIndex               ; E540  the speed the player last selected
        jsr     loadBaudParameters      ; E543  find the cartridge and program the control register
        lda     #$09                    ; E546  DTR on, RTS low, receiver IRQ on, transmit IRQ off
        jsr     aciaSetCommand          ; E548  the receiver is live from here on
        jsr     aciaGetData             ; E54B  throw away anything the receiver had already latched
        jmp     aciaGetStatus           ; E54E  clear the error and IRQ flags, and seed the DCD shadow


; ----------------------------------------------------------------------
; aciaSetCommand - write the command register and keep a shadow of it. DTR, RTS and both interrupt
; enables live in that one byte, so nothing can be changed without rewriting the whole register, and
; some 6551 replicas do not read it back reliably.
; In:  A = command byte
; Out: aciaCommandShadow and the ACIA command register = A; A, X, Y and C unchanged
; ----------------------------------------------------------------------
aciaSetCommand:
        sta     aciaCommandShadow       ; E551  remember what the register now holds
aciaCmdWrite:
        sta     $DE02                   ; E554  operand high byte patched by aciaSetPage
        rts                             ; E557  return

; ----------------------------------------------------------------------
; aciaGetStatus - read the status register, which is also the acknowledge: reading it clears the
; ACIA's IRQ flag. The value is kept in aciaStatusSave so the NMI can test it twice without a second
; read, and so the carrier sampler has a DCD bit to look at without touching the hardware itself.
; In:  none
; Out: A = aciaStatusSave = the status byte; X and Y unchanged
; ----------------------------------------------------------------------
aciaGetStatus:
aciaStatusRead:
        lda     $DE01                   ; E558  operand high byte patched by aciaSetPage
        sta     aciaStatusSave          ; E55B  publish it for the rest of the driver
        rts                             ; E55E  return

; aciaGetData - take the received byte out of the data register, which also clears RDRF.
aciaGetData:
aciaDataRead:
        lda     $DE00                   ; E55F  operand high byte patched by aciaSetPage
        rts                             ; E562  return

; aciaPutData - hand one byte to the transmitter, which also clears TDRE.
aciaPutData:
aciaDataWrite:
        sta     $DE00                   ; E563  operand high byte patched by aciaSetPage
        rts                             ; E566  return

; aciaSetControl - write the control register: stop bits, word length, clock source and baud rate.
; It falls straight through into aciaGetControl, so it also hands the caller back whatever the
; register reads as afterwards. That costs one NOP and buys the probe its write-then-read-back in a
; single JSR instead of two. Every byte that reaches here already has 8 data bits in it: the table's
; are constants, and the adopted one is masked at aciaProbeAdopted $E665, which is the only place a
; control byte the driver did not write itself can enter the module.
; The only other caller, aciaSetControlIdle $E04C, throws A away on its very next instruction.
aciaSetControl:
aciaCtrlWrite:
        sta     $DE03                   ; E567  operand high byte patched by aciaSetPage
        nop                             ; E56A  padding so that aciaGetControl keeps its old address
        ; falls into aciaGetControl

; aciaGetControl - read the control register back. It is the only ACIA register that can be written
; and read with no side effect on the receiver, which is what makes it the one to probe with.
aciaGetControl:
aciaCtrlRead:
        lda     $DE03                   ; E56B  operand high byte patched by aciaSetPage
        rts                             ; E56E  return
        .assert * = $E56F, error, "the ACIA accessors must end at $E56E"


; ----------------------------------------------------------------------
; silenceCia2 - make CIA2 let go of the NMI line and keep it that way. Two separate things are needed
; and the stock driver did both; a 6551 build that only does the first can wedge.
; Writing $7F to the mask stops any further CIA2 source from raising /NMI, and pinning ciaIcrShadow at
; $7F matters because readFilmByte $5874, writeFilmByte $5893 and copyPageUnderIo $58B2 write that
; byte straight back into CIA2_ICR after every film-buffer access - its image value is $90, which
; would arm the FLAG line, and the byte is inside the frozen block so it can only be corrected here.
; But the mask does not clear what has already latched: a 6526 holds its interrupt line down until the
; interrupt register is READ. /NMI is a single wire shared by CIA2, RESTORE and the expansion port, so
; one latched CIA2 flag left standing pins it low for ever and the ACIA can never deliver another
; edge - the link would simply stop. Nothing else in this driver ever reads CIA2_ICR, so this is the
; only chance to clear it. BIT leaves A alone, which is what the caller expects.
; It used to sit at $E727, at the end of the block the NMI handler shares with the page probe. The
; re-entrancy guard and the adopt-mode probe needed those twelve bytes, and these twelve came free
; here when dropDtrLine moved into the seven bytes aciaSetCommandIdle no longer needs at $E408.
; In:  none
; Out: ciaIcrShadow = CIA2_ICR = $7F, CIA2's interrupt latches cleared, A = $7F, X, Y unchanged
; Called from: setCiaNmiMask $E594
; ----------------------------------------------------------------------
silenceCia2:
        lda     #$7F                    ; E56F  $7F clears every CIA2 interrupt source
        sta     ciaIcrShadow            ; E571  the main program restores this into CIA2_ICR for us
        sta     CIA2_ICR                ; E574  so pin it at 'CIA2 silent'
        bit     CIA2_ICR                ; E577  and read it, or a source that latched before the mask was cleared keeps /NMI low and the cartridge is never heard from again
        rts                             ; E57A  return
        .assert * = $E57B, error, "silenceCia2 must end at $E57A"


; ----------------------------------------------------------------------
; installCommNmiVector - point the RAM NMI vector at $FFFA/$FFFB at commNmiHandler, so the
; cartridge's NMI - and every other NMI, which the handler chains on - lands there.
; In:  none
; Out: $FFFA/$FFFB = $E685
; Called from: openCommLink $E2CC
; ----------------------------------------------------------------------
installCommNmiVector:
        lda     #$85                    ; E57B  low byte of commNmiHandler
        ldx     #$E6                    ; E57D  high byte
        sta     nmiVector               ; E57F  the NMI vector lives in RAM at $FFFA/$FFFB (the ROMs are switched out)
        stx     nmiVectorHi             ; E582  the CPU now enters commNmiHandler on every CIA2 interrupt
        rts                             ; E585  return


; ----------------------------------------------------------------------
; restartUart - (re)start the link: forget any byte that was in the transmitter, throw away whatever
; the ACIA latched while it was quiet, ask the modem state machine to re-evaluate, prime the
; quiet-line timer, and fall into setCiaNmiMask with the enable mask so the receiver interrupt comes
; back. This is both the cold-start path and the resume half of the disk-load handshake.
; In:  none
; Out: txCharActive = 0, uartRestartRequest = 1, carrierSampleTimer = $92, the ACIA receiver
;     interrupt armed, nmiSuspendRequest/Ack = 0
; Called from: openCommLink $E2CF, serviceCarrierAndSuspendRequest $E5E1 (resume after a disk load)
; ----------------------------------------------------------------------
restartUart:
        jsr     aciaResumeReceiver      ; E586  clear txCharActive, drain the receiver, re-read status
        lda     #$01                    ; E589  1 = re-evaluate (only $FF forces the phase back to 0)
        sta     uartRestartRequest      ; E58B  ask the modem state machine to re-evaluate the connection
        lda     #$92                    ; E58E  the mask byte setCiaNmiMask reads as 'interrupt on'
        sta     carrierSampleTimer      ; E590  prime the quiet-line timer with the same negative value
        .assert * <= $E593, error, "restartUart overran $E592"

; ----------------------------------------------------------------------
; setCiaNmiMask - the single point where this module's interrupt is armed or disarmed. The callers
; still pass the old CIA2 mask bytes, and only their sign matters: $92 (or $83) means 'let the module
; interrupt', $7F means 'silence it'. Two things happen here that the stock driver did not need.
; First, ciaIcrShadow is pinned at $7F, because the main program writes that byte straight back into
; CIA2_ICR after every film-buffer access ($588C, $58AB, $58D0) and this driver wants no CIA2
; interrupt at all - its own comes from the expansion port. Second, disabling sets command bit 1
; (receiver IRQ off) but leaves bit 0 (DTR) up, so a disk load does not make the modem hang up, and
; then reads the status register so no already-latched IRQ flag is left holding /NMI down.
; The CIA2 half of that is in silenceCia2 below, because clearing the mask is not enough on its own.
; In:  A = $92/$83 to arm, $7F to silence
; Out: ciaIcrShadow = CIA2_ICR = $7F, nmiSuspendRequest/Ack = $00 or $80, the ACIA command register
; Called from: restartUart (fall-through from $E590), stopCommNmi $E5CA
; ----------------------------------------------------------------------
setCiaNmiMask:
        pha                             ; E593  keep the caller's intent
        jsr     silenceCia2             ; E594  pin CIA2 silent and let go of any /NMI it is already holding down
        pla                             ; E597  back to the caller's mask
        eor     #$FF                    ; E598  $92/$83 -> $00, $7F -> $80
        and     #$80                    ; E59A  keep only the sign bit
        sta     nmiSuspendRequest       ; E59C  so a disable mask marks the module suspended...
        sta     nmiSuspendAck           ; E59F  ...and acknowledges it in the same breath
        bne     L_E5A9                  ; E5A2  $80 = the caller wants the interrupt off
aciaCommandReceiveOnly:
        lda     #$09                    ; E5A4  DTR on, RTS low, receiver IRQ on, transmit IRQ off
        jmp     aciaSetCommand          ; E5A6  the ACIA may interrupt again. The NMI's transmit half
                                        ;       calls this label as a subroutine when it finds the
                                        ;       ring dry: 'lda #$09 / jmp aciaSetCommand' is exactly
                                        ;       what it wants and the two bytes it saves are two the
                                        ;       block round commNmiHandler does not have
L_E5A9:
        lda     #$0B                    ; E5A9  the same but with the receiver IRQ disabled; DTR stays up so the call survives the disk load
        jsr     aciaSetCommand          ; E5AB  no ACIA interrupt can fire from here on
        jmp     aciaGetStatus           ; E5AE  clear any latched IRQ flag so /NMI is released

; ----------------------------------------------------------------------
; aciaResumeReceiver - the bring-the-receiver-back half of restartUart: forget the byte that was in
; the transmitter, throw away anything the ACIA latched while the interrupt was off, and read the
; status register so no stale error or IRQ flag is left behind. Whatever arrived during a disk load
; is lost on purpose - the frame layer resynchronises, exactly as it did with the stock driver.
; It used to live at $E658, inside the block the NMI handler shares with the page probe; the
; re-entrancy guard and the adopt-mode probe needed those nine bytes, and these nine came free here
; when the four NOPs above and clearUartState below were dropped. clearUartState was a two-byte call
; into clearInlineVarBlock whose whole inline block was the single byte txCharActive $E5BB, so
; clearTxCharActive $E04F does exactly the same work in the same three bytes at the one call site.
; In:  none
; Out: txCharActive = 0, the receiver drained, aciaStatusSave refreshed
; Called from: restartUart $E586
; ----------------------------------------------------------------------
aciaResumeReceiver:
        jsr     clearTxCharActive       ; E5B1  txCharActive = 0
        jsr     aciaGetData             ; E5B4  discard whatever the receiver latched
        jmp     aciaGetStatus           ; E5B7  clear the error bits and refresh the DCD shadow
        .assert * <= $E5BB, error, "setCiaNmiMask and aciaResumeReceiver overran $E5BA"
        .res    $E5BB - *, $EA          ; E5BA  spare

; ----------------------------------------------------------------------
; UART state, $E5BB-$E5C0. All of it must survive a restart, because it is the driver's only memory
; of hardware it cannot read back safely - which page the cartridge is on is held in the accessors'
; operands, but what was last written to the command register, and what the status register last
; said, are held here. (txCharActive used to be cleared by clearUartState, a two-byte call whose
; inline block was this single byte; clearTxCharActive $E04F does the same thing and the routine is
; gone.)
; ----------------------------------------------------------------------
txCharActive:
        .byte   $00                     ; E5BB  txCharActive: non-zero while a byte handed to the ACIA has not had its TDRE come back
aciaStatusSave:
        .byte   $00                     ; E5BC  aciaStatusSave: the last status byte read, and the only DCD the carrier sampler sees. $00 = carrier present, the right default for a cable with no DCD wire
nmiHandlerState:
        .byte   $00                     ; E5BD  nmiHandlerState: the NMI re-entrancy guard. 0 = no pass of commNmiHandler is running, 1 = one is, 2 = one is and a second NMI arrived while it worked and left a note for it. Nothing else may write it. (Was aciaRxByte, which the handler no longer needs: it tests the error bits before it takes the character out of the data register instead of after)
aciaCommandShadow:
        .byte   $00                     ; E5BE  aciaCommandShadow: our copy of the command register - DTR, RTS and both interrupt enables share that one byte
aciaPageLatch:
        .byte   $00                     ; E5BF  aciaPageLatch: $DE or $DF once the page question has been settled, $00 before that. Written by aciaSetPage with every aim, read by aciaDetect, and cleared only by selectFramedLinkVectors $E3F3 - i.e. only by initCommModule, once per cold start. Its stored $00 is also what a fresh overlay load gives it (was rxBitCounter)
        .byte   $00                     ; E5C0  spare (was rxShiftRegister)

; ----------------------------------------------------------------------
; suspendUartIfRunning - two-byte entry between the UART state block and stopCommNmi: the disk loader
; has asked for a suspend, so shut the NMI UART down unless the acknowledge byte already says it is
; down.
; In:  C = bit 7 of nmiSuspendAck (set when already suspended)
; Out: falls into stopCommNmi when the suspend has not been acknowledged yet
; Called from: serviceCarrierAndSuspendRequest $E5DD
; ----------------------------------------------------------------------
suspendUartIfRunning:
        bcs     L_E5D5                  ; E5C1  the suspend is already acknowledged - nothing to do

; ----------------------------------------------------------------------
; stopCommNmi - tear the software UART down: ask the modem state machine to fall back to phase 0,
; disable every CIA2 NMI source (which also sets both suspend bytes to $80 = suspended), and forget
; the character that was half way out of the transmitter.
; In:  txCharActive
; Out: CIA2 NMI sources off, nmiSuspendRequest/Ack = $80, uartRestartRequest = $FF, uartPendingCount
;     adjusted
; Called from: commLinkControlDispatch $E2AB, suspendUartIfRunning $E5C1
; ----------------------------------------------------------------------
stopCommNmi:
        lda     #$FF                    ; E5C3  $FF asks the modem state machine to fall back to phase 0
        sta     uartRestartRequest      ; E5C5  so the connection falls back to phase 0 when the UART comes back
        lda     #$7F                    ; E5C8  $7F = clear every CIA2 interrupt source
L_E5CA:
        jsr     setCiaNmiMask           ; E5CA  disable the NMI UART (this also sets both suspend bytes to $80)
        lda     txCharActive            ; E5CD  was a character half way out?
        beq     L_E5D5                  ; E5D0  no
        dec     uartPendingCount        ; E5D2  yes - it will never finish, so drop it from the pending count
L_E5D5:
        rts                             ; E5D5  return


; ----------------------------------------------------------------------
; serviceCarrierAndSuspendRequest - first half of the $E00C service. It runs the two-way suspend
; handshake the disk loader uses ($E039 written by the game, $E03A answered here) and then, when the
; link is up, turns the last status byte the NMI read into the $80/$F8 carrier sample the rest of the
; game expects. On a SwiftLink the modem's DCD arrives on status bit 6 - CMD deliberately swapped DCD
; and DSR at the chip so the 6551's receiver is never gated off - and it is active low. Because DCD,
; DSR and CTS are pulled up on the cartridge, a three-wire null-modem cable with no DCD wire reads as
; permanent carrier, which is exactly what a direct connection wants; the C=+C override is still
; there for a cable that does carry a real DCD from a peer that is not asserting it.
; Nothing here reads the ACIA: the status register is sampled only by the NMI, because reading it in
; the main line would clear an IRQ flag the interrupt has not seen yet. A DCD or DSR change raises an
; ACIA interrupt of its own, so the shadow tracks the line even on a silent link.
; In:  nmiSuspendRequest, nmiSuspendAck, isLinkActive, carrierOverrideFlags, aciaStatusSave,
;     carrierSampleTimer
; This is also the once-per-frame path the transmitter's deadlock cure sits on: when the link is up
; the tail of this routine falls through $E604 into startNextTxChar, whose first act is now
; healStuckTransmitter $E057. There is no room to put the call inside this routine - its 41 bytes are
; solid - and no need: the fall-through is unconditional, so the heal runs on every raster IRQ the
; link is up, which is every frame that a stuck transmitter could matter in.
; Out: linkStatusSample, carrierSampleTimer, the receiver interrupt started or stopped
; Called from: pollCarrierState $E3CB
; ----------------------------------------------------------------------
serviceCarrierAndSuspendRequest:
        lda     nmiSuspendAck           ; E5D6  the acknowledge byte
        asl     a                       ; E5D9  carry = bit 7: are we currently suspended?
        lda     nmiSuspendRequest       ; E5DA  the request byte the game writes
        bmi     suspendUartIfRunning    ; E5DD  $C0 = the disk loader wants the interrupt off
        bcc     L_E5E4                  ; E5DF  no request and not suspended - carry on
        jsr     restartUart             ; E5E1  no request but still suspended - bring the receiver back
L_E5E4:
        lda     isLinkActive            ; E5E4  is the link up at all?
        beq     aciaDetectDone          ; E5E7  no - nothing to sample
        lda     carrierOverrideFlags    ; E5E9  the carrier override byte
        asl     a                       ; E5EC  bit 6 into bit 7
        bmi     L_E5F8                  ; E5ED  the C=+C override is on - report carrier without looking at DCD
        lda     #$F0                    ; E5EF  the 'carrier present' pattern, before the shift below
        bit     aciaStatusSave          ; E5F1  V = status bit 6 = the modem's DCD on a SwiftLink
        bvc     L_E5F8                  ; E5F4  DCD is active low, so a clear bit means carrier
        lda     #$00                    ; E5F6  no carrier
L_E5F8:
        sec                             ; E5F8  shift a 1 into bit 7: 'a sample has been taken'
        ror     a                       ; E5F9  -> $80 no carrier, $F8 carrier, $C0 override; bit 6 is the carrier flag
        inc     carrierSampleTimer      ; E5FA  the NMI zeroes this on every byte that arrives
        bpl     L_E604                  ; E5FD  positive means characters have arrived recently - keep bit 7 set
        .assert * <= $E5FF, error, "serviceCarrierAndSuspendRequest overran $E5FE"

; ----------------------------------------------------------------------
; rearmCarrierTimer - the 'no character has arrived recently' tail of the carrier sampler: park the
; sample in carrierSampleTimer (it is negative, so the timer stays negative until the NMI zeroes it on
; the next good character) and strip bit 7 from the value that goes into linkStatusSample, marking the
; link as quiet.
; In:  A = fresh carrier sample
; Out: carrierSampleTimer = A, A = sample with bit 7 cleared
; Called from: fall-through from $E5FD (the solo-trainer build also calls it directly)
; ----------------------------------------------------------------------
rearmCarrierTimer:
        sta     carrierSampleTimer      ; E5FF  quiet line: park the sample here so the timer stays negative
        and     #$7F                    ; E602  and clear the 'data flowing' bit
L_E604:
        sta     linkStatusSample        ; E604  publish the sample for pollCarrierState to debounce

; ----------------------------------------------------------------------
; startNextTxChar - kick the transmitter. With a 6551 there is nothing to shift by hand, so all that
; is left of this routine is the policy the layers above depend on: never transmit unless something
; is owed, and outside terminal mode never transmit without carrier. When those hold it arms the
; ACIA's transmit interrupt, and because TDRE is already set the cartridge raises NMI immediately;
; from there the NMI feeds the transmitter straight from the ring until it runs dry and disarms
; itself. The per-frame pacing the software UART needed is gone - txPaceCounter $E044 is still
; written by beginByteSyncPhase but nothing reads it any more.
; Reached both from queueByteForTransmit, so a byte queued mid-frame goes out at once, and by
; fall-through from the carrier sampler once a frame, which is what restarts a burst that stalled.
; The first test is now healStuckTransmitter rather than a plain read of txCharActive. It answers the
; same question - 'is a byte of ours still in the chip' - but it also repairs the one answer that can
; never change on its own: txCharActive set while the transmit interrupt is disarmed. Nothing would
; ever clear that, and this routine would refuse to run for ever, which is exactly how one machine's
; transmitter died at 2400 baud. Losing the case 'transmit interrupt armed, flag clear' costs nothing
; because the command-shadow test at $E61B refuses that case anyway.
; In:  txCharActive, aciaCommandShadow, nmiSuspendAck, uartPendingCount, connectionPhase,
;     linkStatus/linkStatusSample
; Out: the ACIA command register with the transmit interrupt armed, or nothing at all; txCharActive
;     healed to 0 when the transmit interrupt was found disarmed
; Called from: queueAndKickTx $E719, and by fall-through from the carrier sampler at $E604
; ----------------------------------------------------------------------
startNextTxChar:
        jsr     healStuckTransmitter    ; E607  'is a byte already sitting in the ACIA' - but healing the one answer that can never change on its own first. Was 'lda txCharActive' ($AD $BB $E5); the helper returns the same A this load did, except that when the transmit interrupt is disarmed it forces txCharActive to 0 on the way past. This is the once-per-frame heal: the $E00C service falls in here from $E604 on every raster IRQ the link is up, and queueByteForTransmit reaches it again on every byte queued
        bne     aciaDetectDone          ; E60A  the transmit interrupt will chain the rest
        lda     uartTxCount             ; E60C  is there really something in the ring?
        bne     L_E616                  ; E60F  yes - decide whether it may go out
        sta     uartPendingCount        ; E611  the ring is dry: A = 0 resynchronises the count everything above waits on
        beq     aciaDetectDone          ; E614  always taken - Z is still set from the LDA
L_E616:
        lda     aciaCommandShadow       ; E616  what did we last tell the ACIA?
        and     #$05                    ; E619  bit 2 = transmit interrupt armed, bit 0 = DTR
        cmp     #$01                    ; E61B  arm only with DTR up and the transmitter idle, so a hang-up or C=+H stays hung up
        bne     aciaDetectDone          ; E61D  otherwise leave the command register alone
        lda     nmiSuspendAck           ; E61F  suspended for a disk load?
        bmi     aciaDetectDone          ; E622  then nothing may interrupt until the drive is done
        lda     #$01                    ; E624  phase 1 = modem command / terminal mode
        cmp     connectionPhase         ; E626  which phase are we in?
        beq     L_E634                  ; E629  terminal-mode characters go out without carrier
        lda     linkStatusSample        ; E62B  the live carrier sample
        and     linkStatus              ; E62E  AND the debounced status
        asl     a                       ; E631  bit 6 (carrier) into bit 7
        bpl     aciaDetectDone          ; E632  no carrier - do not transmit into a dead line
L_E634:
        jsr     popUartTxRing           ; E634  the ring was checked above, so this cannot fail
        bcs     aciaDetectDone          ; E637  ...but if it somehow does, do not transmit a stray $00
        jsr     aciaPutData             ; E639  prime the transmitter by hand rather than relying on the ACIA to interrupt the moment TDRE is enabled while it is already true - not every 6551 does
        inc     txCharActive            ; E63C  we are now waiting for this byte's TDRE
        lda     #$05                    ; E63F  DTR on, RTS low, receiver IRQ on, transmit IRQ on
        jmp     aciaSetCommand          ; E641  the rest of the burst comes out of the NMI

; ----------------------------------------------------------------------
; aciaDetect - settle, once and once only, which page the cartridge answers on: $DE00 first, then the
; $DF00 strap option. loadBaudParameters calls this before every write to the chip, which is every
; link open AND every baud hot key, so the one thing it must not do is take the decision again on a
; live link. aciaPageLatch $E5BF is what stops it: aciaSetPage sets the latch with every aim, this
; routine returns immediately while it is non-zero, and only selectFramedLinkVectors $E3F3 - reached
; from initCommModule $E2BA, once, right after the overlay is loaded - puts it back to zero. So the
; hardware is probed on a cold start and never again: a rate change re-programs the ACIA without
; re-deciding where it is, and a foreign cartridge strapped to $DE00 sees at most one probe write per
; cold start instead of one per open and one per hot key.
; What changed with adopt mode: the $DF00 fallback now runs the same routine as the $DE00 attempt
; instead of only aiming at it. It is still taken on trust - the answer is ignored - but the page has
; to be READ before it is written if the user's own line settings are ever to be recovered from it,
; and doing that for both straps is the whole of what the extra call costs. When there is no
; cartridge on either page the accessors are left aimed at $DF00, where every read is open bus and
; every write goes nowhere: harmless, and the only page of the two that is guaranteed not to be some
; other cartridge's registers.
; In:  none
; Out: the six accessor operands and aciaPageLatch point at the chosen page, adoptEntryControl holds
;     that page's own control setting; A and Y clobbered, X unchanged (loadBaudParameters holds the
;     baud index in it)
; Called from: loadBaudParameters $E353
; ----------------------------------------------------------------------
aciaDetect:
        lda     aciaPageLatch           ; E644  has the page question already been settled?
        bne     aciaDetectDone          ; E647  yes - the accessors already point there, so touch nothing
        lda     #$DE                    ; E649  the SwiftLink default
        jsr     aciaProbeAdopted        ; E64B  aim there, remember its settings, ask for two patterns back
        beq     aciaDetectDone          ; E64E  both came back - the latch now says $DE and we are done
        lda     #$DF                    ; E650  nothing at $DE00: fall back to the strap option, whose
        bne     aciaProbeAdopted        ; E652  answer we ignore. Always taken ($DF is not zero); the
                                        ;       routine's own RTS returns for us
aciaDetectDone:
        rts                             ; E654  the shared 'page already settled' exit
        .assert * = $E655, error, "aciaDetect must end at $E654"

; ----------------------------------------------------------------------
; aciaProbeAdopted - aim the six accessors at page A, remember what that page's control register was
; already set to, and then ask the page to prove it is a 6551, twice.
;
; The order matters and is the whole point of the routine. THE DRIVER NO LONGER IMPOSES A LINE RATE:
; the last entry of baudEntryTable is 'adopt', whose control byte is whatever the user's hardware was
; configured for, and this is the only moment at which that value can still be read. The probe below
; overwrites the control register - it has to, because writing and reading it back is the only
; side-effect-free way to tell a 6551 from open bus - so the read has to come first, once, before
; anything else in the module touches the chip.
;
; What is kept and what is corrected:
;   * bits 3-0 (the rate), bit 7 (stop bits) and bit 4 (the receiver clock source) are the user's and
;     are copied through untouched;
;   * bits 6-5 (word length) are forced to %00 = eight data bits by the AND #$9F at $E665 below, the
;     one instruction in the module that masks a control byte - the seven fixed entries of
;     baudEntryTable are constants that already have those bits clear. The frame layer above is NOT
;     7-bit clean: the byte-sync handshake tests the peer's answer for exactly $FF (INX / BNE
;     restartByteSync at $E852), $55 length codes and raw sequence bytes go out unaltered, and
;     computePacketChecksum $E937 is a full eight-bit rotate-and-add. So eight data bits are
;     mandatory and everything else is free;
;   * a value that cannot work is replaced outright. (control & $1F) < $11 means either bit 4 = 0,
;     which selects an external receiver clock that a SwiftLink does not have, or rate bits %0000,
;     which selects an external transmit clock, or both - which is exactly what a cold, unconfigured
;     6551 reads as, because reset clears the whole register to $00. The replacement is $15: 300 baud,
;     8N1, internal generator. That is the disk default the stock driver came up in, the slowest and
;     therefore most forgiving rate in the table, one of only two rates a stock user-port peer can
;     produce - and the game generates about 30 bytes a second, which is exactly what 300 baud carries.
;
; The two probe patterns are unchanged, and both are still legal 6551 control bytes, so a real chip is
; never left in a strange state by being probed. An empty $DE00 is open bus - it returns whatever the
; VIC last put there, which is screen data this driver does not control - so one pattern can be
; matched by luck and the price of that luck is all six accessors aimed at nothing. Requiring $1E and
; then $15 makes that far less likely. aciaSetControl falls through into aciaGetControl, so each
; 'write it and read it back' is one JSR.
; In:  A = $DE or $DF, the page to try
; Out: the accessors and aciaPageLatch aimed at that page whatever the answer, adoptEntryControl set
;     from that page's control register; Z = 1 if it answered both patterns; A and Y clobbered, X
;     unchanged
; Called from: aciaDetect $E64B and $E652
; ----------------------------------------------------------------------
PROBE_PATTERN_1 = $1E                   ; 8N1, internal baud rate generator, 19200 on a SwiftLink
PROBE_PATTERN_2 = $15                   ; 8N1, internal baud rate generator, 300 - the disk default
ADOPT_FALLBACK  = $15                   ; and the same byte again when the chip holds nothing usable

aciaProbeAdopted:
        jsr     aciaSetPage             ; E655  aim all six accessors at the page, and latch it
        jsr     aciaGetControl          ; E658  what the user's hardware has this page set to
        tay                             ; E65B  keep it while it is tested
        and     #$1F                    ; E65C  bit 4 = receiver clock source, bits 3-0 = the rate
        cmp     #$11                    ; E65E  below that one of them says 'external clock'
        tya                             ; E660  the setting itself again
        bcs     adoptSettingUsable      ; E661  it can work - keep the rate, stop bits and clock source
        lda     #ADOPT_FALLBACK         ; E663  it cannot: a cold 6551 reads $00 here
adoptSettingUsable:
        and     #$9F                    ; E665  bits 6-5 = %00 = eight data bits, and nothing else in
                                        ;       the byte is touched. This is the one hard constraint:
                                        ;       the frame layer sends $55 length codes, raw sequence
                                        ;       bytes and rotate-and-add checksums, so it is not
                                        ;       7-bit clean and a 7-, 6- or 5-bit port setting would
                                        ;       corrupt every frame rather than merely slow it down.
                                        ;       Rate, stop bits and clock source stay as configured
        sta     adoptEntryControl       ; E667  park it in baudEntryTable's adopt entry
        lda     #PROBE_PATTERN_1        ; E66A  the first pattern
        jsr     aciaSetControl          ; E66C  write it and read the register straight back
        eor     #PROBE_PATTERN_1        ; E66F  Z = 1 only if the register really held it
        bne     aciaProbeDone           ; E671  it did not - this page is not a 6551
        lda     #PROBE_PATTERN_2        ; E673  a clearly different second pattern
        jsr     aciaSetControl          ; E675  write it and read it back as well
        eor     #PROBE_PATTERN_2        ; E678  Z = 1 only if both patterns came back
aciaProbeDone:
        rts                             ; E67A  return
        .assert * = $E67B, error, "aciaProbeAdopted must end at $E67A"




; ----------------------------------------------------------------------
; The 'not mine' exit of the NMI handler. It restores the caller's $01 first, because the handler
; forced the I/O area back in on entry, then A and X, and chains through nmiChainVector $E031/$E032 -
; the path the RESTORE key takes. Y was never touched. Unlike the stock exit there is no CIA2_ICR to
; put back: this handler never masks CIA2, because setCiaNmiMask has already silenced it for good.
; ----------------------------------------------------------------------
nmiChainExit:
        pla                             ; E67B  the caller's CPU port byte
        sta     CPU_PORT                ; E67C  put the memory map back exactly as we found it
        pla                             ; E67E  restore X
        tax                             ; E67F  the saved X
        pla                             ; E680  and A
        jmp     (nmiChainVector)        ; E681  chain to the game's NMI handler ($1298, a bare RTI)
        .assert * <= $E685, error, "the NMI chain exit overran $E684"
        .res    $E685 - *, $EA          ; E684  the last spare byte before the handler's fixed address


; ----------------------------------------------------------------------
; commNmiHandler - the module's NMI handler, installed at $FFFA/$FFFB by installCommNmiVector. The
; expansion port's /NMI is wired into the same line as CIA2 and the RESTORE key, so the handler still
; has to decide whether the interrupt was its own and chain when it was not.
; Two things it must do that the stock handler did not. It banks the I/O area in itself: readFilmByte
; $5874, writeFilmByte $5893 and copyPageUnderIo $58B2 run with $01 = $34 to reach the film buffer
; under $D000-$DFFF, and SEI does not mask an NMI, so without this the ACIA's registers would be
; ordinary RAM for the roughly 2600 cycles copyPageUnderIo holds that state - about ten character
; times at 38400 baud. And it services RDRF and TDRE directly rather than trusting status bit 7,
; which CMD document as unreliable at 9600 bps and above; bit 7 is only consulted at the very end, to
; tell a DCD or DSR change (ours, nothing more to do) from somebody else's interrupt.
; The receive path owes the layers above three things besides the byte: zero the $EB00 stall counter
; so noteIdlePoll does not tear the connection down, zero carrierSampleTimer so linkStatus keeps its
; 'data is flowing' bit, and push framing, parity and overrun errors through reportLinkError so the
; link state machine restarts byte sync. The transmit path owes exactly one: uartPendingCount must
; come back down to zero, because everything above waits on it.
; A and X are saved, Y is never touched, and both exits restore $01.
;
; ---- THE RE-ENTRANCY GUARD AND THE DRAIN LOOP: why 38400 used to take the machine down ----
;
; The 6502 does not mask NMI. Reading the 6551's status register - which this handler must do, and
; does at nmiDrain, because it is the only way to find out what the chip wants - is also the
; acknowledge: it releases /IRQ. Everything the handler does after that point, 100 to 240 cycles of
; it, is done with the chip free to raise a fresh edge, and a fresh edge re-enters the handler at the
; next instruction boundary. Each re-entry costs six bytes of stack, or eight when it lands inside one
; of the handler's own JSRs.
; At 38400 baud with traffic both ways there are two interrupt-raising events per character time -
; one RDRF, one TDRE - so an edge arrives about every 133 cycles, while one pass of this handler costs
; 175 to 266. Service time exceeded inter-arrival time, so the nesting was not a queue but a stack,
; and it grew without bound: wedgeDiagnosis.md decoded THIRTY-FOUR complete nested frames off a live
; page 1 and timed three descents from a healthy stack to an exhausted one at 8.4, 11.5 and 12.5 ms.
; Once the stack pointer wrapped, every RTI popped a PC, a flag byte and a saved $01 belonging to some
; other frame, and the machine left the handler with a corrupt program counter, a corrupt P or a
; corrupt memory map. That is the whole of the '38400 wedges' fault: four boots, four wedges, one
; ending in a CPU JAM at $0007.
;
; The fix is two co-operating pieces, and neither works without the other.
;
; 1. THE GUARD, nmiHandlerState $E5BD. Zero means no pass is running. The first thing a pass does is
;    test it, and a second NMI that finds it non-zero does not run the handler body at all: it adds
;    one to the byte - a note saying 'there is more to do' - and returns. That costs six bytes of
;    stack and about fifty cycles, and it CANNOT nest again, because a nested pass never reads the
;    status register. /IRQ therefore stays asserted, the shared /NMI line stays low, and an
;    edge-triggered NMI cannot fire again until the running pass acknowledges. The depth is bounded at
;    one nested frame, whatever the line rate, and no arithmetic about cycles per character has to
;    hold for that to be true. The single qualification is the two-instruction window at the test
;    itself, which the comment at $E68F sets out: our own chip cannot use it, and if a foreign NMI
;    does, one extra pass gets through and the byte still ends at zero.
;    Measured at 38400 with two machines in the packet phase (testGuard.py): 12 of 65 and 6 of 64
;    handler entries in five seconds were nested. On the old build every one of those would have been
;    a full re-entrant pass, which is how the runaway started.
; 2. THE DRAIN LOOP. A note would be worthless if nobody read it, and refusing a nested pass would
;    otherwise lose exactly the interrupt the source used to warn about - a dropped TDRE stalls the
;    transmitter for good, because nothing else will ever raise one. So the running pass loops: it
;    services what the status register shows, and at nmiIdle it decrements the guard and goes round
;    again whenever the decrement leaves a note behind. The transmit path loops for the same reason
;    from the other end - after feeding the chip it goes back to nmiDrain rather than returning, so a
;    byte that arrived while it was working is taken now instead of on the next interrupt.
;    The order at nmiIdle is what closes the last window: the DECREMENT COMES FIRST, so from that
;    instruction onwards the guard reads zero and any NMI arriving during the exit sequence is a full
;    pass that drains everything. There is no instant at which work can be left behind with the
;    interrupt swallowed.
;
; What this does NOT claim: the C64 still cannot afford two interrupts per 266 cycles, so 38400 with
; both directions bursting still costs more CPU than there is. What changes is the failure mode -
; the receive ring overflows and the ARQ layer resynchronises, exactly as it already does at 9600 and
; 19200, instead of the stack eating page 1. Degradation instead of death is the whole of the claim.
;
; One deliberate loss: a foreign NMI (RESTORE) that arrives while a pass is running is counted as a
; note and swallowed rather than chained. The window is a couple of hundred cycles and both possible
; chain targets in this game are a bare RTI ($E010 on disk, $1298 once initInterruptVectors has run),
; so nothing observable is given up.
; Also by design, and unchanged: a pass that serviced anything usually leaves through the chain rather
; than the RTI, because the drain loop's last status read - the one that found nothing left - has
; cleared bit 7. Both exits restore $01, X and A identically and both chain targets are that same bare
; RTI, so the two paths differ by two jumps and nothing else.
; In:  the ACIA status/data registers, txCharActive, nmiHandlerState, the UART rings
; Out: the UART receive ring, uartPendingCount, txCharActive, aciaStatusSave, carrierSampleTimer,
;     $EB00, nmiHandlerState; exits with RTI or chains through nmiChainVector
; Called from: the CPU's NMI vector once installCommNmiVector has run
; ----------------------------------------------------------------------
commNmiHandler:
        pha                             ; E685  save A
        txa                             ; E686  X goes on the stack too
        pha                             ; E687  and X
        lda     CPU_PORT                ; E688  the game may have banked the I/O area out to reach the film buffer
        pha                             ; E68A  remember how we found it
        lda     #$35                    ; E68B  I/O visible, ROMs out - the map the game normally runs in
        sta     CPU_PORT                ; E68D  the cartridge is only addressable from here on
        lda     nmiHandlerState         ; E68F  is a pass of ours already running?
        bne     nmiLeaveNote            ; E692  yes - leave it a note and get out
        inc     nmiHandlerState         ; E694  no: 0 -> 1, this pass owns the ACIA from here
                                        ;       Read-then-increment, not increment-then-read, and the
                                        ;       difference matters. If an NMI could land between these
                                        ;       two instructions the other order would have both
                                        ;       passes read 2 and both leave, with the byte stuck at 2
                                        ;       and the driver deaf for good. This way the worst that
                                        ;       window can do is let one extra pass through - the same
                                        ;       single nesting the guard already tolerates - and the
                                        ;       byte still ends at 0. (It cannot normally happen at
                                        ;       all: our own chip has not been acknowledged yet, so
                                        ;       /IRQ is still low and no new edge exists to take. Only
                                        ;       a foreign NMI arriving first opens the window.)
nmiDrain:
        jsr     aciaGetStatus           ; E697  reading the status register is also the acknowledge
        and     #$08                    ; E69A  RDRF: a character has arrived
        beq     nmiTx                   ; E69C  nothing received - look at the transmitter
        lda     aciaStatusSave          ; E69E  the status that came with this character
        and     #$07                    ; E6A1  overrun, framing error, parity error
        bne     nmiRxBad                ; E6A3  any of them and the character is worthless
        lda     #$00                    ; E6A5  a good character means the link is alive:
        sta     D_EB00                  ; E6A7  clear the protocol stall counter
        sta     carrierSampleTimer      ; E6AA  and tell the carrier sampler that data is flowing
        jsr     aciaGetData             ; E6AD  now take the character out, which also clears RDRF
        jsr     pushUartRxRing          ; E6B0  hand it to the protocol; C=1 means the 20-byte ring is full
        bcc     nmiTx                   ; E6B3  stored - on to the transmitter
        bcs     nmiRxError              ; E6B5  the ring was full: report it, the character is already gone
nmiRxBad:
        jsr     aciaGetData             ; E6B7  drain the worthless character anyway, or RDRF never clears
nmiRxError:
        jsr     reportLinkError         ; E6BA  bad character or receive overflow - the link state machine will resync

; ----------------------------------------------------------------------
; Transmit half. TDRE is set whenever the holding register is free, so it only means 'our interrupt'
; when we asked for it; the command shadow is the only way to know that. Shifting the shadow's bit 2
; (transmit interrupt armed) up into bit 4 lets both questions be asked with one AND against the
; status byte, whose bit 4 is TDRE.
; startNextTxChar puts the first byte of a burst in by hand, so every interrupt from here on finds
; txCharActive set and one byte leaves the ring per pass of the loop. That byte is counted off
; uartPendingCount one character time early, because TDRE comes back when the byte moves into the
; shift register rather than when it reaches the wire - harmless, since nothing above measures real
; wire time.
; ----------------------------------------------------------------------
nmiTx:
        lda     aciaCommandShadow       ; E6BD  did we arm the transmit interrupt?
        asl     a                       ; E6C0  command bit 2 = 'armed'...
        asl     a                       ; E6C1  ...lined up with status bit 4 = TDRE
        and     aciaStatusSave          ; E6C2  both at once: armed AND the register is free
        and     #$10                    ; E6C5  nothing else in that byte is ours to read
        beq     nmiIdle                 ; E6C7  not this time
        lda     txCharActive            ; E6C9  was a byte of ours in there?
        beq     nmiTxFeed               ; E6CC  no - nothing to count off, just feed the transmitter
        dec     uartPendingCount        ; E6CE  that byte is on its way out
        dec     txCharActive            ; E6D1  1 -> 0: nothing of ours is in flight. It is only ever 0
                                        ;       or 1 - both INCs below and in startNextTxChar are
                                        ;       guarded by a test that it was 0, and the guard above
                                        ;       is what makes that pair atomic
nmiTxFeed:
        jsr     popUartTxRing           ; E6D4  the next byte to go
        bcs     nmiTxDry                ; E6D7  the ring is empty - stop asking
        jsr     aciaPutData             ; E6D9  hand it to the transmitter
        inc     txCharActive            ; E6DC  and remember that we are waiting for its TDRE
        bne     nmiDrain                ; E6DF  always taken: drain, do not return - look at the chip
                                        ;       again in case something arrived while we were working
nmiTxDry:
        jsr     aciaCommandReceiveOnly  ; E6E1  nothing left: drop back to receive-only, RTS still low,
                                        ;       so the transmit interrupt stops asking. txCharActive is
                                        ;       already 0 here - the half above either cleared it or
                                        ;       found it clear, and with the guard in place nothing
                                        ;       can set it in between - so the old aciaSetCommandIdle
                                        ;       had nothing left to do and its bytes went to dropDtrLine
nmiIdle:
        dec     nmiHandlerState         ; E6E4  1 -> 0, and from this instruction on a fresh NMI is a
                                        ;       full pass again, which is what makes the exit below
                                        ;       safe. 2 -> 1 means a nested pass left a note
        bne     nmiDrain                ; E6E7  it did - service what it could not
        lda     aciaStatusSave          ; E6E9  nothing left at all: was this interrupt even ours?
        bpl     nmiChainExit            ; E6EC  bit 7 clear - not ours, chain to the game's handler
        bmi     nmiExit                 ; E6EE  always taken: a DCD or DSR change of ours, nothing to do
nmiLeaveNote:
        inc     nmiHandlerState         ; E6F0  1 -> 2: tell the running pass there is more to do. Note
                                        ;       what is NOT here - no status read, so /IRQ stays
                                        ;       asserted, the shared /NMI line stays low, and no
                                        ;       further edge can be raised until the running pass
                                        ;       acknowledges. That is what bounds the nesting
nmiExit:
        pla                             ; E6F3  the caller's CPU port byte
        sta     CPU_PORT                ; E6F4  put the memory map back exactly as we found it
        pla                             ; E6F6  restore X
        tax                             ; E6F7  the saved X
        pla                             ; E6F8  and A
        rti                             ; E6F9  return from the NMI

; ----------------------------------------------------------------------
; aciaSetPage - record which page the cartridge answers on by patching the operand high byte of all
; six accessors at $E551-$E56E, and publish that page in aciaPageLatch $E5BF. Nothing else in the
; module names an address in $DE00 or $DF00, so those six bytes are the whole of the driver's
; knowledge of where the hardware is; the latch is a seventh copy of it that can be read back with a
; single LDA, which is what lets aciaDetect answer 'do we already know?' in five bytes.
; The latch is therefore always in step with the accessors: non-zero means the page question has been
; settled and no later call may probe again, and only a cold start - selectFramedLinkVectors $E3F3,
; which initCommModule calls once - puts it back to zero. (This space was nmiStartNextChar, the timer
; B path of the bit-banged transmitter; the three bytes the latch store needed were its padding.)
; In:  A = $DE or $DF
; Out: the six accessor operands and aciaPageLatch patched; A, X, Y and the flags as they came in
; Called from: aciaProbe $E6F2 and the strap fallback in aciaDetect $E652
; ----------------------------------------------------------------------
aciaSetPage:
        sta     aciaCmdWrite+2          ; E6FA  command register writes
        sta     aciaStatusRead+2        ; E6FD  status register reads
        sta     aciaDataRead+2          ; E700  received bytes
        sta     aciaDataWrite+2         ; E703  transmitted bytes
        sta     aciaCtrlWrite+2         ; E706  control register writes
        sta     aciaCtrlRead+2          ; E709  control register reads (the probe)
        sta     aciaPageLatch           ; E70C  and the readable copy aciaDetect tests
        rts                             ; E70F  return
        .assert * = $E710, error, "aciaSetPage must end at $E70F"


; ----------------------------------------------------------------------
; baudEntryTable - eight three-byte entries {hot-key code, ACIA control byte, spare}, indexed by
; baudIndex. The three-byte stride and the first two entries are inherited from the stock
; baudParameterTable so that every existing index keeps its meaning: switchTo1200Baud $E7B0 loads 3
; when it sniffs a CONNECT 1200 result code, and 0 is still 300 baud. Those two rates are also the
; only ones a stock user-port machine can talk at, so they are where a SwiftLink player and a
; stock-driver player meet.
; The control byte of a fixed-rate entry is $10 (8 data bits, one stop bit, internal baud rate
; generator) OR the rate bits. The rates are the SwiftLink ones, i.e. twice what a 6551 data sheet
; says for the same bits, because the cartridge fits a 3.6864 MHz crystal. A SwiftLink rebuilt with
; the standard 1.8432 MHz part will run every one of these at half the speed shown.
;
; TWO CHANGES HERE, AND THEY ARE THE POINT OF THIS PASS.
;
; 1. Index 18 has its hot key back. C= + 8 selects 38400 again. It was taken away because 38400 wedged
;    the machine - four boots, four wedges, one ending in a CPU JAM at $0007 - but that was hiding a
;    defect rather than fixing one, and wedgeDiagnosis.md then established what the defect was: the
;    NMI handler could be re-entered without bound, so at a rate whose interrupts arrive faster than
;    one pass of the handler completes, the nested frames ate the whole of page 1 in 8 to 13 ms. The
;    re-entrancy guard in commNmiHandler $E68F fixes that mechanism, so the rate is selectable again.
;    38400 is still the rate at which this driver has the least headroom - see the note at the guard -
;    but it can no longer take the machine down with it, and a user whose port is configured for it
;    must be able to use it.
;
; 2. Index 21 is new: ADOPT. Its control byte is not a constant at all - aciaProbeAdopted $E655 writes
;    it, once per cold start, from whatever the user's own hardware already had in its control
;    register, corrected only where it cannot work (see that routine). Selecting it therefore means
;    'use the port exactly as it is configured' rather than 'impose 8N1 at a rate of the driver's
;    choosing', which is what every other entry means and what the stock driver always did.
;    It is the DISK DEFAULT: baudIndex $E055 ships as $15 = 21, so a link opened without pressing
;    anything runs at the user's own setting. On a cold, unconfigured chip that setting reads $00,
;    which cannot work, so the fallback puts it at 300 baud - which is what the module did before this
;    change, and what the game needs anyway at about 30 bytes a second. Pressing any speed hot key
;    stores that entry's index and leaves adopt mode; C= + U comes back to it.
;    The spare third byte of this entry is the one the table does not need, so the table is 23 bytes.
; ----------------------------------------------------------------------
BAUD_INDEX_ADOPT = 21                   ; the adopt entry's index, i.e. its offset into the table

baudEntryTable:
        .byte   $B3,$15,$00             ; E710  index 0:  C=+3    300 baud
        .byte   $B1,$17,$00             ; E713  index 3:  C=+1   1200 baud
        .byte   $B2,$18,$00             ; E716  index 6:  C=+2   2400 baud
        .byte   $B4,$1A,$00             ; E719  index 9:  C=+4   4800 baud
        .byte   $B9,$1C,$00             ; E71C  index 12: C=+9   9600 baud
        .byte   $B0,$1E,$00             ; E71F  index 15: C=+0  19200 baud
        .byte   $B8,$1F,$00             ; E722  index 18: C=+8  38400 baud - the hot key is back
        .byte   $D5                     ; E725  index 21: C=+U  adopt: use the port as it is set up
adoptEntryControl:
        .byte   $00                     ; E726  written by aciaProbeAdopted $E667 on a cold start; the
                                        ;       stored $00 is never used, because loadBaudParameters
                                        ;       cannot reach this byte without aciaDetect having run
        .assert * = $E727, error, "baudEntryTable must end at $E726"


; ----------------------------------------------------------------------
; queueAndKickTx - the body of queueByteForTransmit, which does not fit in the seven bytes at $E73C
; once it has to start the transmitter as well. A must come back unchanged: the callers test what
; they just sent.
; In:  A = byte to transmit
; Out: the byte in the transmit ring, uartPendingCount +1, the transmitter armed if it was idle
; Called from: queueByteForTransmit $E73C
; ----------------------------------------------------------------------
queueAndKickTx:
        jsr     pushUartTxRing          ; E727  into the 22-byte ring; silently dropped when it is full
        inc     uartPendingCount        ; E72A  count one more character owed to the wire, even on an overflow - startNextTxChar resynchronises the count when the ring runs dry
        pha                             ; E72D  keep the caller's byte
        jsr     startNextTxChar         ; E72E  start the transmitter if it was idle
        pla                             ; E731  give the byte back
        rts                             ; E732  return
        .assert * = $E733, error, "queueAndKickTx must end at $E732 - the block is full"


        .assert * = $E733, error, "commNmiHandler and its helpers must end at $E732"

; ----------------------------------------------------------------------
; popArqTxRingThunk - the fixed entry the frame layer uses to fetch the next payload byte the packet
; layer queued.
; In:  none
; Out: A = byte, C = 0/1 (see popArqTxRing)
; Called from: the frame builder at $E8AE and $E8D4
; ----------------------------------------------------------------------
popArqTxRingThunk:
        jmp     popArqTxRing            ; E733  the frame builder's entry for taking the next queued payload byte


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
pushArqRxRingThunk:
        jmp     pushArqRxRing           ; E736  JMP $E49E - deliver one decoded command byte into the game's inbound ring $E45C


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
popUartRxRingThunk:
        jmp     popUartRxRing           ; E739  JMP $E4D2 - take the next raw byte the software UART has assembled


; ----------------------------------------------------------------------
; queueByteForTransmit - the single door for outbound bytes. The work is in queueAndKickTx, because
; seven bytes are not enough to queue the byte, count it and start the transmitter as well. A is
; preserved: sendNextModemCommandChar tests what it just sent.
; 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)
; ----------------------------------------------------------------------
queueByteForTransmit:
        jmp     queueAndKickTx          ; E73C  push the byte, count it, and start the transmitter if it was idle. A tail jump rather than a JSR/RTS pair: queueAndKickTx already gives A back unchanged, and the four bytes that saves are countLinkError's new home

; ----------------------------------------------------------------------
; countLinkError - the framed link-error hook: count one bad character. The link state machine tests
; linkErrorCount every tick and restarts byte synchronisation when it is non-zero.
; It lived at $E403 until selectFramedLinkVectors had to grow by five bytes to clear aciaPageLatch on
; a cold start. Nothing calls it by address - both entries of linkIoVectorTable's error pair were
; repointed here - and the leading NOP the stock module had at $E403 was only padding.
; In:  none
; Out: linkErrorCount +1
; Called from: reportLinkError $E3BC through vector $E3C9
; ----------------------------------------------------------------------
countLinkError:
        inc     linkErrorCount          ; E73F  count one bad character; the link state machine restarts byte sync when this is non-zero
        rts                             ; E742  return
        .assert * = $E743, error, "countLinkError must end at $E742"


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
receiveByteTrackZeros:
        jsr     popUartRxRingThunk      ; E743  pop one byte; popUartRxRing returns A = 0 and C = 1 when the ring is empty
        ldx     zeroRunLength           ; E746  X = the run of consecutive $00 bytes seen so far
L_E749:
        bcs     trackZerosReturn        ; E749  ring was empty: leave the run length alone and return with C = 1 and X = the old count
        inx                             ; E74B  provisionally extend the zero run by this byte
        eor     #$00                    ; E74C  EOR #$00 purely to set Z from the byte just popped
        beq     L_E752                  ; E74E  the byte was $00 - keep the extended run length
        ldx     #$00                    ; E750  any non-$00 byte breaks the run
L_E752:
        stx     zeroRunLength           ; E752  store the new run length; it is also returned in X
trackZerosReturn:
        rts                             ; E755  return with C still holding the ring-empty flag - none of the instructions above touch it


; ----------------------------------------------------------------------
; 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
; Consume the restart request posted by restartUart ($E58B writes 1) or stopCommNmi ($E5C5 writes
; $FF).
; ----------------------------------------------------------------------
runModemStateMachine:
        ldx     #$00                    ; E756  X = 0, the value used below to clear the request and the connection phase
        lda     uartRestartRequest      ; E758  restart request: 1 = re-evaluate the link, $FF = tear the link down
        beq     checkConnectionPhase    ; E75B  no request pending
        stx     uartRestartRequest      ; E75D  consume the request
        bpl     serviceLinkTick         ; E760  request was 1: just re-evaluate, keep the current connection phase
        stx     connectionPhase         ; E762  request was $FF: force the connection phase back to 0 (link down)
checkConnectionPhase:
        lda     connectionPhase         ; E765  connection phase: 0 idle, 1 modem terminal, 2 byte sync, 3 packet protocol
        bne     serviceLinkTick         ; E768  phase 1-3, or a pending re-evaluation: there is work to do
        rts                             ; E76A  idle with nothing requested - leave the link alone


; ----------------------------------------------------------------------
; A message packet counts as 'in flight' until the UART transmitter has emptied its ring.
; ----------------------------------------------------------------------
serviceLinkTick:
        lda     uartPendingCount        ; E76B  characters the NMI transmitter still owes
        cmp     frameInFlightFlag       ; E76E  compare with $E0A4, the 'a message packet is in flight' flag (set to 1 at $E8A8)
        bcs     checkTerminalMode       ; E771  transmitter not drained yet - the packet has not left the machine

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
clearInFlightFlags:
        ldx     #$00                    ; E773  X = 0
storeInFlightFlags:
        stx     frameInFlightFlag       ; E775  transmitter drained: the packet in flight has physically gone out
        stx     packetInFlightCount     ; E778  and clear the packet-layer send enable $E0A6 - NOT the only write to it: sendPacketFsm sets it at $E265 with 'sta uartPendingCount,x', X = inFlightSlotIndex = 1

; ----------------------------------------------------------------------
; Choose between the modem dialogue (terminal mode) and the game link.
; ----------------------------------------------------------------------
checkTerminalMode:
        lda     #$01                    ; E77B  1 = the modem command / terminal phase
        cmp     connectionPhase         ; E77D  compare with the current connection phase
        beq     serviceModemInput       ; E780  already in terminal mode: go and service the characters flowing to and from the modem
        ldx     modemReplyTimer         ; E782  modem answer timer / 'an AT command is in progress' byte ($FF while a command string is queued)
        bne     enterTerminalMode       ; E785  non-zero: a modem dialogue is under way, so make sure we are in terminal mode
        bit     linkStatus              ; E787  test the debounced carrier byte; bit 6 (V) = carrier detect, i.e. CIA2 PB4 read low
testCarrierPresent:
        bvs     runLinkStateMachine     ; E78A  carrier is already up - skip the modem dialogue entirely and run the link state machine
drainHostOutRing:
        jsr     popHostOutRing          ; E78C  no carrier: throw away everything the game had queued for the opponent
        bcc     drainHostOutRing        ; E78F  loop until the 16-byte host-output ring $E091 is empty
enterTerminalMode:
        jsr     flushUartTxRing         ; E791  drop anything still queued for the UART transmitter
        lda     #$01                    ; E794  1 = modem command / terminal mode
        sta     connectionPhase         ; E796  enter connection phase 1

; ----------------------------------------------------------------------
; Terminal mode, modem to game: forward what the modem says to the chat line and watch for 'CONNECT
; 1200'.
; ----------------------------------------------------------------------
serviceModemInput:
        jsr     popUartRxRing           ; E799  take the next character the modem sent back
        bcs     tickGuardDelay          ; E79C  nothing arrived this tick
        bit     modemOptionFlags        ; E79E  $E048 modem option flags ($CC as shipped): bit 7 (N) = a Hayes modem is attached, bit 6 (V) = auto-detect the connect speed
        bpl     tickGuardDelay          ; E7A1  no modem configured - discard whatever arrived
        cmp     #$0D                    ; E7A3  CR terminates a verbose result line such as 'CONNECT 1200'
        bne     rememberModemChar       ; E7A5  an ordinary character: just remember and forward it
checkConnectSpeed:
        bvc     substituteSpaceForCr    ; E7A7  V = bit 6 of $E048: auto-baud disabled, so do not sniff the result code
        lda     lastModemChar           ; E7A9  the character that preceded this CR
        cmp     #$30                    ; E7AC  '0' - the last digit of 'CONNECT 1200'; plain 'CONNECT' (300 baud) ends in 'T' and is ignored
        bne     substituteSpaceForCr    ; E7AE  not a 1200 baud connect message
        ldx     #$03                    ; E7B0  index 3 = the second three-byte entry of baudEntryTable $E70F = 1200 baud (ACIA control $17). The SwiftLink
                                        ; table deliberately keeps the stock three-byte stride, so this caller still means 1200 baud without being touched
switchTo1200Baud:
        jsr     loadBaudParameters      ; E7B2  write control $17 to the 6551 and mirror it in bitPeriodLo $E056. baudIndex $E055 is NOT changed: a later re-open with the link down reloads it and would go back to 300, exactly as the stock driver behaved
substituteSpaceForCr:
        lda     #$20                    ; E7B5  replace the CR with a space - the chat line cannot print a CR - and reset the last-character memory
rememberModemChar:
        sta     lastModemChar           ; E7B7  remember this character for the next auto-baud test
        tax                             ; E7BA  test the character
        beq     tickGuardDelay          ; E7BB  never forward a NUL to the game
        jsr     pushHostInRing          ; E7BD  hand the character to the game through the 8-byte host-input ring $E086, which jump-table entry $E006 pops

; ----------------------------------------------------------------------
; Guard-time delays are held in $EB01 as negative tick counts and block both transmission and the
; answer timer while they run.
; ----------------------------------------------------------------------
tickGuardDelay:
        ldx     modemDelayCounter       ; E7C0  guard-time delay counter ($A0 = -96 and $C0 = -64 come straight out of the AT strings)
        bpl     sendNextModemCommandChar; E7C3  no delay pending (counter >= 0): send the next byte of the modem command string
        inx                             ; E7C5  count the guard time up towards zero
storeGuardDelay:
        stx     modemDelayCounter       ; E7C6  store it; this is also where a $80-$FF byte taken out of an AT string is parked as a fresh delay
modemTickReturn:
        rts                             ; E7C9  nothing more to do this tick


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
sendNextModemCommandChar:
        jsr     popHostOutRing          ; E7CA  next byte queued for the modem: AT-string bytes, or the player's chat text in terminal mode
        bcs     keepReplyTimer          ; E7CD  queue empty - nothing to send, just age the answer timer
        tax                             ; E7CF  keep a copy of the byte in X
D_E7D0:
        bmi     storeGuardDelay         ; E7D0  bit 7 set: this is a guard-time delay, not a character - park it in $EB01 and stop for this tick
        jsr     queueByteForTransmit    ; E7D2  hand the character to the UART transmitter (A survives the call)
        ldx     #$79                    ; E7D5  $79 = 121 frames, roughly two seconds, to wait for the modem's answer
        bit     modemReplyTimer         ; E7D7  $E03F with bit 7 set means 'idle, no answer expected' ($FF from queueModemCommandString, $8D from the C=+RETURN hot key)
        bpl     tickModemReplyTimer     ; E7DA  a countdown is already running - just tick it, do not re-arm
        eor     #$0D                    ; E7DC  did we just transmit the CR that terminates an AT command?
        beq     tickModemReplyTimer     ; E7DE  yes - arm the 121-frame answer timeout
keepReplyTimer:
        ldx     modemReplyTimer         ; E7E0  nothing was sent: carry on with the timer value already stored
tickModemReplyTimer:
        bit     linkStatus              ; E7E3  bit 6 (V) of the debounced carrier byte = carrier detect
        bvs     countReplyTimerDown     ; E7E6  carrier present: age the real timer value in X
        ldx     #$02                    ; E7E8  no carrier yet: hold the timer at 1 (2 minus the DEX below) so that it fires on the first tick after carrier appears
countReplyTimerDown:
        dex                             ; E7EA  tick the answer timer down
        bmi     modemTickReturn         ; E7EB  timer was 0 or negative (idle) - nothing to do
        stx     modemReplyTimer         ; E7ED  store the new value
L_E7F0:
        bne     modemTickReturn         ; E7F0  still counting
        lda     #$0D                    ; E7F2  timer reached zero: fabricate the CR the modem never sent
L_E7F4:
        jsr     pushHostInRing          ; E7F4  push it into the host-input ring so the game's chat line unblocks, then fall into the link state machine below

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
runLinkStateMachine:
        lda     connectionPhase         ; E7F7  current connection phase
        ldx     linkErrorCount          ; E7FA  link error counter, bumped from the NMI receiver by countLinkError on a bad stop bit or a receive overflow
        bne     beginByteSyncPhase      ; E7FD  any error since the last tick: throw the connection away and re-synchronise
dispatchCommState:
        cmp     #$02                    ; E7FF  phase 2 = a byte sync is already in progress
        beq     continueByteSync        ; E801  continue the sync where it left off
        bcs     runPacketPhase          ; E803  phase 3 or above: run the reliable packet protocol

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
beginByteSyncPhase:
        lda     #$02                    ; E805  2 = the byte-sync phase
        sta     connectionPhase         ; E807  enter it
drainRxRingLoop:
        jsr     popUartRxRing           ; E80A  throw away every byte already sitting in the UART receive ring
        bcc     drainRxRingLoop         ; E80D  loop until it is empty (C = 1)
restartByteSync:
        jsr     flushUartTxRing         ; E80F  drop anything still queued for transmission; this is also the 'start the sync over' entry
        lda     #$00                    ; E812  A = 0: clears the sync state and doubles as the first sync byte
        sta     linkErrorCount          ; E814  forget any link errors
        sta     rxPayloadRemaining      ; E817  no packet is half received
        sta     packetProtocolState     ; E81A  syncRoundCounter ($EAB7, the first byte of the packet-protocol state block) = 0 = 'waiting for the peer's $00'
queueSyncByte:
        jsr     queueByteForTransmit    ; E81D  queue the sync byte held in A ($00 or $FF) for transmission
        lda     #$02                    ; E820  bit 1 of the transmit pacing counter...
        ora     txPaceCounter           ; E822  ...is forced on...
        sta     txPaceCounter           ; E825  ...so at least two NMI transmit slots separate the sync bytes
continueByteSync:
        lda     packetProtocolState     ; E828  sync round counter
        bne     awaitPeerSyncFf         ; E82B  non-zero: we are past the $00 round and are waiting for the peer's $FF
        lda     uartPendingCount        ; E82D  characters still queued for the transmitter
        beq     queueSyncByte           ; E830  transmitter idle: send another $00 (A is 0 here)
awaitPeerSyncZero:
        lda     zeroRunLength           ; E832  run of consecutive $00 bytes received from the peer
        bne     answerSyncWithFf        ; E835  the peer's $00 has already been seen
        jsr     receiveByteTrackZeros   ; E837  otherwise take one more byte off the line and update the zero run
L_E83A:
        txa                             ; E83A  X = the new run length
        beq     linkTickReturn          ; E83B  nothing from the peer yet - try again next tick
answerSyncWithFf:
        inc     packetProtocolState     ; E83D  the peer answered: move on to round 1
        lda     #$FF                    ; E840  answer with $FF...
        bne     queueSyncByte           ; E842  ...always taken, queue it
linkTickReturn:
        rts                             ; E844  shared 'nothing more to do this tick' exit for the whole link service

awaitPeerSyncFf:
        cmp     #$0A                    ; E845  ten rounds without the peer's $FF?
        bcs     restartByteSync         ; E847  give up and start the sync from scratch
        inc     packetProtocolState     ; E849  count this round
        jsr     receiveByteTrackZeros   ; E84C  look for the peer's answer
        tax                             ; E84F  A = 0 means either a $00 arrived or the ring was empty
        beq     linkTickReturn          ; E850  the peer has not answered $FF yet - wait for the next tick
        inx                             ; E852  was it $FF?
        bne     restartByteSync         ; E853  some other byte on the line: the peer is not where we think it is, resync
        sta     ackPending              ; E855  $FF received, both ends are byte aligned; A = $FF also records that the peer wants an acknowledgement

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
runPacketPhase:
        lda     #$03                    ; E858  3 = the packet-protocol phase
        sta     connectionPhase         ; E85A  enter (or stay in) it
        jsr     receivePacket           ; E85D  decode whatever the peer has sent since the last tick
servicePacketTransmit:
        lda     uartPendingCount        ; E860  characters still queued for the UART transmitter
        cmp     #$02                    ; E863  two or more still to go?
        bcs     linkTickReturn          ; E865  the line is busy - do not start anything else this tick
        ldx     #$00                    ; E867  X = 0, the value used to clear the two raw-byte requests
        txa                             ; E869  A = 0 = the raw $00 byte that means 'resend'
        bit     sendZeroPending         ; E86A  sendZeroPending, made negative by receivePacket when a checksum failed
        stx     sendZeroPending         ; E86D  consume the request
        bmi     queueRawSyncByte        ; E870  a $00 nak is owed - send it
        lda     #$FF                    ; E872  otherwise A = $FF = the raw byte that means 'acknowledge me'
        bit     sendFfPending           ; E874  sendFfPending - nothing in the shipped image ever sets this, so this path is dead
        bpl     checkPacketInFlight     ; E877  no raw byte owed
queueRawSyncByte:
        stx     sendFfPending           ; E879  consume the $FF request
        jsr     queueByteForTransmit    ; E87C  put the raw sync byte on the wire
checkPacketInFlight:
        lda     frameInFlightFlag       ; E87F  is a message packet already in flight?
sub_E882:
        bne     packetPhaseReturn       ; E882  yes - wait until it has been acknowledged
        ldx     resendRequest           ; E884  resendRequest: 1 = the peer sent a lone $00, negative = resend the message with this index
        beq     buildNextMessage        ; E887  nothing to resend
        sta     resendRequest           ; E889  consume the request (A is 0 here)
        bpl     resendOldestMessage     ; E88C  N still comes from the LDX above: a plain $00 nak, so resend the oldest unacknowledged message
        cpx     txOldestUnackedIndex    ; E88E  targeted resend: does the request still name the oldest outstanding message?
        bne     buildNextMessage        ; E891  no - it has already been retired, ignore the stale request
resendOldestMessage:
        lda     txOldestUnackedIndex    ; E893  index of the oldest unacknowledged message; bit 7 = the slot is in use
        bpl     buildAckPacket          ; E896  nothing outstanding - send a bare acknowledgement instead
        and     #$7F                    ; E898  strip the in-use flag to get the message ring index
        jsr     sendPacket              ; E89A  retransmit that message
D_E89D:
        rts                             ; E89D  done


; ----------------------------------------------------------------------
; 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.
; ----------------------------------------------------------------------
buildNextMessage:
        ldx     txNextFreeIndex         ; E89E  index of the next free slot in the 35-byte message ring
        bmi     sendAckIfPending        ; E8A1  negative = both outstanding slots are busy, so no new message may be started
        lda     packetInFlightCount     ; E8A3  packet-layer send enable; sendPacketFsm does set it (indexed, at $E265), so the code below IS reachable
        beq     sendAckIfPending        ; E8A6  not enabled: fall through to the acknowledgement
        inc     frameInFlightFlag       ; E8A8  mark a message as in flight
        stx     txRingIndex             ; E8AB  start writing at the free ring position
        jsr     popArqTxRingThunk       ; E8AE  first byte of the game's queued message; its low nibble is the argument count
        sta     packetIndexSave         ; E8B1  keep the original command byte
        and     #$0F                    ; E8B4  low nibble = number of argument bytes (game commands are $80-$A6 with 0-3 arguments)
        sta     packetByteCounter       ; E8B6  use it as the copy counter
        jsr     writeTxRingByte         ; E8B9  store the argument count as the message's length byte
        inc     txSeqCounter            ; E8BC  advance the transmit sequence number
        lda     txSeqCounter            ; E8BF  read it back...
        and     #$03                    ; E8C2  ...and keep two bits of it
        asl     a                       ; E8C4  shift the sequence...
        asl     a                       ; E8C5  ...into bits 2-3
        eor     packetIndexSave         ; E8C6  merge it with the command byte...
        eor     packetByteCounter       ; E8C9  ...and cancel the length nibble again, giving (command AND $F0) OR (sequence << 2)
copyMessageByteLoop:
        jsr     writeTxRingByte         ; E8CC  append the control byte, and on every later pass one argument byte
        dec     packetByteCounter       ; E8CF  one fewer argument to copy
        bmi     finishMessageBuild      ; E8D2  all arguments copied
        jsr     popArqTxRingThunk       ; E8D4  next argument byte out of the game's outbound ring $E447
        bcc     copyMessageByteLoop     ; E8D7  got one - store it
finishMessageBuild:
        stx     txRingTailIndex         ; E8D9  remember the ring index just past this message
        lda     txNextFreeIndex         ; E8DC  index of the message just built
claimOutstandingSlot:
        pha                             ; E8DF  save it for the transmit call below
        ora     #$80                    ; E8E0  mark the slot as used
        bit     txOldestUnackedIndex    ; E8E2  is the oldest-unacknowledged slot already occupied?
        bmi     storeNextFreeIndex      ; E8E5  yes - this message becomes the second outstanding one
        sta     txOldestUnackedIndex    ; E8E7  no - this message becomes the oldest outstanding message...
        txa                             ; E8EA  ...and the free index moves to the end of the message (X = $EAC4)
storeNextFreeIndex:
        sta     txNextFreeIndex         ; E8EB  store the new free / second-outstanding index
        pla                             ; E8EE  recover the ring index of the message to send
        jsr     sendPacket              ; E8EF  build the packet and clock it out
packetPhaseReturn:
        rts                             ; E8F2  done

sendAckIfPending:
        lda     ackPending              ; E8F3  does the peer still need an acknowledgement?
        beq     packetPhaseReturn       ; E8F6  no - nothing at all to send this tick

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
buildAckPacket:
        lda     #$EC                    ; E8F8  $EC = the bare-acknowledgement control byte (any control byte $E0-$EF is an ack)
        ldx     #$55                    ; E8FA  $55 = packetLengthCodeTable entry 1, the code for a one-byte payload
        stx     packetBuffer            ; E8FC  packet byte 0 = length code
        sta     packetControlByte       ; E8FF  packet byte 1 = control byte
        lda     #$02                    ; E902  two bytes...
        sta     packetLength            ; E904  ...before the check byte
        bne     sendBuiltPacket         ; E907  always taken: skip the message-copy step and go straight to the send tail

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
sendPacket:
        jsr     buildPacketFromRing     ; E909  copy the message at ring index A into packetBuffer and prefix it with its length code
sendBuiltPacket:
        lda     rxSeqCounter            ; E90C  the sequence number of the next packet we expect to receive
        and     #$03                    ; E90F  keep two bits of it
        ora     packetControlByte       ; E911  piggy-back it in the control byte's low two bits...
        sta     packetControlByte       ; E914  ...so every packet also acknowledges what we have received
        jsr     computePacketChecksum   ; E917  checksum packet bytes 0..packetLength-1; X comes back equal to packetLength
        sta     packetBuffer,x          ; E91A  append the check byte right after the payload
        lda     #$00                    ; E91D  this packet carries the acknowledgement itself...
        sta     ackPending              ; E91F  ...so the pending bare-ack request is satisfied
        ldx     #$00                    ; E922  start at packet byte 0
transmitPacketLoop:
        stx     packetByteCounter       ; E924  save the index - queueByteForTransmit clobbers X
        lda     packetBuffer,x          ; E927  next packet byte
        jsr     queueByteForTransmit    ; E92A  hand it to the UART transmitter
        ldx     packetByteCounter       ; E92D  restore the index
        cpx     packetLength            ; E930  have we just queued the check byte?  the carry is taken before the INX below
        inx                             ; E933  advance to the next byte
L_E934:
        bcc     transmitPacketLoop      ; E934  loop; the check byte at index packetLength is the last one sent
        rts                             ; E936  the packet is on the wire


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
computePacketChecksum:
        ldx     #$00                    ; E937  start at packet byte 0
        lda     #$EE                    ; E939  $EE = the checksum seed
checksumLoop:
        asl     a                       ; E93B  rotate the running sum left...
        adc     #$00                    ; E93C  ...folding the bit that fell out back in at the bottom
        adc     packetBuffer,x          ; E93E  add the packet byte
        adc     #$00                    ; E941  fold that carry back in too
        inx                             ; E943  next byte
        cpx     packetLength            ; E944  stop at packetLength - the check byte slot itself is not included
        bcc     checksumLoop            ; E947  loop
        rts                             ; E949  return A = check byte, X = index of the check byte slot


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
buildPacketFromRing:
        tax                             ; E94A  A = ring index of the message's length byte
        jsr     readTxRingByteAt        ; E94B  read it; the ring index is left pointing at the control byte
        tax                             ; E94E  X = argument count
        inx                             ; E94F  payload length = arguments + 1, because the control byte counts as payload too
L_E950:
        stx     packetByteCounter       ; E950  use it as the copy counter
        lda     packetReturn,x          ; E953  packetLengthCodeTable[length]; the table base $EAA0 is the shared RTS, so entry 0 is never selected
        ldx     #$00                    ; E956  packet byte 0...
        beq     storePacketByte         ; E958  ...always taken, store the length code there
copyRingByteLoop:
        jsr     readTxRingByte          ; E95A  next byte of the message out of the ring
sub_E95D:
        ldx     packetLength            ; E95D  append it at the current packet length
storePacketByte:
        sta     packetBuffer,x          ; E960  store the packet byte
        inx                             ; E963  advance...
        stx     packetLength            ; E964  ...and record the new packet length
        dec     packetByteCounter       ; E967  one fewer byte to copy
        bpl     copyRingByteLoop        ; E96A  loop over the control byte and every argument
        rts                             ; E96C  packetBuffer now holds [length code][control][arguments]


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
writeTxRingByte:
        ldx     txRingIndex             ; E96D  current write position in the 35-byte message ring
        sta     txMessageRing,x         ; E970  store the byte
        bpl     advanceTxRingIndex      ; E973  always taken (the index is 0..34): go and advance it

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
readTxRingByte:
        ldx     txRingIndex             ; E975  current read position in the message ring

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
readTxRingByteAt:
        lda     txMessageRing,x         ; E978  read txMessageRing[X]
advanceTxRingIndex:
        inx                             ; E97B  advance the index
        cpx     #$23                    ; E97C  $23 = 35 = the ring size
        bcc     storeTxRingIndex        ; E97E  no wrap needed
        ldx     #$00                    ; E980  wrap back to the start of the ring
storeTxRingIndex:
        stx     txRingIndex             ; E982  save the new index
txRingReturn:
        rts                             ; E985  return A = byte, X = new index


; ----------------------------------------------------------------------
; 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 $E6A5) 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
; ----------------------------------------------------------------------
noteIdlePoll:
        ldx     #$00                    ; E986  X = 0 = 'not stalled'
        lda     txOldestUnackedIndex    ; E988  oldest unacknowledged message slot
        bpl     storeStallCount         ; E98B  positive = nothing outstanding, so an idle line is perfectly normal - reset the counter
        lda     txNextFreeIndex         ; E98D  the second outstanding slot
        bmi     countStalledPoll        ; E990  negative = the send window is full, so we really are stalled
        lda     frameInFlightFlag       ; E992  is a packet still being clocked out?
L_E995:
        bne     storeStallCount         ; E995  yes - progress is being made, reset the counter
countStalledPoll:
        ldx     D_EB00                  ; E997  count one more fruitless service tick
        inx                             ; E99A  bump it
storeStallCount:
        stx     D_EB00                  ; E99B  store the counter
        cpx     #$10                    ; E99E  sixteen consecutive dead ticks?
        bcc     txRingReturn            ; E9A0  not yet - return normally
unwindAndResync:
        pla                             ; E9A2  discard receivePacket's return address...
        pla                             ; E9A3  ...and its caller's...
        jmp     beginByteSyncPhase      ; E9A4  ...so the whole link restarts from the byte-sync handshake


; ----------------------------------------------------------------------
; 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.
; ----------------------------------------------------------------------
noteAckRequest:
        sta     ackPending              ; E9A7  remember that the peer is asking for an acknowledgement, then loop back into the scanner below

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
receivePacket:
        ldx     rxPayloadRemaining      ; E9AA  bytes still expected for a packet whose header has already arrived
        bne     awaitWholePacket        ; E9AD  resume that packet
        jsr     receiveByteTrackZeros   ; E9AF  take the next byte and update the zero-run counter
        bcs     noteIdlePoll            ; E9B2  ring empty - count an idle poll (which may force a resync) and return
        eor     #$00                    ; E9B4  set Z from the byte
        bne     checkForFfSync          ; E9B6  not a $00 inter-packet byte
        dex                             ; E9B8  X = the zero-run length...
        dex                             ; E9B9  ...minus two
L_E9BA:
        bpl     unwindAndResync         ; E9BA  two or more $00 in a row: the peer is not framing any more, resync the link
        lda     #$01                    ; E9BC  a single $00 is the peer's 'resend the last message' nak...
        sta     resendRequest           ; E9BE  ...record it for runPacketPhase
        bne     receivePacket           ; E9C1  always taken: go back and look at the next byte
checkForFfSync:
        cmp     #$FF                    ; E9C3  $FF is the peer's 'acknowledge me' inter-packet byte
        beq     noteAckRequest          ; E9C5  record it and keep scanning
        sta     rxLengthCodeByte        ; E9C7  anything else must be a packet header: remember the length code byte
        ldx     #$00                    ; E9CA  search packetLengthCodeTable from entry 1 upwards
lengthCodeSearchLoop:
        inx                             ; E9CC  next table entry
        cmp     packetReturn,x          ; E9CD  compare; the table is sorted ascending and ends in an $FF sentinel, so the search always terminates
        beq     beginPacketBody         ; E9D0  found: X = payload length 1..16
        bcs     lengthCodeSearchLoop    ; E9D2  the received byte is still above this entry - keep searching
        bcc     unwindAndResync         ; E9D4  it fell between two entries, so it is not a valid length code: resync
beginPacketBody:
        stx     rxPayloadRemaining      ; E9D6  remember how many payload bytes are still to come
awaitWholePacket:
        cpx     uartRxCount             ; E9D9  are the payload and its check byte all in the receive ring yet?  we need X+1 bytes
        bcs     countStalledPoll        ; E9DC  not yet - count the wait and come back next tick with the state preserved
        lda     rxLengthCodeByte        ; E9DE  re-fetch the length code byte
L_E9E1:
        sta     packetBuffer            ; E9E1  it is packet byte 0
D_E9E4:
        ldx     #$00                    ; E9E4  start filling...
        stx     packetLength            ; E9E6  ...at packet byte 1
copyPacketByteLoop:
        jsr     popUartRxRingThunk      ; E9E9  next byte off the wire
        inc     packetLength            ; E9EC  one more byte in the packet...
        ldx     packetLength            ; E9EF  ...index it...
        sta     packetBuffer,x          ; E9F2  ...and store it
        dec     rxPayloadRemaining      ; E9F5  one fewer byte to read
        bpl     copyPacketByteLoop      ; E9F8  loop over the payload and the check byte
        inc     rxPayloadRemaining      ; E9FA  back to 0: no packet is half received any more
        eor     #$00                    ; E9FD  the check byte should never be $00 - that is an inter-packet byte
        beq     unwindAndResync         ; E9FF  it was - treat the packet as garbage and resync
        jsr     computePacketChecksum   ; EA01  recompute the checksum over bytes 0..packetLength-1
        eor     packetBuffer,x          ; EA04  compare it with the received check byte at index packetLength
        beq     dispatchReceivedPacket  ; EA07  they match - act on the packet
        dec     sendZeroPending         ; EA09  bad checksum: make the next tick put a raw $00 nak on the wire
        rts                             ; EA0C  drop the packet


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
dispatchReceivedPacket:
        ldx     packetControlByte       ; EA0D  the control byte, i.e. packet byte 1
L_EA10:
        cpx     #$F0                    ; EA10  $F0 and above is the 1541 pass-through channel
        bcc     handleAckOrDataPacket   ; EA12  below: an ordinary acknowledgement or data packet
        php                             ; EA14  save the interrupt state
L_EA15:
        sei                             ; EA15  the drive handshake is bit-banged, so the UART NMI must not interrupt it
        beq     sendFinalDriveByte      ; EA16  Z still comes from the CPX: a control byte of exactly $F0 sends a single byte
        tay                             ; EA18  A is 0 here (the checksum matched), so Y = 0 = the first payload byte
        txa                             ; EA19  the first byte handed to the drive is the control byte itself
L_EA1A:
        sta     packetIndexSave         ; EA1A  $EAFE counts up to zero, so 256 minus the control byte bytes are sent in total
sendDriveByteLoop:
        jsr     sendByteToDrive         ; EA1D  clock one byte out to the 1541 over CIA2 port A
        lda     packetPayload,y         ; EA20  next payload byte ($EAED = packet byte 2)
        iny                             ; EA23  advance the payload index
        inc     packetIndexSave         ; EA24  count the byte
D_EA27:
        bne     sendDriveByteLoop       ; EA27  loop until the counter wraps to zero
sendFinalDriveByte:
        jsr     sendByteToDrive         ; EA29  send the last byte
        plp                             ; EA2C  restore the interrupt state
L_EA2D:
        rts                             ; EA2D  done with the pass-through

handleAckOrDataPacket:
        stx     rxAckControlByte        ; EA2E  remember the received control byte for the sequence comparison
        lda     txOldestUnackedIndex    ; EA31  is any message of ours outstanding?
        bpl     handleDataPacket        ; EA34  no - there is nothing to retire
D_EA36:
        cpx     #$EC                    ; EA36  control bytes $EC and above also ask for the outstanding message to be resent
sub_EA38:
        bcc     checkOutstandingSequence; EA38  below $EC: a plain acknowledgement
        sta     resendRequest           ; EA3A  ask runPacketPhase to resend the message named by $EAC6 (A is that negative index)
checkOutstandingSequence:
        and     #$7F                    ; EA3D  strip the in-use bit to get the ring index
        tax                             ; EA3F  index the ring with it
        jsr     readTxRingByteAt        ; EA40  skip the message's argument-count byte
        jsr     readTxRingByteAt        ; EA43  read its control byte
sub_EA46:
        lsr     a                       ; EA46  the message's own sequence number sits in bits 2-3...
        lsr     a                       ; EA47  ...so shift it down to bits 0-1
        clc                             ; EA48  CLC before SBC subtracts one extra...
        sbc     rxAckControlByte        ; EA49  ...from the sequence carried in the ack byte's low two bits
        and     #$02                    ; EA4C  bit 1 of the difference separates 'covered by this ack' from 'not yet acknowledged'
        beq     handleDataPacket        ; EA4E  acknowledged - the send window is clear again
        ldx     txRingTailIndex         ; EA50  not acknowledged: shuffle the outstanding slots down one place...
        lda     txNextFreeIndex         ; EA53  ...moving the newest index...
        stx     txNextFreeIndex         ; EA56  ...into the free slot...
        sta     txOldestUnackedIndex    ; EA59  ...and the old free slot into the oldest-unacknowledged slot
        bmi     checkOutstandingSequence; EA5C  another message is still outstanding - test that one too
handleDataPacket:
        lda     packetControlByte       ; EA5E  the control byte again
        cmp     #$E0                    ; EA61  $E0-$EF are pure acknowledgements
        bcs     packetReturn            ; EA63  an ack carries no payload, so there is nothing left to do
acceptDataPacket:
        dec     ackPending              ; EA65  a data packet, so we now owe the peer an acknowledgement
        sta     rxDataControlByte       ; EA68  remember the control byte
        lsr     a                       ; EA6B  its 2-bit sequence number sits in bits 2-3...
        lsr     a                       ; EA6C  ...shift it down
        clc                             ; EA6D  CLC before SBC subtracts one extra, so a difference of 0 means 'the very next packet'
        sbc     rxSeqCounter            ; EA6E  compare with the sequence we expect
        and     #$03                    ; EA71  only the two sequence bits matter
        ora     arqRxCount              ; EA73  and the game's inbound command ring must be empty before we hand it anything
        bne     packetReturn            ; EA76  out of sequence or the game is still busy: drop the packet, the peer will resend it
        inc     rxSeqCounter            ; EA78  accept it: the next packet should carry the following sequence number

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
deliverPacketPayload:
        lda     packetLength            ; EA7B  total packet length, i.e. the index of the check byte
        sec                             ; EA7E  minus the length code...
        sbc     #$02                    ; EA7F  ...and the control byte...
        sta     packetLength            ; EA81  ...leaves the number of argument bytes, which is also the loop counter
        eor     packetControlByte       ; EA84  classic bit merge: A EOR control byte...
        and     #$0F                    ; EA87  ...masked to the low nibble...
        eor     packetControlByte       ; EA89  ...EOR the control byte again gives (ctrl AND $F0) OR (argcount AND $0F) = the game's original command byte
        ldx     #$01                    ; EA8C  packet byte 1 is the control-byte slot; the first argument follows at byte 2
deliverPayloadLoop:
        stx     packetIndexSave         ; EA8E  save the buffer index - the push call clobbers X
        jsr     pushArqRxRingThunk      ; EA91  push the byte into the game's inbound command ring $E45C
        ldx     packetIndexSave         ; EA94  restore the index...
        inx                             ; EA97  ...and step to the next packet byte
        lda     packetBuffer,x          ; EA98  load the argument byte
        dec     packetLength            ; EA9B  one fewer argument to deliver
        bpl     deliverPayloadLoop      ; EA9E  loop, then fall into the shared RTS below

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
packetReturn:
        rts                             ; EAA0  shared exit of the packet layer; also table entry 0 of packetLengthCodeTable, which is never matched


; 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.
packetLengthCodeTable:
        .byte   $55,$56                 ; EAA1  UV  codes for payload lengths 1 and 2
L_EAA3:
        .byte   $59                     ; EAA3  codes for payload lengths 3 to 10
        .byte   $5A,$65,$66,$69,$6A,$95,$96; EAA4  Zefij..
        .byte   $99                     ; EAAB  codes for payload lengths 11 to 16, then the $FF sentinel that ends the search
        .byte   $9A,$A5,$A6,$A9,$AA,$FF ; EAAC  ......

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
clearPacketState:
        ldx     #$4C                    ; EAB2  $4C = 76 bytes to clear
        jsr     clearInlineVarBlock     ; EAB4  zero the 76 bytes that follow this JSR; the helper consumes the return address, so the RTS lands in initCommModule's caller

; ----------------------------------------------------------------------
; 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).
; ----------------------------------------------------------------------
packetProtocolState:
        brk                             ; EAB7  syncRoundCounter: 0 = still waiting for the peer's $00, 1-9 = waiting for its $FF, 10 = give up and restart the sync
zeroRunLength:
        .byte   $00                     ; EAB8  zeroRunLength: consecutive $00 bytes received; 1 = a resend nak, 2 or more = the link has lost sync
rxPayloadRemaining:
        .byte   $00                     ; EAB9  rxPayloadRemaining: payload bytes still to be read for the packet in progress; 0 = no packet in progress
packetByteCounter:
        .byte   $00                     ; EABA  packetByteCounter: scratch loop counter and index, saved across calls that clobber X
rxAckControlByte:
        .byte   $00                     ; EABB  rxAckControlByte: control byte of the acknowledgement being processed
rxDataControlByte:
        .byte   $00                     ; EABC  rxDataControlByte: control byte of the data packet being processed (written at $EA68, never read back)
rxSeqCounter:
        .byte   $00                     ; EABD  rxSeqCounter: sequence number of the next data packet expected; its low two bits ride out in every packet we send
txSeqCounter:
        .byte   $00                     ; EABE  txSeqCounter: sequence number given to the next message we build
resendRequest:
        .byte   $00                     ; EABF  resendRequest: 0 = none, 1 = the peer sent a lone $00, negative = resend the message with this index
ackPending:
        .byte   $00                     ; EAC0  ackPending: non-zero when the peer is owed an acknowledgement
sendFfPending:
        .byte   $00                     ; EAC1  sendFfPending: request to put a raw $FF on the wire - nothing in the shipped image ever sets it
sendZeroPending:
        .byte   $00                     ; EAC2  sendZeroPending: made negative by a checksum failure so that a raw $00 nak goes out next tick
txRingIndex:
        .byte   $00                     ; EAC3  txRingIndex: current read/write position in the 35-byte message ring
txRingTailIndex:
        .byte   $00                     ; EAC4  txRingTailIndex: ring index just past the message most recently built
txNextFreeIndex:
        .byte   $00                     ; EAC5  txNextFreeIndex: where the next message may be written; negative means both outstanding slots are busy
txOldestUnackedIndex:
        .byte   $00                     ; EAC6  txOldestUnackedIndex: bit 7 = slot in use, bits 0-6 = ring index of the oldest unacknowledged message

; 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.
txMessageRing:
        .byte   $00,$00,$00,$00,$00,$00,$00; EAC7  .......  start of the 35-byte transmit message ring
L_EACE:
        .byte   $00                     ; EACE  message ring byte 7
sub_EACF:
        .byte   $00                     ; EACF  message ring bytes 8-15
        .byte   $00,$00,$00,$00,$00,$00,$00; EAD0  .......
        .byte   $00                     ; EAD7  message ring bytes 16-23
        .byte   $00,$00,$00,$00,$00,$00,$00; EAD8  .......
        .byte   $00                     ; EADF  message ring bytes 24-31
        .byte   $00,$00,$00,$00,$00,$00,$00; EAE0  .......
        .byte   $00                     ; EAE7  message ring bytes 32-34, the last of the 35 slots
        .byte   $00,$00                 ; EAE8  ..
packetLength:
        .byte   $00                     ; EAEA  packetLength: number of packet bytes before the check byte, i.e. the index of the check byte slot

; 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.
packetBuffer:
        .byte   $00                     ; EAEB  .  packet byte 0 = the length code taken from packetLengthCodeTable
packetControlByte:
        .byte   $00                     ; EAEC  packet byte 1 = the control byte: command nibble in bits 4-7, sequence in bits 2-3, piggy-backed ack in bits 0-1
packetPayload:
        .byte   $00                     ; EAED  packet byte 2 = the first payload/argument byte
L_EAEE:
        .byte   $00                     ; EAEE  packet bytes 3-5
        .byte   $00,$00                 ; EAEF  ..
L_EAF1:
        .byte   $00                     ; EAF1  packet bytes 6-13
        .byte   $00,$00,$00,$00,$00,$00,$00; EAF2  .......
        .byte   $00                     ; EAF9  packet bytes 14-18; a full 16-byte payload plus length code and check byte uses up to byte 17
        .byte   $00,$00,$00,$00         ; EAFA  ....
packetIndexSave:
        .byte   $00                     ; EAFE  packetIndexSave: buffer index parked across calls that clobber X; also the drive pass-through byte counter
rxLengthCodeByte:
        .byte   $00                     ; EAFF  rxLengthCodeByte: the length code of the packet being received, kept while waiting for its body
D_EB00:
        .byte   $00                     ; EB00  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
modemDelayCounter:
        .byte   $00                     ; EB01  modemDelayCounter: guard-time delay, held as a negative tick count taken from the AT strings
lastModemChar:
        .byte   $00                     ; EB02  lastModemChar: the previous character the modem sent, used to spot the '0' of 'CONNECT 1200'

; ----------------------------------------------------------------------
; 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
; remoteCodeBuffer: unknown, 211 bytes. $EB03-$EBD5: buffer that receives 6502 code sent by the peer
; in messages with a command byte >= $70 (copied by $E1B3, executed by JSR $EB03). Its shipped
; contents are one RTS at $EB03 followed by 210 bytes of leftover build-1 game code (see
; misclassified).
; ----------------------------------------------------------------------
remoteCodeBuffer:
        rts                             ; EB03  shipped as a bare RTS, so the peer's 'run downloaded code' message is a no-op until code has been uploaded

        .byte   $27                     ; EB04  $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
L_EB05:
        .byte   $AD                     ; EB05  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $D5,$F6,$38,$F9,$A4,$F6,$C9; EB06  ..8....
        .byte   $F8                     ; EB0D  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $B0,$04,$C9,$08,$B0,$18 ; EB0E  ......
L_EB14:
        .byte   $BE                     ; EB14  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $00,$88,$EC,$64,$89,$F0,$4A; EB15  ...d..J
        .byte   $BD                     ; EB1C  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $3E,$89,$09,$40,$9D,$3E,$89; EB1D  >..@.>.
        .byte   $30                     ; EB24  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $06,$AD,$CB,$91,$99,$CE,$87; EB25  .......
L_EB2C:
        .byte   $C0                     ; EB2C  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $63,$B0,$36,$C0,$60,$90,$32; EB2D  c.6.`.2
        .byte   $B9                     ; EB34  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $32,$88,$10,$2D,$B9,$00,$88; EB35  2..-...
        .byte   $CD                     ; EB3C  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $54,$89,$B0,$25,$CD,$64,$89; EB3D  T..%.d.
        .byte   $F0                     ; EB44  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $20,$EE,$53,$89,$AD,$53,$89; EB45   .S..S.
        .byte   $AA                     ; EB4C  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $99,$00,$88,$AD,$CB,$91,$99; EB4D  .......
        .byte   $CE                     ; EB54  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $87,$B9,$D0,$F7,$29,$3F,$9D; EB55  ....)?.
        .byte   $D2                     ; EB5C  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $88,$B9,$34,$F8,$29,$3F,$9D; EB5D  ..4.)?.
        .byte   $DE                     ; EB64  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $88                     ; EB65  .
L_EB66:
        .byte   $60                     ; EB66  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
sub_EB67:
        .byte   $A9                     ; EB67  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $03,$38,$ED,$68,$89,$90,$74; EB68  .8.h..t
        .byte   $8D                     ; EB6F  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $67,$89,$AA,$AD,$71,$89,$F0; EB70  g...q..
        .byte   $1E                     ; EB77  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $E0,$02,$90,$67,$CA,$CA,$8E; EB78  ...g...
        .byte   $67                     ; EB7F  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $89,$20,$E4,$EB,$20,$5D,$FD; EB80  . .. ].
        .byte   $29                     ; EB87  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $07,$D0,$58,$A9,$9E,$20,$DD; EB88  ..X.. .
        .byte   $EB                     ; EB8F  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $EE,$67,$89,$4C,$9F,$EB ; EB90  .g.L..
L_EB96:
        .byte   $8A                     ; EB96  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $F0,$4A,$20,$E4,$EB     ; EB97  .J ..
L_EB9C:
        .byte   $20                     ; EB9C  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $A5,$EB                 ; EB9D  ..
L_EB9F:
        .byte   $CE                     ; EB9F  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $67,$89,$D0,$F8,$60     ; EBA0  g...`
sub_EBA5:
        .byte   $20                     ; EBA5  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $5D,$FD,$29,$01,$F0,$37,$AD; EBA6  ].)..7.
        .byte   $27                     ; EBAD  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $FA,$29,$3F,$C9,$2D,$B0,$2E; EBAE  .)?.-..
        .byte   $85                     ; EBB5  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $18,$A9,$32,$38,$E5,$18,$4A; EBB6  ..28..J
        .byte   $4A                     ; EBBD  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $4A,$18,$69,$01,$8D,$98,$88; EBBE  J.i....
        .byte   $18                     ; EBC5  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $6D,$65,$89,$8D,$65,$89,$29; EBC6  me..e.)
        .byte   $03                     ; EBCD  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
        .byte   $D0,$06,$EE,$98,$88,$EE,$65; EBCE  ......e
        .byte   $89                     ; EBD5  dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here

; 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.
modemHangUpString:
        .byte   $0D,"0HTA",'@'|$80,$0D  ; EBD6  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
sub_EBDD:
        .byte   $A0                     ; EBDD  the two guard-time delay bytes and the '+++' escape; the $00 at $EBE2 terminates the copy
        .byte   $2B,$2B,$2B,$A0,$00     ; EBDE  +++..

; modemAnswerInitString: text, 13 bytes. Reversed modem init string for answer mode: 'ATQ0V1X1A' + CR
; preceded by a 64-tick guard delay. Selected when the player presses 'A' (command byte $C1 at $E37A).
modemAnswerInitString:
        .byte   $0D                     ; EBE3  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)
sub_EBE4:
        .byte   $41                     ; EBE4  the string body, stored in reverse order
        .byte   $31,$58,$31,$56,$30,$51,$54; EBE5  1X1V0QT
        .byte   $41                     ; EBEC  the last three bytes of the reversed answer string: 'A', the $C0 guard delay and the leading CR
        .byte   $C0,$0D                 ; EBED  ..
sub_EBEF:
        .byte   $00                     ; EBEF  the $00 terminator

; modemDialInitString: text, 13 bytes. Reversed modem init string for originate mode: 'ATQ0V1X1D' + CR
; preceded by a 64-tick guard delay ('D' = dial). Selected when the player presses 'O' (command byte
; $CF at $E37E).
modemDialInitString:
        .byte   $0D,"D1X1V0QTA",'@'|$80 ; EBF0  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)
L_EBFB:
        .byte   $0D                     ; EBFB  leading CR and the $00 terminator
        .byte   $00                     ; EBFC  .

; hangUpStringPtrLo: byteTable, 1 bytes. Low byte ($D6) of modemHangUpString, read by sub_E38D and
; passed to the string-to-queue routine sub_E393 (the high byte $EB is hard coded in the self-modified
; LDA at $E3A1).
hangUpStringPtrLo:
        .byte   $D6                     ; EBFD  .  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

; modemInitStringPtrLoTable: byteTable, 2 bytes. Low bytes $E3 and $F0 of the answer and dial init
; strings, indexed by modemModeIndex $E011 at $E2E0.
modemInitStringPtrLoTable:
        .byte   $E3,$F0                 ; EBFE  ..  modemInitStringPtrLoTable: low bytes of the answer ($EBE3) and dial ($EBF0) strings, indexed by isOriginateMode $E011 at $E2E0

; ----------------------------------------------------------------------
; 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)
; ==========================================================================
; $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.
; ==========================================================================
; ----------------------------------------------------------------------
clearBattlefieldMap:
        lda     #$00                    ; EC00  low byte of the first map cell...
        sta     mapCellPtr              ; EC02  ...into the fill pointer zp_48/zp_49
        lda     #$F0                    ; EC04  map base page
        sta     mapCellPtrHi            ; EC06  fill pointer = $F000 = map cell (0,0)
        lda     #$3F                    ; EC08  low byte of the last map cell...
D_EC0A:
        sta     fillEndPtr              ; EC0A  ...into the fill limit zp_4A/zp_4B
        lda     #$F6                    ; EC0C  limit page
        sta     fillEndPtrHi            ; EC0E  limit = $F63F = map cell (39,39); 40*40 = 1600 cells
        ldy     #$00                    ; EC10  always index 0 - the pointer itself walks
L_EC12:
        lda     #$40                    ; EC12  $40 = the empty terrain code (the map alphabet runs $40-$58)
L_EC14:
        sta     (mapCellPtr),y          ; EC14  clear this cell
        jsr     advanceFillPointer      ; EC16  C = 1 once the pointer has reached the limit, then step it on one cell
        bcc     L_EC12                  ; EC19  loop over all 1600 cells
        rts                             ; EC1B  the map is blank


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
setViewOriginToHome:
        lda     #$11                    ; EC1C  column 17: the view is 7 cells wide, so the cursor sits on column 20, the middle of the 40-cell map
        sta     viewOriginCol           ; EC1E  live view origin column
        sta     savedViewOriginCol      ; EC20  and the copy that survives a screen change
        lda     #$1C                    ; EC23  row 28: the view is 5 cells tall, so the cursor sits on row 30, well inside the player's own half
        sta     viewOriginRow           ; EC25  live view origin row
        sta     savedViewOriginRow      ; EC27  and its saved copy
        rts                             ; EC2A  done


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
loadMapSeedIntoScenarioRng:
        lda     mapSeed                 ; EC2B  seed byte 0 from the settings block
        sta     scenarioSeed0           ; EC2E  into the scenario RNG state
        ldy     mapSeed1                ; EC30  seed byte 1
        sty     scenarioSeed1           ; EC33  into the RNG state
        ldx     mapSeed2                ; EC35  seed byte 2
        stx     scenarioSeed2           ; EC38  into the RNG state - both players now generate the same map
        rts                             ; EC3A  done


; 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.
mirroredTerrainCodeTable:
        .byte   $40                     ; EC3B  @  index 0 = code $40 (empty terrain), which maps to itself
L_EC3C:
        .byte   $44                     ; EC3C  codes $41-$48 ('A'-'H') mirror to 'D','C','B','A','H','G','F','E'
        .byte   $43,$42,$41,$48,$47,$46,$45; EC3D  CBAHGFE
        .byte   $4B                     ; EC44  codes $49-$4A ('I','J') mirror to 'K','L'
        .byte   $4C                     ; EC45  L
L_EC46:
        .byte   $49                     ; EC46  codes $4B-$52 ('K'-'R') mirror to 'I','J','T','Q','S','R','N','P'
        .byte   $4A,$54,$51,$53,$52,$4E,$50; EC47  JTQSRNP
        .byte   $4F                     ; EC4E  codes $53-$58 ('S'-'X') mirror to 'O','M','X','W','V','U'
        .byte   $4D,$58,$57,$56,$55     ; EC4F  MXWVU

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
makeMapPointSymmetric:
        lda     #$FF                    ; EC54  $FF = mirror one way only, do not copy the far half back
        bne     storeMirrorMode         ; EC56  always taken

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
rotateMapAndMirrorCodes:
        lda     #$00                    ; EC58  0 = swap in both directions, a true 180 degree rotation
storeMirrorMode:
        sta     scratch18               ; EC5A  remember which of the two modes we are in
        lda     #$00                    ; EC5C  low byte of the first map cell
L_EC5E:
        sta     mapCellPtr              ; EC5E  forward pointer zp_48/zp_49 low = $00
        clc                             ; EC60  clear carry for the address arithmetic below
        adc     #$3F                    ; EC61  $00 + $3F...
        sta     fillEndPtr              ; EC63  ...backward pointer zp_4A/zp_4B low = $3F
        lda     #$F0                    ; EC65  map base page
L_EC67:
        sta     mapCellPtrHi            ; EC67  forward pointer = $F000 = map cell (0,0)
        adc     #$06                    ; EC69  $F0 + $06...
        sta     fillEndPtrHi            ; EC6B  ...backward pointer = $F63F = map cell (39,39)
        ldy     #$00                    ; EC6D  always index 0 - both pointers walk

; ----------------------------------------------------------------------
; 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.
; ----------------------------------------------------------------------
mirrorCellPairLoop:
        lda     (fillEndPtr),y          ; EC6F  the cell at the far end of the map
        pha                             ; EC71  keep it on the stack
L_EC72:
        lda     (mapCellPtr),y          ; EC72  the cell at the near end
L_EC74:
        jsr     mirrorTerrainCode       ; EC74  convert it to its 180-degree equivalent
        sta     (fillEndPtr),y          ; EC77  and write it into the far cell
        pla                             ; EC79  recover the far cell's original byte
        ldx     scratch18               ; EC7A  which mirror mode?
        bmi     stepBackwardPointer     ; EC7C  $FF = symmetry pass, so do not write anything back into the near cell
        jsr     mirrorTerrainCode       ; EC7E  rotation pass: mirror the far cell too...
        sta     (mapCellPtr),y          ; EC81  ...and store it in the near cell, completing the swap
stepBackwardPointer:
        lda     fillEndPtr              ; EC83  step the backward pointer down one cell - low byte first
        bne     decBackwardPointer      ; EC85  no page underflow
        dec     fillEndPtrHi            ; EC87  borrow into the high byte
decBackwardPointer:
        dec     fillEndPtr              ; EC89  decrement the low byte
        jsr     advanceFillPointer      ; EC8B  step the forward pointer up and set C when the two pointers have met
        bcc     mirrorCellPairLoop      ; EC8E  loop over all 800 pairs
        rts                             ; EC90  the map is now rotated (or symmetric)


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
mirrorAllUnitCoordinates:
        lda     #$40                    ; EC91  unitColTable low byte...
L_EC93:
        ldy     #$F6                    ; EC93  ...page: array 0 at $F640, the current column of each of the 100 units
        jsr     mirrorCoordinateArray   ; EC95  mirror it
        lda     #$A4                    ; EC98  unitRowTable low byte...
L_EC9A:
        ldy     #$F6                    ; EC9A  ...page: array 1 at $F6A4, $64 further on, the current row
L_EC9C:
        jsr     mirrorCoordinateArray   ; EC9C  mirror it
        lda     #$D0                    ; EC9F  unitDestColTable low byte...
        ldy     #$F7                    ; ECA1  ...page: array 4 at $F7D0, the ordered destination column
L_ECA3:
        jsr     mirrorCoordinateArray   ; ECA3  mirror it
L_ECA6:
        lda     #$34                    ; ECA6  unitDestRowTable low byte...
        ldy     #$F8                    ; ECA8  ...page: array 5 at $F834, the destination row
        jsr     mirrorCoordinateArray   ; ECAA  mirror it
        lda     #$98                    ; ECAD  unitPathColTable low byte...
        ldy     #$F8                    ; ECAF  ...page: array 6 at $F898, the way-point column
        jsr     mirrorCoordinateArray   ; ECB1  mirror it
        lda     #$FC                    ; ECB4  unitPathRowTable low byte...
        ldy     #$F8                    ; ECB6  ...page: array 7 at $F8FC, the way-point row; fall straight into the mirror 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)
; ----------------------------------------------------------------------
mirrorCoordinateArray:
        sta     mapCellPtr              ; ECB8  array pointer low
        sty     mapCellPtrHi            ; ECBA  array pointer high
        ldy     #$63                    ; ECBC  $63 = 99 = the last of the 100 unit slots
mirrorEntryLoop:
        lda     (mapCellPtr),y          ; ECBE  the entry for this unit
        tax                             ; ECC0  keep a copy
        and     #$C0                    ; ECC1  isolate the two flag bits (bits 6-7)
        sta     restoreCoordFlagBits+1  ; ECC3  patch them into the ORA immediate operand at $ECCD - this routine's only state is self-modifying code
        txa                             ; ECC6  back to the whole byte
        and     #$3F                    ; ECC7  the coordinate lives in bits 0-5 (0..39 needs six bits)
        jsr     mirrorMapCoordinate     ; ECC9  39 minus the coordinate
restoreCoordFlagBits:
        ora     #$FF                    ; ECCC  operand patched at $ECC3: put the flag bits back
        sta     (mapCellPtr),y          ; ECCE  store the mirrored entry
        dey                             ; ECD0  previous unit
        bpl     mirrorEntryLoop         ; ECD1  loop over units 99 down to 0
        rts                             ; ECD3  the array is mirrored


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
mirrorTerrainCode:
        tax                             ; ECD4  keep the original byte in X
        bmi     mirrorTerrainReturn     ; ECD5  bit 7 set: a unit stands here and the terrain under it is kept in the per-unit array - leave the cell alone
        sec                             ; ECD7  no borrow
        sbc     #$40                    ; ECD8  convert to a table index by removing the $40 base of the terrain alphabet
        bcs     checkTerrainCodeRange   ; ECDA  code was $40 or above - the normal range
        adc     #$20                    ; ECDC  code was below $40: C is clear, so this adds $20-1 and maps the $20-$3F range onto indices $00-$1F
lookupLowRangeCode:
        tax                             ; ECDE  index the table
        lda     mirroredTerrainCodeTable,x; ECDF  look up the mirrored code
        sec                             ; ECE2  no borrow
L_ECE3:
        sbc     #$20                    ; ECE3  shift the answer back down into the $20-$3F range
        bne     mirrorTerrainReturn     ; ECE5  effectively unconditional: the table only holds $40-$58, so the result can never be zero
checkTerrainCodeRange:
        cmp     #$19                    ; ECE7  $19 = 25 = the number of table entries
L_ECE9:
        bcc     lookupTerrainCode       ; ECE9  index 0-24 is a valid terrain code
        txa                             ; ECEB  out of range - hand back the original byte
        bne     mirrorTerrainReturn     ; ECEC  taken unless the cell byte was $00, which the map never contains
lookupTerrainCode:
        tax                             ; ECEE  index the table
        lda     mirroredTerrainCodeTable,x; ECEF  look up the mirrored code
mirrorTerrainReturn:
        rts                             ; ECF2  return the (possibly unchanged) cell byte


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
loadUnitStartTemplate:
        lda     unitColTable            ; ECF3  first byte of the live unit column array; while the templates are still resident it has bit 7 set
        bpl     templateAlreadyLoaded   ; ECF6  the template has already been consumed - nothing to do
        lda     gameTypeOptions         ; ECF8  the game options byte
        and     #$07                    ; ECFB  bits 0-2 = game type 0-7 (SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER)
L_ECFD:
        tay                             ; ECFD  index the layout table with it
        lda     #$00                    ; ECFE  template area low byte...
        sta     templateCopyLoad+1      ; ED00  ...patched into the LDA operand at $ED31
        lda     #$F0                    ; ED03  template area page...
        sta     templateCopyLoad+2      ; ED05  ...patched into the LDA operand at $ED32, so the copy reads from $F000
        lda     #$40                    ; ED08  unit array low byte...
L_ED0A:
        sta     templateCopyStore+1     ; ED0A  ...patched into the STA operand at $ED34
        lda     #$F6                    ; ED0D  unit array page...
        sta     templateCopyStore+2     ; ED0F  ...patched into the STA operand at $ED35, so the copy writes to $F640
L_ED12:
        lda     unitLayoutByGameType,y  ; ED12  which of the four templates does this game type use?
        beq     copyTemplatePages       ; ED15  template 0 - the source already points at it
        tax                             ; ED17  use the template number as a repeat count
advanceTemplateSource:
        lda     templateCopyLoad+1      ; ED18  advance the patched source address...
        clc                             ; ED1B  ...clear carry...
        adc     #$90                    ; ED1C  ...by $190 = 400 bytes, the size of one template...
        sta     templateCopyLoad+1      ; ED1E  ...low byte...
        lda     templateCopyLoad+2      ; ED21  ...and...
        adc     #$01                    ; ED24  ...the...
        sta     templateCopyLoad+2      ; ED26  ...high byte
        dex                             ; ED29  one template further
        bne     advanceTemplateSource   ; ED2A  repeat until the source points at the chosen template
copyTemplatePages:
        ldy     #$00                    ; ED2C  start at offset 0 of the page
        ldx     #$02                    ; ED2E  two pages = 512 bytes
templateCopyLoad:
        lda     irqVectorHi,y           ; ED30  operand patched at $ED00/$ED05 - reads the template ($FFFF is just what the shipped bytes happen to say)
templateCopyStore:
        sta     irqVectorHi,y           ; ED33  operand patched at $ED0A/$ED0F - writes the live unit arrays
        iny                             ; ED36  next byte
        bne     templateCopyLoad        ; ED37  256 bytes per page
        inc     templateCopyLoad+2      ; ED39  next source page
        inc     templateCopyStore+2     ; ED3C  next destination page
        dex                             ; ED3F  one page done
        bne     templateCopyLoad        ; ED40  copy the second page
templateAlreadyLoaded:
        rts                             ; ED42  return


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
exchangePlayerNamesAndSettings:
        lda     isSoloTrainer           ; ED43  bit 7 set = practice with the solo trainer, so there is no link to talk to
        bpl     handshakeWithPeer       ; ED46  modem game - do the real handshake
        ldy     #$09                    ; ED48  the opponent name is ten characters
copyTrainerNameLoop:
        lda     msgTrainerName,y        ; ED4A  'TRAINER   ' at $884E
        sta     opponentPlayerName,y    ; ED4D  into the opponent name at $FFE8
        dey                             ; ED50  previous character
        bpl     copyTrainerNameLoop     ; ED51  copy all ten
        rts                             ; ED53  done - a solo game has no peer

handshakeWithPeer:
        jsr     waitForLinkMessage      ; ED54  wait until the comm module reports a completed exchange
        ldx     #$02                    ; ED57  X = 2 = release the received message
L_ED59:
        jsr     commJumpTable           ; ED59  call jump-table entry $E000 to free the receive buffer
        ldx     #$00                    ; ED5C  X = 0 = name characters 0-4
        jsr     exchangeNameHalf        ; ED5E  swap the first half of the name
L_ED61:
        ldx     #$05                    ; ED61  X = 5...
L_ED63:
        jsr     exchangeNameHalf        ; ED63  ...swap name characters 5-9
        lda     opponentPlayerName      ; ED66  first character of the name the peer sent
        cmp     #$A0                    ; ED69  $A0 = shifted space: the peer's disk carries no owner name
L_ED6B:
        bne     sendSettingsMessage     ; ED6B  the peer has a name - keep it
        ldy     #$04                    ; ED6D  'ENEMY' is five characters
copyEnemyNameLoop:
        lda     inGameMenuText,y        ; ED6F  'ENEMY' at $CC99
        sta     opponentPlayerName,y    ; ED72  overwrite the blank name
        dey                             ; ED75  previous character
        bpl     copyEnemyNameLoop       ; ED76  copy all five
sendSettingsMessage:
        lda     #$FF                    ; ED78  $FF...
        sta     txPacketBuffer          ; ED7A  ...is the magic first byte of every handshake message
        lda     commBuildId             ; ED7D  commBuildId ($E030): 0 for the modem build, bit 7 set for the trainer build
        sta     txPacketArg1            ; ED80  message byte 1
        lda     gameVersionByte         ; ED83  the game version byte
L_ED86:
        sta     txPacketArg2            ; ED86  message byte 2
        lda     programChecksum         ; ED89  the program checksum computed at boot
        sta     txPacketArg3            ; ED8C  message byte 3
        lda     #$04                    ; ED8F  four bytes...
        sta     txPacketLength          ; ED91  ...is the outgoing message length
        ldx     #$01                    ; ED94  X = 1 = transmit the outgoing message
        jsr     commJumpTable           ; ED96  send it
        jsr     waitForLinkMessage      ; ED99  wait for the peer's reply
        lda     rxPacketLength          ; ED9C  length of the message that came back
        cmp     #$04                    ; ED9F  it must also be four bytes
        bne     abortLinkAndReturnToMenu; EDA1  protocol error - bail out to the main menu
        lda     rxPacketBuffer          ; EDA3  first byte of the reply
L_EDA6:
        cmp     #$FF                    ; EDA6  it must be the $FF magic
        bne     abortLinkAndReturnToMenu; EDA8  protocol error - bail out
        lda     rxPacketArg1            ; EDAA  the peer's comm build id
        sta     peerCommBuildId         ; EDAD  remember it in $E02F
        lda     rxPacketArg3            ; EDB0  the peer's program checksum...
        eor     rxPacketArg2            ; EDB3  ...EOR its version byte...
        sta     selectedUnit            ; EDB6  ...parked in $90F8, which the caller pre-loaded with $DB and compares afterwards to warn about a different build
        ldx     #$02                    ; EDB9  X = 2 = release the received message
        jsr     commJumpTable           ; EDBB  free the receive buffer, then fall into sendEmptyMessage

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
sendEmptyMessage:
        lda     #$00                    ; EDBE  zero length...
        sta     txPacketLength          ; EDC0  ...outgoing message
        ldx     #$01                    ; EDC3  X = 1 = transmit
        jsr     commJumpTable           ; EDC5  send the empty message
L_EDC8:
        rts                             ; EDC8  the handshake is finished


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
exchangeNameHalf:
        stx     restoreNameOffset+1     ; EDC9  park the name offset in the LDX immediate at $EDEB so it survives the blocking wait below
        lda     #$FF                    ; EDCC  $FF...
        sta     txPacketBuffer          ; EDCE  ...the handshake magic goes in message byte 0
        ldy     #$01                    ; EDD1  message bytes 1-5 hold the name characters
copyOwnNameHalfLoop:
        lda     ownPlayerName,x         ; EDD3  character X of our own name
        sta     txPacketBuffer,y        ; EDD6  into the outgoing message
        inx                             ; EDD9  next name character
        iny                             ; EDDA  next message byte
        cpy     #$06                    ; EDDB  six bytes in all
        bcc     copyOwnNameHalfLoop     ; EDDD  copy five characters
        sty     txPacketLength          ; EDDF  Y = 6 = the outgoing message length
        ldx     #$01                    ; EDE2  X = 1 = transmit
        jsr     commJumpTable           ; EDE4  send our half of the name
        jsr     waitForLinkMessage      ; EDE7  block until the peer's half arrives
restoreNameOffset:
        ldx     #$FF                    ; EDEA  operand patched at $EDC9: recover the name offset
        lda     rxPacketLength          ; EDEC  length of the message that came back
        cmp     #$06                    ; EDEF  it must be six bytes
        bne     abortLinkAndReturnToMenu; EDF1  protocol error - bail out to the main menu
        ldy     #$01                    ; EDF3  the payload starts at message byte 1
        lda     rxPacketBuffer          ; EDF5  message byte 0
        cmp     #$FF                    ; EDF8  it must be the $FF magic
        beq     storeOpponentNameHalf   ; EDFA  good - copy the peer's five characters

; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
abortLinkAndReturnToMenu:
        lda     #$03                    ; EDFC  message/sound id 3 - the error beep
        jsr     showStatusMessage       ; EDFE  show it and play its sound immediately, bypassing the message queue
        lda     #$3C                    ; EE01  $3C = 60 frames...
        jsr     waitFrames              ; EE03  ...one second, so the player can read it
        jsr     completeExchange        ; EE06  let the engine finish the exchange it was in the middle of
        jsr     sendEmptyMessage        ; EE09  release the peer with a zero-length message
        jmp     returnToMainMenu        ; EE0C  back to the main menu


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
storeOpponentNameHalf:
        lda     rxPacketBuffer,y        ; EE0F  payload byte of the received message
        sta     opponentPlayerName,x    ; EE12  into the opponent's name
L_EE15:
        inx                             ; EE15  next name character
        iny                             ; EE16  next message byte
        cpy     #$06                    ; EE17  six bytes in the message
L_EE19:
        bcc     storeOpponentNameHalf   ; EE19  copy all five characters
        ldx     #$02                    ; EE1B  X = 2 = release the received message
        jmp     commJumpTable           ; EE1D  free the receive buffer and return through the comm module


; ----------------------------------------------------------------------
; 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)
; ----------------------------------------------------------------------
waitForLinkMessage:
        bit     exchangeFlags           ; EE20  test the exchange flag byte; bit 6 (V) = an exchange has completed
        bvc     waitForLinkMessage      ; EE23  spin until the raster IRQ sets it
        rts                             ; EE25  a message is waiting


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
finaliseRecyclerSetup:
        ldx     #$01                    ; EE26  start with side 1, then side 0
finaliseRecyclerSideLoop:
        lda     recyclerMode,x          ; EE28  how this side's recycler was configured
        cmp     #$02                    ; EE2B  2 = 'none', the setup menu's third choice
sub_EE2D:
        bne     nextRecyclerSide        ; EE2D  this side does have a recycler - leave it alone
        lda     #$FF                    ; EE2F  $FF = 'no position'...
        sta     recyclerCol,x           ; EE31  ...marks the recycler column...
        sta     recyclerRow,x           ; EE34  ...and the row as unset
        cpx     playerSide              ; EE37  is this the local player's own side?
        beq     nextRecyclerSide        ; EE3A  yes - keep the spotted-recycler scan alive
        lda     #$60                    ; EE3C  $60 = the RTS opcode
L_EE3E:
        sta     checkEnemyRecyclerSpotted; EE3E  patch it over the first byte of checkEnemyRecyclerSpotted so the 'SPOTTED RECYCLER!' scan never runs
nextRecyclerSide:
        dex                             ; EE41  next side
        bpl     finaliseRecyclerSideLoop; EE42  loop over sides 1 and 0
        ldy     playerSide              ; EE44  Y = the local player's side
        jmp     drawRecyclerOnMap       ; EE47  draw his recycler onto the battlefield map and return through it


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
loadFilmStartSnapshot:
        lda     #$34                    ; EE4A  film header offset $34...
        sta     filmPtr                 ; EE4C  ...low byte of the film pointer
        lda     #$D0                    ; EE4E  the film buffer lives under the I/O area...
        sta     filmPtrHi               ; EE50  ...so the pointer is $D034, just past the film directory entry
        ldy     #$00                    ; EE52  start of the score/option block
readScoreBlockLoop:
        jsr     readFilmByte            ; EE54  read one byte from under the I/O area
        sta     sideScoreLo,y           ; EE57  into the score and option block at $92A2
        iny                             ; EE5A  next byte
        cpy     #$14                    ; EE5B  $14 = 20 bytes
L_EE5D:
        bcc     readScoreBlockLoop      ; EE5D  loop
        lda     #$48                    ; EE5F  source low byte...
        sta     D_58C3                  ; EE61  ...into copyPageUnderIo's source operand
        lda     #$D0                    ; EE64  source page...
        sta     D_58C4                  ; EE66  ...so the first page comes from $D048
        lda     #$40                    ; EE69  destination low byte...
L_EE6B:
        sta     D_58C6                  ; EE6B  ...into copyPageUnderIo's destination operand
        lda     #$F6                    ; EE6E  destination page...
        sta     D_58C7                  ; EE70  ...so it lands on the unit arrays at $F640
        jsr     copyPageUnderIo         ; EE73  copy 256 bytes with the I/O area banked out
        jsr     getFilmSnapshotDestAddr ; EE76  A/X = $D268, the last page of the snapshot in the film buffer
        sta     D_58C3                  ; EE79  into the source operand low...
        stx     D_58C4                  ; EE7C  ...and high
        jsr     getFilmSnapshotSrcAddr  ; EE7F  A/X = $F860, the last page of the unit arrays
        sta     D_58C6                  ; EE82  into the destination operand low...
sub_EE85:
        stx     D_58C7                  ; EE85  ...and high
        lda     #$02                    ; EE88  three more pages, counted 2, 1, 0...
        sta     scratch19               ; EE8A  ...in zp_19
copySnapshotPageLoop:
        jsr     copyPageUnderIo         ; EE8C  copy one page
        dec     D_58C4                  ; EE8F  walk the source down one page
        dec     D_58C7                  ; EE92  and the destination down one page
L_EE95:
        dec     scratch19               ; EE95  one page fewer
        bpl     copySnapshotPageLoop    ; EE97  loop for three pages
        jsr     rewindFilmPtr           ; EE99  film pointer back to $D000
        ldy     #$30                    ; EE9C  header offset $30
        jsr     readFilmByte            ; EE9E  read the recorded clock value
L_EEA1:
        sta     gameClock               ; EEA1  restore the game clock
        ldy     #$03                    ; EEA4  header offset 3 = high byte of the first-command offset
        jsr     readFilmByte            ; EEA6  read it
        tax                             ; EEA9  keep it in X
        dey                             ; EEAA  header offset 2 = the low byte
        jsr     readFilmByte            ; EEAB  read it
        clc                             ; EEAE  no carry into the addition
L_EEAF:
        adc     #$00                    ; EEAF  the offset is relative to the film buffer, whose low byte is 0
        sta     filmPtr                 ; EEB1  film pointer low
        txa                             ; EEB3  the high half of the offset...
        adc     #$D0                    ; EEB4  ...plus the film buffer page $D0
L_EEB6:
        sta     filmPtrHi               ; EEB6  film pointer high - playback now starts at the first recorded command
        rts                             ; EEB8  done


; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
handleDisplayToggleKey:
        lda     requestedScreenKey      ; EEB9  the function key the IRQ poll last saw: 0 = F1, 1 = F3, 2 = F5, 3 = F7, $FF = none
        beq     beginDisplayToggle      ; EEBB  F1 - toggle the battle display
        rts                             ; EEBD  any other key is not ours

beginDisplayToggle:
        lda     #$3C                    ; EEBE  $3C = 60 frames...
        sta     inputLockoutTimer       ; EEC0  ...of input lockout, so a held key does not repeat

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
announceAndToggleDisplayMode:
        sta     requestedScreenKey      ; EEC3  $3C is not a valid function-key code, so the key stops repeating
        lda     #$28                    ; EEC5  'BEGINNER DISPLAY SET.' low byte...
        ldy     #$EF                    ; EEC7  ...page: the string at $EF28
L_EEC9:
        bit     revealAllUnits          ; EEC9  revealAllUnits: bit 7 set = beginner display, in which every enemy unit is drawn
        bpl     queueDisplaySetMessage  ; EECC  currently standard, so beginner is what is about to be switched on
        clc                             ; EECE  clear carry...
        adc     #$15                    ; EECF  ...+$15 = 21 bytes moves on to 'STANDARD DISPLAY SET.' at $EF3D
        bcc     queueDisplaySetMessage  ; EED1  no page crossing
        iny                             ; EED3  carry into the page
queueDisplaySetMessage:
        ldx     #$21                    ; EED4  message slot $21
        jsr     setMessageSlot          ; EED6  point that slot at the chosen string
        txa                             ; EED9  A = the slot number (setMessageSlot leaves X untouched)
        jsr     queueMessage            ; EEDA  queue it for display, then fall into the toggle itself

; ----------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------
toggleBattleDisplayMode:
        ldy     #$00                    ; EEDD  Y = 0 selects 'BEGINNER' at $EF28
L_EEDF:
        lda     revealAllUnits          ; EEDF  the current display mode
        eor     #$FF                    ; EEE2  flip every bit...
        sta     revealAllUnits          ; EEE4  ...so $00 becomes $FF and back
        bpl     L_EEEB                  ; EEE7  now standard: the prompt should offer BEGINNER, so keep Y = 0
        ldy     #$15                    ; EEE9  now beginner: Y = $15 = 21 selects 'STANDARD' at $EF3D instead
L_EEEB:
        ldx     #$00                    ; EEEB  start of the eight-character hole in the prompt
copyModeNameLoop:
        lda     msgBeginnerDisplaySet,y ; EEED  one character of 'BEGINNER' or 'STANDARD' - the first eight characters of the two 'xxx DISPLAY SET.' strings
        sta     displayModeNameSlot,x   ; EEF0  patch it into the prompt at $EF10
        iny                             ; EEF3  next source character
        inx                             ; EEF4  next destination character
        cpx     #$08                    ; EEF5  eight characters
        bcc     copyModeNameLoop        ; EEF7  copy them all
D_EEF9:
        ldx     #$20                    ; EEF9  message slot $20
        lda     #$08                    ; EEFB  prompt string low byte...
D_EEFD:
        ldy     #$EF                    ; EEFD  ...page: 'F 1 FOR ' at $EF08
        jsr     setMessageSlot          ; EEFF  point slot $20 at it
        txa                             ; EF02  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
        lda     #$20                    ; EF03  message slot $20
L_EF05:
        jmp     queueMessage            ; EF05  queue the prompt and return through queueMessage


; msgDisplayPromptPrefix: text, 8 bytes, 'F 1 FOR ' with no terminator.  First third of a single
; 32-byte string that runs $EF08-$EF27: this prefix, the eight-character mode-name hole at $EF10, and
; ' BATTLE DISPLAY' + the bit-7 terminator '.' at $EF18.  toggleBattleDisplayMode in this file loads
; $EF08 at $EEFB/$EEFD, registers it as message slot $20 with setMessageSlot ($C924) and queues it
; ($EF05), so the status line reads 'F 1 FOR BEGINNER BATTLE DISPLAY.' or 'F 1 FOR STANDARD BATTLE
; DISPLAY.' Solo-trainer text, and setup-phase only.  Both users test isSoloTrainer $0BA5 bit 7 first:
; handleDisplayToggleKey ($EEB9), polled from the menu/idle loop at $600A, and overlay A's call at
; $824B when the game-type screen opens.  $600A is only reached while gamePhase zp_B1 = $FF, the
; pre-battle setup phase.  In a modem game the 74 bytes $EF08-$EF51 are resident but never read; in a
; solo game they are used during setup only, because startGameFromSetup ($0B4F) then replaces
; $EC00-$EFFF with the trainer's own tail from track 35. The L_xxxx / sub_xxxx labels that chop these
; strings into pieces are not part of the data. $EC00-$EFFF is a slot that four different overlays
; occupy at different times - this comm tail (track 34 sectors 12-15), the solo trainer's tail (track
; 35 sectors 12-15), the trainer setup overlay (track 29 sectors 0-3) and the drone-flight AI (track
; 29 sectors 4-5) - and the disassembler shares one label set across all of them, so every branch
; target of those other builds surfaces here in the middle of the text.
msgDisplayPromptPrefix:
        .byte   "F 1 FO"                ; EF08  'F 1 FO' - message slot $20 points here ($EEFB/$EEFD)
L_EF0E:
        .byte   $52                     ; EF0E  'R' - end of 'F 1 FOR'; the split is only the foreign label L_EF0E
L_EF0F:
        .byte   $20                     ; EF0F  the space in front of the eight-character mode name

; displayModeNameSlot: text, 8 bytes.  A hole inside the prompt above, patched at run time by
; copyModeNameLoop ($EEED-$EEF8: LDA msgBeginnerDisplaySet,Y / STA $EF10,X for X = 0..7; XREF lists
; w:EEF0 from this file as the only writer).  Y = 0 copies 'BEGINNER' from $EF28, Y = $15 copies
; 'STANDARD' from $EF3D.  The word written is always the mode F1 will switch to NEXT, because
; toggleBattleDisplayMode chooses it after flipping revealAllUnits $92FC. The shipped bytes are eight
; '*' ($2A).  They can never appear on screen: the only code that queues message slot $20 is the same
; routine that patches them first.
displayModeNameSlot:
        .byte   "********"              ; EF10  '********' - placeholder, always overwritten with BEGINNER or STANDARD before the message is queued

; msgBattleDisplaySuffix: text, 16 bytes, ' BATTLE DISPLAY' followed by '.' with bit 7 set ($AE).
; Tail of the message slot $20 prompt that starts at $EF08; bit 7 on the last character is the string
; terminator every printString ($C133) call looks for.
msgBattleDisplaySuffix:
        .byte   " BATTLE "              ; EF18  ' BATTLE ' - runs straight on from the patched mode name at $EF10
L_EF20:
        .byte   $44,$49,$53             ; EF20  DIS
L_EF23:
        .byte   $50,$4C                 ; EF23  PL
L_EF25:
        .byte   $41                     ; EF25  A
L_EF26:
        .byte   $59,$AE                 ; EF26  Y.

; msgBeginnerDisplaySet: text, 21 bytes, 'BEGINNER DISPLAY SET' + '.'|$80.  Two jobs.  (1)
; announceAndToggleDisplayMode loads it at $EEC5/$EEC7, makes it message slot $21 and queues it, so
; the status line reports the mode that is being switched on - the announcement happens before
; revealAllUnits $92FC is flipped, while the prompt at $EF08 names the other mode.  (2) its first
; eight characters are the source of the 'BEGINNER' patch word copied into $EF10 (XREF: r:EEED from
; this file).
msgBeginnerDisplaySet:
        .byte   "BEG"                   ; EF28  'BEGINNER DISPLAY SET.' - message slot $21; the first 8 bytes double as the patch word
L_EF2B:
        .byte   $49                     ; EF2B  I
L_EF2C:
        .byte   $4E,$4E,$45,$52,$20,$44,$49,$53; EF2C  NNER DIS
        .byte   $50                     ; EF34  P
L_EF35:
        .byte   $4C,$41,$59             ; EF35  LAY
L_EF38:
        .byte   $20,$53,$45,$54,$AE     ; EF38   SET.

; msgStandardDisplaySet: text, 21 bytes, 'STANDARD DISPLAY SET' + '.'|$80.  Never addressed directly:
; both users reach it as msgBeginnerDisplaySet + $15 (ADC #$15 at $EECF for the message pointer, LDY
; #$15 at $EEE9 for the patch word), so the two strings have to stay exactly 21 bytes apart and in
; this order.
msgStandardDisplaySet:
        .byte   "STANDARD DI"           ; EF3D  'STANDARD DISPLAY SET.' = $EF28+$15; its first 8 bytes are the 'STANDARD' patch word
L_EF48:
        .byte   $53,$50,$4C,$41,$59,$20,$53; EF48  SPLAY S
L_EF4F:
        .byte   $45,$54,$AE             ; EF4F  ET.

; Setup-menu label block, $EF52-$EFAE, 93 bytes: ten bit-7 terminated strings printed by the setup,
; option and film menus of overlay A (the map generator / setup build of $6F00-$87FF).  Overlay A
; never names them: it loads each address as two immediates (LDA #low / LDY #$EF) and calls a printer,
; which is why XREF.txt has an entry for only one of them.  Because loadOverlayA chains into
; loadCommTailBuild2 ($1070), this page is present in both modem and solo games, so overlay A can
; print these strings whichever opponent module is loaded.  The survey calls the whole run
; gameOptionLabels; each entry is labelled separately here. msgHandicap: text, 8 bytes, 'HANDICA' +
; 'P'|$80.  Title of the one-row number editor (beginValueEdit, overlay A $84D0) for the DEFENDER
; handicap: 0-50, displayed multiplied by ten as 0-500 points, offered only under CUSTOM rules ($92F3
; bit 7) and only to side 1.
msgHandicap:
        .byte   "H"                     ; EF52  'HANDICAP' - title of the handicap value editor (overlay A $84D0)
L_EF53:
        .byte   $41,$4E,$44,$49         ; EF53  ANDI
L_EF57:
        .byte   $43                     ; EF57  C
L_EF58:
        .byte   $41                     ; EF58  A
L_EF59:
        .byte   $D0                     ; EF59  'P' with bit 7 set = the end-of-string marker

; msgTerrainPts: text, 11 bytes, 'TERRAIN PT' + 'S'|$80.  Title of the TERRAIN PTS value editor
; (overlay A $84F5, editTerrainPointsForSide): a per-side 0-2 setting taken from terrainPointsBySide,
; edited with the joystick and sent to the opponent.
msgTerrainPts:
        .byte   "TERRAIN PT",'S'|$80    ; EF5A  'TERRAIN PTS' - value editor title, overlay A $84F5

; msgDrones: text, 6 bytes, 'DRONE' + 'S'|$80.  Title of the first half of editDronesAndMissiles
; (overlay A $8514): the drone count, 0-9.  Its companion label 'MISSILES' is not here but at $CC9E in
; the text overlay; both numbers are then sent together as command $90.
msgDrones:
        .byte   "DRONE",'S'|$80         ; EF65  'DRONES' - value editor title, overlay A $8514 (0-9)

; msgComcenSpeed: text, 12 bytes, 'COMCEN SPEE' + 'D'|$80.  Title of the COMCEN SPEED editor (overlay
; A $85BF, editComcenSpeed): a class 0-3 sent as command $A5 kind 3 and mapped through the table at
; $78BF into the comcen's actual step rate.
msgComcenSpeed:
        .byte   "CO"                    ; EF6B  'COMCEN SPEED' - value editor title, overlay A $85BF (0-3)
L_EF6D:
        .byte   $4D,$43,$45,$4E,$20,$53,$50,$45; EF6D  MCEN SPE
        .byte   $45,$C4                 ; EF75  E.

; msgMapType: text, 10 bytes, 'MAP TYPE' then $0D and $8D.  $0D is a carriage return
; (handleControlChar $C03D) and $8D is the same code carrying the terminator bit, so printing the
; string leaves the cursor two rows lower and the STANDARD / CUSTOM items land under the title.  Used
; as the menu title by runMapTypeMenu (overlay A $82C7).  Centring counts the two CRs as characters,
; so the title probably sits one column left of exact centre. The only entry of this block with a real
; XREF entry: showCustomSummaryMessage reads it with LDA $EF77,X at $86DF (from game/mapGenerator6F00)
; and copies just the first three characters, masked with AND #$7F, to build the 'CUSTOM:MAP,...'
; status line.
msgMapType:
        .byte   "MAP TYPE",$0D,$8D      ; EF77  'MAP TYPE' + CR + CR|$80 - menu title ($82C7); $86DF also lifts the 'MAP' out of it

; msgMore: text, 8 bytes, ' (MORE)' + CR|$80.  The extra item of the game-film slot menu, shown only
; when a data disk is in the drive (diskIdIndex non-zero): overlay A $7F15 prints it as the eleventh
; line when the menu is built, and $8136 re-prints it when the selector highlights that row.  On the
; game disk the menu stops at ten slots plus ' (EXIT)' ($CAB2).
msgMore:
        .byte   " (MORE)",$8D           ; EF81  ' (MORE)' + CR|$80 - extra film-menu item on a data disk ($7F15/$8136)

; msgLocating: text, 9 bytes, 'LOCATING' + CR|$80.  First line of the two-line banner shown while the
; player positions the recycler: overlay A $83E5 prints it centred and immediately calls
; printNextStringCentred ($83EC), which carries on with whatever string follows in memory -
; msgRecycler.  The two strings therefore have to stay adjacent and in this order.
msgLocating:
        .byte   "LOCATING",$8D          ; EF89  'LOCATING' + CR|$80 - line 1 of the recycler banner ($83E5), continued at $EF92

; msgRecycler: text, 9 bytes, 'RECYCLER' + CR|$80.  Title of the FULL / HALF / NONE recycler menu
; (overlay A $83A0, printed centred above the three words at $0652), and the second line of the
; 'LOCATING RECYCLER' banner when it is reached by fall-through from $EF89.
msgRecycler:
        .byte   "RECYCLER",$8D          ; EF92  'RECYCLER' + CR|$80 - recycler menu title ($83A0), also line 2 of the banner

; msgUnitMenus: text, 12 bytes, 'UNIT MENUS' + $0D + $8D.  Title handed to editOptionBitOnOff (overlay
; A $82AF) with X = mask $08: a two-item OFF / ON menu that clears or sets bit 3 of gameTypeOptions
; $0BA3.  As with 'MAP TYPE' the two carriage returns drop the OFF / ON items two rows below the
; title. The sub_EF9E label in the middle of the string is not a routine of this build: it is the
; entry point droneDistanceToCell of the drone-AI overlay that occupies this page in a solo game, and
; the disassembler's label set is shared between the two.
msgUnitMenus:
        .byte   "UNI"                   ; EF9B  'UNIT MENUS' + CR + CR|$80 - OFF/ON menu for gameTypeOptions bit 3 ($82AF)
sub_EF9E:
        .byte   $54,$20,$4D,$45,$4E,$55,$53,$0D; EF9E  T MENUS.
        .byte   $8D                     ; EFA6  .

; msgDamage: text, 8 bytes, 'DAMAGE' + $0D + $8D.  The second CUSTOM sub-menu, same treatment at
; overlay A $82B8 with X = mask $10 = bit 4 of gameTypeOptions $0BA3.  This is the last entry of the
; label block that began at $EF52.
msgDamage:
        .byte   "DAMAGE",$0D,$8D        ; EFA7  'DAMAGE' + CR + CR|$80 - OFF/ON menu for gameTypeOptions bit 4 ($82B8)

; msgInsertDiskTemplate: text, 33 bytes, 'INSERT DATA DISK AND PRESS SPACE' + '.'|$80, occupying
; $EFAF-$EFCF.  promptInsertDiskAndWaitSpace (overlay A $81B6) registers it as message slot $1E at
; $81BE/$81C0 and leaves it as the persistent status message while the game-film save / load flow
; waits for the space bar.  The four characters at $EFB6 are a patch hole, so the same template serves
; both prompts.
msgInsertDiskTemplate:
        .byte   "INSE"                  ; EFAF  'INSERT xxxx DISK AND PRESS SPACE.' - message slot $1E (overlay A $81BE)
L_EFB3:
        .byte   $52,$54,$20             ; EFB3  RT 

; diskNameSlot: the four characters 'DATA' inside the template above are a hole, not text of their
; own.  promptInsertDiskAndWaitSpace copies a four-character disk name over them with a self-modified
; LDA / STA pair at overlay A $81C7-$81CA (this is the XREF entry w:81CA from game/mapGenerator6F00):
; 'DATA' ($EFD0) via promptInsertDataDisk ($81B2) or 'GAME' ($EFD4) via promptInsertGameDisk ($81A5).
; The image on disk holds 'DATA', the same four bytes as $EFD0, so the unpatched template already
; reads correctly.
diskNameSlot:
        .byte   $44,$41,$54,$41         ; EFB6  DATA
        .byte   $20                     ; EFBA  the fixed part of the template resumes here: ' DISK AND PRESS SPACE.'
        .byte   $44,$49,$53,$4B,$20,$41,$4E,$44; EFBB  DISK AND
        .byte   $20,$50,$52,$45,$53,$53,$20,$53; EFC3   PRESS S
        .byte   $50                     ; EFCB  P
L_EFCC:
        .byte   $41                     ; EFCC  'A' of SPACE - the last byte of this page that differs between the four $EC00 overlays

; msgInsertDiskTail: 'CE' + '.'|$80, the last three characters of the message above (a separate block
; only because the other builds of this page have a branch target at $EFCC). From here to $EFFE the
; page is shared.  $EFCD-$EFFE is byte-for-byte identical in all four images that can occupy
; $EC00-$EFFF - track 34 s12-15 (this comm tail), track 35 s12-15 (trainer tail), track 29 s0-3
; (trainer setup) and track 29 s4-5 (drone AI); only $EFFF differs, $00 here and $FF in the other
; three.  Evidence: a byte comparison of the four decrypted sector images.  The strings from $EFCD on
; were therefore assembled into the tail of every build, which is why overlay A can print them no
; matter which module is resident.
msgInsertDiskTail:
        .byte   "CE",'.'|$80            ; EFCD  'CE.' - end of 'INSERT xxxx DISK AND PRESS SPACE.'; from here on the page is common to all four builds

; wordData: text, 4 bytes, 'DATA' with no terminator - a patch word, never printed on its own.
; promptInsertDataDisk (overlay A $81B2) passes $EFD0 in A/Y and the four bytes are copied into the
; hole at $EFB6.
wordData:
        .byte   "DATA"                  ; EFD0  'DATA' - patch word for $EFB6 (overlay A $81B2)

; wordGame: text, 4 bytes, 'GAME' with no terminator - the other patch word for $EFB6, passed by
; promptInsertGameDisk (overlay A $81A5) before it waits for the OZ game disk to be put back in the
; drive.
wordGame:
        .byte   "GAME"                  ; EFD4  'GAME' - patch word for $EFB6 (overlay A $81A5)

; msgCustomOff: text, 10 bytes, 'CUSTOM OF' + 'F'|$80.  beginGameTypeSelection (overlay A $8218)
; registers it as message slot $1E for the GAME TYPE screen and $821F/$8224 point the store operand at
; $869A/$869B at $EFDF, the 'OFF' inside it.  Every time the player presses '*',
; showToggleStateMessage ($8673) copies three bytes over $EFDF-$EFE1 - 'OFF' from $064D or 'ON'|$80
; plus the stray 'F' that follows it at $0650 - so the same slot reads 'CUSTOM ON' or 'CUSTOM OFF' and
; mirrors gameTypeOptions $0BA3 bit 6 (CUSTOM rules).
msgCustomOff:
        .byte   "CUSTOM OF",'F'|$80     ; EFD8  'CUSTOM OFF' - message slot $1E; $EFDF-$EFE1 is rewritten with ON/OFF by the store at $8699

; msgDestroyOff: text, 11 bytes, 'DESTROY OF' + 'F'|$80.  The same machinery for the unit placement
; phase: setDestroyModeMessage (overlay A $86B1) makes it message slot $1E and $86B8 aims the ON/OFF
; store at $EFEA, so '*' switches the line between 'DESTROY OFF' and 'DESTROY ON' while the off state
; also restores persistent message $0A ('SETUP UNITS. PRESS F 1 WHEN DONE.').
msgDestroyOff:
        .byte   "DESTROY OF",'F'|$80    ; EFE2  'DESTROY OFF' - message slot $1E; $EFEA-$EFEC is the ON/OFF hole

; msgAccepted: text, 8 bytes, 'ACCEPTE' + 'D'|$80.  Printed centred on text row 15 by acceptCurrentMap
; (overlay A $7708-$7714) when the player accepts the battlefield currently on screen.  Last string of
; the block.
msgAccepted:
        .byte   "ACCEPTE",'D'|$80       ; EFED  'ACCEPTED' - printed centred by overlay A $7710

; commTailFiller: 11 unused bytes, $EFF5-$EFFF - $C0,$DF then $00,$FF repeated four times and a final
; $00.  Nothing points at them: XREF.txt has no entry for any address in the range, no immediate pair
; in this file or in overlay A forms an address inside it, and they sit behind the last string of the
; block, so this is padding left at the end of track 34 sector 15.  The same 11 bytes end the trainer
; tail, the trainer setup overlay and the drone-AI overlay, except that their last byte is $FF.
commTailFiller:
        .byte   $C0                     ; EFF5  $C0,$DF then alternating $00/$FF - unreferenced padding at the end of the sector
        .byte   $DF,$00,$FF,$00,$FF,$00,$FF; EFF6  .......
        .byte   $00                     ; EFFD  $EFFF is $00 in this track 34 image but $FF in the track 35 and track 29 images of the page
        .byte   $FF,$00                 ; EFFE  ..
