{
 "_comment": "generated by tools/mergeSurvey.py from survey/*.json - do not edit by hand",
 "notes": {
  "E000": {
   "routine": [
    "commRequest - Jump-table entry 0 (JMP L_E111). The game's per-frame/protocol request call: X selects the function (0 = service tick, 1 = request that the outgoing packet be sent, >=2 = acknowledge/consume a completed exchange). Called from the raster IRQ ($1129 with X=0) and from the packet-exchange code in main and in the $EC00 sub-overlay.",
    "In:  X = function code",
    "Out: see L_E111; updates $E01D exchange flags"
   ],
   "unit": "game/modemDriverE000",
   "block": [
    "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."
   ]
  },
  "E003": {
   "routine": [
    "commLinkControl - Jump-table entry 1 (JMP L_E29A). Link life-cycle control: X=0 open the link, X=1 cold-initialise the module, X=2 shut the UART down, X>=3 hang up then shut down. Called from $0F46 (X=1, right after the module is loaded), $1B70 (X=0, openCommLink) and $1B56 (X=3, hangUpModemSetState).",
    "In:  X = function code",
    "Out: see L_E29A"
   ],
   "unit": "game/modemDriverE000"
  },
  "E006": {
   "routine": [
    "getLinkByte - Jump-table entry 2 (JMP L_E0A9). Pops one byte that arrived from the opponent (or was echoed by the modem in terminal mode) out of the 8-byte host-input ring at $E086. Used by receiveChatCharacter ($1399).",
    "In:  none",
    "Out: A = byte (bit 7 inverted back), C=0 if a byte was returned, C=1 if the ring was empty"
   ],
   "unit": "game/modemDriverE000"
  },
  "E009": {
   "routine": [
    "putLinkByte - Jump-table entry 3 (JMP L_E0F6). Pushes one byte into the 16-byte host-output ring at $E091 for transmission to the opponent (chat characters); the byte is stored EOR $80. Reached by JMP from handleChatSend ($1396).",
    "In:  A = byte to send",
    "Out: $E091 ring, $E02E count; silently drops the byte when the ring already holds 16"
   ],
   "unit": "game/modemDriverE000"
  },
  "E00C": {
   "routine": [
    "pollLinkStatus - Jump-table entry 4 (JMP L_E3CB). Called once per raster IRQ from $112C: services the NMI suspend/resume handshake and samples + debounces the modem carrier-detect line.",
    "In:  $E039, $E03A, $E03B, CIA2_PRB",
    "Out: $E03C (debounced carrier), $E03D (raw sample), $E043 (debounce counter)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E012": {
   "routine": [
    "keyboardScanHook - Jump-table entry 5. In the shipped image it is JMP L_E3B2 (= return A=$FF, no key), but the main program patches the operand bytes $E013/$E014 at $0F4B/$0F50 to $0DB7 = scanKeyboard, so at run time this entry is the keyboard matrix scanner. Called only by readKeyAndHandleModemHotkeys.",
    "In:  none",
    "Out: A = key code ($FF = none), Y = $80 if SHIFT held, X = $80 if the Commodore key is held"
   ],
   "unit": "game/modemDriverE000"
  },
  "E015": {
   "routine": [
    "commKeyEntry - Jump-table entry 6 (JMP L_E2EC). The game's only keyboard read: called every frame by pollKeyboardEvent ($0E56). Runs the keyboard scan through the comm module so that Commodore-key hot keys can control the modem before the game sees them.",
    "In:  none",
    "Out: A = key code or $FF when the key was swallowed as a modem hot key"
   ],
   "unit": "game/modemDriverE000"
  },
  "E018": {
   "routine": [
    "clearPacketVars - LDX #$13 / JSR clearInlineVarBlock followed by the 19 bytes it clears. Zeroes the packet-exchange variables $E01D-$E02F (exchange flags, rx/tx lengths, both 7-byte packet buffers, host-output count). Because clearInlineVarBlock discards the return address, control returns to this routine's caller. Called from initCommModule ($E2B4).",
    "In:  none",
    "Out: $E01D-$E02F = 0"
   ],
   "unit": "game/modemDriverE000"
  },
  "E034": {
   "routine": [
    "clearLinkVars - LDX #$0F / JSR clearInlineVarBlock followed by the 15 bytes it clears. Zeroes the link-state variables $E039-$E047 (NMI suspend request/ack, link-active flag, carrier state, modem command phase, connection phase, error counter). Entered by JMP from L_E2AE, i.e. it is the tail of the 'close the link' path.",
    "In:  none",
    "Out: $E039-$E047 = 0; returns to the caller of the routine that jumped here"
   ],
   "unit": "game/modemDriverE000"
  },
  "E067": {
   "routine": [
    "clearInlineVarBlock - Self-modifying helper used five times in this module. It pulls the return address off the stack, patches it into the STA abs,X at $E071 and then zeroes X bytes starting one byte past the JSR, i.e. the inline data block that immediately follows the call. Since the return address was consumed, the RTS returns to the caller's caller.",
    "In:  X = number of bytes to clear; return address on the stack points at the inline block",
    "Out: inline block zeroed; A=0, X=0; returns two levels up"
   ],
   "unit": "game/modemDriverE000"
  },
  "E078": {
   "routine": [
    "clearProtocolVars - LDX #$2B / JSR clearInlineVarBlock followed by the 43 bytes it clears ($E07D-$E0A7): exchange counter, packet rx/tx state machines, both host rings and their indices, the in-flight counters and the service lock. $E0A8 is deliberately left alone. Called from initCommModule ($E2B1).",
    "In:  none",
    "Out: $E07D-$E0A7 = 0"
   ],
   "unit": "game/modemDriverE000"
  },
  "E0A9": {
   "routine": [
    "popHostInRing - Implementation of the $E006 entry: if $E0A1 (count) is zero return C=1; otherwise read $E086,X with X = read index $E0A2, EOR #$80, decrement the index (wrapping 0 -> 7) and the count, return C=0.",
    "In:  $E0A1, $E0A2, $E086-$E08D",
    "Out: A = byte, C = 0/1, $E0A1, $E0A2 updated"
   ],
   "unit": "game/modemDriverE000"
  },
  "E0C3": {
   "routine": [
    "popHostOutRing - Pops one byte out of the 16-byte host-output ring at $E091 (count $E02E, read index $E08F, index decrements and wraps 0 -> 15). Used to feed the packet transmitter, to drain the ring when the link drops, and to feed the modem transmitter in terminal mode.",
    "In:  $E02E, $E08F, $E091-$E0A0",
    "Out: A = byte, C=0, or C=1 when empty"
   ],
   "unit": "game/modemDriverE000"
  },
  "E0DD": {
   "routine": [
    "pushHostInRing - Pushes A into the 8-byte host-input ring at $E086 (count $E0A1, write index $E0A3). Silently drops the byte when the ring already holds 8. Used by the packet receiver for incoming chat bytes and by the modem terminal loop to echo modem responses to the screen.",
    "In:  A = byte",
    "Out: $E086 ring, $E0A1, $E0A3"
   ],
   "unit": "game/modemDriverE000"
  },
  "E0F6": {
   "routine": [
    "pushHostOutRing - Implementation of the $E009 entry: if the ring already holds 16 bytes do nothing, else store A EOR $80 at $E091,X with X = write index $E090, decrement/wrap the index and bump $E02E.",
    "In:  A = byte",
    "Out: $E091 ring, $E02E, $E090"
   ],
   "unit": "game/modemDriverE000"
  },
  "E111": {
   "routine": [
    "commRequestDispatch - Dispatcher behind $E000. X=0 -> serviceCommTick; X=1 -> requestPacketSend; X>=2 -> if $E01D bit 6 (exchange complete) is set, clear $E01E, $E01F and $E01D, i.e. acknowledge and consume the finished exchange.",
    "In:  X = function code, $E01D",
    "Out: see the three cases"
   ],
   "unit": "game/modemDriverE000"
  },
  "E128": {
   "routine": [
    "requestPacketSend - $E000 with X=1: increments $E01D (setting the low 'send requested' bits) and increments $E085, the count of outstanding send requests. The packet transmitter picks the request up on the next service tick.",
    "In:  none",
    "Out: $E01D +1, $E085 +1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E12F": {
   "routine": [
    "serviceCommTick - $E000 with X=0, called from the raster IRQ every frame. DEC/INC of $E0A7 forms a re-entrancy lock (the body only runs when $E0A7 is 1). It always runs runModemStateMachine, and when the connection has reached phase 3 and the link is active it also runs the packet receive and packet transmit state machines.",
    "In:  $E0A7, $E040, $E03B",
    "Out: drives the whole protocol stack; $E0A7 restored to 1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E14D": {
   "routine": [
    "receivePacketFsm - Packet-layer receiver driven by $E081. State 0 reads a header byte through receiveLinkByte: the low 3 bits are the payload length, bit 7 means the payload is chat/text bytes that go straight into the host-input ring, otherwise the payload is a command packet stored at $E020 with the length in $E01E. Header values $70-$7F are a special case: the payload is copied into the scratch buffer at $EB03 and a zero-length $7x packet does JSR $EB03. A completed packet sets bit 7 of $E01D through setExchangeFlags.",
    "In:  $E081, $E082, $E080, receiveLinkByte",
    "Out: $E020-$E026, $E01E, $E01D bit 7, host-input ring, $EB03 buffer",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E170": {
   "routine": [
    "beginReceivePacket - Mid-routine entry of receivePacketFsm reached only by the T35 build ($E07E): clears $E01E and adds 2 to $E01D (marking 'a packet is being received'), then falls into the body that stores the payload.",
    "In:  $E082",
    "Out: $E01E = 0, $E01D +2",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E1DD": {
   "routine": [
    "sendPacketFsm - Packet-layer transmitter driven by $E083. When nothing is in flight it either sends the pending command packet (header = $E01F | $E07F, then $E01F bytes from $E027) or, when the host-output ring holds chat bytes, a chat frame (header $80 | count, up to 3 bytes popped with popHostOutRing). Once the transmitter has drained it marks the send complete with bit 5 of $E01D and clears $E083.",
    "In:  $E083, $E085, $E02E, $E01F, $E027-$E02D, $E0A6",
    "Out: bytes handed to sendLinkByte, $E0A6 = bytes+1, $E01D bits 3/5, $E084, $E08E",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E283": {
   "routine": [
    "countCompletedExchange - Increments the 16-bit counter $E07D/$E07E and then sets bit 6 of $E01D. Reached from setExchangeFlags whenever bits 7 and 5 are both set but bit 6 is not, i.e. exactly once per completed send+receive exchange. The counter itself is never read anywhere in the image, so it looks like a leftover statistic.",
    "In:  $E07D/$E07E",
    "Out: $E07D/$E07E +1, $E01D bit 6 set",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E28D": {
   "routine": [
    "setExchangeFlags - ORs A into the exchange flag byte $E01D; if the result has bits 7 and 5 set and bit 6 clear (mask $E0 == $A0) the exchange is complete, so it falls into countCompletedExchange which sets bit 6 as well. A = $80 marks 'opponent packet received', $20 marks 'own packet sent', $40 marks 'complete', $08 is used as an idle/keep-alive marker.",
    "In:  A = flag bits",
    "Out: $E01D updated, possibly $E07D/$E07E"
   ],
   "unit": "game/modemDriverE000"
  },
  "E29A": {
   "routine": [
    "commLinkControlDispatch - Dispatcher behind $E003. X=0 -> openCommLink; X=1 -> initCommModule; X=2 -> stopCommNmi + clearLinkVars; X>=3 -> hangUpModem, spin until $E03F <= 1, then stopCommNmi + clearLinkVars.",
    "In:  X",
    "Out: see cases; ends by clearing $E039-$E047"
   ],
   "unit": "game/modemDriverE000"
  },
  "E2B1": {
   "routine": [
    "initCommModule_E2B1 - $E003 with X=1, called from $0F46 immediately after the comm module has been loaded. Clears the protocol vars, the packet vars, the UART/ARQ ring vars and the ARQ state block, and switches the three link I/O vectors to the framed (ARQ) implementations via selectFramedLinkVectors.",
    "In:  none",
    "Out: $E07D-$E0A7, $E01D-$E02F, $E414-$E46D, $EAB7-$EB02 zeroed; $E3B7/$E3BA/$E3BD patched"
   ],
   "unit": "game/modemDriverE000"
  },
  "E2BD": {
   "routine": [
    "clearArqVars - Tail of initCommModule: JSR $EAB2 (which clears $EAB7-$EB02, the frame/ARQ layer state) and RTS.",
    "In:  none",
    "Out: $EAB7-$EB02 = 0"
   ],
   "unit": "game/modemDriverE000"
  },
  "E2C1": {
   "routine": [
    "openCommLink_E2C1 - $E003 with X=0, called from openCommLink ($1B70) in main. If the link is not already up it reloads the baud parameters for the current baud index, installs the NMI vector at $FFFA/$FFFB, restarts the UART and sets $E03B. It then always configures the user-port lines and, unless $E041 says otherwise, queues the Hayes init string selected by $E011 (answer: 'ATQ0V1X1A', originate: 'ATQ0V1X1D'), and finally releases the service lock ($E0A7 = 1).",
    "In:  $E03B, $E055, $E041, $E011",
    "Out: $E03B = 1, NMI vector, CIA2 configured, modem command queued, $E0A7 = 1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E2EC": {
   "routine": [
    "readKeyAndHandleModemHotkeys - Implementation of $E015. Calls the patched keyboard hook, optionally paints the border with the link status colour from borderColourTable (indexed by connectionPhase*2 + carrier), and then, when the Commodore key is held (X bit 7) and a new key code with bit 7 set arrives, interprets it as a modem hot key: $C8 'H' toggles DTR/PB5 via the mask $E04C, $C3 'C' toggles the carrier-override bit in $E04A, $B3 '3' selects 300 baud, $B1 '1' selects 1200 baud, $8D RETURN re-opens the link without re-sending the init string, $D0 'P' hangs up, $C1 'A' selects answer mode and $CF 'O' selects originate mode. A handled key is swallowed by returning $FF.",
    "In:  keyboard hook result (A/X/Y), $E03B, $E03C, $E040, $E046",
    "Out: A = key code or $FF, VIC_BORDER, $E046, $E011, $E055, $E04A, CIA2_PRB, modem command queued",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E353": {
   "routine": [
    "loadBaudParameters - Copies the 3-byte entry at $E059+X into $E056-$E058 (bit-period low, bit-period high, transmit pacing divisor) using the trick 'LDY #$FD / STA $DF59,Y' so that Y=$FD..$FF addresses $E056..$E058. X=0 selects {$50,$0D,$01} = $0D50 cycles = 300 baud, X=3 selects {$53,$03,$02} = $0353 cycles = 1200 baud. Returns A=$FF (so the caller in the hot-key dispatcher swallows the key).",
    "In:  X = 0 or 3",
    "Out: $E056-$E058, A=$FF"
   ],
   "unit": "game/modemDriverE000"
  },
  "E38D": {
   "routine": [
    "hangUpModem_E38D - Drops DTR (dropDtrLine writes $22 to CIA2_PRB) and then queues the string at $EB00+$EBFD ($EBD6), which the transmitter emits in reverse as [pause] '+++' [pause] CR [pause] 'ATH0' CR - the Hayes escape sequence with guard times followed by the hang-up command. Reached by $E003 X>=3 and by the 'P' hot key.",
    "In:  $EBFD, $E048 bit 7",
    "Out: CIA2_PRB, host-output ring loaded with the hang-up string, $E03F = $FF"
   ],
   "unit": "game/modemDriverE000"
  },
  "E393": {
   "routine": [
    "queueModemCommandString - Loads a canned modem command string into the host-output ring. A is the low byte of a string address in page $EB; the routine patches the LDA $EB00,X at $E3A1, copies bytes until it copies a zero, and then sets the ring write index to 0, the read index and the count to the number of bytes copied. Because the ring pops with a decrementing index, the string is transmitted in reverse of how it is stored, which is why the AT strings in the $EBD6-$EBFC table are written backwards. Does nothing (returns A=$FF) when bit 7 of the modem option byte $E048 is clear.",
    "In:  A = offset in page $EB, $E048",
    "Out: $E092.., $E08F, $E090, $E02E, $E03F = $FF, A = $FF"
   ],
   "unit": "game/modemDriverE000"
  },
  "E3B2": {
   "routine": [
    "returnNoKey - LDA #$FF / RTS - the 'no key / handled' return shared by the hot-key dispatcher and by queueModemCommandString when no modem is configured. Also the unpatched target of the $E012 jump-table entry.",
    "In:  none",
    "Out: A = $FF"
   ],
   "unit": "game/modemDriverE000"
  },
  "E3B6": {
   "routine": [
    "sendLinkByte - JMP ($E3BF) - indirect 'send one byte to the link'. The operand byte at $E3B7 selects between $E3BF (raw: push into the UART transmit ring) and $E3C1 (framed: push into the ARQ transmit ring). initCommModule always selects the framed variant.",
    "In:  A = byte",
    "Out: C=1 when the target ring is full"
   ],
   "unit": "game/modemDriverE000"
  },
  "E3B9": {
   "routine": [
    "receiveLinkByte - JMP ($E3C3) - indirect 'get one byte from the link'. The operand at $E3BA selects $E3C3 (raw: pop the UART receive ring) or $E3C5 (framed: pop the ARQ receive ring).",
    "In:  none",
    "Out: A = byte, C=0, or C=1 when empty"
   ],
   "unit": "game/modemDriverE000"
  },
  "E3BC": {
   "routine": [
    "reportLinkError - JMP ($E3C7) - 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 $E3C7 (no-op stub at $E408) or $E3C9 (count the error at $E403). initCommModule selects the counting variant.",
    "In:  none",
    "Out: $E047 incremented (framed variant)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E3CB": {
   "routine": [
    "pollCarrierState - Implementation of the $E00C entry, run once per raster IRQ. Calls serviceCarrierAndSuspendRequest to take a fresh carrier sample into $E03D, then debounces: while the sample differs from the accepted state $E03C it counts up in $E043 and only accepts the new state after $F0 (240) consecutive differing samples; otherwise it merges only bit 7 of the sample into $E03C. Bit 6 of $E03C is the 'carrier present' flag the game tests.",
    "In:  $E03D, $E03C, $E043",
    "Out: $E03C, $E043"
   ],
   "unit": "game/modemDriverE000"
  },
  "E3F3": {
   "routine": [
    "selectFramedLinkVectors - Patches the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD to $C1/$C5/$C9, moving sendLinkByte, receiveLinkByte and reportLinkError from the raw-UART implementations to the framed (ARQ) implementations and the error counter. Called once from initCommModule.",
    "In:  none",
    "Out: $E3B7, $E3BA, $E3BD"
   ],
   "unit": "game/modemDriverE000"
  },
  "E403": {
   "routine": [
    "countLinkError - NOP / INC $E047 / RTS - the framed link error hook: bumps the link error counter that runModemStateMachine inspects at $E7FA. Currently rendered as .byte data by the disassembler.",
    "In:  none",
    "Out: $E047 +1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E408": {
   "routine": [
    "ignoreLinkError - PHP / PHA / NOP / PLA / PLP / RTS - the raw-mode link error hook, a no-op that preserves A and the flags. Followed by a stray RTS at $E40E. Currently rendered as .byte data.",
    "In:  none",
    "Out: none"
   ],
   "unit": "game/modemDriverE000"
  },
  "E40F": {
   "routine": [
    "clearUartVars - LDX #$5A / JSR clearInlineVarBlock followed by the 90 bytes it clears ($E414-$E46D): the UART transmit ring (22), the UART receive ring (20), the ARQ transmit ring (18) and the ARQ receive ring (18) together with their counts and read/write indices. Called from initCommModule.",
    "In:  none",
    "Out: $E414-$E46D = 0"
   ],
   "unit": "game/modemDriverE000"
  },
  "E46E": {
   "routine": [
    "popArqRxRing - Pops one byte from the 18-byte ARQ receive ring at $E45C (count $E459, read index $E45A, index decrements and wraps 0 -> 17); C=1 when empty. This is the 'framed' target of receiveLinkByte, so it is how the packet layer gets the payload the frame layer has validated. Currently rendered as .byte data by the disassembler.",
    "In:  $E459, $E45A, $E45C-$E46D",
    "Out: A = byte, C = 0/1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E486": {
   "routine": [
    "popArqTxRing - Pops one byte from the 18-byte ARQ transmit ring at $E447 (count $E444, read index $E445). Reached through the thunk at $E733 from the frame builder at $E8AE/$E8D4, i.e. this is where the frame layer takes the bytes the packet layer queued.",
    "In:  $E444, $E445, $E447-$E458",
    "Out: A = byte, C = 0/1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E49E": {
   "routine": [
    "pushArqRxRing - Pushes A into the 18-byte ARQ receive ring at $E45C (count $E459, write index $E45B); drops the byte when the ring is full. Reached through the thunk at $E736 from the frame receiver at $EA91.",
    "In:  A",
    "Out: $E45C ring, $E459, $E45B"
   ],
   "unit": "game/modemDriverE000"
  },
  "E4B7": {
   "routine": [
    "pushArqTxRing - Pushes A into the 18-byte ARQ transmit ring at $E447 (count $E444, write index $E446); returns with the ring unchanged when it already holds 18. This is the 'framed' target of sendLinkByte. Currently rendered as .byte data by the disassembler.",
    "In:  A",
    "Out: $E447 ring, $E444, $E446"
   ],
   "unit": "game/modemDriverE000"
  },
  "E4D0": {
   "routine": [
    "returnRingEmpty - SEC / RTS - the shared 'ring empty' exit used by four of the ring pop routines in the $E486-$E51B group.",
    "In:  none",
    "Out: C = 1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E4D2": {
   "routine": [
    "popUartRxRing - Pops one byte from the 20-byte raw UART receive ring at $E430 (count $E42D, read index $E42E). Used directly by the modem terminal loop ($E799, $E80A), through the thunk at $E739, and as the raw target of receiveLinkByte.",
    "In:  $E42D, $E42E, $E430-$E443",
    "Out: A = byte, C = 0/1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E4EA": {
   "routine": [
    "popUartTxRing - Pops one byte from the 22-byte raw UART transmit ring at $E417 (count $E414, read index $E415). Called by the NMI transmitter (startNextTxChar) and by flushUartTxRing.",
    "In:  $E414, $E415, $E417-$E42C",
    "Out: A = byte, C = 0/1"
   ],
   "unit": "game/modemDriverE000"
  },
  "E502": {
   "routine": [
    "pushUartRxRing - Pushes A into the 20-byte raw UART receive ring at $E430 (count $E42D, write index $E42F); returns C untouched and leaves the ring alone when full, which the NMI receiver treats as an overflow error. Called only from the NMI at $E71F.",
    "In:  A = received character",
    "Out: $E430 ring, $E42D, $E42F"
   ],
   "unit": "game/modemDriverE000"
  },
  "E51B": {
   "routine": [
    "pushUartTxRing - Pushes A into the 22-byte raw UART transmit ring at $E417 (count $E414, write index $E416); drops the byte when full. Called from queueByteForTransmit and, in raw mode, from sendLinkByte.",
    "In:  A",
    "Out: $E417 ring, $E414, $E416"
   ],
   "unit": "game/modemDriverE000"
  },
  "E534": {
   "routine": [
    "flushUartTxRing - Drains the raw UART transmit ring by popping until it is empty, then sets the pending-byte count $E0A5 to the current transmitter-busy flag $E5BB. Used when the connection state changes so that stale bytes are not sent.",
    "In:  $E414 ring, $E5BB",
    "Out: transmit ring emptied, $E0A5"
   ],
   "unit": "game/modemDriverE000"
  },
  "E540": {
   "routine": [
    "configureUserPortLines - Sets up CIA2 for the user-port serial link: TA latch $0003 and CRA = $51 (TA running, serial port in output mode), writes the pattern from $E051 indexed by ((3 AND $E011) EOR $E055) into CIA2_SDR, sets CIA2_DDRB to $26 (PB1/PB2/PB5 outputs = RTS, DTR and one spare), drives CIA2_PRB with $E04F,X ($26 = RTS and DTR asserted) and finally raises PA2 (TXD) to the mark level. The purpose of the CIA2_SDR write is unclear - SP2 is a user-port pin, so it may drive an external handshake line, or it may be vestigial.",
    "In:  $E011, $E055, $E04D, $E04F, $E051",
    "Out: CIA2_CRA, CIA2_TA, CIA2_SDR, CIA2_DDRB, CIA2_PRB, CIA2_PRA",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E574": {
   "routine": [
    "dropDtrLine - Writes $E04E ($22) to CIA2_PRB, which clears PB2 = DTR while leaving PB1/PB5 high. On a Hayes modem dropping DTR terminates the call. Called only from hangUpModem.",
    "In:  $E04E",
    "Out: CIA2_PRB"
   ],
   "unit": "game/modemDriverE000"
  },
  "E57B": {
   "routine": [
    "installCommNmiVector - Writes $E685 into the RAM NMI vector at $FFFA/$FFFB so that CIA2 interrupts enter this module's bit-bang UART handler. Called from openCommLink.",
    "In:  none",
    "Out: $FFFA/$FFFB = $E685"
   ],
   "unit": "game/modemDriverE000"
  },
  "E586": {
   "routine": [
    "restartUart - Clears the six bit-level UART state bytes ($E5BB-$E5C0), sets $E042 = 1 (ask the modem state machine to re-evaluate), primes the carrier poll timer $E045 with $92 and falls into setCiaNmiMask with A=$92, which enables the CIA2 FLAG (start bit) and timer B (transmit) NMI sources and marks the module as running.",
    "In:  none",
    "Out: $E5BB-$E5C0 = 0, $E042 = 1, $E045 = $92, CIA2 ICR/CRA/CRB, $E033, $E039, $E03A = 0"
   ],
   "unit": "game/modemDriverE000"
  },
  "E593": {
   "routine": [
    "setCiaNmiMask - Stores A into the ICR shadow $E033, acknowledges pending CIA2 interrupts, writes the new mask to CIA2_ICR, stops both timers, and derives the suspend flags: A EOR $FF AND $80 gives $00 for an enable mask ($92 or $83) and $80 for the disable mask ($7F), which is written to both $E039 and $E03A. Ends by jumping to the 'raise TXD' tail at $E56B.",
    "In:  A = CIA2 ICR command byte",
    "Out: $E033, CIA2_ICR/CRA/CRB, $E039, $E03A, CIA2_PRA bit 2"
   ],
   "unit": "game/modemDriverE000"
  },
  "E5B6": {
   "routine": [
    "clearUartState - LDX #$06 / JSR clearInlineVarBlock followed by the 6 bytes it clears ($E5BB-$E5C0): transmitter-active flag, bit-count reload, transmit bit counter, transmit shift register, receive bit counter and receive shift register.",
    "In:  none",
    "Out: $E5BB-$E5C0 = 0"
   ],
   "unit": "game/modemDriverE000"
  },
  "E5C3": {
   "routine": [
    "stopCommNmi - Sets $E042 = $FF (ask the modem state machine to fall back to phase 0), calls setCiaNmiMask with $7F to disable every CIA2 NMI source (which sets $E039/$E03A = $80 = suspended) and, if a character was still being transmitted, decrements the pending count $E0A5.",
    "In:  $E5BB",
    "Out: CIA2 interrupts off, $E039/$E03A = $80, $E042 = $FF, $E0A5"
   ],
   "unit": "game/modemDriverE000"
  },
  "E5D6": {
   "routine": [
    "serviceCarrierAndSuspendRequest - First half of the $E00C service. Handles the suspend handshake used by the disk loader: $E039 bit 7 set means 'suspend' and calls stopCommNmi unless $E03A already acknowledges it, $E039 clear while $E03A still says suspended calls restartUart. Then, when the link is active, it samples carrier detect - CIA2_PRB bit 4 (DCD), XORed with the polarity/override byte $E04A - and stores $F8 (carrier) or $80 (no carrier) into $E03D. $E045 is a free-running counter that periodically re-arms the sample.",
    "In:  $E039, $E03A, $E03B, $E04A, CIA2_PRB, $E045",
    "Out: $E03D, $E045, NMI enabled/disabled",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E5FF": {
   "routine": [
    "rearmCarrierTimer - Continuation inside serviceCarrierAndSuspendRequest (also called directly by the T35 build): stores the current carrier sample into $E045 and clears bit 7 of the value that goes into $E03D, marking the sample as 'not yet stable'.",
    "In:  A",
    "Out: $E045, A",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E607": {
   "routine": [
    "startNextTxChar - Starts the transmission of the next character. Returns immediately when timer B is already running or a character is still in flight; outside terminal mode it also paces characters with the $E044 counter and requires carrier. It then pops a byte from the UART transmit ring into the shift register $E5BE, sets $E5BB, the 9-bit count $E5BC, reloads the pacing counter from $E058, programs timer B with a short $0150 initial delay and starts it (CRB = $11), and finally falls through into setBitPeriod which reloads the TB latch with the full bit period so every subsequent underflow is one bit time.",
    "In:  $E040, $E044, $E03C, $E03D, $E0A5, transmit ring",
    "Out: $E5BB, $E5BE, $E5BC, $E044, CIA2_TB, CIA2_CRB"
   ],
   "unit": "game/modemDriverE000"
  },
  "E654": {
   "routine": [
    "setBitPeriodFull - LDA $E057 then falls into setBitPeriod: programs the timer selected by X (X=0 timer A, X=2 timer B) with the full bit period $E057:$E056.",
    "In:  X = 0 or 2",
    "Out: CIA2 TA or TB latch, A = $11"
   ],
   "unit": "game/modemDriverE000"
  },
  "E657": {
   "routine": [
    "setBitPeriod - Writes A into CIA2_TA_HI+X and $E056 into CIA2_TA_LO+X, then loads A with $11 (start + force load) for the caller to store into the corresponding control register. X=0 addresses timer A (receive clock), X=2 timer B (transmit clock).",
    "In:  A = high byte, X = 0 or 2, $E056",
    "Out: CIA2 timer latch, A = $11"
   ],
   "unit": "game/modemDriverE000"
  },
  "E663": {
   "routine": [
    "nmiStartNextChar - Timer B path of the NMI when the bit counter has run out. If a character is active it reloads the bit counter from $E5BC (9) and drives TXD low to emit the start bit; otherwise it calls startNextTxChar to fetch the next byte.",
    "In:  $E5BB, $E5BC, CIA2_PRA",
    "Out: $E5BD, CIA2_PRA bit 2"
   ],
   "unit": "game/modemDriverE000"
  },
  "E685": {
   "routine": [
    "commNmiHandler - The module's CIA2 NMI handler (installed at $FFFA/$FFFB by installCommNmiVector; the disassembly leaves this address unlabelled). It saves A and X, masks CIA2 interrupts, reads the ICR (with a timer-B race workaround around CIA2_TB_HI) and, when the interrupt is not from CIA2, restores and chains through JMP ($E031) to the game's NMI handler. Timer B drives the software transmitter one bit at a time out of $E5BE onto PA2 (TXD); the CIA2 FLAG line (RXD start bit) arms timer A at half a bit time and then a full bit time, and each timer A interrupt shifts CIA2_PRB bit 0 (RXD) into $E5C0 until a whole character with a valid stop bit is pushed into the UART receive ring. Framing errors, bad stop bits and receive overflow call reportLinkError.",
    "In:  CIA2_ICR, CIA2_PRB bit 0, $E033, $E5BB-$E5C0",
    "Out: CIA2_PRA bit 2 (TXD), UART receive ring, $E0A5, $E033, $EB00, $E045; RTI or chain to ($E031)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E69C": {
   "routine": [
    "nmiMergeIcrFlags - Two-byte continuation inside commNmiHandler (ORA CIA2_ICR) used to merge any interrupt flags that arrived between the two ICR reads.",
    "In:  A",
    "Out: A",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E733": {
   "routine": [
    "popArqTxRingThunk - JMP popArqTxRing - the entry the frame layer at $E8AE/$E8D4 uses to fetch payload bytes.",
    "In:  none",
    "Out: A, C"
   ],
   "unit": "game/modemDriverE000"
  },
  "E736": {
   "routine": [
    "pushArqRxRingThunk - JMP pushArqRxRing - the entry the frame layer at $EA91 uses to deliver a validated payload byte to the packet layer.",
    "In:  A",
    "Out: ring updated"
   ],
   "unit": "game/modemDriverE000"
  },
  "E739": {
   "routine": [
    "popUartRxRingThunk - JMP popUartRxRing - used by receiveByteTrackZeros and by the frame receiver at $E9E9.",
    "In:  none",
    "Out: A, C"
   ],
   "unit": "game/modemDriverE000"
  },
  "E73C": {
   "routine": [
    "queueByteForTransmit - Pushes A into the raw UART transmit ring and increments the pending-byte count $E0A5, which the NMI decrements as each character finishes. Used by the frame layer and by the terminal loop.",
    "In:  A",
    "Out: transmit ring, $E0A5"
   ],
   "unit": "game/modemDriverE000"
  },
  "E743": {
   "routine": [
    "receiveByteTrackZeros - Pops one byte from the UART receive ring and maintains $EAB8, a counter of consecutive zero bytes (reset to 0 by any non-zero byte). The frame layer uses this to detect an idle or garbage line.",
    "In:  $EAB8, receive ring",
    "Out: A, C, X, $EAB8",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E756": {
   "routine": [
    "runModemStateMachine - Called from serviceCommTick every frame; drives $E040, the connection phase. $E042 requests a re-evaluation ($FF forces phase 0). Phase 0 with no carrier drains the host-output ring, flushes the transmitter and enters phase 1 (modem command/terminal mode); in phase 1 characters coming back from the modem are echoed into the host-input ring so the player sees the modem's responses, and when a verbose result line ends in '0' before CR (i.e. 'CONNECT 1200') it switches to 1200 baud. Bytes going the other way are taken from the host-output ring; a byte with bit 7 set is not sent but loaded into $EB01 as a delay countdown, which is how the guard times inside the '+++'/ATH0 strings are implemented. Once carrier is detected the routine continues past $E7C9 into the connect handshake that advances to phase 2 and then phase 3, where the packet layer runs.",
    "In:  $E042, $E040, $E03C, $E03F, $E047, $E048, $EB01, $EB02, both host rings, UART rings",
    "Out: $E040, $E03F, $E044, $E047, $E0A4, $E0A6, $EB01, $EB02, baud parameters, host-input ring",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E773": {
   "routine": [
    "clearInFlightFlags - Mid-routine entry of runModemStateMachine (also jumped to from the T35 build): clears the frame-in-flight flag $E0A4 and the packet-in-flight count $E0A6, which is what tells sendPacketFsm that the packet has actually left the transmitter.",
    "In:  X = 0",
    "Out: $E0A4 = 0, $E0A6 = 0",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E01D": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetVarBlock: byteTable, 19 bytes. Inline variable block zeroed by clearPacketVars: exchangeFlags, rx/tx packet lengths, the two 7-byte packet buffers, hostOutCount and remoteIdentityByte. The disassembler renders the first byte as BRK."
   ]
  },
  "E030": {
   "unit": "game/modemDriverE000",
   "block": [
    "commBuildAndNmiVars: byteTable, 4 bytes. commBuildId ($00 for this modem build), the two-byte nmiChainVector ($E010 in the file, patched to $1298 by the game) and the CIA2 ICR mask shadow ($90 in the file, $92/$83/$7F at run time)."
   ]
  },
  "E039": {
   "unit": "game/modemDriverE000",
   "block": [
    "linkVarBlock: byteTable, 15 bytes. Inline variable block zeroed by clearLinkVars: NMI suspend request/ack, isLinkActive, carrier state and sample, $E03E (unused), modemCommandPhase, connectionPhase, skipModemInitString, uartRestartRequest, carrierDebounceCount, txPaceCounter, carrierPollTimer, lastHotkeyCode and linkErrorCount. Its file content spells 'DTH' at $E045-$E047, which suggests the page was captured from a running machine (this page comes from track 18 sector 7, outside the encrypted area)."
   ]
  },
  "E048": {
   "unit": "game/modemDriverE000",
   "block": [
    "modemConfigTables: byteTable, 31 bytes. Constants that survive every clear: modemOptionFlags $CC, carrierOverrideFlags, the toggle/port masks $40/$24/$26/$22, the CIA2_PRB table, the CIA2_SDR pattern table, baudIndex, the live bit-period triple, the two-entry baudParameterTable and the eight-entry linkStatusBorderTable at $E05F-$E066."
   ]
  },
  "E07D": {
   "unit": "game/modemDriverE000",
   "block": [
    "protocolVarBlock: byteTable, 44 bytes. Inline variable block zeroed by clearProtocolVars ($E07D-$E0A7) plus the constant packetSlotIndex at $E0A8: the completed-exchange counter, the rx/tx packet state machines, the 8-byte host-input ring and the 16-byte host-output ring with their counts and indices, the in-flight counters and the service lock."
   ]
  },
  "E3B5": {
   "unit": "game/modemDriverE000",
   "block": [
    "padByteE3B5: unknown, 1 bytes. Stray $10 between returnNoKey and the indirect link I/O jumps; not referenced."
   ]
  },
  "E3BF": {
   "unit": "game/modemDriverE000",
   "block": [
    "linkIoVectorTable: addrTable, 12 bytes. Three pairs of alternative targets for the indirect jumps at $E3B6/$E3B9/$E3BC: send = $E51B (raw UART) or $E4B7 (framed), receive = $E4D2 (raw UART) or $E46E (framed), error = $E408 (ignore) or $E403 (count). selectFramedLinkVectors picks the second of each pair."
   ]
  },
  "E414": {
   "unit": "game/modemDriverE000",
   "block": [
    "ringBufferBlock: byteTable, 90 bytes. Inline variable block zeroed by clearUartVars ($E414-$E46D): uartTxCount/indices + 22-byte ring, uartRxCount/indices + 20-byte ring, arqTxCount/indices + 18-byte ring and arqRxCount/indices + 18-byte ring. The last two bytes of the ARQ receive ring ($E46C/$E46D) sit immediately before the popArqRxRing code."
   ]
  },
  "E5BB": {
   "unit": "game/modemDriverE000",
   "block": [
    "uartBitStateBlock: byteTable, 6 bytes. Inline variable block zeroed by clearUartState: txCharActive, txBitCountReload, txBitCounter, txShiftRegister, rxBitCounter and rxShiftRegister - the entire state of the software UART."
   ]
  },
  "E7CA": {
   "routine": [
    "sendNextModemCommandChar - 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.",
    "In:  D_EB01 delay counter, D_E03F reply timer, D_E03C bit 6 (carrier/direct-connect), outbound char queue $E091/$E02E via sub_E0C3",
    "Out: 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)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E7F7": {
   "routine": [
    "runLinkStateMachine - 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.",
    "In:  D_E040 commState, D_E047 resyncRequest",
    "Out: D_E040 advanced; branches into the sync or packet code",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E805": {
   "routine": [
    "beginByteSyncPhase - 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.",
    "In:  UART rings ($E417 tx / $E430 rx), D_EAB7 syncFfCount, D_EAB8 zeroRunLength, L_E0A5 pending tx byte count",
    "Out: 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)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E858": {
   "routine": [
    "runPacketPhase - 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.",
    "In:  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",
    "Out: 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)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E8F8": {
   "routine": [
    "buildAckPacket - 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.",
    "In:  none",
    "Out: D_EAEB = $55, D_EAEC = $EC, D_EAEA = 2, then packet sent; D_EAC0 cleared"
   ],
   "unit": "game/modemDriverE000"
  },
  "E909": {
   "routine": [
    "sendPacket - 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).",
    "In:  A = index into txMessageRing $EAC7; D_EABD sequence counter",
    "Out: D_EAEA/D_EAEB..: finished packet; bytes pushed into the UART tx ring; D_EAC0 cleared; D_EABA, D_EAC3 clobbered"
   ],
   "unit": "game/modemDriverE000"
  },
  "E937": {
   "routine": [
    "computePacketChecksum - 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).",
    "In:  D_EAEA packet length, D_EAEB.. packet bytes",
    "Out: A = checksum, X = D_EAEA (index of the check byte slot)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E94A": {
   "routine": [
    "buildPacketFromRing - 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.",
    "In:  A = ring index; D_EAC7 ring, D_EAA0 length-code table",
    "Out: D_EAEB.. packet, D_EAEA = number of bytes written, D_EAC3 advanced past the message, D_EABA clobbered"
   ],
   "unit": "game/modemDriverE000"
  },
  "E96D": {
   "routine": [
    "writeTxRingByte - Appends A to the 35-byte transmit message ring $EAC7 at index txRingIndex/$EAC3 and advances the index modulo $23.",
    "In:  A = byte, D_EAC3",
    "Out: ring updated, D_EAC3 advanced, X = new index"
   ],
   "unit": "game/modemDriverE000"
  },
  "E975": {
   "routine": [
    "readTxRingByte - Reads the byte at txRingIndex/$EAC3 from the transmit ring and advances the index (wrapping at $23).",
    "In:  D_EAC3",
    "Out: A = byte, D_EAC3 advanced, X = new index"
   ],
   "unit": "game/modemDriverE000"
  },
  "E978": {
   "routine": [
    "readTxRingByteAt - 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.",
    "In:  X = ring index",
    "Out: A = byte, D_EAC3 = X+1 mod $23, X = new index"
   ],
   "unit": "game/modemDriverE000"
  },
  "E986": {
   "routine": [
    "noteIdlePoll - 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.",
    "In:  D_EAC5, D_EAC6, D_E0A4, D_EB00",
    "Out: D_EB00 updated; on overflow the stack is unwound and control jumps to E805 (link resync)"
   ],
   "unit": "game/modemDriverE000"
  },
  "E9AA": {
   "routine": [
    "receivePacket - 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.",
    "In:  UART rx ring $E430/$E42D via sub_E739/sub_E743, D_EAB9 (resumable state), D_EAA0 table",
    "Out: D_EAB9, D_EAFF, D_EAEA, D_EAEB.., D_EABF, D_EAC0, D_EAC2, D_EB00; may resync via L_E9A2"
   ],
   "unit": "game/modemDriverE000"
  },
  "EA0D": {
   "routine": [
    "dispatchReceivedPacket - 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.",
    "In:  D_EAEB.. packet, D_EAEA length, D_EABD rxSeq, D_E459 inbound ring count, D_EAC4/D_EAC5/D_EAC6",
    "Out: 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)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EA7B": {
   "routine": [
    "deliverPacketPayload - 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).",
    "In:  D_EAEA, D_EAEB.., D_EAEC",
    "Out: bytes pushed into $E45C, D_E459 incremented, D_EAEA/D_EAFE clobbered"
   ],
   "unit": "game/modemDriverE000"
  },
  "EAA0": {
   "routine": [
    "packetReturn - 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.",
    "In:  none",
    "Out: returns; the byte $60 is also read as table entry 0 (never matched, since a length of 0 is impossible)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EAB2": {
   "routine": [
    "clearPacketState - 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).",
    "In:  none",
    "Out: $EAB7-$EB02 = 0; returns two levels up"
   ],
   "unit": "game/modemDriverE000"
  },
  "EB03": {
   "routine": [
    "remoteCodeBuffer - 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.",
    "In:  code previously written by $E1B3",
    "Out: whatever the downloaded code does",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000",
   "block": [
    "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)."
   ]
  },
  "EC00": {
   "routine": [
    "clearBattlefieldMap - 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).",
    "In:  none",
    "Out: $F000-$F63F = $40; zp_48/49 = $F640, zp_4A/4B = $F63F, zp_4E/4F advanced by 1600"
   ],
   "unit": "game/modemDriverE000"
  },
  "EC1C": {
   "routine": [
    "setViewOriginToHome - 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).",
    "In:  none",
    "Out: zp_A5 = $11, zp_A6 = $1C, D_92C3 = $11, D_92C4 = $1C",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EC2B": {
   "routine": [
    "loadMapSeedIntoScenarioRng - 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.",
    "In:  D_0B95, D_0B96, D_0B97",
    "Out: zp_5A/zp_5B/zp_5C, X = $0B97 value, Y = $0B96 value"
   ],
   "unit": "game/modemDriverE000"
  },
  "EC54": {
   "routine": [
    "makeMapPointSymmetric - 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.",
    "In:  map $F000-$F63F",
    "Out: map made point-symmetric; zp_18 = $FF, zp_48/49, zp_4A/4B, zp_4E/4F clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EC58": {
   "routine": [
    "rotateMapAndMirrorCodes - 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.",
    "In:  map $F000-$F63F",
    "Out: map rotated in place; zp_18 = 0, zp_48/49, zp_4A/4B, zp_4E/4F clobbered"
   ],
   "unit": "game/modemDriverE000"
  },
  "EC91": {
   "routine": [
    "mirrorAllUnitCoordinates - 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.",
    "In:  the six coordinate arrays",
    "Out: all six arrays mirrored; zp_48/zp_49 clobbered"
   ],
   "unit": "game/modemDriverE000"
  },
  "ECB8": {
   "routine": [
    "mirrorCoordinateArray - 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).",
    "In:  A = array low byte, Y = array high byte",
    "Out: array entries mirrored; zp_48/zp_49 = array pointer; self-modifies the ORA operand at $ECCD"
   ],
   "unit": "game/modemDriverE000"
  },
  "ECD4": {
   "routine": [
    "mirrorTerrainCode - 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.",
    "In:  A = map cell byte",
    "Out: A = mirrored cell byte, X clobbered"
   ],
   "unit": "game/modemDriverE000"
  },
  "ECF3": {
   "routine": [
    "loadUnitStartTemplate - 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.",
    "In:  D_F640 bit 7, D_0BA3 bits 0-2 = game type, unitLayoutByGameType $78B8, template data at $F000",
    "Out: $F640-$F83F overwritten with the template; self-modifies the source/destination operands at $ED31/$ED32 and $ED34/$ED35"
   ],
   "unit": "game/modemDriverE000"
  },
  "ED43": {
   "routine": [
    "exchangePlayerNamesAndSettings - 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.",
    "In:  D_0BA5, ownName $FFDE, D_E030, D_0B94, D_0B7E, comm API $E000/$E01D-$E02A",
    "Out: opponentName $FFE8-$FFF1, D_E02F, D_90F8; link messages sent/consumed"
   ],
   "unit": "game/modemDriverE000"
  },
  "EDBE": {
   "routine": [
    "sendEmptyMessage - 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.",
    "In:  none",
    "Out: D_E01F = 0, transmit triggered"
   ],
   "unit": "game/modemDriverE000"
  },
  "EDC9": {
   "routine": [
    "exchangeNameHalf - 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.",
    "In:  X = offset 0 or 5 into the ten-byte name, D_FFDE ownName",
    "Out: $FFE8+X..+4 = peer name half; comm buffers used; self-modifies $EDEB"
   ],
   "unit": "game/modemDriverE000"
  },
  "EDFC": {
   "routine": [
    "abortLinkAndReturnToMenu - 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).",
    "In:  none",
    "Out: never returns to its caller; game returns to the main menu"
   ],
   "unit": "game/modemDriverE000"
  },
  "EE0F": {
   "routine": [
    "storeOpponentNameHalf - 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).",
    "In:  X = offset, D_E020.. received message",
    "Out: $FFE8+X..+4 written, receive buffer released"
   ],
   "unit": "game/modemDriverE000"
  },
  "EE20": {
   "routine": [
    "waitForLinkMessage - 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.",
    "In:  D_E01D",
    "Out: returns when $E01D bit 6 is set; no registers changed"
   ],
   "unit": "game/modemDriverE000"
  },
  "EE26": {
   "routine": [
    "finaliseRecyclerSetup - 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.",
    "In:  D_92A6, D_0B9F playerSide",
    "Out: D_92A8/$92AA set to $FF for sides without a recycler, $3F57 possibly patched to RTS, map cell drawn"
   ],
   "unit": "game/modemDriverE000"
  },
  "EE4A": {
   "routine": [
    "loadFilmStartSnapshot - 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).",
    "In:  film data at $D000+, readFilmByte $5874, copyPageUnderIo $58B2, rewindFilmPtr $58D5",
    "Out: 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)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EEB9": {
   "routine": [
    "handleDisplayToggleKey - 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.",
    "In:  zp_66 currentKey",
    "Out: D_0B7D = $3C, zp_66 = $3C, display mode toggled and messages queued",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EEC3": {
   "routine": [
    "announceAndToggleDisplayMode - 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.",
    "In:  A = $3C on entry (stored into zp_66), D_92FC revealAllUnits flag",
    "Out: zp_66 = $3C, message slot $21 pointer set (sub_C924) and queued (sub_C566)",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EEDD": {
   "routine": [
    "toggleBattleDisplayMode - 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.",
    "In:  D_92FC",
    "Out: D_92FC toggled, $EF10-$EF17 patched, message slot $20 registered and queued",
    "(confidence: medium)"
   ],
   "unit": "game/modemDriverE000"
  },
  "EAA1": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetLengthCodeTable: byteTable, 17 bytes. 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."
   ]
  },
  "EAB7": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetProtocolState: byteTable, 76 bytes. 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."
   ]
  },
  "EAC7": {
   "unit": "game/modemDriverE000",
   "block": [
    "txMessageRing: byteTable, 35 bytes. 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."
   ]
  },
  "EAEB": {
   "unit": "game/modemDriverE000",
   "block": [
    "packetBuffer: byteTable, 19 bytes. Assembly/receive buffer for one packet: [length code][control byte][payload...][checksum]. Maximum 1+16+1 bytes, so it occupies $EAEB-$EAFD."
   ]
  },
  "EBD6": {
   "unit": "game/modemDriverE000",
   "block": [
    "modemHangUpString: text, 13 bytes. 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."
   ]
  },
  "EBE3": {
   "unit": "game/modemDriverE000",
   "block": [
    "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)."
   ]
  },
  "EBF0": {
   "unit": "game/modemDriverE000",
   "block": [
    "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)."
   ]
  },
  "EBFD": {
   "unit": "game/modemDriverE000",
   "block": [
    "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)."
   ]
  },
  "EBFE": {
   "unit": "game/modemDriverE000",
   "block": [
    "modemInitStringPtrLoTable: byteTable, 2 bytes. Low bytes $E3 and $F0 of the answer and dial init strings, indexed by modemModeIndex $E011 at $E2E0."
   ]
  },
  "EC3B": {
   "unit": "game/modemDriverE000",
   "block": [
    "mirroredTerrainCodeTable: byteTable, 25 bytes. 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."
   ]
  },
  "EF08": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgDisplayPromptPrefix: text, 8 bytes. 'F 1 FOR ' - start of the trainer prompt printed as message slot $20; it runs straight into displayModeNameSlot and msgBattleDisplaySuffix."
   ]
  },
  "EF10": {
   "unit": "game/modemDriverE000",
   "block": [
    "displayModeNameSlot: text, 8 bytes. Eight-character patch area inside the prompt, filled at run time with 'BEGINNER' or 'STANDARD' by toggleBattleDisplayMode. The shipped image contains '********'."
   ]
  },
  "EF18": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgBattleDisplaySuffix: text, 16 bytes. ' BATTLE DISPLAY' plus the bit-7 terminator '.' - the tail of the trainer F1 prompt."
   ]
  },
  "EF28": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgBeginnerDisplaySet: text, 21 bytes. 'BEGINNER DISPLAY SET' + bit-7 '.'. The first eight characters double as the source of the 'BEGINNER' patch word."
   ]
  },
  "EF3D": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgStandardDisplaySet: text, 21 bytes. 'STANDARD DISPLAY SET' + bit-7 '.'. Reached as msgBeginnerDisplaySet+$15; its first eight characters are the 'STANDARD' patch word."
   ]
  },
  "EF52": {
   "unit": "game/modemDriverE000",
   "block": [
    "gameOptionLabels: text, 93 bytes. 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)."
   ]
  },
  "EFAF": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgInsertDiskTemplate: text, 33 bytes. '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."
   ]
  },
  "EFD0": {
   "unit": "game/modemDriverE000",
   "block": [
    "wordData: text, 4 bytes. 'DATA' - patch word for msgInsertDiskTemplate (no terminator)."
   ]
  },
  "EFD4": {
   "unit": "game/modemDriverE000",
   "block": [
    "wordGame: text, 4 bytes. 'GAME' - patch word for msgInsertDiskTemplate (no terminator)."
   ]
  },
  "EFD8": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgCustomOff: text, 10 bytes. 'CUSTOM OF' + bit-7 'F' - installed as message slot $1E by overlay A $8218."
   ]
  },
  "EFE2": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgDestroyOff: text, 11 bytes. 'DESTROY OF' + bit-7 'F' - installed as message slot $1E by overlay A $86B1; $EFEA is used as the ON/OFF patch address."
   ]
  },
  "EFED": {
   "unit": "game/modemDriverE000",
   "block": [
    "msgAccepted: text, 8 bytes. 'ACCEPTE' + bit-7 'D' - printed by overlay A $7710 when the fire button confirms a setting."
   ]
  },
  "EFF5": {
   "unit": "game/modemDriverE000",
   "block": [
    "commTailFiller: unknown, 11 bytes. $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)."
   ]
  }
 }
}