{
 "chunk": "game_modemDriverE000_E000",
 "unit": "game/modemDriverE000",
 "unitLabels": {
  "game/modemDriverE000": {
   "E00F": "commRtsStub",
   "E010": "commRtiStub",
   "E011": "isOriginateMode",
   "E01D": "exchangeFlags",
   "E01E": "rxPacketLength",
   "E01F": "txPacketLength",
   "E020": "rxPacketBuffer",
   "E021": "rxPacketArg1",
   "E022": "rxPacketArg2",
   "E023": "rxPacketArg3",
   "E024": "rxPacketArg4",
   "E027": "txPacketBuffer",
   "E028": "txPacketArg1",
   "E029": "txPacketArg2",
   "E02A": "txPacketArg3",
   "E02E": "hostOutCount",
   "E02F": "peerCommBuildId",
   "E030": "commBuildId",
   "E031": "nmiChainVector",
   "E032": "nmiChainVectorHi",
   "E033": "ciaIcrShadow",
   "E039": "nmiSuspendRequest",
   "E03A": "nmiSuspendAck",
   "E03B": "isLinkActive",
   "E03C": "linkStatus",
   "E03D": "linkStatusSample",
   "E03F": "modemReplyTimer",
   "E040": "connectionPhase",
   "E041": "skipModemInitString",
   "E042": "uartRestartRequest",
   "E043": "carrierDebounceCount",
   "E044": "txPaceCounter",
   "E045": "carrierSampleTimer",
   "E046": "lastHotkeyCode",
   "E047": "linkErrorCount",
   "E048": "modemOptionFlags",
   "E04A": "carrierOverrideFlags",
   "E04B": "carrierOverrideToggleMask",
   "E04C": "dtrToggleMask",
   "E04D": "userPortDdrbValue",
   "E04E": "userPortDtrDropped",
   "E04F": "userPortIdleTable",
   "E051": "serialShiftPatternTable",
   "E054": "serialShiftPatternAnswer1200",
   "E055": "baudIndex",
   "E056": "bitPeriodLo",
   "E057": "bitPeriodHi",
   "E058": "txPaceReload",
   "E059": "baudParameterTable",
   "E05F": "linkStatusBorderTable",
   "E060": "linkStatusBorderPhase0Carrier",
   "E071": "clearBlockStore",
   "E07D": "exchangeCounterLo",
   "E07E": "exchangeCounterHi",
   "E07F": "txHeaderExtraBits",
   "E080": "rxHeaderByte",
   "E081": "rxPacketState",
   "E082": "rxBytesRemaining",
   "E083": "txPacketState",
   "E084": "txPayloadIndex",
   "E085": "sendRequestCount",
   "E086": "hostInRing",
   "E08E": "chatFrameCounter",
   "E08F": "hostOutReadIndex",
   "E090": "hostOutWriteIndex",
   "E091": "hostOutRing",
   "E092": "modemCommandChars",
   "E0A1": "hostInCount",
   "E0A2": "hostInReadIndex",
   "E0A3": "hostInWriteIndex",
   "E0A4": "frameInFlightFlag",
   "E0A5": "uartPendingCount",
   "E0A6": "packetInFlightCount",
   "E0A7": "serviceLock",
   "E0A8": "inFlightSlotIndex",
   "E0DB": "returnHostRingEmpty",
   "E1AD": "remoteCodeWriteIndex",
   "E306": "hotkeyCommodoreFlag",
   "E3A1": "modemStringLoad",
   "E414": "uartTxCount",
   "E415": "uartTxReadIndex",
   "E416": "uartTxWriteIndex",
   "E417": "uartTxRing",
   "E42D": "uartRxCount",
   "E42E": "uartRxReadIndex",
   "E42F": "uartRxWriteIndex",
   "E430": "uartRxRing",
   "E444": "arqTxCount",
   "E445": "arqTxReadIndex",
   "E446": "arqTxWriteIndex",
   "E447": "arqTxRing",
   "E459": "arqRxCount",
   "E45A": "arqRxReadIndex",
   "E45B": "arqRxWriteIndex",
   "E45C": "arqRxRing",
   "E5BB": "txCharActive",
   "E5BC": "txBitCountReload",
   "E5BD": "txBitCounter",
   "E5BE": "txShiftRegister",
   "E5BF": "rxBitCounter",
   "E5C0": "rxShiftRegister",
   "E5C1": "suspendUartIfRunning",
   "E68A": "nmiMaskCiaSources",
   "E693": "nmiTestCiaSource",
   "E70B": "nmiArmDataBits",
   "E71C": "nmiDeliverRxChar"
  }
 },
 "notes": {
  "E000": {
   "unit": "game/modemDriverE000",
   "routine": [
    "commRequest - entry 0 of the seven-entry jump table that every opponent module must present. X picks the service: 0 = per-frame protocol tick, 1 = 'transmit the packet I have prepared in txPacketBuffer', 2 or more = acknowledge and throw away a finished exchange.",
    "In:  X = function code",
    "Out: falls through to commRequestDispatch; updates exchangeFlags ($E01D)",
    "Called from: raster IRQ $1129 (X=0), buildOutgoingPacket $56AE (X=1), completeExchange $570F/$5727, $4EFF, $526F, $573B/$5767/$5776, and the handshake in this file ($ED59/$ED96/$EDBB/$EDC5/$EDE4/$EE1D)"
   ],
   "line": "entry 0: per-frame tick / send request / acknowledge, selected by X"
  },
  "E003": {
   "unit": "game/modemDriverE000",
   "routine": [
    "commLinkControl - jump-table entry 1, life cycle of the link. X=0 open, X=1 cold-initialise the module, X=2 shut the UART down, X>=3 hang the modem up and then shut down.",
    "In:  X = function code",
    "Out: see commLinkControlDispatch",
    "Called from: initCommModule $0F46 (X=1, right after the overlay is loaded), openCommLink $1B70 (X=0), hangUpModem $1B56 (X=3)"
   ],
   "line": "entry 1: open, initialise, close or hang up the link, selected by X"
  },
  "E006": {
   "unit": "game/modemDriverE000",
   "routine": [
    "getLinkByte - jump-table entry 2: take one byte that has arrived from the opponent (or was echoed back by the modem in terminal mode) out of the 8-byte hostInRing.",
    "In:  none",
    "Out: A = byte with bit 7 flipped back, C=0 when a byte was returned, C=1 when the ring was empty",
    "Called from: receiveChatCharacter $1399"
   ],
   "line": "entry 2: take one byte the opponent sent out of the host input ring"
  },
  "E009": {
   "unit": "game/modemDriverE000",
   "routine": [
    "putLinkByte - jump-table entry 3: hand one byte to the module for transmission to the opponent (the chat channel). Stored EOR $80 in the 16-byte hostOutRing.",
    "In:  A = byte to send",
    "Out: hostOutRing/hostOutCount updated; the byte is dropped when the ring already holds 16",
    "Called from: handleChatSend $1396 (JMP)"
   ],
   "line": "entry 3: hand one chat byte over for transmission"
  },
  "E00C": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pollLinkStatus - jump-table entry 4, called once per raster IRQ: runs the NMI suspend/resume handshake the disk loader needs and samples plus debounces the modem's carrier-detect line.",
    "In:  nmiSuspendRequest/nmiSuspendAck, isLinkActive, CIA2_PRB bit 4",
    "Out: linkStatus (debounced), linkStatusSample, carrierDebounceCount",
    "Called from: raster IRQ $112C"
   ],
   "line": "entry 4: per-frame carrier poll and NMI suspend handshake"
  },
  "E00F": {
   "unit": "game/modemDriverE000",
   "line": "spare RTS stub sitting in the jump table's constant pool"
  },
  "E010": {
   "unit": "game/modemDriverE000",
   "line": "spare RTI stub - the value nmiChainVector points at until the game patches it"
  },
  "E011": {
   "unit": "game/modemDriverE000",
   "line": "0 = answer mode (ATQ0V1X1A), 1 = originate/dial mode (ATQ0V1X1D); set by $0CDE from the A/O menu key and by the C=+A / C=+O hot keys"
  },
  "E012": {
   "unit": "game/modemDriverE000",
   "routine": [
    "keyboardScanHook - jump-table entry 5, the module's keyboard scanner vector. On disk it points at returnNoKey (always 'no key'); the main program overwrites the operand bytes $E013/$E014 at $0F4B/$0F50 with $0DB7 = scanKeyboard, so at run time this is the game's keyboard matrix scan.",
    "In:  none",
    "Out: A = key code ($FF = none), Y bit 7 = SHIFT held, X bit 7 = Commodore key held",
    "Called from: readKeyAndHandleModemHotkeys $E2EC only"
   ],
   "line": "entry 5: keyboard scan - operand patched to $0DB7 (scanKeyboard) by $0F4B/$0F50"
  },
  "E015": {
   "unit": "game/modemDriverE000",
   "routine": [
    "commKeyEntry - jump-table entry 6 and the game's only keyboard read. Routing every key through the comm module lets the Commodore-key modem hot keys be intercepted before the game sees them.",
    "In:  none",
    "Out: A = key code, or $FF when the key was swallowed as a modem hot key",
    "Called from: pollKeyboardEvent $0E56 (every frame)"
   ],
   "line": "entry 6: read a key, filtering out the Commodore-key modem hot keys"
  },
  "E018": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearPacketVars - zeroes the 19-byte packet-exchange block $E01D-$E02F (exchange flags, both packet lengths, both 7-byte packet buffers, the host-output count and the peer build id) by calling clearInlineVarBlock with the block sitting immediately after the JSR. Because clearInlineVarBlock eats the return address, the RTS lands in this routine's caller.",
    "In:  none",
    "Out: $E01D-$E02F = 0; returns to the caller of clearPacketVars",
    "Called from: initCommModule $E2B4"
   ],
   "line": "19 bytes to clear: exchangeFlags through peerCommBuildId"
  },
  "E01A": {
   "unit": "game/modemDriverE000",
   "line": "clear the inline block that follows and return to our caller's caller"
  },
  "E01D": {
   "unit": "game/modemDriverE000",
   "block": [
    "Packet-exchange variables, $E01D-$E02F. This is the inline block clearPacketVars hands to",
    "clearInlineVarBlock, so the JSR above is followed by 19 bytes of data, not code - the BRK the",
    "disassembler shows is never executed. The game reads and writes most of these directly:",
    "$E01D flags, $E01E/$E01F lengths, $E020 and $E027 the two 7-byte command packets."
   ],
   "line": "exchangeFlags: bit 0 send requested, bit 1 receive in progress, bit 3 idle marker sent, bit 5 own packet sent, bit 6 exchange complete, bit 7 opponent packet received (rendered as BRK - it is data, never executed)"
  },
  "E01E": {
   "unit": "game/modemDriverE000",
   "line": "rxPacketLength: number of bytes of the opponent's packet held in rxPacketBuffer"
  },
  "E01F": {
   "unit": "game/modemDriverE000",
   "line": "txPacketLength: number of bytes the game put in txPacketBuffer (0-4 in practice)"
  },
  "E020": {
   "unit": "game/modemDriverE000",
   "line": "rxPacketBuffer: the command byte of the packet just received (7 bytes, $E020-$E026)"
  },
  "E021": {
   "unit": "game/modemDriverE000",
   "line": "first argument byte of the received command"
  },
  "E022": {
   "unit": "game/modemDriverE000",
   "line": "second argument byte of the received command"
  },
  "E023": {
   "unit": "game/modemDriverE000",
   "line": "third argument byte of the received command"
  },
  "E024": {
   "unit": "game/modemDriverE000",
   "line": "the remaining three bytes of the 7-byte receive buffer"
  },
  "E027": {
   "unit": "game/modemDriverE000",
   "line": "txPacketBuffer: command byte to send (7 bytes, $E027-$E02D)"
  },
  "E028": {
   "unit": "game/modemDriverE000",
   "line": "first argument byte of the command being sent"
  },
  "E029": {
   "unit": "game/modemDriverE000",
   "line": "second argument byte of the command being sent"
  },
  "E02A": {
   "unit": "game/modemDriverE000",
   "line": "the remaining four bytes of the 7-byte transmit buffer"
  },
  "E02E": {
   "unit": "game/modemDriverE000",
   "line": "hostOutCount: characters waiting in hostOutRing (chat text, or a queued AT command string)"
  },
  "E02F": {
   "unit": "game/modemDriverE000",
   "line": "peerCommBuildId: the opponent's commBuildId, stored by the handshake at $EDAD"
  },
  "E030": {
   "unit": "game/modemDriverE000",
   "block": [
    "Four bytes that survive every clear: the build id the two machines swap during the handshake,",
    "the vector this module chains non-CIA2 NMIs through, and the shadow of the CIA2 interrupt mask."
   ],
   "line": "commBuildId: $00 = this modem build; the trainer overlay has bit 7 set here. Read by main at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D and sent to the peer during the handshake"
  },
  "E031": {
   "unit": "game/modemDriverE000",
   "line": "nmiChainVector low byte - $E010 (the RTI stub) on disk, patched to $1298 by initInterruptVectors $0BF3"
  },
  "E032": {
   "unit": "game/modemDriverE000",
   "line": "nmiChainVector high byte - patched to $12 by $0BFB"
  },
  "E033": {
   "unit": "game/modemDriverE000",
   "line": "ciaIcrShadow: the CIA2 interrupt sources this module currently wants ($92 idle, $83 while receiving, $7F when suspended)"
  },
  "E034": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearLinkVars - zeroes the 15-byte link-state block $E039-$E047 (suspend handshake, link-active flag, carrier state, modem timers, connection phase, error counter) through clearInlineVarBlock.",
    "In:  none",
    "Out: $E039-$E047 = 0; returns to the caller of the routine that jumped here",
    "Called from: commLinkControlDispatch $E2AE (JMP - the tail of the 'close the link' path)"
   ],
   "line": "15 bytes to clear: nmiSuspendRequest through linkErrorCount"
  },
  "E036": {
   "unit": "game/modemDriverE000",
   "line": "clear the inline block that follows; the RTS lands in the caller of whoever jumped here"
  },
  "E039": {
   "unit": "game/modemDriverE000",
   "block": [
    "Link-state variables, $E039-$E047: the inline block clearLinkVars zeroes. $E039/$E03A are the",
    "two-way suspend handshake with the disk loader, $E03B-$E03D the link and carrier state, and the",
    "rest timers and counters used by the modem state machine. The 'DTH' at $E045-$E047 is leftover",
    "content from the sector image (this page comes from track 18 sector 7, outside the encrypted area)."
   ],
   "line": "nmiSuspendRequest: the game writes $C0 here before a disk load and 0 afterwards (rendered as BRK - data, never executed)"
  },
  "E03A": {
   "unit": "game/modemDriverE000",
   "line": "nmiSuspendAck: this module answers $80 when the UART NMI is really off, 0 when it is running"
  },
  "E03B": {
   "unit": "game/modemDriverE000",
   "line": "isLinkActive: non-zero once openCommLink has started the UART"
  },
  "E03C": {
   "unit": "game/modemDriverE000",
   "line": "linkStatus, debounced: bit 6 = carrier present, bit 7 = characters still arriving"
  },
  "E03D": {
   "unit": "game/modemDriverE000",
   "line": "linkStatusSample ($E03D) and an unused byte ($E03E): the raw carrier sample taken each frame"
  },
  "E03F": {
   "unit": "game/modemDriverE000",
   "line": "modemReplyTimer: counts down while waiting for the modem's answer; negative means a command string is still going out"
  },
  "E040": {
   "unit": "game/modemDriverE000",
   "line": "connectionPhase: 0 idle, 1 modem-command/terminal mode, 2 byte sync, 3 packet protocol"
  },
  "E041": {
   "unit": "game/modemDriverE000",
   "line": "skipModemInitString: non-zero after C=+RETURN so openCommLink re-opens without re-sending the AT string"
  },
  "E042": {
   "unit": "game/modemDriverE000",
   "line": "uartRestartRequest: 1 = re-evaluate the connection, $FF = force back to phase 0"
  },
  "E043": {
   "unit": "game/modemDriverE000",
   "line": "carrierDebounceCount: consecutive samples that disagree with linkStatus"
  },
  "E044": {
   "unit": "game/modemDriverE000",
   "line": "txPaceCounter: counts frames between transmitted characters"
  },
  "E045": {
   "unit": "game/modemDriverE000",
   "line": "carrierSampleTimer: zeroed by the NMI on every good character, so a negative value means the line is quiet ('D' here is leftover from the sector image)"
  },
  "E046": {
   "unit": "game/modemDriverE000",
   "line": "lastHotkeyCode: the previous hot-key code, used to ignore auto-repeat ('T' is leftover)"
  },
  "E047": {
   "unit": "game/modemDriverE000",
   "line": "linkErrorCount: bumped by countLinkError; non-zero makes the link state machine restart byte sync ('H' is leftover)"
  },
  "E048": {
   "unit": "game/modemDriverE000",
   "block": [
    "Modem/UART constants, $E048-$E066. None of them is inside a cleared block, so they keep their",
    "disk values for the whole session: option flags, the CIA2 port bit masks, the two baud parameter",
    "sets and the border-colour table for the link status indicator."
   ],
   "line": "modemOptionFlags $CC (bit 7 = a modem is attached, bit 6 = watch the result codes for CONNECT 1200) and an unreferenced $08"
  },
  "E04A": {
   "unit": "game/modemDriverE000",
   "line": "carrierOverrideFlags: bit 6 = pretend carrier is present (C=+C toggle, for a direct null-modem cable)"
  },
  "E04B": {
   "unit": "game/modemDriverE000",
   "line": "$40 = the carrier-override bit the C=+C hot key toggles"
  },
  "E04C": {
   "unit": "game/modemDriverE000",
   "line": "$24 = the CIA2_PRB bits the C=+H hot key toggles: PB2 (DTR) and PB5"
  },
  "E04D": {
   "unit": "game/modemDriverE000",
   "line": "$26 = CIA2_DDRB: PB1 (RTS), PB2 (DTR) and PB5 are outputs, everything else an input"
  },
  "E04E": {
   "unit": "game/modemDriverE000",
   "line": "$22 = CIA2_PRB with DTR (PB2) low: hang up"
  },
  "E04F": {
   "unit": "game/modemDriverE000",
   "line": "userPortIdleTable, indexed by isOriginateMode: both entries $26 = RTS, DTR and PB5 asserted"
  },
  "E051": {
   "unit": "game/modemDriverE000",
   "line": "serialShiftPatternTable entries 0-2, indexed by isOriginateMode EOR baudIndex: $27 answer/300, $2F originate/300, $3F originate/1200"
  },
  "E054": {
   "unit": "game/modemDriverE000",
   "line": "serialShiftPatternTable entry 3: $37 = answer at 1200 baud"
  },
  "E055": {
   "unit": "game/modemDriverE000",
   "line": "baudIndex: 0 = 300 baud, 3 = 1200 baud (also the stride into baudParameterTable)"
  },
  "E056": {
   "unit": "game/modemDriverE000",
   "line": "bitPeriodLo: low byte of the CIA timer latch for one bit time"
  },
  "E057": {
   "unit": "game/modemDriverE000",
   "line": "bitPeriodHi: high byte of the bit period; $0D50 = 3408 cycles = 300 baud"
  },
  "E058": {
   "unit": "game/modemDriverE000",
   "line": "txPaceReload: frames between transmitted characters (1 at 300 baud, 2 at 1200)"
  },
  "E059": {
   "unit": "game/modemDriverE000",
   "block": [
    "baudParameterTable: two 3-byte entries selected by baudIndex (0 or 3). Each entry is",
    "{bit period low, bit period high, transmit pacing}. $0D50 = 3408 cycles and $0353 = 851 cycles,",
    "which on an NTSC C64 (1022727 Hz) give 300.1 and 1201.8 baud."
   ],
   "line": "baudParameterTable: two 3-byte entries {periodLo, periodHi, pacing} - $0D50/1 = 300 baud, $0353/2 = 1200 baud"
  },
  "E05F": {
   "unit": "game/modemDriverE000",
   "block": [
    "linkStatusBorderTable: 8 entries indexed by connectionPhase*2 + carrier. Each byte is the border",
    "colour shifted left one place with bit 0 set when the colour should be shown even if the",
    "Commodore key is not held. Phase 1 (talking to the modem) is the only always-visible pair."
   ],
   "line": "linkStatusBorderTable entry 0 (phase 0, no carrier): $00 = black, low bit clear so it is only painted while the Commodore key is held"
  },
  "E060": {
   "unit": "game/modemDriverE000",
   "line": "linkStatusBorderTable entries 1-7: value = colour*2 + 'always paint' flag. $0E yellow, $1D light blue + always, $0D blue + always, $14 light red, $04 red, $1A light green, $0A green"
  },
  "E067": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearInlineVarBlock - shared 'zero the X bytes that follow my JSR' helper, used six times in this module. It pulls the return address off the stack, plants it in the absolute operand of the STA at clearBlockStore, and stores 0 through it with X counting down. The return address is gone, so the closing RTS returns two levels up, to the caller of the routine that made the JSR.",
    "In:  X = number of bytes to clear (1..90); the JSR's return address points one byte before the inline block",
    "Out: the inline block is zeroed, A=0, X=0, and control returns to the caller's caller",
    "Called from: clearPacketVars $E01A, clearLinkVars $E036, clearProtocolVars $E07A, clearUartVars $E411, clearUartState $E5B8, clearPacketState $EAB4"
   ],
   "line": "take the low byte of the return address, which points at the byte before the inline block"
  },
  "E068": {
   "unit": "game/modemDriverE000",
   "line": "plant it in the address operand of the STA below"
  },
  "E06B": {
   "unit": "game/modemDriverE000",
   "line": "take the high byte of the return address as well - the RTS will now go one level further up"
  },
  "E06C": {
   "unit": "game/modemDriverE000",
   "line": "plant the high byte too"
  },
  "E06F": {
   "unit": "game/modemDriverE000",
   "line": "the block is cleared with zeroes"
  },
  "E071": {
   "unit": "game/modemDriverE000",
   "line": "operand patched above; with X counting down this covers block+X-1 .. block, the X bytes right after the JSR"
  },
  "E074": {
   "unit": "game/modemDriverE000",
   "line": "next byte down"
  },
  "E075": {
   "unit": "game/modemDriverE000",
   "line": "keep going until X reaches 0 (index 0 would hit the byte before the block and is skipped)"
  },
  "E077": {
   "unit": "game/modemDriverE000",
   "line": "return to the caller of the routine that called us"
  },
  "E078": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearProtocolVars - zeroes the 43-byte protocol block $E07D-$E0A7: the completed-exchange counter, the receive and transmit packet state machines, both host character rings with their counts and indices, the in-flight counters and the service lock. The constant inFlightSlotIndex at $E0A8 is deliberately left out of the count.",
    "In:  none",
    "Out: $E07D-$E0A7 = 0; returns to the caller of clearProtocolVars",
    "Called from: initCommModule $E2B1"
   ],
   "line": "43 bytes to clear: exchangeCounter through serviceLock (inFlightSlotIndex at $E0A8 is left alone)"
  },
  "E07A": {
   "unit": "game/modemDriverE000",
   "line": "clear the inline block that follows and return to our caller's caller"
  },
  "E07D": {
   "unit": "game/modemDriverE000",
   "block": [
    "Protocol variables, $E07D-$E0A7, the inline block clearProtocolVars zeroes, plus the constant",
    "inFlightSlotIndex at $E0A8 which is deliberately left out of the count. It holds the receive and",
    "transmit packet state machines and the two character rings that connect the module to the game."
   ],
   "line": "exchangeCounterLo - low byte of the completed-exchange counter; nothing in the image ever reads it (rendered as BRK - data, never executed)"
  },
  "E07E": {
   "unit": "game/modemDriverE000",
   "line": "exchangeCounterHi - high byte of the same unused statistic"
  },
  "E07F": {
   "unit": "game/modemDriverE000",
   "line": "txHeaderExtraBits: ORed into every outgoing packet header, but never written anywhere, so headers carry only the length"
  },
  "E080": {
   "unit": "game/modemDriverE000",
   "line": "rxHeaderByte: the header byte of the packet currently being received"
  },
  "E081": {
   "unit": "game/modemDriverE000",
   "line": "rxPacketState: 0 = expect a header, 1/2 = collecting a command payload, $80 = collecting chat bytes"
  },
  "E082": {
   "unit": "game/modemDriverE000",
   "line": "rxBytesRemaining: payload bytes still to be read for this packet"
  },
  "E083": {
   "unit": "game/modemDriverE000",
   "line": "txPacketState: bit 0 = payload in progress, bit 1 = frame handed over, bit 7 = the frame was chat"
  },
  "E084": {
   "unit": "game/modemDriverE000",
   "line": "txPayloadIndex: index of the next payload byte to send (and the byte count of a chat frame)"
  },
  "E085": {
   "unit": "game/modemDriverE000",
   "line": "sendRequestCount: outstanding $E000/X=1 send requests"
  },
  "E086": {
   "unit": "game/modemDriverE000",
   "block": [
    "hostInRing: 8-byte descending circular buffer of characters for the game, filled by the packet",
    "receiver and by the terminal-mode echo, drained through the $E006 entry."
   ],
   "line": "hostInRing: 8-byte descending ring of bytes the game reads with $E006"
  },
  "E08E": {
   "unit": "game/modemDriverE000",
   "line": "chatFrameCounter: bytes still to pop while a chat frame is going out"
  },
  "E08F": {
   "unit": "game/modemDriverE000",
   "line": "hostOutReadIndex: next slot popHostOutRing will read (counts down, wraps 0 -> 15)"
  },
  "E090": {
   "unit": "game/modemDriverE000",
   "line": "hostOutWriteIndex: next slot pushHostOutRing will fill"
  },
  "E091": {
   "unit": "game/modemDriverE000",
   "block": [
    "hostOutRing: 16-byte descending circular buffer of characters to send. The game pushes chat text",
    "through $E009; queueModemCommandString instead loads a whole AT string into slots 1..n, which is",
    "why the strings in page $EB are stored back to front."
   ],
   "line": "hostOutRing: 16-byte descending ring of characters to send (slot 0; the AT-string loader starts at slot 1)"
  },
  "E092": {
   "unit": "game/modemDriverE000",
   "line": "modemCommandChars = hostOutRing+1: queueModemCommandString writes an AT string here forwards, and the descending read index sends it back to front"
  },
  "E09A": {
   "unit": "game/modemDriverE000",
   "line": "the rest of the 16-byte host output ring"
  },
  "E0A1": {
   "unit": "game/modemDriverE000",
   "line": "hostInCount: bytes waiting in hostInRing (maximum 8)"
  },
  "E0A2": {
   "unit": "game/modemDriverE000",
   "line": "hostInReadIndex: next slot popHostInRing will read"
  },
  "E0A3": {
   "unit": "game/modemDriverE000",
   "line": "hostInWriteIndex: next slot pushHostInRing will fill"
  },
  "E0A4": {
   "unit": "game/modemDriverE000",
   "line": "frameInFlightFlag: the uartPendingCount the frame layer saw when it queued a frame"
  },
  "E0A5": {
   "unit": "game/modemDriverE000",
   "line": "uartPendingCount: characters queued for the UART that have not finished transmitting"
  },
  "E0A6": {
   "unit": "game/modemDriverE000",
   "line": "packetInFlightCount: bytes of the packet the packet layer just handed over, cleared once they have drained"
  },
  "E0A7": {
   "unit": "game/modemDriverE000",
   "line": "serviceLock: rests at 1; serviceCommTick decrements it so a re-entrant call does nothing"
  },
  "E0A8": {
   "unit": "game/modemDriverE000",
   "line": "inFlightSlotIndex = 1: selects packetInFlightCount ($E0A5+1) as the counter the packet layer waits on; 0 would make it wait on the raw UART count instead"
  },
  "E0A9": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popHostInRing - implementation of the $E006 entry. Pops the oldest byte out of the 8-byte hostInRing and undoes the EOR $80 the producers applied, so the game sees the character the opponent typed.",
    "In:  hostInCount, hostInReadIndex, hostInRing",
    "Out: A = byte, C=0; C=1 and A undefined when the ring is empty; count and read index updated",
    "Called from: the $E006 jump-table entry"
   ],
   "line": "any bytes waiting in the host input ring?"
  },
  "E0AC": {
   "unit": "game/modemDriverE000",
   "line": "empty - return C=1"
  },
  "E0AE": {
   "unit": "game/modemDriverE000",
   "line": "oldest slot"
  },
  "E0B1": {
   "unit": "game/modemDriverE000",
   "line": "fetch the character"
  },
  "E0B4": {
   "unit": "game/modemDriverE000",
   "line": "undo the EOR $80 the producers applied"
  },
  "E0B6": {
   "unit": "game/modemDriverE000",
   "line": "walk the read index down"
  },
  "E0B7": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E0B9": {
   "unit": "game/modemDriverE000",
   "line": "wrapped past slot 0 - go back to the top slot of the 8-byte ring"
  },
  "E0BB": {
   "unit": "game/modemDriverE000",
   "line": "store the new read index"
  },
  "E0BE": {
   "unit": "game/modemDriverE000",
   "line": "one byte fewer waiting"
  },
  "E0C1": {
   "unit": "game/modemDriverE000",
   "line": "C=0 says 'here is a byte'"
  },
  "E0C2": {
   "unit": "game/modemDriverE000",
   "line": "back to the game"
  },
  "E0C3": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popHostOutRing - pops the next byte out of the 16-byte hostOutRing. Feeds the packet transmitter with chat characters, drains the ring when the link drops, and clocks canned modem command strings out in terminal mode.",
    "In:  hostOutCount, hostOutReadIndex, hostOutRing",
    "Out: A = byte, C=0, or C=1 when the ring is empty",
    "Called from: sendPacketFsm $E253, runModemStateMachine $E78C, sendNextModemCommandChar $E7CA"
   ],
   "line": "any characters queued for transmission?"
  },
  "E0C6": {
   "unit": "game/modemDriverE000",
   "line": "empty - return C=1"
  },
  "E0C8": {
   "unit": "game/modemDriverE000",
   "line": "oldest slot"
  },
  "E0CB": {
   "unit": "game/modemDriverE000",
   "line": "fetch the character"
  },
  "E0CE": {
   "unit": "game/modemDriverE000",
   "line": "walk the read index down"
  },
  "E0CF": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E0D1": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - go back to the top slot of the 16-byte ring"
  },
  "E0D3": {
   "unit": "game/modemDriverE000",
   "line": "store the new read index"
  },
  "E0D6": {
   "unit": "game/modemDriverE000",
   "line": "one character fewer waiting"
  },
  "E0D9": {
   "unit": "game/modemDriverE000",
   "line": "C=0 says 'here is a byte'"
  },
  "E0DA": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E0DB": {
   "unit": "game/modemDriverE000",
   "line": "shared 'that ring is empty' exit for both host-ring pops"
  },
  "E0DC": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E0DD": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushHostInRing - pushes one byte into the 8-byte hostInRing, the queue the game reads with $E006. Silently drops the byte when the ring already holds 8.",
    "In:  A = byte",
    "Out: hostInRing, hostInCount, hostInWriteIndex",
    "Called from: receivePacketFsm $E1D3 (chat payload), runModemStateMachine $E7BD (echo of the modem's answer), sendNextModemCommandChar $E7F4 (fabricated CR when the modem does not answer)"
   ],
   "line": "how full is the host input ring?"
  },
  "E0E0": {
   "unit": "game/modemDriverE000",
   "line": "the ring holds 8 bytes"
  },
  "E0E2": {
   "unit": "game/modemDriverE000",
   "line": "full - drop the byte on the floor"
  },
  "E0E4": {
   "unit": "game/modemDriverE000",
   "line": "next free slot"
  },
  "E0E7": {
   "unit": "game/modemDriverE000",
   "line": "store the byte"
  },
  "E0EA": {
   "unit": "game/modemDriverE000",
   "line": "walk the write index down"
  },
  "E0EB": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E0ED": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to the top slot"
  },
  "E0EF": {
   "unit": "game/modemDriverE000",
   "line": "store the new write index"
  },
  "E0F2": {
   "unit": "game/modemDriverE000",
   "line": "one more byte waiting for the game"
  },
  "E0F5": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E0F6": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushHostOutRing - implementation of the $E009 entry: store A EOR $80 in the 16-byte hostOutRing and bump the count, or do nothing at all when the ring already holds 16 bytes.",
    "In:  A = byte to send",
    "Out: hostOutRing, hostOutWriteIndex, hostOutCount",
    "Called from: the $E009 jump-table entry"
   ],
   "line": "how full is the host output ring?"
  },
  "E0F9": {
   "unit": "game/modemDriverE000",
   "line": "the ring holds 16 characters"
  },
  "E0FB": {
   "unit": "game/modemDriverE000",
   "line": "full - drop the character silently"
  },
  "E0FD": {
   "unit": "game/modemDriverE000",
   "line": "next free slot"
  },
  "E100": {
   "unit": "game/modemDriverE000",
   "line": "characters travel with bit 7 inverted"
  },
  "E102": {
   "unit": "game/modemDriverE000",
   "line": "store it"
  },
  "E105": {
   "unit": "game/modemDriverE000",
   "line": "walk the write index down"
  },
  "E106": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E108": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to the top slot"
  },
  "E10A": {
   "unit": "game/modemDriverE000",
   "line": "store the new write index"
  },
  "E10D": {
   "unit": "game/modemDriverE000",
   "line": "one more character to transmit"
  },
  "E110": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E111": {
   "unit": "game/modemDriverE000",
   "routine": [
    "commRequestDispatch - the dispatcher behind $E000. X=0 runs serviceCommTick, X=1 runs requestPacketSend, X>=2 acknowledges a finished exchange: if bit 6 of exchangeFlags is set both packet lengths and the whole flag byte are cleared, which releases rxPacketBuffer for the next packet.",
    "In:  X = function code, exchangeFlags",
    "Out: one of the three cases above",
    "Called from: the $E000 jump-table entry"
   ],
   "line": "X=0 -> $FF, X=1 -> 0, X>=2 -> positive"
  },
  "E112": {
   "unit": "game/modemDriverE000",
   "line": "X was 0: run the per-frame service tick"
  },
  "E114": {
   "unit": "game/modemDriverE000",
   "line": "X was 1: the game wants its packet transmitted"
  },
  "E116": {
   "unit": "game/modemDriverE000",
   "line": "X was 2 or more: is the exchange finished (bit 6)?"
  },
  "E119": {
   "unit": "game/modemDriverE000",
   "line": "yes - consume it"
  },
  "E11B": {
   "unit": "game/modemDriverE000",
   "line": "not finished yet, leave everything alone"
  },
  "E11C": {
   "unit": "game/modemDriverE000",
   "line": "clear both packet lengths and all the flags"
  },
  "E11E": {
   "unit": "game/modemDriverE000",
   "line": "the received packet has been read out"
  },
  "E121": {
   "unit": "game/modemDriverE000",
   "line": "the transmitted packet is done with"
  },
  "E124": {
   "unit": "game/modemDriverE000",
   "line": "clearing the flags releases the buffers for the next exchange"
  },
  "E127": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E128": {
   "unit": "game/modemDriverE000",
   "routine": [
    "requestPacketSend - $E000 with X=1. The game has filled txPacketBuffer and set txPacketLength; this bumps bit 0 of exchangeFlags ('a send is pending') and the outstanding-request count, and sendPacketFsm picks the request up on the next service tick.",
    "In:  none (txPacketLength/txPacketBuffer already filled by the caller)",
    "Out: exchangeFlags +1, sendRequestCount +1",
    "Called from: commRequestDispatch $E114"
   ],
   "line": "set bit 0 of exchangeFlags: a send is pending"
  },
  "E12B": {
   "unit": "game/modemDriverE000",
   "line": "count the request so sendPacketFsm knows the game is waiting"
  },
  "E12E": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E12F": {
   "unit": "game/modemDriverE000",
   "routine": [
    "serviceCommTick - $E000 with X=0, called from the raster IRQ once per frame. The DEC/INC pair around the body is a re-entrancy lock (serviceLock rests at 1, so only the outermost call runs). It always drives the modem/connection state machine, and only once the connection has reached phase 3 with the link up does it also run the packet receive and packet transmit state machines.",
    "In:  serviceLock, connectionPhase, isLinkActive",
    "Out: drives the whole stack; serviceLock restored to 1",
    "Called from: commRequestDispatch $E112"
   ],
   "line": "take the re-entrancy lock (rests at 1, so this reaches 0 only in the outermost call)"
  },
  "E132": {
   "unit": "game/modemDriverE000",
   "line": "already inside the service - do nothing this frame"
  },
  "E134": {
   "unit": "game/modemDriverE000",
   "line": "always drive the modem / connection state machine"
  },
  "E137": {
   "unit": "game/modemDriverE000",
   "line": "how far has the connection got?"
  },
  "E13A": {
   "unit": "game/modemDriverE000",
   "line": "phase 3 = the packet protocol is running"
  },
  "E13C": {
   "unit": "game/modemDriverE000",
   "line": "not connected yet - no packet traffic"
  },
  "E13E": {
   "unit": "game/modemDriverE000",
   "line": "is the UART actually up?"
  },
  "E141": {
   "unit": "game/modemDriverE000",
   "line": "link down - skip the packet layer"
  },
  "E143": {
   "unit": "game/modemDriverE000",
   "line": "collect anything the opponent has sent"
  },
  "E146": {
   "unit": "game/modemDriverE000",
   "line": "and push out whatever we have to say"
  },
  "E149": {
   "unit": "game/modemDriverE000",
   "line": "release the lock"
  },
  "E14C": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E14D": {
   "unit": "game/modemDriverE000",
   "routine": [
    "receivePacketFsm - the packet-layer receiver, a resumable state machine held in rxPacketState (0 = expecting a header, 1/2 = collecting a command payload, $80 = collecting chat bytes). The header byte's low three bits are the payload length; bit 7 means the payload is chat text that goes straight into hostInRing, otherwise it is a game command packet collected in rxPacketBuffer with the length in rxPacketLength. Headers $70-$7F are a third case: the payload is appended to the downloadable-code buffer at $EB03, and a $7x header with no payload calls it. A finished command packet sets bit 7 of exchangeFlags.",
    "In:  rxPacketState, rxBytesRemaining, rxHeaderByte, bytes from receiveLinkByte",
    "Out: rxPacketBuffer, rxPacketLength, exchangeFlags bit 7, hostInRing, the $EB03 buffer",
    "Called from: serviceCommTick $E143"
   ],
   "line": "where did we leave off?"
  },
  "E150": {
   "unit": "game/modemDriverE000",
   "line": "state $80: we are in the middle of a chat payload"
  },
  "E152": {
   "unit": "game/modemDriverE000",
   "line": "state 0 -> $FF, 1 -> 0, 2 -> 1"
  },
  "E153": {
   "unit": "game/modemDriverE000",
   "line": "state 1: the header is in, start the packet"
  },
  "E155": {
   "unit": "game/modemDriverE000",
   "line": "state 2 or more: keep collecting payload bytes"
  },
  "E157": {
   "unit": "game/modemDriverE000",
   "block": [
    "State 0: read the header byte and work out what kind of frame is arriving."
   ],
   "line": "state 0: try to read a header byte"
  },
  "E15A": {
   "unit": "game/modemDriverE000",
   "line": "nothing has arrived yet - come back next frame"
  },
  "E15C": {
   "unit": "game/modemDriverE000",
   "line": "keep the header in X"
  },
  "E15D": {
   "unit": "game/modemDriverE000",
   "line": "remember it for the $70-$7F test later"
  },
  "E160": {
   "unit": "game/modemDriverE000",
   "line": "the low three bits are the payload length"
  },
  "E162": {
   "unit": "game/modemDriverE000",
   "line": "that many payload bytes to collect"
  },
  "E165": {
   "unit": "game/modemDriverE000",
   "line": "look at the header again"
  },
  "E166": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 set: the payload is chat text for the game's message line"
  },
  "E168": {
   "unit": "game/modemDriverE000",
   "line": "command packet: advance to state 1"
  },
  "E16B": {
   "unit": "game/modemDriverE000",
   "line": "has the game read the previous packet yet (bit 7 of exchangeFlags)?"
  },
  "E16E": {
   "unit": "game/modemDriverE000",
   "line": "no - leave the payload in the ring and wait"
  },
  "E170": {
   "unit": "game/modemDriverE000",
   "routine": [
    "beginReceivePacket - the 'a packet is starting' step of receivePacketFsm: clear the received length and add 2 to exchangeFlags to mark 'reception in progress', then fall into the payload loop. Reached by fall-through here; the solo-trainer build calls it as a subroutine from $E07E.",
    "In:  rxBytesRemaining = payload length",
    "Out: rxPacketLength = 0, exchangeFlags +2",
    "Called from: fall-through from $E16E in this build"
   ],
   "line": "start a fresh packet"
  },
  "E172": {
   "unit": "game/modemDriverE000",
   "line": "nothing stored yet"
  },
  "E175": {
   "unit": "game/modemDriverE000",
   "line": "add 2 to exchangeFlags: reception in progress"
  },
  "E178": {
   "unit": "game/modemDriverE000",
   "line": "(two INCs because there is no ADC-style flag setter here)"
  },
  "E17B": {
   "unit": "game/modemDriverE000",
   "line": "how long is the payload?"
  },
  "E17E": {
   "unit": "game/modemDriverE000",
   "line": "a header-only packet - go straight to the delivery step"
  },
  "E180": {
   "unit": "game/modemDriverE000",
   "line": "advance to state 2: collecting the payload"
  },
  "E183": {
   "unit": "game/modemDriverE000",
   "block": [
    "Payload loop: read the remaining bytes of a command packet into rxPacketBuffer, resuming from",
    "state 2 on a later frame whenever the link runs dry."
   ],
   "line": "read the next payload byte"
  },
  "E186": {
   "unit": "game/modemDriverE000",
   "line": "nothing yet - resume from state 2 next frame"
  },
  "E188": {
   "unit": "game/modemDriverE000",
   "line": "append it to the receive buffer"
  },
  "E18B": {
   "unit": "game/modemDriverE000",
   "line": "rxPacketBuffer holds the command byte and up to 6 arguments"
  },
  "E18E": {
   "unit": "game/modemDriverE000",
   "line": "one more byte stored"
  },
  "E191": {
   "unit": "game/modemDriverE000",
   "line": "one fewer to go"
  },
  "E194": {
   "unit": "game/modemDriverE000",
   "line": "keep reading until the payload is complete"
  },
  "E196": {
   "unit": "game/modemDriverE000",
   "block": [
    "The packet is complete. Headers $70-$7F carry 6502 code for the downloadable-code buffer at",
    "$EB03; everything else is a game command packet, which is announced by setting bit 7 of",
    "exchangeFlags so the main program can pick it up."
   ],
   "line": "look at the header again"
  },
  "E199": {
   "unit": "game/modemDriverE000",
   "line": "headers $70-$7F address the downloadable-code buffer"
  },
  "E19B": {
   "unit": "game/modemDriverE000",
   "line": "an ordinary game command packet - just flag it"
  },
  "E19D": {
   "unit": "game/modemDriverE000",
   "line": "did this $7x message carry a payload?"
  },
  "E1A0": {
   "unit": "game/modemDriverE000",
   "line": "yes - append it to the code buffer"
  },
  "E1A2": {
   "unit": "game/modemDriverE000",
   "line": "no payload: call whatever the peer has downloaded ($EB03 holds a bare RTS in the shipped image)"
  },
  "E1A5": {
   "unit": "game/modemDriverE000",
   "line": "and reset the buffer write offset"
  },
  "E1A7": {
   "unit": "game/modemDriverE000",
   "line": "always taken (Z was set by the LDX)"
  },
  "E1A9": {
   "unit": "game/modemDriverE000",
   "line": "Y is in use by the caller"
  },
  "E1AA": {
   "unit": "game/modemDriverE000",
   "line": "so save it"
  },
  "E1AB": {
   "unit": "game/modemDriverE000",
   "line": "read the payload from the start"
  },
  "E1AD": {
   "unit": "game/modemDriverE000",
   "line": "operand patched at $E1BE: where in the code buffer to continue writing"
  },
  "E1AF": {
   "unit": "game/modemDriverE000",
   "line": "next payload byte"
  },
  "E1B2": {
   "unit": "game/modemDriverE000",
   "line": "advance through the received payload"
  },
  "E1B3": {
   "unit": "game/modemDriverE000",
   "line": "append it to the downloadable-code buffer at $EB03"
  },
  "E1B6": {
   "unit": "game/modemDriverE000",
   "line": "advance the buffer offset"
  },
  "E1B7": {
   "unit": "game/modemDriverE000",
   "line": "count the byte off"
  },
  "E1BA": {
   "unit": "game/modemDriverE000",
   "line": "until the whole payload has been copied"
  },
  "E1BC": {
   "unit": "game/modemDriverE000",
   "line": "restore the caller's Y"
  },
  "E1BD": {
   "unit": "game/modemDriverE000",
   "line": "restore the caller's Y"
  },
  "E1BE": {
   "unit": "game/modemDriverE000",
   "line": "self-modify: remember the buffer offset for the next $7x message"
  },
  "E1C1": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 = 'a packet from the opponent is waiting'"
  },
  "E1C3": {
   "unit": "game/modemDriverE000",
   "line": "flag it (and complete the exchange if our own packet has gone out)"
  },
  "E1C6": {
   "unit": "game/modemDriverE000",
   "line": "back to state 0, expecting the next header"
  },
  "E1C8": {
   "unit": "game/modemDriverE000",
   "line": "store the new receive state"
  },
  "E1CB": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E1CC": {
   "unit": "game/modemDriverE000",
   "block": [
    "Chat payload: the header had bit 7 set, so its 0-3 bytes are text that goes straight into the",
    "ring the game reads with $E006 - it never touches the command packet buffer."
   ],
   "line": "chat payload: read one character"
  },
  "E1CF": {
   "unit": "game/modemDriverE000",
   "line": "the state to save if nothing has arrived: stay in 'chat payload'"
  },
  "E1D1": {
   "unit": "game/modemDriverE000",
   "line": "nothing yet - save state $80 and resume next frame"
  },
  "E1D3": {
   "unit": "game/modemDriverE000",
   "line": "hand the character to the game's message line"
  },
  "E1D6": {
   "unit": "game/modemDriverE000",
   "line": "one fewer chat byte to go"
  },
  "E1D9": {
   "unit": "game/modemDriverE000",
   "line": "keep going while the frame has more"
  },
  "E1DB": {
   "unit": "game/modemDriverE000",
   "line": "frame complete - back to state 0 (always taken)"
  },
  "E1DD": {
   "unit": "game/modemDriverE000",
   "routine": [
    "sendPacketFsm - the packet-layer transmitter, a resumable state machine in txPacketState (bit 0 = 'header sent, payload in progress', bit 1 = 'frame handed over, waiting for it to drain', bit 7 = 'that frame was chat, not a game packet'). With nothing in flight it prefers the pending game command packet (header = txPacketLength, then that many bytes from txPacketBuffer); otherwise it emits a chat frame with header $80|count and up to three characters from hostOutRing, which doubles as the keep-alive when both sides are idle. When the frame has drained it sets bit 5 of exchangeFlags and clears the state.",
    "In:  txPacketState, sendRequestCount, hostOutCount, txPacketLength, txPacketBuffer, packetInFlightCount",
    "Out: bytes handed to sendLinkByte, packetInFlightCount = bytes+1, exchangeFlags bits 3/5, txPayloadIndex, chatFrameCounter",
    "Called from: serviceCommTick $E146"
   ],
   "line": "what is the transmitter doing?"
  },
  "E1E0": {
   "unit": "game/modemDriverE000",
   "line": "bit 0 into carry"
  },
  "E1E1": {
   "unit": "game/modemDriverE000",
   "line": "a payload is half way out - carry on where we stopped"
  },
  "E1E3": {
   "unit": "game/modemDriverE000",
   "line": "bit 1 into carry"
  },
  "E1E4": {
   "unit": "game/modemDriverE000",
   "line": "nothing in flight - decide what to send"
  },
  "E1E6": {
   "unit": "game/modemDriverE000",
   "line": "a frame has been handed over - just watch for it to drain"
  },
  "E1E9": {
   "unit": "game/modemDriverE000",
   "block": [
    "Nothing is in flight: decide between the game's command packet, a chat frame, and the bare",
    "keep-alive frame that keeps the exchange ticking over when neither side has anything to say."
   ],
   "line": "1: the packet layer waits on packetInFlightCount"
  },
  "E1EC": {
   "unit": "game/modemDriverE000",
   "line": "is the previous frame still on its way out?"
  },
  "E1EF": {
   "unit": "game/modemDriverE000",
   "line": "yes - nothing to do this tick"
  },
  "E1F1": {
   "unit": "game/modemDriverE000",
   "line": "A is 0 here, so C=1 means no send has been requested"
  },
  "E1F4": {
   "unit": "game/modemDriverE000",
   "line": "any chat characters queued?"
  },
  "E1F7": {
   "unit": "game/modemDriverE000",
   "line": "none - only a command packet could go now"
  },
  "E1F9": {
   "unit": "game/modemDriverE000",
   "line": "chat waiting and no packet requested - send a chat frame"
  },
  "E1FB": {
   "unit": "game/modemDriverE000",
   "line": "a packet was requested: is there anything in either packet buffer?"
  },
  "E1FE": {
   "unit": "game/modemDriverE000",
   "line": "and the length of the packet just received"
  },
  "E201": {
   "unit": "game/modemDriverE000",
   "line": "yes - send the command packet"
  },
  "E203": {
   "unit": "game/modemDriverE000",
   "line": "$08 = the idle keep-alive marker"
  },
  "E205": {
   "unit": "game/modemDriverE000",
   "line": "has that marker already been set in exchangeFlags?"
  },
  "E208": {
   "unit": "game/modemDriverE000",
   "line": "not yet - set it and send one bare frame to keep the link ticking over"
  },
  "E20A": {
   "unit": "game/modemDriverE000",
   "line": "no send requested (C=1) - nothing to do"
  },
  "E20C": {
   "unit": "game/modemDriverE000",
   "block": [
    "Send a game command packet: header = payload length, then the bytes the game left in",
    "txPacketBuffer. Any full ring simply stops the loop; the state byte makes it resume here."
   ],
   "line": "the header is simply the payload length"
  },
  "E20F": {
   "unit": "game/modemDriverE000",
   "line": "plus txHeaderExtraBits, which is always 0 in this build"
  },
  "E212": {
   "unit": "game/modemDriverE000",
   "line": "hand the header to the link"
  },
  "E215": {
   "unit": "game/modemDriverE000",
   "line": "the link ring is full - try again next tick"
  },
  "E217": {
   "unit": "game/modemDriverE000",
   "line": "0 into both counters below"
  },
  "E219": {
   "unit": "game/modemDriverE000",
   "line": "start the payload at byte 0"
  },
  "E21C": {
   "unit": "game/modemDriverE000",
   "line": "the request has been taken"
  },
  "E21F": {
   "unit": "game/modemDriverE000",
   "line": "set bit 0: payload in progress"
  },
  "E222": {
   "unit": "game/modemDriverE000",
   "line": "how many payload bytes have gone?"
  },
  "E225": {
   "unit": "game/modemDriverE000",
   "line": "all of them?"
  },
  "E228": {
   "unit": "game/modemDriverE000",
   "line": "yes - the frame is complete"
  },
  "E22A": {
   "unit": "game/modemDriverE000",
   "line": "next byte of the command the game built"
  },
  "E22D": {
   "unit": "game/modemDriverE000",
   "line": "hand it to the link"
  },
  "E230": {
   "unit": "game/modemDriverE000",
   "line": "ring full - resume from this byte next tick"
  },
  "E232": {
   "unit": "game/modemDriverE000",
   "line": "one more byte away"
  },
  "E235": {
   "unit": "game/modemDriverE000",
   "line": "keep going (always taken)"
  },
  "E237": {
   "unit": "game/modemDriverE000",
   "line": "A=$08: remember that the idle keep-alive has been sent"
  },
  "E23A": {
   "unit": "game/modemDriverE000",
   "block": [
    "Send a chat frame: header $80 | count, then up to three characters from hostOutRing. With the",
    "count at zero this is the one-byte $80 keep-alive."
   ],
   "line": "how many chat characters are waiting?"
  },
  "E23D": {
   "unit": "game/modemDriverE000",
   "line": "at most three travel in one frame"
  },
  "E23F": {
   "unit": "game/modemDriverE000",
   "line": "fewer than three - send them all"
  },
  "E241": {
   "unit": "game/modemDriverE000",
   "line": "clamp to three"
  },
  "E243": {
   "unit": "game/modemDriverE000",
   "line": "that many payload bytes"
  },
  "E246": {
   "unit": "game/modemDriverE000",
   "line": "and that many pops to do"
  },
  "E249": {
   "unit": "game/modemDriverE000",
   "line": "chat frames are marked by bit 7 of the header"
  },
  "E24B": {
   "unit": "game/modemDriverE000",
   "line": "send the header, then each character in turn"
  },
  "E24E": {
   "unit": "game/modemDriverE000",
   "line": "one fewer to pop"
  },
  "E251": {
   "unit": "game/modemDriverE000",
   "line": "all sent"
  },
  "E253": {
   "unit": "game/modemDriverE000",
   "line": "take the next character out of the host output ring"
  },
  "E256": {
   "unit": "game/modemDriverE000",
   "line": "and send it (C=1 would mean the ring lied about its count)"
  },
  "E258": {
   "unit": "game/modemDriverE000",
   "line": "state $81: a chat frame is on its way (bit 7 keeps it out of the exchange flags)"
  },
  "E25A": {
   "unit": "game/modemDriverE000",
   "line": "remember it"
  },
  "E25D": {
   "unit": "game/modemDriverE000",
   "line": "the payload byte count"
  },
  "E260": {
   "unit": "game/modemDriverE000",
   "line": "plus the header byte"
  },
  "E261": {
   "unit": "game/modemDriverE000",
   "line": "into A for the in-flight counter"
  },
  "E262": {
   "unit": "game/modemDriverE000",
   "line": "1: the in-flight counter lives at $E0A6"
  },
  "E265": {
   "unit": "game/modemDriverE000",
   "line": "tell the frame layer how many bytes belong to this packet"
  },
  "E268": {
   "unit": "game/modemDriverE000",
   "line": "state -> 2 (or $82 for chat): waiting for the frame to drain"
  },
  "E26B": {
   "unit": "game/modemDriverE000",
   "block": [
    "Both paths end here: wait until the frame layer says the bytes have really gone, then flag",
    "'own packet sent' (chat frames are excluded) and go idle."
   ],
   "line": "1: read the in-flight counter"
  },
  "E26E": {
   "unit": "game/modemDriverE000",
   "line": "has the frame left the machine?"
  },
  "E271": {
   "unit": "game/modemDriverE000",
   "line": "not yet - keep waiting"
  },
  "E273": {
   "unit": "game/modemDriverE000",
   "line": "was it a chat frame (bit 7)?"
  },
  "E276": {
   "unit": "game/modemDriverE000",
   "line": "yes - chat does not complete a game exchange"
  },
  "E278": {
   "unit": "game/modemDriverE000",
   "line": "bit 5 = 'our own packet has gone out'"
  },
  "E27A": {
   "unit": "game/modemDriverE000",
   "line": "flag it (and complete the exchange if the opponent's packet has arrived)"
  },
  "E27D": {
   "unit": "game/modemDriverE000",
   "line": "transmitter idle again"
  },
  "E27F": {
   "unit": "game/modemDriverE000",
   "line": "clear the transmit state"
  },
  "E282": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E283": {
   "unit": "game/modemDriverE000",
   "routine": [
    "countCompletedExchange - bumps the 16-bit exchangeCounter and then sets bit 6 of exchangeFlags ('exchange complete'). Reached from setExchangeFlags exactly once per finished send+receive pair. Nothing in the whole image ever reads exchangeCounter, so it looks like a leftover debug statistic.",
    "In:  exchangeCounterLo/Hi",
    "Out: exchangeCounter +1, falls into setExchangeFlags with A=$40",
    "Called from: setExchangeFlags $E297"
   ],
   "line": "count this completed exchange (low byte)"
  },
  "E286": {
   "unit": "game/modemDriverE000",
   "line": "no carry into the high byte"
  },
  "E288": {
   "unit": "game/modemDriverE000",
   "line": "16-bit counter that nothing ever reads"
  },
  "E28B": {
   "unit": "game/modemDriverE000",
   "line": "bit 6 = 'exchange complete', which is what the game waits for"
  },
  "E28D": {
   "unit": "game/modemDriverE000",
   "routine": [
    "setExchangeFlags - ORs A into exchangeFlags and checks for a completed exchange: when bits 7 and 5 are both set but bit 6 is not (top three bits = %101) the round trip is done, so it drops into countCompletedExchange which sets bit 6 as well. A=$80 marks 'opponent packet received', $20 'own packet sent', $40 'complete', $08 the idle keep-alive marker.",
    "In:  A = flag bits to add",
    "Out: exchangeFlags updated, possibly exchangeCounter",
    "Called from: receivePacketFsm $E1C3, sendPacketFsm $E237/$E27A"
   ],
   "line": "add the new flag bits"
  },
  "E290": {
   "unit": "game/modemDriverE000",
   "line": "store the updated flags"
  },
  "E293": {
   "unit": "game/modemDriverE000",
   "line": "look at bits 7, 6 and 5 only"
  },
  "E295": {
   "unit": "game/modemDriverE000",
   "line": "received + sent but not yet marked complete?"
  },
  "E297": {
   "unit": "game/modemDriverE000",
   "line": "yes - the round trip is finished"
  },
  "E299": {
   "unit": "game/modemDriverE000",
   "line": "otherwise just return with the flag added"
  },
  "E29A": {
   "unit": "game/modemDriverE000",
   "routine": [
    "commLinkControlDispatch - the dispatcher behind $E003. X=0 opens the link, X=1 cold-initialises the module, X=2 stops the NMI UART and clears the link variables, X>=3 first sends the Hayes hang-up string, spins until it has gone out, and then does the same shutdown.",
    "In:  X = function code",
    "Out: see the four cases; every path except X=0/X=1 ends by zeroing $E039-$E047",
    "Called from: the $E003 jump-table entry"
   ],
   "line": "X=0 -> $FF, 1 -> 0, 2 -> 1, 3 -> 2"
  },
  "E29B": {
   "unit": "game/modemDriverE000",
   "line": "X was 0: open the link"
  },
  "E29D": {
   "unit": "game/modemDriverE000",
   "line": "X was 1: cold-initialise the module"
  },
  "E29F": {
   "unit": "game/modemDriverE000",
   "line": "X=2 -> $FF, X=3 -> 0"
  },
  "E2A0": {
   "unit": "game/modemDriverE000",
   "line": "X was 2: shut the UART down without hanging up"
  },
  "E2A2": {
   "unit": "game/modemDriverE000",
   "line": "X>=3: queue '+++' / ATH0 first"
  },
  "E2A5": {
   "unit": "game/modemDriverE000",
   "line": "wait for the hang-up string to be delivered"
  },
  "E2A8": {
   "unit": "game/modemDriverE000",
   "line": "modemReplyTimer 0 or 1 means the modem has answered / given up"
  },
  "E2A9": {
   "unit": "game/modemDriverE000",
   "line": "spin until it does"
  },
  "E2AB": {
   "unit": "game/modemDriverE000",
   "line": "stop the NMI UART"
  },
  "E2AE": {
   "unit": "game/modemDriverE000",
   "line": "and zero the link variables (its RTS returns to the $E003 caller)"
  },
  "E2B1": {
   "unit": "game/modemDriverE000",
   "routine": [
    "initCommModule (the $E003 X=1 case) - cold start, called once immediately after the overlay has been loaded. Clears the protocol block, the packet block, all four ring buffers and the ARQ/frame state block, and points the three indirect link I/O jumps at their framed implementations.",
    "In:  none",
    "Out: $E07D-$E0A7, $E01D-$E02F, $E414-$E46D and $EAB7-$EB02 zeroed; the operands at $E3B7/$E3BA/$E3BD patched",
    "Called from: commLinkControlDispatch $E29D (reached from initCommModule $0F46 with X=1)"
   ],
   "line": "wipe the protocol block $E07D-$E0A7"
  },
  "E2B4": {
   "unit": "game/modemDriverE000",
   "line": "wipe the packet block $E01D-$E02F"
  },
  "E2B7": {
   "unit": "game/modemDriverE000",
   "line": "wipe all four ring buffers $E414-$E46D"
  },
  "E2BA": {
   "unit": "game/modemDriverE000",
   "line": "route link I/O through the framed (ARQ) rings"
  },
  "E2BD": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearArqVars - tail of initCommModule: zero the 76-byte frame/ARQ state block $EAB7-$EB02 (sync counters, sequence numbers, the retransmit ring, the packet assembly buffer and the modem delay bytes).",
    "In:  none",
    "Out: $EAB7-$EB02 = 0",
    "Called from: fall-through from initCommModule $E2BA"
   ],
   "line": "wipe the frame/ARQ state block $EAB7-$EB02"
  },
  "E2C0": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E2C1": {
   "unit": "game/modemDriverE000",
   "routine": [
    "openCommLink (the $E003 X=0 case) - bring the link up. If it is not already up it loads the baud parameters for the selected speed, installs this module's NMI vector, starts the software UART and marks the link active. It then always re-programs the user-port lines and, unless the C=+RETURN hot key asked for a silent re-open, queues the Hayes init string for answer or originate mode. Finally it releases the per-frame service lock.",
    "In:  isLinkActive, baudIndex, skipModemInitString, isOriginateMode",
    "Out: isLinkActive = 1, NMI vector at $FFFA/$FFFB, CIA2 configured, an AT string queued, serviceLock = 1",
    "Called from: commLinkControlDispatch $E29B, and the C=+RETURN hot key at $E369"
   ],
   "line": "is the UART already running?"
  },
  "E2C4": {
   "unit": "game/modemDriverE000",
   "line": "yes - only refresh the port lines and the AT string"
  },
  "E2C6": {
   "unit": "game/modemDriverE000",
   "line": "0 = 300 baud, 3 = 1200 baud"
  },
  "E2C9": {
   "unit": "game/modemDriverE000",
   "line": "load that speed's timer values"
  },
  "E2CC": {
   "unit": "game/modemDriverE000",
   "line": "point $FFFA/$FFFB at commNmiHandler"
  },
  "E2CF": {
   "unit": "game/modemDriverE000",
   "line": "clear the bit-level state and arm the CIA2 NMI sources"
  },
  "E2D2": {
   "unit": "game/modemDriverE000",
   "line": "the link is now up"
  },
  "E2D5": {
   "unit": "game/modemDriverE000",
   "line": "set RTS/DTR, DDRB and the idle TXD level"
  },
  "E2D8": {
   "unit": "game/modemDriverE000",
   "line": "did C=+RETURN ask for a silent re-open?"
  },
  "E2DB": {
   "unit": "game/modemDriverE000",
   "line": "yes - do not send the AT string again"
  },
  "E2DD": {
   "unit": "game/modemDriverE000",
   "line": "0 = answer, 1 = originate"
  },
  "E2E0": {
   "unit": "game/modemDriverE000",
   "line": "low byte of 'ATQ0V1X1A' ($E3) or 'ATQ0V1X1D' ($F0), both stored back to front"
  },
  "E2E3": {
   "unit": "game/modemDriverE000",
   "line": "load it into the host output ring for the terminal-mode transmitter"
  },
  "E2E6": {
   "unit": "game/modemDriverE000",
   "line": "the per-frame service lock rests at 1"
  },
  "E2E8": {
   "unit": "game/modemDriverE000",
   "line": "release it, so serviceCommTick may run"
  },
  "E2EB": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E2EC": {
   "unit": "game/modemDriverE000",
   "routine": [
    "readKeyAndHandleModemHotkeys - the implementation of $E015. It scans the keyboard through the patched hook, optionally repaints the border with the link-status colour, and then interprets a Commodore-key combination as a modem hot key: H toggles DTR, C toggles the carrier override, 3 and 1 pick 300/1200 baud, RETURN re-opens the link without re-sending the init string, P hangs up, A selects answer mode and O selects originate mode. A hot key is swallowed by returning $FF so the game never sees it.",
    "In:  the keyboard hook result (A = key code, X bit 7 = Commodore key), isLinkActive, linkStatus, connectionPhase, lastHotkeyCode",
    "Out: A = key code or $FF; may write VIC_BORDER, lastHotkeyCode, isOriginateMode, baudIndex, carrierOverrideFlags, CIA2_PRB, or queue a modem command string",
    "Called from: the $E015 jump-table entry"
   ],
   "line": "scan the keyboard (the game patched this vector to $0DB7)"
  },
  "E2EF": {
   "unit": "game/modemDriverE000",
   "line": "keep the key code"
  },
  "E2F0": {
   "unit": "game/modemDriverE000",
   "block": [
    "Link status indicator: while the link is up the border shows the connection phase and carrier",
    "state. Phase 1 (terminal mode) paints unconditionally, the other phases only while the",
    "Commodore key is held down, which is also when the hot keys below are active."
   ],
   "line": "is there a link to report on?"
  },
  "E2F3": {
   "unit": "game/modemDriverE000",
   "line": "no - leave the border alone"
  },
  "E2F5": {
   "unit": "game/modemDriverE000",
   "line": "self-modify: stash the Commodore-key flag, because the border code needs X"
  },
  "E2F8": {
   "unit": "game/modemDriverE000",
   "line": "debounced link status"
  },
  "E2FB": {
   "unit": "game/modemDriverE000",
   "line": "shift bit 7 (data flowing) out"
  },
  "E2FC": {
   "unit": "game/modemDriverE000",
   "line": "now carry holds bit 6 = carrier present"
  },
  "E2FD": {
   "unit": "game/modemDriverE000",
   "line": "connection phase 0-3"
  },
  "E300": {
   "unit": "game/modemDriverE000",
   "line": "index = phase*2 + carrier"
  },
  "E301": {
   "unit": "game/modemDriverE000",
   "line": "into X to index the colour table"
  },
  "E302": {
   "unit": "game/modemDriverE000",
   "line": "colour*2 + 'always paint' flag"
  },
  "E305": {
   "unit": "game/modemDriverE000",
   "line": "carry = always-paint flag, A = the colour"
  },
  "E306": {
   "unit": "game/modemDriverE000",
   "line": "operand patched at $E2F5: $80 while the Commodore key is held"
  },
  "E308": {
   "unit": "game/modemDriverE000",
   "line": "Commodore key held - show the link status whatever the phase"
  },
  "E30A": {
   "unit": "game/modemDriverE000",
   "line": "otherwise only phases 1 (terminal mode) paint on their own"
  },
  "E30C": {
   "unit": "game/modemDriverE000",
   "line": "paint the border with the link status colour"
  },
  "E30F": {
   "unit": "game/modemDriverE000",
   "line": "get the key code back"
  },
  "E310": {
   "unit": "game/modemDriverE000",
   "line": "codes below $80 are ordinary keys, never hot keys"
  },
  "E312": {
   "unit": "game/modemDriverE000",
   "line": "the same code as last time?"
  },
  "E315": {
   "unit": "game/modemDriverE000",
   "line": "no - a fresh key press"
  },
  "E317": {
   "unit": "game/modemDriverE000",
   "line": "same key: is the Commodore key still down?"
  },
  "E319": {
   "unit": "game/modemDriverE000",
   "line": "yes - swallow the repeat"
  },
  "E31B": {
   "unit": "game/modemDriverE000",
   "line": "hand the key to the game"
  },
  "E31C": {
   "unit": "game/modemDriverE000",
   "block": [
    "Modem hot keys - a new key code with bit 7 set while the Commodore key is held. Every one of them",
    "is swallowed by returning $FF so the game never sees it."
   ],
   "line": "remember the code so it is not handled twice"
  },
  "E31F": {
   "unit": "game/modemDriverE000",
   "line": "was the Commodore key held?"
  },
  "E321": {
   "unit": "game/modemDriverE000",
   "line": "no - it is an ordinary key for the game"
  },
  "E323": {
   "unit": "game/modemDriverE000",
   "line": "C=+H"
  },
  "E325": {
   "unit": "game/modemDriverE000",
   "line": "not that key - try the next"
  },
  "E327": {
   "unit": "game/modemDriverE000",
   "line": "$24 = PB2 (DTR) + PB5"
  },
  "E32A": {
   "unit": "game/modemDriverE000",
   "line": "flip those two user-port outputs"
  },
  "E32D": {
   "unit": "game/modemDriverE000",
   "line": "write the toggled pattern back to the user port"
  },
  "E330": {
   "unit": "game/modemDriverE000",
   "line": "carry is still set from the CPX - swallow the key"
  },
  "E332": {
   "unit": "game/modemDriverE000",
   "line": "C=+C"
  },
  "E334": {
   "unit": "game/modemDriverE000",
   "line": "not that key - try the next"
  },
  "E336": {
   "unit": "game/modemDriverE000",
   "line": "$40 = the carrier-override bit"
  },
  "E339": {
   "unit": "game/modemDriverE000",
   "line": "flip 'pretend the carrier is present'"
  },
  "E33C": {
   "unit": "game/modemDriverE000",
   "line": "(lets two machines play over a direct cable with no DCD)"
  },
  "E33F": {
   "unit": "game/modemDriverE000",
   "line": "$FF tells the game 'no key'"
  },
  "E341": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E342": {
   "unit": "game/modemDriverE000",
   "line": "C=+3"
  },
  "E344": {
   "unit": "game/modemDriverE000",
   "line": "not that key - try the next"
  },
  "E346": {
   "unit": "game/modemDriverE000",
   "line": "baud table entry 0 = 300 baud"
  },
  "E348": {
   "unit": "game/modemDriverE000",
   "line": "always taken"
  },
  "E34A": {
   "unit": "game/modemDriverE000",
   "line": "C=+1"
  },
  "E34C": {
   "unit": "game/modemDriverE000",
   "line": "not a baud key - try the rest"
  },
  "E34E": {
   "unit": "game/modemDriverE000",
   "line": "baud table entry 1 (offset 3) = 1200 baud"
  },
  "E350": {
   "unit": "game/modemDriverE000",
   "line": "remember the speed for the next open"
  },
  "E353": {
   "unit": "game/modemDriverE000",
   "routine": [
    "loadBaudParameters - copies the three-byte baud entry at baudParameterTable+X into bitPeriodLo/bitPeriodHi/txPaceReload. The destination walk uses Y = $FD..$FF against the base $DF59, because $DF59+$FD = $E056, so one register both counts and indexes. X=0 selects $0D50 cycles = 300 baud with pacing 1, X=3 selects $0353 = 1200 baud with pacing 2. Returns A=$FF, which is what the hot-key path needs to swallow the key.",
    "In:  X = 0 (300 baud) or 3 (1200 baud)",
    "Out: bitPeriodLo/bitPeriodHi/txPaceReload, A = $FF",
    "Called from: openCommLink $E2C9, the C=+3 / C=+1 hot keys (fall-through from $E350), runModemStateMachine $E7B2 when it reads a 'CONNECT 1200' result code"
   ],
   "line": "Y counts $FD, $FE, $FF so that $DF59+Y walks $E056, $E057, $E058"
  },
  "E355": {
   "unit": "game/modemDriverE000",
   "line": "next byte of the selected baud entry"
  },
  "E358": {
   "unit": "game/modemDriverE000",
   "line": "$DF59+Y = bitPeriodLo / bitPeriodHi / txPaceReload"
  },
  "E35B": {
   "unit": "game/modemDriverE000",
   "line": "next source byte"
  },
  "E35C": {
   "unit": "game/modemDriverE000",
   "line": "next destination byte"
  },
  "E35D": {
   "unit": "game/modemDriverE000",
   "line": "three bytes copied when Y wraps to 0"
  },
  "E35F": {
   "unit": "game/modemDriverE000",
   "line": "swallow the hot key"
  },
  "E361": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E362": {
   "unit": "game/modemDriverE000",
   "line": "C=+RETURN"
  },
  "E364": {
   "unit": "game/modemDriverE000",
   "line": "not that key - try the next"
  },
  "E366": {
   "unit": "game/modemDriverE000",
   "line": "0 -> $FF: re-open without re-sending the AT string"
  },
  "E369": {
   "unit": "game/modemDriverE000",
   "line": "bring the link back up"
  },
  "E36C": {
   "unit": "game/modemDriverE000",
   "line": "a negative reply timer means 'not waiting for an answer'"
  },
  "E36E": {
   "unit": "game/modemDriverE000",
   "line": "so the state machine does not time the modem out"
  },
  "E371": {
   "unit": "game/modemDriverE000",
   "line": "clear the Commodore-key flag the caller sees"
  },
  "E373": {
   "unit": "game/modemDriverE000",
   "line": "and pass RETURN on to the game"
  },
  "E374": {
   "unit": "game/modemDriverE000",
   "line": "C=+P"
  },
  "E376": {
   "unit": "game/modemDriverE000",
   "line": "hang up (its tail returns $FF, so the key is swallowed)"
  },
  "E378": {
   "unit": "game/modemDriverE000",
   "line": "default: answer mode"
  },
  "E37A": {
   "unit": "game/modemDriverE000",
   "line": "C=+A"
  },
  "E37C": {
   "unit": "game/modemDriverE000",
   "line": "answer mode wanted"
  },
  "E37E": {
   "unit": "game/modemDriverE000",
   "line": "C=+O"
  },
  "E380": {
   "unit": "game/modemDriverE000",
   "line": "some other Commodore-key combination - just swallow it"
  },
  "E382": {
   "unit": "game/modemDriverE000",
   "line": "originate mode"
  },
  "E384": {
   "unit": "game/modemDriverE000",
   "line": "remember which AT string to send"
  },
  "E387": {
   "unit": "game/modemDriverE000",
   "line": "the port lines depend on answer/originate"
  },
  "E38A": {
   "unit": "game/modemDriverE000",
   "line": "swallow the hot key"
  },
  "E38C": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E38D": {
   "unit": "game/modemDriverE000",
   "routine": [
    "hangUpModem - drop DTR and then queue the canned Hayes hang-up string, which the terminal-mode transmitter emits in reverse as [guard] '+++' [guard] CR [guard] 'ATH0' CR.",
    "In:  hangUpStringPtrLo ($EBFD = $D6), modemOptionFlags bit 7",
    "Out: CIA2_PRB with DTR low, hostOutRing loaded with the escape/hang-up string, modemReplyTimer = $FF",
    "Called from: commLinkControlDispatch $E2A2 (X>=3), the C=+P hot key at $E376"
   ],
   "line": "drop DTR, which by itself makes most modems hang up"
  },
  "E390": {
   "unit": "game/modemDriverE000",
   "line": "$D6 = low byte of the '+++ ... ATH0' string at $EBD6"
  },
  "E393": {
   "unit": "game/modemDriverE000",
   "routine": [
    "queueModemCommandString - load a canned modem command string into the host-output ring. A is the low byte of an address in page $EB; the routine plants it in the LDA operand at modemStringLoad, copies bytes (including the terminating 0) forwards into the ring at modemCommandChars, then sets the write index to 0 and both the read index and the count to the number of characters. Since the ring pops with a decrementing index the string leaves the machine back to front, which is exactly why the AT strings at $EBD6-$EBFC are stored reversed. Does nothing but return A=$FF when bit 7 of modemOptionFlags says no modem is configured.",
    "In:  A = low byte of the string address in page $EB, modemOptionFlags",
    "Out: modemCommandChars.., hostOutReadIndex, hostOutWriteIndex, hostOutCount, modemReplyTimer = $FF, A = $FF",
    "Called from: openCommLink $E2E3, hangUpModem via fall-through from $E390"
   ],
   "line": "is a modem configured at all (bit 7)?"
  },
  "E396": {
   "unit": "game/modemDriverE000",
   "line": "no modem - just return $FF"
  },
  "E398": {
   "unit": "game/modemDriverE000",
   "line": "self-modify the LDA below with the string's low byte"
  },
  "E39B": {
   "unit": "game/modemDriverE000",
   "line": "start one before the first character"
  },
  "E39D": {
   "unit": "game/modemDriverE000",
   "line": "negative reply timer: a command string is going out"
  },
  "E3A0": {
   "unit": "game/modemDriverE000",
   "block": [
    "Copy loop: the string is copied forwards into the ring, terminator included, and the read index",
    "is then set to the last character so the descending pop order sends it in the right order."
   ],
   "line": "next character"
  },
  "E3A1": {
   "unit": "game/modemDriverE000",
   "line": "operand low byte patched at $E398; the page is fixed at $EB"
  },
  "E3A4": {
   "unit": "game/modemDriverE000",
   "line": "copy it into the host output ring from slot 1 upwards"
  },
  "E3A7": {
   "unit": "game/modemDriverE000",
   "line": "keep going until the terminating 0 has been copied"
  },
  "E3A9": {
   "unit": "game/modemDriverE000",
   "line": "A is 0: the ring is written from the bottom again"
  },
  "E3AC": {
   "unit": "game/modemDriverE000",
   "line": "read index = the character count, so the string pops out back to front"
  },
  "E3AF": {
   "unit": "game/modemDriverE000",
   "line": "that many characters to send (the terminator is never popped)"
  },
  "E3B2": {
   "unit": "game/modemDriverE000",
   "routine": [
    "returnNoKey - the shared 'no key / key handled' return: A = $FF. It is the disk-image target of the $E012 keyboard hook and the early exit of queueModemCommandString when no modem is configured.",
    "In:  none",
    "Out: A = $FF",
    "Called from: keyboardScanHook $E012 (before the game patches it), queueModemCommandString $E396, and by fall-through from $E3AF"
   ],
   "line": "$FF = 'no key' / 'handled'"
  },
  "E3B4": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E3B5": {
   "unit": "game/modemDriverE000",
   "line": "unreferenced pad byte between returnNoKey and the indirect jumps"
  },
  "E3B6": {
   "unit": "game/modemDriverE000",
   "routine": [
    "sendLinkByte - indirect 'give one byte to the link'. The operand byte at $E3B7 selects which half of linkIoVectorTable is used: $BF = raw (straight into the UART transmit ring) or $C1 = framed (into the ARQ transmit ring). initCommModule always selects the framed one, so the raw target is dead in the shipped game.",
    "In:  A = byte",
    "Out: as the selected ring push; C=1 when that ring is full",
    "Called from: sendPacketFsm $E212/$E22D/$E24B"
   ],
   "line": "operand low byte is $BF (raw UART ring) or $C1 (framed ARQ ring)"
  },
  "E3B9": {
   "unit": "game/modemDriverE000",
   "routine": [
    "receiveLinkByte - indirect 'take one byte from the link'. The operand at $E3BA selects $C3 = raw (pop the UART receive ring) or $C5 = framed (pop the ARQ receive ring, i.e. only payload the frame layer has already checksum-verified).",
    "In:  none",
    "Out: A = byte with C=0, or C=1 when nothing is queued",
    "Called from: receivePacketFsm $E157/$E183/$E1CC"
   ],
   "line": "operand low byte is $C3 (raw UART ring) or $C5 (framed ARQ ring)"
  },
  "E3BC": {
   "unit": "game/modemDriverE000",
   "routine": [
    "reportLinkError - indirect link-error hook, called by the NMI receiver on a framing error, a bad stop bit or a receive-ring overflow. The operand at $E3BD selects $C7 = the no-op stub or $C9 = the counting stub; initCommModule selects the counter, whose value makes the link state machine restart byte sync.",
    "In:  none",
    "Out: linkErrorCount +1 in the framed configuration",
    "Called from: commNmiHandler $E724"
   ],
   "line": "operand low byte is $C7 (ignore) or $C9 (count the error)"
  },
  "E3BF": {
   "unit": "game/modemDriverE000",
   "block": [
    "linkIoVectorTable: three pairs of alternative targets for the indirect jumps above. The first of",
    "each pair is the raw UART implementation, the second the framed (ARQ) one that",
    "selectFramedLinkVectors installs. Only the operand bytes of the JMPs are patched, never this table."
   ],
   "line": "send, raw: straight into the UART transmit ring (never selected)"
  },
  "E3C1": {
   "unit": "game/modemDriverE000",
   "line": "send, framed: into the ARQ transmit ring - the one initCommModule selects"
  },
  "E3C3": {
   "unit": "game/modemDriverE000",
   "line": "receive, raw: pop the UART receive ring (never selected)"
  },
  "E3C5": {
   "unit": "game/modemDriverE000",
   "line": "receive, framed: pop the checksum-verified ARQ receive ring"
  },
  "E3C7": {
   "unit": "game/modemDriverE000",
   "line": "error, raw: the no-op stub (never selected)"
  },
  "E3C9": {
   "unit": "game/modemDriverE000",
   "line": "error, framed: count it in linkErrorCount"
  },
  "E3CB": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pollCarrierState - implementation of $E00C. Takes a fresh carrier sample (which also services the NMI suspend handshake), then debounces it: while the sample disagrees with the accepted linkStatus it counts up, and only after 240 consecutive disagreeing samples does it accept the new value. In between it merges just bit 7 of the sample, the 'characters are still arriving' bit, which the game watches before hanging up. Bit 6 of linkStatus is the debounced 'carrier present' flag.",
    "In:  linkStatusSample, linkStatus, carrierDebounceCount",
    "Out: linkStatus, carrierDebounceCount",
    "Called from: the $E00C jump-table entry"
   ],
   "line": "run the suspend handshake and take a fresh carrier sample"
  },
  "E3CE": {
   "unit": "game/modemDriverE000",
   "line": "the new sample"
  },
  "E3D1": {
   "unit": "game/modemDriverE000",
   "line": "does it agree with the accepted status?"
  },
  "E3D4": {
   "unit": "game/modemDriverE000",
   "line": "yes - reset the debounce counter"
  },
  "E3D6": {
   "unit": "game/modemDriverE000",
   "line": "no - count one disagreeing sample"
  },
  "E3D9": {
   "unit": "game/modemDriverE000",
   "line": "how long has it disagreed?"
  },
  "E3DC": {
   "unit": "game/modemDriverE000",
   "line": "240 frames of disagreement (about 4 seconds)"
  },
  "E3DE": {
   "unit": "game/modemDriverE000",
   "line": "not yet - only take the activity bit for now"
  },
  "E3E0": {
   "unit": "game/modemDriverE000",
   "line": "accept the new carrier state"
  },
  "E3E3": {
   "unit": "game/modemDriverE000",
   "line": "restart the debounce count"
  },
  "E3E5": {
   "unit": "game/modemDriverE000",
   "line": "and start counting again from zero"
  },
  "E3E8": {
   "unit": "game/modemDriverE000",
   "line": "always taken"
  },
  "E3EA": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 only: 'characters are still arriving'"
  },
  "E3EC": {
   "unit": "game/modemDriverE000",
   "line": "merge it into the status without touching the carrier bit"
  },
  "E3EF": {
   "unit": "game/modemDriverE000",
   "line": "store the merged status"
  },
  "E3F2": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E3F3": {
   "unit": "game/modemDriverE000",
   "routine": [
    "selectFramedLinkVectors - rewrites the low byte of the three JMP (abs) operands at $E3B7/$E3BA/$E3BD so that sendLinkByte, receiveLinkByte and reportLinkError use the framed (ARQ) rings and the error counter instead of the raw UART rings and the no-op error stub. Note it patches the pointers to the vectors, not the vectors themselves.",
    "In:  none",
    "Out: $E3B7 = $C1, $E3BA = $C5, $E3BD = $C9",
    "Called from: initCommModule $E2BA (the only caller, so the raw configuration never runs)"
   ],
   "line": "$C1 = pushArqTxRing"
  },
  "E3F5": {
   "unit": "game/modemDriverE000",
   "line": "patch the operand of the JMP ($xxxx) at sendLinkByte"
  },
  "E3F8": {
   "unit": "game/modemDriverE000",
   "line": "$C5 = popArqRxRing"
  },
  "E3FA": {
   "unit": "game/modemDriverE000",
   "line": "patch the operand at receiveLinkByte"
  },
  "E3FD": {
   "unit": "game/modemDriverE000",
   "line": "$C9 = countLinkError"
  },
  "E3FF": {
   "unit": "game/modemDriverE000",
   "line": "patch the operand at reportLinkError"
  },
  "E402": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E403": {
   "unit": "game/modemDriverE000",
   "routine": [
    "countLinkError - the framed link-error hook: count one bad character. The link state machine tests linkErrorCount every tick and restarts byte synchronisation when it is non-zero.",
    "In:  none",
    "Out: linkErrorCount +1",
    "Called from: reportLinkError $E3BC through vector $E3C9"
   ],
   "line": "(padding so the two hooks are three bytes apart)"
  },
  "E404": {
   "unit": "game/modemDriverE000",
   "line": "count one bad character; the link state machine restarts byte sync when this is non-zero"
  },
  "E407": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E408": {
   "unit": "game/modemDriverE000",
   "routine": [
    "ignoreLinkError - the raw-mode link-error hook: PHP / PHA / NOP / PLA / PLP / RTS, a no-op that preserves A and the flags, followed by a stray second RTS at $E40E. Dead in the shipped game because initCommModule always selects countLinkError instead. The disassembler still renders it as data.",
    "In:  none",
    "Out: nothing changed",
    "Called from: reportLinkError $E3BC through vector $E3C7 (never selected)"
   ],
   "line": "PHP / PHA / NOP / PLA / PLP / RTS - the do-nothing error hook, plus a stray RTS at $E40E"
  },
  "E40F": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearUartVars - zeroes the 90-byte ring-buffer block $E414-$E46D: all four descending circular buffers (UART transmit 22, UART receive 20, ARQ transmit 18, ARQ receive 18) with their counts and read/write indices.",
    "In:  none",
    "Out: $E414-$E46D = 0; returns to the caller of clearUartVars",
    "Called from: initCommModule $E2B7"
   ],
   "line": "90 bytes to clear: all four rings with their counts and indices"
  },
  "E411": {
   "unit": "game/modemDriverE000",
   "line": "clear the inline block that follows and return to our caller's caller"
  },
  "E414": {
   "unit": "game/modemDriverE000",
   "block": [
    "Ring buffers, $E414-$E46D, the inline block clearUartVars zeroes. Four descending circular",
    "buffers, each with a count byte and separate read and write indices: UART transmit (22), UART",
    "receive (20), frame-layer transmit (18) and frame-layer receive (18)."
   ],
   "line": "uartTxCount: characters in the 22-byte UART transmit ring (rendered as BRK - data, never executed)"
  },
  "E415": {
   "unit": "game/modemDriverE000",
   "line": "uartTxReadIndex"
  },
  "E416": {
   "unit": "game/modemDriverE000",
   "line": "uartTxWriteIndex"
  },
  "E417": {
   "unit": "game/modemDriverE000",
   "line": "uartTxRing: 22 bytes, $E417-$E42C"
  },
  "E41F": {
   "unit": "game/modemDriverE000",
   "line": "uartTxRing continued"
  },
  "E427": {
   "unit": "game/modemDriverE000",
   "line": "uartTxRing continued"
  },
  "E42A": {
   "unit": "game/modemDriverE000",
   "line": "the last three bytes of the 22-byte transmit ring"
  },
  "E42D": {
   "unit": "game/modemDriverE000",
   "line": "uartRxCount: characters in the 20-byte UART receive ring"
  },
  "E42E": {
   "unit": "game/modemDriverE000",
   "line": "uartRxReadIndex"
  },
  "E42F": {
   "unit": "game/modemDriverE000",
   "line": "uartRxWriteIndex"
  },
  "E430": {
   "unit": "game/modemDriverE000",
   "line": "uartRxRing: 20 bytes, $E430-$E443"
  },
  "E431": {
   "unit": "game/modemDriverE000",
   "line": "uartRxRing continued"
  },
  "E435": {
   "unit": "game/modemDriverE000",
   "line": "uartRxRing continued"
  },
  "E43D": {
   "unit": "game/modemDriverE000",
   "line": "the last seven bytes of the 20-byte receive ring"
  },
  "E444": {
   "unit": "game/modemDriverE000",
   "line": "arqTxCount: bytes in the 18-byte frame-layer transmit ring"
  },
  "E445": {
   "unit": "game/modemDriverE000",
   "line": "arqTxReadIndex"
  },
  "E446": {
   "unit": "game/modemDriverE000",
   "line": "arqTxWriteIndex"
  },
  "E447": {
   "unit": "game/modemDriverE000",
   "line": "arqTxRing: 18 bytes, $E447-$E458 - packet layer to frame layer"
  },
  "E44F": {
   "unit": "game/modemDriverE000",
   "line": "arqTxRing continued"
  },
  "E457": {
   "unit": "game/modemDriverE000",
   "line": "the last two bytes of the 18-byte frame transmit ring"
  },
  "E459": {
   "unit": "game/modemDriverE000",
   "line": "arqRxCount: bytes in the 18-byte frame-layer receive ring"
  },
  "E45A": {
   "unit": "game/modemDriverE000",
   "line": "arqRxReadIndex"
  },
  "E45B": {
   "unit": "game/modemDriverE000",
   "line": "arqRxWriteIndex"
  },
  "E45C": {
   "unit": "game/modemDriverE000",
   "line": "arqRxRing: 18 bytes, $E45C-$E46D - frame layer to packet layer"
  },
  "E464": {
   "unit": "game/modemDriverE000",
   "line": "arqRxRing continued"
  },
  "E46C": {
   "unit": "game/modemDriverE000",
   "line": "the last two bytes of the ring sit immediately before popArqRxRing"
  },
  "E46E": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popArqRxRing - pop the oldest byte out of the 18-byte ARQ receive ring, the queue the frame layer fills with checksum-verified payload. This is the framed target of receiveLinkByte, i.e. how the packet layer gets its bytes.",
    "In:  arqRxCount, arqRxReadIndex, arqRxRing",
    "Out: A = byte with C=0, or C=1 when empty",
    "Called from: receiveLinkByte $E3B9 through vector $E3C5"
   ],
   "line": "anything in the frame-layer receive ring?"
  },
  "E471": {
   "unit": "game/modemDriverE000",
   "line": "empty - C=1"
  },
  "E473": {
   "unit": "game/modemDriverE000",
   "line": "oldest slot"
  },
  "E476": {
   "unit": "game/modemDriverE000",
   "line": "fetch the byte"
  },
  "E479": {
   "unit": "game/modemDriverE000",
   "line": "walk the read index down"
  },
  "E47A": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E47C": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 17 of the 18-byte ring"
  },
  "E47E": {
   "unit": "game/modemDriverE000",
   "line": "store the new read index"
  },
  "E481": {
   "unit": "game/modemDriverE000",
   "line": "one byte fewer"
  },
  "E484": {
   "unit": "game/modemDriverE000",
   "line": "C=0: here is a byte"
  },
  "E485": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E486": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popArqTxRing - pop the oldest byte out of the 18-byte ARQ transmit ring, i.e. take the bytes the packet layer queued so the frame builder can wrap them in a checksummed packet.",
    "In:  arqTxCount, arqTxReadIndex, arqTxRing",
    "Out: A = byte with C=0, or C=1 when empty",
    "Called from: popArqTxRingThunk $E733 (used by the frame builder at $E8AE/$E8D4)"
   ],
   "line": "anything queued for the frame builder?"
  },
  "E489": {
   "unit": "game/modemDriverE000",
   "line": "empty - C=1"
  },
  "E48B": {
   "unit": "game/modemDriverE000",
   "line": "oldest slot"
  },
  "E48E": {
   "unit": "game/modemDriverE000",
   "line": "fetch the byte"
  },
  "E491": {
   "unit": "game/modemDriverE000",
   "line": "walk the read index down"
  },
  "E492": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E494": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 17"
  },
  "E496": {
   "unit": "game/modemDriverE000",
   "line": "store the new read index"
  },
  "E499": {
   "unit": "game/modemDriverE000",
   "line": "one byte fewer"
  },
  "E49C": {
   "unit": "game/modemDriverE000",
   "line": "C=0: here is a byte"
  },
  "E49D": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E49E": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushArqRxRing - push one validated payload byte into the 18-byte ARQ receive ring; the byte is dropped when the ring already holds 18.",
    "In:  A = byte",
    "Out: arqRxRing, arqRxCount, arqRxWriteIndex",
    "Called from: pushArqRxRingThunk $E736 (used by the frame receiver at $EA91)"
   ],
   "line": "how full is the frame-layer receive ring?"
  },
  "E4A1": {
   "unit": "game/modemDriverE000",
   "line": "it holds 18 bytes"
  },
  "E4A3": {
   "unit": "game/modemDriverE000",
   "line": "full - drop the byte"
  },
  "E4A5": {
   "unit": "game/modemDriverE000",
   "line": "next free slot"
  },
  "E4A8": {
   "unit": "game/modemDriverE000",
   "line": "store the validated payload byte"
  },
  "E4AB": {
   "unit": "game/modemDriverE000",
   "line": "walk the write index down"
  },
  "E4AC": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E4AE": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 17"
  },
  "E4B0": {
   "unit": "game/modemDriverE000",
   "line": "store the new write index"
  },
  "E4B3": {
   "unit": "game/modemDriverE000",
   "line": "one more byte for the packet layer"
  },
  "E4B6": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E4B7": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushArqTxRing - push one byte into the 18-byte ARQ transmit ring; the byte is dropped when the ring is full, and the caller sees C=1 because the count compare left it set. This is the framed target of sendLinkByte.",
    "In:  A = byte",
    "Out: arqTxRing, arqTxCount, arqTxWriteIndex",
    "Called from: sendLinkByte $E3B6 through vector $E3C1"
   ],
   "line": "how full is the frame-layer transmit ring?"
  },
  "E4BA": {
   "unit": "game/modemDriverE000",
   "line": "it holds 18 bytes"
  },
  "E4BC": {
   "unit": "game/modemDriverE000",
   "line": "full - return with C=1 so the packet layer retries"
  },
  "E4BE": {
   "unit": "game/modemDriverE000",
   "line": "next free slot"
  },
  "E4C1": {
   "unit": "game/modemDriverE000",
   "line": "store the byte"
  },
  "E4C4": {
   "unit": "game/modemDriverE000",
   "line": "walk the write index down"
  },
  "E4C5": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E4C7": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 17"
  },
  "E4C9": {
   "unit": "game/modemDriverE000",
   "line": "store the new write index"
  },
  "E4CC": {
   "unit": "game/modemDriverE000",
   "line": "one more byte for the frame builder"
  },
  "E4CF": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E4D0": {
   "unit": "game/modemDriverE000",
   "routine": [
    "returnRingEmpty - the shared 'nothing in that ring' exit (SEC / RTS) used by the four ring pop routines.",
    "In:  none",
    "Out: C = 1",
    "Called from: popArqRxRing $E471, popArqTxRing $E489, popUartRxRing $E4D5, popUartTxRing $E4ED"
   ],
   "line": "C=1 means 'that ring was empty'"
  },
  "E4D1": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E4D2": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popUartRxRing - pop one received character out of the 20-byte raw UART receive ring the NMI fills.",
    "In:  uartRxCount, uartRxReadIndex, uartRxRing",
    "Out: A = character with C=0, or C=1 when empty",
    "Called from: runModemStateMachine $E799 (terminal mode), beginByteSyncPhase $E80A, popUartRxRingThunk $E739, and receiveLinkByte through vector $E3C3 in the never-selected raw configuration"
   ],
   "line": "any received characters?"
  },
  "E4D5": {
   "unit": "game/modemDriverE000",
   "line": "none - C=1"
  },
  "E4D7": {
   "unit": "game/modemDriverE000",
   "line": "oldest slot"
  },
  "E4DA": {
   "unit": "game/modemDriverE000",
   "line": "fetch the character"
  },
  "E4DD": {
   "unit": "game/modemDriverE000",
   "line": "walk the read index down"
  },
  "E4DE": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E4E0": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 19 of the 20-byte ring"
  },
  "E4E2": {
   "unit": "game/modemDriverE000",
   "line": "store the new read index"
  },
  "E4E5": {
   "unit": "game/modemDriverE000",
   "line": "one character fewer"
  },
  "E4E8": {
   "unit": "game/modemDriverE000",
   "line": "C=0: here is a character"
  },
  "E4E9": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E4EA": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popUartTxRing - pop the next character to transmit out of the 22-byte raw UART transmit ring.",
    "In:  uartTxCount, uartTxReadIndex, uartTxRing",
    "Out: A = character with C=0, or C=1 when empty",
    "Called from: startNextTxChar $E62D, flushUartTxRing $E534"
   ],
   "line": "anything left to transmit?"
  },
  "E4ED": {
   "unit": "game/modemDriverE000",
   "line": "no - C=1"
  },
  "E4EF": {
   "unit": "game/modemDriverE000",
   "line": "oldest slot"
  },
  "E4F2": {
   "unit": "game/modemDriverE000",
   "line": "fetch the character"
  },
  "E4F5": {
   "unit": "game/modemDriverE000",
   "line": "walk the read index down"
  },
  "E4F6": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E4F8": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 21 of the 22-byte ring"
  },
  "E4FA": {
   "unit": "game/modemDriverE000",
   "line": "store the new read index"
  },
  "E4FD": {
   "unit": "game/modemDriverE000",
   "line": "one character fewer"
  },
  "E500": {
   "unit": "game/modemDriverE000",
   "line": "C=0: here is a character"
  },
  "E501": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E502": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushUartRxRing - push one fully received character into the 20-byte raw UART receive ring. When the ring is full it leaves everything alone and returns with C=1 from the count compare, which the NMI treats as a receive overflow.",
    "In:  A = received character",
    "Out: uartRxRing, uartRxCount, uartRxWriteIndex; C=1 on overflow",
    "Called from: commNmiHandler $E71F (the only caller)"
   ],
   "line": "how full is the receive ring?"
  },
  "E505": {
   "unit": "game/modemDriverE000",
   "line": "it holds 20 characters"
  },
  "E507": {
   "unit": "game/modemDriverE000",
   "line": "full - leave it alone and return with C=1, which the NMI treats as an overflow error"
  },
  "E509": {
   "unit": "game/modemDriverE000",
   "line": "next free slot"
  },
  "E50C": {
   "unit": "game/modemDriverE000",
   "line": "store the character the UART just assembled"
  },
  "E50F": {
   "unit": "game/modemDriverE000",
   "line": "walk the write index down"
  },
  "E510": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E512": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 19"
  },
  "E514": {
   "unit": "game/modemDriverE000",
   "line": "store the new write index"
  },
  "E517": {
   "unit": "game/modemDriverE000",
   "line": "one more character for the protocol"
  },
  "E51A": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E51B": {
   "unit": "game/modemDriverE000",
   "routine": [
    "pushUartTxRing - push one character into the 22-byte raw UART transmit ring; the byte is dropped when the ring is full.",
    "In:  A = character",
    "Out: uartTxRing, uartTxCount, uartTxWriteIndex",
    "Called from: queueByteForTransmit $E73C, and sendLinkByte through vector $E3BF in the never-selected raw configuration"
   ],
   "line": "how full is the transmit ring?"
  },
  "E51E": {
   "unit": "game/modemDriverE000",
   "line": "it holds 22 characters"
  },
  "E520": {
   "unit": "game/modemDriverE000",
   "line": "full - drop the character"
  },
  "E522": {
   "unit": "game/modemDriverE000",
   "line": "next free slot"
  },
  "E525": {
   "unit": "game/modemDriverE000",
   "line": "store it"
  },
  "E528": {
   "unit": "game/modemDriverE000",
   "line": "walk the write index down"
  },
  "E529": {
   "unit": "game/modemDriverE000",
   "line": "still inside the ring?"
  },
  "E52B": {
   "unit": "game/modemDriverE000",
   "line": "wrapped - back to slot 21"
  },
  "E52D": {
   "unit": "game/modemDriverE000",
   "line": "store the new write index"
  },
  "E530": {
   "unit": "game/modemDriverE000",
   "line": "one more character to shift out"
  },
  "E533": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E534": {
   "unit": "game/modemDriverE000",
   "routine": [
    "flushUartTxRing - throw away everything still queued for transmission, then set the pending-byte count to 1 if a character is still being shifted out or 0 if not. Used whenever the connection state changes so stale bytes cannot reach the peer.",
    "In:  the UART transmit ring, txCharActive",
    "Out: transmit ring emptied, uartPendingCount resynchronised",
    "Called from: runModemStateMachine $E791, beginByteSyncPhase $E80F"
   ],
   "line": "throw the next queued character away"
  },
  "E537": {
   "unit": "game/modemDriverE000",
   "line": "until the ring is empty"
  },
  "E539": {
   "unit": "game/modemDriverE000",
   "line": "1 if a character is still being shifted out"
  },
  "E53C": {
   "unit": "game/modemDriverE000",
   "line": "resynchronise the pending count with reality"
  },
  "E53F": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E540": {
   "unit": "game/modemDriverE000",
   "routine": [
    "configureUserPortLines - set CIA2 up for the user-port serial link: timer A latch $0003 with CRA = $51 (timer A running, serial port in output mode), a fixed bit pattern into the serial data register, DDRB = $26 so PB1 (RTS), PB2 (DTR) and PB5 are outputs, both handshake lines asserted, and finally PA2 (TXD) raised to the idle mark level. What the serial-shift-register write is for is unclear - SP2 is user-port pin 7 and is not part of the standard RS-232 wiring, so it may drive a modem control input or be vestigial.",
    "In:  isOriginateMode, baudIndex, userPortDdrbValue, userPortIdleTable, serialShiftPatternTable",
    "Out: CIA2_CRA, CIA2 timer A latch, CIA2_SDR, CIA2_DDRB, CIA2_PRB, CIA2_PRA bit 2",
    "Called from: openCommLink $E2D5, the C=+A / C=+O hot keys at $E387"
   ],
   "line": "timer A high byte = 0"
  },
  "E542": {
   "unit": "game/modemDriverE000",
   "line": "stop timer A while its latch is reprogrammed"
  },
  "E545": {
   "unit": "game/modemDriverE000",
   "line": "latch $0003: the serial shift register runs at about 128 kHz"
  },
  "E547": {
   "unit": "game/modemDriverE000",
   "line": "latch the fast shift-register clock"
  },
  "E54A": {
   "unit": "game/modemDriverE000",
   "line": "$51 = timer A running in continuous mode with the serial port in OUTPUT mode"
  },
  "E54C": {
   "unit": "game/modemDriverE000",
   "line": "start timer A with the serial port in output mode"
  },
  "E54F": {
   "unit": "game/modemDriverE000",
   "line": "A is still $03: keep the low two bits of the mode flag"
  },
  "E552": {
   "unit": "game/modemDriverE000",
   "line": "index = isOriginateMode EOR baudIndex (0..3)"
  },
  "E555": {
   "unit": "game/modemDriverE000",
   "line": "into X to index the pattern table"
  },
  "E556": {
   "unit": "game/modemDriverE000",
   "line": "$27/$2F/$3F/$37 depending on mode and speed"
  },
  "E559": {
   "unit": "game/modemDriverE000",
   "line": "shifted out of SP2 (user-port pin 7); its purpose is unclear - possibly a modem control input, possibly vestigial"
  },
  "E55C": {
   "unit": "game/modemDriverE000",
   "line": "$26: PB1 (RTS), PB2 (DTR) and PB5 are outputs"
  },
  "E55F": {
   "unit": "game/modemDriverE000",
   "line": "set the user-port data direction"
  },
  "E562": {
   "unit": "game/modemDriverE000",
   "line": "answer or originate"
  },
  "E565": {
   "unit": "game/modemDriverE000",
   "line": "both entries are $26"
  },
  "E568": {
   "unit": "game/modemDriverE000",
   "line": "assert RTS, DTR and PB5"
  },
  "E56B": {
   "unit": "game/modemDriverE000",
   "line": "PA2 is TXD"
  },
  "E56D": {
   "unit": "game/modemDriverE000",
   "line": "drive it high, the RS-232 idle (mark) level"
  },
  "E570": {
   "unit": "game/modemDriverE000",
   "line": "leave the line idling at mark"
  },
  "E573": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E574": {
   "unit": "game/modemDriverE000",
   "routine": [
    "dropDtrLine - write $22 to CIA2_PRB, clearing PB2 (DTR) while leaving PB1 (RTS) and PB5 high. Dropping DTR is what makes a Hayes modem let go of the line.",
    "In:  userPortDtrDropped",
    "Out: CIA2_PRB",
    "Called from: hangUpModem $E38D (the only caller)"
   ],
   "line": "$22 = the same port pattern with DTR (PB2) low"
  },
  "E577": {
   "unit": "game/modemDriverE000",
   "line": "dropping DTR terminates the call"
  },
  "E57A": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E57B": {
   "unit": "game/modemDriverE000",
   "routine": [
    "installCommNmiVector - point the RAM NMI vector at $FFFA/$FFFB at this module's bit-bang UART handler, so every CIA2 interrupt lands in commNmiHandler.",
    "In:  none",
    "Out: $FFFA/$FFFB = $E685",
    "Called from: openCommLink $E2CC"
   ],
   "line": "low byte of commNmiHandler"
  },
  "E57D": {
   "unit": "game/modemDriverE000",
   "line": "high byte"
  },
  "E57F": {
   "unit": "game/modemDriverE000",
   "line": "the NMI vector lives in RAM at $FFFA/$FFFB (the ROMs are switched out)"
  },
  "E582": {
   "unit": "game/modemDriverE000",
   "line": "the CPU now enters commNmiHandler on every CIA2 interrupt"
  },
  "E585": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E586": {
   "unit": "game/modemDriverE000",
   "routine": [
    "restartUart - (re)start the software UART: clear the six bit-level state bytes, ask the modem state machine to re-evaluate, prime the carrier sample timer, and fall into setCiaNmiMask with $92 = enable the CIA2 FLAG line (RXD start bit) and timer B (transmit clock) as NMI sources.",
    "In:  none",
    "Out: $E5BB-$E5C0 = 0, uartRestartRequest = 1, carrierSampleTimer = $92, CIA2 NMI sources armed, nmiSuspendRequest/Ack = 0",
    "Called from: openCommLink $E2CF, serviceCarrierAndSuspendRequest $E5E1 (resume after a disk load)"
   ],
   "line": "zero the six bit-level UART variables"
  },
  "E589": {
   "unit": "game/modemDriverE000",
   "line": "1 = re-evaluate (only $FF forces the phase back to 0)"
  },
  "E58B": {
   "unit": "game/modemDriverE000",
   "line": "ask the modem state machine to re-evaluate the connection"
  },
  "E58E": {
   "unit": "game/modemDriverE000",
   "line": "$92 = set FLAG (RXD start bit) and timer B (transmit clock) as NMI sources"
  },
  "E590": {
   "unit": "game/modemDriverE000",
   "line": "prime the carrier sample timer with the same negative value"
  },
  "E593": {
   "unit": "game/modemDriverE000",
   "routine": [
    "setCiaNmiMask - write a CIA2 interrupt-control command byte ($92 = FLAG+TB on, $83 = TA+TB on, $7F = everything off), acknowledging whatever was pending and stopping both timers first. It retries if an NMI changed the shadow underneath it. The suspend request and acknowledge bytes are then both set from the mask ($00 for an enable mask, $80 for the disable mask), and the routine exits through the 'raise TXD' tail of configureUserPortLines.",
    "In:  A = ICR command byte",
    "Out: ciaIcrShadow, CIA2_ICR/CRA/CRB, nmiSuspendRequest, nmiSuspendAck, CIA2_PRA bit 2",
    "Called from: restartUart (fall-through from $E590), stopCommNmi $E5CA"
   ],
   "line": "remember which sources we want"
  },
  "E596": {
   "unit": "game/modemDriverE000",
   "line": "read the interrupt register to clear anything pending"
  },
  "E599": {
   "unit": "game/modemDriverE000",
   "line": "0 stops a timer when written to its control register"
  },
  "E59B": {
   "unit": "game/modemDriverE000",
   "line": "arm or disarm the sources"
  },
  "E59E": {
   "unit": "game/modemDriverE000",
   "line": "stop timer A"
  },
  "E5A1": {
   "unit": "game/modemDriverE000",
   "line": "stop timer B"
  },
  "E5A4": {
   "unit": "game/modemDriverE000",
   "line": "did an NMI change the shadow while we were writing?"
  },
  "E5A7": {
   "unit": "game/modemDriverE000",
   "line": "yes (it asked for more sources than we did) - do it again"
  },
  "E5A9": {
   "unit": "game/modemDriverE000",
   "line": "$92/$83 -> 0, $7F -> $80"
  },
  "E5AB": {
   "unit": "game/modemDriverE000",
   "line": "keep only the sign bit"
  },
  "E5AD": {
   "unit": "game/modemDriverE000",
   "line": "so a disable mask marks the module suspended..."
  },
  "E5B0": {
   "unit": "game/modemDriverE000",
   "line": "...and acknowledges it in the same breath"
  },
  "E5B3": {
   "unit": "game/modemDriverE000",
   "line": "exit through the 'raise TXD to mark' tail"
  },
  "E5B6": {
   "unit": "game/modemDriverE000",
   "routine": [
    "clearUartState - zeroes the six bytes at $E5BB-$E5C0 that hold the entire state of the software UART: transmitter busy flag, bit-count reload, transmit bit counter, transmit shift register, receive bit counter and receive shift register.",
    "In:  none",
    "Out: $E5BB-$E5C0 = 0; returns to the caller of clearUartState",
    "Called from: restartUart $E586"
   ],
   "line": "6 bytes to clear: the whole software-UART bit state"
  },
  "E5B8": {
   "unit": "game/modemDriverE000",
   "line": "clear the inline block that follows and return to our caller's caller"
  },
  "E5BB": {
   "unit": "game/modemDriverE000",
   "block": [
    "Software UART bit state, $E5BB-$E5C0 - the inline block clearUartState zeroes. Everything the",
    "NMI needs to shift one character in and one character out."
   ],
   "line": "txCharActive: non-zero while a character is being shifted out (rendered as BRK - data, never executed)"
  },
  "E5BC": {
   "unit": "game/modemDriverE000",
   "line": "txBitCountReload: 9 = 8 data bits plus the stop bit"
  },
  "E5BD": {
   "unit": "game/modemDriverE000",
   "line": "txBitCounter: bits left in the character being sent"
  },
  "E5BE": {
   "unit": "game/modemDriverE000",
   "line": "txShiftRegister: the character being shifted out, LSB first"
  },
  "E5BF": {
   "unit": "game/modemDriverE000",
   "line": "rxBitCounter: negative = waiting for the start-bit sample, 9..1 = data bits, 0 = stop bit"
  },
  "E5C0": {
   "unit": "game/modemDriverE000",
   "line": "rxShiftRegister: the character being shifted in"
  },
  "E5C1": {
   "unit": "game/modemDriverE000",
   "routine": [
    "suspendUartIfRunning - two-byte entry between the UART state block and stopCommNmi: the disk loader has asked for a suspend, so shut the NMI UART down unless the acknowledge byte already says it is down.",
    "In:  C = bit 7 of nmiSuspendAck (set when already suspended)",
    "Out: falls into stopCommNmi when the suspend has not been acknowledged yet",
    "Called from: serviceCarrierAndSuspendRequest $E5DD"
   ],
   "line": "the suspend is already acknowledged - nothing to do"
  },
  "E5C3": {
   "unit": "game/modemDriverE000",
   "routine": [
    "stopCommNmi - tear the software UART down: ask the modem state machine to fall back to phase 0, disable every CIA2 NMI source (which also sets both suspend bytes to $80 = suspended), and forget the character that was half way out of the transmitter.",
    "In:  txCharActive",
    "Out: CIA2 NMI sources off, nmiSuspendRequest/Ack = $80, uartRestartRequest = $FF, uartPendingCount adjusted",
    "Called from: commLinkControlDispatch $E2AB, suspendUartIfRunning $E5C1"
   ],
   "line": "$FF asks the modem state machine to fall back to phase 0"
  },
  "E5C5": {
   "unit": "game/modemDriverE000",
   "line": "so the connection falls back to phase 0 when the UART comes back"
  },
  "E5C8": {
   "unit": "game/modemDriverE000",
   "line": "$7F = clear every CIA2 interrupt source"
  },
  "E5CA": {
   "unit": "game/modemDriverE000",
   "line": "disable the NMI UART (this also sets both suspend bytes to $80)"
  },
  "E5CD": {
   "unit": "game/modemDriverE000",
   "line": "was a character half way out?"
  },
  "E5D0": {
   "unit": "game/modemDriverE000",
   "line": "no"
  },
  "E5D2": {
   "unit": "game/modemDriverE000",
   "line": "yes - it will never finish, so drop it from the pending count"
  },
  "E5D5": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E5D6": {
   "unit": "game/modemDriverE000",
   "routine": [
    "serviceCarrierAndSuspendRequest - first half of the $E00C service. It runs the two-way suspend handshake the disk loader uses ($E039 written by the game, $E03A answered here) and then, when the link is up, samples carrier detect on CIA2_PRB bit 4 through the polarity/override byte and leaves $F8 (carrier) or $80 (no carrier) in linkStatusSample. Bit 7 of the sample is only kept when a character has arrived recently, so it doubles as a 'data is flowing' indicator.",
    "In:  nmiSuspendRequest, nmiSuspendAck, isLinkActive, carrierOverrideFlags, CIA2_PRB, carrierSampleTimer",
    "Out: linkStatusSample, carrierSampleTimer, the NMI UART started or stopped",
    "Called from: pollCarrierState $E3CB"
   ],
   "line": "the acknowledge byte"
  },
  "E5D9": {
   "unit": "game/modemDriverE000",
   "line": "carry = bit 7: are we currently suspended?"
  },
  "E5DA": {
   "unit": "game/modemDriverE000",
   "line": "the request byte the game writes"
  },
  "E5DD": {
   "unit": "game/modemDriverE000",
   "line": "$C0 = the disk loader wants the NMI off"
  },
  "E5DF": {
   "unit": "game/modemDriverE000",
   "line": "no request and not suspended - carry on"
  },
  "E5E1": {
   "unit": "game/modemDriverE000",
   "line": "no request but still suspended - bring the UART back"
  },
  "E5E4": {
   "unit": "game/modemDriverE000",
   "line": "is the link up at all?"
  },
  "E5E7": {
   "unit": "game/modemDriverE000",
   "line": "no - nothing to sample"
  },
  "E5E9": {
   "unit": "game/modemDriverE000",
   "line": "carrier polarity / override"
  },
  "E5EC": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 would force the sample byte outright (never set by any code here)"
  },
  "E5EE": {
   "unit": "game/modemDriverE000",
   "line": "bit 6 into bit 7"
  },
  "E5EF": {
   "unit": "game/modemDriverE000",
   "line": "the C=+C override is on - report carrier without looking at the line"
  },
  "E5F1": {
   "unit": "game/modemDriverE000",
   "line": "read the user port through the polarity mask"
  },
  "E5F4": {
   "unit": "game/modemDriverE000",
   "line": "PB4 = DCD"
  },
  "E5F6": {
   "unit": "game/modemDriverE000",
   "line": "DCD high -> A=$00 C=1, DCD low (carrier) -> A=$F0 C=0"
  },
  "E5F8": {
   "unit": "game/modemDriverE000",
   "line": "shift a 1 into bit 7: 'a sample has been taken'"
  },
  "E5F9": {
   "unit": "game/modemDriverE000",
   "line": "-> $80 no carrier, $F8 carrier, $C0 override; bit 6 is the carrier flag"
  },
  "E5FA": {
   "unit": "game/modemDriverE000",
   "line": "the NMI zeroes this on every good character"
  },
  "E5FD": {
   "unit": "game/modemDriverE000",
   "line": "positive means characters have arrived recently - keep bit 7 set"
  },
  "E5FF": {
   "unit": "game/modemDriverE000",
   "routine": [
    "rearmCarrierTimer - the 'no character has arrived recently' tail of the carrier sampler: park the sample in carrierSampleTimer (it is negative, so the timer stays negative until the NMI zeroes it on the next good character) and strip bit 7 from the value that goes into linkStatusSample, marking the link as quiet.",
    "In:  A = fresh carrier sample",
    "Out: carrierSampleTimer = A, A = sample with bit 7 cleared",
    "Called from: fall-through from $E5FD (the solo-trainer build also calls it directly)"
   ],
   "line": "quiet line: park the sample here so the timer stays negative"
  },
  "E602": {
   "unit": "game/modemDriverE000",
   "line": "and clear the 'data flowing' bit"
  },
  "E604": {
   "unit": "game/modemDriverE000",
   "line": "publish the sample for pollCarrierState to debounce"
  },
  "E607": {
   "unit": "game/modemDriverE000",
   "routine": [
    "startNextTxChar - start shifting the next character out of the software UART. It gives up at once when timer B is already running or a character is still in flight; outside terminal mode it also paces characters with txPaceCounter and refuses to transmit without carrier. Otherwise it pops a byte into the shift register, arms the 9-bit counter, programs timer B with a deliberately short first interval and starts it, then falls into setBitPeriodFull so every following underflow is exactly one bit time.",
    "In:  connectionPhase, txPaceCounter, linkStatus/linkStatusSample, uartPendingCount, the UART transmit ring",
    "Out: txCharActive, txShiftRegister, txBitCountReload, txPaceCounter, CIA2 timer B and CRB",
    "Called from: nmiStartNextChar $E673, and by fall-through from the carrier sampler at $E604 (so it is retried once per frame as well as from the NMI)"
   ],
   "line": "is timer B still running?"
  },
  "E60A": {
   "unit": "game/modemDriverE000",
   "line": "or a character still in flight?"
  },
  "E60D": {
   "unit": "game/modemDriverE000",
   "line": "CRB bit 0 = timer B started"
  },
  "E60E": {
   "unit": "game/modemDriverE000",
   "line": "busy - come back later"
  },
  "E610": {
   "unit": "game/modemDriverE000",
   "line": "phase 1 = modem command / terminal mode"
  },
  "E612": {
   "unit": "game/modemDriverE000",
   "line": "which phase are we in?"
  },
  "E615": {
   "unit": "game/modemDriverE000",
   "line": "in terminal mode characters go out unpaced and without carrier"
  },
  "E617": {
   "unit": "game/modemDriverE000",
   "line": "otherwise pace them"
  },
  "E61A": {
   "unit": "game/modemDriverE000",
   "line": "not this frame"
  },
  "E61C": {
   "unit": "game/modemDriverE000",
   "line": "reload with 1 so it retries next frame"
  },
  "E61F": {
   "unit": "game/modemDriverE000",
   "line": "the live carrier sample"
  },
  "E622": {
   "unit": "game/modemDriverE000",
   "line": "AND the debounced status"
  },
  "E625": {
   "unit": "game/modemDriverE000",
   "line": "bit 6 (carrier) into bit 7"
  },
  "E626": {
   "unit": "game/modemDriverE000",
   "line": "no carrier - do not transmit into a dead line"
  },
  "E628": {
   "unit": "game/modemDriverE000",
   "block": [
    "Fetch the next character and set the software UART shifting: 9 bit times out of timer B, with a",
    "deliberately short first interval so the start bit follows quickly."
   ],
   "line": "anything queued?"
  },
  "E62B": {
   "unit": "game/modemDriverE000",
   "line": "no"
  },
  "E62D": {
   "unit": "game/modemDriverE000",
   "line": "take the next character"
  },
  "E630": {
   "unit": "game/modemDriverE000",
   "line": "got one"
  },
  "E632": {
   "unit": "game/modemDriverE000",
   "line": "the ring was empty although the count said otherwise"
  },
  "E634": {
   "unit": "game/modemDriverE000",
   "line": "resynchronise the count"
  },
  "E637": {
   "unit": "game/modemDriverE000",
   "line": "always taken"
  },
  "E639": {
   "unit": "game/modemDriverE000",
   "line": "mark the transmitter busy"
  },
  "E63C": {
   "unit": "game/modemDriverE000",
   "line": "load the shift register"
  },
  "E63F": {
   "unit": "game/modemDriverE000",
   "line": "8 data bits plus the stop bit"
  },
  "E641": {
   "unit": "game/modemDriverE000",
   "line": "arm the transmit bit counter for the first character"
  },
  "E644": {
   "unit": "game/modemDriverE000",
   "line": "1 at 300 baud, 2 at 1200"
  },
  "E647": {
   "unit": "game/modemDriverE000",
   "line": "reload the pacing counter"
  },
  "E64A": {
   "unit": "game/modemDriverE000",
   "line": "$01 : bitPeriodLo = a short first interval"
  },
  "E64C": {
   "unit": "game/modemDriverE000",
   "line": "X=2 selects timer B, the transmit clock"
  },
  "E64E": {
   "unit": "game/modemDriverE000",
   "line": "program the latch and get $11 back"
  },
  "E651": {
   "unit": "game/modemDriverE000",
   "line": "$11 = force load + start timer B; its first underflow will emit the start bit"
  },
  "E654": {
   "unit": "game/modemDriverE000",
   "routine": [
    "setBitPeriodFull - load A with the high byte of the current bit period and fall into setBitPeriod, i.e. program the selected timer with the full one-bit interval.",
    "In:  X = 0 for timer A (receive clock) or 2 for timer B (transmit clock), bitPeriodLo/bitPeriodHi",
    "Out: the CIA2 timer latch; A = $11 for the caller to store into the control register",
    "Called from: commNmiHandler $E6EC (after the half-bit start-bit delay)"
   ],
   "line": "high byte of one whole bit time"
  },
  "E657": {
   "unit": "game/modemDriverE000",
   "routine": [
    "setBitPeriod - write A:bitPeriodLo into the CIA2 timer latch selected by X (0 = timer A at $DD04/$DD05, 2 = timer B at $DD06/$DD07) and return A = $11 = 'force load + start' for the caller to put into CRA or CRB.",
    "In:  A = high byte of the interval, X = 0 or 2, bitPeriodLo",
    "Out: CIA2 timer latch, A = $11",
    "Called from: startNextTxChar $E64E, commNmiHandler $E6E6, setBitPeriodFull (fall-through)"
   ],
   "line": "X=0 -> $DD04/$DD05 (timer A), X=2 -> $DD06/$DD07 (timer B)"
  },
  "E65A": {
   "unit": "game/modemDriverE000",
   "line": "the low byte is always the full one"
  },
  "E65D": {
   "unit": "game/modemDriverE000",
   "line": "low byte of the timer latch"
  },
  "E660": {
   "unit": "game/modemDriverE000",
   "line": "$11 = force load + start, for the caller to store into CRA/CRB"
  },
  "E662": {
   "unit": "game/modemDriverE000",
   "line": "return"
  },
  "E663": {
   "unit": "game/modemDriverE000",
   "routine": [
    "nmiStartNextChar - the timer B path of the NMI once the bit counter has run out. If a character is active the counter is reloaded and TXD is driven low to emit its start bit; if not, the transmitter tries to fetch the next byte.",
    "In:  txCharActive, txBitCountReload, CIA2_PRA",
    "Out: txBitCounter, CIA2_PRA bit 2 (TXD)",
    "Called from: commNmiHandler $E6A7"
   ],
   "line": "is a character being sent?"
  },
  "E666": {
   "unit": "game/modemDriverE000",
   "line": "no - try to fetch the next one"
  },
  "E668": {
   "unit": "game/modemDriverE000",
   "line": "9 bits to go"
  },
  "E66B": {
   "unit": "game/modemDriverE000",
   "line": "arm the bit counter for this character"
  },
  "E66E": {
   "unit": "game/modemDriverE000",
   "line": "PA2 is TXD; PRA is never 0 (the VIC bank bits are in it)"
  },
  "E671": {
   "unit": "game/modemDriverE000",
   "line": "so this is an unconditional jump to 'drive TXD low' = the start bit"
  },
  "E673": {
   "unit": "game/modemDriverE000",
   "line": "nothing in flight - start the next character if there is one"
  },
  "E676": {
   "unit": "game/modemDriverE000",
   "line": "force the branch below"
  },
  "E677": {
   "unit": "game/modemDriverE000",
   "line": "always taken: skip the transmit-bit code"
  },
  "E679": {
   "unit": "game/modemDriverE000",
   "line": "not our interrupt: restore the sources we want"
  },
  "E67C": {
   "unit": "game/modemDriverE000",
   "line": "put our own interrupt mask back"
  },
  "E67F": {
   "unit": "game/modemDriverE000",
   "line": "restore X"
  },
  "E680": {
   "unit": "game/modemDriverE000",
   "line": "the saved X"
  },
  "E681": {
   "unit": "game/modemDriverE000",
   "line": "and A"
  },
  "E682": {
   "unit": "game/modemDriverE000",
   "line": "chain to the game's NMI handler ($1298, a bare RTI)"
  },
  "E685": {
   "unit": "game/modemDriverE000",
   "routine": [
    "commNmiHandler - the module's CIA2 NMI handler, installed at $FFFA/$FFFB by installCommNmiVector. It saves A and X, masks CIA2 interrupts, reads the interrupt register with a workaround for timer B underflowing between the two reads, and chains to the game's NMI vector when the interrupt was not CIA2's. Timer B clocks the software transmitter one bit at a time out of txShiftRegister onto PA2 (TXD); the CIA2 FLAG line (the RXD start-bit edge) arms timer A at half a bit time and then at a full bit time, and each timer A interrupt shifts CIA2_PRB bit 0 (RXD) into rxShiftRegister until a whole character with a good stop bit reaches the UART receive ring. Framing errors, bad stop bits and receive overflow all go through reportLinkError.",
    "In:  CIA2_ICR, CIA2_PRB bit 0, ciaIcrShadow, $E5BB-$E5C0",
    "Out: CIA2_PRA bit 2, the UART receive ring, uartPendingCount, ciaIcrShadow, carrierSampleTimer, the $EB00 stall counter; exits with RTI or chains through nmiChainVector",
    "Called from: the CPU's NMI vector once installCommNmiVector has run"
   ],
   "line": "save A"
  },
  "E686": {
   "unit": "game/modemDriverE000",
   "line": "X goes on the stack too"
  },
  "E687": {
   "unit": "game/modemDriverE000",
   "line": "and X"
  },
  "E688": {
   "unit": "game/modemDriverE000",
   "line": "$7F = disable every CIA2 source while we work"
  },
  "E68A": {
   "unit": "game/modemDriverE000",
   "line": "no CIA2 interrupt may fire while we shift bits"
  },
  "E68D": {
   "unit": "game/modemDriverE000",
   "line": "sample timer B's high byte before reading the flags"
  },
  "E690": {
   "unit": "game/modemDriverE000",
   "line": "reading the interrupt register also clears it"
  },
  "E693": {
   "unit": "game/modemDriverE000",
   "line": "bit 7 clear: this NMI did not come from CIA2 (RESTORE key)"
  },
  "E695": {
   "unit": "game/modemDriverE000",
   "line": "did timer B wrap between the two reads?"
  },
  "E698": {
   "unit": "game/modemDriverE000",
   "line": "no - the flags we read are complete"
  },
  "E69A": {
   "unit": "game/modemDriverE000",
   "line": "yes - add the timer B flag by hand"
  },
  "E69C": {
   "unit": "game/modemDriverE000",
   "routine": [
    "nmiMergeIcrFlags - the two-byte continuation of the timer B race workaround: OR in any interrupt flags that arrived between the handler's two reads of the interrupt register, so nothing is lost.",
    "In:  A = flags read so far",
    "Out: A = flags including the late arrivals",
    "Called from: fall-through from $E69A in this build"
   ],
   "line": "and merge anything else that arrived meanwhile"
  },
  "E69F": {
   "unit": "game/modemDriverE000",
   "block": [
    "Transmit half: one timer B underflow = one bit time. The bit counter runs 9, 8 ... 1, 0 and then",
    "goes negative, at which point the next character (and its start bit) is set up."
   ],
   "line": "keep the flag byte for the receive half"
  },
  "E6A0": {
   "unit": "game/modemDriverE000",
   "line": "timer B = the transmit bit clock"
  },
  "E6A2": {
   "unit": "game/modemDriverE000",
   "line": "not a transmit tick - go and look at the receiver"
  },
  "E6A4": {
   "unit": "game/modemDriverE000",
   "line": "one bit of this character done"
  },
  "E6A7": {
   "unit": "game/modemDriverE000",
   "line": "the whole character has gone - start the next one"
  },
  "E6A9": {
   "unit": "game/modemDriverE000",
   "line": "still data bits to send"
  },
  "E6AB": {
   "unit": "game/modemDriverE000",
   "line": "$09 = stop timer B but keep force-load armed"
  },
  "E6AD": {
   "unit": "game/modemDriverE000",
   "line": "stop the transmit clock; the line now rests at mark"
  },
  "E6B0": {
   "unit": "game/modemDriverE000",
   "line": "one fewer character pending"
  },
  "E6B3": {
   "unit": "game/modemDriverE000",
   "line": "clear the transmit-busy flag"
  },
  "E6B5": {
   "unit": "game/modemDriverE000",
   "line": "the transmitter is idle again"
  },
  "E6B8": {
   "unit": "game/modemDriverE000",
   "line": "shift a mark bit in behind the data"
  },
  "E6B9": {
   "unit": "game/modemDriverE000",
   "line": "next bit of the character into carry, LSB first"
  },
  "E6BC": {
   "unit": "game/modemDriverE000",
   "line": "read the port so the other bits survive"
  },
  "E6BF": {
   "unit": "game/modemDriverE000",
   "line": "a 1 bit means TXD high (mark)"
  },
  "E6C1": {
   "unit": "game/modemDriverE000",
   "line": "the shifted-out bit was a 1"
  },
  "E6C3": {
   "unit": "game/modemDriverE000",
   "line": "a 0 bit means TXD low (space)"
  },
  "E6C5": {
   "unit": "game/modemDriverE000",
   "line": "drive PA2 with this bit"
  },
  "E6C8": {
   "unit": "game/modemDriverE000",
   "block": [
    "Receive half: either the FLAG line has seen the falling edge of a start bit, or timer A has timed",
    "out in the middle of a bit and it is time to sample RXD."
   ],
   "line": "$82 = re-enable the timer B (transmit) interrupt"
  },
  "E6CA": {
   "unit": "game/modemDriverE000",
   "line": "so the next bit time still interrupts us"
  },
  "E6CD": {
   "unit": "game/modemDriverE000",
   "line": "get the flag byte back"
  },
  "E6CE": {
   "unit": "game/modemDriverE000",
   "line": "only look at sources we actually enabled"
  },
  "E6D1": {
   "unit": "game/modemDriverE000",
   "line": "carry = timer A = the receive bit clock"
  },
  "E6D2": {
   "unit": "game/modemDriverE000",
   "line": "a data bit is due to be sampled"
  },
  "E6D4": {
   "unit": "game/modemDriverE000",
   "line": "bit 3 after the shift = the FLAG line = an RXD start-bit edge"
  },
  "E6D6": {
   "unit": "game/modemDriverE000",
   "line": "neither - done"
  },
  "E6D8": {
   "unit": "game/modemDriverE000",
   "block": [
    "Start bit detected on the FLAG line: arm timer A half a bit time from now so the first sample",
    "lands in the middle of the start bit, and every following one in the middle of a data bit."
   ],
   "line": "$83 = accept timer A and timer B from now on"
  },
  "E6DA": {
   "unit": "game/modemDriverE000",
   "line": "remember the new source mask"
  },
  "E6DD": {
   "unit": "game/modemDriverE000",
   "line": "any negative value marks 'the next sample is the start bit'"
  },
  "E6E0": {
   "unit": "game/modemDriverE000",
   "line": "X=0 selects timer A, the receive clock"
  },
  "E6E2": {
   "unit": "game/modemDriverE000",
   "line": "half a bit time (the low byte is left alone)"
  },
  "E6E5": {
   "unit": "game/modemDriverE000",
   "line": "halve the high byte"
  },
  "E6E6": {
   "unit": "game/modemDriverE000",
   "line": "program the latch and get $11 back"
  },
  "E6E9": {
   "unit": "game/modemDriverE000",
   "line": "start timer A: the first sample lands in the middle of the start bit"
  },
  "E6EC": {
   "unit": "game/modemDriverE000",
   "line": "reload the latch with a whole bit time for every following sample"
  },
  "E6EF": {
   "unit": "game/modemDriverE000",
   "line": "always taken (A = $11)"
  },
  "E6F1": {
   "unit": "game/modemDriverE000",
   "block": [
    "A timer A tick: sample RXD and decide whether this is the start bit, a data bit or the stop bit."
   ],
   "line": "PB0 is RXD"
  },
  "E6F4": {
   "unit": "game/modemDriverE000",
   "line": "the received bit into carry"
  },
  "E6F5": {
   "unit": "game/modemDriverE000",
   "line": "which bit of the character is this?"
  },
  "E6F8": {
   "unit": "game/modemDriverE000",
   "line": "negative: this is the start-bit sample"
  },
  "E6FA": {
   "unit": "game/modemDriverE000",
   "line": "zero: this is the stop-bit sample"
  },
  "E6FC": {
   "unit": "game/modemDriverE000",
   "line": "otherwise shift the data bit in, LSB first"
  },
  "E6FF": {
   "unit": "game/modemDriverE000",
   "line": "restore the sources we want"
  },
  "E702": {
   "unit": "game/modemDriverE000",
   "line": "re-arm the sources we want"
  },
  "E705": {
   "unit": "game/modemDriverE000",
   "line": "restore X"
  },
  "E706": {
   "unit": "game/modemDriverE000",
   "line": "the saved X"
  },
  "E707": {
   "unit": "game/modemDriverE000",
   "line": "and A"
  },
  "E708": {
   "unit": "game/modemDriverE000",
   "line": "return from the NMI"
  },
  "E709": {
   "unit": "game/modemDriverE000",
   "line": "the start bit must be low; high means noise or a framing error"
  },
  "E70B": {
   "unit": "game/modemDriverE000",
   "line": "8 data bits plus the stop bit to sample"
  },
  "E70D": {
   "unit": "game/modemDriverE000",
   "line": "reload the receive bit counter"
  },
  "E710": {
   "unit": "game/modemDriverE000",
   "line": "always taken"
  },
  "E712": {
   "unit": "game/modemDriverE000",
   "line": "the stop bit must be high; low is a framing error"
  },
  "E714": {
   "unit": "game/modemDriverE000",
   "line": "a good character means the link is alive:"
  },
  "E716": {
   "unit": "game/modemDriverE000",
   "line": "clear the protocol stall counter"
  },
  "E719": {
   "unit": "game/modemDriverE000",
   "line": "and tell the carrier sampler that data is flowing"
  },
  "E71C": {
   "unit": "game/modemDriverE000",
   "line": "the assembled character"
  },
  "E71F": {
   "unit": "game/modemDriverE000",
   "line": "hand it to the protocol"
  },
  "E722": {
   "unit": "game/modemDriverE000",
   "line": "stored - done"
  },
  "E724": {
   "unit": "game/modemDriverE000",
   "line": "framing error, bad stop bit or receive overflow"
  },
  "E727": {
   "unit": "game/modemDriverE000",
   "line": "stop timer A: this character is finished"
  },
  "E729": {
   "unit": "game/modemDriverE000",
   "line": "0 in the control register stops the receive clock"
  },
  "E72C": {
   "unit": "game/modemDriverE000",
   "line": "$92 = back to waiting for the next start bit on FLAG"
  },
  "E72E": {
   "unit": "game/modemDriverE000",
   "line": "and remember it in the shadow"
  },
  "E731": {
   "unit": "game/modemDriverE000",
   "line": "always taken"
  },
  "E733": {
   "unit": "game/modemDriverE000",
   "routine": [
    "popArqTxRingThunk - the fixed entry the frame layer uses to fetch the next payload byte the packet layer queued.",
    "In:  none",
    "Out: A = byte, C = 0/1 (see popArqTxRing)",
    "Called from: the frame builder at $E8AE and $E8D4"
   ],
   "line": "the frame builder's entry for taking the next queued payload byte"
  }
 },
 "dataTypes": {
  "E020": [
   "byte",
   7
  ],
  "E027": [
   "byte",
   7
  ],
  "E04F": [
   "byte",
   2
  ],
  "E051": [
   "byte",
   4
  ],
  "E059": [
   "grid",
   6,
   3
  ],
  "E05F": [
   "byte",
   8
  ],
  "E086": [
   "byte",
   8
  ],
  "E091": [
   "byte",
   16
  ],
  "E3BF": [
   "addr",
   12
  ],
  "E417": [
   "byte",
   22
  ],
  "E430": [
   "byte",
   20
  ],
  "E447": [
   "byte",
   18
  ],
  "E45C": [
   "byte",
   18
  ]
 }
}
