{ "chunk": "game_ovl_E000_T34_E7CA", "unit": "game/ovl_E000_T34", "routines": [ { "addr": "E7CA", "name": "sendNextModemCommandChar", "summary": "Tail of the per-tick link service routine sub_E756 (reached by BPL from E7C3 once the inter-character delay counter modemDelayCounter/$EB01 has expired). Takes the next byte of the pending modem command string from the module's outbound character queue (sub_E0C3, buffer $E091), and either arms a new delay (byte with bit 7 set, e.g. $A0/$C0 = 96/64 ticks around the '+++' escape) or transmits it with queueTxByte/sub_E73C. After a CR it loads modemReplyTimer/$E03F with $79 (or 2 when the carrier bit $E03C bit 6 is clear); when that timer expires it fabricates a CR into the module's inbound character queue (sub_E0DD) so the game does not hang waiting for a modem answer.", "inputs": "D_EB01 delay counter, D_E03F reply timer, D_E03C bit 6 (carrier/direct-connect), outbound char queue $E091/$E02E via sub_E0C3", "outputs": "byte queued for transmission (sub_E73C -> $E417 ring, L_E0A5++), D_EB01, D_E03F updated; may push CR into the game-visible receive queue $E086 via sub_E0DD; falls through to the link state machine at E7F7", "confidence": "medium" }, { "addr": "E7F7", "name": "runLinkStateMachine", "summary": "Second half of the per-tick link service (sub_E756). Dispatches on commState/$E040: state >= 2 with resyncRequest/$E047 non-zero, or any state below 2, restarts the byte-sync phase at E805; state 2 continues the sync phase at E828; state 3 runs the packet phase at E858.", "inputs": "D_E040 commState, D_E047 resyncRequest", "outputs": "D_E040 advanced; branches into the sync or packet code", "confidence": "medium" }, { "addr": "E805", "name": "beginByteSyncPhase", "summary": "Enters commState 2: drains the UART receive ring (sub_E4D2 until empty), flushes the transmit ring (sub_E534), clears resyncRequest/$E047, rxPacketRemaining/$EAB9 and syncFfCount/$EAB7, then loops sending $00 and $FF and counting the peer's answers. It leaves for state 3 (E858) once the peer's $FF has been seen; after ten unsuccessful rounds (syncFfCount >= 10) it restarts the whole sync from E80F.", "inputs": "UART rings ($E417 tx / $E430 rx), D_EAB7 syncFfCount, D_EAB8 zeroRunLength, L_E0A5 pending tx byte count", "outputs": "D_E040 = 2 then 3, D_EAB7/D_EAB8/D_EAB9/D_E047 reset, $00/$FF bytes transmitted, D_E044 |= 2, D_EAC0 ackPending set on success", "confidence": "medium" }, { "addr": "E858", "name": "runPacketPhase", "summary": "commState 3: the reliable packet layer. Calls receivePacket (sub_E9AA), then, when fewer than two bytes are still queued for transmission, sends the deferred raw sync bytes ($00 when sendZeroFlag/$EAC2 is negative), handles a retransmission request ($EABF) by resending the oldest unacknowledged message, would build a new message from the game's outbound ring when txEnable/$E0A6 is set (never set in this build - see insights), and otherwise emits a bare acknowledgement packet when ackPending/$EAC0 is set.", "inputs": "L_E0A5, D_EAC1/D_EAC2 raw-byte flags, D_E0A4 txBusy, D_E0A6 txEnable, D_EABF resendRequest, D_EAC5/D_EAC6 outstanding-message indices, D_EAC0 ackPending", "outputs": "packets transmitted through sub_E909; D_EAC0/D_EAC1/D_EAC2/D_EABF cleared; D_EAC3/D_EAC4/D_EAC5/D_EAC6/D_EABE updated", "confidence": "medium" }, { "addr": "E8F8", "name": "buildAckPacket", "summary": "Builds a two-byte bare acknowledgement packet in packetBuffer/$EAEB: length code $55 (= one payload byte) followed by control byte $EC, then falls into the common packet tail at E90C which adds the sequence bits and the checksum and transmits it.", "inputs": "none", "outputs": "D_EAEB = $55, D_EAEC = $EC, D_EAEA = 2, then packet sent; D_EAC0 cleared", "confidence": "high" }, { "addr": "E909", "name": "sendPacket", "summary": "Sends the message stored in the transmit ring at index A: builds the packet with buildPacketFromRing, ORs the low two bits of rxSeqCounter/$EABD into the control byte, appends the checksum from computePacketChecksum and clocks all bytes out through queueTxByte/sub_E73C. Entry point E90C skips the build step (used by buildAckPacket).", "inputs": "A = index into txMessageRing $EAC7; D_EABD sequence counter", "outputs": "D_EAEA/D_EAEB..: finished packet; bytes pushed into the UART tx ring; D_EAC0 cleared; D_EABA, D_EAC3 clobbered", "confidence": "high" }, { "addr": "E937", "name": "computePacketChecksum", "summary": "Rotate-and-add checksum over packetBuffer[0..packetLength-1]: A starts at $EE and for each byte does ASL / ADC #0 (fold the carry back in) / ADC byte / ADC #0. Used both when sending (result appended after the payload) and when receiving (result EORed with the received check byte, zero = good).", "inputs": "D_EAEA packet length, D_EAEB.. packet bytes", "outputs": "A = checksum, X = D_EAEA (index of the check byte slot)", "confidence": "high" }, { "addr": "E94A", "name": "buildPacketFromRing", "summary": "Copies one queued message out of the 35-byte transmit ring into packetBuffer. A = ring index of the message's length byte; the length is turned into one of the 16 transition-rich code bytes from packetLengthCodeTable and written as packet byte 0, then length+1 further ring bytes (the command byte and its arguments) are appended.", "inputs": "A = ring index; D_EAC7 ring, D_EAA0 length-code table", "outputs": "D_EAEB.. packet, D_EAEA = number of bytes written, D_EAC3 advanced past the message, D_EABA clobbered", "confidence": "high" }, { "addr": "E96D", "name": "writeTxRingByte", "summary": "Appends A to the 35-byte transmit message ring $EAC7 at index txRingIndex/$EAC3 and advances the index modulo $23.", "inputs": "A = byte, D_EAC3", "outputs": "ring updated, D_EAC3 advanced, X = new index", "confidence": "high" }, { "addr": "E975", "name": "readTxRingByte", "summary": "Reads the byte at txRingIndex/$EAC3 from the transmit ring and advances the index (wrapping at $23).", "inputs": "D_EAC3", "outputs": "A = byte, D_EAC3 advanced, X = new index", "confidence": "high" }, { "addr": "E978", "name": "readTxRingByteAt", "summary": "Reads the transmit-ring byte at the arbitrary index X, then sets txRingIndex/$EAC3 to X+1 (wrapped). Used to re-read a message for retransmission and to inspect an acknowledged message's header.", "inputs": "X = ring index", "outputs": "A = byte, D_EAC3 = X+1 mod $23, X = new index", "confidence": "high" }, { "addr": "E986", "name": "noteIdlePoll", "summary": "Called when receivePacket found no data. Only counts the poll as a stall if something is actually outstanding (oldest unacked index $EAC6 negative, or current message index $EAC5 negative, or txBusy $E0A4 set); otherwise stallCounter/$EB00 is reset to 0. Entry sub_E997 always bumps the counter. When the counter reaches 16 it drops the caller's return address (PLA/PLA) and jumps back to beginByteSyncPhase.", "inputs": "D_EAC5, D_EAC6, D_E0A4, D_EB00", "outputs": "D_EB00 updated; on overflow the stack is unwound and control jumps to E805 (link resync)", "confidence": "high" }, { "addr": "E9AA", "name": "receivePacket", "summary": "Assembles one incoming packet from the UART receive ring. Skips single $00 bytes (a lone $00 sets resendRequest/$EABF = 1; two or more in a row force a resync) and $FF bytes (which set ackPending/$EAC0). The first non-sync byte is looked up in packetLengthCodeTable to get the payload length; the routine then waits until that many bytes are queued ($E42D), copies them into packetBuffer and verifies the checksum. A bad checksum decrements sendZeroFlag/$EAC2 (making the sender emit a $00 nak on the next tick) and returns; a good packet falls into dispatchReceivedPacket.", "inputs": "UART rx ring $E430/$E42D via sub_E739/sub_E743, D_EAB9 (resumable state), D_EAA0 table", "outputs": "D_EAB9, D_EAFF, D_EAEA, D_EAEB.., D_EABF, D_EAC0, D_EAC2, D_EB00; may resync via L_E9A2", "confidence": "high" }, { "addr": "EA0D", "name": "dispatchReceivedPacket", "summary": "Acts on a checksum-verified packet. Control byte >= $F0 is forwarded to the 1541 with sendByteToDrive/$089C inside an SEI (see problems - the payload index looks wrong in this build). Control byte $E0..$EF is a pure acknowledgement: it retires the outstanding message(s) by comparing the sequence field of the acked message against the ack byte and swapping the two outstanding-message slots $EAC5/$EAC6/$EAC4. Anything below $E0 is a data packet: it is accepted only when its 2-bit sequence field ((byte>>2)-rxSeq-1)&3 == 0 and the game's inbound ring is empty ($E459 == 0), then rxSeqCounter/$EABD is bumped, the original command byte is rebuilt as (control & $F0) | (payloadLength & $0F) and the command plus its arguments are pushed into the game's inbound ring through sub_E736.", "inputs": "D_EAEB.. packet, D_EAEA length, D_EABD rxSeq, D_E459 inbound ring count, D_EAC4/D_EAC5/D_EAC6", "outputs": "D_EABB/D_EABC/D_EABD/D_EABF/D_EAC0/D_EAC5/D_EAC6 updated; decoded command bytes pushed into the $E45C ring", "confidence": "medium" }, { "addr": "EA7B", "name": "deliverPacketPayload", "summary": "Inner loop of dispatchReceivedPacket: length-2 payload bytes plus the reconstructed command byte are pushed one at a time into the game's inbound command ring with sub_E736 ($E49E, ring $E45C).", "inputs": "D_EAEA, D_EAEB.., D_EAEC", "outputs": "bytes pushed into $E45C, D_E459 incremented, D_EAEA/D_EAFE clobbered", "confidence": "high" }, { "addr": "EAA0", "name": "packetReturn", "summary": "A single RTS that doubles as index 0 of packetLengthCodeTable (the table is addressed as D_EAA0,x with x = 1..16). Several branches in the packet layer target it as their exit.", "inputs": "none", "outputs": "returns; the byte $60 is also read as table entry 0 (never matched, since a length of 0 is impossible)", "confidence": "high" }, { "addr": "EAB2", "name": "clearPacketState", "summary": "Zeroes the whole 76-byte packet-protocol state block $EAB7-$EB02 by calling the shared 'zero X bytes that follow the JSR' helper sub_E067 with X = $4C. Because sub_E067 pops the return address to build its self-modified STA, this routine returns to its caller's caller ($E2BD, the module init path reached through the $E003 jump-table entry).", "inputs": "none", "outputs": "$EAB7-$EB02 = 0; returns two levels up", "confidence": "high" }, { "addr": "EB03", "name": "remoteCodeBuffer", "summary": "Entry of the downloadable-code buffer. The message engine at $E196 copies the payload of every received message whose command byte is >= $70 to $EB03 onward (self-modified index at $E1AE) and executes it with JSR $EB03 when a $70+ message arrives with an empty payload. In the shipped image the first byte is $60 (RTS), so the call is a no-op until the peer has downloaded code.", "inputs": "code previously written by $E1B3", "outputs": "whatever the downloaded code does", "confidence": "medium" }, { "addr": "EC00", "name": "clearBattlefieldMap", "summary": "Fills the 1600-byte battlefield map $F000-$F63F with $40 (empty terrain) using the zp_48/zp_49 source pointer, zp_4A/zp_4B limit and the shared pointer-advance/compare helper sub_C3F9. Called by overlay A from generateBattlefieldMap ($6F30) and from the map editor ($76F2).", "inputs": "none", "outputs": "$F000-$F63F = $40; zp_48/49 = $F640, zp_4A/4B = $F63F, zp_4E/4F advanced by 1600", "confidence": "high" }, { "addr": "EC1C", "name": "setViewOriginToHome", "summary": "Sets the battlefield view origin to column $11 (17) row $1C (28) both in the live zero-page pair zp_A5/zp_A6 and in the saved copy $92C3/$92C4, which puts the cursor cell (origin+3, origin+2) at map (20,30), the middle of the player's own end. Called by overlay A at $7AEC (battle start), $7D51 and $83EF (recycler placement).", "inputs": "none", "outputs": "zp_A5 = $11, zp_A6 = $1C, D_92C3 = $11, D_92C4 = $1C", "confidence": "medium" }, { "addr": "EC2B", "name": "loadMapSeedIntoScenarioRng", "summary": "Copies the 24-bit map seed from the game settings block ($0B95-$0B97) into the scenario random-number state zp_5A/zp_5B/zp_5C, so that both machines generate the identical procedural battlefield. Called by overlay A at $6F30-$6F33 (generateBattlefieldMap) and $7BB2.", "inputs": "D_0B95, D_0B96, D_0B97", "outputs": "zp_5A/zp_5B/zp_5C, X = $0B97 value, Y = $0B96 value", "confidence": "high" }, { "addr": "EC54", "name": "makeMapPointSymmetric", "summary": "Entry with mirrorMode/zp_18 = $FF: walks a forward pointer from $F000 and a backward pointer from $F63F over the whole map and writes mirrorTerrainCode(map[i]) into map[1599-i]. Because the mirror is an involution the second half of the pass is a no-op, so the net effect is that the second half of the map becomes the point-mirrored copy of the first half - the map generator's symmetry step. Called from overlay A $6F77.", "inputs": "map $F000-$F63F", "outputs": "map made point-symmetric; zp_18 = $FF, zp_48/49, zp_4A/4B, zp_4E/4F clobbered", "confidence": "medium" }, { "addr": "EC58", "name": "rotateMapAndMirrorCodes", "summary": "Entry with mirrorMode/zp_18 = $00: exchanges map[i] and map[1599-i] for the whole map, passing both bytes through mirrorTerrainCode, i.e. rotates the battlefield 180 degrees and flips the direction-dependent terrain codes with it. Used so that each player always sees his own side at the bottom. Called from overlay A at $76B5, $7749, $7AE1 and $7D2E.", "inputs": "map $F000-$F63F", "outputs": "map rotated in place; zp_18 = 0, zp_48/49, zp_4A/4B, zp_4E/4F clobbered", "confidence": "high" }, { "addr": "EC91", "name": "mirrorAllUnitCoordinates", "summary": "Mirrors the six 100-entry unit coordinate arrays that follow the map - unitCol $F640, unitRow $F6A4, unitDestCol $F7D0, unitDestRow $F834, unitWaypointCol $F898 and unitWaypointRow $F8FC - by calling mirrorCoordinateArray on each. Called from overlay A $7AE9 when the local player is side 1, so his units line up with the rotated map.", "inputs": "the six coordinate arrays", "outputs": "all six arrays mirrored; zp_48/zp_49 clobbered", "confidence": "high" }, { "addr": "ECB8", "name": "mirrorCoordinateArray", "summary": "For the 100-byte array at A/Y, replaces the low 6 bits of every entry with mirrorMapCoordinate ($3FA3, i.e. 39-n) while preserving the top two flag bits (which carry cloak/blitz/idle markers).", "inputs": "A = array low byte, Y = array high byte", "outputs": "array entries mirrored; zp_48/zp_49 = array pointer; self-modifies the ORA operand at $ECCD", "confidence": "high" }, { "addr": "ECD4", "name": "mirrorTerrainCode", "summary": "Maps one map cell byte to its 180-degree-rotated equivalent using mirroredTerrainCodeTable. Bytes with bit 7 set (cells holding a unit index) are returned unchanged; codes $40..$58 are looked up directly; codes $20..$3F are looked up through the same table with a -$20 shift; anything else is passed through.", "inputs": "A = map cell byte", "outputs": "A = mirrored cell byte, X clobbered", "confidence": "high" }, { "addr": "ECF3", "name": "loadUnitStartTemplate", "summary": "If $F640 has bit 7 set (template data still present), copies 512 bytes from $F000 + 400*unitLayoutByGameType[gameType & 7] to $F640, installing the unit start-position template (unitCol, unitRow, flags, type) for the selected game type; track 18 sectors 8-14 hold four such 400-byte templates rather than a battlefield map. Called from overlay A at $79E6 and $82FD, before the map is generated.", "inputs": "D_F640 bit 7, D_0BA3 bits 0-2 = game type, unitLayoutByGameType $78B8, template data at $F000", "outputs": "$F640-$F83F overwritten with the template; self-modifies the source/destination operands at $ED31/$ED32 and $ED34/$ED35", "confidence": "high" }, { "addr": "ED43", "name": "exchangePlayerNamesAndSettings", "summary": "Start-of-session handshake. In trainer mode ($0BA5 bit 7 set) it just copies the ten-character string 'TRAINER ' from $884E into opponentName $FFE8 and returns. Otherwise it waits for the link, releases any pending message, exchanges the two halves of the ten-byte owner name with exchangeNameHalf (offsets 0 and 5), substitutes 'ENEMY' ($CC99) when the peer's name starts with a space, and then swaps a four-byte settings message ($FF, commBuildId $E030, gameSettingsBlock $0B94, $0B7E), storing the peer's build id in $E02F and the EOR of the peer's last two bytes in $90F8. Any protocol error goes to abortLinkAndReturnToMenu. Called from overlay A $79D1.", "inputs": "D_0BA5, ownName $FFDE, D_E030, D_0B94, D_0B7E, comm API $E000/$E01D-$E02A", "outputs": "opponentName $FFE8-$FFF1, D_E02F, D_90F8; link messages sent/consumed", "confidence": "high" }, { "addr": "EDBE", "name": "sendEmptyMessage", "summary": "Sets the outgoing message length $E01F to 0 and asks the comm module to transmit it (jump-table entry $E000 with X = 1). Used to close the handshake and as part of the error path.", "inputs": "none", "outputs": "D_E01F = 0, transmit triggered", "confidence": "high" }, { "addr": "EDC9", "name": "exchangeNameHalf", "summary": "Sends a six-byte message ($FF followed by ownName[X..X+4]) to the peer, waits for the reply, checks that it is six bytes starting with $FF and copies the peer's five characters into opponentName $FFE8+X; on any mismatch it falls into abortLinkAndReturnToMenu. X is preserved across the exchange in the self-modified LDX at $EDEB.", "inputs": "X = offset 0 or 5 into the ten-byte name, D_FFDE ownName", "outputs": "$FFE8+X..+4 = peer name half; comm buffers used; self-modifies $EDEB", "confidence": "high" }, { "addr": "EDFC", "name": "abortLinkAndReturnToMenu", "summary": "Handshake failure path: plays sound 3 (sub_C6FF), waits 60 frames (sub_C37F), runs completeExchange ($56F1), sends an empty message and jumps to returnToMainMenu ($0AA3).", "inputs": "none", "outputs": "never returns to its caller; game returns to the main menu", "confidence": "high" }, { "addr": "EE0F", "name": "storeOpponentNameHalf", "summary": "Success tail of exchangeNameHalf: copies the five payload bytes of the received message into opponentName $FFE8+X and releases the receive buffer with the comm API (X = 2).", "inputs": "X = offset, D_E020.. received message", "outputs": "$FFE8+X..+4 written, receive buffer released", "confidence": "high" }, { "addr": "EE20", "name": "waitForLinkMessage", "summary": "Spins until bit 6 of the comm status byte $E01D is set, i.e. until a complete message has been received (or the pending transmit has completed). Used by the handshake code.", "inputs": "D_E01D", "outputs": "returns when $E01D bit 6 is set; no registers changed", "confidence": "high" }, { "addr": "EE26", "name": "finaliseRecyclerSetup", "summary": "For both sides (X = 1 then 0): if recyclerMode/$92A6[X] is 2 ('none'), marks that side's recycler coordinates $92A8/$92AA as $FF, and when the side is the opponent also patches the first byte of checkEnemyRecyclerSpotted ($3F57) with $60 (RTS) so the 'SPOTTED RECYCLER!' scan never runs. Finally draws the local player's recycler on the map (drawRecyclerOnMap with Y = playerSide $0B9F). Entered by JMP from overlay A $7BAF.", "inputs": "D_92A6, D_0B9F playerSide", "outputs": "D_92A8/$92AA set to $FF for sides without a recycler, $3F57 possibly patched to RTS, map cell drawn", "confidence": "high" }, { "addr": "EE4A", "name": "loadFilmStartSnapshot", "summary": "Restores the initial game state from the game-film recording: points the film pointer zp_BC/zp_BD at $D034, reads 20 bytes into the score/option block $92A2, copies one page from $D048 to the unit arrays at $F640 with copyPageUnderIo, copies three more pages using the snapshot source/destination addresses returned by getFilmSnapshotDestAddr/$5983 and getFilmSnapshotSrcAddr/$5988, rewinds the film pointer and finally reads the clock byte into $91CB and the film restart pointer into zp_BC/zp_BD. Called from overlay A $7D41 (start of film playback).", "inputs": "film data at $D000+, readFilmByte $5874, copyPageUnderIo $58B2, rewindFilmPtr $58D5", "outputs": "D_92A2..$92B5 (20 bytes), unit arrays at $F640+, D_91CB, zp_BC/zp_BD film pointer, $58C3/$58C4/$58C6/$58C7 copy operands, zp_19", "confidence": "medium" }, { "addr": "EEB9", "name": "handleDisplayToggleKey", "summary": "Trainer-mode F1 handler, polled every frame from the main idle loop at $600A (only when $0BA5 bit 7 is set). Acts only when currentKey/zp_66 is 0 (the F1 key code); then sets a 60-frame input lockout ($0B7D) and falls into announceAndToggleDisplayMode, whose first store puts $3C into zp_66 so the key does not retrigger.", "inputs": "zp_66 currentKey", "outputs": "D_0B7D = $3C, zp_66 = $3C, display mode toggled and messages queued", "confidence": "medium" }, { "addr": "EEC3", "name": "announceAndToggleDisplayMode", "summary": "Queues message slot $21 with 'BEGINNER DISPLAY SET.' ($EF28) or 'STANDARD DISPLAY SET.' ($EF3D) - the message names the mode that is about to be entered, since the flag is still the old value - and falls into toggleBattleDisplayMode.", "inputs": "A = $3C on entry (stored into zp_66), D_92FC revealAllUnits flag", "outputs": "zp_66 = $3C, message slot $21 pointer set (sub_C924) and queued (sub_C566)", "confidence": "medium" }, { "addr": "EEDD", "name": "toggleBattleDisplayMode", "summary": "Flips revealAllUnits/$92FC (bit 7 set = beginner display, all enemy units visible), then patches the eight-character slot at $EF10 with 'BEGINNER' or 'STANDARD' (whichever F1 will switch to next) and queues the resulting prompt 'F 1 FOR xxxxxxxx BATTLE DISPLAY.' ($EF08) as message slot $20. Also called on its own from overlay A $824B when a trainer battle starts.", "inputs": "D_92FC", "outputs": "D_92FC toggled, $EF10-$EF17 patched, message slot $20 registered and queued", "confidence": "medium" } ], "variables": [ { "addr": "E040", "scope": "abs", "name": "commState", "meaning": "Link phase: 0 idle, 1 modem-command/terminal mode, 2 byte sync, 3 packet protocol running", "confidence": "medium" }, { "addr": "E047", "scope": "abs", "name": "resyncRequest", "meaning": "Non-zero forces the state machine back into the sync phase; cleared at E814", "confidence": "medium" }, { "addr": "E03F", "scope": "abs", "name": "modemReplyTimer", "meaning": "Countdown started after a modem command is sent; on expiry a CR is faked into the game's receive queue. $FF while a command string is being emitted", "confidence": "medium" }, { "addr": "E03C", "scope": "abs", "name": "linkStatusFlags", "meaning": "Handshake/carrier sample; bit 6 selects the real-modem timing paths (tested at E787 and E7E3)", "confidence": "low" }, { "addr": "E0A4", "scope": "abs", "name": "packetTxBusy", "meaning": "Incremented when the packet layer starts building an outgoing message, cleared at E775; gates the transmit path", "confidence": "medium" }, { "addr": "E0A5", "scope": "abs", "name": "pendingTxByteCount", "meaning": "Number of bytes still to be clocked out of the UART transmit ring; incremented by sub_E73C, decremented by the CIA2 interrupt handler", "confidence": "high" }, { "addr": "E0A6", "scope": "abs", "name": "packetTxEnable", "meaning": "Gate for the packet layer's data-transmit path; only ever cleared (E778) in either comm build, so that path is dead code", "confidence": "high" }, { "addr": "E42D", "scope": "abs", "name": "rxRingCount", "meaning": "Number of bytes waiting in the UART receive ring $E430 (capacity 20); read at E9D9 to see whether a whole packet has arrived", "confidence": "high" }, { "addr": "E459", "scope": "abs", "name": "inboundMessageRingCount", "meaning": "Number of bytes in the ring $E45C that carries decoded messages up to the game; a packet is only accepted while this is 0", "confidence": "high" }, { "addr": "E01D", "scope": "abs", "name": "commStatusFlags", "meaning": "Comm module status byte; bit 6 = message received / exchange complete (polled by waitForLinkMessage and by the game at $4E4B, $56C0, $7B64)", "confidence": "high" }, { "addr": "E01E", "scope": "abs", "name": "rxMessageLength", "meaning": "Length of the message in the receive buffer $E020", "confidence": "high" }, { "addr": "E01F", "scope": "abs", "name": "txMessageLength", "meaning": "Length of the message to send from $E027", "confidence": "high" }, { "addr": "E02F", "scope": "abs", "name": "peerCommBuildId", "meaning": "The opponent's $E030 comm-build identifier as received during the handshake", "confidence": "medium" }, { "addr": "EAB7", "scope": "abs", "name": "syncFfCount", "meaning": "Rounds of the byte-sync phase; 10 or more restarts the sync", "confidence": "medium" }, { "addr": "EAB8", "scope": "abs", "name": "zeroRunLength", "meaning": "Number of consecutive $00 bytes received (maintained by sub_E743); two or more mean the peer wants a resync", "confidence": "high" }, { "addr": "EAB9", "scope": "abs", "name": "rxPacketRemaining", "meaning": "Payload bytes still expected for the packet being assembled; non-zero means receivePacket resumes mid-packet", "confidence": "high" }, { "addr": "EABA", "scope": "abs", "name": "packetByteCounter", "meaning": "Scratch loop counter used while building or copying a packet", "confidence": "high" }, { "addr": "EABB", "scope": "abs", "name": "rxControlByte", "meaning": "Control byte of the packet currently being dispatched", "confidence": "high" }, { "addr": "EABC", "scope": "abs", "name": "lastAcceptedControlByte", "meaning": "Control byte of the last accepted data packet", "confidence": "medium" }, { "addr": "EABD", "scope": "abs", "name": "rxSeqCounter", "meaning": "Two-bit receive sequence number; incremented for every accepted data packet and echoed in outgoing packets", "confidence": "high" }, { "addr": "EABE", "scope": "abs", "name": "txSeqCounter", "meaning": "Two-bit transmit sequence number embedded in bits 2-3 of the command byte of each queued message", "confidence": "high" }, { "addr": "EABF", "scope": "abs", "name": "resendRequest", "meaning": "Set to 1 by a lone $00 byte and to the outstanding message index by a high ack byte; makes the transmitter resend", "confidence": "medium" }, { "addr": "EAC0", "scope": "abs", "name": "ackPending", "meaning": "Non-zero when an acknowledgement packet ($EC) has to be sent; set on a received $FF and on an accepted data packet, cleared after any packet is transmitted", "confidence": "high" }, { "addr": "EAC1", "scope": "abs", "name": "sendFfFlag", "meaning": "Would make the transmitter emit a raw $FF; never set anywhere in the image, so the branch at E872 is dead", "confidence": "high" }, { "addr": "EAC2", "scope": "abs", "name": "sendZeroFlag", "meaning": "Decremented (to $FF) on a checksum failure; makes the transmitter emit a raw $00 nak on the next tick", "confidence": "high" }, { "addr": "EAC3", "scope": "abs", "name": "txRingIndex", "meaning": "Current read/write index into the 35-byte transmit message ring $EAC7", "confidence": "high" }, { "addr": "EAC4", "scope": "abs", "name": "txRingNextIndex", "meaning": "Ring index just past the message that was queued last", "confidence": "medium" }, { "addr": "EAC5", "scope": "abs", "name": "txCurrentMsgIndex", "meaning": "Ring index of the newest queued message (bit 7 = slot in use)", "confidence": "medium" }, { "addr": "EAC6", "scope": "abs", "name": "txOldestUnackedIndex", "meaning": "Ring index of the oldest unacknowledged message (bit 7 = slot in use); the two slots give a window of two outstanding messages", "confidence": "medium" }, { "addr": "EAEA", "scope": "abs", "name": "packetLength", "meaning": "Number of bytes currently in packetBuffer (excluding the trailing checksum while building)", "confidence": "high" }, { "addr": "EAFE", "scope": "abs", "name": "packetLoopTemp", "meaning": "Scratch byte used to preserve loop indices across subroutine calls in the packet code", "confidence": "high" }, { "addr": "EAFF", "scope": "abs", "name": "rxLengthCodeByte", "meaning": "The raw length-code byte of the packet being received, re-stored as packetBuffer[0] for the checksum", "confidence": "high" }, { "addr": "EB00", "scope": "abs", "name": "linkStallCounter", "meaning": "Counts consecutive service ticks with no protocol progress; 16 forces a full resync. Also cleared at $E716 by the CIA2 interrupt handler when a character frame starts", "confidence": "medium" }, { "addr": "EB01", "scope": "abs", "name": "modemDelayCounter", "meaning": "Negative countdown for the guard times embedded in the modem command strings (a string byte with bit 7 set is loaded here); while negative no further command characters are sent", "confidence": "high" }, { "addr": "EB02", "scope": "abs", "name": "lastTerminalChar", "meaning": "Last character delivered to the game in terminal mode; used to spot the modem result '0' immediately before a CR", "confidence": "medium" }, { "addr": "E011", "scope": "abs", "name": "modemModeIndex", "meaning": "0 = answer, 1 = originate/dial; indexes modemInitStringPtrLoTable at $EBFE", "confidence": "high" }, { "addr": "92FC", "scope": "abs", "name": "revealAllUnits", "meaning": "Bit 7 set = beginner battle display, every enemy unit is drawn whether spotted or not (tested by renderMapTile at $284F)", "confidence": "high" }, { "addr": "66", "scope": "zp", "name": "currentKey", "meaning": "Key code from the module's keyboard entry ($E015); $FF = none. Value 0 is the code the display-toggle handler reacts to (the F1 key)", "confidence": "medium" }, { "addr": "92C3", "scope": "abs", "name": "savedViewOriginCol", "meaning": "Saved copy of the view origin column zp_A5", "confidence": "medium" }, { "addr": "92C4", "scope": "abs", "name": "savedViewOriginRow", "meaning": "Saved copy of the view origin row zp_A6", "confidence": "medium" }, { "addr": "FFDE", "scope": "abs", "name": "ownPlayerName", "meaning": "Ten-character owner name read from the disk BAM, sent to the peer during the handshake", "confidence": "high" }, { "addr": "FFE8", "scope": "abs", "name": "opponentPlayerName", "meaning": "Ten-character opponent name received during the handshake ('TRAINER ' in solo mode, 'ENEMY' when the peer's name is blank)", "confidence": "high" }, { "addr": "0BA5", "scope": "abs", "name": "linkTypeAtStart", "meaning": "Bit 7 set selects comm build 1 (T35) and, in this tail, the solo trainer behaviour: canned opponent name, no handshake, F1 battle-display toggle enabled", "confidence": "high" } ], "dataBlocks": [ { "addr": "EAA1", "length": 17, "type": "byteTable", "name": "packetLengthCodeTable", "description": "The 16 byte values $55,$56,$59,$5A,$65,$66,$69,$6A,$95,$96,$99,$9A,$A5,$A6,$A9,$AA followed by an $FF sentinel. Every entry is a byte whose four bit pairs are each 01 or 10, so a length code can never be confused with the $00/$FF sync bytes and always carries plenty of transitions. Addressed as D_EAA0,x with x = payload length (1..16); the table must stay sorted because the receiver searches it with CMP/BCS." }, { "addr": "EAB7", "length": 76, "type": "byteTable", "name": "packetProtocolState", "description": "The complete state block of the packet layer, $EAB7-$EB02, zeroed in one go by clearPacketState. Contains the sync counters, sequence numbers, ring indices, the 35-byte transmit message ring at $EAC7, the packet assembly buffer at $EAEB and the modem-delay/terminal bytes at $EB01/$EB02." }, { "addr": "EAC7", "length": 35, "type": "byteTable", "name": "txMessageRing", "description": "35-byte circular buffer of queued outgoing messages. Each entry is [argumentCount][commandByte with the 2-bit sequence in bits 2-3][arguments...]. Wraps at $23; indices are held in $EAC3/$EAC4/$EAC5/$EAC6." }, { "addr": "EAEB", "length": 19, "type": "byteTable", "name": "packetBuffer", "description": "Assembly/receive buffer for one packet: [length code][control byte][payload...][checksum]. Maximum 1+16+1 bytes, so it occupies $EAEB-$EAFD." }, { "addr": "EB03", "length": 211, "type": "unknown", "name": "remoteCodeBuffer", "description": "$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)." }, { "addr": "EBD6", "length": 13, "type": "text", "name": "modemHangUpString", "description": "Modem command string, stored back to front because the character queue at $E091 is read downwards: emitted as '+' '+' '+' CR 'A' 'T' 'H' '0' CR. Bytes with bit 7 set are guard-time delays (96 and 64 ticks), which is exactly the Hayes escape-sequence guard time. Terminated by the $00 at $EBE2." }, { "addr": "EBE3", "length": 13, "type": "text", "name": "modemAnswerInitString", "description": "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)." }, { "addr": "EBF0", "length": 13, "type": "text", "name": "modemDialInitString", "description": "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)." }, { "addr": "EBFD", "length": 1, "type": "byteTable", "name": "hangUpStringPtrLo", "description": "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)." }, { "addr": "EBFE", "length": 2, "type": "byteTable", "name": "modemInitStringPtrLoTable", "description": "Low bytes $E3 and $F0 of the answer and dial init strings, indexed by modemModeIndex $E011 at $E2E0." }, { "addr": "EC3B", "length": 25, "type": "byteTable", "name": "mirroredTerrainCodeTable", "description": "Maps terrain codes $40..$58 (index 0..24) to their 180-degree-rotated equivalents: $40 stays, then A<->D, B<->C, E<->H, F<->G, I<->K, J<->L, M<->T, N<->Q, O<->S, P<->R, U<->X, V<->W. Also used with a -$20 shift for the second code range $20..$3F." }, { "addr": "EF08", "length": 8, "type": "text", "name": "msgDisplayPromptPrefix", "description": "'F 1 FOR ' - start of the trainer prompt printed as message slot $20; it runs straight into displayModeNameSlot and msgBattleDisplaySuffix." }, { "addr": "EF10", "length": 8, "type": "text", "name": "displayModeNameSlot", "description": "Eight-character patch area inside the prompt, filled at run time with 'BEGINNER' or 'STANDARD' by toggleBattleDisplayMode. The shipped image contains '********'." }, { "addr": "EF18", "length": 16, "type": "text", "name": "msgBattleDisplaySuffix", "description": "' BATTLE DISPLAY' plus the bit-7 terminator '.' - the tail of the trainer F1 prompt." }, { "addr": "EF28", "length": 21, "type": "text", "name": "msgBeginnerDisplaySet", "description": "'BEGINNER DISPLAY SET' + bit-7 '.'. The first eight characters double as the source of the 'BEGINNER' patch word." }, { "addr": "EF3D", "length": 21, "type": "text", "name": "msgStandardDisplaySet", "description": "'STANDARD DISPLAY SET' + bit-7 '.'. Reached as msgBeginnerDisplaySet+$15; its first eight characters are the 'STANDARD' patch word." }, { "addr": "EF52", "length": 93, "type": "text", "name": "gameOptionLabels", "description": "Run of bit-7 terminated labels used by overlay A's setup menus: 'HANDICAP' ($EF52), 'TERRAIN PTS' ($EF5A), 'DRONES' ($EF65), 'COMCEN SPEED' ($EF6B), 'MAP TYPE'+CR+CR ($EF77), ' (MORE)'+CR ($EF81), 'LOCATING'+CR ($EF89), 'RECYCLER'+CR ($EF92), 'UNIT MENUS'+CR+CR ($EF9B) and 'DAMAGE'+CR+CR ($EFA7)." }, { "addr": "EFAF", "length": 33, "type": "text", "name": "msgInsertDiskTemplate", "description": "'INSERT DATA DISK AND PRESS SPACE' + bit-7 '.'. Overlay A's promptInsertDiskAndWaitSpace ($81B2) patches the four characters at $EFB6 with either 'DATA' or 'GAME' before showing it as message slot $1E." }, { "addr": "EFD0", "length": 4, "type": "text", "name": "wordData", "description": "'DATA' - patch word for msgInsertDiskTemplate (no terminator)." }, { "addr": "EFD4", "length": 4, "type": "text", "name": "wordGame", "description": "'GAME' - patch word for msgInsertDiskTemplate (no terminator)." }, { "addr": "EFD8", "length": 10, "type": "text", "name": "msgCustomOff", "description": "'CUSTOM OF' + bit-7 'F' - installed as message slot $1E by overlay A $8218." }, { "addr": "EFE2", "length": 11, "type": "text", "name": "msgDestroyOff", "description": "'DESTROY OF' + bit-7 'F' - installed as message slot $1E by overlay A $86B1; $EFEA is used as the ON/OFF patch address." }, { "addr": "EFED", "length": 8, "type": "text", "name": "msgAccepted", "description": "'ACCEPTE' + bit-7 'D' - printed by overlay A $7710 when the fire button confirms a setting." }, { "addr": "EFF5", "length": 11, "type": "unknown", "name": "commTailFiller", "description": "$C0,$DF,$00 followed by four $FF,$00 pairs; unreferenced padding at the end of the sector (the last byte $EFFF also differs between the T34 and T35 tails)." } ], "misclassified": [ { "addr": "EB04", "length": 210, "actual": "code", "evidence": "Shown as .byte in the listing but disassembles cleanly as 6502 game code ($EB05 lda $F6D5 / sec / sbc $F6A4,y ..., $EB67 lda #$03 / sec / sbc $8968 ..., $EBA5 jsr $FD5D ...) referencing the unit arrays $F6A4/$F7D0/$F834, the message overlay $8800-$89xx, $91CB and the resident random routine $FD5D. The 210 bytes $EB04-$EBD5 are byte-for-byte identical in the T34 and T35 comm builds, and in the T35 build the same code continues coherently past $EBD6 and is live (called from $E1CD and $E21B). In the T34 build nothing calls it, its calls to $EBDD/$EBE3/$EBE4 land in the modem command strings, and the area doubles as the remote-code buffer written by $E1B3 - so it is dead leftover here and marking it as data is defensible; listing it for completeness." } ], "insights": [ "Packet protocol (build 2 only, $E7F7-$EAA0). On top of the raw CIA2 serial link the T34 comm module runs a small ARQ protocol. A packet is [length code][control byte][payload 0..15][checksum]. The length code is one of 16 values from packetLengthCodeTable ($EAA1) whose bit pairs are all 01 or 10, so it can never be mistaken for the raw sync bytes $00/$FF. The control byte carries the original game command in bits 4-7 and a 2-bit sequence number in bits 2-3; bits 0-3 are refilled with the payload length on delivery, restoring the game's command byte exactly. The checksum ($E937) seeds A with $EE and does ASL / ADC #0 / ADC byte / ADC #0 over every packet byte. Raw $FF means 'acknowledge me', raw $00 means 'resend', control byte $EC is a bare ACK, control bytes $E0-$EF are ACKs generally and >= $F0 is a drive pass-through.", "Link state machine. sub_E756 (called every tick from the $E000 jump-table entry with X=0) drives commState/$E040: 0 = idle, 1 = terminal/modem-command mode (characters flow between the game and the modem through the two small character rings at $E086 and $E091, each converted with EOR #$80), 2 = byte sync (both ends spam $00/$FF until ten rounds have passed or the peer's $FF is seen), 3 = packet protocol. Sixteen consecutive service ticks without progress ($EB00) or two consecutive received $00 bytes force a return to state 2.", "Modem command strings are stored BACK TO FRONT. sub_E393 copies a NUL-terminated string from $EBxx into the queue at $E092 and sets the read index to the string length; the reader sub_E0C3 walks the index downwards, so the bytes come out in reverse. Characters with bit 7 set are not text but guard-time delays loaded into $EB01 ($A0 = 96 ticks, $C0 = 64). $EBD6 therefore emits ' +++ ' with a one-second guard on each side followed by 'ATH0' (hang up), $EBE3 emits 'ATQ0V1X1A' (answer) and $EBF0 'ATQ0V1X1D' (dial); $E011 (0 = answer after key 'A', 1 = originate after key 'O') picks between the last two through the pointer bytes at $EBFE.", "Downloadable code channel. In build 2 only, any received message whose command byte is >= $70 has its payload appended to $EB03 (self-modified index at $E1AE) and, when such a message arrives with an empty payload, $EB03 is called. The shipped image has RTS there, so the peer can effectively upload and run 6502 code on the other machine. Build 1 (T35) has no such call; instead $EB67 there is ordinary live game code.", "CORRECTED 2026-08-22: the packet-building path at $E8A8 IS reachable. The original claim here was that $E0A6 is only ever written with 0 (at $E778); that came from a byte-level scan which missed the INDEXED store at $E265, 'sta uartPendingCount,x' with X = inFlightSlotIndex $E0A8 = 1, whose effective address is $E0A6. sendPacketFsm sets it there and reads it back at $E1EC and $E26E, and the listing's own comments at $E262/$E26B already said the in-flight counter lives at $E0A6. Consequence: $E0A5 and $E0A6 must stay adjacent in that order, and the message-builder branch at $E8A8 is live. The rest of the original observation still holds: $EAC1 (the raw-$FF sender at $E872) really is never written - no direct store and no indexed store reaches it - and outgoing game traffic does go through the engine at $E1DD, which sub_E3F3 re-points at the packet rings $E447/$E45C by rewriting the indirect-JMP operands at $E3B7/$E3BA/$E3BD.", "Buffer map of the comm module: $E086 (8) game-visible receive chars, $E091 (16) chars to send to the modem, $E417 (22) UART transmit ring, $E430 (20) UART receive ring, $E447 (18) game -> packet layer messages, $E45C (18) packet layer -> game messages, $EAC7 (35) retransmit ring. All of them are descending circular buffers (index decremented, wrapping to capacity-1) with a separate count byte.", "$EC00-$EFFF in this file is the 'comm tail' loaded from track 34 sectors 12-15 at start-up by $1070/loadCommTailBuild2, and it is not really comm code at all - it holds map and unit helpers used by overlay A: clearBattlefieldMap ($EC00), setViewOriginToHome ($EC1C), loadMapSeedIntoScenarioRng ($EC2B), makeMapPointSymmetric/rotateMapAndMirrorCodes ($EC54/$EC58), mirrorAllUnitCoordinates ($EC91), mirrorTerrainCode ($ECD4), loadUnitStartTemplate ($ECF3), the link handshake ($ED43), the recycler finaliser ($EE26), the film-snapshot loader ($EE4A) and the trainer display toggle ($EEB9), plus a block of message text at $EF08-$EFF4.", "Handshake and player names. $ED43 exchanges the ten-byte owner name that lives at $FFDE (read from the disk BAM) in two five-byte messages and stores the peer's name at $FFE8; a blank peer name is replaced with 'ENEMY' ($CC99) and, in solo trainer mode ($0BA5 bit 7), the whole exchange is skipped and 'TRAINER ' ($884E) is used. A four-byte settings message then swaps the comm build id ($E030), the first byte of the settings block ($0B94) and $0B7E. Any length or magic mismatch beeps, waits a second and drops back to the main menu ($EDFC).", "Map mirroring. The map cell alphabet has direction-dependent codes; mirrorTerrainCode ($ECD4) pairs them (A<->D, E<->H, I<->K, M<->T, N<->Q, O<->S, P<->R, U<->X, V<->W) so the battlefield can be rotated 180 degrees. rotateMapAndMirrorCodes and mirrorAllUnitCoordinates together let both players see themselves at the bottom of the screen; the same table also drives the map generator's symmetry pass ($EC54) so the two halves of a generated map are exactly fair.", "The trainer-only F1 toggle: $92FC bit 7 = 'beginner battle display' (all enemy units drawn, tested by renderMapTile at $284F). $EEB9 is polled from the main idle loop at $600A but only when $0BA5 bit 7 is set, fires when zp_66 (currentKey) is 0, announces the mode being entered ('BEGINNER/STANDARD DISPLAY SET.') and rewrites the prompt at $EF10 so it always advertises the other mode ('F 1 FOR xxxxxxxx BATTLE DISPLAY.')." ] }