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