modemwars/swiftlink/swiftlinkDriverE000.s
2026-08-23 02:09:40 -05:00

4662 lines
328 KiB
ArmAsm

; ============================================================================
; $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 ..