4662 lines
328 KiB
ArmAsm
4662 lines
328 KiB
ArmAsm
; ============================================================================
|
|
; $E000-$EFFF - modem driver, SwiftLink build: 6551 ACIA, Hayes control and the packet protocol
|
|
; ============================================================================
|
|
; Loaded by $0F31 for a modem game ($0BA5 bit 7 clear). Three layers: the UART, a Hayes AT command
|
|
; layer with a small terminal mode, and the packet layer that exchanges game commands and chat text
|
|
; with the opponent. Same jump table at $E000 as the solo trainer, so the rest of the game does not
|
|
; care which one is loaded.
|
|
;
|
|
; This is the SwiftLink build. Only the bottom layer differs from the shipped module: instead of a
|
|
; software UART bit-banged out of the CIA2 NMI on the user port, it drives a 6551 ACIA on a SwiftLink
|
|
; cartridge at $DE00 (or $DF00 - the base is probed at open time). Everything above the seam is
|
|
; untouched, so at the same line rate this module is byte-for-byte compatible on the wire with the
|
|
; stock driver: same $00/$FF byte-sync handshake, same length/control/sequence/checksum frame and the
|
|
; same retransmit rules. 300 and 1200 baud are where a SwiftLink player and a stock user-port player
|
|
; can meet; C=+2, C=+4, C=+9, C=+0 and C=+8 add 2400 up to 38400 for two SwiftLinks.
|
|
;
|
|
; THE LINE SETTINGS ARE THE USER'S, NOT THE DRIVER'S. The driver comes up in adopt mode - baudIndex
|
|
; $E055 ships as 21, the last entry of baudEntryTable - which means it reads the 6551's control
|
|
; register before it touches anything (aciaProbeAdopted $E655) and uses whatever rate, stop bits and
|
|
; clock source it finds there, so a C64 Ultimate, a real SwiftLink or anything else that has already
|
|
; been configured is played at its own setting rather than overridden. C=+U comes back to that mode
|
|
; at any time; the seven speed hot keys still impose a fixed rate for anyone who wants one.
|
|
; ONE THING IS NOT NEGOTIABLE: EIGHT DATA BITS. The frame layer sends $55 length codes, raw sequence
|
|
; bytes and rotate-and-add checksums, so it is not 7-bit clean, and adopt mode corrects the word
|
|
; length - and only the word length - of whatever it finds ($E667). Parity and stop bits are free.
|
|
; An unconfigured chip reads $00 in that register, which selects external clocks the cartridge does
|
|
; not have, so an unusable setting falls back to 300 baud 8N1 - which is also what the module used to
|
|
; impose, and which carries the roughly 30 bytes a second the game generates.
|
|
;
|
|
; Everything the rest of the game reaches into keeps its original address: $E000-$E017, $E01D-$E047
|
|
; and $EC00-$EFFF come out byte-identical to the shipped module, and checkAbi.py enforces that on
|
|
; every build. Internal helpers have moved twice to make room in a block that has never had more than
|
|
; a handful of spare bytes in it - ADDRESS_CHANGES.md is the old-to-new map. CIA2 is never touched
|
|
; except to keep its interrupt mask at $7F.
|
|
;
|
|
; One SwiftLink caveat worth knowing: some modern rebuilds use a WDC W65C51N, whose TDRE bit reads as
|
|
; permanently set. This driver is interrupt driven and never polls TDRE in a loop, but on such a board
|
|
; the transmit interrupt fires continuously and characters would be overrun; those boards need a timed
|
|
; delay per character instead. A genuine 6551 (and VICE's emulation) behaves.
|
|
|
|
.setcpu "6502"
|
|
.include "c64.inc"
|
|
.include "zeropage.inc"
|
|
|
|
; ---- references to code/data outside this file ----
|
|
sendByteToDrive := $089C
|
|
returnToMainMenu := $0AA3
|
|
inputLockoutTimer := $0B7D
|
|
programChecksum := $0B7E
|
|
gameVersionByte := $0B94
|
|
mapSeed := $0B95
|
|
mapSeed1 := $0B96
|
|
mapSeed2 := $0B97
|
|
playerSide := $0B9F
|
|
gameTypeOptions := $0BA3
|
|
isSoloTrainer := $0BA5
|
|
D_114C := $114C
|
|
drawRecyclerOnMap := $3F27
|
|
checkEnemyRecyclerSpotted := $3F57
|
|
mirrorMapCoordinate := $3FA3
|
|
D_4CE0 := $4CE0
|
|
D_4CE1 := $4CE1
|
|
D_4CE3 := $4CE3
|
|
completeExchange := $56F1
|
|
readFilmByte := $5874
|
|
copyPageUnderIo := $58B2
|
|
D_58C3 := $58C3
|
|
D_58C4 := $58C4
|
|
D_58C6 := $58C6
|
|
D_58C7 := $58C7
|
|
rewindFilmPtr := $58D5
|
|
getFilmSnapshotDestAddr := $5983
|
|
getFilmSnapshotSrcAddr := $5988
|
|
D_60E3 := $60E3
|
|
unitLayoutByGameType := $78B8
|
|
msgTrainerName := $884E
|
|
selectedUnit := $90F8
|
|
gameClock := $91CB
|
|
sideScoreLo := $92A2
|
|
recyclerMode := $92A6
|
|
recyclerCol := $92A8
|
|
recyclerRow := $92AA
|
|
savedViewOriginCol := $92C3
|
|
savedViewOriginRow := $92C4
|
|
revealAllUnits := $92FC
|
|
D_A94C := $A94C
|
|
D_B24C := $B24C
|
|
waitFrames := $C37F
|
|
advanceFillPointer := $C3F9
|
|
queueMessage := $C566
|
|
showStatusMessage := $C6FF
|
|
setMessageSlot := $C924
|
|
D_CB4C := $CB4C
|
|
inGameMenuText := $CC99
|
|
unitColTable := $F640
|
|
ownPlayerName := $FFDE
|
|
opponentPlayerName := $FFE8
|
|
nmiVector := $FFFA
|
|
nmiVectorHi := $FFFB
|
|
irqVectorHi := $FFFF
|
|
|
|
; Contents
|
|
; --------
|
|
; $E000 commJumpTable commRequest - entry 0 of the seven-entry jump table that every opponent module
|
|
; must present.
|
|
; $E003 commLinkControl jump-table entry 1, life cycle of the link.
|
|
; $E006 getLinkByte jump-table entry 2: take one byte that has arrived from the opponent (or was
|
|
; echoed back by the modem in terminal mode) out of the 8-byte hostInRing.
|
|
; $E009 putLinkByte jump-table entry 3: hand one byte to the module for transmission to the
|
|
; opponent (the chat channel).
|
|
; $E00C pollLinkStatus jump-table entry 4, called once per raster IRQ: runs the NMI suspend/resume
|
|
; handshake the disk loader needs and samples plus debounces the modem's carrier-detect line.
|
|
; $E012 keyboardScanHook jump-table entry 5, the module's keyboard scanner vector.
|
|
; $E015 commKeyEntry jump-table entry 6 and the game's only keyboard read.
|
|
; $E018 clearPacketVars zeroes the 19-byte packet-exchange block $E01D-$E02F (exchange flags, both
|
|
; packet lengths, both 7-byte packet buffers, the host-output count and the peer build id) by calling
|
|
; clearInlineVarBlock with the block sitting immediately after the JSR.
|
|
; $E034 clearLinkVars zeroes the 15-byte link-state block $E039-$E047 (suspend handshake, link-active
|
|
; flag, carrier state, modem timers, connection phase, error counter) through clearInlineVarBlock.
|
|
; $E04C aciaSetControlIdle program the ACIA control register and then clear txCharActive, so a rate change
|
|
; cannot leave the driver waiting for the TDRE of a byte the new rate stranded. In the dead user-port constants.
|
|
; $E04F clearTxCharActive forget the byte the driver believed was in the ACIA's transmit holding register.
|
|
; $E057 healStuckTransmitter the once-per-frame heal: txCharActive set with the transmit interrupt disarmed is
|
|
; unrecoverable, so clear it on sight. Returns the answer startNextTxChar's first test used to compute.
|
|
; $E067 clearInlineVarBlock shared 'zero the X bytes that follow my JSR' helper, used six times in this
|
|
; module.
|
|
; $E078 clearProtocolVars zeroes the 43-byte protocol block $E07D-$E0A7: the completed-exchange counter,
|
|
; the receive and transmit packet state machines, both host character rings with their counts and indices, the in-flight
|
|
; counters and the service lock.
|
|
; $E0A9 popHostInRing implementation of the $E006 entry.
|
|
; $E0C3 popHostOutRing pops the next byte out of the 16-byte hostOutRing.
|
|
; $E0DD pushHostInRing pushes one byte into the 8-byte hostInRing, the queue the game reads with
|
|
; $E006.
|
|
; $E0F6 pushHostOutRing implementation of the $E009 entry: store A EOR $80 in the 16-byte hostOutRing
|
|
; and bump the count, or do nothing at all when the ring already holds 16 bytes.
|
|
; $E111 commRequestDispatch the dispatcher behind $E000.
|
|
; $E128 requestPacketSend $E000 with X=1.
|
|
; $E12F serviceCommTick $E000 with X=0, called from the raster IRQ once per frame.
|
|
; $E14D receivePacketFsm the packet-layer receiver, a resumable state machine held in rxPacketState (0 =
|
|
; expecting a header, 1/2 = collecting a command payload, $80 = collecting chat bytes).
|
|
; $E170 beginReceivePacket the 'a packet is starting' step of receivePacketFsm: clear the received length
|
|
; and add 2 to exchangeFlags to mark 'reception in progress', then fall into the payload loop.
|
|
; $E1DD sendPacketFsm the packet-layer transmitter, a resumable state machine in txPacketState (bit 0
|
|
; = 'header sent, payload in progress', bit 1 = 'frame handed over, waiting for it to drain', bit 7 = 'that frame was
|
|
; chat, not a game packet').
|
|
; $E283 countCompletedExchange bumps the 16-bit exchangeCounter and then sets bit 6 of exchangeFlags
|
|
; ('exchange complete').
|
|
; $E28D setExchangeFlags ORs A into exchangeFlags and checks for a completed exchange: when bits 7 and 5
|
|
; are both set but bit 6 is not (top three bits = %101) the round trip is done, so it drops into countCompletedExchange
|
|
; which sets bit 6 as well.
|
|
; $E29A commLinkControlDispatch the dispatcher behind $E003.
|
|
; $E2B1 initCommModule_E2B1 initCommModule (the $E003 X=1 case) - cold start, called once immediately after
|
|
; the overlay has been loaded.
|
|
; $E2BD clearArqVars tail of initCommModule: zero the 76-byte frame/ARQ state block $EAB7-$EB02
|
|
; (sync counters, sequence numbers, the retransmit ring, the packet assembly buffer and the modem delay bytes).
|
|
; $E2C1 openCommLink_E2C1 openCommLink (the $E003 X=0 case) - bring the link up.
|
|
; $E2EC readKeyAndHandleModemHotkeys the implementation of $E015.
|
|
; $E353 loadBaudParameters hand the 6551 the control byte for the speed selected by X and keep it in
|
|
; bitPeriodLo. The rates are SwiftLink rates, i.e. twice the 6551 data sheet's, because the cartridge fits a 3.6864 MHz crystal.
|
|
; $E38D hangUpModem_E38D hangUpModem - drop DTR and then queue the canned Hayes hang-up string, which
|
|
; the terminal-mode transmitter emits in reverse as [guard] '+++' [guard] CR [guard] 'ATH0' CR.
|
|
; $E393 queueModemCommandString load a canned modem command string into the host-output ring.
|
|
; $E3B2 returnNoKey the shared 'no key / key handled' return: A = $FF.
|
|
; $E3B6 sendLinkByte indirect 'give one byte to the link'.
|
|
; $E3B9 receiveLinkByte indirect 'take one byte from the link'.
|
|
; $E3BC reportLinkError indirect link-error hook, called by the NMI receiver on a framing error, a bad
|
|
; stop bit or a receive-ring overflow.
|
|
; $E3CB pollCarrierState implementation of $E00C.
|
|
; $E3F3 selectFramedLinkVectors rewrites the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD so
|
|
; that sendLinkByte, receiveLinkByte and reportLinkError use the framed (ARQ) rings and the error counter instead of the
|
|
; raw UART rings and the no-op error stub.
|
|
; $E408 dropDtrLine clear the whole ACIA command register: DTR down, receiver and all interrupts
|
|
; off. (It moved here from $E574; aciaSetCommandIdle, which used to be here, is gone - see $E685.)
|
|
; $E40F clearUartVars zeroes the 90-byte ring-buffer block $E414-$E46D: all four descending circular
|
|
; buffers (UART transmit 22, UART receive 20, ARQ transmit 18, ARQ receive 18) with their counts and read/write indices.
|
|
; $E46E popArqRxRing pop the oldest byte out of the 18-byte ARQ receive ring, the queue the frame
|
|
; layer fills with checksum-verified payload.
|
|
; $E486 popArqTxRing pop the oldest byte out of the 18-byte ARQ transmit ring, i.e.
|
|
; $E49E pushArqRxRing push one validated payload byte into the 18-byte ARQ receive ring; the byte is
|
|
; dropped when the ring already holds 18.
|
|
; $E4B7 pushArqTxRing push one byte into the 18-byte ARQ transmit ring; the byte is dropped when the
|
|
; ring is full, and the caller sees C=1 because the count compare left it set.
|
|
; $E4D0 returnRingEmpty the shared 'nothing in that ring' exit (SEC / RTS) used by the four ring pop
|
|
; routines.
|
|
; $E4D2 popUartRxRing pop one received character out of the 20-byte raw UART receive ring the NMI
|
|
; fills.
|
|
; $E4EA popUartTxRing pop the next character to transmit out of the 22-byte raw UART transmit ring.
|
|
; $E502 pushUartRxRing push one fully received character into the 20-byte raw UART receive ring.
|
|
; $E51B pushUartTxRing push one character into the 22-byte raw UART transmit ring; the byte is dropped
|
|
; when the ring is full.
|
|
; $E534 flushUartTxRing throw away everything still queued for transmission, then set the pending-byte
|
|
; count to 1 if a character is still being shifted out or 0 if not.
|
|
; $E540 configureUserPortLines open the ACIA: control register from the selected speed, command register $09
|
|
; (DTR on, RTS low, receiver IRQ on, transmit IRQ off), then drain the receiver and read the status register.
|
|
; $E551 aciaSetCommand write the ACIA command register and keep a shadow of it.
|
|
; $E558 aciaGetStatus read the ACIA status register - which is also the interrupt acknowledge - into
|
|
; aciaStatusSave.
|
|
; $E55F aciaGetData read the ACIA data register.
|
|
; $E563 aciaPutData write the ACIA data register.
|
|
; $E567 aciaSetControl write the ACIA control register.
|
|
; $E56B aciaGetControl read the ACIA control register back; aciaSetControl falls into it, so a write
|
|
; and its read-back are one call.
|
|
; $E56F silenceCia2 pin ciaIcrShadow and CIA2_ICR at $7F and read CIA2_ICR, so CIA2 stops raising
|
|
; the shared /NMI line and lets go of one it is already holding down. (It moved here from $E727.)
|
|
; $E57B installCommNmiVector point the RAM NMI vector at $FFFA/$FFFB at commNmiHandler.
|
|
; $E586 restartUart (re)start the link: drain the receiver, ask the modem state machine to
|
|
; re-evaluate, prime the carrier sample timer, and fall into setCiaNmiMask with the 'interrupt on' mask.
|
|
; $E593 setCiaNmiMask arm or silence the module's interrupt: call silenceCia2 and then set the ACIA
|
|
; command register to $09 (receiver IRQ on) or $0B (off, with DTR still up).
|
|
; $E5B1 aciaResumeReceiver forget the byte in the transmitter, drain the receiver and re-read the status.
|
|
; (It moved here from $E658; clearUartState, which used to be at $E5B6, is gone - clearTxCharActive $E04F does its work.)
|
|
; $E5C1 suspendUartIfRunning two-byte entry between the UART state block and stopCommNmi: the disk loader
|
|
; has asked for a suspend, so silence the ACIA unless the acknowledge byte already says it is silent.
|
|
; $E5C3 stopCommNmi tear the link down: ask the modem state machine to fall back to phase 0, disable
|
|
; the ACIA's receiver interrupt (which also sets both suspend bytes to $80), and forget the character in the transmitter.
|
|
; $E5D6 serviceCarrierAndSuspendRequest first half of the $E00C service.
|
|
; $E5FF rearmCarrierTimer the 'no character has arrived recently' tail of the carrier sampler: park the
|
|
; sample in carrierSampleTimer (it is negative, so the timer stays negative until the NMI zeroes it on the next good
|
|
; character) and strip bit 7 from the value that goes into linkStatusSample, marking the link as quiet.
|
|
; $E607 startNextTxChar kick the transmitter: arm the ACIA's transmit interrupt when there is something
|
|
; in the ring, DTR is up and, outside terminal mode, carrier is present.
|
|
; $E644 aciaDetect settle once which page the cartridge answers on - $DE00 probed with two
|
|
; patterns, $DF00 taken on trust - and never probe again until the next cold start.
|
|
; $E654 aciaDetectDone the shared bare RTS the detect path and the transmit path both exit through
|
|
; (it was setBitPeriodFull, a dead stub of the software UART; setBitPeriod, the other one, is gone).
|
|
; $E655 aciaProbeAdopted aim the accessors at page A, remember the line settings that page was already
|
|
; configured for, and make it read two different control-register patterns back before believing there is a 6551 on it.
|
|
; $E685 commNmiHandler the module's NMI handler, installed at $FFFA/$FFFB by installCommNmiVector. It
|
|
; banks the I/O area in itself, services RDRF and TDRE, and chains a foreign NMI through $E031.
|
|
; $E6F9 aciaSetPage record the cartridge's base page by patching the six accessors' operands and
|
|
; publishing it in aciaPageLatch $E5BF. (It moved here from $E663.)
|
|
; $E70F baudEntryTable eight three-byte entries {hot-key code, ACIA control byte, spare}: seven fixed
|
|
; rates, C=+3/1/2/4/9/0/8 for 300 to 38400, and entry 21 = adopt, C=+U, whose control byte is whatever the user's own
|
|
; hardware was already configured for. (It moved here from $E706.)
|
|
; $E726 queueAndKickTx the body of queueByteForTransmit. (It moved here from $E71B.)
|
|
; $E733 popArqTxRingThunk the fixed entry the frame layer uses to fetch the next payload byte the packet
|
|
; layer queued.
|
|
; $E736 pushArqRxRingThunk Three-byte trampoline: JMP pushArqRxRing ($E49E).
|
|
; $E739 popUartRxRingThunk Three-byte trampoline: JMP popUartRxRing ($E4D2), the packet layer's only door
|
|
; into the raw 20-byte UART receive ring.
|
|
; $E73C queueByteForTransmit Pushes one byte into the 22-byte raw UART transmit ring $E417, counts it in
|
|
; $E0A5, and starts the transmitter if it was idle.
|
|
; $E73F countLinkError the framed link-error hook: count one bad character.
|
|
; $E743 receiveByteTrackZeros Pops one byte from the raw UART receive ring and maintains zeroRunLength
|
|
; ($EAB8), the count of consecutive $00 bytes on the line.
|
|
; $E756 runModemStateMachine The per-frame link service.
|
|
; $E773 clearInFlightFlags Mid-routine entry of runModemStateMachine, also used by the solo trainer build:
|
|
; clears the frame-in-flight flag $E0A4 and the packet-layer send enable $E0A6.
|
|
; $E7CA sendNextModemCommandChar Tail of the per-frame link service.
|
|
; $E7F7 runLinkStateMachine Second half of the per-frame link service: it dispatches on connectionPhase
|
|
; $E040.
|
|
; $E805 beginByteSyncPhase Connection phase 2, the byte-alignment handshake that runs before any packet
|
|
; may be sent.
|
|
; $E858 runPacketPhase Connection phase 3, one tick of the reliable packet layer.
|
|
; $E8F8 buildAckPacket Builds the three-byte bare acknowledgement in packetBuffer: length code $55
|
|
; (one payload byte) and control byte $EC, then falls into the common send tail at $E90C which ORs in the receive
|
|
; sequence number, appends the check byte and transmits it.
|
|
; $E909 sendPacket Sends one message from the 35-byte transmit ring: buildPacketFromRing copies it
|
|
; into packetBuffer, the low two bits of rxSeqCounter are piggy-backed into the control byte as the acknowledgement of
|
|
; everything received so far, computePacketChecksum appends the check byte, and every byte is pushed into the UART
|
|
; transmit ring.
|
|
; $E937 computePacketChecksum Rotate-and-add check byte over packetBuffer[0..packetLength-1].
|
|
; $E94A buildPacketFromRing Copies one queued message out of the 35-byte transmit ring into packetBuffer.
|
|
; $E96D writeTxRingByte Appends A to the 35-byte transmit message ring $EAC7 at txRingIndex and
|
|
; advances the index modulo 35.
|
|
; $E975 readTxRingByte Reads the transmit-ring byte at txRingIndex and advances the index modulo 35.
|
|
; $E978 readTxRingByteAt Reads the transmit-ring byte at an arbitrary index X and leaves txRingIndex
|
|
; pointing one past it.
|
|
; $E986 noteIdlePoll Called when the receiver found nothing on the line.
|
|
; $E9AA receivePacket Assembles one incoming packet from the UART receive ring.
|
|
; $EA0D dispatchReceivedPacket Acts on a packet whose checksum was good.
|
|
; $EA7B deliverPacketPayload Hands an accepted data packet to the game.
|
|
; $EAA0 packetReturn A single RTS used as the common exit of the packet layer.
|
|
; $EAB2 clearPacketState Zeroes the entire 76-byte packet-protocol state block $EAB7-$EB02 in one call.
|
|
; $EB03 remoteCodeBuffer Landing area for 6502 code sent by the peer.
|
|
; $EC00 clearBattlefieldMap Fills all 1600 cells of the 40x40 battlefield map $F000-$F63F with $40, the
|
|
; empty-terrain code, using the shared pointer-advance helper advanceFillPointer ($C3F9).
|
|
; $EC1C setViewOriginToHome Puts the 7x5 tactical view back at the middle of the player's own end of the
|
|
; battlefield: origin column 17, row 28, so the cursor cell (origin+3, origin+2) lands on map (20,30).
|
|
; $EC2B loadMapSeedIntoScenarioRng Copies the 24-bit map seed out of the game settings block into the scenario
|
|
; random-number state, so that both machines run the procedural map generator from the identical seed and get the
|
|
; identical battlefield.
|
|
; $EC54 makeMapPointSymmetric Entered with mirrorMode zp_18 = $FF: walks a forward pointer up from $F000 and
|
|
; a backward pointer down from $F63F and writes mirrorTerrainCode(map[i]) into map[1599-i] without copying anything back
|
|
; the other way.
|
|
; $EC58 rotateMapAndMirrorCodes Entered with mirrorMode zp_18 = 0: exchanges map[i] and map[1599-i] over the
|
|
; whole map, passing both bytes through mirrorTerrainCode, i.e.
|
|
; $EC91 mirrorAllUnitCoordinates Mirrors the six 100-entry unit coordinate arrays that follow the map, so that
|
|
; unit positions match a battlefield rotateMapAndMirrorCodes has just turned 180 degrees.
|
|
; $ECB8 mirrorCoordinateArray Replaces the coordinate in each of the 100 entries of the array at A/Y with 39
|
|
; minus itself, leaving the two top flag bits of every entry untouched (they carry per-unit markers).
|
|
; $ECD4 mirrorTerrainCode Maps one map cell byte to its 180-degree-rotated equivalent.
|
|
; $ECF3 loadUnitStartTemplate Installs the unit start positions for the selected game type.
|
|
; $ED43 exchangePlayerNamesAndSettings The start-of-session handshake.
|
|
; $EDBE sendEmptyMessage Sets the outgoing message length to zero and asks the comm module to transmit
|
|
; it, which closes the handshake and, on the error path, unblocks the peer.
|
|
; $EDC9 exchangeNameHalf Sends one half of the local player's name - a six-byte message of $FF followed
|
|
; by ownPlayerName[X..X+4] - waits for the peer's reply and checks that it is six bytes long and starts with the same
|
|
; $FF magic.
|
|
; $EDFC abortLinkAndReturnToMenu The handshake failure path: show message/sound 3, wait a second, let the engine
|
|
; finish the exchange it is in the middle of, release the peer with an empty message and jump back to the main menu.
|
|
; $EE0F storeOpponentNameHalf Success tail of exchangeNameHalf: copies the five payload bytes of the received
|
|
; message into the opponent name and releases the receive buffer.
|
|
; $EE20 waitForLinkMessage Busy-waits until bit 6 of the comm status byte $E01D is set, i.e.
|
|
; $EE26 finaliseRecyclerSetup Last step of the setup phase.
|
|
; $EE4A loadFilmStartSnapshot Restores the game state a 'game film' was recorded from.
|
|
; $EEB9 handleDisplayToggleKey The solo trainer's F1 handler, polled every frame from the main idle loop at
|
|
; $600A but only while $0BA5 bit 7 (practice mode) is set.
|
|
; $EEC3 announceAndToggleDisplayMode Queues message slot $21 with either 'BEGINNER DISPLAY SET.' or 'STANDARD
|
|
; DISPLAY SET.'.
|
|
; $EEDD toggleBattleDisplayMode Flips revealAllUnits $92FC (bit 7 set = beginner display, in which
|
|
; renderMapTile at $284F draws every enemy unit), patches the eight-character hole at $EF10 with the name of the mode F1
|
|
; will switch to NEXT, and queues the resulting prompt 'F 1 FOR xxxxxxxx BATTLE DISPLAY.' as message slot $20.
|
|
|
|
.org $E000
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commRequest - entry 0 of the seven-entry jump table that every opponent module must present. X picks
|
|
; the service: 0 = per-frame protocol tick, 1 = 'transmit the packet I have prepared in
|
|
; txPacketBuffer', 2 or more = acknowledge and throw away a finished exchange.
|
|
; In: X = function code
|
|
; Out: falls through to commRequestDispatch; updates exchangeFlags ($E01D)
|
|
; Called from: raster IRQ $1129 (X=0), buildOutgoingPacket $56AE (X=1), completeExchange $570F/$5727,
|
|
; $4EFF, $526F, $573B/$5767/$5776, and the handshake in this file
|
|
; ($ED59/$ED96/$EDBB/$EDC5/$EDE4/$EE1D)
|
|
; commJumpTable: addrTable, 24 bytes. Seven JMP entries at $E000/$E003/$E006/$E009/$E00C/$E012/$E015,
|
|
; with a two-byte constant pool at $E00F ($60 = RTS, $E010 = $40 = RTI, the file's default
|
|
; nmiChainVector target) and the isOriginateMode flag at $E011 wedged between them.
|
|
; ----------------------------------------------------------------------
|
|
commJumpTable:
|
|
jmp commRequestDispatch ; E000 entry 0: per-frame tick / send request / acknowledge, selected by X
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commLinkControl - jump-table entry 1, life cycle of the link. X=0 open, X=1 cold-initialise the
|
|
; module, X=2 shut the UART down, X>=3 hang the modem up and then shut down.
|
|
; In: X = function code
|
|
; Out: see commLinkControlDispatch
|
|
; Called from: initCommModule $0F46 (X=1, right after the overlay is loaded), openCommLink $1B70
|
|
; (X=0), hangUpModem $1B56 (X=3)
|
|
; ----------------------------------------------------------------------
|
|
commLinkControl:
|
|
jmp commLinkControlDispatch ; E003 entry 1: open, initialise, close or hang up the link, selected by X
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; getLinkByte - jump-table entry 2: take one byte that has arrived from the opponent (or was echoed
|
|
; back by the modem in terminal mode) out of the 8-byte hostInRing.
|
|
; In: none
|
|
; Out: A = byte with bit 7 flipped back, C=0 when a byte was returned, C=1 when the ring was empty
|
|
; Called from: receiveChatCharacter $1399
|
|
; ----------------------------------------------------------------------
|
|
getLinkByte:
|
|
jmp popHostInRing ; E006 entry 2: take one byte the opponent sent out of the host input ring
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; putLinkByte - jump-table entry 3: hand one byte to the module for transmission to the opponent (the
|
|
; chat channel). Stored EOR $80 in the 16-byte hostOutRing.
|
|
; In: A = byte to send
|
|
; Out: hostOutRing/hostOutCount updated; the byte is dropped when the ring already holds 16
|
|
; Called from: handleChatSend $1396 (JMP)
|
|
; ----------------------------------------------------------------------
|
|
putLinkByte:
|
|
jmp pushHostOutRing ; E009 entry 3: hand one chat byte over for transmission
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pollLinkStatus - jump-table entry 4, called once per raster IRQ: runs the NMI suspend/resume
|
|
; handshake the disk loader needs and samples plus debounces the modem's carrier-detect line.
|
|
; In: nmiSuspendRequest/nmiSuspendAck, isLinkActive, CIA2_PRB bit 4
|
|
; Out: linkStatus (debounced), linkStatusSample, carrierDebounceCount
|
|
; Called from: raster IRQ $112C
|
|
; ----------------------------------------------------------------------
|
|
pollLinkStatus:
|
|
jmp pollCarrierState ; E00C entry 4: per-frame carrier poll and NMI suspend handshake
|
|
|
|
commRtsStub:
|
|
rts ; E00F spare RTS stub sitting in the jump table's constant pool
|
|
|
|
commRtiStub:
|
|
.byte $40 ; E010 spare RTI stub - the value nmiChainVector points at until the game patches it
|
|
isOriginateMode:
|
|
.byte $00 ; E011 0 = answer mode (ATQ0V1X1A), 1 = originate/dial mode (ATQ0V1X1D); set by $0CDE from the A/O menu key and by the C=+A / C=+O hot keys
|
|
|
|
; ----------------------------------------------------------------------
|
|
; keyboardScanHook - jump-table entry 5, the module's keyboard scanner vector. On disk it points at
|
|
; returnNoKey (always 'no key'); the main program overwrites the operand bytes $E013/$E014 at
|
|
; $0F4B/$0F50 with $0DB7 = scanKeyboard, so at run time this is the game's keyboard matrix scan.
|
|
; In: none
|
|
; Out: A = key code ($FF = none), Y bit 7 = SHIFT held, X bit 7 = Commodore key held
|
|
; Called from: readKeyAndHandleModemHotkeys $E2EC only
|
|
; ----------------------------------------------------------------------
|
|
keyboardScanHook:
|
|
jmp returnNoKey ; E012 entry 5: keyboard scan - operand patched to $0DB7 (scanKeyboard) by $0F4B/$0F50
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commKeyEntry - jump-table entry 6 and the game's only keyboard read. Routing every key through the
|
|
; comm module lets the Commodore-key modem hot keys be intercepted before the game sees them.
|
|
; In: none
|
|
; Out: A = key code, or $FF when the key was swallowed as a modem hot key
|
|
; Called from: pollKeyboardEvent $0E56 (every frame)
|
|
; ----------------------------------------------------------------------
|
|
commKeyEntry:
|
|
jmp readKeyAndHandleModemHotkeys; E015 entry 6: read a key, filtering out the Commodore-key modem hot keys
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearPacketVars - zeroes the 19-byte packet-exchange block $E01D-$E02F (exchange flags, both packet
|
|
; lengths, both 7-byte packet buffers, the host-output count and the peer build id) by calling
|
|
; clearInlineVarBlock with the block sitting immediately after the JSR. Because clearInlineVarBlock
|
|
; eats the return address, the RTS lands in this routine's caller.
|
|
; In: none
|
|
; Out: $E01D-$E02F = 0; returns to the caller of clearPacketVars
|
|
; Called from: initCommModule $E2B4
|
|
; ----------------------------------------------------------------------
|
|
clearPacketVars:
|
|
ldx #$13 ; E018 19 bytes to clear: exchangeFlags through peerCommBuildId
|
|
jsr clearInlineVarBlock ; E01A clear the inline block that follows and return to our caller's caller
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Packet-exchange variables, $E01D-$E02F. This is the inline block clearPacketVars hands to
|
|
; clearInlineVarBlock, so the JSR above is followed by 19 bytes of data, not code - the BRK the
|
|
; disassembler shows is never executed. The game reads and writes most of these directly:
|
|
; $E01D flags, $E01E/$E01F lengths, $E020 and $E027 the two 7-byte command packets.
|
|
; ----------------------------------------------------------------------
|
|
exchangeFlags:
|
|
brk ; E01D exchangeFlags: bit 0 send requested, bit 1 receive in progress, bit 3 idle marker sent, bit 5 own packet sent, bit 6 exchange complete, bit 7 opponent packet received (rendered as BRK - it is data, never executed)
|
|
rxPacketLength:
|
|
.byte $00 ; E01E rxPacketLength: number of bytes of the opponent's packet held in rxPacketBuffer
|
|
txPacketLength:
|
|
.byte $00 ; E01F txPacketLength: number of bytes the game put in txPacketBuffer (0-4 in practice)
|
|
rxPacketBuffer:
|
|
.byte $00 ; E020 . rxPacketBuffer: the command byte of the packet just received (7 bytes, $E020-$E026)
|
|
rxPacketArg1:
|
|
.byte $00 ; E021 first argument byte of the received command
|
|
rxPacketArg2:
|
|
.byte $00 ; E022 second argument byte of the received command
|
|
rxPacketArg3:
|
|
.byte $00 ; E023 third argument byte of the received command
|
|
rxPacketArg4:
|
|
.byte $00 ; E024 the remaining three bytes of the 7-byte receive buffer
|
|
.byte $00,$00 ; E025 ..
|
|
txPacketBuffer:
|
|
.byte $00 ; E027 . txPacketBuffer: command byte to send (7 bytes, $E027-$E02D)
|
|
txPacketArg1:
|
|
.byte $00 ; E028 first argument byte of the command being sent
|
|
txPacketArg2:
|
|
.byte $00 ; E029 second argument byte of the command being sent
|
|
txPacketArg3:
|
|
.byte $00 ; E02A the remaining four bytes of the 7-byte transmit buffer
|
|
.byte $00,$00,$00 ; E02B ...
|
|
hostOutCount:
|
|
.byte $00 ; E02E hostOutCount: characters waiting in hostOutRing (chat text, or a queued AT command string)
|
|
peerCommBuildId:
|
|
.byte $00 ; E02F peerCommBuildId: the opponent's commBuildId, stored by the handshake at $EDAD
|
|
|
|
; Four bytes that survive every clear: the build id the two machines swap during the handshake, the
|
|
; vector this module chains non-CIA2 NMIs through, and the shadow of the CIA2 interrupt mask.
|
|
commBuildId:
|
|
.byte $00 ; E030 commBuildId: $00 = this modem build; the trainer overlay has bit 7 set here. Read by main at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D and sent to the peer during the handshake
|
|
nmiChainVector:
|
|
.byte $10 ; E031 nmiChainVector low byte - $E010 (the RTI stub) on disk, patched to $1298 by initInterruptVectors $0BF3
|
|
nmiChainVectorHi:
|
|
.byte $E0 ; E032 nmiChainVector high byte - patched to $12 by $0BFB
|
|
ciaIcrShadow:
|
|
.byte $90 ; E033 ciaIcrShadow: the CIA2 interrupt sources this module wants. setCiaNmiMask pins it at $7F = none, because the main program writes this byte back into CIA2_ICR after every film-buffer access and this driver's interrupt comes from the cartridge
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearLinkVars - zeroes the 15-byte link-state block $E039-$E047 (suspend handshake, link-active
|
|
; flag, carrier state, modem timers, connection phase, error counter) through clearInlineVarBlock.
|
|
; In: none
|
|
; Out: $E039-$E047 = 0; returns to the caller of the routine that jumped here
|
|
; Called from: commLinkControlDispatch $E2AE (JMP - the tail of the 'close the link' path)
|
|
; ----------------------------------------------------------------------
|
|
clearLinkVars:
|
|
ldx #$0F ; E034 15 bytes to clear: nmiSuspendRequest through linkErrorCount
|
|
L_E036:
|
|
jsr clearInlineVarBlock ; E036 clear the inline block that follows; the RTS lands in the caller of whoever jumped here
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Link-state variables, $E039-$E047: the inline block clearLinkVars zeroes. $E039/$E03A are the
|
|
; two-way suspend handshake with the disk loader, $E03B-$E03D the link and carrier state, and the rest
|
|
; timers and counters used by the modem state machine. The 'DTH' at $E045-$E047 is leftover content
|
|
; from the sector image (this page comes from track 18 sector 7, outside the encrypted area).
|
|
; ----------------------------------------------------------------------
|
|
nmiSuspendRequest:
|
|
brk ; E039 nmiSuspendRequest: the game writes $C0 here before a disk load and 0 afterwards (rendered as BRK - data, never executed)
|
|
nmiSuspendAck:
|
|
.byte $00 ; E03A nmiSuspendAck: this module answers $80 when the UART NMI is really off, 0 when it is running
|
|
isLinkActive:
|
|
.byte $00 ; E03B isLinkActive: non-zero once openCommLink has started the UART
|
|
linkStatus:
|
|
.byte $00 ; E03C linkStatus, debounced: bit 6 = carrier present, bit 7 = characters still arriving
|
|
linkStatusSample:
|
|
.byte $00 ; E03D linkStatusSample ($E03D) and an unused byte ($E03E): the raw carrier sample taken each frame
|
|
.byte $00 ; E03E .
|
|
modemReplyTimer:
|
|
.byte $00 ; E03F modemReplyTimer: counts down while waiting for the modem's answer; negative means a command string is still going out
|
|
connectionPhase:
|
|
.byte $00 ; E040 connectionPhase: 0 idle, 1 modem-command/terminal mode, 2 byte sync, 3 packet protocol
|
|
skipModemInitString:
|
|
.byte $00 ; E041 skipModemInitString: non-zero after C=+RETURN so openCommLink re-opens without re-sending the AT string
|
|
uartRestartRequest:
|
|
.byte $00 ; E042 uartRestartRequest: 1 = re-evaluate the connection, $FF = force back to phase 0
|
|
carrierDebounceCount:
|
|
.byte $00 ; E043 carrierDebounceCount: consecutive samples that disagree with linkStatus
|
|
txPaceCounter:
|
|
.byte $00 ; E044 txPaceCounter: dead, but beginByteSyncPhase $E820 still writes it, so the byte has to stay
|
|
carrierSampleTimer:
|
|
.byte $44 ; E045 carrierSampleTimer: zeroed by the NMI on every byte that arrives without an error, so a negative value means the line is quiet ('D' here is leftover from the sector image)
|
|
lastHotkeyCode:
|
|
.byte $54 ; E046 lastHotkeyCode: the previous hot-key code, used to ignore auto-repeat ('T' is leftover)
|
|
linkErrorCount:
|
|
.byte $48 ; E047 linkErrorCount: bumped by countLinkError; non-zero makes the link state machine restart byte sync ('H' is leftover)
|
|
|
|
; Modem/UART constants, $E048-$E066. None of them is inside a cleared block, so they keep their disk
|
|
; values for the whole session: option flags, the CIA2 port bit masks, the two baud parameter sets and
|
|
; the border-colour table for the link status indicator.
|
|
modemOptionFlags:
|
|
.byte $CC ; E048 modemOptionFlags $CC (bit 7 = a modem is attached, bit 6 = watch the result codes for CONNECT 1200) and an unreferenced $08
|
|
.byte $08 ; E049 .
|
|
carrierOverrideFlags:
|
|
.byte $00 ; E04A carrierOverrideFlags: bit 6 = pretend carrier is present (C=+C toggle, for a direct null-modem cable)
|
|
carrierOverrideToggleMask:
|
|
.byte $40 ; E04B $40 = the carrier-override bit the C=+C hot key toggles
|
|
; ----------------------------------------------------------------------
|
|
; aciaSetControlIdle / clearTxCharActive - two thirds of the transmitter-deadlock fix, living in the
|
|
; nine bytes the stock user-port constants left behind. These were dtrToggleMask $24 ($E04C),
|
|
; userPortDdrbValue $26 ($E04D), userPortDtrDropped $22 ($E04E), userPortIdleTable $26 $26 ($E04F),
|
|
; serialShiftPatternTable $27 $2F $3F ($E051) and serialShiftPatternAnswer1200 $37 ($E054) - nine
|
|
; bytes of CIA2 bit-banger constants that nothing has read since the software UART was replaced. The
|
|
; .res gaps left in the UART layer itself are all five bytes or fewer except the one at $E72A, and
|
|
; the fix needs three separate places to be able to clear one flag, so the shared parts sit here.
|
|
;
|
|
; The flag is txCharActive $E5BB, and it exists for exactly one reason: to stop a second byte being
|
|
; pushed into the ACIA's transmit holding register while the first is still in it. Nothing else reads
|
|
; meaning into it. That makes it safe to drop the moment the transmit interrupt goes off, and it makes
|
|
; it fatal to leave set when the transmit interrupt goes off: startNextTxChar $E607 returns while the
|
|
; flag is set, and the NMI's transmit half ignores TDRE while the command register says the transmit
|
|
; interrupt is disarmed, so nothing is left that could ever clear it. A machine in that state stops
|
|
; transmitting for good - measured at 2400 baud, six times, and cured every time by writing a single
|
|
; $00 over $E5BB from the monitor.
|
|
; Clearing it does not push a byte in on top of another, either. The route back to the chip is
|
|
; startNextTxChar, which touches nothing unless the command shadow says the transmitter is idle and
|
|
; DTR is up, and each place that clears the flag only clears it where the holding register really is
|
|
; free: the NMI's disarm site has just seen TDRE, and the once-per-frame heal only fires when the
|
|
; transmit interrupt is off. The one case where a byte can still be in flight with the interrupt off
|
|
; is a disk-load suspend, command $0B - and startNextTxChar refuses to transmit while nmiSuspendAck
|
|
; is negative, so nothing is written there either.
|
|
; In: A = the ACIA control byte (aciaSetControlIdle only)
|
|
; Out: the control register programmed (aciaSetControlIdle only), txCharActive = 0, A = 0, Z = 1;
|
|
; X and Y unchanged
|
|
; Called from: loadBaudParameters $E35C (aciaSetControlIdle); healStuckTransmitter $E05C and
|
|
; aciaResumeReceiver $E5B1 (clearTxCharActive)
|
|
; ----------------------------------------------------------------------
|
|
aciaSetControlIdle:
|
|
jsr aciaSetControl ; E04C program the new line rate first, then fall into the clear
|
|
clearTxCharActive:
|
|
lda #$00 ; E04F the driver no longer believes a byte is in the ACIA
|
|
sta txCharActive ; E051 ...so the next tick may hand it a fresh one
|
|
rts ; E054 A = 0 and Z = 1, which is what startNextTxChar wants
|
|
.assert * = $E055, error, "the txCharActive helpers must end at $E054"
|
|
|
|
baudIndex:
|
|
.byte $15 ; E055 baudIndex: the three-byte index into baudEntryTable $E70F - 0 = 300, 3 = 1200, 6 = 2400, 9 = 4800, 12 = 9600, 15 = 19200, 18 = 38400, 21 = adopt. The disk value is 21, so the driver comes up using whatever the user's port is already configured for; it was $00 (300 baud), which is what adopt falls back to when the chip holds nothing usable
|
|
bitPeriodLo:
|
|
.byte $50 ; E056 the live ACIA control byte, written by loadBaudParameters. The $50 stored here is the stock 300-baud timer byte and is never used: openCommLink loads a real one before the ACIA is opened
|
|
; ----------------------------------------------------------------------
|
|
; healStuckTransmitter - the self-healing half of the deadlock fix, in the eight dead bytes that were
|
|
; bitPeriodHi $0D ($E057), txPaceReload $01 ($E058) and the stock baudParameterTable $50 $0D $01 $53
|
|
; $03 $02 ($E059-$E05E). All eight belonged to the software UART's timing and have been unread since
|
|
; this build replaced it; the SwiftLink speed table is baudEntryTable $E70F.
|
|
; The rule it enforces once a frame: txCharActive set while command bit 2 is clear cannot be true for
|
|
; any healthy reason. Bit 2 clear means the transmit interrupt is disarmed, which means no interrupt
|
|
; will ever come back to clear the flag, and startNextTxChar will not run while the flag is set. So
|
|
; the combination is unrecoverable by definition and can be cleared on sight, whatever produced it -
|
|
; the known NMI re-entrancy path, a rate change that landed on a byte in flight, or something not yet
|
|
; identified. Clearing a flag that is already clear costs nothing, so it is not worth testing first.
|
|
; The answer it returns is also the answer startNextTxChar wants from its own first test, which is
|
|
; why it can replace that test outright: A = 0 (go ahead, nothing of ours is in the chip) when the
|
|
; transmit interrupt is off, and A = $04 (leave) when it is on - and when it is on, startNextTxChar
|
|
; would have refused at $E61B anyway, because it only arms a transmitter it finds idle.
|
|
; In: aciaCommandShadow, txCharActive
|
|
; Out: txCharActive = 0 when the transmit interrupt is disarmed; A = 0 / Z = 1 in that case, A = $04 /
|
|
; Z = 0 when it is armed; X and Y unchanged
|
|
; Called from: startNextTxChar $E607, which the once-per-frame link service falls into at $E604
|
|
; ----------------------------------------------------------------------
|
|
healStuckTransmitter:
|
|
lda aciaCommandShadow ; E057 the last byte we handed the command register
|
|
and #$04 ; E05A bit 2 = transmit interrupt armed; A = 0 when it is not
|
|
beq clearTxCharActive ; E05C disarmed - drop the flag and return A = 0
|
|
rts ; E05E armed - the NMI still owns the transmitter
|
|
.assert * = $E05F, error, "healStuckTransmitter must end at $E05E"
|
|
|
|
; linkStatusBorderTable: 8 entries indexed by connectionPhase*2 + carrier. Each byte is the border
|
|
; colour shifted left one place with bit 0 set when the colour should be shown even if the Commodore
|
|
; key is not held. Phase 1 (talking to the modem) is the only always-visible pair.
|
|
linkStatusBorderTable:
|
|
.byte $00 ; E05F . linkStatusBorderTable entry 0 (phase 0, no carrier): $00 = black, low bit clear so it is only painted while the Commodore key is held
|
|
linkStatusBorderPhase0Carrier:
|
|
.byte $0E ; E060 linkStatusBorderTable entries 1-7: value = colour*2 + 'always paint' flag. $0E yellow, $1D light blue + always, $0D blue + always, $14 light red, $04 red, $1A light green, $0A green
|
|
.byte $1D,$0D,$14,$04,$1A,$0A ; E061 ......
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearInlineVarBlock - shared 'zero the X bytes that follow my JSR' helper, used six times in this
|
|
; module. It pulls the return address off the stack, plants it in the absolute operand of the STA at
|
|
; clearBlockStore, and stores 0 through it with X counting down. The return address is gone, so the
|
|
; closing RTS returns two levels up, to the caller of the routine that made the JSR.
|
|
; In: X = number of bytes to clear (1..90); the JSR's return address points one byte before the
|
|
; inline block
|
|
; Out: the inline block is zeroed, A=0, X=0, and control returns to the caller's caller
|
|
; Called from: clearPacketVars $E01A, clearLinkVars $E036, clearProtocolVars $E07A, clearUartVars
|
|
; $E411 and clearPacketState $EAB4
|
|
; ----------------------------------------------------------------------
|
|
clearInlineVarBlock:
|
|
pla ; E067 take the low byte of the return address, which points at the byte before the inline block
|
|
sta clearBlockStore+1 ; E068 plant it in the address operand of the STA below
|
|
L_E06B:
|
|
pla ; E06B take the high byte of the return address as well - the RTS will now go one level further up
|
|
sta clearBlockStore+2 ; E06C plant the high byte too
|
|
lda #$00 ; E06F the block is cleared with zeroes
|
|
clearBlockStore:
|
|
sta L_E036+2,x ; E071 operand patched above; with X counting down this covers block+X-1 .. block, the X bytes right after the JSR
|
|
dex ; E074 next byte down
|
|
L_E075:
|
|
bne clearBlockStore ; E075 keep going until X reaches 0 (index 0 would hit the byte before the block and is skipped)
|
|
L_E077:
|
|
rts ; E077 return to the caller of the routine that called us
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearProtocolVars - zeroes the 43-byte protocol block $E07D-$E0A7: the completed-exchange counter,
|
|
; the receive and transmit packet state machines, both host character rings with their counts and
|
|
; indices, the in-flight counters and the service lock. The constant inFlightSlotIndex at $E0A8 is
|
|
; deliberately left out of the count.
|
|
; In: none
|
|
; Out: $E07D-$E0A7 = 0; returns to the caller of clearProtocolVars
|
|
; Called from: initCommModule $E2B1
|
|
; ----------------------------------------------------------------------
|
|
clearProtocolVars:
|
|
ldx #$2B ; E078 43 bytes to clear: exchangeCounter through serviceLock (inFlightSlotIndex at $E0A8 is left alone)
|
|
jsr clearInlineVarBlock ; E07A clear the inline block that follows and return to our caller's caller
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Protocol variables, $E07D-$E0A7, the inline block clearProtocolVars zeroes, plus the constant
|
|
; inFlightSlotIndex at $E0A8 which is deliberately left out of the count. It holds the receive and
|
|
; transmit packet state machines and the two character rings that connect the module to the game.
|
|
; ----------------------------------------------------------------------
|
|
exchangeCounterLo:
|
|
brk ; E07D exchangeCounterLo - low byte of the completed-exchange counter; nothing in the image ever reads it (rendered as BRK - data, never executed)
|
|
exchangeCounterHi:
|
|
.byte $00 ; E07E exchangeCounterHi - high byte of the same unused statistic
|
|
txHeaderExtraBits:
|
|
.byte $00 ; E07F txHeaderExtraBits: ORed into every outgoing packet header, but never written anywhere, so headers carry only the length
|
|
rxHeaderByte:
|
|
.byte $00 ; E080 rxHeaderByte: the header byte of the packet currently being received
|
|
rxPacketState:
|
|
.byte $00 ; E081 rxPacketState: 0 = expect a header, 1/2 = collecting a command payload, $80 = collecting chat bytes
|
|
rxBytesRemaining:
|
|
.byte $00 ; E082 rxBytesRemaining: payload bytes still to be read for this packet
|
|
txPacketState:
|
|
.byte $00 ; E083 txPacketState: bit 0 = payload in progress, bit 1 = frame handed over, bit 7 = the frame was chat
|
|
txPayloadIndex:
|
|
.byte $00 ; E084 txPayloadIndex: index of the next payload byte to send (and the byte count of a chat frame)
|
|
sendRequestCount:
|
|
.byte $00 ; E085 sendRequestCount: outstanding $E000/X=1 send requests
|
|
|
|
; hostInRing: 8-byte descending circular buffer of characters for the game, filled by the packet
|
|
; receiver and by the terminal-mode echo, drained through the $E006 entry.
|
|
hostInRing:
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; E086 ........ hostInRing: 8-byte descending ring of bytes the game reads with $E006
|
|
chatFrameCounter:
|
|
.byte $00 ; E08E chatFrameCounter: bytes still to pop while a chat frame is going out
|
|
hostOutReadIndex:
|
|
.byte $00 ; E08F hostOutReadIndex: next slot popHostOutRing will read (counts down, wraps 0 -> 15)
|
|
hostOutWriteIndex:
|
|
.byte $00 ; E090 hostOutWriteIndex: next slot pushHostOutRing will fill
|
|
|
|
; hostOutRing: 16-byte descending circular buffer of characters to send. The game pushes chat text
|
|
; through $E009; queueModemCommandString instead loads a whole AT string into slots 1..n, which is why
|
|
; the strings in page $EB are stored back to front.
|
|
hostOutRing:
|
|
.byte $00 ; E091 . hostOutRing: 16-byte descending ring of characters to send (slot 0; the AT-string loader starts at slot 1)
|
|
modemCommandChars:
|
|
.byte $00 ; E092 modemCommandChars = hostOutRing+1: queueModemCommandString writes an AT string here forwards, and the descending read index sends it back to front
|
|
.byte $00,$00,$00,$00,$00,$00,$00; E093 .......
|
|
.byte $00 ; E09A the rest of the 16-byte host output ring
|
|
.byte $00,$00,$00,$00,$00,$00 ; E09B ......
|
|
hostInCount:
|
|
.byte $00 ; E0A1 hostInCount: bytes waiting in hostInRing (maximum 8)
|
|
hostInReadIndex:
|
|
.byte $00 ; E0A2 hostInReadIndex: next slot popHostInRing will read
|
|
hostInWriteIndex:
|
|
.byte $00 ; E0A3 hostInWriteIndex: next slot pushHostInRing will fill
|
|
frameInFlightFlag:
|
|
.byte $00 ; E0A4 frameInFlightFlag: the uartPendingCount the frame layer saw when it queued a frame
|
|
uartPendingCount:
|
|
.byte $00 ; E0A5 uartPendingCount: characters queued for the UART that have not finished transmitting
|
|
packetInFlightCount:
|
|
.byte $00 ; E0A6 packetInFlightCount: bytes of the packet the packet layer just handed over, cleared once they have drained
|
|
serviceLock:
|
|
.byte $00 ; E0A7 serviceLock: rests at 1; serviceCommTick decrements it so a re-entrant call does nothing
|
|
inFlightSlotIndex:
|
|
.byte $01 ; E0A8 inFlightSlotIndex = 1: selects packetInFlightCount ($E0A5+1) as the counter the packet layer waits on; 0 would make it wait on the raw UART count instead
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popHostInRing - implementation of the $E006 entry. Pops the oldest byte out of the 8-byte hostInRing
|
|
; and undoes the EOR $80 the producers applied, so the game sees the character the opponent typed.
|
|
; In: hostInCount, hostInReadIndex, hostInRing
|
|
; Out: A = byte, C=0; C=1 and A undefined when the ring is empty; count and read index updated
|
|
; Called from: the $E006 jump-table entry
|
|
; ----------------------------------------------------------------------
|
|
popHostInRing:
|
|
lda hostInCount ; E0A9 any bytes waiting in the host input ring?
|
|
beq returnHostRingEmpty ; E0AC empty - return C=1
|
|
ldx hostInReadIndex ; E0AE oldest slot
|
|
lda hostInRing,x ; E0B1 fetch the character
|
|
eor #$80 ; E0B4 undo the EOR $80 the producers applied
|
|
dex ; E0B6 walk the read index down
|
|
bpl L_E0BB ; E0B7 still inside the ring?
|
|
ldx #$07 ; E0B9 wrapped past slot 0 - go back to the top slot of the 8-byte ring
|
|
L_E0BB:
|
|
stx hostInReadIndex ; E0BB store the new read index
|
|
dec hostInCount ; E0BE one byte fewer waiting
|
|
L_E0C1:
|
|
clc ; E0C1 C=0 says 'here is a byte'
|
|
rts ; E0C2 back to the game
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popHostOutRing - pops the next byte out of the 16-byte hostOutRing. Feeds the packet transmitter
|
|
; with chat characters, drains the ring when the link drops, and clocks canned modem command strings
|
|
; out in terminal mode.
|
|
; In: hostOutCount, hostOutReadIndex, hostOutRing
|
|
; Out: A = byte, C=0, or C=1 when the ring is empty
|
|
; Called from: sendPacketFsm $E253, runModemStateMachine $E78C, sendNextModemCommandChar $E7CA
|
|
; ----------------------------------------------------------------------
|
|
popHostOutRing:
|
|
lda hostOutCount ; E0C3 any characters queued for transmission?
|
|
beq returnHostRingEmpty ; E0C6 empty - return C=1
|
|
ldx hostOutReadIndex ; E0C8 oldest slot
|
|
lda hostOutRing,x ; E0CB fetch the character
|
|
dex ; E0CE walk the read index down
|
|
bpl L_E0D3 ; E0CF still inside the ring?
|
|
ldx #$0F ; E0D1 wrapped - go back to the top slot of the 16-byte ring
|
|
L_E0D3:
|
|
stx hostOutReadIndex ; E0D3 store the new read index
|
|
dec hostOutCount ; E0D6 one character fewer waiting
|
|
clc ; E0D9 C=0 says 'here is a byte'
|
|
rts ; E0DA return
|
|
|
|
returnHostRingEmpty:
|
|
sec ; E0DB shared 'that ring is empty' exit for both host-ring pops
|
|
L_E0DC:
|
|
rts ; E0DC return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushHostInRing - pushes one byte into the 8-byte hostInRing, the queue the game reads with $E006.
|
|
; Silently drops the byte when the ring already holds 8.
|
|
; In: A = byte
|
|
; Out: hostInRing, hostInCount, hostInWriteIndex
|
|
; Called from: receivePacketFsm $E1D3 (chat payload), runModemStateMachine $E7BD (echo of the modem's
|
|
; answer), sendNextModemCommandChar $E7F4 (fabricated CR when the modem does not answer)
|
|
; ----------------------------------------------------------------------
|
|
pushHostInRing:
|
|
ldx hostInCount ; E0DD how full is the host input ring?
|
|
cpx #$08 ; E0E0 the ring holds 8 bytes
|
|
bcs L_E0F5 ; E0E2 full - drop the byte on the floor
|
|
L_E0E4:
|
|
ldx hostInWriteIndex ; E0E4 next free slot
|
|
L_E0E7:
|
|
sta hostInRing,x ; E0E7 store the byte
|
|
dex ; E0EA walk the write index down
|
|
bpl L_E0EF ; E0EB still inside the ring?
|
|
ldx #$07 ; E0ED wrapped - back to the top slot
|
|
L_E0EF:
|
|
stx hostInWriteIndex ; E0EF store the new write index
|
|
inc hostInCount ; E0F2 one more byte waiting for the game
|
|
L_E0F5:
|
|
rts ; E0F5 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushHostOutRing - implementation of the $E009 entry: store A EOR $80 in the 16-byte hostOutRing and
|
|
; bump the count, or do nothing at all when the ring already holds 16 bytes.
|
|
; In: A = byte to send
|
|
; Out: hostOutRing, hostOutWriteIndex, hostOutCount
|
|
; Called from: the $E009 jump-table entry
|
|
; ----------------------------------------------------------------------
|
|
pushHostOutRing:
|
|
ldx hostOutCount ; E0F6 how full is the host output ring?
|
|
cpx #$10 ; E0F9 the ring holds 16 characters
|
|
bcs L_E110 ; E0FB full - drop the character silently
|
|
ldx hostOutWriteIndex ; E0FD next free slot
|
|
eor #$80 ; E100 characters travel with bit 7 inverted
|
|
sta hostOutRing,x ; E102 store it
|
|
dex ; E105 walk the write index down
|
|
bpl L_E10A ; E106 still inside the ring?
|
|
ldx #$0F ; E108 wrapped - back to the top slot
|
|
L_E10A:
|
|
stx hostOutWriteIndex ; E10A store the new write index
|
|
inc hostOutCount ; E10D one more character to transmit
|
|
L_E110:
|
|
rts ; E110 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commRequestDispatch - the dispatcher behind $E000. X=0 runs serviceCommTick, X=1 runs
|
|
; requestPacketSend, X>=2 acknowledges a finished exchange: if bit 6 of exchangeFlags is set both
|
|
; packet lengths and the whole flag byte are cleared, which releases rxPacketBuffer for the next
|
|
; packet.
|
|
; In: X = function code, exchangeFlags
|
|
; Out: one of the three cases above
|
|
; Called from: the $E000 jump-table entry
|
|
; ----------------------------------------------------------------------
|
|
commRequestDispatch:
|
|
dex ; E111 X=0 -> $FF, X=1 -> 0, X>=2 -> positive
|
|
bmi serviceCommTick ; E112 X was 0: run the per-frame service tick
|
|
beq requestPacketSend ; E114 X was 1: the game wants its packet transmitted
|
|
bit exchangeFlags ; E116 X was 2 or more: is the exchange finished (bit 6)?
|
|
L_E119:
|
|
bvs L_E11C ; E119 yes - consume it
|
|
rts ; E11B not finished yet, leave everything alone
|
|
|
|
L_E11C:
|
|
lda #$00 ; E11C clear both packet lengths and all the flags
|
|
sta rxPacketLength ; E11E the received packet has been read out
|
|
sta txPacketLength ; E121 the transmitted packet is done with
|
|
sta exchangeFlags ; E124 clearing the flags releases the buffers for the next exchange
|
|
L_E127:
|
|
rts ; E127 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; requestPacketSend - $E000 with X=1. The game has filled txPacketBuffer and set txPacketLength; this
|
|
; bumps bit 0 of exchangeFlags ('a send is pending') and the outstanding-request count, and
|
|
; sendPacketFsm picks the request up on the next service tick.
|
|
; In: none (txPacketLength/txPacketBuffer already filled by the caller)
|
|
; Out: exchangeFlags +1, sendRequestCount +1
|
|
; Called from: commRequestDispatch $E114
|
|
; ----------------------------------------------------------------------
|
|
requestPacketSend:
|
|
inc exchangeFlags ; E128 set bit 0 of exchangeFlags: a send is pending
|
|
inc sendRequestCount ; E12B count the request so sendPacketFsm knows the game is waiting
|
|
rts ; E12E return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; serviceCommTick - $E000 with X=0, called from the raster IRQ once per frame. The DEC/INC pair around
|
|
; the body is a re-entrancy lock (serviceLock rests at 1, so only the outermost call runs). It always
|
|
; drives the modem/connection state machine, and only once the connection has reached phase 3 with the
|
|
; link up does it also run the packet receive and packet transmit state machines.
|
|
; In: serviceLock, connectionPhase, isLinkActive
|
|
; Out: drives the whole stack; serviceLock restored to 1
|
|
; Called from: commRequestDispatch $E112
|
|
; ----------------------------------------------------------------------
|
|
serviceCommTick:
|
|
dec serviceLock ; E12F take the re-entrancy lock (rests at 1, so this reaches 0 only in the outermost call)
|
|
bne L_E149 ; E132 already inside the service - do nothing this frame
|
|
jsr runModemStateMachine ; E134 always drive the modem / connection state machine
|
|
lda connectionPhase ; E137 how far has the connection got?
|
|
cmp #$03 ; E13A phase 3 = the packet protocol is running
|
|
bcc L_E149 ; E13C not connected yet - no packet traffic
|
|
ldx isLinkActive ; E13E is the UART actually up?
|
|
beq L_E149 ; E141 link down - skip the packet layer
|
|
jsr receivePacketFsm ; E143 collect anything the opponent has sent
|
|
jsr sendPacketFsm ; E146 and push out whatever we have to say
|
|
L_E149:
|
|
inc serviceLock ; E149 release the lock
|
|
rts ; E14C return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; receivePacketFsm - the packet-layer receiver, a resumable state machine held in rxPacketState (0 =
|
|
; expecting a header, 1/2 = collecting a command payload, $80 = collecting chat bytes). The header
|
|
; byte's low three bits are the payload length; bit 7 means the payload is chat text that goes
|
|
; straight into hostInRing, otherwise it is a game command packet collected in rxPacketBuffer with the
|
|
; length in rxPacketLength. Headers $70-$7F are a third case: the payload is appended to the
|
|
; downloadable-code buffer at $EB03, and a $7x header with no payload calls it. A finished command
|
|
; packet sets bit 7 of exchangeFlags.
|
|
; In: rxPacketState, rxBytesRemaining, rxHeaderByte, bytes from receiveLinkByte
|
|
; Out: rxPacketBuffer, rxPacketLength, exchangeFlags bit 7, hostInRing, the $EB03 buffer
|
|
; Called from: serviceCommTick $E143
|
|
; ----------------------------------------------------------------------
|
|
receivePacketFsm:
|
|
ldx rxPacketState ; E14D where did we leave off?
|
|
bmi L_E1CC ; E150 state $80: we are in the middle of a chat payload
|
|
dex ; E152 state 0 -> $FF, 1 -> 0, 2 -> 1
|
|
beq L_E16B ; E153 state 1: the header is in, start the packet
|
|
bpl L_E183 ; E155 state 2 or more: keep collecting payload bytes
|
|
|
|
; ----------------------------------------------------------------------
|
|
; State 0: read the header byte and work out what kind of frame is arriving.
|
|
; ----------------------------------------------------------------------
|
|
jsr receiveLinkByte ; E157 state 0: try to read a header byte
|
|
bcs L_E1CB ; E15A nothing has arrived yet - come back next frame
|
|
tax ; E15C keep the header in X
|
|
sta rxHeaderByte ; E15D remember it for the $70-$7F test later
|
|
and #$07 ; E160 the low three bits are the payload length
|
|
L_E162:
|
|
sta rxBytesRemaining ; E162 that many payload bytes to collect
|
|
txa ; E165 look at the header again
|
|
bmi L_E1CC ; E166 bit 7 set: the payload is chat text for the game's message line
|
|
inc rxPacketState ; E168 command packet: advance to state 1
|
|
L_E16B:
|
|
bit exchangeFlags ; E16B has the game read the previous packet yet (bit 7 of exchangeFlags)?
|
|
bmi L_E1CB ; E16E no - leave the payload in the ring and wait
|
|
|
|
; ----------------------------------------------------------------------
|
|
; beginReceivePacket - the 'a packet is starting' step of receivePacketFsm: clear the received length
|
|
; and add 2 to exchangeFlags to mark 'reception in progress', then fall into the payload loop. Reached
|
|
; by fall-through here; the solo-trainer build calls it as a subroutine from $E07E.
|
|
; In: rxBytesRemaining = payload length
|
|
; Out: rxPacketLength = 0, exchangeFlags +2
|
|
; Called from: fall-through from $E16E in this build
|
|
; ----------------------------------------------------------------------
|
|
beginReceivePacket:
|
|
lda #$00 ; E170 start a fresh packet
|
|
sta rxPacketLength ; E172 nothing stored yet
|
|
inc exchangeFlags ; E175 add 2 to exchangeFlags: reception in progress
|
|
inc exchangeFlags ; E178 (two INCs because there is no ADC-style flag setter here)
|
|
lda rxBytesRemaining ; E17B how long is the payload?
|
|
beq L_E196 ; E17E a header-only packet - go straight to the delivery step
|
|
inc rxPacketState ; E180 advance to state 2: collecting the payload
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Payload loop: read the remaining bytes of a command packet into rxPacketBuffer, resuming from state
|
|
; 2 on a later frame whenever the link runs dry.
|
|
; ----------------------------------------------------------------------
|
|
L_E183:
|
|
jsr receiveLinkByte ; E183 read the next payload byte
|
|
bcs L_E1CB ; E186 nothing yet - resume from state 2 next frame
|
|
L_E188:
|
|
ldx rxPacketLength ; E188 append it to the receive buffer
|
|
sta rxPacketBuffer,x ; E18B rxPacketBuffer holds the command byte and up to 6 arguments
|
|
inc rxPacketLength ; E18E one more byte stored
|
|
dec rxBytesRemaining ; E191 one fewer to go
|
|
bne L_E183 ; E194 keep reading until the payload is complete
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The packet is complete. Headers $70-$7F carry 6502 code for the downloadable-code buffer at
|
|
; $EB03; everything else is a game command packet, which is announced by setting bit 7 of
|
|
; exchangeFlags so the main program can pick it up.
|
|
; ----------------------------------------------------------------------
|
|
L_E196:
|
|
lda rxHeaderByte ; E196 look at the header again
|
|
cmp #$70 ; E199 headers $70-$7F address the downloadable-code buffer
|
|
bcc L_E1C1 ; E19B an ordinary game command packet - just flag it
|
|
lda rxPacketLength ; E19D did this $7x message carry a payload?
|
|
bne L_E1A9 ; E1A0 yes - append it to the code buffer
|
|
jsr remoteCodeBuffer ; E1A2 no payload: call whatever the peer has downloaded ($EB03 holds a bare RTS in the shipped image)
|
|
ldx #$00 ; E1A5 and reset the buffer write offset
|
|
beq L_E1BE ; E1A7 always taken (Z was set by the LDX)
|
|
L_E1A9:
|
|
tya ; E1A9 Y is in use by the caller
|
|
pha ; E1AA so save it
|
|
ldy #$00 ; E1AB read the payload from the start
|
|
remoteCodeWriteIndex:
|
|
ldx #$00 ; E1AD operand patched at $E1BE: where in the code buffer to continue writing
|
|
L_E1AF:
|
|
lda rxPacketBuffer,y ; E1AF next payload byte
|
|
iny ; E1B2 advance through the received payload
|
|
sta remoteCodeBuffer,x ; E1B3 append it to the downloadable-code buffer at $EB03
|
|
inx ; E1B6 advance the buffer offset
|
|
dec rxPacketLength ; E1B7 count the byte off
|
|
bne L_E1AF ; E1BA until the whole payload has been copied
|
|
pla ; E1BC restore the caller's Y
|
|
tay ; E1BD restore the caller's Y
|
|
L_E1BE:
|
|
stx remoteCodeWriteIndex+1 ; E1BE self-modify: remember the buffer offset for the next $7x message
|
|
L_E1C1:
|
|
lda #$80 ; E1C1 bit 7 = 'a packet from the opponent is waiting'
|
|
jsr setExchangeFlags ; E1C3 flag it (and complete the exchange if our own packet has gone out)
|
|
L_E1C6:
|
|
ldx #$00 ; E1C6 back to state 0, expecting the next header
|
|
L_E1C8:
|
|
stx rxPacketState ; E1C8 store the new receive state
|
|
L_E1CB:
|
|
rts ; E1CB return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Chat payload: the header had bit 7 set, so its 0-3 bytes are text that goes straight into the ring
|
|
; the game reads with $E006 - it never touches the command packet buffer.
|
|
; ----------------------------------------------------------------------
|
|
L_E1CC:
|
|
jsr receiveLinkByte ; E1CC chat payload: read one character
|
|
ldx #$80 ; E1CF the state to save if nothing has arrived: stay in 'chat payload'
|
|
L_E1D1:
|
|
bcs L_E1C8 ; E1D1 nothing yet - save state $80 and resume next frame
|
|
jsr pushHostInRing ; E1D3 hand the character to the game's message line
|
|
dec rxBytesRemaining ; E1D6 one fewer chat byte to go
|
|
bne L_E1CC ; E1D9 keep going while the frame has more
|
|
beq L_E1C6 ; E1DB frame complete - back to state 0 (always taken)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendPacketFsm - the packet-layer transmitter, a resumable state machine in txPacketState (bit 0 =
|
|
; 'header sent, payload in progress', bit 1 = 'frame handed over, waiting for it to drain', bit 7 =
|
|
; 'that frame was chat, not a game packet'). With nothing in flight it prefers the pending game
|
|
; command packet (header = txPacketLength, then that many bytes from txPacketBuffer); otherwise it
|
|
; emits a chat frame with header $80|count and up to three characters from hostOutRing, which doubles
|
|
; as the keep-alive when both sides are idle. When the frame has drained it sets bit 5 of
|
|
; exchangeFlags and clears the state.
|
|
; In: txPacketState, sendRequestCount, hostOutCount, txPacketLength, txPacketBuffer,
|
|
; packetInFlightCount
|
|
; Out: bytes handed to sendLinkByte, packetInFlightCount = bytes+1, exchangeFlags bits 3/5,
|
|
; txPayloadIndex, chatFrameCounter
|
|
; Called from: serviceCommTick $E146
|
|
; ----------------------------------------------------------------------
|
|
sendPacketFsm:
|
|
lda txPacketState ; E1DD what is the transmitter doing?
|
|
lsr a ; E1E0 bit 0 into carry
|
|
bcs L_E222 ; E1E1 a payload is half way out - carry on where we stopped
|
|
lsr a ; E1E3 bit 1 into carry
|
|
bcc L_E1E9 ; E1E4 nothing in flight - decide what to send
|
|
jmp L_E26B ; E1E6 a frame has been handed over - just watch for it to drain
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Nothing is in flight: decide between the game's command packet, a chat frame, and the bare
|
|
; keep-alive frame that keeps the exchange ticking over when neither side has anything to say.
|
|
; ----------------------------------------------------------------------
|
|
L_E1E9:
|
|
ldx inFlightSlotIndex ; E1E9 1: the packet layer waits on packetInFlightCount
|
|
lda uartPendingCount,x ; E1EC is the previous frame still on its way out?
|
|
bne L_E1CB ; E1EF yes - nothing to do this tick
|
|
cmp sendRequestCount ; E1F1 A is 0 here, so C=1 means no send has been requested
|
|
lda hostOutCount ; E1F4 any chat characters queued?
|
|
beq L_E20A ; E1F7 none - only a command packet could go now
|
|
L_E1F9:
|
|
bcs L_E23A ; E1F9 chat waiting and no packet requested - send a chat frame
|
|
lda txPacketLength ; E1FB a packet was requested: is there anything in either packet buffer?
|
|
ora rxPacketLength ; E1FE and the length of the packet just received
|
|
bne L_E20A ; E201 yes - send the command packet
|
|
lda #$08 ; E203 $08 = the idle keep-alive marker
|
|
bit exchangeFlags ; E205 has that marker already been set in exchangeFlags?
|
|
beq L_E237 ; E208 not yet - set it and send one bare frame to keep the link ticking over
|
|
L_E20A:
|
|
bcs L_E1CB ; E20A no send requested (C=1) - nothing to do
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Send a game command packet: header = payload length, then the bytes the game left in txPacketBuffer.
|
|
; Any full ring simply stops the loop; the state byte makes it resume here.
|
|
; ----------------------------------------------------------------------
|
|
lda txPacketLength ; E20C the header is simply the payload length
|
|
ora txHeaderExtraBits ; E20F plus txHeaderExtraBits, which is always 0 in this build
|
|
jsr sendLinkByte ; E212 hand the header to the link
|
|
bcs L_E282 ; E215 the link ring is full - try again next tick
|
|
lda #$00 ; E217 0 into both counters below
|
|
sta txPayloadIndex ; E219 start the payload at byte 0
|
|
sta sendRequestCount ; E21C the request has been taken
|
|
L_E21F:
|
|
inc txPacketState ; E21F set bit 0: payload in progress
|
|
L_E222:
|
|
ldx txPayloadIndex ; E222 how many payload bytes have gone?
|
|
cpx txPacketLength ; E225 all of them?
|
|
beq L_E260 ; E228 yes - the frame is complete
|
|
lda txPacketBuffer,x ; E22A next byte of the command the game built
|
|
jsr sendLinkByte ; E22D hand it to the link
|
|
bcs L_E282 ; E230 ring full - resume from this byte next tick
|
|
inc txPayloadIndex ; E232 one more byte away
|
|
bne L_E222 ; E235 keep going (always taken)
|
|
L_E237:
|
|
jsr setExchangeFlags ; E237 A=$08: remember that the idle keep-alive has been sent
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Send a chat frame: header $80 | count, then up to three characters from hostOutRing. With the count
|
|
; at zero this is the one-byte $80 keep-alive.
|
|
; ----------------------------------------------------------------------
|
|
L_E23A:
|
|
lda hostOutCount ; E23A how many chat characters are waiting?
|
|
cmp #$03 ; E23D at most three travel in one frame
|
|
bcc L_E243 ; E23F fewer than three - send them all
|
|
lda #$03 ; E241 clamp to three
|
|
L_E243:
|
|
sta txPayloadIndex ; E243 that many payload bytes
|
|
sta chatFrameCounter ; E246 and that many pops to do
|
|
ora #$80 ; E249 chat frames are marked by bit 7 of the header
|
|
L_E24B:
|
|
jsr sendLinkByte ; E24B send the header, then each character in turn
|
|
dec chatFrameCounter ; E24E one fewer to pop
|
|
bmi L_E258 ; E251 all sent
|
|
jsr popHostOutRing ; E253 take the next character out of the host output ring
|
|
bcc L_E24B ; E256 and send it (C=1 would mean the ring lied about its count)
|
|
L_E258:
|
|
lda #$81 ; E258 state $81: a chat frame is on its way (bit 7 keeps it out of the exchange flags)
|
|
sta txPacketState ; E25A remember it
|
|
ldx txPayloadIndex ; E25D the payload byte count
|
|
L_E260:
|
|
inx ; E260 plus the header byte
|
|
txa ; E261 into A for the in-flight counter
|
|
ldx inFlightSlotIndex ; E262 1: the in-flight counter lives at $E0A6
|
|
L_E265:
|
|
sta uartPendingCount,x ; E265 tell the frame layer how many bytes belong to this packet
|
|
inc txPacketState ; E268 state -> 2 (or $82 for chat): waiting for the frame to drain
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Both paths end here: wait until the frame layer says the bytes have really gone, then flag 'own
|
|
; packet sent' (chat frames are excluded) and go idle.
|
|
; ----------------------------------------------------------------------
|
|
L_E26B:
|
|
ldx inFlightSlotIndex ; E26B 1: read the in-flight counter
|
|
lda uartPendingCount,x ; E26E has the frame left the machine?
|
|
bne L_E282 ; E271 not yet - keep waiting
|
|
bit txPacketState ; E273 was it a chat frame (bit 7)?
|
|
bmi L_E27D ; E276 yes - chat does not complete a game exchange
|
|
lda #$20 ; E278 bit 5 = 'our own packet has gone out'
|
|
jsr setExchangeFlags ; E27A flag it (and complete the exchange if the opponent's packet has arrived)
|
|
L_E27D:
|
|
lda #$00 ; E27D transmitter idle again
|
|
sta txPacketState ; E27F clear the transmit state
|
|
L_E282:
|
|
rts ; E282 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; countCompletedExchange - bumps the 16-bit exchangeCounter and then sets bit 6 of exchangeFlags
|
|
; ('exchange complete'). Reached from setExchangeFlags exactly once per finished send+receive pair.
|
|
; Nothing in the whole image ever reads exchangeCounter, so it looks like a leftover debug statistic.
|
|
; In: exchangeCounterLo/Hi
|
|
; Out: exchangeCounter +1, falls into setExchangeFlags with A=$40
|
|
; Called from: setExchangeFlags $E297
|
|
; ----------------------------------------------------------------------
|
|
countCompletedExchange:
|
|
inc exchangeCounterLo ; E283 count this completed exchange (low byte)
|
|
bne L_E28B ; E286 no carry into the high byte
|
|
inc exchangeCounterHi ; E288 16-bit counter that nothing ever reads
|
|
L_E28B:
|
|
lda #$40 ; E28B bit 6 = 'exchange complete', which is what the game waits for
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setExchangeFlags - ORs A into exchangeFlags and checks for a completed exchange: when bits 7 and 5
|
|
; are both set but bit 6 is not (top three bits = %101) the round trip is done, so it drops into
|
|
; countCompletedExchange which sets bit 6 as well. A=$80 marks 'opponent packet received', $20 'own
|
|
; packet sent', $40 'complete', $08 the idle keep-alive marker.
|
|
; In: A = flag bits to add
|
|
; Out: exchangeFlags updated, possibly exchangeCounter
|
|
; Called from: receivePacketFsm $E1C3, sendPacketFsm $E237/$E27A
|
|
; ----------------------------------------------------------------------
|
|
setExchangeFlags:
|
|
ora exchangeFlags ; E28D add the new flag bits
|
|
sta exchangeFlags ; E290 store the updated flags
|
|
and #$E0 ; E293 look at bits 7, 6 and 5 only
|
|
eor #$A0 ; E295 received + sent but not yet marked complete?
|
|
beq countCompletedExchange ; E297 yes - the round trip is finished
|
|
rts ; E299 otherwise just return with the flag added
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commLinkControlDispatch - the dispatcher behind $E003. X=0 opens the link, X=1 cold-initialises the
|
|
; module, X=2 stops the NMI UART and clears the link variables, X>=3 first sends the Hayes hang-up
|
|
; string, spins until it has gone out, and then does the same shutdown.
|
|
; In: X = function code
|
|
; Out: see the four cases; every path except X=0/X=1 ends by zeroing $E039-$E047
|
|
; Called from: the $E003 jump-table entry
|
|
; ----------------------------------------------------------------------
|
|
commLinkControlDispatch:
|
|
dex ; E29A X=0 -> $FF, 1 -> 0, 2 -> 1, 3 -> 2
|
|
bmi openCommLink_E2C1 ; E29B X was 0: open the link
|
|
beq initCommModule_E2B1 ; E29D X was 1: cold-initialise the module
|
|
L_E29F:
|
|
dex ; E29F X=2 -> $FF, X=3 -> 0
|
|
beq L_E2AB ; E2A0 X was 2: shut the UART down without hanging up
|
|
jsr hangUpModem_E38D ; E2A2 X>=3: queue '+++' / ATH0 first
|
|
L_E2A5:
|
|
lda modemReplyTimer ; E2A5 wait for the hang-up string to be delivered
|
|
lsr a ; E2A8 modemReplyTimer 0 or 1 means the modem has answered / given up
|
|
bne L_E2A5 ; E2A9 spin until it does
|
|
L_E2AB:
|
|
jsr stopCommNmi ; E2AB stop the NMI UART
|
|
jmp clearLinkVars ; E2AE and zero the link variables (its RTS returns to the $E003 caller)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; initCommModule (the $E003 X=1 case) - cold start, called once immediately after the overlay has been
|
|
; loaded. Clears the protocol block, the packet block, all four ring buffers and the ARQ/frame state
|
|
; block, and points the three indirect link I/O jumps at their framed implementations.
|
|
; In: none
|
|
; Out: $E07D-$E0A7, $E01D-$E02F, $E414-$E46D and $EAB7-$EB02 zeroed; the operands at $E3B7/$E3BA/$E3BD
|
|
; patched
|
|
; Called from: commLinkControlDispatch $E29D (reached from initCommModule $0F46 with X=1)
|
|
; ----------------------------------------------------------------------
|
|
initCommModule_E2B1:
|
|
jsr clearProtocolVars ; E2B1 wipe the protocol block $E07D-$E0A7
|
|
jsr clearPacketVars ; E2B4 wipe the packet block $E01D-$E02F
|
|
D_E2B7:
|
|
jsr clearUartVars ; E2B7 wipe all four ring buffers $E414-$E46D
|
|
jsr selectFramedLinkVectors ; E2BA route link I/O through the framed (ARQ) rings
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearArqVars - tail of initCommModule: zero the 76-byte frame/ARQ state block $EAB7-$EB02 (sync
|
|
; counters, sequence numbers, the retransmit ring, the packet assembly buffer and the modem delay
|
|
; bytes).
|
|
; In: none
|
|
; Out: $EAB7-$EB02 = 0
|
|
; Called from: fall-through from initCommModule $E2BA
|
|
; ----------------------------------------------------------------------
|
|
clearArqVars:
|
|
jsr clearPacketState ; E2BD wipe the frame/ARQ state block $EAB7-$EB02
|
|
rts ; E2C0 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; openCommLink (the $E003 X=0 case) - bring the link up. If it is not already up it loads the baud
|
|
; parameters for the selected speed, installs this module's NMI vector, starts the software UART and
|
|
; marks the link active. It then always re-programs the user-port lines and, unless the C=+RETURN hot
|
|
; key asked for a silent re-open, queues the Hayes init string for answer or originate mode. Finally
|
|
; it releases the per-frame service lock.
|
|
; In: isLinkActive, baudIndex, skipModemInitString, isOriginateMode
|
|
; Out: isLinkActive = 1, NMI vector at $FFFA/$FFFB, CIA2 configured, an AT string queued, serviceLock
|
|
; = 1
|
|
; Called from: commLinkControlDispatch $E29B, and the C=+RETURN hot key at $E369
|
|
; ----------------------------------------------------------------------
|
|
openCommLink_E2C1:
|
|
lda isLinkActive ; E2C1 is the UART already running?
|
|
bne L_E2D5 ; E2C4 yes - only refresh the port lines and the AT string
|
|
ldx baudIndex ; E2C6 0 = 300 baud, 3 = 1200 baud
|
|
jsr loadBaudParameters ; E2C9 load that speed's timer values
|
|
jsr installCommNmiVector ; E2CC point $FFFA/$FFFB at commNmiHandler
|
|
jsr restartUart ; E2CF clear the bit-level state and arm the CIA2 NMI sources
|
|
inc isLinkActive ; E2D2 the link is now up
|
|
L_E2D5:
|
|
jsr configureUserPortLines ; E2D5 set RTS/DTR, DDRB and the idle TXD level
|
|
lda skipModemInitString ; E2D8 did C=+RETURN ask for a silent re-open?
|
|
bne L_E2E6 ; E2DB yes - do not send the AT string again
|
|
ldx isOriginateMode ; E2DD 0 = answer, 1 = originate
|
|
L_E2E0:
|
|
lda modemInitStringPtrLoTable,x; E2E0 low byte of 'ATQ0V1X1A' ($E3) or 'ATQ0V1X1D' ($F0), both stored back to front
|
|
jsr queueModemCommandString ; E2E3 load it into the host output ring for the terminal-mode transmitter
|
|
L_E2E6:
|
|
lda #$01 ; E2E6 the per-frame service lock rests at 1
|
|
sta serviceLock ; E2E8 release it, so serviceCommTick may run
|
|
L_E2EB:
|
|
rts ; E2EB return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readKeyAndHandleModemHotkeys - the implementation of $E015. It scans the keyboard through the
|
|
; patched hook, optionally repaints the border with the link-status colour, and then interprets a
|
|
; Commodore-key combination as a modem hot key: H toggles DTR, C toggles the carrier override, U goes
|
|
; back to the port's own line settings, 3/1/2/4/9/0/8 impose 300, 1200, 2400, 4800, 9600, 19200 or
|
|
; 38400 baud instead, RETURN re-opens the link without re-sending the init string, P hangs up, A
|
|
; selects answer mode and O selects originate mode. A hot key is swallowed by returning $FF so the
|
|
; game never sees it.
|
|
; In: the keyboard hook result (A = key code, X bit 7 = Commodore key), isLinkActive, linkStatus,
|
|
; connectionPhase, lastHotkeyCode
|
|
; Out: A = key code or $FF; may write VIC_BORDER, lastHotkeyCode, isOriginateMode, baudIndex,
|
|
; carrierOverrideFlags, CIA2_PRB, or queue a modem command string
|
|
; Called from: the $E015 jump-table entry
|
|
; ----------------------------------------------------------------------
|
|
readKeyAndHandleModemHotkeys:
|
|
jsr keyboardScanHook ; E2EC scan the keyboard (the game patched this vector to $0DB7)
|
|
pha ; E2EF keep the key code
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Link status indicator: while the link is up the border shows the connection phase and carrier state.
|
|
; Phase 1 (terminal mode) paints unconditionally, the other phases only while the Commodore key is
|
|
; held down, which is also when the hot keys below are active.
|
|
; ----------------------------------------------------------------------
|
|
L_E2F0:
|
|
lda isLinkActive ; E2F0 is there a link to report on?
|
|
beq L_E30F ; E2F3 no - leave the border alone
|
|
stx hotkeyCommodoreFlag+1 ; E2F5 self-modify: stash the Commodore-key flag, because the border code needs X
|
|
lda linkStatus ; E2F8 debounced link status
|
|
asl a ; E2FB shift bit 7 (data flowing) out
|
|
asl a ; E2FC now carry holds bit 6 = carrier present
|
|
lda connectionPhase ; E2FD connection phase 0-3
|
|
rol a ; E300 index = phase*2 + carrier
|
|
tax ; E301 into X to index the colour table
|
|
lda linkStatusBorderTable,x ; E302 colour*2 + 'always paint' flag
|
|
lsr a ; E305 carry = always-paint flag, A = the colour
|
|
hotkeyCommodoreFlag:
|
|
ldx #$00 ; E306 operand patched at $E2F5: $80 while the Commodore key is held
|
|
bmi L_E30C ; E308 Commodore key held - show the link status whatever the phase
|
|
bcc L_E30F ; E30A otherwise only phases 1 (terminal mode) paint on their own
|
|
L_E30C:
|
|
sta VIC_BORDER ; E30C paint the border with the link status colour
|
|
L_E30F:
|
|
pla ; E30F get the key code back
|
|
bpl L_E31B ; E310 codes below $80 are ordinary keys, never hot keys
|
|
cmp lastHotkeyCode ; E312 the same code as last time?
|
|
bne L_E31C ; E315 no - a fresh key press
|
|
cpx #$80 ; E317 same key: is the Commodore key still down?
|
|
L_E319:
|
|
bcs L_E33F ; E319 yes - swallow the repeat
|
|
L_E31B:
|
|
rts ; E31B hand the key to the game
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Modem hot keys - a new key code with bit 7 set while the Commodore key is held. Every one of them is
|
|
; swallowed by returning $FF so the game never sees it.
|
|
; ----------------------------------------------------------------------
|
|
L_E31C:
|
|
sta lastHotkeyCode ; E31C remember the code so it is not handled twice
|
|
cpx #$80 ; E31F was the Commodore key held?
|
|
bcc L_E31B ; E321 no - it is an ordinary key for the game
|
|
cmp #$C8 ; E323 C=+H
|
|
bne L_E332 ; E325 not that key - try the next
|
|
lda #$01 ; E327 command register bit 0 is DTR
|
|
eor aciaCommandShadow ; E329 flip it in the shadow, since the register cannot be read back on every clone
|
|
jsr aciaSetCommand ; E32C and hand the new value to the ACIA
|
|
bcs L_E33F ; E32F carry is still set from the CPX at $E31F - swallow the key
|
|
.assert * <= $E332, error, "C=+H hot key overran $E331"
|
|
.res $E332 - *, $EA ; E331 was the last byte of 'sta CIA2_PRB'
|
|
L_E332:
|
|
cmp #$C3 ; E332 C=+C
|
|
bne L_E342 ; E334 not that key - try the next
|
|
lda carrierOverrideToggleMask; E336 $40 = the carrier-override bit
|
|
L_E339:
|
|
eor carrierOverrideFlags ; E339 flip 'pretend the carrier is present'
|
|
sta carrierOverrideFlags ; E33C (lets two machines play over a direct cable with no DCD)
|
|
L_E33F:
|
|
lda #$FF ; E33F $FF tells the game 'no key'
|
|
rts ; E341 return
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Baud hot keys. The stock driver knew two speeds and tested for them one key at a time; the
|
|
; SwiftLink knows seven, so the same fourteen bytes now walk baudEntryTable backwards looking for the
|
|
; key code. Entries are still three bytes apart and still in the stock order, so index 0 is 300 baud
|
|
; and index 3 is 1200 baud - which is what openCommLink's disk-default baudIndex and the CONNECT 1200
|
|
; sniffer at $E7B0 both assume. C=+3 and C=+1 therefore keep their old meanings, and C=+2, C=+4,
|
|
; C=+9, C=+0 add 2400, 4800, 9600 and 19200. Anything else falls through to $E362.
|
|
; C=+8 selects 38400 again. Its hot-key byte was blanked when 38400 wedged the machine, which hid the
|
|
; defect instead of fixing it: wedgeDiagnosis.md then traced the wedge to unbounded NMI re-entrancy in
|
|
; commNmiHandler, and the guard at $E68F bounds it at one nested frame whatever the rate, so the entry
|
|
; is reachable from the keyboard once more.
|
|
; The walk now starts at 21, not 18, because there is an eighth entry: ADOPT. C=+U selects it and it
|
|
; means 'stop imposing a rate - use the control register the user's hardware already holds'. It is
|
|
; also the disk default, so a link opened without pressing anything comes up on the port's own
|
|
; settings. Pressing any of the seven speed keys stores that entry's index instead and takes the
|
|
; driver out of adopt mode until C=+U is pressed again.
|
|
; ----------------------------------------------------------------------
|
|
L_E342:
|
|
ldx #BAUD_INDEX_ADOPT ; E342 start at the last entry, 21 = adopt: C=+U goes back to whatever the user's own hardware is configured for, and it costs nothing to search for it because the walk had to start somewhere. 18 = 38400 is the entry below it and its hot key, C=+8, works again - the wedge it used to cause was an NMI re-entrancy fault in commNmiHandler, not a property of the rate, and the guard at $E68F fixes it (wedgeDiagnosis.md)
|
|
L_E344:
|
|
cmp baudEntryTable,x ; E344 does the key code match this entry?
|
|
beq L_E350 ; E347 yes - this is the speed the player asked for
|
|
dex ; E349 three bytes per entry, the same stride the stock ...
|
|
dex ; E34A ... baudParameterTable used, so that every ...
|
|
dex ; E34B ... existing caller's index still means the same speed
|
|
bpl L_E344 ; E34C keep looking
|
|
bmi L_E362 ; E34E ran off the bottom - not a baud key, try the rest
|
|
L_E350:
|
|
stx baudIndex ; E350 remember the speed for the next open
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadBaudParameters - hand the 6551 the control byte the entry selected by X carries. For the seven
|
|
; fixed rates that byte is $10 (8 data bits, one stop bit, internal baud rate generator) OR the rate
|
|
; bits; a SwiftLink clocks its 6551 from a 3.6864 MHz crystal, twice what the chip expects, so every
|
|
; rate in the data sheet's table comes out doubled and $15 is 300 baud rather than 150.
|
|
; For X = 21 - adopt, the disk default - the byte is not a constant at all: aciaProbeAdopted $E655
|
|
; put the user's own control register there on the cold start, so what this routine writes back is the
|
|
; rate, stop bits and clock source the hardware was already configured for. Either way the routine
|
|
; itself does not care: it reads the table and writes what it finds, and the only thing it insists on
|
|
; comes from the entry, not from here.
|
|
; The byte is parked in bitPeriodLo, where the software UART's bit period used to live, so the live
|
|
; setting still sits in the module's own constants and can be read from a monitor.
|
|
; Returns A = $FF, which is what the hot-key path needs in order to swallow the key.
|
|
; In: X = 0, 3, 6, 9, 12, 15, 18 (fixed rates) or 21 (adopt) - a three-byte index into baudEntryTable
|
|
; Out: the ACIA control register and bitPeriodLo programmed, A = $FF, X unchanged, Y clobbered by
|
|
; aciaDetect on the one call per cold start that probes
|
|
; Called from: openCommLink $E2C9, the baud hot keys (fall-through from $E350),
|
|
; runModemStateMachine $E7B2 when it reads a 'CONNECT 1200' result code
|
|
; ----------------------------------------------------------------------
|
|
loadBaudParameters:
|
|
jsr aciaDetect ; E353 make sure we know which page the cartridge answers on. On every call but the first this is five bytes and an RTS: aciaPageLatch $E5BF already holds the answer, so a baud hot key on a live link re-programs the chip without re-probing for it and without writing a probe pattern into whatever is at $DE00
|
|
lda baudEntryTable+1,x ; E356 the control byte for this speed
|
|
sta bitPeriodLo ; E359 keep the live value where the bit period used to be
|
|
jsr aciaSetControlIdle ; E35C 8N1 with the internal baud rate generator, then forget any byte in the transmitter - was 'jsr aciaSetControl' ($20 $67 $E5). A rate change is the one thing that can strand a character half way out of a 6551: the control register is rewritten under it and what the chip then does with the byte in its shift register is undefined, so its TDRE may never come back and txCharActive would stay set for ever. Clearing it here lets the next tick re-arm from a known state
|
|
lda #$FF ; E35F swallow the hot key
|
|
rts ; E361 return
|
|
.assert * <= $E362, error, "loadBaudParameters overran $E361"
|
|
|
|
L_E362:
|
|
cmp #$8D ; E362 C=+RETURN
|
|
bne L_E374 ; E364 not that key - try the next
|
|
dec skipModemInitString ; E366 0 -> $FF: re-open without re-sending the AT string
|
|
jsr openCommLink_E2C1 ; E369 bring the link back up
|
|
lda #$8D ; E36C a negative reply timer means 'not waiting for an answer'
|
|
sta modemReplyTimer ; E36E so the state machine does not time the modem out
|
|
ldx #$00 ; E371 clear the Commodore-key flag the caller sees
|
|
rts ; E373 and pass RETURN on to the game
|
|
|
|
L_E374:
|
|
cmp #$D0 ; E374 C=+P
|
|
beq hangUpModem_E38D ; E376 hang up (its tail returns $FF, so the key is swallowed)
|
|
ldx #$00 ; E378 default: answer mode
|
|
L_E37A:
|
|
cmp #$C1 ; E37A C=+A
|
|
beq L_E384 ; E37C answer mode wanted
|
|
cmp #$CF ; E37E C=+O
|
|
bne L_E38A ; E380 some other Commodore-key combination - just swallow it
|
|
ldx #$01 ; E382 originate mode
|
|
L_E384:
|
|
stx isOriginateMode ; E384 remember which AT string to send
|
|
jsr configureUserPortLines ; E387 the port lines depend on answer/originate
|
|
L_E38A:
|
|
lda #$FF ; E38A swallow the hot key
|
|
rts ; E38C return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; hangUpModem - drop DTR and then queue the canned Hayes hang-up string, which the terminal-mode
|
|
; transmitter emits in reverse as [guard] '+++' [guard] CR [guard] 'ATH0' CR.
|
|
; In: hangUpStringPtrLo ($EBFD = $D6), modemOptionFlags bit 7
|
|
; Out: CIA2_PRB with DTR low, hostOutRing loaded with the escape/hang-up string, modemReplyTimer = $FF
|
|
; Called from: commLinkControlDispatch $E2A2 (X>=3), the C=+P hot key at $E376
|
|
; ----------------------------------------------------------------------
|
|
hangUpModem_E38D:
|
|
jsr dropDtrLine ; E38D drop DTR, which by itself makes most modems hang up
|
|
lda hangUpStringPtrLo ; E390 $D6 = low byte of the '+++ ... ATH0' string at $EBD6
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueModemCommandString - load a canned modem command string into the host-output ring. A is the low
|
|
; byte of an address in page $EB; the routine plants it in the LDA operand at modemStringLoad, copies
|
|
; bytes (including the terminating 0) forwards into the ring at modemCommandChars, then sets the write
|
|
; index to 0 and both the read index and the count to the number of characters. Since the ring pops
|
|
; with a decrementing index the string leaves the machine back to front, which is exactly why the AT
|
|
; strings at $EBD6-$EBFC are stored reversed. Does nothing but return A=$FF when bit 7 of
|
|
; modemOptionFlags says no modem is configured.
|
|
; In: A = low byte of the string address in page $EB, modemOptionFlags
|
|
; Out: modemCommandChars.., hostOutReadIndex, hostOutWriteIndex, hostOutCount, modemReplyTimer = $FF,
|
|
; A = $FF
|
|
; Called from: openCommLink $E2E3, hangUpModem via fall-through from $E390
|
|
; ----------------------------------------------------------------------
|
|
queueModemCommandString:
|
|
ldx modemOptionFlags ; E393 is a modem configured at all (bit 7)?
|
|
bpl returnNoKey ; E396 no modem - just return $FF
|
|
sta modemStringLoad+1 ; E398 self-modify the LDA below with the string's low byte
|
|
ldx #$FF ; E39B start one before the first character
|
|
stx modemReplyTimer ; E39D negative reply timer: a command string is going out
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Copy loop: the string is copied forwards into the ring, terminator included, and the read index is
|
|
; then set to the last character so the descending pop order sends it in the right order.
|
|
; ----------------------------------------------------------------------
|
|
L_E3A0:
|
|
inx ; E3A0 next character
|
|
modemStringLoad:
|
|
lda D_EB00,x ; E3A1 operand low byte patched at $E398; the page is fixed at $EB
|
|
sta modemCommandChars,x ; E3A4 copy it into the host output ring from slot 1 upwards
|
|
bne L_E3A0 ; E3A7 keep going until the terminating 0 has been copied
|
|
sta hostOutWriteIndex ; E3A9 A is 0: the ring is written from the bottom again
|
|
stx hostOutReadIndex ; E3AC read index = the character count, so the string pops out back to front
|
|
stx hostOutCount ; E3AF that many characters to send (the terminator is never popped)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; returnNoKey - the shared 'no key / key handled' return: A = $FF. It is the disk-image target of the
|
|
; $E012 keyboard hook and the early exit of queueModemCommandString when no modem is configured.
|
|
; In: none
|
|
; Out: A = $FF
|
|
; Called from: keyboardScanHook $E012 (before the game patches it), queueModemCommandString $E396, and
|
|
; by fall-through from $E3AF
|
|
; ----------------------------------------------------------------------
|
|
returnNoKey:
|
|
lda #$FF ; E3B2 $FF = 'no key' / 'handled'
|
|
rts ; E3B4 return
|
|
|
|
|
|
; padByteE3B5: unknown, 1 bytes. Stray $10 between returnNoKey and the indirect link I/O jumps; not
|
|
; referenced.
|
|
padByteE3B5:
|
|
.byte $10 ; E3B5 unreferenced pad byte between returnNoKey and the indirect jumps
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendLinkByte - indirect 'give one byte to the link'. The operand byte at $E3B7 selects which half of
|
|
; linkIoVectorTable is used: $BF = raw (straight into the UART transmit ring) or $C1 = framed (into
|
|
; the ARQ transmit ring). initCommModule always selects the framed one, so the raw target is dead in
|
|
; the shipped game.
|
|
; In: A = byte
|
|
; Out: as the selected ring push; C=1 when that ring is full
|
|
; Called from: sendPacketFsm $E212/$E22D/$E24B
|
|
; ----------------------------------------------------------------------
|
|
sendLinkByte:
|
|
jmp (linkIoVectorTable) ; E3B6 operand low byte is $BF (raw UART ring) or $C1 (framed ARQ ring)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; receiveLinkByte - indirect 'take one byte from the link'. The operand at $E3BA selects $C3 = raw
|
|
; (pop the UART receive ring) or $C5 = framed (pop the ARQ receive ring, i.e. only payload the frame
|
|
; layer has already checksum-verified).
|
|
; In: none
|
|
; Out: A = byte with C=0, or C=1 when nothing is queued
|
|
; Called from: receivePacketFsm $E157/$E183/$E1CC
|
|
; ----------------------------------------------------------------------
|
|
receiveLinkByte:
|
|
jmp ($E3C3) ; E3B9 operand low byte is $C3 (raw UART ring) or $C5 (framed ARQ ring)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; reportLinkError - indirect link-error hook, called by the NMI receiver on a framing error, a bad
|
|
; stop bit or a receive-ring overflow. The operand at $E3BD selects $C7 = the no-op stub or $C9 = the
|
|
; counting stub; initCommModule selects the counter, whose value makes the link state machine restart
|
|
; byte sync.
|
|
; In: none
|
|
; Out: linkErrorCount +1 in the framed configuration
|
|
; Called from: commNmiHandler $E6B3
|
|
; ----------------------------------------------------------------------
|
|
reportLinkError:
|
|
jmp ($E3C7) ; E3BC operand low byte is $C7 (ignore) or $C9 (count the error)
|
|
|
|
|
|
; linkIoVectorTable: three pairs of alternative targets for the indirect jumps above. The first of
|
|
; each pair is the raw UART implementation, the second the framed (ARQ) one that
|
|
; selectFramedLinkVectors installs. Only the operand bytes of the JMPs are patched, never this table.
|
|
linkIoVectorTable:
|
|
.addr pushUartTxRing ; E3BF send, raw: straight into the UART transmit ring (never selected)
|
|
.byte $B7 ; E3C1 send, framed: into the ARQ transmit ring - the one initCommModule selects
|
|
.byte $E4 ; E3C2 .
|
|
.byte $D2 ; E3C3 receive, raw: pop the UART receive ring (never selected)
|
|
.byte $E4 ; E3C4 .
|
|
.byte $6E ; E3C5 receive, framed: pop the checksum-verified ARQ receive ring
|
|
.byte $E4 ; E3C6 .
|
|
.byte $3F ; E3C7 error, raw: was the no-op stub $E408, which is dead code the module no longer contains; both halves of the pair now point at countLinkError so neither can land on something that is not an error hook (never selected)
|
|
L_E3C8:
|
|
.byte $E7 ; E3C8 .
|
|
.byte $3F ; E3C9 error, framed: count it in linkErrorCount - countLinkError moved from $E403 to $E73F so that selectFramedLinkVectors could grow the five bytes that clear aciaPageLatch
|
|
.byte $E7 ; E3CA .
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pollCarrierState - implementation of $E00C. Takes a fresh carrier sample (which also services the
|
|
; NMI suspend handshake), then debounces it: while the sample disagrees with the accepted linkStatus
|
|
; it counts up, and only after 240 consecutive disagreeing samples does it accept the new value. In
|
|
; between it merges just bit 7 of the sample, the 'characters are still arriving' bit, which the game
|
|
; watches before hanging up. Bit 6 of linkStatus is the debounced 'carrier present' flag.
|
|
; In: linkStatusSample, linkStatus, carrierDebounceCount
|
|
; Out: linkStatus, carrierDebounceCount
|
|
; Called from: the $E00C jump-table entry
|
|
; ----------------------------------------------------------------------
|
|
pollCarrierState:
|
|
jsr serviceCarrierAndSuspendRequest; E3CB run the suspend handshake and take a fresh carrier sample
|
|
lda linkStatusSample ; E3CE the new sample
|
|
cmp linkStatus ; E3D1 does it agree with the accepted status?
|
|
beq L_E3E3 ; E3D4 yes - reset the debounce counter
|
|
inc carrierDebounceCount ; E3D6 no - count one disagreeing sample
|
|
ldx carrierDebounceCount ; E3D9 how long has it disagreed?
|
|
cpx #$F0 ; E3DC 240 frames of disagreement (about 4 seconds)
|
|
bcc L_E3EA ; E3DE not yet - only take the activity bit for now
|
|
sta linkStatus ; E3E0 accept the new carrier state
|
|
L_E3E3:
|
|
ldx #$00 ; E3E3 restart the debounce count
|
|
stx carrierDebounceCount ; E3E5 and start counting again from zero
|
|
beq L_E3F2 ; E3E8 always taken
|
|
L_E3EA:
|
|
and #$80 ; E3EA bit 7 only: 'characters are still arriving'
|
|
ora linkStatus ; E3EC merge it into the status without touching the carrier bit
|
|
sta linkStatus ; E3EF store the merged status
|
|
L_E3F2:
|
|
rts ; E3F2 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; selectFramedLinkVectors - rewrites the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD
|
|
; so that sendLinkByte, receiveLinkByte and reportLinkError use the framed (ARQ) rings and the error
|
|
; counter instead of the raw UART rings and the no-op error stub. Note it patches the pointers to the
|
|
; vectors, not the vectors themselves.
|
|
; It is also where the ACIA page latch is cleared, and the only place that clears it. This is the one
|
|
; routine in the module that runs exactly once per cold start and nowhere else: initCommModule $E2BA
|
|
; is its only caller, and initCommModule is the $E003 X=1 entry the game makes immediately after the
|
|
; overlay is loaded. Clearing the latch here is therefore 'clear it on a cold start and never again',
|
|
; which is exactly what aciaDetect needs - a link open or a baud hot key must not re-decide which page
|
|
; the cartridge is on. The five bytes it takes came from countLinkError, which moved to $E73F.
|
|
; In: none
|
|
; Out: $E3B7 = $C1, $E3BA = $C5, $E3BD = $C9, aciaPageLatch = 0
|
|
; Called from: initCommModule $E2BA (the only caller, so the raw configuration never runs)
|
|
; ----------------------------------------------------------------------
|
|
selectFramedLinkVectors:
|
|
lda #$C1 ; E3F3 $C1 = pushArqTxRing
|
|
sta sendLinkByte+1 ; E3F5 patch the operand of the JMP ($xxxx) at sendLinkByte
|
|
lda #$C5 ; E3F8 $C5 = popArqRxRing
|
|
sta receiveLinkByte+1 ; E3FA patch the operand at receiveLinkByte
|
|
lda #$C9 ; E3FD $C9 = countLinkError
|
|
sta reportLinkError+1 ; E3FF patch the operand at reportLinkError
|
|
lda #$00 ; E402 cold start: forget which page the cartridge was on...
|
|
sta aciaPageLatch ; E404 ...so the next link open probes for it once more
|
|
rts ; E407 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; dropDtrLine - clear the whole command register. Bit 0 is DTR, and dropping it is what makes a Hayes
|
|
; modem let go of the line; on a 6551 clearing it also shuts the receiver and every ACIA interrupt
|
|
; down, which is exactly what hanging up wants. commLinkControlDispatch follows this with stopCommNmi.
|
|
; It moved here from $E574, into the bytes aciaSetCommandIdle used to occupy, so that silenceCia2
|
|
; could leave the NMI block. aciaSetCommandIdle - 'write the command register and then forget the byte
|
|
; the driver believed was in the transmitter' - is gone because the re-entrancy guard makes its second
|
|
; half provably unnecessary: the NMI's disarm site at $E6E3 is reached only after the transmit half
|
|
; has already cleared txCharActive (or found it clear), and no nested pass can run the body of the
|
|
; handler any more, which is the one path that used to be able to set the flag again in between.
|
|
; healStuckTransmitter $E057 still repairs the flag once a frame if anything else ever strands it.
|
|
; In: none
|
|
; Out: the ACIA command register and aciaCommandShadow = 0
|
|
; Called from: hangUpModem $E38D (the only caller)
|
|
; ----------------------------------------------------------------------
|
|
dropDtrLine:
|
|
lda #$00 ; E408 no DTR, no RTS, no interrupts
|
|
jmp aciaSetCommand ; E40A the modem drops the call
|
|
.assert * <= $E40F, error, "dropDtrLine overran $E40E"
|
|
.res $E40F - *, $EA ; E40D spare
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearUartVars - zeroes the 90-byte ring-buffer block $E414-$E46D: all four descending circular
|
|
; buffers (UART transmit 22, UART receive 20, ARQ transmit 18, ARQ receive 18) with their counts and
|
|
; read/write indices.
|
|
; In: none
|
|
; Out: $E414-$E46D = 0; returns to the caller of clearUartVars
|
|
; Called from: initCommModule $E2B7
|
|
; ----------------------------------------------------------------------
|
|
clearUartVars:
|
|
ldx #$5A ; E40F 90 bytes to clear: all four rings with their counts and indices
|
|
jsr clearInlineVarBlock ; E411 clear the inline block that follows and return to our caller's caller
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Ring buffers, $E414-$E46D, the inline block clearUartVars zeroes. Four descending circular buffers,
|
|
; each with a count byte and separate read and write indices: UART transmit (22), UART receive (20),
|
|
; frame-layer transmit (18) and frame-layer receive (18).
|
|
; ----------------------------------------------------------------------
|
|
uartTxCount:
|
|
brk ; E414 uartTxCount: characters in the 22-byte UART transmit ring (rendered as BRK - data, never executed)
|
|
uartTxReadIndex:
|
|
.byte $00 ; E415 uartTxReadIndex
|
|
uartTxWriteIndex:
|
|
.byte $00 ; E416 uartTxWriteIndex
|
|
uartTxRing:
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; E417 ........ uartTxRing: 22 bytes, $E417-$E42C
|
|
.byte $00 ; E41F uartTxRing continued
|
|
.byte $00,$00,$00,$00,$00,$00,$00; E420 .......
|
|
.byte $00 ; E427 uartTxRing continued
|
|
.byte $00,$00 ; E428 ..
|
|
L_E42A:
|
|
.byte $00 ; E42A the last three bytes of the 22-byte transmit ring
|
|
.byte $00,$00 ; E42B ..
|
|
uartRxCount:
|
|
.byte $00 ; E42D uartRxCount: characters in the 20-byte UART receive ring
|
|
uartRxReadIndex:
|
|
.byte $00 ; E42E uartRxReadIndex
|
|
uartRxWriteIndex:
|
|
.byte $00 ; E42F uartRxWriteIndex
|
|
uartRxRing:
|
|
.byte $00 ; E430 . uartRxRing: 20 bytes, $E430-$E443
|
|
L_E431:
|
|
.byte $00 ; E431 uartRxRing continued
|
|
.byte $00,$00,$00 ; E432 ...
|
|
L_E435:
|
|
.byte $00 ; E435 uartRxRing continued
|
|
.byte $00,$00,$00,$00,$00,$00,$00; E436 .......
|
|
.byte $00 ; E43D the last seven bytes of the 20-byte receive ring
|
|
.byte $00,$00,$00,$00,$00,$00 ; E43E ......
|
|
arqTxCount:
|
|
.byte $00 ; E444 arqTxCount: bytes in the 18-byte frame-layer transmit ring
|
|
arqTxReadIndex:
|
|
.byte $00 ; E445 arqTxReadIndex
|
|
arqTxWriteIndex:
|
|
.byte $00 ; E446 arqTxWriteIndex
|
|
arqTxRing:
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; E447 ........ arqTxRing: 18 bytes, $E447-$E458 - packet layer to frame layer
|
|
.byte $00 ; E44F arqTxRing continued
|
|
.byte $00,$00,$00,$00,$00,$00,$00; E450 .......
|
|
.byte $00 ; E457 the last two bytes of the 18-byte frame transmit ring
|
|
.byte $00 ; E458 .
|
|
arqRxCount:
|
|
.byte $00 ; E459 arqRxCount: bytes in the 18-byte frame-layer receive ring
|
|
arqRxReadIndex:
|
|
.byte $00 ; E45A arqRxReadIndex
|
|
arqRxWriteIndex:
|
|
.byte $00 ; E45B arqRxWriteIndex
|
|
arqRxRing:
|
|
.byte $00,$00,$00,$00,$00,$00,$00,$00; E45C ........ arqRxRing: 18 bytes, $E45C-$E46D - frame layer to packet layer
|
|
L_E464:
|
|
.byte $00 ; E464 arqRxRing continued
|
|
.byte $00,$00,$00,$00,$00,$00,$00; E465 .......
|
|
.byte $00 ; E46C the last two bytes of the ring sit immediately before popArqRxRing
|
|
.byte $00 ; E46D .
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popArqRxRing - pop the oldest byte out of the 18-byte ARQ receive ring, the queue the frame layer
|
|
; fills with checksum-verified payload. This is the framed target of receiveLinkByte, i.e. how the
|
|
; packet layer gets its bytes.
|
|
; In: arqRxCount, arqRxReadIndex, arqRxRing
|
|
; Out: A = byte with C=0, or C=1 when empty
|
|
; Called from: receiveLinkByte $E3B9 through vector $E3C5
|
|
; ----------------------------------------------------------------------
|
|
popArqRxRing:
|
|
lda arqRxCount ; E46E anything in the frame-layer receive ring?
|
|
beq returnRingEmpty ; E471 empty - C=1
|
|
ldx arqRxReadIndex ; E473 oldest slot
|
|
lda arqRxRing,x ; E476 fetch the byte
|
|
dex ; E479 walk the read index down
|
|
bpl L_E47E ; E47A still inside the ring?
|
|
ldx #$11 ; E47C wrapped - back to slot 17 of the 18-byte ring
|
|
L_E47E:
|
|
stx arqRxReadIndex ; E47E store the new read index
|
|
dec arqRxCount ; E481 one byte fewer
|
|
clc ; E484 C=0: here is a byte
|
|
rts ; E485 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popArqTxRing - pop the oldest byte out of the 18-byte ARQ transmit ring, i.e. take the bytes the
|
|
; packet layer queued so the frame builder can wrap them in a checksummed packet.
|
|
; In: arqTxCount, arqTxReadIndex, arqTxRing
|
|
; Out: A = byte with C=0, or C=1 when empty
|
|
; Called from: popArqTxRingThunk $E733 (used by the frame builder at $E8AE/$E8D4)
|
|
; ----------------------------------------------------------------------
|
|
popArqTxRing:
|
|
lda arqTxCount ; E486 anything queued for the frame builder?
|
|
beq returnRingEmpty ; E489 empty - C=1
|
|
ldx arqTxReadIndex ; E48B oldest slot
|
|
lda arqTxRing,x ; E48E fetch the byte
|
|
dex ; E491 walk the read index down
|
|
bpl L_E496 ; E492 still inside the ring?
|
|
ldx #$11 ; E494 wrapped - back to slot 17
|
|
L_E496:
|
|
stx arqTxReadIndex ; E496 store the new read index
|
|
dec arqTxCount ; E499 one byte fewer
|
|
clc ; E49C C=0: here is a byte
|
|
rts ; E49D return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushArqRxRing - push one validated payload byte into the 18-byte ARQ receive ring; the byte is
|
|
; dropped when the ring already holds 18.
|
|
; In: A = byte
|
|
; Out: arqRxRing, arqRxCount, arqRxWriteIndex
|
|
; Called from: pushArqRxRingThunk $E736 (used by the frame receiver at $EA91)
|
|
; ----------------------------------------------------------------------
|
|
pushArqRxRing:
|
|
ldx arqRxCount ; E49E how full is the frame-layer receive ring?
|
|
cpx #$12 ; E4A1 it holds 18 bytes
|
|
bcs L_E4B6 ; E4A3 full - drop the byte
|
|
ldx arqRxWriteIndex ; E4A5 next free slot
|
|
sta arqRxRing,x ; E4A8 store the validated payload byte
|
|
dex ; E4AB walk the write index down
|
|
bpl L_E4B0 ; E4AC still inside the ring?
|
|
L_E4AE:
|
|
ldx #$11 ; E4AE wrapped - back to slot 17
|
|
L_E4B0:
|
|
stx arqRxWriteIndex ; E4B0 store the new write index
|
|
inc arqRxCount ; E4B3 one more byte for the packet layer
|
|
L_E4B6:
|
|
rts ; E4B6 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushArqTxRing - push one byte into the 18-byte ARQ transmit ring; the byte is dropped when the ring
|
|
; is full, and the caller sees C=1 because the count compare left it set. This is the framed target of
|
|
; sendLinkByte.
|
|
; In: A = byte
|
|
; Out: arqTxRing, arqTxCount, arqTxWriteIndex
|
|
; Called from: sendLinkByte $E3B6 through vector $E3C1
|
|
; ----------------------------------------------------------------------
|
|
pushArqTxRing:
|
|
ldx arqTxCount ; E4B7 how full is the frame-layer transmit ring?
|
|
cpx #$12 ; E4BA it holds 18 bytes
|
|
bcs L_E4CF ; E4BC full - return with C=1 so the packet layer retries
|
|
ldx arqTxWriteIndex ; E4BE next free slot
|
|
sta arqTxRing,x ; E4C1 store the byte
|
|
dex ; E4C4 walk the write index down
|
|
bpl L_E4C9 ; E4C5 still inside the ring?
|
|
L_E4C7:
|
|
ldx #$11 ; E4C7 wrapped - back to slot 17
|
|
L_E4C9:
|
|
stx arqTxWriteIndex ; E4C9 store the new write index
|
|
inc arqTxCount ; E4CC one more byte for the frame builder
|
|
L_E4CF:
|
|
rts ; E4CF return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; returnRingEmpty - the shared 'nothing in that ring' exit (SEC / RTS) used by the four ring pop
|
|
; routines.
|
|
; In: none
|
|
; Out: C = 1
|
|
; Called from: popArqRxRing $E471, popArqTxRing $E489, popUartRxRing $E4D5, popUartTxRing $E4ED
|
|
; ----------------------------------------------------------------------
|
|
returnRingEmpty:
|
|
sec ; E4D0 C=1 means 'that ring was empty'
|
|
rts ; E4D1 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popUartRxRing - pop one received character out of the 20-byte raw UART receive ring the NMI fills.
|
|
; In: uartRxCount, uartRxReadIndex, uartRxRing
|
|
; Out: A = character with C=0, or C=1 when empty
|
|
; Called from: runModemStateMachine $E799 (terminal mode), beginByteSyncPhase $E80A,
|
|
; popUartRxRingThunk $E739, and receiveLinkByte through vector $E3C3 in the never-selected raw
|
|
; configuration
|
|
; ----------------------------------------------------------------------
|
|
popUartRxRing:
|
|
lda uartRxCount ; E4D2 any received characters?
|
|
beq returnRingEmpty ; E4D5 none - C=1
|
|
ldx uartRxReadIndex ; E4D7 oldest slot
|
|
lda uartRxRing,x ; E4DA fetch the character
|
|
dex ; E4DD walk the read index down
|
|
bpl L_E4E2 ; E4DE still inside the ring?
|
|
ldx #$13 ; E4E0 wrapped - back to slot 19 of the 20-byte ring
|
|
L_E4E2:
|
|
stx uartRxReadIndex ; E4E2 store the new read index
|
|
dec uartRxCount ; E4E5 one character fewer
|
|
clc ; E4E8 C=0: here is a character
|
|
rts ; E4E9 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popUartTxRing - pop the next character to transmit out of the 22-byte raw UART transmit ring.
|
|
; In: uartTxCount, uartTxReadIndex, uartTxRing
|
|
; Out: A = character with C=0, or C=1 when empty
|
|
; Called from: commNmiHandler $E6D1, flushUartTxRing $E534
|
|
; ----------------------------------------------------------------------
|
|
popUartTxRing:
|
|
lda uartTxCount ; E4EA anything left to transmit?
|
|
beq returnRingEmpty ; E4ED no - C=1
|
|
ldx uartTxReadIndex ; E4EF oldest slot
|
|
lda uartTxRing,x ; E4F2 fetch the character
|
|
dex ; E4F5 walk the read index down
|
|
bpl L_E4FA ; E4F6 still inside the ring?
|
|
ldx #$15 ; E4F8 wrapped - back to slot 21 of the 22-byte ring
|
|
L_E4FA:
|
|
stx uartTxReadIndex ; E4FA store the new read index
|
|
dec uartTxCount ; E4FD one character fewer
|
|
clc ; E500 C=0: here is a character
|
|
rts ; E501 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushUartRxRing - push one fully received character into the 20-byte raw UART receive ring. When the
|
|
; ring is full it leaves everything alone and returns with C=1 from the count compare, which the NMI
|
|
; treats as a receive overflow.
|
|
; In: A = received character
|
|
; Out: uartRxRing, uartRxCount, uartRxWriteIndex; C=1 on overflow
|
|
; Called from: commNmiHandler $E6AE (the only caller)
|
|
; ----------------------------------------------------------------------
|
|
pushUartRxRing:
|
|
ldx uartRxCount ; E502 how full is the receive ring?
|
|
cpx #$14 ; E505 it holds 20 characters
|
|
bcs L_E51A ; E507 full - leave it alone and return with C=1, which the NMI treats as an overflow error
|
|
ldx uartRxWriteIndex ; E509 next free slot
|
|
sta uartRxRing,x ; E50C store the character the UART just assembled
|
|
dex ; E50F walk the write index down
|
|
bpl L_E514 ; E510 still inside the ring?
|
|
ldx #$13 ; E512 wrapped - back to slot 19
|
|
L_E514:
|
|
stx uartRxWriteIndex ; E514 store the new write index
|
|
inc uartRxCount ; E517 one more character for the protocol
|
|
L_E51A:
|
|
rts ; E51A return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushUartTxRing - push one character into the 22-byte raw UART transmit ring; the byte is dropped
|
|
; when the ring is full.
|
|
; In: A = character
|
|
; Out: uartTxRing, uartTxCount, uartTxWriteIndex
|
|
; Called from: queueByteForTransmit $E73C, and sendLinkByte through vector $E3BF in the never-selected
|
|
; raw configuration
|
|
; ----------------------------------------------------------------------
|
|
pushUartTxRing:
|
|
ldx uartTxCount ; E51B how full is the transmit ring?
|
|
cpx #$16 ; E51E it holds 22 characters
|
|
bcs L_E533 ; E520 full - drop the character
|
|
ldx uartTxWriteIndex ; E522 next free slot
|
|
sta uartTxRing,x ; E525 store it
|
|
dex ; E528 walk the write index down
|
|
L_E529:
|
|
bpl L_E52D ; E529 still inside the ring?
|
|
ldx #$15 ; E52B wrapped - back to slot 21
|
|
L_E52D:
|
|
stx uartTxWriteIndex ; E52D store the new write index
|
|
L_E530:
|
|
inc uartTxCount ; E530 one more character to shift out
|
|
L_E533:
|
|
rts ; E533 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; flushUartTxRing - throw away everything still queued for transmission, then set the pending-byte
|
|
; count to 1 if a character is still being shifted out or 0 if not. Used whenever the connection state
|
|
; changes so stale bytes cannot reach the peer.
|
|
; In: the UART transmit ring, txCharActive
|
|
; Out: transmit ring emptied, uartPendingCount resynchronised
|
|
; Called from: runModemStateMachine $E791, beginByteSyncPhase $E80F
|
|
; ----------------------------------------------------------------------
|
|
flushUartTxRing:
|
|
jsr popUartTxRing ; E534 throw the next queued character away
|
|
bcc flushUartTxRing ; E537 until the ring is empty
|
|
lda txCharActive ; E539 1 if a character is still being shifted out
|
|
sta uartPendingCount ; E53C resynchronise the pending count with reality
|
|
rts ; E53F return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; configureUserPortLines - on a SwiftLink there is no user port to configure, so this is now 'open
|
|
; the ACIA'. It re-derives the control byte from the speed the player last chose - openCommLink skips
|
|
; loadBaudParameters when the link is already up, so this is the only place a re-open gets its baud
|
|
; rate - then writes the command register with DTR asserted, RTS low, the receiver interrupt on and
|
|
; the transmit interrupt off. Finally it drains the receiver and reads the status register, which
|
|
; clears the error and IRQ flags and seeds the DCD shadow the carrier sampler reads.
|
|
; The bytes the CIA2 programming used to occupy now hold the six one-instruction accessors that every
|
|
; other routine uses to reach the ACIA. Their operand high bytes are the only record of the
|
|
; cartridge's base page, and aciaSetPage patches all six at once.
|
|
; In: baudIndex
|
|
; Out: the ACIA control and command registers programmed, aciaStatusSave seeded
|
|
; Called from: openCommLink $E2D5, the C=+A / C=+O hot keys at $E387
|
|
; ----------------------------------------------------------------------
|
|
configureUserPortLines:
|
|
ldx baudIndex ; E540 the speed the player last selected
|
|
jsr loadBaudParameters ; E543 find the cartridge and program the control register
|
|
lda #$09 ; E546 DTR on, RTS low, receiver IRQ on, transmit IRQ off
|
|
jsr aciaSetCommand ; E548 the receiver is live from here on
|
|
jsr aciaGetData ; E54B throw away anything the receiver had already latched
|
|
jmp aciaGetStatus ; E54E clear the error and IRQ flags, and seed the DCD shadow
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aciaSetCommand - write the command register and keep a shadow of it. DTR, RTS and both interrupt
|
|
; enables live in that one byte, so nothing can be changed without rewriting the whole register, and
|
|
; some 6551 replicas do not read it back reliably.
|
|
; In: A = command byte
|
|
; Out: aciaCommandShadow and the ACIA command register = A; A, X, Y and C unchanged
|
|
; ----------------------------------------------------------------------
|
|
aciaSetCommand:
|
|
sta aciaCommandShadow ; E551 remember what the register now holds
|
|
aciaCmdWrite:
|
|
sta $DE02 ; E554 operand high byte patched by aciaSetPage
|
|
rts ; E557 return
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aciaGetStatus - read the status register, which is also the acknowledge: reading it clears the
|
|
; ACIA's IRQ flag. The value is kept in aciaStatusSave so the NMI can test it twice without a second
|
|
; read, and so the carrier sampler has a DCD bit to look at without touching the hardware itself.
|
|
; In: none
|
|
; Out: A = aciaStatusSave = the status byte; X and Y unchanged
|
|
; ----------------------------------------------------------------------
|
|
aciaGetStatus:
|
|
aciaStatusRead:
|
|
lda $DE01 ; E558 operand high byte patched by aciaSetPage
|
|
sta aciaStatusSave ; E55B publish it for the rest of the driver
|
|
rts ; E55E return
|
|
|
|
; aciaGetData - take the received byte out of the data register, which also clears RDRF.
|
|
aciaGetData:
|
|
aciaDataRead:
|
|
lda $DE00 ; E55F operand high byte patched by aciaSetPage
|
|
rts ; E562 return
|
|
|
|
; aciaPutData - hand one byte to the transmitter, which also clears TDRE.
|
|
aciaPutData:
|
|
aciaDataWrite:
|
|
sta $DE00 ; E563 operand high byte patched by aciaSetPage
|
|
rts ; E566 return
|
|
|
|
; aciaSetControl - write the control register: stop bits, word length, clock source and baud rate.
|
|
; It falls straight through into aciaGetControl, so it also hands the caller back whatever the
|
|
; register reads as afterwards. That costs one NOP and buys the probe its write-then-read-back in a
|
|
; single JSR instead of two. Every byte that reaches here already has 8 data bits in it: the table's
|
|
; are constants, and the adopted one is masked at aciaProbeAdopted $E665, which is the only place a
|
|
; control byte the driver did not write itself can enter the module.
|
|
; The only other caller, aciaSetControlIdle $E04C, throws A away on its very next instruction.
|
|
aciaSetControl:
|
|
aciaCtrlWrite:
|
|
sta $DE03 ; E567 operand high byte patched by aciaSetPage
|
|
nop ; E56A padding so that aciaGetControl keeps its old address
|
|
; falls into aciaGetControl
|
|
|
|
; aciaGetControl - read the control register back. It is the only ACIA register that can be written
|
|
; and read with no side effect on the receiver, which is what makes it the one to probe with.
|
|
aciaGetControl:
|
|
aciaCtrlRead:
|
|
lda $DE03 ; E56B operand high byte patched by aciaSetPage
|
|
rts ; E56E return
|
|
.assert * = $E56F, error, "the ACIA accessors must end at $E56E"
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; silenceCia2 - make CIA2 let go of the NMI line and keep it that way. Two separate things are needed
|
|
; and the stock driver did both; a 6551 build that only does the first can wedge.
|
|
; Writing $7F to the mask stops any further CIA2 source from raising /NMI, and pinning ciaIcrShadow at
|
|
; $7F matters because readFilmByte $5874, writeFilmByte $5893 and copyPageUnderIo $58B2 write that
|
|
; byte straight back into CIA2_ICR after every film-buffer access - its image value is $90, which
|
|
; would arm the FLAG line, and the byte is inside the frozen block so it can only be corrected here.
|
|
; But the mask does not clear what has already latched: a 6526 holds its interrupt line down until the
|
|
; interrupt register is READ. /NMI is a single wire shared by CIA2, RESTORE and the expansion port, so
|
|
; one latched CIA2 flag left standing pins it low for ever and the ACIA can never deliver another
|
|
; edge - the link would simply stop. Nothing else in this driver ever reads CIA2_ICR, so this is the
|
|
; only chance to clear it. BIT leaves A alone, which is what the caller expects.
|
|
; It used to sit at $E727, at the end of the block the NMI handler shares with the page probe. The
|
|
; re-entrancy guard and the adopt-mode probe needed those twelve bytes, and these twelve came free
|
|
; here when dropDtrLine moved into the seven bytes aciaSetCommandIdle no longer needs at $E408.
|
|
; In: none
|
|
; Out: ciaIcrShadow = CIA2_ICR = $7F, CIA2's interrupt latches cleared, A = $7F, X, Y unchanged
|
|
; Called from: setCiaNmiMask $E594
|
|
; ----------------------------------------------------------------------
|
|
silenceCia2:
|
|
lda #$7F ; E56F $7F clears every CIA2 interrupt source
|
|
sta ciaIcrShadow ; E571 the main program restores this into CIA2_ICR for us
|
|
sta CIA2_ICR ; E574 so pin it at 'CIA2 silent'
|
|
bit CIA2_ICR ; E577 and read it, or a source that latched before the mask was cleared keeps /NMI low and the cartridge is never heard from again
|
|
rts ; E57A return
|
|
.assert * = $E57B, error, "silenceCia2 must end at $E57A"
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; installCommNmiVector - point the RAM NMI vector at $FFFA/$FFFB at commNmiHandler, so the
|
|
; cartridge's NMI - and every other NMI, which the handler chains on - lands there.
|
|
; In: none
|
|
; Out: $FFFA/$FFFB = $E685
|
|
; Called from: openCommLink $E2CC
|
|
; ----------------------------------------------------------------------
|
|
installCommNmiVector:
|
|
lda #$85 ; E57B low byte of commNmiHandler
|
|
ldx #$E6 ; E57D high byte
|
|
sta nmiVector ; E57F the NMI vector lives in RAM at $FFFA/$FFFB (the ROMs are switched out)
|
|
stx nmiVectorHi ; E582 the CPU now enters commNmiHandler on every CIA2 interrupt
|
|
rts ; E585 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; restartUart - (re)start the link: forget any byte that was in the transmitter, throw away whatever
|
|
; the ACIA latched while it was quiet, ask the modem state machine to re-evaluate, prime the
|
|
; quiet-line timer, and fall into setCiaNmiMask with the enable mask so the receiver interrupt comes
|
|
; back. This is both the cold-start path and the resume half of the disk-load handshake.
|
|
; In: none
|
|
; Out: txCharActive = 0, uartRestartRequest = 1, carrierSampleTimer = $92, the ACIA receiver
|
|
; interrupt armed, nmiSuspendRequest/Ack = 0
|
|
; Called from: openCommLink $E2CF, serviceCarrierAndSuspendRequest $E5E1 (resume after a disk load)
|
|
; ----------------------------------------------------------------------
|
|
restartUart:
|
|
jsr aciaResumeReceiver ; E586 clear txCharActive, drain the receiver, re-read status
|
|
lda #$01 ; E589 1 = re-evaluate (only $FF forces the phase back to 0)
|
|
sta uartRestartRequest ; E58B ask the modem state machine to re-evaluate the connection
|
|
lda #$92 ; E58E the mask byte setCiaNmiMask reads as 'interrupt on'
|
|
sta carrierSampleTimer ; E590 prime the quiet-line timer with the same negative value
|
|
.assert * <= $E593, error, "restartUart overran $E592"
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setCiaNmiMask - the single point where this module's interrupt is armed or disarmed. The callers
|
|
; still pass the old CIA2 mask bytes, and only their sign matters: $92 (or $83) means 'let the module
|
|
; interrupt', $7F means 'silence it'. Two things happen here that the stock driver did not need.
|
|
; First, ciaIcrShadow is pinned at $7F, because the main program writes that byte straight back into
|
|
; CIA2_ICR after every film-buffer access ($588C, $58AB, $58D0) and this driver wants no CIA2
|
|
; interrupt at all - its own comes from the expansion port. Second, disabling sets command bit 1
|
|
; (receiver IRQ off) but leaves bit 0 (DTR) up, so a disk load does not make the modem hang up, and
|
|
; then reads the status register so no already-latched IRQ flag is left holding /NMI down.
|
|
; The CIA2 half of that is in silenceCia2 below, because clearing the mask is not enough on its own.
|
|
; In: A = $92/$83 to arm, $7F to silence
|
|
; Out: ciaIcrShadow = CIA2_ICR = $7F, nmiSuspendRequest/Ack = $00 or $80, the ACIA command register
|
|
; Called from: restartUart (fall-through from $E590), stopCommNmi $E5CA
|
|
; ----------------------------------------------------------------------
|
|
setCiaNmiMask:
|
|
pha ; E593 keep the caller's intent
|
|
jsr silenceCia2 ; E594 pin CIA2 silent and let go of any /NMI it is already holding down
|
|
pla ; E597 back to the caller's mask
|
|
eor #$FF ; E598 $92/$83 -> $00, $7F -> $80
|
|
and #$80 ; E59A keep only the sign bit
|
|
sta nmiSuspendRequest ; E59C so a disable mask marks the module suspended...
|
|
sta nmiSuspendAck ; E59F ...and acknowledges it in the same breath
|
|
bne L_E5A9 ; E5A2 $80 = the caller wants the interrupt off
|
|
aciaCommandReceiveOnly:
|
|
lda #$09 ; E5A4 DTR on, RTS low, receiver IRQ on, transmit IRQ off
|
|
jmp aciaSetCommand ; E5A6 the ACIA may interrupt again. The NMI's transmit half
|
|
; calls this label as a subroutine when it finds the
|
|
; ring dry: 'lda #$09 / jmp aciaSetCommand' is exactly
|
|
; what it wants and the two bytes it saves are two the
|
|
; block round commNmiHandler does not have
|
|
L_E5A9:
|
|
lda #$0B ; E5A9 the same but with the receiver IRQ disabled; DTR stays up so the call survives the disk load
|
|
jsr aciaSetCommand ; E5AB no ACIA interrupt can fire from here on
|
|
jmp aciaGetStatus ; E5AE clear any latched IRQ flag so /NMI is released
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aciaResumeReceiver - the bring-the-receiver-back half of restartUart: forget the byte that was in
|
|
; the transmitter, throw away anything the ACIA latched while the interrupt was off, and read the
|
|
; status register so no stale error or IRQ flag is left behind. Whatever arrived during a disk load
|
|
; is lost on purpose - the frame layer resynchronises, exactly as it did with the stock driver.
|
|
; It used to live at $E658, inside the block the NMI handler shares with the page probe; the
|
|
; re-entrancy guard and the adopt-mode probe needed those nine bytes, and these nine came free here
|
|
; when the four NOPs above and clearUartState below were dropped. clearUartState was a two-byte call
|
|
; into clearInlineVarBlock whose whole inline block was the single byte txCharActive $E5BB, so
|
|
; clearTxCharActive $E04F does exactly the same work in the same three bytes at the one call site.
|
|
; In: none
|
|
; Out: txCharActive = 0, the receiver drained, aciaStatusSave refreshed
|
|
; Called from: restartUart $E586
|
|
; ----------------------------------------------------------------------
|
|
aciaResumeReceiver:
|
|
jsr clearTxCharActive ; E5B1 txCharActive = 0
|
|
jsr aciaGetData ; E5B4 discard whatever the receiver latched
|
|
jmp aciaGetStatus ; E5B7 clear the error bits and refresh the DCD shadow
|
|
.assert * <= $E5BB, error, "setCiaNmiMask and aciaResumeReceiver overran $E5BA"
|
|
.res $E5BB - *, $EA ; E5BA spare
|
|
|
|
; ----------------------------------------------------------------------
|
|
; UART state, $E5BB-$E5C0. All of it must survive a restart, because it is the driver's only memory
|
|
; of hardware it cannot read back safely - which page the cartridge is on is held in the accessors'
|
|
; operands, but what was last written to the command register, and what the status register last
|
|
; said, are held here. (txCharActive used to be cleared by clearUartState, a two-byte call whose
|
|
; inline block was this single byte; clearTxCharActive $E04F does the same thing and the routine is
|
|
; gone.)
|
|
; ----------------------------------------------------------------------
|
|
txCharActive:
|
|
.byte $00 ; E5BB txCharActive: non-zero while a byte handed to the ACIA has not had its TDRE come back
|
|
aciaStatusSave:
|
|
.byte $00 ; E5BC aciaStatusSave: the last status byte read, and the only DCD the carrier sampler sees. $00 = carrier present, the right default for a cable with no DCD wire
|
|
nmiHandlerState:
|
|
.byte $00 ; E5BD nmiHandlerState: the NMI re-entrancy guard. 0 = no pass of commNmiHandler is running, 1 = one is, 2 = one is and a second NMI arrived while it worked and left a note for it. Nothing else may write it. (Was aciaRxByte, which the handler no longer needs: it tests the error bits before it takes the character out of the data register instead of after)
|
|
aciaCommandShadow:
|
|
.byte $00 ; E5BE aciaCommandShadow: our copy of the command register - DTR, RTS and both interrupt enables share that one byte
|
|
aciaPageLatch:
|
|
.byte $00 ; E5BF aciaPageLatch: $DE or $DF once the page question has been settled, $00 before that. Written by aciaSetPage with every aim, read by aciaDetect, and cleared only by selectFramedLinkVectors $E3F3 - i.e. only by initCommModule, once per cold start. Its stored $00 is also what a fresh overlay load gives it (was rxBitCounter)
|
|
.byte $00 ; E5C0 spare (was rxShiftRegister)
|
|
|
|
; ----------------------------------------------------------------------
|
|
; suspendUartIfRunning - two-byte entry between the UART state block and stopCommNmi: the disk loader
|
|
; has asked for a suspend, so shut the NMI UART down unless the acknowledge byte already says it is
|
|
; down.
|
|
; In: C = bit 7 of nmiSuspendAck (set when already suspended)
|
|
; Out: falls into stopCommNmi when the suspend has not been acknowledged yet
|
|
; Called from: serviceCarrierAndSuspendRequest $E5DD
|
|
; ----------------------------------------------------------------------
|
|
suspendUartIfRunning:
|
|
bcs L_E5D5 ; E5C1 the suspend is already acknowledged - nothing to do
|
|
|
|
; ----------------------------------------------------------------------
|
|
; stopCommNmi - tear the software UART down: ask the modem state machine to fall back to phase 0,
|
|
; disable every CIA2 NMI source (which also sets both suspend bytes to $80 = suspended), and forget
|
|
; the character that was half way out of the transmitter.
|
|
; In: txCharActive
|
|
; Out: CIA2 NMI sources off, nmiSuspendRequest/Ack = $80, uartRestartRequest = $FF, uartPendingCount
|
|
; adjusted
|
|
; Called from: commLinkControlDispatch $E2AB, suspendUartIfRunning $E5C1
|
|
; ----------------------------------------------------------------------
|
|
stopCommNmi:
|
|
lda #$FF ; E5C3 $FF asks the modem state machine to fall back to phase 0
|
|
sta uartRestartRequest ; E5C5 so the connection falls back to phase 0 when the UART comes back
|
|
lda #$7F ; E5C8 $7F = clear every CIA2 interrupt source
|
|
L_E5CA:
|
|
jsr setCiaNmiMask ; E5CA disable the NMI UART (this also sets both suspend bytes to $80)
|
|
lda txCharActive ; E5CD was a character half way out?
|
|
beq L_E5D5 ; E5D0 no
|
|
dec uartPendingCount ; E5D2 yes - it will never finish, so drop it from the pending count
|
|
L_E5D5:
|
|
rts ; E5D5 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; serviceCarrierAndSuspendRequest - first half of the $E00C service. It runs the two-way suspend
|
|
; handshake the disk loader uses ($E039 written by the game, $E03A answered here) and then, when the
|
|
; link is up, turns the last status byte the NMI read into the $80/$F8 carrier sample the rest of the
|
|
; game expects. On a SwiftLink the modem's DCD arrives on status bit 6 - CMD deliberately swapped DCD
|
|
; and DSR at the chip so the 6551's receiver is never gated off - and it is active low. Because DCD,
|
|
; DSR and CTS are pulled up on the cartridge, a three-wire null-modem cable with no DCD wire reads as
|
|
; permanent carrier, which is exactly what a direct connection wants; the C=+C override is still
|
|
; there for a cable that does carry a real DCD from a peer that is not asserting it.
|
|
; Nothing here reads the ACIA: the status register is sampled only by the NMI, because reading it in
|
|
; the main line would clear an IRQ flag the interrupt has not seen yet. A DCD or DSR change raises an
|
|
; ACIA interrupt of its own, so the shadow tracks the line even on a silent link.
|
|
; In: nmiSuspendRequest, nmiSuspendAck, isLinkActive, carrierOverrideFlags, aciaStatusSave,
|
|
; carrierSampleTimer
|
|
; This is also the once-per-frame path the transmitter's deadlock cure sits on: when the link is up
|
|
; the tail of this routine falls through $E604 into startNextTxChar, whose first act is now
|
|
; healStuckTransmitter $E057. There is no room to put the call inside this routine - its 41 bytes are
|
|
; solid - and no need: the fall-through is unconditional, so the heal runs on every raster IRQ the
|
|
; link is up, which is every frame that a stuck transmitter could matter in.
|
|
; Out: linkStatusSample, carrierSampleTimer, the receiver interrupt started or stopped
|
|
; Called from: pollCarrierState $E3CB
|
|
; ----------------------------------------------------------------------
|
|
serviceCarrierAndSuspendRequest:
|
|
lda nmiSuspendAck ; E5D6 the acknowledge byte
|
|
asl a ; E5D9 carry = bit 7: are we currently suspended?
|
|
lda nmiSuspendRequest ; E5DA the request byte the game writes
|
|
bmi suspendUartIfRunning ; E5DD $C0 = the disk loader wants the interrupt off
|
|
bcc L_E5E4 ; E5DF no request and not suspended - carry on
|
|
jsr restartUart ; E5E1 no request but still suspended - bring the receiver back
|
|
L_E5E4:
|
|
lda isLinkActive ; E5E4 is the link up at all?
|
|
beq aciaDetectDone ; E5E7 no - nothing to sample
|
|
lda carrierOverrideFlags ; E5E9 the carrier override byte
|
|
asl a ; E5EC bit 6 into bit 7
|
|
bmi L_E5F8 ; E5ED the C=+C override is on - report carrier without looking at DCD
|
|
lda #$F0 ; E5EF the 'carrier present' pattern, before the shift below
|
|
bit aciaStatusSave ; E5F1 V = status bit 6 = the modem's DCD on a SwiftLink
|
|
bvc L_E5F8 ; E5F4 DCD is active low, so a clear bit means carrier
|
|
lda #$00 ; E5F6 no carrier
|
|
L_E5F8:
|
|
sec ; E5F8 shift a 1 into bit 7: 'a sample has been taken'
|
|
ror a ; E5F9 -> $80 no carrier, $F8 carrier, $C0 override; bit 6 is the carrier flag
|
|
inc carrierSampleTimer ; E5FA the NMI zeroes this on every byte that arrives
|
|
bpl L_E604 ; E5FD positive means characters have arrived recently - keep bit 7 set
|
|
.assert * <= $E5FF, error, "serviceCarrierAndSuspendRequest overran $E5FE"
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rearmCarrierTimer - the 'no character has arrived recently' tail of the carrier sampler: park the
|
|
; sample in carrierSampleTimer (it is negative, so the timer stays negative until the NMI zeroes it on
|
|
; the next good character) and strip bit 7 from the value that goes into linkStatusSample, marking the
|
|
; link as quiet.
|
|
; In: A = fresh carrier sample
|
|
; Out: carrierSampleTimer = A, A = sample with bit 7 cleared
|
|
; Called from: fall-through from $E5FD (the solo-trainer build also calls it directly)
|
|
; ----------------------------------------------------------------------
|
|
rearmCarrierTimer:
|
|
sta carrierSampleTimer ; E5FF quiet line: park the sample here so the timer stays negative
|
|
and #$7F ; E602 and clear the 'data flowing' bit
|
|
L_E604:
|
|
sta linkStatusSample ; E604 publish the sample for pollCarrierState to debounce
|
|
|
|
; ----------------------------------------------------------------------
|
|
; startNextTxChar - kick the transmitter. With a 6551 there is nothing to shift by hand, so all that
|
|
; is left of this routine is the policy the layers above depend on: never transmit unless something
|
|
; is owed, and outside terminal mode never transmit without carrier. When those hold it arms the
|
|
; ACIA's transmit interrupt, and because TDRE is already set the cartridge raises NMI immediately;
|
|
; from there the NMI feeds the transmitter straight from the ring until it runs dry and disarms
|
|
; itself. The per-frame pacing the software UART needed is gone - txPaceCounter $E044 is still
|
|
; written by beginByteSyncPhase but nothing reads it any more.
|
|
; Reached both from queueByteForTransmit, so a byte queued mid-frame goes out at once, and by
|
|
; fall-through from the carrier sampler once a frame, which is what restarts a burst that stalled.
|
|
; The first test is now healStuckTransmitter rather than a plain read of txCharActive. It answers the
|
|
; same question - 'is a byte of ours still in the chip' - but it also repairs the one answer that can
|
|
; never change on its own: txCharActive set while the transmit interrupt is disarmed. Nothing would
|
|
; ever clear that, and this routine would refuse to run for ever, which is exactly how one machine's
|
|
; transmitter died at 2400 baud. Losing the case 'transmit interrupt armed, flag clear' costs nothing
|
|
; because the command-shadow test at $E61B refuses that case anyway.
|
|
; In: txCharActive, aciaCommandShadow, nmiSuspendAck, uartPendingCount, connectionPhase,
|
|
; linkStatus/linkStatusSample
|
|
; Out: the ACIA command register with the transmit interrupt armed, or nothing at all; txCharActive
|
|
; healed to 0 when the transmit interrupt was found disarmed
|
|
; Called from: queueAndKickTx $E719, and by fall-through from the carrier sampler at $E604
|
|
; ----------------------------------------------------------------------
|
|
startNextTxChar:
|
|
jsr healStuckTransmitter ; E607 'is a byte already sitting in the ACIA' - but healing the one answer that can never change on its own first. Was 'lda txCharActive' ($AD $BB $E5); the helper returns the same A this load did, except that when the transmit interrupt is disarmed it forces txCharActive to 0 on the way past. This is the once-per-frame heal: the $E00C service falls in here from $E604 on every raster IRQ the link is up, and queueByteForTransmit reaches it again on every byte queued
|
|
bne aciaDetectDone ; E60A the transmit interrupt will chain the rest
|
|
lda uartTxCount ; E60C is there really something in the ring?
|
|
bne L_E616 ; E60F yes - decide whether it may go out
|
|
sta uartPendingCount ; E611 the ring is dry: A = 0 resynchronises the count everything above waits on
|
|
beq aciaDetectDone ; E614 always taken - Z is still set from the LDA
|
|
L_E616:
|
|
lda aciaCommandShadow ; E616 what did we last tell the ACIA?
|
|
and #$05 ; E619 bit 2 = transmit interrupt armed, bit 0 = DTR
|
|
cmp #$01 ; E61B arm only with DTR up and the transmitter idle, so a hang-up or C=+H stays hung up
|
|
bne aciaDetectDone ; E61D otherwise leave the command register alone
|
|
lda nmiSuspendAck ; E61F suspended for a disk load?
|
|
bmi aciaDetectDone ; E622 then nothing may interrupt until the drive is done
|
|
lda #$01 ; E624 phase 1 = modem command / terminal mode
|
|
cmp connectionPhase ; E626 which phase are we in?
|
|
beq L_E634 ; E629 terminal-mode characters go out without carrier
|
|
lda linkStatusSample ; E62B the live carrier sample
|
|
and linkStatus ; E62E AND the debounced status
|
|
asl a ; E631 bit 6 (carrier) into bit 7
|
|
bpl aciaDetectDone ; E632 no carrier - do not transmit into a dead line
|
|
L_E634:
|
|
jsr popUartTxRing ; E634 the ring was checked above, so this cannot fail
|
|
bcs aciaDetectDone ; E637 ...but if it somehow does, do not transmit a stray $00
|
|
jsr aciaPutData ; E639 prime the transmitter by hand rather than relying on the ACIA to interrupt the moment TDRE is enabled while it is already true - not every 6551 does
|
|
inc txCharActive ; E63C we are now waiting for this byte's TDRE
|
|
lda #$05 ; E63F DTR on, RTS low, receiver IRQ on, transmit IRQ on
|
|
jmp aciaSetCommand ; E641 the rest of the burst comes out of the NMI
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aciaDetect - settle, once and once only, which page the cartridge answers on: $DE00 first, then the
|
|
; $DF00 strap option. loadBaudParameters calls this before every write to the chip, which is every
|
|
; link open AND every baud hot key, so the one thing it must not do is take the decision again on a
|
|
; live link. aciaPageLatch $E5BF is what stops it: aciaSetPage sets the latch with every aim, this
|
|
; routine returns immediately while it is non-zero, and only selectFramedLinkVectors $E3F3 - reached
|
|
; from initCommModule $E2BA, once, right after the overlay is loaded - puts it back to zero. So the
|
|
; hardware is probed on a cold start and never again: a rate change re-programs the ACIA without
|
|
; re-deciding where it is, and a foreign cartridge strapped to $DE00 sees at most one probe write per
|
|
; cold start instead of one per open and one per hot key.
|
|
; What changed with adopt mode: the $DF00 fallback now runs the same routine as the $DE00 attempt
|
|
; instead of only aiming at it. It is still taken on trust - the answer is ignored - but the page has
|
|
; to be READ before it is written if the user's own line settings are ever to be recovered from it,
|
|
; and doing that for both straps is the whole of what the extra call costs. When there is no
|
|
; cartridge on either page the accessors are left aimed at $DF00, where every read is open bus and
|
|
; every write goes nowhere: harmless, and the only page of the two that is guaranteed not to be some
|
|
; other cartridge's registers.
|
|
; In: none
|
|
; Out: the six accessor operands and aciaPageLatch point at the chosen page, adoptEntryControl holds
|
|
; that page's own control setting; A and Y clobbered, X unchanged (loadBaudParameters holds the
|
|
; baud index in it)
|
|
; Called from: loadBaudParameters $E353
|
|
; ----------------------------------------------------------------------
|
|
aciaDetect:
|
|
lda aciaPageLatch ; E644 has the page question already been settled?
|
|
bne aciaDetectDone ; E647 yes - the accessors already point there, so touch nothing
|
|
lda #$DE ; E649 the SwiftLink default
|
|
jsr aciaProbeAdopted ; E64B aim there, remember its settings, ask for two patterns back
|
|
beq aciaDetectDone ; E64E both came back - the latch now says $DE and we are done
|
|
lda #$DF ; E650 nothing at $DE00: fall back to the strap option, whose
|
|
bne aciaProbeAdopted ; E652 answer we ignore. Always taken ($DF is not zero); the
|
|
; routine's own RTS returns for us
|
|
aciaDetectDone:
|
|
rts ; E654 the shared 'page already settled' exit
|
|
.assert * = $E655, error, "aciaDetect must end at $E654"
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aciaProbeAdopted - aim the six accessors at page A, remember what that page's control register was
|
|
; already set to, and then ask the page to prove it is a 6551, twice.
|
|
;
|
|
; The order matters and is the whole point of the routine. THE DRIVER NO LONGER IMPOSES A LINE RATE:
|
|
; the last entry of baudEntryTable is 'adopt', whose control byte is whatever the user's hardware was
|
|
; configured for, and this is the only moment at which that value can still be read. The probe below
|
|
; overwrites the control register - it has to, because writing and reading it back is the only
|
|
; side-effect-free way to tell a 6551 from open bus - so the read has to come first, once, before
|
|
; anything else in the module touches the chip.
|
|
;
|
|
; What is kept and what is corrected:
|
|
; * bits 3-0 (the rate), bit 7 (stop bits) and bit 4 (the receiver clock source) are the user's and
|
|
; are copied through untouched;
|
|
; * bits 6-5 (word length) are forced to %00 = eight data bits by the AND #$9F at $E665 below, the
|
|
; one instruction in the module that masks a control byte - the seven fixed entries of
|
|
; baudEntryTable are constants that already have those bits clear. The frame layer above is NOT
|
|
; 7-bit clean: the byte-sync handshake tests the peer's answer for exactly $FF (INX / BNE
|
|
; restartByteSync at $E852), $55 length codes and raw sequence bytes go out unaltered, and
|
|
; computePacketChecksum $E937 is a full eight-bit rotate-and-add. So eight data bits are
|
|
; mandatory and everything else is free;
|
|
; * a value that cannot work is replaced outright. (control & $1F) < $11 means either bit 4 = 0,
|
|
; which selects an external receiver clock that a SwiftLink does not have, or rate bits %0000,
|
|
; which selects an external transmit clock, or both - which is exactly what a cold, unconfigured
|
|
; 6551 reads as, because reset clears the whole register to $00. The replacement is $15: 300 baud,
|
|
; 8N1, internal generator. That is the disk default the stock driver came up in, the slowest and
|
|
; therefore most forgiving rate in the table, one of only two rates a stock user-port peer can
|
|
; produce - and the game generates about 30 bytes a second, which is exactly what 300 baud carries.
|
|
;
|
|
; The two probe patterns are unchanged, and both are still legal 6551 control bytes, so a real chip is
|
|
; never left in a strange state by being probed. An empty $DE00 is open bus - it returns whatever the
|
|
; VIC last put there, which is screen data this driver does not control - so one pattern can be
|
|
; matched by luck and the price of that luck is all six accessors aimed at nothing. Requiring $1E and
|
|
; then $15 makes that far less likely. aciaSetControl falls through into aciaGetControl, so each
|
|
; 'write it and read it back' is one JSR.
|
|
; In: A = $DE or $DF, the page to try
|
|
; Out: the accessors and aciaPageLatch aimed at that page whatever the answer, adoptEntryControl set
|
|
; from that page's control register; Z = 1 if it answered both patterns; A and Y clobbered, X
|
|
; unchanged
|
|
; Called from: aciaDetect $E64B and $E652
|
|
; ----------------------------------------------------------------------
|
|
PROBE_PATTERN_1 = $1E ; 8N1, internal baud rate generator, 19200 on a SwiftLink
|
|
PROBE_PATTERN_2 = $15 ; 8N1, internal baud rate generator, 300 - the disk default
|
|
ADOPT_FALLBACK = $15 ; and the same byte again when the chip holds nothing usable
|
|
|
|
aciaProbeAdopted:
|
|
jsr aciaSetPage ; E655 aim all six accessors at the page, and latch it
|
|
jsr aciaGetControl ; E658 what the user's hardware has this page set to
|
|
tay ; E65B keep it while it is tested
|
|
and #$1F ; E65C bit 4 = receiver clock source, bits 3-0 = the rate
|
|
cmp #$11 ; E65E below that one of them says 'external clock'
|
|
tya ; E660 the setting itself again
|
|
bcs adoptSettingUsable ; E661 it can work - keep the rate, stop bits and clock source
|
|
lda #ADOPT_FALLBACK ; E663 it cannot: a cold 6551 reads $00 here
|
|
adoptSettingUsable:
|
|
and #$9F ; E665 bits 6-5 = %00 = eight data bits, and nothing else in
|
|
; the byte is touched. This is the one hard constraint:
|
|
; the frame layer sends $55 length codes, raw sequence
|
|
; bytes and rotate-and-add checksums, so it is not
|
|
; 7-bit clean and a 7-, 6- or 5-bit port setting would
|
|
; corrupt every frame rather than merely slow it down.
|
|
; Rate, stop bits and clock source stay as configured
|
|
sta adoptEntryControl ; E667 park it in baudEntryTable's adopt entry
|
|
lda #PROBE_PATTERN_1 ; E66A the first pattern
|
|
jsr aciaSetControl ; E66C write it and read the register straight back
|
|
eor #PROBE_PATTERN_1 ; E66F Z = 1 only if the register really held it
|
|
bne aciaProbeDone ; E671 it did not - this page is not a 6551
|
|
lda #PROBE_PATTERN_2 ; E673 a clearly different second pattern
|
|
jsr aciaSetControl ; E675 write it and read it back as well
|
|
eor #PROBE_PATTERN_2 ; E678 Z = 1 only if both patterns came back
|
|
aciaProbeDone:
|
|
rts ; E67A return
|
|
.assert * = $E67B, error, "aciaProbeAdopted must end at $E67A"
|
|
|
|
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; The 'not mine' exit of the NMI handler. It restores the caller's $01 first, because the handler
|
|
; forced the I/O area back in on entry, then A and X, and chains through nmiChainVector $E031/$E032 -
|
|
; the path the RESTORE key takes. Y was never touched. Unlike the stock exit there is no CIA2_ICR to
|
|
; put back: this handler never masks CIA2, because setCiaNmiMask has already silenced it for good.
|
|
; ----------------------------------------------------------------------
|
|
nmiChainExit:
|
|
pla ; E67B the caller's CPU port byte
|
|
sta CPU_PORT ; E67C put the memory map back exactly as we found it
|
|
pla ; E67E restore X
|
|
tax ; E67F the saved X
|
|
pla ; E680 and A
|
|
jmp (nmiChainVector) ; E681 chain to the game's NMI handler ($1298, a bare RTI)
|
|
.assert * <= $E685, error, "the NMI chain exit overran $E684"
|
|
.res $E685 - *, $EA ; E684 the last spare byte before the handler's fixed address
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; commNmiHandler - the module's NMI handler, installed at $FFFA/$FFFB by installCommNmiVector. The
|
|
; expansion port's /NMI is wired into the same line as CIA2 and the RESTORE key, so the handler still
|
|
; has to decide whether the interrupt was its own and chain when it was not.
|
|
; Two things it must do that the stock handler did not. It banks the I/O area in itself: readFilmByte
|
|
; $5874, writeFilmByte $5893 and copyPageUnderIo $58B2 run with $01 = $34 to reach the film buffer
|
|
; under $D000-$DFFF, and SEI does not mask an NMI, so without this the ACIA's registers would be
|
|
; ordinary RAM for the roughly 2600 cycles copyPageUnderIo holds that state - about ten character
|
|
; times at 38400 baud. And it services RDRF and TDRE directly rather than trusting status bit 7,
|
|
; which CMD document as unreliable at 9600 bps and above; bit 7 is only consulted at the very end, to
|
|
; tell a DCD or DSR change (ours, nothing more to do) from somebody else's interrupt.
|
|
; The receive path owes the layers above three things besides the byte: zero the $EB00 stall counter
|
|
; so noteIdlePoll does not tear the connection down, zero carrierSampleTimer so linkStatus keeps its
|
|
; 'data is flowing' bit, and push framing, parity and overrun errors through reportLinkError so the
|
|
; link state machine restarts byte sync. The transmit path owes exactly one: uartPendingCount must
|
|
; come back down to zero, because everything above waits on it.
|
|
; A and X are saved, Y is never touched, and both exits restore $01.
|
|
;
|
|
; ---- THE RE-ENTRANCY GUARD AND THE DRAIN LOOP: why 38400 used to take the machine down ----
|
|
;
|
|
; The 6502 does not mask NMI. Reading the 6551's status register - which this handler must do, and
|
|
; does at nmiDrain, because it is the only way to find out what the chip wants - is also the
|
|
; acknowledge: it releases /IRQ. Everything the handler does after that point, 100 to 240 cycles of
|
|
; it, is done with the chip free to raise a fresh edge, and a fresh edge re-enters the handler at the
|
|
; next instruction boundary. Each re-entry costs six bytes of stack, or eight when it lands inside one
|
|
; of the handler's own JSRs.
|
|
; At 38400 baud with traffic both ways there are two interrupt-raising events per character time -
|
|
; one RDRF, one TDRE - so an edge arrives about every 133 cycles, while one pass of this handler costs
|
|
; 175 to 266. Service time exceeded inter-arrival time, so the nesting was not a queue but a stack,
|
|
; and it grew without bound: wedgeDiagnosis.md decoded THIRTY-FOUR complete nested frames off a live
|
|
; page 1 and timed three descents from a healthy stack to an exhausted one at 8.4, 11.5 and 12.5 ms.
|
|
; Once the stack pointer wrapped, every RTI popped a PC, a flag byte and a saved $01 belonging to some
|
|
; other frame, and the machine left the handler with a corrupt program counter, a corrupt P or a
|
|
; corrupt memory map. That is the whole of the '38400 wedges' fault: four boots, four wedges, one
|
|
; ending in a CPU JAM at $0007.
|
|
;
|
|
; The fix is two co-operating pieces, and neither works without the other.
|
|
;
|
|
; 1. THE GUARD, nmiHandlerState $E5BD. Zero means no pass is running. The first thing a pass does is
|
|
; test it, and a second NMI that finds it non-zero does not run the handler body at all: it adds
|
|
; one to the byte - a note saying 'there is more to do' - and returns. That costs six bytes of
|
|
; stack and about fifty cycles, and it CANNOT nest again, because a nested pass never reads the
|
|
; status register. /IRQ therefore stays asserted, the shared /NMI line stays low, and an
|
|
; edge-triggered NMI cannot fire again until the running pass acknowledges. The depth is bounded at
|
|
; one nested frame, whatever the line rate, and no arithmetic about cycles per character has to
|
|
; hold for that to be true. The single qualification is the two-instruction window at the test
|
|
; itself, which the comment at $E68F sets out: our own chip cannot use it, and if a foreign NMI
|
|
; does, one extra pass gets through and the byte still ends at zero.
|
|
; Measured at 38400 with two machines in the packet phase (testGuard.py): 12 of 65 and 6 of 64
|
|
; handler entries in five seconds were nested. On the old build every one of those would have been
|
|
; a full re-entrant pass, which is how the runaway started.
|
|
; 2. THE DRAIN LOOP. A note would be worthless if nobody read it, and refusing a nested pass would
|
|
; otherwise lose exactly the interrupt the source used to warn about - a dropped TDRE stalls the
|
|
; transmitter for good, because nothing else will ever raise one. So the running pass loops: it
|
|
; services what the status register shows, and at nmiIdle it decrements the guard and goes round
|
|
; again whenever the decrement leaves a note behind. The transmit path loops for the same reason
|
|
; from the other end - after feeding the chip it goes back to nmiDrain rather than returning, so a
|
|
; byte that arrived while it was working is taken now instead of on the next interrupt.
|
|
; The order at nmiIdle is what closes the last window: the DECREMENT COMES FIRST, so from that
|
|
; instruction onwards the guard reads zero and any NMI arriving during the exit sequence is a full
|
|
; pass that drains everything. There is no instant at which work can be left behind with the
|
|
; interrupt swallowed.
|
|
;
|
|
; What this does NOT claim: the C64 still cannot afford two interrupts per 266 cycles, so 38400 with
|
|
; both directions bursting still costs more CPU than there is. What changes is the failure mode -
|
|
; the receive ring overflows and the ARQ layer resynchronises, exactly as it already does at 9600 and
|
|
; 19200, instead of the stack eating page 1. Degradation instead of death is the whole of the claim.
|
|
;
|
|
; One deliberate loss: a foreign NMI (RESTORE) that arrives while a pass is running is counted as a
|
|
; note and swallowed rather than chained. The window is a couple of hundred cycles and both possible
|
|
; chain targets in this game are a bare RTI ($E010 on disk, $1298 once initInterruptVectors has run),
|
|
; so nothing observable is given up.
|
|
; Also by design, and unchanged: a pass that serviced anything usually leaves through the chain rather
|
|
; than the RTI, because the drain loop's last status read - the one that found nothing left - has
|
|
; cleared bit 7. Both exits restore $01, X and A identically and both chain targets are that same bare
|
|
; RTI, so the two paths differ by two jumps and nothing else.
|
|
; In: the ACIA status/data registers, txCharActive, nmiHandlerState, the UART rings
|
|
; Out: the UART receive ring, uartPendingCount, txCharActive, aciaStatusSave, carrierSampleTimer,
|
|
; $EB00, nmiHandlerState; exits with RTI or chains through nmiChainVector
|
|
; Called from: the CPU's NMI vector once installCommNmiVector has run
|
|
; ----------------------------------------------------------------------
|
|
commNmiHandler:
|
|
pha ; E685 save A
|
|
txa ; E686 X goes on the stack too
|
|
pha ; E687 and X
|
|
lda CPU_PORT ; E688 the game may have banked the I/O area out to reach the film buffer
|
|
pha ; E68A remember how we found it
|
|
lda #$35 ; E68B I/O visible, ROMs out - the map the game normally runs in
|
|
sta CPU_PORT ; E68D the cartridge is only addressable from here on
|
|
lda nmiHandlerState ; E68F is a pass of ours already running?
|
|
bne nmiLeaveNote ; E692 yes - leave it a note and get out
|
|
inc nmiHandlerState ; E694 no: 0 -> 1, this pass owns the ACIA from here
|
|
; Read-then-increment, not increment-then-read, and the
|
|
; difference matters. If an NMI could land between these
|
|
; two instructions the other order would have both
|
|
; passes read 2 and both leave, with the byte stuck at 2
|
|
; and the driver deaf for good. This way the worst that
|
|
; window can do is let one extra pass through - the same
|
|
; single nesting the guard already tolerates - and the
|
|
; byte still ends at 0. (It cannot normally happen at
|
|
; all: our own chip has not been acknowledged yet, so
|
|
; /IRQ is still low and no new edge exists to take. Only
|
|
; a foreign NMI arriving first opens the window.)
|
|
nmiDrain:
|
|
jsr aciaGetStatus ; E697 reading the status register is also the acknowledge
|
|
and #$08 ; E69A RDRF: a character has arrived
|
|
beq nmiTx ; E69C nothing received - look at the transmitter
|
|
lda aciaStatusSave ; E69E the status that came with this character
|
|
and #$07 ; E6A1 overrun, framing error, parity error
|
|
bne nmiRxBad ; E6A3 any of them and the character is worthless
|
|
lda #$00 ; E6A5 a good character means the link is alive:
|
|
sta D_EB00 ; E6A7 clear the protocol stall counter
|
|
sta carrierSampleTimer ; E6AA and tell the carrier sampler that data is flowing
|
|
jsr aciaGetData ; E6AD now take the character out, which also clears RDRF
|
|
jsr pushUartRxRing ; E6B0 hand it to the protocol; C=1 means the 20-byte ring is full
|
|
bcc nmiTx ; E6B3 stored - on to the transmitter
|
|
bcs nmiRxError ; E6B5 the ring was full: report it, the character is already gone
|
|
nmiRxBad:
|
|
jsr aciaGetData ; E6B7 drain the worthless character anyway, or RDRF never clears
|
|
nmiRxError:
|
|
jsr reportLinkError ; E6BA bad character or receive overflow - the link state machine will resync
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Transmit half. TDRE is set whenever the holding register is free, so it only means 'our interrupt'
|
|
; when we asked for it; the command shadow is the only way to know that. Shifting the shadow's bit 2
|
|
; (transmit interrupt armed) up into bit 4 lets both questions be asked with one AND against the
|
|
; status byte, whose bit 4 is TDRE.
|
|
; startNextTxChar puts the first byte of a burst in by hand, so every interrupt from here on finds
|
|
; txCharActive set and one byte leaves the ring per pass of the loop. That byte is counted off
|
|
; uartPendingCount one character time early, because TDRE comes back when the byte moves into the
|
|
; shift register rather than when it reaches the wire - harmless, since nothing above measures real
|
|
; wire time.
|
|
; ----------------------------------------------------------------------
|
|
nmiTx:
|
|
lda aciaCommandShadow ; E6BD did we arm the transmit interrupt?
|
|
asl a ; E6C0 command bit 2 = 'armed'...
|
|
asl a ; E6C1 ...lined up with status bit 4 = TDRE
|
|
and aciaStatusSave ; E6C2 both at once: armed AND the register is free
|
|
and #$10 ; E6C5 nothing else in that byte is ours to read
|
|
beq nmiIdle ; E6C7 not this time
|
|
lda txCharActive ; E6C9 was a byte of ours in there?
|
|
beq nmiTxFeed ; E6CC no - nothing to count off, just feed the transmitter
|
|
dec uartPendingCount ; E6CE that byte is on its way out
|
|
dec txCharActive ; E6D1 1 -> 0: nothing of ours is in flight. It is only ever 0
|
|
; or 1 - both INCs below and in startNextTxChar are
|
|
; guarded by a test that it was 0, and the guard above
|
|
; is what makes that pair atomic
|
|
nmiTxFeed:
|
|
jsr popUartTxRing ; E6D4 the next byte to go
|
|
bcs nmiTxDry ; E6D7 the ring is empty - stop asking
|
|
jsr aciaPutData ; E6D9 hand it to the transmitter
|
|
inc txCharActive ; E6DC and remember that we are waiting for its TDRE
|
|
bne nmiDrain ; E6DF always taken: drain, do not return - look at the chip
|
|
; again in case something arrived while we were working
|
|
nmiTxDry:
|
|
jsr aciaCommandReceiveOnly ; E6E1 nothing left: drop back to receive-only, RTS still low,
|
|
; so the transmit interrupt stops asking. txCharActive is
|
|
; already 0 here - the half above either cleared it or
|
|
; found it clear, and with the guard in place nothing
|
|
; can set it in between - so the old aciaSetCommandIdle
|
|
; had nothing left to do and its bytes went to dropDtrLine
|
|
nmiIdle:
|
|
dec nmiHandlerState ; E6E4 1 -> 0, and from this instruction on a fresh NMI is a
|
|
; full pass again, which is what makes the exit below
|
|
; safe. 2 -> 1 means a nested pass left a note
|
|
bne nmiDrain ; E6E7 it did - service what it could not
|
|
lda aciaStatusSave ; E6E9 nothing left at all: was this interrupt even ours?
|
|
bpl nmiChainExit ; E6EC bit 7 clear - not ours, chain to the game's handler
|
|
bmi nmiExit ; E6EE always taken: a DCD or DSR change of ours, nothing to do
|
|
nmiLeaveNote:
|
|
inc nmiHandlerState ; E6F0 1 -> 2: tell the running pass there is more to do. Note
|
|
; what is NOT here - no status read, so /IRQ stays
|
|
; asserted, the shared /NMI line stays low, and no
|
|
; further edge can be raised until the running pass
|
|
; acknowledges. That is what bounds the nesting
|
|
nmiExit:
|
|
pla ; E6F3 the caller's CPU port byte
|
|
sta CPU_PORT ; E6F4 put the memory map back exactly as we found it
|
|
pla ; E6F6 restore X
|
|
tax ; E6F7 the saved X
|
|
pla ; E6F8 and A
|
|
rti ; E6F9 return from the NMI
|
|
|
|
; ----------------------------------------------------------------------
|
|
; aciaSetPage - record which page the cartridge answers on by patching the operand high byte of all
|
|
; six accessors at $E551-$E56E, and publish that page in aciaPageLatch $E5BF. Nothing else in the
|
|
; module names an address in $DE00 or $DF00, so those six bytes are the whole of the driver's
|
|
; knowledge of where the hardware is; the latch is a seventh copy of it that can be read back with a
|
|
; single LDA, which is what lets aciaDetect answer 'do we already know?' in five bytes.
|
|
; The latch is therefore always in step with the accessors: non-zero means the page question has been
|
|
; settled and no later call may probe again, and only a cold start - selectFramedLinkVectors $E3F3,
|
|
; which initCommModule calls once - puts it back to zero. (This space was nmiStartNextChar, the timer
|
|
; B path of the bit-banged transmitter; the three bytes the latch store needed were its padding.)
|
|
; In: A = $DE or $DF
|
|
; Out: the six accessor operands and aciaPageLatch patched; A, X, Y and the flags as they came in
|
|
; Called from: aciaProbe $E6F2 and the strap fallback in aciaDetect $E652
|
|
; ----------------------------------------------------------------------
|
|
aciaSetPage:
|
|
sta aciaCmdWrite+2 ; E6FA command register writes
|
|
sta aciaStatusRead+2 ; E6FD status register reads
|
|
sta aciaDataRead+2 ; E700 received bytes
|
|
sta aciaDataWrite+2 ; E703 transmitted bytes
|
|
sta aciaCtrlWrite+2 ; E706 control register writes
|
|
sta aciaCtrlRead+2 ; E709 control register reads (the probe)
|
|
sta aciaPageLatch ; E70C and the readable copy aciaDetect tests
|
|
rts ; E70F return
|
|
.assert * = $E710, error, "aciaSetPage must end at $E70F"
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; baudEntryTable - eight three-byte entries {hot-key code, ACIA control byte, spare}, indexed by
|
|
; baudIndex. The three-byte stride and the first two entries are inherited from the stock
|
|
; baudParameterTable so that every existing index keeps its meaning: switchTo1200Baud $E7B0 loads 3
|
|
; when it sniffs a CONNECT 1200 result code, and 0 is still 300 baud. Those two rates are also the
|
|
; only ones a stock user-port machine can talk at, so they are where a SwiftLink player and a
|
|
; stock-driver player meet.
|
|
; The control byte of a fixed-rate entry is $10 (8 data bits, one stop bit, internal baud rate
|
|
; generator) OR the rate bits. The rates are the SwiftLink ones, i.e. twice what a 6551 data sheet
|
|
; says for the same bits, because the cartridge fits a 3.6864 MHz crystal. A SwiftLink rebuilt with
|
|
; the standard 1.8432 MHz part will run every one of these at half the speed shown.
|
|
;
|
|
; TWO CHANGES HERE, AND THEY ARE THE POINT OF THIS PASS.
|
|
;
|
|
; 1. Index 18 has its hot key back. C= + 8 selects 38400 again. It was taken away because 38400 wedged
|
|
; the machine - four boots, four wedges, one ending in a CPU JAM at $0007 - but that was hiding a
|
|
; defect rather than fixing one, and wedgeDiagnosis.md then established what the defect was: the
|
|
; NMI handler could be re-entered without bound, so at a rate whose interrupts arrive faster than
|
|
; one pass of the handler completes, the nested frames ate the whole of page 1 in 8 to 13 ms. The
|
|
; re-entrancy guard in commNmiHandler $E68F fixes that mechanism, so the rate is selectable again.
|
|
; 38400 is still the rate at which this driver has the least headroom - see the note at the guard -
|
|
; but it can no longer take the machine down with it, and a user whose port is configured for it
|
|
; must be able to use it.
|
|
;
|
|
; 2. Index 21 is new: ADOPT. Its control byte is not a constant at all - aciaProbeAdopted $E655 writes
|
|
; it, once per cold start, from whatever the user's own hardware already had in its control
|
|
; register, corrected only where it cannot work (see that routine). Selecting it therefore means
|
|
; 'use the port exactly as it is configured' rather than 'impose 8N1 at a rate of the driver's
|
|
; choosing', which is what every other entry means and what the stock driver always did.
|
|
; It is the DISK DEFAULT: baudIndex $E055 ships as $15 = 21, so a link opened without pressing
|
|
; anything runs at the user's own setting. On a cold, unconfigured chip that setting reads $00,
|
|
; which cannot work, so the fallback puts it at 300 baud - which is what the module did before this
|
|
; change, and what the game needs anyway at about 30 bytes a second. Pressing any speed hot key
|
|
; stores that entry's index and leaves adopt mode; C= + U comes back to it.
|
|
; The spare third byte of this entry is the one the table does not need, so the table is 23 bytes.
|
|
; ----------------------------------------------------------------------
|
|
BAUD_INDEX_ADOPT = 21 ; the adopt entry's index, i.e. its offset into the table
|
|
|
|
baudEntryTable:
|
|
.byte $B3,$15,$00 ; E710 index 0: C=+3 300 baud
|
|
.byte $B1,$17,$00 ; E713 index 3: C=+1 1200 baud
|
|
.byte $B2,$18,$00 ; E716 index 6: C=+2 2400 baud
|
|
.byte $B4,$1A,$00 ; E719 index 9: C=+4 4800 baud
|
|
.byte $B9,$1C,$00 ; E71C index 12: C=+9 9600 baud
|
|
.byte $B0,$1E,$00 ; E71F index 15: C=+0 19200 baud
|
|
.byte $B8,$1F,$00 ; E722 index 18: C=+8 38400 baud - the hot key is back
|
|
.byte $D5 ; E725 index 21: C=+U adopt: use the port as it is set up
|
|
adoptEntryControl:
|
|
.byte $00 ; E726 written by aciaProbeAdopted $E667 on a cold start; the
|
|
; stored $00 is never used, because loadBaudParameters
|
|
; cannot reach this byte without aciaDetect having run
|
|
.assert * = $E727, error, "baudEntryTable must end at $E726"
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueAndKickTx - the body of queueByteForTransmit, which does not fit in the seven bytes at $E73C
|
|
; once it has to start the transmitter as well. A must come back unchanged: the callers test what
|
|
; they just sent.
|
|
; In: A = byte to transmit
|
|
; Out: the byte in the transmit ring, uartPendingCount +1, the transmitter armed if it was idle
|
|
; Called from: queueByteForTransmit $E73C
|
|
; ----------------------------------------------------------------------
|
|
queueAndKickTx:
|
|
jsr pushUartTxRing ; E727 into the 22-byte ring; silently dropped when it is full
|
|
inc uartPendingCount ; E72A count one more character owed to the wire, even on an overflow - startNextTxChar resynchronises the count when the ring runs dry
|
|
pha ; E72D keep the caller's byte
|
|
jsr startNextTxChar ; E72E start the transmitter if it was idle
|
|
pla ; E731 give the byte back
|
|
rts ; E732 return
|
|
.assert * = $E733, error, "queueAndKickTx must end at $E732 - the block is full"
|
|
|
|
|
|
.assert * = $E733, error, "commNmiHandler and its helpers must end at $E732"
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popArqTxRingThunk - the fixed entry the frame layer uses to fetch the next payload byte the packet
|
|
; layer queued.
|
|
; In: none
|
|
; Out: A = byte, C = 0/1 (see popArqTxRing)
|
|
; Called from: the frame builder at $E8AE and $E8D4
|
|
; ----------------------------------------------------------------------
|
|
popArqTxRingThunk:
|
|
jmp popArqTxRing ; E733 the frame builder's entry for taking the next queued payload byte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; pushArqRxRingThunk - Three-byte trampoline: JMP pushArqRxRing ($E49E). It gives the packet layer a
|
|
; fixed address for the game-facing inbound ring, the same address in both $E000 modules.
|
|
; In: A = byte to hand to the game
|
|
; Out: byte appended to the 18-byte inbound command ring $E45C, count $E459 bumped
|
|
; Called from: deliverPacketPayload ($EA91)
|
|
; ----------------------------------------------------------------------
|
|
pushArqRxRingThunk:
|
|
jmp pushArqRxRing ; E736 JMP $E49E - deliver one decoded command byte into the game's inbound ring $E45C
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; popUartRxRingThunk - Three-byte trampoline: JMP popUartRxRing ($E4D2), the packet layer's only door
|
|
; into the raw 20-byte UART receive ring.
|
|
; In: none
|
|
; Out: A = received byte (A = 0 and C = 1 when the ring is empty), X clobbered
|
|
; Called from: receiveByteTrackZeros ($E743) and the packet body copier ($E9E9)
|
|
; ----------------------------------------------------------------------
|
|
popUartRxRingThunk:
|
|
jmp popUartRxRing ; E739 JMP $E4D2 - take the next raw byte the software UART has assembled
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; queueByteForTransmit - the single door for outbound bytes. The work is in queueAndKickTx, because
|
|
; seven bytes are not enough to queue the byte, count it and start the transmitter as well. A is
|
|
; preserved: sendNextModemCommandChar tests what it just sent.
|
|
; In: A = byte to transmit
|
|
; Out: $E417 ring, $E414 count, $E416 write index, $E0A5 incremented; A preserved, X clobbered
|
|
; Called from: sendNextModemCommandChar ($E7D2), beginByteSyncPhase ($E81D), runPacketPhase ($E87C)
|
|
; and sendPacket ($E92A)
|
|
; ----------------------------------------------------------------------
|
|
queueByteForTransmit:
|
|
jmp queueAndKickTx ; E73C push the byte, count it, and start the transmitter if it was idle. A tail jump rather than a JSR/RTS pair: queueAndKickTx already gives A back unchanged, and the four bytes that saves are countLinkError's new home
|
|
|
|
; ----------------------------------------------------------------------
|
|
; countLinkError - the framed link-error hook: count one bad character. The link state machine tests
|
|
; linkErrorCount every tick and restarts byte synchronisation when it is non-zero.
|
|
; It lived at $E403 until selectFramedLinkVectors had to grow by five bytes to clear aciaPageLatch on
|
|
; a cold start. Nothing calls it by address - both entries of linkIoVectorTable's error pair were
|
|
; repointed here - and the leading NOP the stock module had at $E403 was only padding.
|
|
; In: none
|
|
; Out: linkErrorCount +1
|
|
; Called from: reportLinkError $E3BC through vector $E3C9
|
|
; ----------------------------------------------------------------------
|
|
countLinkError:
|
|
inc linkErrorCount ; E73F count one bad character; the link state machine restarts byte sync when this is non-zero
|
|
rts ; E742 return
|
|
.assert * = $E743, error, "countLinkError must end at $E742"
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; receiveByteTrackZeros - Pops one byte from the raw UART receive ring and maintains zeroRunLength
|
|
; ($EAB8), the count of consecutive $00 bytes on the line. Any non-$00 byte resets the run; an empty
|
|
; ring leaves it untouched. The sync and packet layers use the run length to tell a single $00 (the
|
|
; peer's 'resend' nak) from a stream of $00s (a dead or unsynchronised line).
|
|
; In: the UART receive ring $E430/$E42D, zeroRunLength $EAB8
|
|
; Out: A = byte ($00 when the ring was empty), X = the new run length, C = 1 when the ring was empty,
|
|
; $EAB8 updated
|
|
; Called from: beginByteSyncPhase ($E837, $E84C) and receivePacket ($E9AF)
|
|
; ----------------------------------------------------------------------
|
|
receiveByteTrackZeros:
|
|
jsr popUartRxRingThunk ; E743 pop one byte; popUartRxRing returns A = 0 and C = 1 when the ring is empty
|
|
ldx zeroRunLength ; E746 X = the run of consecutive $00 bytes seen so far
|
|
L_E749:
|
|
bcs trackZerosReturn ; E749 ring was empty: leave the run length alone and return with C = 1 and X = the old count
|
|
inx ; E74B provisionally extend the zero run by this byte
|
|
eor #$00 ; E74C EOR #$00 purely to set Z from the byte just popped
|
|
beq L_E752 ; E74E the byte was $00 - keep the extended run length
|
|
ldx #$00 ; E750 any non-$00 byte breaks the run
|
|
L_E752:
|
|
stx zeroRunLength ; E752 store the new run length; it is also returned in X
|
|
trackZerosReturn:
|
|
rts ; E755 return with C still holding the ring-empty flag - none of the instructions above touch it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runModemStateMachine - The per-frame link service. It consumes the restart request in $E042,
|
|
; retires a packet once the transmitter has drained, and drives connectionPhase $E040: 0 = idle, 1 =
|
|
; modem command/terminal mode, 2 = byte sync, 3 = packet protocol. In phase 1 it shuttles characters
|
|
; between the modem and the game's two host character rings, sniffs the modem's verbose result codes
|
|
; so it can switch to 1200 baud on a 'CONNECT 1200', and honours the guard-time delay bytes embedded
|
|
; in the AT strings. It falls through into sendNextModemCommandChar and from there, once the modem
|
|
; answer timer expires, into runLinkStateMachine.
|
|
; In: $E042 restart request, $E040 connection phase, $E03C debounced carrier byte, $E03F modem answer
|
|
; timer, $E048 modem option flags, $E0A4/$E0A5/$E0A6, modemDelayCounter $EB01, lastModemChar
|
|
; $EB02, both host character rings and both UART rings
|
|
; Out: $E040, $E03F, $E044, $E0A4, $E0A6, $EB01, $EB02, the baud parameters $E056-$E058, the
|
|
; host-input ring $E086 and the UART transmit ring
|
|
; Called from: serviceCommTick ($E134), i.e. once per raster IRQ through jump-table entry $E000 with X
|
|
; = 0
|
|
; Consume the restart request posted by restartUart ($E58B writes 1) or stopCommNmi ($E5C5 writes
|
|
; $FF).
|
|
; ----------------------------------------------------------------------
|
|
runModemStateMachine:
|
|
ldx #$00 ; E756 X = 0, the value used below to clear the request and the connection phase
|
|
lda uartRestartRequest ; E758 restart request: 1 = re-evaluate the link, $FF = tear the link down
|
|
beq checkConnectionPhase ; E75B no request pending
|
|
stx uartRestartRequest ; E75D consume the request
|
|
bpl serviceLinkTick ; E760 request was 1: just re-evaluate, keep the current connection phase
|
|
stx connectionPhase ; E762 request was $FF: force the connection phase back to 0 (link down)
|
|
checkConnectionPhase:
|
|
lda connectionPhase ; E765 connection phase: 0 idle, 1 modem terminal, 2 byte sync, 3 packet protocol
|
|
bne serviceLinkTick ; E768 phase 1-3, or a pending re-evaluation: there is work to do
|
|
rts ; E76A idle with nothing requested - leave the link alone
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; A message packet counts as 'in flight' until the UART transmitter has emptied its ring.
|
|
; ----------------------------------------------------------------------
|
|
serviceLinkTick:
|
|
lda uartPendingCount ; E76B characters the NMI transmitter still owes
|
|
cmp frameInFlightFlag ; E76E compare with $E0A4, the 'a message packet is in flight' flag (set to 1 at $E8A8)
|
|
bcs checkTerminalMode ; E771 transmitter not drained yet - the packet has not left the machine
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearInFlightFlags - Mid-routine entry of runModemStateMachine, also used by the solo trainer build:
|
|
; clears the frame-in-flight flag $E0A4 and the packet-layer send enable $E0A6. Clearing $E0A4 is
|
|
; what tells runPacketPhase that the packet it queued has physically left the transmitter and a new
|
|
; one may be built.
|
|
; In: X = 0
|
|
; Out: $E0A4 = 0, $E0A6 = 0
|
|
; Called from: runModemStateMachine by fall-through from $E771; the trainer build of this module also
|
|
; jumps here
|
|
; ----------------------------------------------------------------------
|
|
clearInFlightFlags:
|
|
ldx #$00 ; E773 X = 0
|
|
storeInFlightFlags:
|
|
stx frameInFlightFlag ; E775 transmitter drained: the packet in flight has physically gone out
|
|
stx packetInFlightCount ; E778 and clear the packet-layer send enable $E0A6 - NOT the only write to it: sendPacketFsm sets it at $E265 with 'sta uartPendingCount,x', X = inFlightSlotIndex = 1
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Choose between the modem dialogue (terminal mode) and the game link.
|
|
; ----------------------------------------------------------------------
|
|
checkTerminalMode:
|
|
lda #$01 ; E77B 1 = the modem command / terminal phase
|
|
cmp connectionPhase ; E77D compare with the current connection phase
|
|
beq serviceModemInput ; E780 already in terminal mode: go and service the characters flowing to and from the modem
|
|
ldx modemReplyTimer ; E782 modem answer timer / 'an AT command is in progress' byte ($FF while a command string is queued)
|
|
bne enterTerminalMode ; E785 non-zero: a modem dialogue is under way, so make sure we are in terminal mode
|
|
bit linkStatus ; E787 test the debounced carrier byte; bit 6 (V) = carrier detect, i.e. CIA2 PB4 read low
|
|
testCarrierPresent:
|
|
bvs runLinkStateMachine ; E78A carrier is already up - skip the modem dialogue entirely and run the link state machine
|
|
drainHostOutRing:
|
|
jsr popHostOutRing ; E78C no carrier: throw away everything the game had queued for the opponent
|
|
bcc drainHostOutRing ; E78F loop until the 16-byte host-output ring $E091 is empty
|
|
enterTerminalMode:
|
|
jsr flushUartTxRing ; E791 drop anything still queued for the UART transmitter
|
|
lda #$01 ; E794 1 = modem command / terminal mode
|
|
sta connectionPhase ; E796 enter connection phase 1
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Terminal mode, modem to game: forward what the modem says to the chat line and watch for 'CONNECT
|
|
; 1200'.
|
|
; ----------------------------------------------------------------------
|
|
serviceModemInput:
|
|
jsr popUartRxRing ; E799 take the next character the modem sent back
|
|
bcs tickGuardDelay ; E79C nothing arrived this tick
|
|
bit modemOptionFlags ; E79E $E048 modem option flags ($CC as shipped): bit 7 (N) = a Hayes modem is attached, bit 6 (V) = auto-detect the connect speed
|
|
bpl tickGuardDelay ; E7A1 no modem configured - discard whatever arrived
|
|
cmp #$0D ; E7A3 CR terminates a verbose result line such as 'CONNECT 1200'
|
|
bne rememberModemChar ; E7A5 an ordinary character: just remember and forward it
|
|
checkConnectSpeed:
|
|
bvc substituteSpaceForCr ; E7A7 V = bit 6 of $E048: auto-baud disabled, so do not sniff the result code
|
|
lda lastModemChar ; E7A9 the character that preceded this CR
|
|
cmp #$30 ; E7AC '0' - the last digit of 'CONNECT 1200'; plain 'CONNECT' (300 baud) ends in 'T' and is ignored
|
|
bne substituteSpaceForCr ; E7AE not a 1200 baud connect message
|
|
ldx #$03 ; E7B0 index 3 = the second three-byte entry of baudEntryTable $E70F = 1200 baud (ACIA control $17). The SwiftLink
|
|
; table deliberately keeps the stock three-byte stride, so this caller still means 1200 baud without being touched
|
|
switchTo1200Baud:
|
|
jsr loadBaudParameters ; E7B2 write control $17 to the 6551 and mirror it in bitPeriodLo $E056. baudIndex $E055 is NOT changed: a later re-open with the link down reloads it and would go back to 300, exactly as the stock driver behaved
|
|
substituteSpaceForCr:
|
|
lda #$20 ; E7B5 replace the CR with a space - the chat line cannot print a CR - and reset the last-character memory
|
|
rememberModemChar:
|
|
sta lastModemChar ; E7B7 remember this character for the next auto-baud test
|
|
tax ; E7BA test the character
|
|
beq tickGuardDelay ; E7BB never forward a NUL to the game
|
|
jsr pushHostInRing ; E7BD hand the character to the game through the 8-byte host-input ring $E086, which jump-table entry $E006 pops
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Guard-time delays are held in $EB01 as negative tick counts and block both transmission and the
|
|
; answer timer while they run.
|
|
; ----------------------------------------------------------------------
|
|
tickGuardDelay:
|
|
ldx modemDelayCounter ; E7C0 guard-time delay counter ($A0 = -96 and $C0 = -64 come straight out of the AT strings)
|
|
bpl sendNextModemCommandChar; E7C3 no delay pending (counter >= 0): send the next byte of the modem command string
|
|
inx ; E7C5 count the guard time up towards zero
|
|
storeGuardDelay:
|
|
stx modemDelayCounter ; E7C6 store it; this is also where a $80-$FF byte taken out of an AT string is parked as a fresh delay
|
|
modemTickReturn:
|
|
rts ; E7C9 nothing more to do this tick
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendNextModemCommandChar - Tail of the per-frame link service. It pops the next byte queued for the
|
|
; modem: a byte with bit 7 set is not a character but a guard-time delay and is parked in
|
|
; modemDelayCounter $EB01, anything else goes to the UART transmitter. It then ages the modem answer
|
|
; timer $E03F. Sending the CR that ends an AT command arms the timer with $79 (121 frames, about two
|
|
; seconds); while no carrier is present the timer is pinned at 1 so it fires on the first tick after
|
|
; carrier appears. When it does fire the routine fabricates a CR into the game's chat input and falls
|
|
; straight into runLinkStateMachine - which is how a modem game leaves terminal mode and starts the
|
|
; byte sync.
|
|
; In: the 16-byte host-output ring $E091 (via popHostOutRing), $E03F modem answer timer, $E03C bit 6
|
|
; = carrier detect, $EB01 guard-time counter
|
|
; Out: byte queued for transmission, $EB01 or $E03F updated, possibly a CR pushed into the host-input
|
|
; ring $E086; falls through into runLinkStateMachine
|
|
; Called from: runModemStateMachine ($E7C3), its only entry
|
|
; ----------------------------------------------------------------------
|
|
sendNextModemCommandChar:
|
|
jsr popHostOutRing ; E7CA next byte queued for the modem: AT-string bytes, or the player's chat text in terminal mode
|
|
bcs keepReplyTimer ; E7CD queue empty - nothing to send, just age the answer timer
|
|
tax ; E7CF keep a copy of the byte in X
|
|
D_E7D0:
|
|
bmi storeGuardDelay ; E7D0 bit 7 set: this is a guard-time delay, not a character - park it in $EB01 and stop for this tick
|
|
jsr queueByteForTransmit ; E7D2 hand the character to the UART transmitter (A survives the call)
|
|
ldx #$79 ; E7D5 $79 = 121 frames, roughly two seconds, to wait for the modem's answer
|
|
bit modemReplyTimer ; E7D7 $E03F with bit 7 set means 'idle, no answer expected' ($FF from queueModemCommandString, $8D from the C=+RETURN hot key)
|
|
bpl tickModemReplyTimer ; E7DA a countdown is already running - just tick it, do not re-arm
|
|
eor #$0D ; E7DC did we just transmit the CR that terminates an AT command?
|
|
beq tickModemReplyTimer ; E7DE yes - arm the 121-frame answer timeout
|
|
keepReplyTimer:
|
|
ldx modemReplyTimer ; E7E0 nothing was sent: carry on with the timer value already stored
|
|
tickModemReplyTimer:
|
|
bit linkStatus ; E7E3 bit 6 (V) of the debounced carrier byte = carrier detect
|
|
bvs countReplyTimerDown ; E7E6 carrier present: age the real timer value in X
|
|
ldx #$02 ; E7E8 no carrier yet: hold the timer at 1 (2 minus the DEX below) so that it fires on the first tick after carrier appears
|
|
countReplyTimerDown:
|
|
dex ; E7EA tick the answer timer down
|
|
bmi modemTickReturn ; E7EB timer was 0 or negative (idle) - nothing to do
|
|
stx modemReplyTimer ; E7ED store the new value
|
|
L_E7F0:
|
|
bne modemTickReturn ; E7F0 still counting
|
|
lda #$0D ; E7F2 timer reached zero: fabricate the CR the modem never sent
|
|
L_E7F4:
|
|
jsr pushHostInRing ; E7F4 push it into the host-input ring so the game's chat line unblocks, then fall into the link state machine below
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runLinkStateMachine - Second half of the per-frame link service: it dispatches on connectionPhase
|
|
; $E040. Any error counted by countLinkError ($E403) since the last tick restarts the byte sync
|
|
; whatever the phase; otherwise phase 2 continues the sync, phase 3 (or above) runs the packet
|
|
; protocol, and phases 0 and 1 fall through into a fresh byte sync.
|
|
; In: $E040 connection phase, $E047 link error counter
|
|
; Out: branches into beginByteSyncPhase, the sync continuation at $E828 or runPacketPhase; never
|
|
; returns to its caller directly
|
|
; Called from: runModemStateMachine ($E78A when carrier is already up) and by fall-through from
|
|
; sendNextModemCommandChar ($E7F4)
|
|
; ----------------------------------------------------------------------
|
|
runLinkStateMachine:
|
|
lda connectionPhase ; E7F7 current connection phase
|
|
ldx linkErrorCount ; E7FA link error counter, bumped from the NMI receiver by countLinkError on a bad stop bit or a receive overflow
|
|
bne beginByteSyncPhase ; E7FD any error since the last tick: throw the connection away and re-synchronise
|
|
dispatchCommState:
|
|
cmp #$02 ; E7FF phase 2 = a byte sync is already in progress
|
|
beq continueByteSync ; E801 continue the sync where it left off
|
|
bcs runPacketPhase ; E803 phase 3 or above: run the reliable packet protocol
|
|
|
|
; ----------------------------------------------------------------------
|
|
; beginByteSyncPhase - Connection phase 2, the byte-alignment handshake that runs before any packet
|
|
; may be sent. It empties both UART rings, clears the sync state and then plays a two-step game with
|
|
; the peer: keep sending $00 until a $00 comes back, then answer $FF and wait for the peer's $FF. Ten
|
|
; rounds without the peer's $FF restart the whole thing. On success the peer's request for an
|
|
; acknowledgement is recorded and control drops into runPacketPhase.
|
|
; In: the UART rings ($E417 transmit, $E430 receive), syncRoundCounter $EAB7, zeroRunLength $EAB8,
|
|
; $E0A5 pending transmit count
|
|
; Out: $E040 = 2 then 3, $E047/$EAB7/$EAB8/$EAB9 cleared, $00 and $FF bytes transmitted, $E044 paced,
|
|
; ackPending $EAC0 = $FF on success
|
|
; Called from: runLinkStateMachine ($E7FD, and by fall-through past $E803) and noteIdlePoll ($E9A4)
|
|
; after sixteen dead service ticks
|
|
; ----------------------------------------------------------------------
|
|
beginByteSyncPhase:
|
|
lda #$02 ; E805 2 = the byte-sync phase
|
|
sta connectionPhase ; E807 enter it
|
|
drainRxRingLoop:
|
|
jsr popUartRxRing ; E80A throw away every byte already sitting in the UART receive ring
|
|
bcc drainRxRingLoop ; E80D loop until it is empty (C = 1)
|
|
restartByteSync:
|
|
jsr flushUartTxRing ; E80F drop anything still queued for transmission; this is also the 'start the sync over' entry
|
|
lda #$00 ; E812 A = 0: clears the sync state and doubles as the first sync byte
|
|
sta linkErrorCount ; E814 forget any link errors
|
|
sta rxPayloadRemaining ; E817 no packet is half received
|
|
sta packetProtocolState ; E81A syncRoundCounter ($EAB7, the first byte of the packet-protocol state block) = 0 = 'waiting for the peer's $00'
|
|
queueSyncByte:
|
|
jsr queueByteForTransmit ; E81D queue the sync byte held in A ($00 or $FF) for transmission
|
|
lda #$02 ; E820 bit 1 of the transmit pacing counter...
|
|
ora txPaceCounter ; E822 ...is forced on...
|
|
sta txPaceCounter ; E825 ...so at least two NMI transmit slots separate the sync bytes
|
|
continueByteSync:
|
|
lda packetProtocolState ; E828 sync round counter
|
|
bne awaitPeerSyncFf ; E82B non-zero: we are past the $00 round and are waiting for the peer's $FF
|
|
lda uartPendingCount ; E82D characters still queued for the transmitter
|
|
beq queueSyncByte ; E830 transmitter idle: send another $00 (A is 0 here)
|
|
awaitPeerSyncZero:
|
|
lda zeroRunLength ; E832 run of consecutive $00 bytes received from the peer
|
|
bne answerSyncWithFf ; E835 the peer's $00 has already been seen
|
|
jsr receiveByteTrackZeros ; E837 otherwise take one more byte off the line and update the zero run
|
|
L_E83A:
|
|
txa ; E83A X = the new run length
|
|
beq linkTickReturn ; E83B nothing from the peer yet - try again next tick
|
|
answerSyncWithFf:
|
|
inc packetProtocolState ; E83D the peer answered: move on to round 1
|
|
lda #$FF ; E840 answer with $FF...
|
|
bne queueSyncByte ; E842 ...always taken, queue it
|
|
linkTickReturn:
|
|
rts ; E844 shared 'nothing more to do this tick' exit for the whole link service
|
|
|
|
awaitPeerSyncFf:
|
|
cmp #$0A ; E845 ten rounds without the peer's $FF?
|
|
bcs restartByteSync ; E847 give up and start the sync from scratch
|
|
inc packetProtocolState ; E849 count this round
|
|
jsr receiveByteTrackZeros ; E84C look for the peer's answer
|
|
tax ; E84F A = 0 means either a $00 arrived or the ring was empty
|
|
beq linkTickReturn ; E850 the peer has not answered $FF yet - wait for the next tick
|
|
inx ; E852 was it $FF?
|
|
bne restartByteSync ; E853 some other byte on the line: the peer is not where we think it is, resync
|
|
sta ackPending ; E855 $FF received, both ends are byte aligned; A = $FF also records that the peer wants an acknowledgement
|
|
|
|
; ----------------------------------------------------------------------
|
|
; runPacketPhase - Connection phase 3, one tick of the reliable packet layer. It first decodes
|
|
; anything the peer sent, then, if the transmitter has room, emits at most one thing: a deferred raw
|
|
; sync byte ($00 nak or $FF ack request), a retransmission of the oldest unacknowledged message, a
|
|
; freshly built message packet, or a bare acknowledgement. The message-building branch at $E8A8 is
|
|
; dead in the shipped build: its gate $E0A6 is only ever written with 0 (at $E778), so outgoing game
|
|
; traffic really leaves through the older frame engine at $E1DD and this layer only receives, decodes
|
|
; and acknowledges.
|
|
; In: $E0A5 pending transmit count, sendZeroPending $EAC2, sendFfPending $EAC1, $E0A4
|
|
; packet-in-flight, $E0A6 send enable, resendRequest $EABF, the outstanding-message indices
|
|
; $EAC4/$EAC5/$EAC6, ackPending $EAC0
|
|
; Out: packets clocked out through sendPacket; $EAC0/$EAC1/$EAC2/$EABF cleared,
|
|
; $EAC3/$EAC4/$EAC5/$EAC6/$EABE updated
|
|
; Called from: runLinkStateMachine ($E803) and by fall-through from the successful end of the byte
|
|
; sync ($E855)
|
|
; ----------------------------------------------------------------------
|
|
runPacketPhase:
|
|
lda #$03 ; E858 3 = the packet-protocol phase
|
|
sta connectionPhase ; E85A enter (or stay in) it
|
|
jsr receivePacket ; E85D decode whatever the peer has sent since the last tick
|
|
servicePacketTransmit:
|
|
lda uartPendingCount ; E860 characters still queued for the UART transmitter
|
|
cmp #$02 ; E863 two or more still to go?
|
|
bcs linkTickReturn ; E865 the line is busy - do not start anything else this tick
|
|
ldx #$00 ; E867 X = 0, the value used to clear the two raw-byte requests
|
|
txa ; E869 A = 0 = the raw $00 byte that means 'resend'
|
|
bit sendZeroPending ; E86A sendZeroPending, made negative by receivePacket when a checksum failed
|
|
stx sendZeroPending ; E86D consume the request
|
|
bmi queueRawSyncByte ; E870 a $00 nak is owed - send it
|
|
lda #$FF ; E872 otherwise A = $FF = the raw byte that means 'acknowledge me'
|
|
bit sendFfPending ; E874 sendFfPending - nothing in the shipped image ever sets this, so this path is dead
|
|
bpl checkPacketInFlight ; E877 no raw byte owed
|
|
queueRawSyncByte:
|
|
stx sendFfPending ; E879 consume the $FF request
|
|
jsr queueByteForTransmit ; E87C put the raw sync byte on the wire
|
|
checkPacketInFlight:
|
|
lda frameInFlightFlag ; E87F is a message packet already in flight?
|
|
sub_E882:
|
|
bne packetPhaseReturn ; E882 yes - wait until it has been acknowledged
|
|
ldx resendRequest ; E884 resendRequest: 1 = the peer sent a lone $00, negative = resend the message with this index
|
|
beq buildNextMessage ; E887 nothing to resend
|
|
sta resendRequest ; E889 consume the request (A is 0 here)
|
|
bpl resendOldestMessage ; E88C N still comes from the LDX above: a plain $00 nak, so resend the oldest unacknowledged message
|
|
cpx txOldestUnackedIndex ; E88E targeted resend: does the request still name the oldest outstanding message?
|
|
bne buildNextMessage ; E891 no - it has already been retired, ignore the stale request
|
|
resendOldestMessage:
|
|
lda txOldestUnackedIndex ; E893 index of the oldest unacknowledged message; bit 7 = the slot is in use
|
|
bpl buildAckPacket ; E896 nothing outstanding - send a bare acknowledgement instead
|
|
and #$7F ; E898 strip the in-use flag to get the message ring index
|
|
jsr sendPacket ; E89A retransmit that message
|
|
D_E89D:
|
|
rts ; E89D done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Build a brand new packet out of the game's outbound message ring. Unreachable in the shipped build:
|
|
; $E0A6 is only ever written with 0.
|
|
; ----------------------------------------------------------------------
|
|
buildNextMessage:
|
|
ldx txNextFreeIndex ; E89E index of the next free slot in the 35-byte message ring
|
|
bmi sendAckIfPending ; E8A1 negative = both outstanding slots are busy, so no new message may be started
|
|
lda packetInFlightCount ; E8A3 packet-layer send enable; sendPacketFsm does set it (indexed, at $E265), so the code below IS reachable
|
|
beq sendAckIfPending ; E8A6 not enabled: fall through to the acknowledgement
|
|
inc frameInFlightFlag ; E8A8 mark a message as in flight
|
|
stx txRingIndex ; E8AB start writing at the free ring position
|
|
jsr popArqTxRingThunk ; E8AE first byte of the game's queued message; its low nibble is the argument count
|
|
sta packetIndexSave ; E8B1 keep the original command byte
|
|
and #$0F ; E8B4 low nibble = number of argument bytes (game commands are $80-$A6 with 0-3 arguments)
|
|
sta packetByteCounter ; E8B6 use it as the copy counter
|
|
jsr writeTxRingByte ; E8B9 store the argument count as the message's length byte
|
|
inc txSeqCounter ; E8BC advance the transmit sequence number
|
|
lda txSeqCounter ; E8BF read it back...
|
|
and #$03 ; E8C2 ...and keep two bits of it
|
|
asl a ; E8C4 shift the sequence...
|
|
asl a ; E8C5 ...into bits 2-3
|
|
eor packetIndexSave ; E8C6 merge it with the command byte...
|
|
eor packetByteCounter ; E8C9 ...and cancel the length nibble again, giving (command AND $F0) OR (sequence << 2)
|
|
copyMessageByteLoop:
|
|
jsr writeTxRingByte ; E8CC append the control byte, and on every later pass one argument byte
|
|
dec packetByteCounter ; E8CF one fewer argument to copy
|
|
bmi finishMessageBuild ; E8D2 all arguments copied
|
|
jsr popArqTxRingThunk ; E8D4 next argument byte out of the game's outbound ring $E447
|
|
bcc copyMessageByteLoop ; E8D7 got one - store it
|
|
finishMessageBuild:
|
|
stx txRingTailIndex ; E8D9 remember the ring index just past this message
|
|
lda txNextFreeIndex ; E8DC index of the message just built
|
|
claimOutstandingSlot:
|
|
pha ; E8DF save it for the transmit call below
|
|
ora #$80 ; E8E0 mark the slot as used
|
|
bit txOldestUnackedIndex ; E8E2 is the oldest-unacknowledged slot already occupied?
|
|
bmi storeNextFreeIndex ; E8E5 yes - this message becomes the second outstanding one
|
|
sta txOldestUnackedIndex ; E8E7 no - this message becomes the oldest outstanding message...
|
|
txa ; E8EA ...and the free index moves to the end of the message (X = $EAC4)
|
|
storeNextFreeIndex:
|
|
sta txNextFreeIndex ; E8EB store the new free / second-outstanding index
|
|
pla ; E8EE recover the ring index of the message to send
|
|
jsr sendPacket ; E8EF build the packet and clock it out
|
|
packetPhaseReturn:
|
|
rts ; E8F2 done
|
|
|
|
sendAckIfPending:
|
|
lda ackPending ; E8F3 does the peer still need an acknowledgement?
|
|
beq packetPhaseReturn ; E8F6 no - nothing at all to send this tick
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildAckPacket - Builds the three-byte bare acknowledgement in packetBuffer: length code $55 (one
|
|
; payload byte) and control byte $EC, then falls into the common send tail at $E90C which ORs in the
|
|
; receive sequence number, appends the check byte and transmits it.
|
|
; In: none
|
|
; Out: packetBuffer = $55,$EC, packetLength = 2; packet transmitted, ackPending $EAC0 cleared
|
|
; Called from: runPacketPhase ($E896) when a data packet has arrived but no message of our own is
|
|
; outstanding
|
|
; ----------------------------------------------------------------------
|
|
buildAckPacket:
|
|
lda #$EC ; E8F8 $EC = the bare-acknowledgement control byte (any control byte $E0-$EF is an ack)
|
|
ldx #$55 ; E8FA $55 = packetLengthCodeTable entry 1, the code for a one-byte payload
|
|
stx packetBuffer ; E8FC packet byte 0 = length code
|
|
sta packetControlByte ; E8FF packet byte 1 = control byte
|
|
lda #$02 ; E902 two bytes...
|
|
sta packetLength ; E904 ...before the check byte
|
|
bne sendBuiltPacket ; E907 always taken: skip the message-copy step and go straight to the send tail
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendPacket - Sends one message from the 35-byte transmit ring: buildPacketFromRing copies it into
|
|
; packetBuffer, the low two bits of rxSeqCounter are piggy-backed into the control byte as the
|
|
; acknowledgement of everything received so far, computePacketChecksum appends the check byte, and
|
|
; every byte is pushed into the UART transmit ring. Entry $E90C skips the copy step and is used by
|
|
; buildAckPacket.
|
|
; In: A = index into txMessageRing $EAC7 (entry $E909 only); rxSeqCounter $EABD; packetBuffer and
|
|
; packetLength for entry $E90C
|
|
; Out: finished packet in $EAEB.., all of its bytes queued for transmission, ackPending $EAC0 cleared,
|
|
; $EABA and txRingIndex $EAC3 clobbered
|
|
; Called from: runPacketPhase ($E89A retransmit, $E8EF new message) and by fall-through from
|
|
; buildAckPacket
|
|
; ----------------------------------------------------------------------
|
|
sendPacket:
|
|
jsr buildPacketFromRing ; E909 copy the message at ring index A into packetBuffer and prefix it with its length code
|
|
sendBuiltPacket:
|
|
lda rxSeqCounter ; E90C the sequence number of the next packet we expect to receive
|
|
and #$03 ; E90F keep two bits of it
|
|
ora packetControlByte ; E911 piggy-back it in the control byte's low two bits...
|
|
sta packetControlByte ; E914 ...so every packet also acknowledges what we have received
|
|
jsr computePacketChecksum ; E917 checksum packet bytes 0..packetLength-1; X comes back equal to packetLength
|
|
sta packetBuffer,x ; E91A append the check byte right after the payload
|
|
lda #$00 ; E91D this packet carries the acknowledgement itself...
|
|
sta ackPending ; E91F ...so the pending bare-ack request is satisfied
|
|
ldx #$00 ; E922 start at packet byte 0
|
|
transmitPacketLoop:
|
|
stx packetByteCounter ; E924 save the index - queueByteForTransmit clobbers X
|
|
lda packetBuffer,x ; E927 next packet byte
|
|
jsr queueByteForTransmit ; E92A hand it to the UART transmitter
|
|
ldx packetByteCounter ; E92D restore the index
|
|
cpx packetLength ; E930 have we just queued the check byte? the carry is taken before the INX below
|
|
inx ; E933 advance to the next byte
|
|
L_E934:
|
|
bcc transmitPacketLoop ; E934 loop; the check byte at index packetLength is the last one sent
|
|
rts ; E936 the packet is on the wire
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; computePacketChecksum - Rotate-and-add check byte over packetBuffer[0..packetLength-1]. A starts at
|
|
; $EE and each byte contributes ASL / ADC #0 (fold the bit shifted out back in) / ADC byte / ADC #0
|
|
; (fold the add carry back in), so both the value and the position of every byte matter.
|
|
; In: packetLength $EAEA, packetBuffer $EAEB..
|
|
; Out: A = check byte, X = packetLength, i.e. the index of the check byte slot
|
|
; Called from: sendPacket ($E917, result appended) and receivePacket ($EA01, result EORed with the
|
|
; received check byte)
|
|
; ----------------------------------------------------------------------
|
|
computePacketChecksum:
|
|
ldx #$00 ; E937 start at packet byte 0
|
|
lda #$EE ; E939 $EE = the checksum seed
|
|
checksumLoop:
|
|
asl a ; E93B rotate the running sum left...
|
|
adc #$00 ; E93C ...folding the bit that fell out back in at the bottom
|
|
adc packetBuffer,x ; E93E add the packet byte
|
|
adc #$00 ; E941 fold that carry back in too
|
|
inx ; E943 next byte
|
|
cpx packetLength ; E944 stop at packetLength - the check byte slot itself is not included
|
|
bcc checksumLoop ; E947 loop
|
|
rts ; E949 return A = check byte, X = index of the check byte slot
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; buildPacketFromRing - Copies one queued message out of the 35-byte transmit ring into packetBuffer.
|
|
; A message is stored as [argument count][control byte][arguments...]; the argument count plus one is
|
|
; the payload length and is turned into one of the sixteen transition-rich code bytes of
|
|
; packetLengthCodeTable, which becomes packet byte 0.
|
|
; In: A = ring index of the message's length byte; txMessageRing $EAC7, packetLengthCodeTable $EAA0
|
|
; Out: packetBuffer filled, packetLength $EAEA = number of bytes before the check byte, txRingIndex
|
|
; $EAC3 left just past the message, $EABA clobbered
|
|
; Called from: sendPacket ($E909)
|
|
; ----------------------------------------------------------------------
|
|
buildPacketFromRing:
|
|
tax ; E94A A = ring index of the message's length byte
|
|
jsr readTxRingByteAt ; E94B read it; the ring index is left pointing at the control byte
|
|
tax ; E94E X = argument count
|
|
inx ; E94F payload length = arguments + 1, because the control byte counts as payload too
|
|
L_E950:
|
|
stx packetByteCounter ; E950 use it as the copy counter
|
|
lda packetReturn,x ; E953 packetLengthCodeTable[length]; the table base $EAA0 is the shared RTS, so entry 0 is never selected
|
|
ldx #$00 ; E956 packet byte 0...
|
|
beq storePacketByte ; E958 ...always taken, store the length code there
|
|
copyRingByteLoop:
|
|
jsr readTxRingByte ; E95A next byte of the message out of the ring
|
|
sub_E95D:
|
|
ldx packetLength ; E95D append it at the current packet length
|
|
storePacketByte:
|
|
sta packetBuffer,x ; E960 store the packet byte
|
|
inx ; E963 advance...
|
|
stx packetLength ; E964 ...and record the new packet length
|
|
dec packetByteCounter ; E967 one fewer byte to copy
|
|
bpl copyRingByteLoop ; E96A loop over the control byte and every argument
|
|
rts ; E96C packetBuffer now holds [length code][control][arguments]
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; writeTxRingByte - Appends A to the 35-byte transmit message ring $EAC7 at txRingIndex and advances
|
|
; the index modulo 35.
|
|
; In: A = byte, txRingIndex $EAC3
|
|
; Out: ring updated, $EAC3 advanced (wrapping at $23), X = the new index
|
|
; Called from: the message builder in runPacketPhase ($E8B9, $E8CC)
|
|
; ----------------------------------------------------------------------
|
|
writeTxRingByte:
|
|
ldx txRingIndex ; E96D current write position in the 35-byte message ring
|
|
sta txMessageRing,x ; E970 store the byte
|
|
bpl advanceTxRingIndex ; E973 always taken (the index is 0..34): go and advance it
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readTxRingByte - Reads the transmit-ring byte at txRingIndex and advances the index modulo 35.
|
|
; In: txRingIndex $EAC3
|
|
; Out: A = byte, $EAC3 advanced, X = the new index
|
|
; Called from: buildPacketFromRing ($E95A)
|
|
; ----------------------------------------------------------------------
|
|
readTxRingByte:
|
|
ldx txRingIndex ; E975 current read position in the message ring
|
|
|
|
; ----------------------------------------------------------------------
|
|
; readTxRingByteAt - Reads the transmit-ring byte at an arbitrary index X and leaves txRingIndex
|
|
; pointing one past it. Used to start a retransmission and to inspect the header of a message the
|
|
; peer has acknowledged.
|
|
; In: X = ring index 0..34
|
|
; Out: A = byte, txRingIndex $EAC3 = X+1 modulo 35, X = the new index
|
|
; Called from: buildPacketFromRing ($E94B) and the acknowledgement handler ($EA40, $EA43)
|
|
; ----------------------------------------------------------------------
|
|
readTxRingByteAt:
|
|
lda txMessageRing,x ; E978 read txMessageRing[X]
|
|
advanceTxRingIndex:
|
|
inx ; E97B advance the index
|
|
cpx #$23 ; E97C $23 = 35 = the ring size
|
|
bcc storeTxRingIndex ; E97E no wrap needed
|
|
ldx #$00 ; E980 wrap back to the start of the ring
|
|
storeTxRingIndex:
|
|
stx txRingIndex ; E982 save the new index
|
|
txRingReturn:
|
|
rts ; E985 return A = byte, X = new index
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; noteIdlePoll - Called when the receiver found nothing on the line. It only counts a stall when a
|
|
; message really is outstanding and we are not still transmitting it; otherwise silence is normal and
|
|
; stallCounter $EB00 is reset. The NMI receiver also zeroes $EB00 (at $E6A5) on every character that
|
|
; arrives, so the counter really measures consecutive dead service ticks. Sixteen of them drop two
|
|
; return addresses off the stack and restart the byte sync.
|
|
; In: the outstanding-message indices $EAC5/$EAC6, $E0A4 packet-in-flight, stallCounter $EB00
|
|
; Out: $EB00 updated; on overflow the stack is unwound and control jumps to beginByteSyncPhase, so the
|
|
; caller never returns
|
|
; Called from: receivePacket ($E9B2 when the ring is empty); entry $E997 also from $E9DC when a packet
|
|
; is only half received
|
|
; ----------------------------------------------------------------------
|
|
noteIdlePoll:
|
|
ldx #$00 ; E986 X = 0 = 'not stalled'
|
|
lda txOldestUnackedIndex ; E988 oldest unacknowledged message slot
|
|
bpl storeStallCount ; E98B positive = nothing outstanding, so an idle line is perfectly normal - reset the counter
|
|
lda txNextFreeIndex ; E98D the second outstanding slot
|
|
bmi countStalledPoll ; E990 negative = the send window is full, so we really are stalled
|
|
lda frameInFlightFlag ; E992 is a packet still being clocked out?
|
|
L_E995:
|
|
bne storeStallCount ; E995 yes - progress is being made, reset the counter
|
|
countStalledPoll:
|
|
ldx D_EB00 ; E997 count one more fruitless service tick
|
|
inx ; E99A bump it
|
|
storeStallCount:
|
|
stx D_EB00 ; E99B store the counter
|
|
cpx #$10 ; E99E sixteen consecutive dead ticks?
|
|
bcc txRingReturn ; E9A0 not yet - return normally
|
|
unwindAndResync:
|
|
pla ; E9A2 discard receivePacket's return address...
|
|
pla ; E9A3 ...and its caller's...
|
|
jmp beginByteSyncPhase ; E9A4 ...so the whole link restarts from the byte-sync handshake
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Entered from $E9C5: a raw $FF from the peer means 'acknowledge me'. Record it and fall into the top
|
|
; of receivePacket to look at the next byte.
|
|
; ----------------------------------------------------------------------
|
|
noteAckRequest:
|
|
sta ackPending ; E9A7 remember that the peer is asking for an acknowledgement, then loop back into the scanner below
|
|
|
|
; ----------------------------------------------------------------------
|
|
; receivePacket - Assembles one incoming packet from the UART receive ring. The inter-packet bytes
|
|
; are handled first: a lone $00 is the peer's 'resend' nak, two or more in a row mean the line has
|
|
; lost sync, and $FF asks for an acknowledgement. The first other byte is looked up in
|
|
; packetLengthCodeTable to learn the payload length; the routine then waits (across ticks, remembering
|
|
; its state in rxPayloadRemaining) until payload and check byte are both queued, copies them into
|
|
; packetBuffer and verifies the checksum. A bad checksum arranges a $00 nak for the next tick; a good
|
|
; packet falls into dispatchReceivedPacket.
|
|
; In: the UART receive ring $E430/$E42D, zeroRunLength $EAB8, rxPayloadRemaining $EAB9,
|
|
; packetLengthCodeTable $EAA0
|
|
; Out: $EAB9, rxLengthCodeByte $EAFF, packetLength $EAEA, packetBuffer, resendRequest $EABF,
|
|
; ackPending $EAC0, sendZeroPending $EAC2; may resync through $E9A2
|
|
; Called from: runPacketPhase ($E85D), once per frame while the link is in phase 3
|
|
; ----------------------------------------------------------------------
|
|
receivePacket:
|
|
ldx rxPayloadRemaining ; E9AA bytes still expected for a packet whose header has already arrived
|
|
bne awaitWholePacket ; E9AD resume that packet
|
|
jsr receiveByteTrackZeros ; E9AF take the next byte and update the zero-run counter
|
|
bcs noteIdlePoll ; E9B2 ring empty - count an idle poll (which may force a resync) and return
|
|
eor #$00 ; E9B4 set Z from the byte
|
|
bne checkForFfSync ; E9B6 not a $00 inter-packet byte
|
|
dex ; E9B8 X = the zero-run length...
|
|
dex ; E9B9 ...minus two
|
|
L_E9BA:
|
|
bpl unwindAndResync ; E9BA two or more $00 in a row: the peer is not framing any more, resync the link
|
|
lda #$01 ; E9BC a single $00 is the peer's 'resend the last message' nak...
|
|
sta resendRequest ; E9BE ...record it for runPacketPhase
|
|
bne receivePacket ; E9C1 always taken: go back and look at the next byte
|
|
checkForFfSync:
|
|
cmp #$FF ; E9C3 $FF is the peer's 'acknowledge me' inter-packet byte
|
|
beq noteAckRequest ; E9C5 record it and keep scanning
|
|
sta rxLengthCodeByte ; E9C7 anything else must be a packet header: remember the length code byte
|
|
ldx #$00 ; E9CA search packetLengthCodeTable from entry 1 upwards
|
|
lengthCodeSearchLoop:
|
|
inx ; E9CC next table entry
|
|
cmp packetReturn,x ; E9CD compare; the table is sorted ascending and ends in an $FF sentinel, so the search always terminates
|
|
beq beginPacketBody ; E9D0 found: X = payload length 1..16
|
|
bcs lengthCodeSearchLoop ; E9D2 the received byte is still above this entry - keep searching
|
|
bcc unwindAndResync ; E9D4 it fell between two entries, so it is not a valid length code: resync
|
|
beginPacketBody:
|
|
stx rxPayloadRemaining ; E9D6 remember how many payload bytes are still to come
|
|
awaitWholePacket:
|
|
cpx uartRxCount ; E9D9 are the payload and its check byte all in the receive ring yet? we need X+1 bytes
|
|
bcs countStalledPoll ; E9DC not yet - count the wait and come back next tick with the state preserved
|
|
lda rxLengthCodeByte ; E9DE re-fetch the length code byte
|
|
L_E9E1:
|
|
sta packetBuffer ; E9E1 it is packet byte 0
|
|
D_E9E4:
|
|
ldx #$00 ; E9E4 start filling...
|
|
stx packetLength ; E9E6 ...at packet byte 1
|
|
copyPacketByteLoop:
|
|
jsr popUartRxRingThunk ; E9E9 next byte off the wire
|
|
inc packetLength ; E9EC one more byte in the packet...
|
|
ldx packetLength ; E9EF ...index it...
|
|
sta packetBuffer,x ; E9F2 ...and store it
|
|
dec rxPayloadRemaining ; E9F5 one fewer byte to read
|
|
bpl copyPacketByteLoop ; E9F8 loop over the payload and the check byte
|
|
inc rxPayloadRemaining ; E9FA back to 0: no packet is half received any more
|
|
eor #$00 ; E9FD the check byte should never be $00 - that is an inter-packet byte
|
|
beq unwindAndResync ; E9FF it was - treat the packet as garbage and resync
|
|
jsr computePacketChecksum ; EA01 recompute the checksum over bytes 0..packetLength-1
|
|
eor packetBuffer,x ; EA04 compare it with the received check byte at index packetLength
|
|
beq dispatchReceivedPacket ; EA07 they match - act on the packet
|
|
dec sendZeroPending ; EA09 bad checksum: make the next tick put a raw $00 nak on the wire
|
|
rts ; EA0C drop the packet
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; dispatchReceivedPacket - Acts on a packet whose checksum was good. A control byte of $F0 or above
|
|
; is a 1541 pass-through: the bytes are clocked straight to the drive with interrupts off. $E0-$EF is
|
|
; a pure acknowledgement, which retires the outstanding message(s) by comparing the sequence field of
|
|
; the acknowledged message with the ack byte. Anything below $E0 is a data packet: it is accepted
|
|
; only when its 2-bit sequence field is the one expected next and the game's inbound command ring is
|
|
; empty, and the game's original command byte is then rebuilt from the control byte and the payload
|
|
; length. The pass-through byte count comes from the control byte itself (256 minus its value), not
|
|
; from the packet length, so a control byte that disagrees with the payload length would read past the
|
|
; packet buffer. Together with remoteCodeBuffer at $EB03 this gives the peer two ways to reach into
|
|
; this machine.
|
|
; In: packetBuffer/packetControlByte, packetLength $EAEA, rxSeqCounter $EABD, $E459 inbound ring
|
|
; count, the outstanding-message indices $EAC4/$EAC5/$EAC6
|
|
; Out: $EABB/$EABC/$EABD/$EABF/$EAC0/$EAC5/$EAC6 updated; decoded command bytes pushed into the
|
|
; inbound ring $E45C
|
|
; Called from: receivePacket ($EA07), by fall-through, on every packet that passes the checksum
|
|
; ----------------------------------------------------------------------
|
|
dispatchReceivedPacket:
|
|
ldx packetControlByte ; EA0D the control byte, i.e. packet byte 1
|
|
L_EA10:
|
|
cpx #$F0 ; EA10 $F0 and above is the 1541 pass-through channel
|
|
bcc handleAckOrDataPacket ; EA12 below: an ordinary acknowledgement or data packet
|
|
php ; EA14 save the interrupt state
|
|
L_EA15:
|
|
sei ; EA15 the drive handshake is bit-banged, so the UART NMI must not interrupt it
|
|
beq sendFinalDriveByte ; EA16 Z still comes from the CPX: a control byte of exactly $F0 sends a single byte
|
|
tay ; EA18 A is 0 here (the checksum matched), so Y = 0 = the first payload byte
|
|
txa ; EA19 the first byte handed to the drive is the control byte itself
|
|
L_EA1A:
|
|
sta packetIndexSave ; EA1A $EAFE counts up to zero, so 256 minus the control byte bytes are sent in total
|
|
sendDriveByteLoop:
|
|
jsr sendByteToDrive ; EA1D clock one byte out to the 1541 over CIA2 port A
|
|
lda packetPayload,y ; EA20 next payload byte ($EAED = packet byte 2)
|
|
iny ; EA23 advance the payload index
|
|
inc packetIndexSave ; EA24 count the byte
|
|
D_EA27:
|
|
bne sendDriveByteLoop ; EA27 loop until the counter wraps to zero
|
|
sendFinalDriveByte:
|
|
jsr sendByteToDrive ; EA29 send the last byte
|
|
plp ; EA2C restore the interrupt state
|
|
L_EA2D:
|
|
rts ; EA2D done with the pass-through
|
|
|
|
handleAckOrDataPacket:
|
|
stx rxAckControlByte ; EA2E remember the received control byte for the sequence comparison
|
|
lda txOldestUnackedIndex ; EA31 is any message of ours outstanding?
|
|
bpl handleDataPacket ; EA34 no - there is nothing to retire
|
|
D_EA36:
|
|
cpx #$EC ; EA36 control bytes $EC and above also ask for the outstanding message to be resent
|
|
sub_EA38:
|
|
bcc checkOutstandingSequence; EA38 below $EC: a plain acknowledgement
|
|
sta resendRequest ; EA3A ask runPacketPhase to resend the message named by $EAC6 (A is that negative index)
|
|
checkOutstandingSequence:
|
|
and #$7F ; EA3D strip the in-use bit to get the ring index
|
|
tax ; EA3F index the ring with it
|
|
jsr readTxRingByteAt ; EA40 skip the message's argument-count byte
|
|
jsr readTxRingByteAt ; EA43 read its control byte
|
|
sub_EA46:
|
|
lsr a ; EA46 the message's own sequence number sits in bits 2-3...
|
|
lsr a ; EA47 ...so shift it down to bits 0-1
|
|
clc ; EA48 CLC before SBC subtracts one extra...
|
|
sbc rxAckControlByte ; EA49 ...from the sequence carried in the ack byte's low two bits
|
|
and #$02 ; EA4C bit 1 of the difference separates 'covered by this ack' from 'not yet acknowledged'
|
|
beq handleDataPacket ; EA4E acknowledged - the send window is clear again
|
|
ldx txRingTailIndex ; EA50 not acknowledged: shuffle the outstanding slots down one place...
|
|
lda txNextFreeIndex ; EA53 ...moving the newest index...
|
|
stx txNextFreeIndex ; EA56 ...into the free slot...
|
|
sta txOldestUnackedIndex ; EA59 ...and the old free slot into the oldest-unacknowledged slot
|
|
bmi checkOutstandingSequence; EA5C another message is still outstanding - test that one too
|
|
handleDataPacket:
|
|
lda packetControlByte ; EA5E the control byte again
|
|
cmp #$E0 ; EA61 $E0-$EF are pure acknowledgements
|
|
bcs packetReturn ; EA63 an ack carries no payload, so there is nothing left to do
|
|
acceptDataPacket:
|
|
dec ackPending ; EA65 a data packet, so we now owe the peer an acknowledgement
|
|
sta rxDataControlByte ; EA68 remember the control byte
|
|
lsr a ; EA6B its 2-bit sequence number sits in bits 2-3...
|
|
lsr a ; EA6C ...shift it down
|
|
clc ; EA6D CLC before SBC subtracts one extra, so a difference of 0 means 'the very next packet'
|
|
sbc rxSeqCounter ; EA6E compare with the sequence we expect
|
|
and #$03 ; EA71 only the two sequence bits matter
|
|
ora arqRxCount ; EA73 and the game's inbound command ring must be empty before we hand it anything
|
|
bne packetReturn ; EA76 out of sequence or the game is still busy: drop the packet, the peer will resend it
|
|
inc rxSeqCounter ; EA78 accept it: the next packet should carry the following sequence number
|
|
|
|
; ----------------------------------------------------------------------
|
|
; deliverPacketPayload - Hands an accepted data packet to the game. The command byte is rebuilt as
|
|
; (control byte AND $F0) OR (argument count AND $0F) - the low nibble the sender replaced with the
|
|
; sequence number is restored from the packet length - and it and its arguments are pushed one at a
|
|
; time into the game's 18-byte inbound command ring $E45C.
|
|
; In: packetLength $EAEA, packetBuffer, packetControlByte $EAEC
|
|
; Out: command byte plus arguments pushed into $E45C, $E459 incremented, $EAEA and $EAFE clobbered
|
|
; Called from: dispatchReceivedPacket, by fall-through from $EA78
|
|
; ----------------------------------------------------------------------
|
|
deliverPacketPayload:
|
|
lda packetLength ; EA7B total packet length, i.e. the index of the check byte
|
|
sec ; EA7E minus the length code...
|
|
sbc #$02 ; EA7F ...and the control byte...
|
|
sta packetLength ; EA81 ...leaves the number of argument bytes, which is also the loop counter
|
|
eor packetControlByte ; EA84 classic bit merge: A EOR control byte...
|
|
and #$0F ; EA87 ...masked to the low nibble...
|
|
eor packetControlByte ; EA89 ...EOR the control byte again gives (ctrl AND $F0) OR (argcount AND $0F) = the game's original command byte
|
|
ldx #$01 ; EA8C packet byte 1 is the control-byte slot; the first argument follows at byte 2
|
|
deliverPayloadLoop:
|
|
stx packetIndexSave ; EA8E save the buffer index - the push call clobbers X
|
|
jsr pushArqRxRingThunk ; EA91 push the byte into the game's inbound command ring $E45C
|
|
ldx packetIndexSave ; EA94 restore the index...
|
|
inx ; EA97 ...and step to the next packet byte
|
|
lda packetBuffer,x ; EA98 load the argument byte
|
|
dec packetLength ; EA9B one fewer argument to deliver
|
|
bpl deliverPayloadLoop ; EA9E loop, then fall into the shared RTS below
|
|
|
|
; ----------------------------------------------------------------------
|
|
; packetReturn - A single RTS used as the common exit of the packet layer. The same $60 byte is also
|
|
; read as packetLengthCodeTable entry 0, which can never be selected because a packet always carries
|
|
; at least one payload byte.
|
|
; In: none
|
|
; Out: returns to the caller
|
|
; Called from: branch target from $EA63 and $EA76, fall-through from deliverPacketPayload; read as
|
|
; table entry 0 at $E953 and $E9CD
|
|
; ----------------------------------------------------------------------
|
|
packetReturn:
|
|
rts ; EAA0 shared exit of the packet layer; also table entry 0 of packetLengthCodeTable, which is never matched
|
|
|
|
|
|
; packetLengthCodeTable - one byte per payload length 1..16, indexed as $EAA0,X. Every code is a byte
|
|
; whose four bit pairs are each 01 or 10, so it can never be confused with the inter-packet bytes
|
|
; $00 and $FF and always carries plenty of edges for the receiver's bit sampler. The table must stay
|
|
; sorted ascending: receivePacket searches it with CMP/BCS and gives up as soon as an entry is larger.
|
|
packetLengthCodeTable:
|
|
.byte $55,$56 ; EAA1 UV codes for payload lengths 1 and 2
|
|
L_EAA3:
|
|
.byte $59 ; EAA3 codes for payload lengths 3 to 10
|
|
.byte $5A,$65,$66,$69,$6A,$95,$96; EAA4 Zefij..
|
|
.byte $99 ; EAAB codes for payload lengths 11 to 16, then the $FF sentinel that ends the search
|
|
.byte $9A,$A5,$A6,$A9,$AA,$FF ; EAAC ......
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearPacketState - Zeroes the entire 76-byte packet-protocol state block $EAB7-$EB02 in one call.
|
|
; clearInlineVarBlock pops the return address to build a self-modified STA abs,X over the bytes that
|
|
; follow the JSR, so this routine returns two levels up, to the caller of initCommModule.
|
|
; In: none
|
|
; Out: $EAB7-$EB02 = 0; returns to its caller's caller
|
|
; Called from: clearArqVars ($E2BD), i.e. jump-table entry $E003 with X = 1 right after the module is
|
|
; loaded
|
|
; ----------------------------------------------------------------------
|
|
clearPacketState:
|
|
ldx #$4C ; EAB2 $4C = 76 bytes to clear
|
|
jsr clearInlineVarBlock ; EAB4 zero the 76 bytes that follow this JSR; the helper consumes the return address, so the RTS lands in initCommModule's caller
|
|
|
|
; ----------------------------------------------------------------------
|
|
; packetProtocolState - the whole state of the packet layer, $EAB7-$EB02, cleared in one go by the
|
|
; call above. It is data, not code: the disassembler renders the first byte as BRK but nothing ever
|
|
; executes it. Layout: sixteen scalars ($EAB7-$EAC6), the 35-byte transmit message ring ($EAC7), the
|
|
; packet length and assembly buffer ($EAEA-$EAFD) and five more scalars ($EAFE-$EB02).
|
|
; ----------------------------------------------------------------------
|
|
packetProtocolState:
|
|
brk ; EAB7 syncRoundCounter: 0 = still waiting for the peer's $00, 1-9 = waiting for its $FF, 10 = give up and restart the sync
|
|
zeroRunLength:
|
|
.byte $00 ; EAB8 zeroRunLength: consecutive $00 bytes received; 1 = a resend nak, 2 or more = the link has lost sync
|
|
rxPayloadRemaining:
|
|
.byte $00 ; EAB9 rxPayloadRemaining: payload bytes still to be read for the packet in progress; 0 = no packet in progress
|
|
packetByteCounter:
|
|
.byte $00 ; EABA packetByteCounter: scratch loop counter and index, saved across calls that clobber X
|
|
rxAckControlByte:
|
|
.byte $00 ; EABB rxAckControlByte: control byte of the acknowledgement being processed
|
|
rxDataControlByte:
|
|
.byte $00 ; EABC rxDataControlByte: control byte of the data packet being processed (written at $EA68, never read back)
|
|
rxSeqCounter:
|
|
.byte $00 ; EABD rxSeqCounter: sequence number of the next data packet expected; its low two bits ride out in every packet we send
|
|
txSeqCounter:
|
|
.byte $00 ; EABE txSeqCounter: sequence number given to the next message we build
|
|
resendRequest:
|
|
.byte $00 ; EABF resendRequest: 0 = none, 1 = the peer sent a lone $00, negative = resend the message with this index
|
|
ackPending:
|
|
.byte $00 ; EAC0 ackPending: non-zero when the peer is owed an acknowledgement
|
|
sendFfPending:
|
|
.byte $00 ; EAC1 sendFfPending: request to put a raw $FF on the wire - nothing in the shipped image ever sets it
|
|
sendZeroPending:
|
|
.byte $00 ; EAC2 sendZeroPending: made negative by a checksum failure so that a raw $00 nak goes out next tick
|
|
txRingIndex:
|
|
.byte $00 ; EAC3 txRingIndex: current read/write position in the 35-byte message ring
|
|
txRingTailIndex:
|
|
.byte $00 ; EAC4 txRingTailIndex: ring index just past the message most recently built
|
|
txNextFreeIndex:
|
|
.byte $00 ; EAC5 txNextFreeIndex: where the next message may be written; negative means both outstanding slots are busy
|
|
txOldestUnackedIndex:
|
|
.byte $00 ; EAC6 txOldestUnackedIndex: bit 7 = slot in use, bits 0-6 = ring index of the oldest unacknowledged message
|
|
|
|
; txMessageRing - 35-byte circular buffer of queued outgoing messages. Each entry is [argument
|
|
; count][control byte = (command AND $F0) OR (sequence << 2)][arguments...]; indices wrap at $23 and
|
|
; live in $EAC3-$EAC6. Empty in the shipped image, because the only code that fills it is gated by
|
|
; $E0A6, which is never set.
|
|
txMessageRing:
|
|
.byte $00,$00,$00,$00,$00,$00,$00; EAC7 ....... start of the 35-byte transmit message ring
|
|
L_EACE:
|
|
.byte $00 ; EACE message ring byte 7
|
|
sub_EACF:
|
|
.byte $00 ; EACF message ring bytes 8-15
|
|
.byte $00,$00,$00,$00,$00,$00,$00; EAD0 .......
|
|
.byte $00 ; EAD7 message ring bytes 16-23
|
|
.byte $00,$00,$00,$00,$00,$00,$00; EAD8 .......
|
|
.byte $00 ; EADF message ring bytes 24-31
|
|
.byte $00,$00,$00,$00,$00,$00,$00; EAE0 .......
|
|
.byte $00 ; EAE7 message ring bytes 32-34, the last of the 35 slots
|
|
.byte $00,$00 ; EAE8 ..
|
|
packetLength:
|
|
.byte $00 ; EAEA packetLength: number of packet bytes before the check byte, i.e. the index of the check byte slot
|
|
|
|
; packetBuffer - one packet under assembly or reception: [length code][control byte][payload
|
|
; 0..14][check byte]. A payload is at most 16 bytes, so the buffer occupies $EAEB-$EAFD.
|
|
packetBuffer:
|
|
.byte $00 ; EAEB . packet byte 0 = the length code taken from packetLengthCodeTable
|
|
packetControlByte:
|
|
.byte $00 ; EAEC packet byte 1 = the control byte: command nibble in bits 4-7, sequence in bits 2-3, piggy-backed ack in bits 0-1
|
|
packetPayload:
|
|
.byte $00 ; EAED packet byte 2 = the first payload/argument byte
|
|
L_EAEE:
|
|
.byte $00 ; EAEE packet bytes 3-5
|
|
.byte $00,$00 ; EAEF ..
|
|
L_EAF1:
|
|
.byte $00 ; EAF1 packet bytes 6-13
|
|
.byte $00,$00,$00,$00,$00,$00,$00; EAF2 .......
|
|
.byte $00 ; EAF9 packet bytes 14-18; a full 16-byte payload plus length code and check byte uses up to byte 17
|
|
.byte $00,$00,$00,$00 ; EAFA ....
|
|
packetIndexSave:
|
|
.byte $00 ; EAFE packetIndexSave: buffer index parked across calls that clobber X; also the drive pass-through byte counter
|
|
rxLengthCodeByte:
|
|
.byte $00 ; EAFF rxLengthCodeByte: the length code of the packet being received, kept while waiting for its body
|
|
D_EB00:
|
|
.byte $00 ; EB00 stallCounter: consecutive service ticks with no progress; the NMI zeroes it at $E716 on every character received and sixteen of them force a resync. It is also the untouched base of the self-modified LDA $EBxx,X at $E3A1 that loads the AT strings
|
|
modemDelayCounter:
|
|
.byte $00 ; EB01 modemDelayCounter: guard-time delay, held as a negative tick count taken from the AT strings
|
|
lastModemChar:
|
|
.byte $00 ; EB02 lastModemChar: the previous character the modem sent, used to spot the '0' of 'CONNECT 1200'
|
|
|
|
; ----------------------------------------------------------------------
|
|
; remoteCodeBuffer - Landing area for 6502 code sent by the peer. The message engine at $E196 appends
|
|
; the payload of every received message whose command byte is $70 or above here (self-modified index
|
|
; at $E1AE) and calls JSR $EB03 when such a message arrives with an empty payload. The shipped first
|
|
; byte is $60 (RTS), so the call does nothing until the opponent has actually downloaded something.
|
|
; In: code previously written by $E1B3
|
|
; Out: whatever the downloaded code does
|
|
; Called from: $E1A2 (JSR $EB03); written by $E1B3
|
|
; remoteCodeBuffer: unknown, 211 bytes. $EB03-$EBD5: buffer that receives 6502 code sent by the peer
|
|
; in messages with a command byte >= $70 (copied by $E1B3, executed by JSR $EB03). Its shipped
|
|
; contents are one RTS at $EB03 followed by 210 bytes of leftover build-1 game code (see
|
|
; misclassified).
|
|
; ----------------------------------------------------------------------
|
|
remoteCodeBuffer:
|
|
rts ; EB03 shipped as a bare RTS, so the peer's 'run downloaded code' message is a no-op until code has been uploaded
|
|
|
|
.byte $27 ; EB04 $EB04-$EBD5: dead filler. These 210 bytes are a fragment of the SOLO TRAINER build's code - they read the unit arrays at $F6A4/$F7D0/$F834 and the trainer work RAM at $88xx/$89xx - that the mastering system left in the sector. The modem driver only ever uses this area as a write buffer, so the bytes are never executed
|
|
L_EB05:
|
|
.byte $AD ; EB05 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $D5,$F6,$38,$F9,$A4,$F6,$C9; EB06 ..8....
|
|
.byte $F8 ; EB0D dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $B0,$04,$C9,$08,$B0,$18 ; EB0E ......
|
|
L_EB14:
|
|
.byte $BE ; EB14 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $00,$88,$EC,$64,$89,$F0,$4A; EB15 ...d..J
|
|
.byte $BD ; EB1C dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $3E,$89,$09,$40,$9D,$3E,$89; EB1D >..@.>.
|
|
.byte $30 ; EB24 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $06,$AD,$CB,$91,$99,$CE,$87; EB25 .......
|
|
L_EB2C:
|
|
.byte $C0 ; EB2C dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $63,$B0,$36,$C0,$60,$90,$32; EB2D c.6.`.2
|
|
.byte $B9 ; EB34 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $32,$88,$10,$2D,$B9,$00,$88; EB35 2..-...
|
|
.byte $CD ; EB3C dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $54,$89,$B0,$25,$CD,$64,$89; EB3D T..%.d.
|
|
.byte $F0 ; EB44 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $20,$EE,$53,$89,$AD,$53,$89; EB45 .S..S.
|
|
.byte $AA ; EB4C dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $99,$00,$88,$AD,$CB,$91,$99; EB4D .......
|
|
.byte $CE ; EB54 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $87,$B9,$D0,$F7,$29,$3F,$9D; EB55 ....)?.
|
|
.byte $D2 ; EB5C dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $88,$B9,$34,$F8,$29,$3F,$9D; EB5D ..4.)?.
|
|
.byte $DE ; EB64 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $88 ; EB65 .
|
|
L_EB66:
|
|
.byte $60 ; EB66 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
sub_EB67:
|
|
.byte $A9 ; EB67 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $03,$38,$ED,$68,$89,$90,$74; EB68 .8.h..t
|
|
.byte $8D ; EB6F dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $67,$89,$AA,$AD,$71,$89,$F0; EB70 g...q..
|
|
.byte $1E ; EB77 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $E0,$02,$90,$67,$CA,$CA,$8E; EB78 ...g...
|
|
.byte $67 ; EB7F dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $89,$20,$E4,$EB,$20,$5D,$FD; EB80 . .. ].
|
|
.byte $29 ; EB87 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $07,$D0,$58,$A9,$9E,$20,$DD; EB88 ..X.. .
|
|
.byte $EB ; EB8F dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $EE,$67,$89,$4C,$9F,$EB ; EB90 .g.L..
|
|
L_EB96:
|
|
.byte $8A ; EB96 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $F0,$4A,$20,$E4,$EB ; EB97 .J ..
|
|
L_EB9C:
|
|
.byte $20 ; EB9C dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $A5,$EB ; EB9D ..
|
|
L_EB9F:
|
|
.byte $CE ; EB9F dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $67,$89,$D0,$F8,$60 ; EBA0 g...`
|
|
sub_EBA5:
|
|
.byte $20 ; EBA5 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $5D,$FD,$29,$01,$F0,$37,$AD; EBA6 ].)..7.
|
|
.byte $27 ; EBAD dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $FA,$29,$3F,$C9,$2D,$B0,$2E; EBAE .)?.-..
|
|
.byte $85 ; EBB5 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $18,$A9,$32,$38,$E5,$18,$4A; EBB6 ..28..J
|
|
.byte $4A ; EBBD dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $4A,$18,$69,$01,$8D,$98,$88; EBBE J.i....
|
|
.byte $18 ; EBC5 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $6D,$65,$89,$8D,$65,$89,$29; EBC6 me..e.)
|
|
.byte $03 ; EBCD dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
.byte $D0,$06,$EE,$98,$88,$EE,$65; EBCE ......e
|
|
.byte $89 ; EBD5 dead filler inside the remote-code buffer: solo-trainer code left in the sector by the mastering system, never executed here
|
|
|
|
; The three canned Hayes command strings, stored BACK TO FRONT: queueModemCommandString ($E393) copies
|
|
; them forwards into the host-output ring at $E092 and sets the read index to the string length, and
|
|
; popHostOutRing walks that index downwards, so they leave the ring in reverse order. A byte with bit
|
|
; 7 set is never transmitted: runModemStateMachine parks it in modemDelayCounter
|
|
; $EB01 as a negative tick count, which is how the Hayes escape guard times are produced.
|
|
modemHangUpString:
|
|
.byte $0D,"0HTA",'@'|$80,$0D ; EBD6 modemHangUpString, emitted from $EBE1 downwards: delay $A0 (96 ticks), '+','+','+', delay $A0, CR, delay $C0 (64 ticks), 'A','T','H','0', CR - the Hayes escape sequence with its guard times followed by the hang-up command
|
|
sub_EBDD:
|
|
.byte $A0 ; EBDD the two guard-time delay bytes and the '+++' escape; the $00 at $EBE2 terminates the copy
|
|
.byte $2B,$2B,$2B,$A0,$00 ; EBDE +++..
|
|
|
|
; modemAnswerInitString: text, 13 bytes. Reversed modem init string for answer mode: 'ATQ0V1X1A' + CR
|
|
; preceded by a 64-tick guard delay. Selected when the player presses 'A' (command byte $C1 at $E37A).
|
|
modemAnswerInitString:
|
|
.byte $0D ; EBE3 modemAnswerInitString, emitted backwards as delay $C0, 'A','T','Q','0','V','1','X','1','A', CR - quiet off, verbose on, extended result codes, answer. Selected by the C=+A hot key ($E37A)
|
|
sub_EBE4:
|
|
.byte $41 ; EBE4 the string body, stored in reverse order
|
|
.byte $31,$58,$31,$56,$30,$51,$54; EBE5 1X1V0QT
|
|
.byte $41 ; EBEC the last three bytes of the reversed answer string: 'A', the $C0 guard delay and the leading CR
|
|
.byte $C0,$0D ; EBED ..
|
|
sub_EBEF:
|
|
.byte $00 ; EBEF the $00 terminator
|
|
|
|
; modemDialInitString: text, 13 bytes. Reversed modem init string for originate mode: 'ATQ0V1X1D' + CR
|
|
; preceded by a 64-tick guard delay ('D' = dial). Selected when the player presses 'O' (command byte
|
|
; $CF at $E37E).
|
|
modemDialInitString:
|
|
.byte $0D,"D1X1V0QTA",'@'|$80 ; EBF0 modemDialInitString, emitted backwards as delay $C0, 'A','T','Q','0','V','1','X','1','D', CR - the same setup but 'D' = dial. Selected by the C=+O hot key ($E37E)
|
|
L_EBFB:
|
|
.byte $0D ; EBFB leading CR and the $00 terminator
|
|
.byte $00 ; EBFC .
|
|
|
|
; hangUpStringPtrLo: byteTable, 1 bytes. Low byte ($D6) of modemHangUpString, read by sub_E38D and
|
|
; passed to the string-to-queue routine sub_E393 (the high byte $EB is hard coded in the self-modified
|
|
; LDA at $E3A1).
|
|
hangUpStringPtrLo:
|
|
.byte $D6 ; EBFD . hangUpStringPtrLo: low byte of modemHangUpString, read by hangUpModem ($E390); the high byte $EB is the un-patched half of the self-modified LDA at $E3A1
|
|
|
|
; modemInitStringPtrLoTable: byteTable, 2 bytes. Low bytes $E3 and $F0 of the answer and dial init
|
|
; strings, indexed by modemModeIndex $E011 at $E2E0.
|
|
modemInitStringPtrLoTable:
|
|
.byte $E3,$F0 ; EBFE .. modemInitStringPtrLoTable: low bytes of the answer ($EBE3) and dial ($EBF0) strings, indexed by isOriginateMode $E011 at $E2E0
|
|
|
|
; ----------------------------------------------------------------------
|
|
; clearBattlefieldMap - Fills all 1600 cells of the 40x40 battlefield map $F000-$F63F with $40, the
|
|
; empty-terrain code, using the shared pointer-advance helper advanceFillPointer ($C3F9).
|
|
; In: none
|
|
; Out: $F000-$F63F = $40; zp_48/zp_49 left at $F640, zp_4A/zp_4B = $F63F, the zp_4E/zp_4F counter
|
|
; advanced by 1600
|
|
; Called from: overlay A: generateBattlefieldMap ($6F30) and the map editor ($76F2)
|
|
; ==========================================================================
|
|
; $EC00-$EFFF is the 'comm tail', loaded separately from track 34 sectors 12-15 by
|
|
; loadCommTailBuild2 ($1070). Despite living inside the modem driver it is not comm code at all: it
|
|
; holds battlefield-map and unit helpers, the start-of-session handshake, the film-snapshot loader and
|
|
; a block of message text, all called by overlay A. The solo trainer has its own, completely
|
|
; different, tail at the same addresses.
|
|
; ==========================================================================
|
|
; ----------------------------------------------------------------------
|
|
clearBattlefieldMap:
|
|
lda #$00 ; EC00 low byte of the first map cell...
|
|
sta mapCellPtr ; EC02 ...into the fill pointer zp_48/zp_49
|
|
lda #$F0 ; EC04 map base page
|
|
sta mapCellPtrHi ; EC06 fill pointer = $F000 = map cell (0,0)
|
|
lda #$3F ; EC08 low byte of the last map cell...
|
|
D_EC0A:
|
|
sta fillEndPtr ; EC0A ...into the fill limit zp_4A/zp_4B
|
|
lda #$F6 ; EC0C limit page
|
|
sta fillEndPtrHi ; EC0E limit = $F63F = map cell (39,39); 40*40 = 1600 cells
|
|
ldy #$00 ; EC10 always index 0 - the pointer itself walks
|
|
L_EC12:
|
|
lda #$40 ; EC12 $40 = the empty terrain code (the map alphabet runs $40-$58)
|
|
L_EC14:
|
|
sta (mapCellPtr),y ; EC14 clear this cell
|
|
jsr advanceFillPointer ; EC16 C = 1 once the pointer has reached the limit, then step it on one cell
|
|
bcc L_EC12 ; EC19 loop over all 1600 cells
|
|
rts ; EC1B the map is blank
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; setViewOriginToHome - Puts the 7x5 tactical view back at the middle of the player's own end of the
|
|
; battlefield: origin column 17, row 28, so the cursor cell (origin+3, origin+2) lands on map (20,30).
|
|
; Both the live zero-page pair and the saved copy at $92C3/$92C4 are set.
|
|
; In: none
|
|
; Out: zp_A5 = $11, zp_A6 = $1C, $92C3 = $11, $92C4 = $1C
|
|
; Called from: overlay A at $7AEC (battle start), $7D51 (film playback) and $83EF (recycler placement)
|
|
; ----------------------------------------------------------------------
|
|
setViewOriginToHome:
|
|
lda #$11 ; EC1C column 17: the view is 7 cells wide, so the cursor sits on column 20, the middle of the 40-cell map
|
|
sta viewOriginCol ; EC1E live view origin column
|
|
sta savedViewOriginCol ; EC20 and the copy that survives a screen change
|
|
lda #$1C ; EC23 row 28: the view is 5 cells tall, so the cursor sits on row 30, well inside the player's own half
|
|
sta viewOriginRow ; EC25 live view origin row
|
|
sta savedViewOriginRow ; EC27 and its saved copy
|
|
rts ; EC2A done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadMapSeedIntoScenarioRng - Copies the 24-bit map seed out of the game settings block into the
|
|
; scenario random-number state, so that both machines run the procedural map generator from the
|
|
; identical seed and get the identical battlefield. That seed written in base 32 is exactly the map
|
|
; name the two players compare before a game.
|
|
; In: mapSeed $0B95/$0B96/$0B97
|
|
; Out: zp_5A/zp_5B/zp_5C = the seed; A, Y and X left holding the three bytes
|
|
; Called from: overlay A: generateBattlefieldMap ($6F30-$6F33) and $7BB2
|
|
; ----------------------------------------------------------------------
|
|
loadMapSeedIntoScenarioRng:
|
|
lda mapSeed ; EC2B seed byte 0 from the settings block
|
|
sta scenarioSeed0 ; EC2E into the scenario RNG state
|
|
ldy mapSeed1 ; EC30 seed byte 1
|
|
sty scenarioSeed1 ; EC33 into the RNG state
|
|
ldx mapSeed2 ; EC35 seed byte 2
|
|
stx scenarioSeed2 ; EC38 into the RNG state - both players now generate the same map
|
|
rts ; EC3A done
|
|
|
|
|
|
; mirroredTerrainCodeTable - 25 entries indexed by terrainCode-$40, giving the code that must replace
|
|
; it when the battlefield is turned 180 degrees. $40 (empty) maps to itself; the direction-dependent
|
|
; codes pair up A<->D, B<->C, E<->H, F<->G, I<->K, J<->L, M<->T, N<->Q, O<->S, P<->R, U<->X and V<->W.
|
|
; mirrorTerrainCode also uses the table with a -$20 shift for the second code range $20-$3F.
|
|
mirroredTerrainCodeTable:
|
|
.byte $40 ; EC3B @ index 0 = code $40 (empty terrain), which maps to itself
|
|
L_EC3C:
|
|
.byte $44 ; EC3C codes $41-$48 ('A'-'H') mirror to 'D','C','B','A','H','G','F','E'
|
|
.byte $43,$42,$41,$48,$47,$46,$45; EC3D CBAHGFE
|
|
.byte $4B ; EC44 codes $49-$4A ('I','J') mirror to 'K','L'
|
|
.byte $4C ; EC45 L
|
|
L_EC46:
|
|
.byte $49 ; EC46 codes $4B-$52 ('K'-'R') mirror to 'I','J','T','Q','S','R','N','P'
|
|
.byte $4A,$54,$51,$53,$52,$4E,$50; EC47 JTQSRNP
|
|
.byte $4F ; EC4E codes $53-$58 ('S'-'X') mirror to 'O','M','X','W','V','U'
|
|
.byte $4D,$58,$57,$56,$55 ; EC4F MXWVU
|
|
|
|
; ----------------------------------------------------------------------
|
|
; makeMapPointSymmetric - Entered with mirrorMode zp_18 = $FF: walks a forward pointer up from $F000
|
|
; and a backward pointer down from $F63F and writes mirrorTerrainCode(map[i]) into map[1599-i] without
|
|
; copying anything back the other way. The net effect is that the second half of the map becomes the
|
|
; point-mirrored image of the first half, which is the map generator's fairness step.
|
|
; In: the battlefield map $F000-$F63F
|
|
; Out: the map made point symmetric; zp_18 = $FF, zp_48/zp_49, zp_4A/zp_4B and zp_4E/zp_4F clobbered
|
|
; Called from: overlay A at $6F77 (generateBattlefieldMap)
|
|
; ----------------------------------------------------------------------
|
|
makeMapPointSymmetric:
|
|
lda #$FF ; EC54 $FF = mirror one way only, do not copy the far half back
|
|
bne storeMirrorMode ; EC56 always taken
|
|
|
|
; ----------------------------------------------------------------------
|
|
; rotateMapAndMirrorCodes - Entered with mirrorMode zp_18 = 0: exchanges map[i] and map[1599-i] over
|
|
; the whole map, passing both bytes through mirrorTerrainCode, i.e. rotates the battlefield 180
|
|
; degrees and flips the direction-dependent terrain codes with it. Together with
|
|
; mirrorAllUnitCoordinates this is what lets both players always see their own side at the bottom of
|
|
; the screen.
|
|
; In: the battlefield map $F000-$F63F
|
|
; Out: the map rotated in place; zp_18 = 0, zp_48/zp_49, zp_4A/zp_4B and zp_4E/zp_4F clobbered
|
|
; Called from: overlay A at $76B5, $7749, $7AE1 and $7D2E
|
|
; ----------------------------------------------------------------------
|
|
rotateMapAndMirrorCodes:
|
|
lda #$00 ; EC58 0 = swap in both directions, a true 180 degree rotation
|
|
storeMirrorMode:
|
|
sta scratch18 ; EC5A remember which of the two modes we are in
|
|
lda #$00 ; EC5C low byte of the first map cell
|
|
L_EC5E:
|
|
sta mapCellPtr ; EC5E forward pointer zp_48/zp_49 low = $00
|
|
clc ; EC60 clear carry for the address arithmetic below
|
|
adc #$3F ; EC61 $00 + $3F...
|
|
sta fillEndPtr ; EC63 ...backward pointer zp_4A/zp_4B low = $3F
|
|
lda #$F0 ; EC65 map base page
|
|
L_EC67:
|
|
sta mapCellPtrHi ; EC67 forward pointer = $F000 = map cell (0,0)
|
|
adc #$06 ; EC69 $F0 + $06...
|
|
sta fillEndPtrHi ; EC6B ...backward pointer = $F63F = map cell (39,39)
|
|
ldy #$00 ; EC6D always index 0 - both pointers walk
|
|
|
|
; ----------------------------------------------------------------------
|
|
; Loop over the 800 cell pairs: the forward pointer climbs from $F000 and the backward pointer falls
|
|
; from $F63F until the two meet in the middle. advanceFillPointer compares them and returns C = 1
|
|
; once they have.
|
|
; ----------------------------------------------------------------------
|
|
mirrorCellPairLoop:
|
|
lda (fillEndPtr),y ; EC6F the cell at the far end of the map
|
|
pha ; EC71 keep it on the stack
|
|
L_EC72:
|
|
lda (mapCellPtr),y ; EC72 the cell at the near end
|
|
L_EC74:
|
|
jsr mirrorTerrainCode ; EC74 convert it to its 180-degree equivalent
|
|
sta (fillEndPtr),y ; EC77 and write it into the far cell
|
|
pla ; EC79 recover the far cell's original byte
|
|
ldx scratch18 ; EC7A which mirror mode?
|
|
bmi stepBackwardPointer ; EC7C $FF = symmetry pass, so do not write anything back into the near cell
|
|
jsr mirrorTerrainCode ; EC7E rotation pass: mirror the far cell too...
|
|
sta (mapCellPtr),y ; EC81 ...and store it in the near cell, completing the swap
|
|
stepBackwardPointer:
|
|
lda fillEndPtr ; EC83 step the backward pointer down one cell - low byte first
|
|
bne decBackwardPointer ; EC85 no page underflow
|
|
dec fillEndPtrHi ; EC87 borrow into the high byte
|
|
decBackwardPointer:
|
|
dec fillEndPtr ; EC89 decrement the low byte
|
|
jsr advanceFillPointer ; EC8B step the forward pointer up and set C when the two pointers have met
|
|
bcc mirrorCellPairLoop ; EC8E loop over all 800 pairs
|
|
rts ; EC90 the map is now rotated (or symmetric)
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorAllUnitCoordinates - Mirrors the six 100-entry unit coordinate arrays that follow the map, so
|
|
; that unit positions match a battlefield rotateMapAndMirrorCodes has just turned 180 degrees. The
|
|
; arrays are $64 apart from $F640: unitCol $F640, unitRow $F6A4, unitDestCol $F7D0, unitDestRow $F834,
|
|
; unitPathCol $F898 and unitPathRow $F8FC. The flags array $F708 and the type array $F76C hold no
|
|
; coordinates and are skipped.
|
|
; In: the six coordinate arrays
|
|
; Out: every entry replaced by 39 minus its coordinate with the flag bits preserved; zp_48/zp_49
|
|
; clobbered
|
|
; Called from: overlay A at $7AE9, only when the local player is side 1
|
|
; ----------------------------------------------------------------------
|
|
mirrorAllUnitCoordinates:
|
|
lda #$40 ; EC91 unitColTable low byte...
|
|
L_EC93:
|
|
ldy #$F6 ; EC93 ...page: array 0 at $F640, the current column of each of the 100 units
|
|
jsr mirrorCoordinateArray ; EC95 mirror it
|
|
lda #$A4 ; EC98 unitRowTable low byte...
|
|
L_EC9A:
|
|
ldy #$F6 ; EC9A ...page: array 1 at $F6A4, $64 further on, the current row
|
|
L_EC9C:
|
|
jsr mirrorCoordinateArray ; EC9C mirror it
|
|
lda #$D0 ; EC9F unitDestColTable low byte...
|
|
ldy #$F7 ; ECA1 ...page: array 4 at $F7D0, the ordered destination column
|
|
L_ECA3:
|
|
jsr mirrorCoordinateArray ; ECA3 mirror it
|
|
L_ECA6:
|
|
lda #$34 ; ECA6 unitDestRowTable low byte...
|
|
ldy #$F8 ; ECA8 ...page: array 5 at $F834, the destination row
|
|
jsr mirrorCoordinateArray ; ECAA mirror it
|
|
lda #$98 ; ECAD unitPathColTable low byte...
|
|
ldy #$F8 ; ECAF ...page: array 6 at $F898, the way-point column
|
|
jsr mirrorCoordinateArray ; ECB1 mirror it
|
|
lda #$FC ; ECB4 unitPathRowTable low byte...
|
|
ldy #$F8 ; ECB6 ...page: array 7 at $F8FC, the way-point row; fall straight into the mirror routine
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorCoordinateArray - Replaces the coordinate in each of the 100 entries of the array at A/Y with
|
|
; 39 minus itself, leaving the two top flag bits of every entry untouched (they carry per-unit
|
|
; markers).
|
|
; In: A = array low byte, Y = array high byte; mirrorMapCoordinate $3FA3
|
|
; Out: all 100 entries mirrored; zp_48/zp_49 = the array pointer; the ORA immediate operand at $ECCD
|
|
; is self-modified
|
|
; Called from: mirrorAllUnitCoordinates ($EC95, $EC9C, $ECA3, $ECAA, $ECB1 and by fall-through from
|
|
; $ECB6)
|
|
; ----------------------------------------------------------------------
|
|
mirrorCoordinateArray:
|
|
sta mapCellPtr ; ECB8 array pointer low
|
|
sty mapCellPtrHi ; ECBA array pointer high
|
|
ldy #$63 ; ECBC $63 = 99 = the last of the 100 unit slots
|
|
mirrorEntryLoop:
|
|
lda (mapCellPtr),y ; ECBE the entry for this unit
|
|
tax ; ECC0 keep a copy
|
|
and #$C0 ; ECC1 isolate the two flag bits (bits 6-7)
|
|
sta restoreCoordFlagBits+1 ; ECC3 patch them into the ORA immediate operand at $ECCD - this routine's only state is self-modifying code
|
|
txa ; ECC6 back to the whole byte
|
|
and #$3F ; ECC7 the coordinate lives in bits 0-5 (0..39 needs six bits)
|
|
jsr mirrorMapCoordinate ; ECC9 39 minus the coordinate
|
|
restoreCoordFlagBits:
|
|
ora #$FF ; ECCC operand patched at $ECC3: put the flag bits back
|
|
sta (mapCellPtr),y ; ECCE store the mirrored entry
|
|
dey ; ECD0 previous unit
|
|
bpl mirrorEntryLoop ; ECD1 loop over units 99 down to 0
|
|
rts ; ECD3 the array is mirrored
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; mirrorTerrainCode - Maps one map cell byte to its 180-degree-rotated equivalent. A cell with bit 7
|
|
; set holds a unit index rather than terrain and is returned unchanged; codes $40-$58 are looked up
|
|
; directly in mirroredTerrainCodeTable, codes $20-$3F go through the same table with a -$20 shift, and
|
|
; anything else is passed through untouched.
|
|
; In: A = map cell byte
|
|
; Out: A = the mirrored cell byte, X clobbered
|
|
; Called from: rotateMapAndMirrorCodes / makeMapPointSymmetric ($EC74 and $EC7E)
|
|
; ----------------------------------------------------------------------
|
|
mirrorTerrainCode:
|
|
tax ; ECD4 keep the original byte in X
|
|
bmi mirrorTerrainReturn ; ECD5 bit 7 set: a unit stands here and the terrain under it is kept in the per-unit array - leave the cell alone
|
|
sec ; ECD7 no borrow
|
|
sbc #$40 ; ECD8 convert to a table index by removing the $40 base of the terrain alphabet
|
|
bcs checkTerrainCodeRange ; ECDA code was $40 or above - the normal range
|
|
adc #$20 ; ECDC code was below $40: C is clear, so this adds $20-1 and maps the $20-$3F range onto indices $00-$1F
|
|
lookupLowRangeCode:
|
|
tax ; ECDE index the table
|
|
lda mirroredTerrainCodeTable,x; ECDF look up the mirrored code
|
|
sec ; ECE2 no borrow
|
|
L_ECE3:
|
|
sbc #$20 ; ECE3 shift the answer back down into the $20-$3F range
|
|
bne mirrorTerrainReturn ; ECE5 effectively unconditional: the table only holds $40-$58, so the result can never be zero
|
|
checkTerrainCodeRange:
|
|
cmp #$19 ; ECE7 $19 = 25 = the number of table entries
|
|
L_ECE9:
|
|
bcc lookupTerrainCode ; ECE9 index 0-24 is a valid terrain code
|
|
txa ; ECEB out of range - hand back the original byte
|
|
bne mirrorTerrainReturn ; ECEC taken unless the cell byte was $00, which the map never contains
|
|
lookupTerrainCode:
|
|
tax ; ECEE index the table
|
|
lda mirroredTerrainCodeTable,x; ECEF look up the mirrored code
|
|
mirrorTerrainReturn:
|
|
rts ; ECF2 return the (possibly unchanged) cell byte
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadUnitStartTemplate - Installs the unit start positions for the selected game type. Track 18
|
|
; sectors 8-14 are loaded to $F000 and hold four 400-byte templates rather than a battlefield;
|
|
; unitLayoutByGameType ($78B8) says which one this game type uses, and it is copied to $F640 where the
|
|
; live unit arrays begin. Bit 7 of $F640 marks 'the template data is still present', so a second call
|
|
; does nothing. It copies two whole pages (512 bytes) although a template is only 400 bytes, so the
|
|
; 112 bytes after the type array - the start of unitDestCol $F7D0 - are filled with the beginning of
|
|
; the next template. Harmless, because destinations are initialised later.
|
|
; In: $F640 bit 7, gameTypeOptions $0BA3 bits 0-2, unitLayoutByGameType $78B8, template data at $F000
|
|
; Out: $F640-$F83F overwritten with the template (unitCol, unitRow, flags, type); the source and
|
|
; destination operands at $ED31/$ED32 and $ED34/$ED35 are self-modified
|
|
; Called from: overlay A at $79E6 and $82FD, before the map is generated
|
|
; ----------------------------------------------------------------------
|
|
loadUnitStartTemplate:
|
|
lda unitColTable ; ECF3 first byte of the live unit column array; while the templates are still resident it has bit 7 set
|
|
bpl templateAlreadyLoaded ; ECF6 the template has already been consumed - nothing to do
|
|
lda gameTypeOptions ; ECF8 the game options byte
|
|
and #$07 ; ECFB bits 0-2 = game type 0-7 (SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER)
|
|
L_ECFD:
|
|
tay ; ECFD index the layout table with it
|
|
lda #$00 ; ECFE template area low byte...
|
|
sta templateCopyLoad+1 ; ED00 ...patched into the LDA operand at $ED31
|
|
lda #$F0 ; ED03 template area page...
|
|
sta templateCopyLoad+2 ; ED05 ...patched into the LDA operand at $ED32, so the copy reads from $F000
|
|
lda #$40 ; ED08 unit array low byte...
|
|
L_ED0A:
|
|
sta templateCopyStore+1 ; ED0A ...patched into the STA operand at $ED34
|
|
lda #$F6 ; ED0D unit array page...
|
|
sta templateCopyStore+2 ; ED0F ...patched into the STA operand at $ED35, so the copy writes to $F640
|
|
L_ED12:
|
|
lda unitLayoutByGameType,y ; ED12 which of the four templates does this game type use?
|
|
beq copyTemplatePages ; ED15 template 0 - the source already points at it
|
|
tax ; ED17 use the template number as a repeat count
|
|
advanceTemplateSource:
|
|
lda templateCopyLoad+1 ; ED18 advance the patched source address...
|
|
clc ; ED1B ...clear carry...
|
|
adc #$90 ; ED1C ...by $190 = 400 bytes, the size of one template...
|
|
sta templateCopyLoad+1 ; ED1E ...low byte...
|
|
lda templateCopyLoad+2 ; ED21 ...and...
|
|
adc #$01 ; ED24 ...the...
|
|
sta templateCopyLoad+2 ; ED26 ...high byte
|
|
dex ; ED29 one template further
|
|
bne advanceTemplateSource ; ED2A repeat until the source points at the chosen template
|
|
copyTemplatePages:
|
|
ldy #$00 ; ED2C start at offset 0 of the page
|
|
ldx #$02 ; ED2E two pages = 512 bytes
|
|
templateCopyLoad:
|
|
lda irqVectorHi,y ; ED30 operand patched at $ED00/$ED05 - reads the template ($FFFF is just what the shipped bytes happen to say)
|
|
templateCopyStore:
|
|
sta irqVectorHi,y ; ED33 operand patched at $ED0A/$ED0F - writes the live unit arrays
|
|
iny ; ED36 next byte
|
|
bne templateCopyLoad ; ED37 256 bytes per page
|
|
inc templateCopyLoad+2 ; ED39 next source page
|
|
inc templateCopyStore+2 ; ED3C next destination page
|
|
dex ; ED3F one page done
|
|
bne templateCopyLoad ; ED40 copy the second page
|
|
templateAlreadyLoaded:
|
|
rts ; ED42 return
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; exchangePlayerNamesAndSettings - The start-of-session handshake. In a solo game it simply copies
|
|
; 'TRAINER ' into the opponent name and returns. In a modem game it waits for the link, swaps the
|
|
; ten-character owner name (read from the disk BAM into $FFDE) with the peer in two five-character
|
|
; messages, substitutes 'ENEMY' when the peer's name is blank, and then swaps a four-byte settings
|
|
; message carrying the comm build id, the game version byte and the program checksum. The EOR of the
|
|
; peer's last two bytes is left in $90F8, which the caller uses as a 'same build?' test. Any length
|
|
; or magic mismatch drops the player back to the main menu.
|
|
; In: isSoloTrainer $0BA5 bit 7, ownPlayerName $FFDE, commBuildId $E030, gameVersionByte $0B94,
|
|
; programChecksum $0B7E, the comm API $E000 and the message buffers $E01D-$E02A
|
|
; Out: opponentPlayerName $FFE8-$FFF1, the peer's build id in $E02F, the peer's version signature in
|
|
; $90F8; link messages sent and consumed
|
|
; Called from: overlay A at $79D1, which pre-loads $90F8 with $DB and shows 'OPPONENT USING
|
|
; NON-STANDARD GAME.' ($895E) if it comes back different
|
|
; ----------------------------------------------------------------------
|
|
exchangePlayerNamesAndSettings:
|
|
lda isSoloTrainer ; ED43 bit 7 set = practice with the solo trainer, so there is no link to talk to
|
|
bpl handshakeWithPeer ; ED46 modem game - do the real handshake
|
|
ldy #$09 ; ED48 the opponent name is ten characters
|
|
copyTrainerNameLoop:
|
|
lda msgTrainerName,y ; ED4A 'TRAINER ' at $884E
|
|
sta opponentPlayerName,y ; ED4D into the opponent name at $FFE8
|
|
dey ; ED50 previous character
|
|
bpl copyTrainerNameLoop ; ED51 copy all ten
|
|
rts ; ED53 done - a solo game has no peer
|
|
|
|
handshakeWithPeer:
|
|
jsr waitForLinkMessage ; ED54 wait until the comm module reports a completed exchange
|
|
ldx #$02 ; ED57 X = 2 = release the received message
|
|
L_ED59:
|
|
jsr commJumpTable ; ED59 call jump-table entry $E000 to free the receive buffer
|
|
ldx #$00 ; ED5C X = 0 = name characters 0-4
|
|
jsr exchangeNameHalf ; ED5E swap the first half of the name
|
|
L_ED61:
|
|
ldx #$05 ; ED61 X = 5...
|
|
L_ED63:
|
|
jsr exchangeNameHalf ; ED63 ...swap name characters 5-9
|
|
lda opponentPlayerName ; ED66 first character of the name the peer sent
|
|
cmp #$A0 ; ED69 $A0 = shifted space: the peer's disk carries no owner name
|
|
L_ED6B:
|
|
bne sendSettingsMessage ; ED6B the peer has a name - keep it
|
|
ldy #$04 ; ED6D 'ENEMY' is five characters
|
|
copyEnemyNameLoop:
|
|
lda inGameMenuText,y ; ED6F 'ENEMY' at $CC99
|
|
sta opponentPlayerName,y ; ED72 overwrite the blank name
|
|
dey ; ED75 previous character
|
|
bpl copyEnemyNameLoop ; ED76 copy all five
|
|
sendSettingsMessage:
|
|
lda #$FF ; ED78 $FF...
|
|
sta txPacketBuffer ; ED7A ...is the magic first byte of every handshake message
|
|
lda commBuildId ; ED7D commBuildId ($E030): 0 for the modem build, bit 7 set for the trainer build
|
|
sta txPacketArg1 ; ED80 message byte 1
|
|
lda gameVersionByte ; ED83 the game version byte
|
|
L_ED86:
|
|
sta txPacketArg2 ; ED86 message byte 2
|
|
lda programChecksum ; ED89 the program checksum computed at boot
|
|
sta txPacketArg3 ; ED8C message byte 3
|
|
lda #$04 ; ED8F four bytes...
|
|
sta txPacketLength ; ED91 ...is the outgoing message length
|
|
ldx #$01 ; ED94 X = 1 = transmit the outgoing message
|
|
jsr commJumpTable ; ED96 send it
|
|
jsr waitForLinkMessage ; ED99 wait for the peer's reply
|
|
lda rxPacketLength ; ED9C length of the message that came back
|
|
cmp #$04 ; ED9F it must also be four bytes
|
|
bne abortLinkAndReturnToMenu; EDA1 protocol error - bail out to the main menu
|
|
lda rxPacketBuffer ; EDA3 first byte of the reply
|
|
L_EDA6:
|
|
cmp #$FF ; EDA6 it must be the $FF magic
|
|
bne abortLinkAndReturnToMenu; EDA8 protocol error - bail out
|
|
lda rxPacketArg1 ; EDAA the peer's comm build id
|
|
sta peerCommBuildId ; EDAD remember it in $E02F
|
|
lda rxPacketArg3 ; EDB0 the peer's program checksum...
|
|
eor rxPacketArg2 ; EDB3 ...EOR its version byte...
|
|
sta selectedUnit ; EDB6 ...parked in $90F8, which the caller pre-loaded with $DB and compares afterwards to warn about a different build
|
|
ldx #$02 ; EDB9 X = 2 = release the received message
|
|
jsr commJumpTable ; EDBB free the receive buffer, then fall into sendEmptyMessage
|
|
|
|
; ----------------------------------------------------------------------
|
|
; sendEmptyMessage - Sets the outgoing message length to zero and asks the comm module to transmit it,
|
|
; which closes the handshake and, on the error path, unblocks the peer.
|
|
; In: none
|
|
; Out: $E01F = 0 and jump-table entry $E000 called with X = 1
|
|
; Called from: the tail of exchangePlayerNamesAndSettings (fall-through from $EDBB) and
|
|
; abortLinkAndReturnToMenu ($EE09)
|
|
; ----------------------------------------------------------------------
|
|
sendEmptyMessage:
|
|
lda #$00 ; EDBE zero length...
|
|
sta txPacketLength ; EDC0 ...outgoing message
|
|
ldx #$01 ; EDC3 X = 1 = transmit
|
|
jsr commJumpTable ; EDC5 send the empty message
|
|
L_EDC8:
|
|
rts ; EDC8 the handshake is finished
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; exchangeNameHalf - Sends one half of the local player's name - a six-byte message of $FF followed by
|
|
; ownPlayerName[X..X+4] - waits for the peer's reply and checks that it is six bytes long and starts
|
|
; with the same $FF magic. X is carried across the blocking wait in a self-modified LDX immediate.
|
|
; Any mismatch falls into abortLinkAndReturnToMenu.
|
|
; In: X = 0 or 5, the offset into the ten-byte name; ownPlayerName $FFDE
|
|
; Out: opponentPlayerName $FFE8+X..+4 filled in; the LDX immediate operand at $EDEB is self-modified
|
|
; Called from: exchangePlayerNamesAndSettings ($ED5E with X = 0 and $ED63 with X = 5)
|
|
; ----------------------------------------------------------------------
|
|
exchangeNameHalf:
|
|
stx restoreNameOffset+1 ; EDC9 park the name offset in the LDX immediate at $EDEB so it survives the blocking wait below
|
|
lda #$FF ; EDCC $FF...
|
|
sta txPacketBuffer ; EDCE ...the handshake magic goes in message byte 0
|
|
ldy #$01 ; EDD1 message bytes 1-5 hold the name characters
|
|
copyOwnNameHalfLoop:
|
|
lda ownPlayerName,x ; EDD3 character X of our own name
|
|
sta txPacketBuffer,y ; EDD6 into the outgoing message
|
|
inx ; EDD9 next name character
|
|
iny ; EDDA next message byte
|
|
cpy #$06 ; EDDB six bytes in all
|
|
bcc copyOwnNameHalfLoop ; EDDD copy five characters
|
|
sty txPacketLength ; EDDF Y = 6 = the outgoing message length
|
|
ldx #$01 ; EDE2 X = 1 = transmit
|
|
jsr commJumpTable ; EDE4 send our half of the name
|
|
jsr waitForLinkMessage ; EDE7 block until the peer's half arrives
|
|
restoreNameOffset:
|
|
ldx #$FF ; EDEA operand patched at $EDC9: recover the name offset
|
|
lda rxPacketLength ; EDEC length of the message that came back
|
|
cmp #$06 ; EDEF it must be six bytes
|
|
bne abortLinkAndReturnToMenu; EDF1 protocol error - bail out to the main menu
|
|
ldy #$01 ; EDF3 the payload starts at message byte 1
|
|
lda rxPacketBuffer ; EDF5 message byte 0
|
|
cmp #$FF ; EDF8 it must be the $FF magic
|
|
beq storeOpponentNameHalf ; EDFA good - copy the peer's five characters
|
|
|
|
; ----------------------------------------------------------------------
|
|
; abortLinkAndReturnToMenu - The handshake failure path: show message/sound 3, wait a second, let the
|
|
; engine finish the exchange it is in the middle of, release the peer with an empty message and jump
|
|
; back to the main menu. It never returns to its caller.
|
|
; In: none
|
|
; Out: the game is back at the main menu; this routine does not return
|
|
; Called from: exchangeNameHalf ($EDF1 and by fall-through past $EDFA) and
|
|
; exchangePlayerNamesAndSettings ($EDA1, $EDA8)
|
|
; ----------------------------------------------------------------------
|
|
abortLinkAndReturnToMenu:
|
|
lda #$03 ; EDFC message/sound id 3 - the error beep
|
|
jsr showStatusMessage ; EDFE show it and play its sound immediately, bypassing the message queue
|
|
lda #$3C ; EE01 $3C = 60 frames...
|
|
jsr waitFrames ; EE03 ...one second, so the player can read it
|
|
jsr completeExchange ; EE06 let the engine finish the exchange it was in the middle of
|
|
jsr sendEmptyMessage ; EE09 release the peer with a zero-length message
|
|
jmp returnToMainMenu ; EE0C back to the main menu
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; storeOpponentNameHalf - Success tail of exchangeNameHalf: copies the five payload bytes of the
|
|
; received message into the opponent name and releases the receive buffer.
|
|
; In: X = the name offset, Y = 1, the received message at $E020
|
|
; Out: opponentPlayerName $FFE8+X..+4 written; the comm module's receive buffer released (jump-table
|
|
; entry $E000 with X = 2)
|
|
; Called from: exchangeNameHalf ($EDFA)
|
|
; ----------------------------------------------------------------------
|
|
storeOpponentNameHalf:
|
|
lda rxPacketBuffer,y ; EE0F payload byte of the received message
|
|
sta opponentPlayerName,x ; EE12 into the opponent's name
|
|
L_EE15:
|
|
inx ; EE15 next name character
|
|
iny ; EE16 next message byte
|
|
cpy #$06 ; EE17 six bytes in the message
|
|
L_EE19:
|
|
bcc storeOpponentNameHalf ; EE19 copy all five characters
|
|
ldx #$02 ; EE1B X = 2 = release the received message
|
|
jmp commJumpTable ; EE1D free the receive buffer and return through the comm module
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; waitForLinkMessage - Busy-waits until bit 6 of the comm status byte $E01D is set, i.e. until the
|
|
; module reports that an exchange has completed: a message has been received, or the pending transmit
|
|
; has gone out.
|
|
; In: $E01D exchange flags, kept up to date by the raster IRQ
|
|
; Out: returns when bit 6 is set; no registers changed
|
|
; Called from: exchangePlayerNamesAndSettings ($ED54, $ED99) and exchangeNameHalf ($EDE7)
|
|
; ----------------------------------------------------------------------
|
|
waitForLinkMessage:
|
|
bit exchangeFlags ; EE20 test the exchange flag byte; bit 6 (V) = an exchange has completed
|
|
bvc waitForLinkMessage ; EE23 spin until the raster IRQ sets it
|
|
rts ; EE25 a message is waiting
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; finaliseRecyclerSetup - Last step of the setup phase. For each side, a recyclerMode of 2 means
|
|
; 'this side has no recycler': its coordinates are marked $FF, and if it is the opponent's side the
|
|
; 'SPOTTED RECYCLER!' scan checkEnemyRecyclerSpotted ($3F57) is disabled by patching an RTS over its
|
|
; first byte. Finally the local player's recycler is drawn onto the map.
|
|
; In: recyclerMode $92A6[side], playerSide $0B9F
|
|
; Out: $92A8/$92AA set to $FF for a side without a recycler, $3F57 possibly patched to $60, and the
|
|
; local recycler drawn into the map
|
|
; Called from: overlay A at $7BAF, by JMP
|
|
; ----------------------------------------------------------------------
|
|
finaliseRecyclerSetup:
|
|
ldx #$01 ; EE26 start with side 1, then side 0
|
|
finaliseRecyclerSideLoop:
|
|
lda recyclerMode,x ; EE28 how this side's recycler was configured
|
|
cmp #$02 ; EE2B 2 = 'none', the setup menu's third choice
|
|
sub_EE2D:
|
|
bne nextRecyclerSide ; EE2D this side does have a recycler - leave it alone
|
|
lda #$FF ; EE2F $FF = 'no position'...
|
|
sta recyclerCol,x ; EE31 ...marks the recycler column...
|
|
sta recyclerRow,x ; EE34 ...and the row as unset
|
|
cpx playerSide ; EE37 is this the local player's own side?
|
|
beq nextRecyclerSide ; EE3A yes - keep the spotted-recycler scan alive
|
|
lda #$60 ; EE3C $60 = the RTS opcode
|
|
L_EE3E:
|
|
sta checkEnemyRecyclerSpotted; EE3E patch it over the first byte of checkEnemyRecyclerSpotted so the 'SPOTTED RECYCLER!' scan never runs
|
|
nextRecyclerSide:
|
|
dex ; EE41 next side
|
|
bpl finaliseRecyclerSideLoop; EE42 loop over sides 1 and 0
|
|
ldy playerSide ; EE44 Y = the local player's side
|
|
jmp drawRecyclerOnMap ; EE47 draw his recycler onto the battlefield map and return through it
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; loadFilmStartSnapshot - Restores the game state a 'game film' was recorded from. The film buffer
|
|
; lives in the RAM under the I/O area at $D000, so every access goes through
|
|
; readFilmByte/copyPageUnderIo which bank $01 to $34 with the CIA2 interrupts held off. Twenty bytes
|
|
; of scores and options are read back, four pages of unit arrays are copied into place, and the film
|
|
; pointer is then set to the first recorded command.
|
|
; In: the film buffer at $D000+, readFilmByte $5874, copyPageUnderIo $58B2, rewindFilmPtr $58D5,
|
|
; getFilmSnapshotDestAddr $5983, getFilmSnapshotSrcAddr $5988
|
|
; Out: $92A2-$92B5 (20 bytes), the unit arrays from $F640 up, gameClock $91CB, filmPtr zp_BC/zp_BD;
|
|
; the copy operands $58C3/$58C4/$58C6/$58C7 and zp_19 clobbered
|
|
; Called from: overlay A at $7D41, at the start of film playback
|
|
; ----------------------------------------------------------------------
|
|
loadFilmStartSnapshot:
|
|
lda #$34 ; EE4A film header offset $34...
|
|
sta filmPtr ; EE4C ...low byte of the film pointer
|
|
lda #$D0 ; EE4E the film buffer lives under the I/O area...
|
|
sta filmPtrHi ; EE50 ...so the pointer is $D034, just past the film directory entry
|
|
ldy #$00 ; EE52 start of the score/option block
|
|
readScoreBlockLoop:
|
|
jsr readFilmByte ; EE54 read one byte from under the I/O area
|
|
sta sideScoreLo,y ; EE57 into the score and option block at $92A2
|
|
iny ; EE5A next byte
|
|
cpy #$14 ; EE5B $14 = 20 bytes
|
|
L_EE5D:
|
|
bcc readScoreBlockLoop ; EE5D loop
|
|
lda #$48 ; EE5F source low byte...
|
|
sta D_58C3 ; EE61 ...into copyPageUnderIo's source operand
|
|
lda #$D0 ; EE64 source page...
|
|
sta D_58C4 ; EE66 ...so the first page comes from $D048
|
|
lda #$40 ; EE69 destination low byte...
|
|
L_EE6B:
|
|
sta D_58C6 ; EE6B ...into copyPageUnderIo's destination operand
|
|
lda #$F6 ; EE6E destination page...
|
|
sta D_58C7 ; EE70 ...so it lands on the unit arrays at $F640
|
|
jsr copyPageUnderIo ; EE73 copy 256 bytes with the I/O area banked out
|
|
jsr getFilmSnapshotDestAddr ; EE76 A/X = $D268, the last page of the snapshot in the film buffer
|
|
sta D_58C3 ; EE79 into the source operand low...
|
|
stx D_58C4 ; EE7C ...and high
|
|
jsr getFilmSnapshotSrcAddr ; EE7F A/X = $F860, the last page of the unit arrays
|
|
sta D_58C6 ; EE82 into the destination operand low...
|
|
sub_EE85:
|
|
stx D_58C7 ; EE85 ...and high
|
|
lda #$02 ; EE88 three more pages, counted 2, 1, 0...
|
|
sta scratch19 ; EE8A ...in zp_19
|
|
copySnapshotPageLoop:
|
|
jsr copyPageUnderIo ; EE8C copy one page
|
|
dec D_58C4 ; EE8F walk the source down one page
|
|
dec D_58C7 ; EE92 and the destination down one page
|
|
L_EE95:
|
|
dec scratch19 ; EE95 one page fewer
|
|
bpl copySnapshotPageLoop ; EE97 loop for three pages
|
|
jsr rewindFilmPtr ; EE99 film pointer back to $D000
|
|
ldy #$30 ; EE9C header offset $30
|
|
jsr readFilmByte ; EE9E read the recorded clock value
|
|
L_EEA1:
|
|
sta gameClock ; EEA1 restore the game clock
|
|
ldy #$03 ; EEA4 header offset 3 = high byte of the first-command offset
|
|
jsr readFilmByte ; EEA6 read it
|
|
tax ; EEA9 keep it in X
|
|
dey ; EEAA header offset 2 = the low byte
|
|
jsr readFilmByte ; EEAB read it
|
|
clc ; EEAE no carry into the addition
|
|
L_EEAF:
|
|
adc #$00 ; EEAF the offset is relative to the film buffer, whose low byte is 0
|
|
sta filmPtr ; EEB1 film pointer low
|
|
txa ; EEB3 the high half of the offset...
|
|
adc #$D0 ; EEB4 ...plus the film buffer page $D0
|
|
L_EEB6:
|
|
sta filmPtrHi ; EEB6 film pointer high - playback now starts at the first recorded command
|
|
rts ; EEB8 done
|
|
|
|
|
|
; ----------------------------------------------------------------------
|
|
; handleDisplayToggleKey - The solo trainer's F1 handler, polled every frame from the main idle loop
|
|
; at $600A but only while $0BA5 bit 7 (practice mode) is set. It fires when the pending screen key is
|
|
; 0 (F1), sets a 60-frame input lockout and falls into announceAndToggleDisplayMode, whose first store
|
|
; also puts $3C into zp_66 so the key cannot retrigger.
|
|
; In: requestedScreenKey zp_66 (0 = F1, $FF = none)
|
|
; Out: inputLockoutTimer $0B7D = $3C, zp_66 = $3C, the display mode toggled and two messages queued
|
|
; Called from: the main idle loop at $600A, in trainer mode only
|
|
; ----------------------------------------------------------------------
|
|
handleDisplayToggleKey:
|
|
lda requestedScreenKey ; EEB9 the function key the IRQ poll last saw: 0 = F1, 1 = F3, 2 = F5, 3 = F7, $FF = none
|
|
beq beginDisplayToggle ; EEBB F1 - toggle the battle display
|
|
rts ; EEBD any other key is not ours
|
|
|
|
beginDisplayToggle:
|
|
lda #$3C ; EEBE $3C = 60 frames...
|
|
sta inputLockoutTimer ; EEC0 ...of input lockout, so a held key does not repeat
|
|
|
|
; ----------------------------------------------------------------------
|
|
; announceAndToggleDisplayMode - Queues message slot $21 with either 'BEGINNER DISPLAY SET.' or
|
|
; 'STANDARD DISPLAY SET.'. The flag still holds the old value here, so the message names the mode
|
|
; that is about to be entered. It then falls into toggleBattleDisplayMode, which performs the switch.
|
|
; In: A = $3C from the caller, revealAllUnits $92FC
|
|
; Out: zp_66 = $3C so F1 does not retrigger; message slot $21 pointed at the right string and queued
|
|
; Called from: handleDisplayToggleKey, by fall-through from $EEC0
|
|
; ----------------------------------------------------------------------
|
|
announceAndToggleDisplayMode:
|
|
sta requestedScreenKey ; EEC3 $3C is not a valid function-key code, so the key stops repeating
|
|
lda #$28 ; EEC5 'BEGINNER DISPLAY SET.' low byte...
|
|
ldy #$EF ; EEC7 ...page: the string at $EF28
|
|
L_EEC9:
|
|
bit revealAllUnits ; EEC9 revealAllUnits: bit 7 set = beginner display, in which every enemy unit is drawn
|
|
bpl queueDisplaySetMessage ; EECC currently standard, so beginner is what is about to be switched on
|
|
clc ; EECE clear carry...
|
|
adc #$15 ; EECF ...+$15 = 21 bytes moves on to 'STANDARD DISPLAY SET.' at $EF3D
|
|
bcc queueDisplaySetMessage ; EED1 no page crossing
|
|
iny ; EED3 carry into the page
|
|
queueDisplaySetMessage:
|
|
ldx #$21 ; EED4 message slot $21
|
|
jsr setMessageSlot ; EED6 point that slot at the chosen string
|
|
txa ; EED9 A = the slot number (setMessageSlot leaves X untouched)
|
|
jsr queueMessage ; EEDA queue it for display, then fall into the toggle itself
|
|
|
|
; ----------------------------------------------------------------------
|
|
; toggleBattleDisplayMode - Flips revealAllUnits $92FC (bit 7 set = beginner display, in which
|
|
; renderMapTile at $284F draws every enemy unit), patches the eight-character hole at $EF10 with the
|
|
; name of the mode F1 will switch to NEXT, and queues the resulting prompt 'F 1 FOR xxxxxxxx BATTLE
|
|
; DISPLAY.' as message slot $20.
|
|
; In: revealAllUnits $92FC
|
|
; Out: $92FC toggled, displayModeNameSlot $EF10-$EF17 patched, message slot $20 registered and queued
|
|
; Called from: announceAndToggleDisplayMode by fall-through, and overlay A at $824B when a trainer
|
|
; battle starts
|
|
; ----------------------------------------------------------------------
|
|
toggleBattleDisplayMode:
|
|
ldy #$00 ; EEDD Y = 0 selects 'BEGINNER' at $EF28
|
|
L_EEDF:
|
|
lda revealAllUnits ; EEDF the current display mode
|
|
eor #$FF ; EEE2 flip every bit...
|
|
sta revealAllUnits ; EEE4 ...so $00 becomes $FF and back
|
|
bpl L_EEEB ; EEE7 now standard: the prompt should offer BEGINNER, so keep Y = 0
|
|
ldy #$15 ; EEE9 now beginner: Y = $15 = 21 selects 'STANDARD' at $EF3D instead
|
|
L_EEEB:
|
|
ldx #$00 ; EEEB start of the eight-character hole in the prompt
|
|
copyModeNameLoop:
|
|
lda msgBeginnerDisplaySet,y ; EEED one character of 'BEGINNER' or 'STANDARD' - the first eight characters of the two 'xxx DISPLAY SET.' strings
|
|
sta displayModeNameSlot,x ; EEF0 patch it into the prompt at $EF10
|
|
iny ; EEF3 next source character
|
|
inx ; EEF4 next destination character
|
|
cpx #$08 ; EEF5 eight characters
|
|
bcc copyModeNameLoop ; EEF7 copy them all
|
|
D_EEF9:
|
|
ldx #$20 ; EEF9 message slot $20
|
|
lda #$08 ; EEFB prompt string low byte...
|
|
D_EEFD:
|
|
ldy #$EF ; EEFD ...page: 'F 1 FOR ' at $EF08
|
|
jsr setMessageSlot ; EEFF point slot $20 at it
|
|
txa ; EF02 dead instruction - A is overwritten by the very next LDA. Left over from a version that took the slot number from X, as $EED9 still does
|
|
lda #$20 ; EF03 message slot $20
|
|
L_EF05:
|
|
jmp queueMessage ; EF05 queue the prompt and return through queueMessage
|
|
|
|
|
|
; msgDisplayPromptPrefix: text, 8 bytes, 'F 1 FOR ' with no terminator. First third of a single
|
|
; 32-byte string that runs $EF08-$EF27: this prefix, the eight-character mode-name hole at $EF10, and
|
|
; ' BATTLE DISPLAY' + the bit-7 terminator '.' at $EF18. toggleBattleDisplayMode in this file loads
|
|
; $EF08 at $EEFB/$EEFD, registers it as message slot $20 with setMessageSlot ($C924) and queues it
|
|
; ($EF05), so the status line reads 'F 1 FOR BEGINNER BATTLE DISPLAY.' or 'F 1 FOR STANDARD BATTLE
|
|
; DISPLAY.' Solo-trainer text, and setup-phase only. Both users test isSoloTrainer $0BA5 bit 7 first:
|
|
; handleDisplayToggleKey ($EEB9), polled from the menu/idle loop at $600A, and overlay A's call at
|
|
; $824B when the game-type screen opens. $600A is only reached while gamePhase zp_B1 = $FF, the
|
|
; pre-battle setup phase. In a modem game the 74 bytes $EF08-$EF51 are resident but never read; in a
|
|
; solo game they are used during setup only, because startGameFromSetup ($0B4F) then replaces
|
|
; $EC00-$EFFF with the trainer's own tail from track 35. The L_xxxx / sub_xxxx labels that chop these
|
|
; strings into pieces are not part of the data. $EC00-$EFFF is a slot that four different overlays
|
|
; occupy at different times - this comm tail (track 34 sectors 12-15), the solo trainer's tail (track
|
|
; 35 sectors 12-15), the trainer setup overlay (track 29 sectors 0-3) and the drone-flight AI (track
|
|
; 29 sectors 4-5) - and the disassembler shares one label set across all of them, so every branch
|
|
; target of those other builds surfaces here in the middle of the text.
|
|
msgDisplayPromptPrefix:
|
|
.byte "F 1 FO" ; EF08 'F 1 FO' - message slot $20 points here ($EEFB/$EEFD)
|
|
L_EF0E:
|
|
.byte $52 ; EF0E 'R' - end of 'F 1 FOR'; the split is only the foreign label L_EF0E
|
|
L_EF0F:
|
|
.byte $20 ; EF0F the space in front of the eight-character mode name
|
|
|
|
; displayModeNameSlot: text, 8 bytes. A hole inside the prompt above, patched at run time by
|
|
; copyModeNameLoop ($EEED-$EEF8: LDA msgBeginnerDisplaySet,Y / STA $EF10,X for X = 0..7; XREF lists
|
|
; w:EEF0 from this file as the only writer). Y = 0 copies 'BEGINNER' from $EF28, Y = $15 copies
|
|
; 'STANDARD' from $EF3D. The word written is always the mode F1 will switch to NEXT, because
|
|
; toggleBattleDisplayMode chooses it after flipping revealAllUnits $92FC. The shipped bytes are eight
|
|
; '*' ($2A). They can never appear on screen: the only code that queues message slot $20 is the same
|
|
; routine that patches them first.
|
|
displayModeNameSlot:
|
|
.byte "********" ; EF10 '********' - placeholder, always overwritten with BEGINNER or STANDARD before the message is queued
|
|
|
|
; msgBattleDisplaySuffix: text, 16 bytes, ' BATTLE DISPLAY' followed by '.' with bit 7 set ($AE).
|
|
; Tail of the message slot $20 prompt that starts at $EF08; bit 7 on the last character is the string
|
|
; terminator every printString ($C133) call looks for.
|
|
msgBattleDisplaySuffix:
|
|
.byte " BATTLE " ; EF18 ' BATTLE ' - runs straight on from the patched mode name at $EF10
|
|
L_EF20:
|
|
.byte $44,$49,$53 ; EF20 DIS
|
|
L_EF23:
|
|
.byte $50,$4C ; EF23 PL
|
|
L_EF25:
|
|
.byte $41 ; EF25 A
|
|
L_EF26:
|
|
.byte $59,$AE ; EF26 Y.
|
|
|
|
; msgBeginnerDisplaySet: text, 21 bytes, 'BEGINNER DISPLAY SET' + '.'|$80. Two jobs. (1)
|
|
; announceAndToggleDisplayMode loads it at $EEC5/$EEC7, makes it message slot $21 and queues it, so
|
|
; the status line reports the mode that is being switched on - the announcement happens before
|
|
; revealAllUnits $92FC is flipped, while the prompt at $EF08 names the other mode. (2) its first
|
|
; eight characters are the source of the 'BEGINNER' patch word copied into $EF10 (XREF: r:EEED from
|
|
; this file).
|
|
msgBeginnerDisplaySet:
|
|
.byte "BEG" ; EF28 'BEGINNER DISPLAY SET.' - message slot $21; the first 8 bytes double as the patch word
|
|
L_EF2B:
|
|
.byte $49 ; EF2B I
|
|
L_EF2C:
|
|
.byte $4E,$4E,$45,$52,$20,$44,$49,$53; EF2C NNER DIS
|
|
.byte $50 ; EF34 P
|
|
L_EF35:
|
|
.byte $4C,$41,$59 ; EF35 LAY
|
|
L_EF38:
|
|
.byte $20,$53,$45,$54,$AE ; EF38 SET.
|
|
|
|
; msgStandardDisplaySet: text, 21 bytes, 'STANDARD DISPLAY SET' + '.'|$80. Never addressed directly:
|
|
; both users reach it as msgBeginnerDisplaySet + $15 (ADC #$15 at $EECF for the message pointer, LDY
|
|
; #$15 at $EEE9 for the patch word), so the two strings have to stay exactly 21 bytes apart and in
|
|
; this order.
|
|
msgStandardDisplaySet:
|
|
.byte "STANDARD DI" ; EF3D 'STANDARD DISPLAY SET.' = $EF28+$15; its first 8 bytes are the 'STANDARD' patch word
|
|
L_EF48:
|
|
.byte $53,$50,$4C,$41,$59,$20,$53; EF48 SPLAY S
|
|
L_EF4F:
|
|
.byte $45,$54,$AE ; EF4F ET.
|
|
|
|
; Setup-menu label block, $EF52-$EFAE, 93 bytes: ten bit-7 terminated strings printed by the setup,
|
|
; option and film menus of overlay A (the map generator / setup build of $6F00-$87FF). Overlay A
|
|
; never names them: it loads each address as two immediates (LDA #low / LDY #$EF) and calls a printer,
|
|
; which is why XREF.txt has an entry for only one of them. Because loadOverlayA chains into
|
|
; loadCommTailBuild2 ($1070), this page is present in both modem and solo games, so overlay A can
|
|
; print these strings whichever opponent module is loaded. The survey calls the whole run
|
|
; gameOptionLabels; each entry is labelled separately here. msgHandicap: text, 8 bytes, 'HANDICA' +
|
|
; 'P'|$80. Title of the one-row number editor (beginValueEdit, overlay A $84D0) for the DEFENDER
|
|
; handicap: 0-50, displayed multiplied by ten as 0-500 points, offered only under CUSTOM rules ($92F3
|
|
; bit 7) and only to side 1.
|
|
msgHandicap:
|
|
.byte "H" ; EF52 'HANDICAP' - title of the handicap value editor (overlay A $84D0)
|
|
L_EF53:
|
|
.byte $41,$4E,$44,$49 ; EF53 ANDI
|
|
L_EF57:
|
|
.byte $43 ; EF57 C
|
|
L_EF58:
|
|
.byte $41 ; EF58 A
|
|
L_EF59:
|
|
.byte $D0 ; EF59 'P' with bit 7 set = the end-of-string marker
|
|
|
|
; msgTerrainPts: text, 11 bytes, 'TERRAIN PT' + 'S'|$80. Title of the TERRAIN PTS value editor
|
|
; (overlay A $84F5, editTerrainPointsForSide): a per-side 0-2 setting taken from terrainPointsBySide,
|
|
; edited with the joystick and sent to the opponent.
|
|
msgTerrainPts:
|
|
.byte "TERRAIN PT",'S'|$80 ; EF5A 'TERRAIN PTS' - value editor title, overlay A $84F5
|
|
|
|
; msgDrones: text, 6 bytes, 'DRONE' + 'S'|$80. Title of the first half of editDronesAndMissiles
|
|
; (overlay A $8514): the drone count, 0-9. Its companion label 'MISSILES' is not here but at $CC9E in
|
|
; the text overlay; both numbers are then sent together as command $90.
|
|
msgDrones:
|
|
.byte "DRONE",'S'|$80 ; EF65 'DRONES' - value editor title, overlay A $8514 (0-9)
|
|
|
|
; msgComcenSpeed: text, 12 bytes, 'COMCEN SPEE' + 'D'|$80. Title of the COMCEN SPEED editor (overlay
|
|
; A $85BF, editComcenSpeed): a class 0-3 sent as command $A5 kind 3 and mapped through the table at
|
|
; $78BF into the comcen's actual step rate.
|
|
msgComcenSpeed:
|
|
.byte "CO" ; EF6B 'COMCEN SPEED' - value editor title, overlay A $85BF (0-3)
|
|
L_EF6D:
|
|
.byte $4D,$43,$45,$4E,$20,$53,$50,$45; EF6D MCEN SPE
|
|
.byte $45,$C4 ; EF75 E.
|
|
|
|
; msgMapType: text, 10 bytes, 'MAP TYPE' then $0D and $8D. $0D is a carriage return
|
|
; (handleControlChar $C03D) and $8D is the same code carrying the terminator bit, so printing the
|
|
; string leaves the cursor two rows lower and the STANDARD / CUSTOM items land under the title. Used
|
|
; as the menu title by runMapTypeMenu (overlay A $82C7). Centring counts the two CRs as characters,
|
|
; so the title probably sits one column left of exact centre. The only entry of this block with a real
|
|
; XREF entry: showCustomSummaryMessage reads it with LDA $EF77,X at $86DF (from game/mapGenerator6F00)
|
|
; and copies just the first three characters, masked with AND #$7F, to build the 'CUSTOM:MAP,...'
|
|
; status line.
|
|
msgMapType:
|
|
.byte "MAP TYPE",$0D,$8D ; EF77 'MAP TYPE' + CR + CR|$80 - menu title ($82C7); $86DF also lifts the 'MAP' out of it
|
|
|
|
; msgMore: text, 8 bytes, ' (MORE)' + CR|$80. The extra item of the game-film slot menu, shown only
|
|
; when a data disk is in the drive (diskIdIndex non-zero): overlay A $7F15 prints it as the eleventh
|
|
; line when the menu is built, and $8136 re-prints it when the selector highlights that row. On the
|
|
; game disk the menu stops at ten slots plus ' (EXIT)' ($CAB2).
|
|
msgMore:
|
|
.byte " (MORE)",$8D ; EF81 ' (MORE)' + CR|$80 - extra film-menu item on a data disk ($7F15/$8136)
|
|
|
|
; msgLocating: text, 9 bytes, 'LOCATING' + CR|$80. First line of the two-line banner shown while the
|
|
; player positions the recycler: overlay A $83E5 prints it centred and immediately calls
|
|
; printNextStringCentred ($83EC), which carries on with whatever string follows in memory -
|
|
; msgRecycler. The two strings therefore have to stay adjacent and in this order.
|
|
msgLocating:
|
|
.byte "LOCATING",$8D ; EF89 'LOCATING' + CR|$80 - line 1 of the recycler banner ($83E5), continued at $EF92
|
|
|
|
; msgRecycler: text, 9 bytes, 'RECYCLER' + CR|$80. Title of the FULL / HALF / NONE recycler menu
|
|
; (overlay A $83A0, printed centred above the three words at $0652), and the second line of the
|
|
; 'LOCATING RECYCLER' banner when it is reached by fall-through from $EF89.
|
|
msgRecycler:
|
|
.byte "RECYCLER",$8D ; EF92 'RECYCLER' + CR|$80 - recycler menu title ($83A0), also line 2 of the banner
|
|
|
|
; msgUnitMenus: text, 12 bytes, 'UNIT MENUS' + $0D + $8D. Title handed to editOptionBitOnOff (overlay
|
|
; A $82AF) with X = mask $08: a two-item OFF / ON menu that clears or sets bit 3 of gameTypeOptions
|
|
; $0BA3. As with 'MAP TYPE' the two carriage returns drop the OFF / ON items two rows below the
|
|
; title. The sub_EF9E label in the middle of the string is not a routine of this build: it is the
|
|
; entry point droneDistanceToCell of the drone-AI overlay that occupies this page in a solo game, and
|
|
; the disassembler's label set is shared between the two.
|
|
msgUnitMenus:
|
|
.byte "UNI" ; EF9B 'UNIT MENUS' + CR + CR|$80 - OFF/ON menu for gameTypeOptions bit 3 ($82AF)
|
|
sub_EF9E:
|
|
.byte $54,$20,$4D,$45,$4E,$55,$53,$0D; EF9E T MENUS.
|
|
.byte $8D ; EFA6 .
|
|
|
|
; msgDamage: text, 8 bytes, 'DAMAGE' + $0D + $8D. The second CUSTOM sub-menu, same treatment at
|
|
; overlay A $82B8 with X = mask $10 = bit 4 of gameTypeOptions $0BA3. This is the last entry of the
|
|
; label block that began at $EF52.
|
|
msgDamage:
|
|
.byte "DAMAGE",$0D,$8D ; EFA7 'DAMAGE' + CR + CR|$80 - OFF/ON menu for gameTypeOptions bit 4 ($82B8)
|
|
|
|
; msgInsertDiskTemplate: text, 33 bytes, 'INSERT DATA DISK AND PRESS SPACE' + '.'|$80, occupying
|
|
; $EFAF-$EFCF. promptInsertDiskAndWaitSpace (overlay A $81B6) registers it as message slot $1E at
|
|
; $81BE/$81C0 and leaves it as the persistent status message while the game-film save / load flow
|
|
; waits for the space bar. The four characters at $EFB6 are a patch hole, so the same template serves
|
|
; both prompts.
|
|
msgInsertDiskTemplate:
|
|
.byte "INSE" ; EFAF 'INSERT xxxx DISK AND PRESS SPACE.' - message slot $1E (overlay A $81BE)
|
|
L_EFB3:
|
|
.byte $52,$54,$20 ; EFB3 RT
|
|
|
|
; diskNameSlot: the four characters 'DATA' inside the template above are a hole, not text of their
|
|
; own. promptInsertDiskAndWaitSpace copies a four-character disk name over them with a self-modified
|
|
; LDA / STA pair at overlay A $81C7-$81CA (this is the XREF entry w:81CA from game/mapGenerator6F00):
|
|
; 'DATA' ($EFD0) via promptInsertDataDisk ($81B2) or 'GAME' ($EFD4) via promptInsertGameDisk ($81A5).
|
|
; The image on disk holds 'DATA', the same four bytes as $EFD0, so the unpatched template already
|
|
; reads correctly.
|
|
diskNameSlot:
|
|
.byte $44,$41,$54,$41 ; EFB6 DATA
|
|
.byte $20 ; EFBA the fixed part of the template resumes here: ' DISK AND PRESS SPACE.'
|
|
.byte $44,$49,$53,$4B,$20,$41,$4E,$44; EFBB DISK AND
|
|
.byte $20,$50,$52,$45,$53,$53,$20,$53; EFC3 PRESS S
|
|
.byte $50 ; EFCB P
|
|
L_EFCC:
|
|
.byte $41 ; EFCC 'A' of SPACE - the last byte of this page that differs between the four $EC00 overlays
|
|
|
|
; msgInsertDiskTail: 'CE' + '.'|$80, the last three characters of the message above (a separate block
|
|
; only because the other builds of this page have a branch target at $EFCC). From here to $EFFE the
|
|
; page is shared. $EFCD-$EFFE is byte-for-byte identical in all four images that can occupy
|
|
; $EC00-$EFFF - track 34 s12-15 (this comm tail), track 35 s12-15 (trainer tail), track 29 s0-3
|
|
; (trainer setup) and track 29 s4-5 (drone AI); only $EFFF differs, $00 here and $FF in the other
|
|
; three. Evidence: a byte comparison of the four decrypted sector images. The strings from $EFCD on
|
|
; were therefore assembled into the tail of every build, which is why overlay A can print them no
|
|
; matter which module is resident.
|
|
msgInsertDiskTail:
|
|
.byte "CE",'.'|$80 ; EFCD 'CE.' - end of 'INSERT xxxx DISK AND PRESS SPACE.'; from here on the page is common to all four builds
|
|
|
|
; wordData: text, 4 bytes, 'DATA' with no terminator - a patch word, never printed on its own.
|
|
; promptInsertDataDisk (overlay A $81B2) passes $EFD0 in A/Y and the four bytes are copied into the
|
|
; hole at $EFB6.
|
|
wordData:
|
|
.byte "DATA" ; EFD0 'DATA' - patch word for $EFB6 (overlay A $81B2)
|
|
|
|
; wordGame: text, 4 bytes, 'GAME' with no terminator - the other patch word for $EFB6, passed by
|
|
; promptInsertGameDisk (overlay A $81A5) before it waits for the OZ game disk to be put back in the
|
|
; drive.
|
|
wordGame:
|
|
.byte "GAME" ; EFD4 'GAME' - patch word for $EFB6 (overlay A $81A5)
|
|
|
|
; msgCustomOff: text, 10 bytes, 'CUSTOM OF' + 'F'|$80. beginGameTypeSelection (overlay A $8218)
|
|
; registers it as message slot $1E for the GAME TYPE screen and $821F/$8224 point the store operand at
|
|
; $869A/$869B at $EFDF, the 'OFF' inside it. Every time the player presses '*',
|
|
; showToggleStateMessage ($8673) copies three bytes over $EFDF-$EFE1 - 'OFF' from $064D or 'ON'|$80
|
|
; plus the stray 'F' that follows it at $0650 - so the same slot reads 'CUSTOM ON' or 'CUSTOM OFF' and
|
|
; mirrors gameTypeOptions $0BA3 bit 6 (CUSTOM rules).
|
|
msgCustomOff:
|
|
.byte "CUSTOM OF",'F'|$80 ; EFD8 'CUSTOM OFF' - message slot $1E; $EFDF-$EFE1 is rewritten with ON/OFF by the store at $8699
|
|
|
|
; msgDestroyOff: text, 11 bytes, 'DESTROY OF' + 'F'|$80. The same machinery for the unit placement
|
|
; phase: setDestroyModeMessage (overlay A $86B1) makes it message slot $1E and $86B8 aims the ON/OFF
|
|
; store at $EFEA, so '*' switches the line between 'DESTROY OFF' and 'DESTROY ON' while the off state
|
|
; also restores persistent message $0A ('SETUP UNITS. PRESS F 1 WHEN DONE.').
|
|
msgDestroyOff:
|
|
.byte "DESTROY OF",'F'|$80 ; EFE2 'DESTROY OFF' - message slot $1E; $EFEA-$EFEC is the ON/OFF hole
|
|
|
|
; msgAccepted: text, 8 bytes, 'ACCEPTE' + 'D'|$80. Printed centred on text row 15 by acceptCurrentMap
|
|
; (overlay A $7708-$7714) when the player accepts the battlefield currently on screen. Last string of
|
|
; the block.
|
|
msgAccepted:
|
|
.byte "ACCEPTE",'D'|$80 ; EFED 'ACCEPTED' - printed centred by overlay A $7710
|
|
|
|
; commTailFiller: 11 unused bytes, $EFF5-$EFFF - $C0,$DF then $00,$FF repeated four times and a final
|
|
; $00. Nothing points at them: XREF.txt has no entry for any address in the range, no immediate pair
|
|
; in this file or in overlay A forms an address inside it, and they sit behind the last string of the
|
|
; block, so this is padding left at the end of track 34 sector 15. The same 11 bytes end the trainer
|
|
; tail, the trainer setup overlay and the drone-AI overlay, except that their last byte is $FF.
|
|
commTailFiller:
|
|
.byte $C0 ; EFF5 $C0,$DF then alternating $00/$FF - unreferenced padding at the end of the sector
|
|
.byte $DF,$00,$FF,$00,$FF,$00,$FF; EFF6 .......
|
|
.byte $00 ; EFFD $EFFF is $00 in this track 34 image but $FF in the track 35 and track 29 images of the page
|
|
.byte $FF,$00 ; EFFE ..
|