modemwars/disassembly/game/modemDriverE000.s
2026-08-23 02:09:40 -05:00

4219 lines
286 KiB
ArmAsm

; ============================================================================
; $E000-$EFFF - modem driver: bit-banged RS-232, Hayes control and the packet protocol (track 18 sector 7 + track 34 sectors 1-15)
; ============================================================================
; Loaded by $0F31 for a modem game ($0BA5 bit 7 clear). Three layers: a software UART on the user port
; driven from the CIA2 NMI (300/1200 baud), 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.
.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
D_DF59 := $DF59
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.
; $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 copies the three-byte baud entry at baudParameterTable+X into
; bitPeriodLo/bitPeriodHi/txPaceReload.
; $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.
; $E403 countLinkError the framed link-error hook: count one bad character.
; $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 set CIA2 up for the user-port serial link: timer A latch $0003 with CRA = $51
; (timer A running, serial port in output mode), a fixed bit pattern into the serial data register, DDRB = $26 so PB1
; (RTS), PB2 (DTR) and PB5 are outputs, both handshake lines asserted, and finally PA2 (TXD) raised to the idle mark
; level.
; $E574 dropDtrLine write $22 to CIA2_PRB, clearing PB2 (DTR) while leaving PB1 (RTS) and PB5 high.
; $E57B installCommNmiVector point the RAM NMI vector at $FFFA/$FFFB at this module's bit-bang UART handler,
; so every CIA2 interrupt lands in commNmiHandler.
; $E586 restartUart (re)start the software UART: clear the six bit-level state bytes, ask the modem
; state machine to re-evaluate, prime the carrier sample timer, and fall into setCiaNmiMask with $92 = enable the CIA2
; FLAG line (RXD start bit) and timer B (transmit clock) as NMI sources.
; $E593 setCiaNmiMask write a CIA2 interrupt-control command byte ($92 = FLAG+TB on, $83 = TA+TB on,
; $7F = everything off), acknowledging whatever was pending and stopping both timers first.
; $E5B6 clearUartState zeroes the six bytes at $E5BB-$E5C0 that hold the entire state of the software
; UART: transmitter busy flag, bit-count reload, transmit bit counter, transmit shift register, receive bit counter and
; receive shift register.
; $E5C1 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.
; $E5C3 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.
; $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 start shifting the next character out of the software UART.
; $E654 setBitPeriodFull load A with the high byte of the current bit period and fall into setBitPeriod,
; i.e.
; $E657 setBitPeriod write A:bitPeriodLo into the CIA2 timer latch selected by X (0 = timer A at
; $DD04/$DD05, 2 = timer B at $DD06/$DD07) and return A = $11 = 'force load + start' for the caller to put into CRA or
; CRB.
; $E663 nmiStartNextChar the timer B path of the NMI once the bit counter has run out.
; $E685 commNmiHandler the module's CIA2 NMI handler, installed at $FFFA/$FFFB by
; installCommNmiVector.
; $E69C nmiMergeIcrFlags the two-byte continuation of the timer B race workaround: OR in any interrupt
; flags that arrived between the handler's two reads of the interrupt register, so nothing is lost.
; $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 and counts it in
; $E0A5, the number of characters the NMI transmitter still owes.
; $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 currently wants ($92 idle, $83 while receiving, $7F when suspended)
; ----------------------------------------------------------------------
; 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: counts frames between transmitted characters
carrierSampleTimer:
.byte $44 ; E045 carrierSampleTimer: zeroed by the NMI on every good character, 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
dtrToggleMask:
.byte $24 ; E04C $24 = the CIA2_PRB bits the C=+H hot key toggles: PB2 (DTR) and PB5
userPortDdrbValue:
.byte $26 ; E04D $26 = CIA2_DDRB: PB1 (RTS), PB2 (DTR) and PB5 are outputs, everything else an input
userPortDtrDropped:
.byte $22 ; E04E $22 = CIA2_PRB with DTR (PB2) low: hang up
userPortIdleTable:
.byte $26,$26 ; E04F && userPortIdleTable, indexed by isOriginateMode: both entries $26 = RTS, DTR and PB5 asserted
serialShiftPatternTable:
.byte $27,$2F,$3F ; E051 '/? serialShiftPatternTable entries 0-2, indexed by isOriginateMode EOR baudIndex: $27 answer/300, $2F originate/300, $3F originate/1200
serialShiftPatternAnswer1200:
.byte $37 ; E054 serialShiftPatternTable entry 3: $37 = answer at 1200 baud
baudIndex:
.byte $00 ; E055 baudIndex: 0 = 300 baud, 3 = 1200 baud (also the stride into baudParameterTable)
bitPeriodLo:
.byte $50 ; E056 bitPeriodLo: low byte of the CIA timer latch for one bit time
bitPeriodHi:
.byte $0D ; E057 bitPeriodHi: high byte of the bit period; $0D50 = 3408 cycles = 300 baud
txPaceReload:
.byte $01 ; E058 txPaceReload: frames between transmitted characters (1 at 300 baud, 2 at 1200)
; baudParameterTable: two 3-byte entries selected by baudIndex (0 or 3). Each entry is {bit period
; low, bit period high, transmit pacing}. $0D50 = 3408 cycles and $0353 = 851 cycles, which on an NTSC
; C64 (1022727 Hz) give 300.1 and 1201.8 baud.
baudParameterTable:
.byte $50,$0D,$01 ; E059 P.. baudParameterTable: two 3-byte entries {periodLo, periodHi, pacing} - $0D50/1 = 300 baud, $0353/2 = 1200 baud
.byte $53,$03,$02 ; E05C S..
; 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, clearUartState $E5B8, 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, 3 and 1
; pick 300/1200 baud, 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 dtrToggleMask ; E327 $24 = PB2 (DTR) + PB5
eor CIA2_PRB ; E32A flip those two user-port outputs
sta CIA2_PRB ; E32D write the toggled pattern back to the user port
bcs L_E33F ; E330 carry is still set from the CPX - swallow the key
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
L_E342:
cmp #$B3 ; E342 C=+3
bne L_E34A ; E344 not that key - try the next
ldx #$00 ; E346 baud table entry 0 = 300 baud
beq L_E350 ; E348 always taken
L_E34A:
cmp #$B1 ; E34A C=+1
bne L_E362 ; E34C not a baud key - try the rest
ldx #$03 ; E34E baud table entry 1 (offset 3) = 1200 baud
L_E350:
stx baudIndex ; E350 remember the speed for the next open
; ----------------------------------------------------------------------
; loadBaudParameters - copies the three-byte baud entry at baudParameterTable+X into
; bitPeriodLo/bitPeriodHi/txPaceReload. The destination walk uses Y = $FD..$FF against the base $DF59,
; because $DF59+$FD = $E056, so one register both counts and indexes. X=0 selects $0D50 cycles = 300
; baud with pacing 1, X=3 selects $0353 = 1200 baud with pacing 2. Returns A=$FF, which is what the
; hot-key path needs to swallow the key.
; In: X = 0 (300 baud) or 3 (1200 baud)
; Out: bitPeriodLo/bitPeriodHi/txPaceReload, A = $FF
; Called from: openCommLink $E2C9, the C=+3 / C=+1 hot keys (fall-through from $E350),
; runModemStateMachine $E7B2 when it reads a 'CONNECT 1200' result code
; ----------------------------------------------------------------------
loadBaudParameters:
ldy #$FD ; E353 Y counts $FD, $FE, $FF so that $DF59+Y walks $E056, $E057, $E058
L_E355:
lda baudParameterTable,x ; E355 next byte of the selected baud entry
sta D_DF59,y ; E358 $DF59+Y = bitPeriodLo / bitPeriodHi / txPaceReload
inx ; E35B next source byte
iny ; E35C next destination byte
bne L_E355 ; E35D three bytes copied when Y wraps to 0
L_E35F:
lda #$FF ; E35F swallow the hot key
rts ; E361 return
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 $E724
; ----------------------------------------------------------------------
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 $08 ; E3C7 error, raw: the no-op stub (never selected)
L_E3C8:
.byte $E4 ; E3C8 .
.byte $03 ; E3C9 error, framed: count it in linkErrorCount
.byte $E4 ; 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.
; In: none
; Out: $E3B7 = $C1, $E3BA = $C5, $E3BD = $C9
; 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
rts ; E402 return
; ----------------------------------------------------------------------
; 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.
; In: none
; Out: linkErrorCount +1
; Called from: reportLinkError $E3BC through vector $E3C9
; ----------------------------------------------------------------------
countLinkError:
nop ; E403 (padding so the two hooks are three bytes apart)
inc linkErrorCount ; E404 count one bad character; the link state machine restarts byte sync when this is non-zero
rts ; E407 return
; ignoreLinkError - the raw-mode link-error hook: PHP / PHA / NOP / PLA / PLP / RTS, a no-op that
; preserves A and the flags, followed by a stray second RTS at $E40E. Dead in the shipped game because
; initCommModule always selects countLinkError instead. The disassembler still renders it as data.
; In: none
; Out: nothing changed
; Called from: reportLinkError $E3BC through vector $E3C7 (never selected)
ignoreLinkError:
.byte $08 ; E408 PHP / PHA / NOP / PLA / PLP / RTS - the do-nothing error hook, plus a stray RTS at $E40E
.byte $48,$EA,$68,$28,$60,$60 ; E409 H.h(``
; ----------------------------------------------------------------------
; 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: startNextTxChar $E62D, 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 $E71F (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 - set CIA2 up for the user-port serial link: timer A latch $0003 with CRA =
; $51 (timer A running, serial port in output mode), a fixed bit pattern into the serial data
; register, DDRB = $26 so PB1 (RTS), PB2 (DTR) and PB5 are outputs, both handshake lines asserted, and
; finally PA2 (TXD) raised to the idle mark level. What the serial-shift-register write is for is
; unclear - SP2 is user-port pin 7 and is not part of the standard RS-232 wiring, so it may drive a
; modem control input or be vestigial.
; In: isOriginateMode, baudIndex, userPortDdrbValue, userPortIdleTable, serialShiftPatternTable
; Out: CIA2_CRA, CIA2 timer A latch, CIA2_SDR, CIA2_DDRB, CIA2_PRB, CIA2_PRA bit 2
; Called from: openCommLink $E2D5, the C=+A / C=+O hot keys at $E387
; ----------------------------------------------------------------------
configureUserPortLines:
lda #$00 ; E540 timer A high byte = 0
sta CIA2_TA_HI ; E542 stop timer A while its latch is reprogrammed
lda #$03 ; E545 latch $0003: the serial shift register runs at about 128 kHz
sta CIA2_TA_LO ; E547 latch the fast shift-register clock
ldx #$51 ; E54A $51 = timer A running in continuous mode with the serial port in OUTPUT mode
L_E54C:
stx CIA2_CRA ; E54C start timer A with the serial port in output mode
and isOriginateMode ; E54F A is still $03: keep the low two bits of the mode flag
eor baudIndex ; E552 index = isOriginateMode EOR baudIndex (0..3)
tax ; E555 into X to index the pattern table
lda serialShiftPatternTable,x; E556 $27/$2F/$3F/$37 depending on mode and speed
sta CIA2_SDR ; E559 shifted out of SP2 (user-port pin 7); its purpose is unclear - possibly a modem control input, possibly vestigial
lda userPortDdrbValue ; E55C $26: PB1 (RTS), PB2 (DTR) and PB5 are outputs
sta CIA2_DDRB ; E55F set the user-port data direction
L_E562:
ldx isOriginateMode ; E562 answer or originate
lda userPortIdleTable,x ; E565 both entries are $26
sta CIA2_PRB ; E568 assert RTS, DTR and PB5
L_E56B:
lda #$04 ; E56B PA2 is TXD
ora CIA2_PRA ; E56D drive it high, the RS-232 idle (mark) level
sta CIA2_PRA ; E570 leave the line idling at mark
rts ; E573 return
; ----------------------------------------------------------------------
; dropDtrLine - write $22 to CIA2_PRB, clearing PB2 (DTR) while leaving PB1 (RTS) and PB5 high.
; Dropping DTR is what makes a Hayes modem let go of the line.
; In: userPortDtrDropped
; Out: CIA2_PRB
; Called from: hangUpModem $E38D (the only caller)
; ----------------------------------------------------------------------
dropDtrLine:
lda userPortDtrDropped ; E574 $22 = the same port pattern with DTR (PB2) low
sta CIA2_PRB ; E577 dropping DTR terminates the call
rts ; E57A return
; ----------------------------------------------------------------------
; installCommNmiVector - point the RAM NMI vector at $FFFA/$FFFB at this module's bit-bang UART
; handler, so every CIA2 interrupt lands in commNmiHandler.
; 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 software UART: clear the six bit-level state bytes, ask the modem state
; machine to re-evaluate, prime the carrier sample timer, and fall into setCiaNmiMask with $92 =
; enable the CIA2 FLAG line (RXD start bit) and timer B (transmit clock) as NMI sources.
; In: none
; Out: $E5BB-$E5C0 = 0, uartRestartRequest = 1, carrierSampleTimer = $92, CIA2 NMI sources armed,
; nmiSuspendRequest/Ack = 0
; Called from: openCommLink $E2CF, serviceCarrierAndSuspendRequest $E5E1 (resume after a disk load)
; ----------------------------------------------------------------------
restartUart:
jsr clearUartState ; E586 zero the six bit-level UART variables
lda #$01 ; E589 1 = re-evaluate (only $FF forces the phase back to 0)
L_E58B:
sta uartRestartRequest ; E58B ask the modem state machine to re-evaluate the connection
lda #$92 ; E58E $92 = set FLAG (RXD start bit) and timer B (transmit clock) as NMI sources
sta carrierSampleTimer ; E590 prime the carrier sample timer with the same negative value
; ----------------------------------------------------------------------
; setCiaNmiMask - write a CIA2 interrupt-control command byte ($92 = FLAG+TB on, $83 = TA+TB on, $7F =
; everything off), acknowledging whatever was pending and stopping both timers first. It retries if an
; NMI changed the shadow underneath it. The suspend request and acknowledge bytes are then both set
; from the mask ($00 for an enable mask, $80 for the disable mask), and the routine exits through the
; 'raise TXD' tail of configureUserPortLines.
; In: A = ICR command byte
; Out: ciaIcrShadow, CIA2_ICR/CRA/CRB, nmiSuspendRequest, nmiSuspendAck, CIA2_PRA bit 2
; Called from: restartUart (fall-through from $E590), stopCommNmi $E5CA
; ----------------------------------------------------------------------
setCiaNmiMask:
sta ciaIcrShadow ; E593 remember which sources we want
bit CIA2_ICR ; E596 read the interrupt register to clear anything pending
ldx #$00 ; E599 0 stops a timer when written to its control register
sta CIA2_ICR ; E59B arm or disarm the sources
stx CIA2_CRA ; E59E stop timer A
stx CIA2_CRB ; E5A1 stop timer B
cmp ciaIcrShadow ; E5A4 did an NMI change the shadow while we were writing?
bcc setCiaNmiMask ; E5A7 yes (it asked for more sources than we did) - do it again
eor #$FF ; E5A9 $92/$83 -> 0, $7F -> $80
and #$80 ; E5AB keep only the sign bit
sta nmiSuspendRequest ; E5AD so a disable mask marks the module suspended...
sta nmiSuspendAck ; E5B0 ...and acknowledges it in the same breath
jmp L_E56B ; E5B3 exit through the 'raise TXD to mark' tail
; ----------------------------------------------------------------------
; clearUartState - zeroes the six bytes at $E5BB-$E5C0 that hold the entire state of the software
; UART: transmitter busy flag, bit-count reload, transmit bit counter, transmit shift register,
; receive bit counter and receive shift register.
; In: none
; Out: $E5BB-$E5C0 = 0; returns to the caller of clearUartState
; Called from: restartUart $E586
; ----------------------------------------------------------------------
clearUartState:
ldx #$06 ; E5B6 6 bytes to clear: the whole software-UART bit state
jsr clearInlineVarBlock ; E5B8 clear the inline block that follows and return to our caller's caller
; ----------------------------------------------------------------------
; Software UART bit state, $E5BB-$E5C0 - the inline block clearUartState zeroes. Everything the NMI
; needs to shift one character in and one character out.
; ----------------------------------------------------------------------
txCharActive:
brk ; E5BB txCharActive: non-zero while a character is being shifted out (rendered as BRK - data, never executed)
txBitCountReload:
.byte $00 ; E5BC txBitCountReload: 9 = 8 data bits plus the stop bit
txBitCounter:
.byte $00 ; E5BD txBitCounter: bits left in the character being sent
txShiftRegister:
.byte $00 ; E5BE txShiftRegister: the character being shifted out, LSB first
rxBitCounter:
.byte $00 ; E5BF rxBitCounter: negative = waiting for the start-bit sample, 9..1 = data bits, 0 = stop bit
rxShiftRegister:
.byte $00 ; E5C0 rxShiftRegister: the character being shifted in
; ----------------------------------------------------------------------
; 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, samples carrier detect on CIA2_PRB bit 4 through the polarity/override byte and leaves
; $F8 (carrier) or $80 (no carrier) in linkStatusSample. Bit 7 of the sample is only kept when a
; character has arrived recently, so it doubles as a 'data is flowing' indicator.
; In: nmiSuspendRequest, nmiSuspendAck, isLinkActive, carrierOverrideFlags, CIA2_PRB,
; carrierSampleTimer
; Out: linkStatusSample, carrierSampleTimer, the NMI UART 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 NMI off
bcc L_E5E4 ; E5DF no request and not suspended - carry on
jsr restartUart ; E5E1 no request but still suspended - bring the UART back
L_E5E4:
lda isLinkActive ; E5E4 is the link up at all?
beq L_E662 ; E5E7 no - nothing to sample
lda carrierOverrideFlags ; E5E9 carrier polarity / override
bmi L_E604 ; E5EC bit 7 would force the sample byte outright (never set by any code here)
asl a ; E5EE bit 6 into bit 7
bmi L_E5F8 ; E5EF the C=+C override is on - report carrier without looking at the line
eor CIA2_PRB ; E5F1 read the user port through the polarity mask
and #$10 ; E5F4 PB4 = DCD
adc #$F0 ; E5F6 DCD high -> A=$00 C=1, DCD low (carrier) -> A=$F0 C=0
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 good character
bpl L_E604 ; E5FD positive means characters have arrived recently - keep bit 7 set
; ----------------------------------------------------------------------
; 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 - start shifting the next character out of the software UART. It gives up at once
; when timer B is already running or a character is still in flight; outside terminal mode it also
; paces characters with txPaceCounter and refuses to transmit without carrier. Otherwise it pops a
; byte into the shift register, arms the 9-bit counter, programs timer B with a deliberately short
; first interval and starts it, then falls into setBitPeriodFull so every following underflow is
; exactly one bit time.
; In: connectionPhase, txPaceCounter, linkStatus/linkStatusSample, uartPendingCount, the UART
; transmit ring
; Out: txCharActive, txShiftRegister, txBitCountReload, txPaceCounter, CIA2 timer B and CRB
; Called from: nmiStartNextChar $E673, and by fall-through from the carrier sampler at $E604 (so it is
; retried once per frame as well as from the NMI)
; ----------------------------------------------------------------------
startNextTxChar:
lda CIA2_CRB ; E607 is timer B still running?
ora txCharActive ; E60A or a character still in flight?
lsr a ; E60D CRB bit 0 = timer B started
bcs L_E662 ; E60E busy - come back later
lda #$01 ; E610 phase 1 = modem command / terminal mode
cmp connectionPhase ; E612 which phase are we in?
beq L_E628 ; E615 in terminal mode characters go out unpaced and without carrier
dec txPaceCounter ; E617 otherwise pace them
bne L_E662 ; E61A not this frame
sta txPaceCounter ; E61C reload with 1 so it retries next frame
lda linkStatusSample ; E61F the live carrier sample
and linkStatus ; E622 AND the debounced status
asl a ; E625 bit 6 (carrier) into bit 7
bpl L_E662 ; E626 no carrier - do not transmit into a dead line
; ----------------------------------------------------------------------
; Fetch the next character and set the software UART shifting: 9 bit times out of timer B, with a
; deliberately short first interval so the start bit follows quickly.
; ----------------------------------------------------------------------
L_E628:
lda uartPendingCount ; E628 anything queued?
beq L_E662 ; E62B no
jsr popUartTxRing ; E62D take the next character
bcc L_E639 ; E630 got one
lda #$00 ; E632 the ring was empty although the count said otherwise
sta uartPendingCount ; E634 resynchronise the count
bcs L_E662 ; E637 always taken
L_E639:
inc txCharActive ; E639 mark the transmitter busy
sta txShiftRegister ; E63C load the shift register
lda #$09 ; E63F 8 data bits plus the stop bit
sta txBitCountReload ; E641 arm the transmit bit counter for the first character
lda txPaceReload ; E644 1 at 300 baud, 2 at 1200
sta txPaceCounter ; E647 reload the pacing counter
lda #$01 ; E64A $01 : bitPeriodLo = a short first interval
ldx #$02 ; E64C X=2 selects timer B, the transmit clock
jsr setBitPeriod ; E64E program the latch and get $11 back
sta CIA2_CRB ; E651 $11 = force load + start timer B; its first underflow will emit the start bit
; ----------------------------------------------------------------------
; setBitPeriodFull - load A with the high byte of the current bit period and fall into setBitPeriod,
; i.e. program the selected timer with the full one-bit interval.
; In: X = 0 for timer A (receive clock) or 2 for timer B (transmit clock), bitPeriodLo/bitPeriodHi
; Out: the CIA2 timer latch; A = $11 for the caller to store into the control register
; Called from: commNmiHandler $E6EC (after the half-bit start-bit delay)
; ----------------------------------------------------------------------
setBitPeriodFull:
lda bitPeriodHi ; E654 high byte of one whole bit time
; ----------------------------------------------------------------------
; setBitPeriod - write A:bitPeriodLo into the CIA2 timer latch selected by X (0 = timer A at
; $DD04/$DD05, 2 = timer B at $DD06/$DD07) and return A = $11 = 'force load + start' for the caller to
; put into CRA or CRB.
; In: A = high byte of the interval, X = 0 or 2, bitPeriodLo
; Out: CIA2 timer latch, A = $11
; Called from: startNextTxChar $E64E, commNmiHandler $E6E6, setBitPeriodFull (fall-through)
; ----------------------------------------------------------------------
setBitPeriod:
sta CIA2_TA_HI,x ; E657 X=0 -> $DD04/$DD05 (timer A), X=2 -> $DD06/$DD07 (timer B)
lda bitPeriodLo ; E65A the low byte is always the full one
sta CIA2_TA_LO,x ; E65D low byte of the timer latch
lda #$11 ; E660 $11 = force load + start, for the caller to store into CRA/CRB
L_E662:
rts ; E662 return
; ----------------------------------------------------------------------
; nmiStartNextChar - the timer B path of the NMI once the bit counter has run out. If a character is
; active the counter is reloaded and TXD is driven low to emit its start bit; if not, the transmitter
; tries to fetch the next byte.
; In: txCharActive, txBitCountReload, CIA2_PRA
; Out: txBitCounter, CIA2_PRA bit 2 (TXD)
; Called from: commNmiHandler $E6A7
; ----------------------------------------------------------------------
nmiStartNextChar:
lda txCharActive ; E663 is a character being sent?
beq L_E673 ; E666 no - try to fetch the next one
lda txBitCountReload ; E668 9 bits to go
sta txBitCounter ; E66B arm the bit counter for this character
lda CIA2_PRA ; E66E PA2 is TXD; PRA is never 0 (the VIC bank bits are in it)
bne L_E6C3 ; E671 so this is an unconditional jump to 'drive TXD low' = the start bit
L_E673:
jsr startNextTxChar ; E673 nothing in flight - start the next character if there is one
clc ; E676 force the branch below
bcc L_E6C8 ; E677 always taken: skip the transmit-bit code
L_E679:
lda ciaIcrShadow ; E679 not our interrupt: restore the sources we want
sta CIA2_ICR ; E67C put our own interrupt mask back
pla ; E67F restore X
tax ; E680 the saved X
pla ; E681 and A
jmp (nmiChainVector) ; E682 chain to the game's NMI handler ($1298, a bare RTI)
; ----------------------------------------------------------------------
; commNmiHandler - the module's CIA2 NMI handler, installed at $FFFA/$FFFB by installCommNmiVector. It
; saves A and X, masks CIA2 interrupts, reads the interrupt register with a workaround for timer B
; underflowing between the two reads, and chains to the game's NMI vector when the interrupt was not
; CIA2's. Timer B clocks the software transmitter one bit at a time out of txShiftRegister onto PA2
; (TXD); the CIA2 FLAG line (the RXD start-bit edge) arms timer A at half a bit time and then at a
; full bit time, and each timer A interrupt shifts CIA2_PRB bit 0 (RXD) into rxShiftRegister until a
; whole character with a good stop bit reaches the UART receive ring. Framing errors, bad stop bits
; and receive overflow all go through reportLinkError.
; In: CIA2_ICR, CIA2_PRB bit 0, ciaIcrShadow, $E5BB-$E5C0
; Out: CIA2_PRA bit 2, the UART receive ring, uartPendingCount, ciaIcrShadow, carrierSampleTimer, the
; $EB00 stall counter; 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 #$7F ; E688 $7F = disable every CIA2 source while we work
nmiMaskCiaSources:
sta CIA2_ICR ; E68A no CIA2 interrupt may fire while we shift bits
ldx CIA2_TB_HI ; E68D sample timer B's high byte before reading the flags
lda CIA2_ICR ; E690 reading the interrupt register also clears it
nmiTestCiaSource:
bpl L_E679 ; E693 bit 7 clear: this NMI did not come from CIA2 (RESTORE key)
cpx CIA2_TB_HI ; E695 did timer B wrap between the two reads?
bcs L_E69F ; E698 no - the flags we read are complete
ora #$02 ; E69A yes - add the timer B flag by hand
; ----------------------------------------------------------------------
; nmiMergeIcrFlags - the two-byte continuation of the timer B race workaround: OR in any interrupt
; flags that arrived between the handler's two reads of the interrupt register, so nothing is lost.
; In: A = flags read so far
; Out: A = flags including the late arrivals
; Called from: fall-through from $E69A in this build
; ----------------------------------------------------------------------
nmiMergeIcrFlags:
ora CIA2_ICR ; E69C and merge anything else that arrived meanwhile
; ----------------------------------------------------------------------
; Transmit half: one timer B underflow = one bit time. The bit counter runs 9, 8 ... 1, 0 and then
; goes negative, at which point the next character (and its start bit) is set up.
; ----------------------------------------------------------------------
L_E69F:
pha ; E69F keep the flag byte for the receive half
and #$02 ; E6A0 timer B = the transmit bit clock
L_E6A2:
beq L_E6C8 ; E6A2 not a transmit tick - go and look at the receiver
dec txBitCounter ; E6A4 one bit of this character done
bmi nmiStartNextChar ; E6A7 the whole character has gone - start the next one
bne L_E6B8 ; E6A9 still data bits to send
lda #$09 ; E6AB $09 = START | one-shot - this does not stop timer B, and bit 4 (force load) is clear
sta CIA2_CRB ; E6AD run exactly one more bit period, the stop bit, after which the CIA clears START itself; that final underflow drives txBitCounter negative into nmiStartNextChar, which is how characters chain back to back without waiting for the frame tick
dec uartPendingCount ; E6B0 one fewer character pending
lda #$00 ; E6B3 clear the transmit-busy flag
sta txCharActive ; E6B5 the transmitter is idle again
L_E6B8:
sec ; E6B8 shift a mark bit in behind the data
ror txShiftRegister ; E6B9 next bit of the character into carry, LSB first
lda CIA2_PRA ; E6BC read the port so the other bits survive
ora #$04 ; E6BF a 1 bit means TXD high (mark)
bcs L_E6C5 ; E6C1 the shifted-out bit was a 1
L_E6C3:
and #$FB ; E6C3 a 0 bit means TXD low (space)
L_E6C5:
sta CIA2_PRA ; E6C5 drive PA2 with this bit
; ----------------------------------------------------------------------
; Receive half: either the FLAG line has seen the falling edge of a start bit, or timer A has timed
; out in the middle of a bit and it is time to sample RXD.
; ----------------------------------------------------------------------
L_E6C8:
lda #$82 ; E6C8 $82 = re-enable the timer B (transmit) interrupt
sta CIA2_ICR ; E6CA so the next bit time still interrupts us
pla ; E6CD get the flag byte back
and ciaIcrShadow ; E6CE only look at sources we actually enabled
lsr a ; E6D1 carry = timer A = the receive bit clock
L_E6D2:
bcs L_E6F1 ; E6D2 a data bit is due to be sampled
and #$08 ; E6D4 bit 3 after the shift = the FLAG line = an RXD start-bit edge
beq L_E6FF ; E6D6 neither - done
; ----------------------------------------------------------------------
; Start bit detected on the FLAG line: arm timer A half a bit time from now so the first sample lands
; in the middle of the start bit, and every following one in the middle of a data bit.
; ----------------------------------------------------------------------
lda #$83 ; E6D8 $83 = accept timer A and timer B from now on
sta ciaIcrShadow ; E6DA remember the new source mask
sta rxBitCounter ; E6DD any negative value marks 'the next sample is the start bit'
ldx #$00 ; E6E0 X=0 selects timer A, the receive clock
lda bitPeriodHi ; E6E2 half a bit time (the low byte is left alone)
lsr a ; E6E5 halve the high byte
jsr setBitPeriod ; E6E6 program the latch and get $11 back
sta CIA2_CRA ; E6E9 start timer A: the first sample lands in the middle of the start bit
L_E6EC:
jsr setBitPeriodFull ; E6EC reload the latch with a whole bit time for every following sample
bne L_E6FF ; E6EF always taken (A = $11)
; ----------------------------------------------------------------------
; A timer A tick: sample RXD and decide whether this is the start bit, a data bit or the stop bit.
; ----------------------------------------------------------------------
L_E6F1:
lda CIA2_PRB ; E6F1 PB0 is RXD
lsr a ; E6F4 the received bit into carry
dec rxBitCounter ; E6F5 which bit of the character is this?
bmi L_E709 ; E6F8 negative: this is the start-bit sample
beq L_E712 ; E6FA zero: this is the stop-bit sample
L_E6FC:
ror rxShiftRegister ; E6FC otherwise shift the data bit in, LSB first
L_E6FF:
lda ciaIcrShadow ; E6FF restore the sources we want
sta CIA2_ICR ; E702 re-arm the sources we want
pla ; E705 restore X
tax ; E706 the saved X
pla ; E707 and A
rti ; E708 return from the NMI
L_E709:
bcs L_E724 ; E709 the start bit must be low; high means noise or a framing error
nmiArmDataBits:
lda #$09 ; E70B 8 data bits plus the stop bit to sample
sta rxBitCounter ; E70D reload the receive bit counter
bne L_E6FF ; E710 always taken
L_E712:
bcc L_E724 ; E712 the stop bit must be high; low is a framing error
lda #$00 ; E714 a good character means the link is alive:
sta D_EB00 ; E716 clear the protocol stall counter
sta carrierSampleTimer ; E719 and tell the carrier sampler that data is flowing
nmiDeliverRxChar:
lda rxShiftRegister ; E71C the assembled character
jsr pushUartRxRing ; E71F hand it to the protocol
bcc L_E727 ; E722 stored - done
L_E724:
jsr reportLinkError ; E724 framing error, bad stop bit or receive overflow
L_E727:
lda #$00 ; E727 stop timer A: this character is finished
sta CIA2_CRA ; E729 0 in the control register stops the receive clock
lda #$92 ; E72C $92 = back to waiting for the next start bit on FLAG
sta ciaIcrShadow ; E72E and remember it in the shadow
bne L_E6FF ; E731 always taken
; ----------------------------------------------------------------------
; 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 - Pushes one byte into the 22-byte raw UART transmit ring $E417 and counts it
; in $E0A5, the number of characters the NMI transmitter still owes. The NMI decrements $E0A5 as each
; character finishes ($E6B0) and zeroes it if the ring unexpectedly runs dry ($E634).
; In: A = byte to transmit
; Out: $E417 ring, $E414 count, $E416 write index, $E0A5 incremented; A preserved, X clobbered
; Called from: sendNextModemCommandChar ($E7D2), beginByteSyncPhase ($E81D), runPacketPhase ($E87C)
; and sendPacket ($E92A)
; ----------------------------------------------------------------------
queueByteForTransmit:
jsr pushUartTxRing ; E73C push the byte into the 22-byte UART transmit ring $E417; pushUartTxRing silently drops it when the ring already holds 22
inc uartPendingCount ; E73F count one more character owed to the NMI transmitter - note this happens even when the push above overflowed, but the NMI resets $E0A5 to 0 when the ring runs dry
rts ; E742 A still holds the byte, so the caller can test what it just sent
; ----------------------------------------------------------------------
; 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 baudParameterTable $E059 = {$53,$03,$02} = $0353 cycles = 1200 baud
switchTo1200Baud:
jsr loadBaudParameters ; E7B2 copy that entry into the live bit period $E056/$E057 and the pacing divisor $E058
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 $E716) on every character that
; arrives, so the counter really measures consecutive dead service ticks. Sixteen of them drop two
; return addresses off the stack and restart the byte sync.
; In: the outstanding-message indices $EAC5/$EAC6, $E0A4 packet-in-flight, stallCounter $EB00
; Out: $EB00 updated; on overflow the stack is unwound and control jumps to beginByteSyncPhase, so the
; caller never returns
; Called from: receivePacket ($E9B2 when the ring is empty); entry $E997 also from $E9DC when a packet
; is only half received
; ----------------------------------------------------------------------
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 ..