4219 lines
286 KiB
ArmAsm
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 ..
|