803 lines
No EOL
45 KiB
JSON
Generated
803 lines
No EOL
45 KiB
JSON
Generated
{
|
|
"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 <delay $A0> '+' '+' '+' <delay $A0> CR <delay $C0> '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.')."
|
|
]
|
|
} |