{
 "chunk": "game_main_new_0DB7",
 "unit": "game/main_0800",
 "routines": [
  {
   "addr": "0DB7",
   "name": "scanKeyboard",
   "summary": "Full C64 keyboard-matrix scanner; the game polls the keyboard itself because the KERNAL is banked out ($01=$35). Installed into the comm module's key vector $E013/$E014 by initCommModule and invoked once per frame through $E015 -> pollKeyboardEvent. It first drives PA=$FF and checks that PB and PA both read $FF (a joystick plugged into port 1 or 2 pulls lines low), then that at least one key is down with all rows selected; it tests RSHIFT (PA6/PB4), LSHIFT (PA1/PB7) and C= (PA7/PB5) as modifiers, then scans rows 7..0 (masks from D_2F8D) and columns 7..0 (masks from spriteBitMasks) and translates the first key found through keyMatrixCodeTable[row*8+col]. Table value $FE (the shift/C= cells) is skipped and scanning continues, $FF (CTRL) aborts with 'no key'.",
   "inputs": "CIA1_PRA/PRB/DDRA/DDRB; keyMatrixCodeTable ($0D67); row masks D_2F8D ($2F8D); column masks spriteBitMasks ($2F85)",
   "outputs": "A = key code or $FF for none; Y = $80 when SHIFT is held else $00; X = $80 when C= is held else $00. Leaves CIA1_DDRA=$FF, CIA1_DDRB=$00 and CIA1_PRA holding the last row mask (the IRQ relies on DDRA=$FF to read joystick 2 afterwards). Self-modifies the operands at $0E2B, $0E3A, $0E41, $0E43.",
   "confidence": "high"
  },
  {
   "addr": "0E45",
   "name": "testKeyMask",
   "summary": "Helper for scanKeyboard: selects the keyboard row given by the port-A pattern in Y, reads CIA1 port B twice until two reads agree (debounce) and ANDs the result with the column mask A (patched into the operand at $0E54). Z=1 means that single key is pressed.",
   "inputs": "Y = row select pattern for CIA1_PRA, A = column bit mask",
   "outputs": "A = masked port B value, Z=1 if the key is down. Self-modifies $0E54.",
   "confidence": "high"
  },
  {
   "addr": "0E56",
   "name": "pollKeyboardEvent",
   "summary": "Per-frame keyboard front end, called only from rasterIrqHandler. Calls the comm module key entry $E015 (which vectors to scanKeyboard), stores the raw results in rawKeyCode ($92A0), shiftHeldFlag ($92E4) and commodoreHeldFlag ($92E5), then does auto-repeat: a key identical to lastKeyCode ($90E9) is ignored until the 20-frame timer zp_70 expires. A fresh key sets zp_70=$14, newKeyEvent ($90EA) and lastKeyCode; codes >= $80 (printable PETSCII|$80) are also stored in lastTypedKey ($9248) and in currentKey (zp_66), and when SHIFT is held codes $AC-$BB (',' '-' '.' '/' '0'-'9' ':' ';') are replaced from shiftedKeyTable. Codes < $80 (F1/F3/F5/F7 = 0..3, RETURN, DEL, cursor keys) go to currentKey only and clear newKeyEvent.",
   "inputs": "comm module entry $E015 -> scanKeyboard results; zp_70; $90E9",
   "outputs": "zp_66 currentKey, zp_70 repeat timer, $90E9 lastKeyCode, $90EA newKeyEvent, $9248 lastTypedKey, $92A0/$92E4/$92E5 raw scan results",
   "confidence": "high"
  },
  {
   "addr": "0EB1",
   "name": "checkFirePressed",
   "summary": "Edge-detects a fresh joystick fire-button press and is the standard 'wait for the button' primitive (callers loop while it returns non-zero). Returns A=1 (Z=0) when the command ring is nearly full (isCmdRingNearlyFull $59B3 returns C=1), while the 10-frame debounce timer zp_6D is running, or while both zp_5F (fire) and zp_79 (armed flag) are zero. On a genuine new press (zp_5F=0 after zp_79 was set) it reloads zp_6D=10, plays click sound 1 and returns A=0; a release just re-arms zp_79.",
   "inputs": "zp_5F fire state (0 = pressed), zp_79 armed flag, zp_6D debounce timer, $908D/$908E via isCmdRingNearlyFull",
   "outputs": "A/Z (0 = fresh press), zp_79, zp_6D, sound 1 queued",
   "confidence": "high"
  },
  {
   "addr": "0ED5",
   "name": "checkFirePressedInteractive",
   "summary": "checkFirePressed plus a 'is the local player allowed to act' gate; renamed from the phase-1 name checkFirePressedNotSolo, which is a misnomer (the solo trainer sets $0B9B=0, film playback sets $0B9B=$82). Returns A=0 only for a fresh fire press while filmPlaybackFlag ($0B9B) is not negative and gameOverFlags ($923F) bit 6 is clear, i.e. not while a game film is being watched and not after the game has ended.",
   "inputs": "zp_5F/zp_79/zp_6D, $0B9B, $923F",
   "outputs": "A/Z (0 = press accepted)",
   "confidence": "medium"
  },
  {
   "addr": "0EE5",
   "name": "isPlayerInputAllowed",
   "summary": "The gate half of checkFirePressedInteractive without the button test: loads A=0 and jumps into $0EDA, so it returns Z=1 only when filmPlaybackFlag ($0B9B) is not negative and gameOverFlags ($923F) bit 6 is clear. Called from the REPAIR tab menu loop at $658B to decide whether attemptSystemRepair may run for the selected system. (Phase 1 named this getSoloOrFlag923F and recorded it as unreferenced; the XREF shows the call from $658B.)",
   "inputs": "$0B9B, $923F",
   "outputs": "A/Z (Z=1 = the player may act)",
   "confidence": "medium"
  },
  {
   "addr": "0EEA",
   "name": "isFireHeld",
   "summary": "Level (not edge) version of checkFirePressed with no debounce reload, no sound and no ring-buffer check: returns A=0 while the fire button is held after having been released once, A=1 otherwise. Used by the map/console loops at $2376 and $407A where a held button must keep an action running.",
   "inputs": "zp_5F, zp_79, zp_6D",
   "outputs": "A/Z, zp_79",
   "confidence": "high"
  },
  {
   "addr": "0EFE",
   "name": "stopAllSound",
   "summary": "Silences everything by calling the $C000 overlay sound entry $C89A with A=0, which fills the four sound-request slots $92E7-$92EA with $FF/$00. Called from initIrqAndInputState ($0BB5) and $1AF7. The seed disassembly labelled this loadRuntimeOverlays; the actual overlay loading starts at $0F03.",
   "inputs": "none",
   "outputs": "$92E7-$92EA reset, all SID voices released",
   "confidence": "high"
  },
  {
   "addr": "0F03",
   "name": "loadRuntimeOverlays_0F03",
   "summary": "One-shot start-up loader called from mainStart ($0A81): fast-loads track 30 sectors 0-15 into $C000-$CFFF (the runtime overlay that replaces the boot loader), then loadOverlayA (which chains message strings and the comm tail) and falls straight into loadCommModule.",
   "inputs": "$0BA5 (read further down the chain)",
   "outputs": "$C000-$CFFF, $6F00-$87FF (variant A), $8800-$89FF, $EC00-$EFFF, $E000-$EBFF, comm module initialised",
   "confidence": "high"
  },
  {
   "addr": "0F16",
   "name": "loadCommModule",
   "summary": "Loads the $E000 communications module and initialises it. With linkTypeFlag ($0BA5) bit 7 set (solo trainer / build 1) it disables the CIA interrupts, reads track 35 sectors 0-11 and then points the NMI vector at irqRti; otherwise it falls into loadCommBuild2 for the modem/null-modem build. Called from $0AF4 as well as via fall-through from loadRuntimeOverlays_0F03.",
   "inputs": "$0BA5 bit 7",
   "outputs": "$E000-$EBFF, comm module initialised, NMI vector (build 1)",
   "confidence": "high"
  },
  {
   "addr": "0F31",
   "name": "loadCommBuild2",
   "summary": "Loads communications module build 2 with interrupts off: track 18 sector 7 supplies the first page at $E000 (that sector lives on the unencrypted directory track), then track 34 sectors 1-11 fill $E100-$EBFF, and control falls into loadCommFinish.",
   "inputs": "load destination already set to $E000",
   "outputs": "$E000-$EBFF",
   "confidence": "high"
  },
  {
   "addr": "0F41",
   "name": "loadCommFinish",
   "summary": "Shared tail of the two comm-module load paths: performs the final loadSectors with the registers the caller prepared and falls into initCommModule.",
   "inputs": "A = sector count, Y = track, X = first sector",
   "outputs": "comm module loaded and initialised",
   "confidence": "high"
  },
  {
   "addr": "0F44",
   "name": "initCommModule",
   "summary": "Calls the comm module's init entry ($E003) with X=1, then patches the module's keyboard vector $E013/$E014 to point at scanKeyboard ($0DB7) so the module's $E012/$E015 entries call the game's own matrix scanner, and finally CLIs. Also called directly from $0AFC.",
   "inputs": "comm module resident at $E000",
   "outputs": "$E013/$E014 = $0DB7, I flag cleared",
   "confidence": "high"
  },
  {
   "addr": "0F55",
   "name": "loadSectorsDelayed",
   "summary": "Wrapper around loadSectors that first waits 8 frames through the $C000 raster-timed delay $C37F, giving the drive time to settle (and the previous fast-load handshake time to finish) before the next request. Every overlay/map/setup load in this chunk goes through it.",
   "inputs": "A = sector count, Y = track, X = first sector, load destination from setLoadDest",
   "outputs": "as loadSectors (memory filled, C = error)",
   "confidence": "high"
  },
  {
   "addr": "0F5F",
   "name": "suspendCommModule",
   "summary": "Asks the comm module to pause before the game touches the disk (fast loading needs interrupts off and exact timing). When the link is up ($E03B non-zero) it writes request code $C0 to $E039 and spins until the module acknowledges by setting bit 7 of $E03A. A is preserved across the call.",
   "inputs": "$E03B link-active flag",
   "outputs": "$E039 = $C0, blocks until $E03A bit 7 set",
   "confidence": "medium"
  },
  {
   "addr": "0F71",
   "name": "resumeCommModule",
   "summary": "Counterpart of suspendCommModule: when the link is up it clears the request byte $E039 and spins until the module clears bit 7 of $E03A.",
   "inputs": "$E03B",
   "outputs": "$E039 = 0, blocks until $E03A bit 7 clear",
   "confidence": "medium"
  },
  {
   "addr": "0FB1",
   "name": "checkDiskId",
   "summary": "Verifies which disk is in the drive. Reads the BAM (track 18 sector 0, unencrypted) to $0200 through loadSectorsDelayed and compares $02A2/$02A3 with the two-character id from diskIdTable[X] (X=0 -> 'OZ' game disk, X=2 -> 'EA' data disk); X is remembered in $92F0. On a match with the game disk while filmPlaybackFlag ($0B9B) is not negative it also copies the ten bytes at $02AB (disk name/serial) to $FFDE-$FFE7, which the link protocol uses to check that both players run the same disk. A on entry is a silent flag: bit 7 set returns C=1 without a message, otherwise the mismatch shows 'INSERT GAME DISK.' or (when the expected id starts with 'E') 'NOT A DATA DISK!' through showDiskMessageAndFail. Called from waitForGameDisk and from the film save/load code in overlay A ($7E53/$7E5C); $100D is an alternate entry used by overlay A ($7E71) just to show 'NOT A DATA DISK!'.",
   "inputs": "A = silent flag (bit 7), X = 0 or 2 index into diskIdTable",
   "outputs": "C=0 ok / C=1 wrong disk or drive error, $92F0, $FFDE-$FFE7, $0200-$02FF clobbered by the BAM, self-modifies $0FDD/$0FE4/$0FFF",
   "confidence": "high"
  },
  {
   "addr": "1018",
   "name": "diskErrorExit",
   "summary": "Loads the address of msgDiskError ('DISK ERROR.') and falls into showDiskMessageAndFail. Reached from checkDiskId when the BAM read fails and jumped to from overlay A ($7E9D).",
   "inputs": "none",
   "outputs": "C=1",
   "confidence": "high"
  },
  {
   "addr": "101C",
   "name": "showDiskMessageAndFail",
   "summary": "Common failure exit for the disk routines: plays sound 3 (error beep), displays the message whose address is in A/Y through the $C000 text entry $C3D8, waits 100 frames ($C37F), clears the message area ($C39F) and returns with C=1.",
   "inputs": "A = message address low byte, Y = high byte",
   "outputs": "C=1, message area restored",
   "confidence": "high"
  },
  {
   "addr": "1030",
   "name": "reloadOverlayForVariant",
   "summary": "Re-reads whichever $6F00 overlay variant should be resident after another overlay temporarily occupied that space. Suspends the comm module, waits for the game disk, then: overlayVariantFlag ($87FF) bit 7 clear means variant B is already in place so only the message strings and the comm tail are refreshed, bit 7 set means variant A is reloaded in full; finally the comm module is resumed. Called from $1C72 with $0BA7 bumped around it.",
   "inputs": "$87FF bit 7",
   "outputs": "overlay/strings reloaded, comm module suspended and resumed",
   "confidence": "medium"
  },
  {
   "addr": "1047",
   "name": "loadOverlayA",
   "summary": "Loads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00 and track 32 sectors 0-7 to $8000, then falls into loadStringsAndCommTail. Variant A holds the setup/menu/film code.",
   "inputs": "none",
   "outputs": "$6F00-$87FF = variant A, plus $8800-$89FF and $EC00-$EFFF from the fall-through",
   "confidence": "high"
  },
  {
   "addr": "1060",
   "name": "loadStringsAndCommTail",
   "summary": "Loads the message-string block from track 29 sectors 9-10 into $8800-$89FF, then falls into loadCommTailBuild2.",
   "inputs": "none",
   "outputs": "$8800-$89FF, $EC00-$EFFF",
   "confidence": "high"
  },
  {
   "addr": "1070",
   "name": "loadCommTailBuild2",
   "summary": "Loads the last page-group of comm build 2 (track 34 sectors 12-15) into $EC00-$EFFF. Also called from overlay A ($7B33) to put the comm tail back after the $EC00 sub-overlay from track 29 was temporarily loaded there.",
   "inputs": "none",
   "outputs": "$EC00-$EFFF",
   "confidence": "high"
  },
  {
   "addr": "1080",
   "name": "loadOverlayBIfNeeded_1080",
   "summary": "Called from $0B5F during game start: loads overlay B only when overlayVariantFlag ($87FF) bit 7 is clear, i.e. when B is the variant that should be resident but A is currently in memory; otherwise returns immediately.",
   "inputs": "$87FF bit 7",
   "outputs": "see loadOverlayB",
   "confidence": "high"
  },
  {
   "addr": "1086",
   "name": "loadOverlayB",
   "summary": "Loads the $6F00-$87FF overlay variant B (the in-game battlefield code): suspends the comm module, reads track 33 sectors 0-16 to $6F00 and track 32 sectors 8-15 to $8000, copies the byte at $206B into overlay location $750A (a per-side branch/opcode patch established by the main program), then resumes the comm module.",
   "inputs": "$206B",
   "outputs": "$6F00-$87FF = variant B, $750A patched",
   "confidence": "high"
  },
  {
   "addr": "10AB",
   "name": "loadMapAndSetup_10AB",
   "summary": "Loads the 40x40 battlefield map from track 18 sectors 8-14 into $F000-$F6FF and then falls into loadSetupBlock. Called from startGameFromSetup ($0B35) with $0BA7 bumped so the IRQ suppresses keyboard input while the disk is busy.",
   "inputs": "none",
   "outputs": "$F000-$F6FF map, $0200-$03FF setup block",
   "confidence": "high"
  },
  {
   "addr": "10BB",
   "name": "loadSetupBlock",
   "summary": "Loads the saved setup block (track 18 sectors 15-16) into $0200-$03FF; it carries the persisted game settings and the little startup code at $0300. Also called on its own from mainStart ($0A84).",
   "inputs": "none",
   "outputs": "$0200-$03FF",
   "confidence": "high"
  },
  {
   "addr": "10CB",
   "name": "suspendCommForDiskAccess",
   "summary": "Standard prologue for disk access: suspendCommModule followed by the $C000 entry $C853, which sets messageState ($92A1) to 1 and paints the status line (the drive-busy notice). Used by all the overlay/film load and save paths.",
   "inputs": "none",
   "outputs": "comm module paused, $92A1 = 1",
   "confidence": "medium"
  },
  {
   "addr": "10D1",
   "name": "resumeCommAfterDiskAccess",
   "summary": "Epilogue for disk access: resumeCommModule, clear messageState ($92A1) and clear/redraw the message area through $C39F.",
   "inputs": "none",
   "outputs": "comm module running, $92A1 = 0",
   "confidence": "medium"
  },
  {
   "addr": "10DC",
   "name": "waitForGameDiskInserted",
   "summary": "Suspends the comm module and then loops at $10DF: call checkDiskId(X=0, verbose); if the game disk is not present, show the 'press button' prompt and wait through $C85B, then retry. Returns only once the OZ disk is in the drive. Called from mainStart ($0ADC), reloadOverlayForVariant and overlay A ($81AC).",
   "inputs": "none",
   "outputs": "blocks until C=0 from checkDiskId; comm module left suspended for the caller",
   "confidence": "high"
  },
  {
   "addr": "10F1",
   "name": "rasterIrqHandler",
   "summary": "The game's only interrupt handler, entered from the RAM vector $FFFE at raster line $FA (installed by installRasterIrq $0C66). It first checks the B flag in the pushed status byte and treats a BRK as a no-op by jumping through the patchable operand at $10FF/$1100 (initInterruptVectors sets it to irqExit; it is effectively a debug hook). It then acknowledges the VIC IRQ, re-arms the raster line and the $FFFE/$FFFF vector from the shadows zp_AA/zp_AB/zp_AC, and guards against re-entry with irqInProgress (zp_38). Phase 1: copy VIC_CTRL1/CTRL2 from the shadows $9000/$9001 and call the comm module entries $E000 (X=0, bit/byte timing) and $E00C (carrier tracking) - this half runs even while a disk load is in progress. Phase 2 (skipped only if $0BA7 were negative): copy the rest of the VIC shadows ($9002-$9006, $9020, $9023, $9024), then for sprites 7..0 write the colour shadow $9008+n (values >= $10 are two packed nibbles that alternate every other frame to make the sprite flash), the X shadow $9010+n and the Y shadow $9018+n, each biased by the fine-scroll offsets derived from $92F4/$92F5 when $92C8 is non-zero, and finally the enable mask $9022 (also stashed in spriteEnableScratch). It bumps the 24-bit frame counter zp_45/46/47, CLIs, and unless inputLockoutTimer ($0B7D) is counting down polls the keyboard (pollKeyboardEvent) and - only when no key is pending - reads joystick 2 from CIA1_PRA into zp_5F/zp_61 (fire) and zp_5D/zp_60 (direction). It then maintains the idle counter $90EE, steps the RNG, decrements the six countdown timers zp_6C-zp_71, suppresses keys while $0BA7 is non-zero, detects SHIFT+C=+F7 as the abort hotkey (sets $0BA7=1, inputLockoutTimer=$FF and JMPs to returnToMainMenu, which rebuilds the stack, so this IRQ never returns), blinks the cursor colour, and calls updateFrameTimers, handleChatSend, receiveChatCharacter, the sound dequeue $C8E9 (starting the returned sound through startSound $6728) and soundPlayerTick ($6637). Finally it clears irqInProgress and falls into irqExit.",
   "inputs": "VIC/CIA1 registers, shadows $9000-$9024, $0B7D, $0BA7, $92C8/$92F4/$92F5, comm module entries $E000/$E00C",
   "outputs": "all VIC registers and sprite registers, zp_45-zp_47, zp_5D/zp_5F/zp_60/zp_61, zp_66, zp_38, timers zp_6C-zp_71, $90E9/$90EA/$90EE/$9248/$92A0/$92E4/$92E5, chat buffers, sound state",
   "confidence": "high"
  },
  {
   "addr": "1293",
   "name": "irqExit",
   "summary": "Pulls Y, X and A back off the stack and falls into irqRti. It is both the normal end of rasterIrqHandler and the target patched into the BRK-exit JMP at $10FF/$1100 by initInterruptVectors.",
   "inputs": "stack layout pushed by rasterIrqHandler",
   "outputs": "registers restored, RTI",
   "confidence": "high"
  },
  {
   "addr": "1298",
   "name": "irqRti",
   "summary": "A bare RTI. Used as the NMI vector target for the whole game ($FFFA/$FFFB and the comm module's copy at $E031/$E032 are set to $1298 by initInterruptVectors and setNmiVectorRti), so RS-232 NMIs from a modem cannot disturb the game.",
   "inputs": "none",
   "outputs": "none",
   "confidence": "high"
  },
  {
   "addr": "129C",
   "name": "updateFrameTimers",
   "summary": "Per-frame housekeeping called from rasterIrqHandler. Increments the two free-running animation ticks $92F8 and $92FB. Every 8 frames (every 16 while zp_B1 = $FF, i.e. during actual play) it advances linkTimeoutCounter ($9223) while that counter is negative, saturating at $FF - this is the packet-exchange watchdog that checkLinkTimeout ($5A14) arms with bit 7 and that a completed exchange clears at $4E69. In the same 8-frame slot it counts gameEndCountdown ($92C9) up from 1 towards $80 and decrements zp_7F. Every 4 frames it decrements zp_72 and $9163; every frame it decrements $90FC, $91C8, $9234 (unless it holds the sentinel $80 = frozen) and $923E, each only while non-zero.",
   "inputs": "zp_45 frame counter, zp_B1 game phase",
   "outputs": "$92F8, $92FB, $9223, $92C9, zp_7F, zp_72, $9163, $90FC, $91C8, $9234, $923E",
   "confidence": "high"
  },
  {
   "addr": "130A",
   "name": "stepRandomSeed",
   "summary": "Called from the IRQ every frame: if the 24-bit RNG state zp_57/zp_58/zp_59 is all zero it re-seeds the low byte from the current joystick direction zp_5D (so the generator can never lock up and is entropy-fed by the player), then advances the LFSR through nextGameRandom ($FD5D in the high-memory resident block).",
   "inputs": "zp_57-zp_59, zp_5D",
   "outputs": "zp_57-zp_59 advanced, A = the new random byte",
   "confidence": "high"
  },
  {
   "addr": "131B",
   "name": "blinkCursorColor",
   "summary": "Blinks the map cursor: a 30-frame countdown in cursorBlinkTimer ($131A) toggles bit 0 of sprite colour shadow $9008 and copies the result to $900A, so sprites 0 and 2 alternate between two adjacent colours. The IRQ only calls it while currentScreen ($90FB) is 0 (the battlefield screen) and zp_B1 is not negative.",
   "inputs": "$131A, $9008",
   "outputs": "$131A, $9008, $900A",
   "confidence": "high"
  },
  {
   "addr": "1331",
   "name": "handleChatSend",
   "summary": "Outgoing half of the modem chat ('message to opponent') feature, run every frame from the IRQ. It does nothing and clears chatState ($929C) when the link is down ($E03B = 0) or while the film-name entry is using the same line buffer ($92E6 non-zero). Otherwise: with chatState positive, pressing RETURN (newKeyEvent = $8D) or a chatState of $7F opens typing mode - chatState = $C0, the cursor glyph $9E is put at the head of the outgoing line buffer $9278, the key is swallowed and sound 2 plays; this only starts when the comm transmit queue $E02E is empty. With chatState negative it takes the character count from bits 0-5, and if the transmit queue is not full (< $10 bytes) it calls the $C000 line editor $C7D5 with A=$A3 (max 35 characters, scroll when full) to consume the next key into $9278, then queues that character to the opponent through the comm send entry $E009 and stores the new count back as ($count ^ $80) | $40. If the queue has filled up it instead sends RETURN ($8D) and leaves typing mode by clearing bit 7 of chatState.",
   "inputs": "$E03B, $E02E, $92E6, $929C, $90EA, $9248",
   "outputs": "$929C, $9278.. line buffer, $90EA/$9248 cleared, one byte queued to $E009, sounds 2",
   "confidence": "medium"
  },
  {
   "addr": "1399",
   "name": "receiveChatCharacter",
   "summary": "Incoming half of the chat, called every frame from the IRQ. Pulls one byte from the comm module receive ring ($E006 returns C=1 when empty) and ignores anything below $80. RETURN ($8D) marks the message complete by setting incomingChatLength ($9277) to $80 and falls into the completion tail; DEL ($88) backspaces by overwriting the previous character with a space through appendChatCharacter; everything else falls into appendChatCharacter.",
   "inputs": "comm module receive ring via $E006, $9277",
   "outputs": "$9253-$9276 buffer, $9277, zp_72, sound 5",
   "confidence": "high"
  },
  {
   "addr": "13C4",
   "name": "appendChatCharacter",
   "summary": "Appends character A to the 36-byte incoming-message buffer at $9253 (index/length in $9277; a negative $9277, meaning the previous message was complete, restarts at 0, and when the buffer is full at 35 characters the whole buffer is scrolled left one byte). It then rewrites the whole visible status line: the leading glyph $1E goes to $0548 and the message text to $0549 onwards, with only the newest character carrying bit 7 - the game's string terminator convention - so the line always ends exactly at the last character received. Finally it clears the message hold timer zp_72 and plays sound 5 (the incoming-character click).",
   "inputs": "A = character, $9277, $9253 buffer",
   "outputs": "$9253-$9276, $9277, $0548-$056C status line, zp_72 = 0, sound 5",
   "confidence": "medium"
  }
 ],
 "variables": [
  {
   "addr": "33",
   "scope": "zp",
   "name": "spriteFlashPhase",
   "meaning": "Scratch used only inside rasterIrqHandler: zp_45 rotated right twice so bit 7 mirrors bit 0 of the frame counter; BIT-tested to alternate two-nibble sprite colours every other frame.",
   "confidence": "high"
  },
  {
   "addr": "38",
   "scope": "zp",
   "name": "irqInProgress",
   "meaning": "Re-entry guard for rasterIrqHandler. Set on entry, cleared just before irqExit; because the handler does a CLI half-way through, a second raster IRQ can fire and is turned into an immediate exit by this flag. Initialised to 0 by installRasterIrq.",
   "confidence": "high"
  },
  {
   "addr": "45",
   "scope": "zp",
   "name": "frameCounter",
   "meaning": "Low byte of the free-running 24-bit frame counter incremented once per raster IRQ; its low bits drive the flash phase and all the divide-by-4/8/16 timers in updateFrameTimers.",
   "confidence": "high"
  },
  {
   "addr": "46",
   "scope": "zp",
   "name": "frameCounterMid",
   "meaning": "Middle byte of the 24-bit frame counter.",
   "confidence": "high"
  },
  {
   "addr": "47",
   "scope": "zp",
   "name": "frameCounterHi",
   "meaning": "High byte of the 24-bit frame counter.",
   "confidence": "high"
  },
  {
   "addr": "57",
   "scope": "zp",
   "name": "rngState",
   "meaning": "Low byte of the 24-bit random-number state; re-seeded from the joystick direction by stepRandomSeed whenever all three bytes are zero.",
   "confidence": "high"
  },
  {
   "addr": "58",
   "scope": "zp",
   "name": "rngStateMid",
   "meaning": "Middle byte of the 24-bit random-number state.",
   "confidence": "high"
  },
  {
   "addr": "59",
   "scope": "zp",
   "name": "rngStateHi",
   "meaning": "High byte of the 24-bit random-number state.",
   "confidence": "high"
  },
  {
   "addr": "5D",
   "scope": "zp",
   "name": "joyDirection",
   "meaning": "Joystick 2 direction nibble latched by the IRQ, active low ($0F = centred). Forced to $0F for one frame whenever a keypress is pending.",
   "confidence": "high"
  },
  {
   "addr": "5F",
   "scope": "zp",
   "name": "joyFireUp",
   "meaning": "Joystick 2 fire state latched by the IRQ: 0 = button down, 1 = released.",
   "confidence": "high"
  },
  {
   "addr": "60",
   "scope": "zp",
   "name": "joyDirectionCopy",
   "meaning": "Second copy of joyDirection written by the IRQ at $1216. No reader appears anywhere in the traced code - an unconsumed shadow (dead store).",
   "confidence": "medium"
  },
  {
   "addr": "61",
   "scope": "zp",
   "name": "joyFireUpCopy",
   "meaning": "Second copy of joyFireUp written by the IRQ at $1210. Like zp_60 it has no reader in the traced code.",
   "confidence": "medium"
  },
  {
   "addr": "66",
   "scope": "zp",
   "name": "currentKey",
   "meaning": "Current key as seen by the game loops ($FF = none). Set by pollKeyboardEvent, forced to $FF by the IRQ while $0BA7 is non-zero and after the abort hotkey. Codes 0-3 (F1/F3/F5/F7) request a screen change through sub_06BA.",
   "confidence": "high"
  },
  {
   "addr": "6C",
   "scope": "zp",
   "name": "joyRepeatTimer",
   "meaning": "First of the six per-frame countdown timers zp_6C-zp_71 decremented by the IRQ; set by setJoystickRepeatDelay to 16 frames for the first auto-repeat step and 6 for later ones.",
   "confidence": "high"
  },
  {
   "addr": "6D",
   "scope": "zp",
   "name": "fireDebounceTimer",
   "meaning": "10-frame debounce reloaded by checkFirePressed after a click; while non-zero both fire-test routines report 'not pressed'.",
   "confidence": "high"
  },
  {
   "addr": "70",
   "scope": "zp",
   "name": "keyRepeatTimer",
   "meaning": "20-frame key auto-repeat timer maintained by pollKeyboardEvent and decremented by the IRQ; a held key produces a new event only after it expires.",
   "confidence": "high"
  },
  {
   "addr": "72",
   "scope": "zp",
   "name": "messageHoldTimer",
   "meaning": "Timer decremented every 4 frames by updateFrameTimers and cleared to 0 by appendChatCharacter and by the $C000 message code; while non-zero the status-line message stays put ($C4F6 returns early).",
   "confidence": "medium"
  },
  {
   "addr": "79",
   "scope": "zp",
   "name": "fireArmedFlag",
   "meaning": "Fire-button edge latch: set when the button is seen released, cleared when a press is accepted, so checkFirePressed reports each press exactly once.",
   "confidence": "high"
  },
  {
   "addr": "7F",
   "scope": "zp",
   "name": "slowTimer7F",
   "meaning": "Countdown decremented every 8 frames by updateFrameTimers; loaded and tested around $1AAE/$1B49/$5714 (link/setup sequencing).",
   "confidence": "low"
  },
  {
   "addr": "AA",
   "scope": "zp",
   "name": "irqRasterLine",
   "meaning": "Shadow of the raster line the handler re-arms VIC_RASTER with each interrupt; set to $FA by installRasterIrq.",
   "confidence": "high"
  },
  {
   "addr": "AB",
   "scope": "zp",
   "name": "irqVectorLo",
   "meaning": "Shadow low byte of the IRQ vector; rasterIrqHandler writes it back to $FFFE every interrupt, so changing it retargets the next IRQ. Only ever set to $F1 (i.e. $10F1).",
   "confidence": "high"
  },
  {
   "addr": "AC",
   "scope": "zp",
   "name": "irqVectorHi",
   "meaning": "Shadow high byte of the IRQ vector, written back to $FFFF each interrupt. Only ever set to $10.",
   "confidence": "high"
  },
  {
   "addr": "B1",
   "scope": "zp",
   "name": "gamePhase",
   "meaning": "Game phase / console context byte ($FF while the battle is running, other values during setup, film and console screens). In this chunk it halves the link-timeout tick rate and gates the cursor blink.",
   "confidence": "medium"
  },
  {
   "addr": "0548",
   "scope": "abs",
   "name": "statusLineBuffer",
   "meaning": "First byte of the status/message line rendered by the $C000 text engine (the line lives in the $0400-$07FF block that was copied from $6F00-$72FF). appendChatCharacter puts the glyph $1E there in front of an incoming chat message; $C62A resets it to $9E (empty).",
   "confidence": "medium"
  },
  {
   "addr": "0549",
   "scope": "abs",
   "name": "statusLineText",
   "meaning": "Text of the status line, $0549-$056C. Written by appendChatCharacter with every character plain except the newest, which carries bit 7 as the terminator.",
   "confidence": "medium"
  },
  {
   "addr": "0B7D",
   "scope": "abs",
   "name": "inputLockoutTimer",
   "meaning": "Frame countdown that freezes keyboard and joystick sampling in the IRQ; set to $FF by the abort hotkey, $9E at start-up, $3C by $C48B and various menus.",
   "confidence": "high"
  },
  {
   "addr": "0B9B",
   "scope": "abs",
   "name": "filmPlaybackFlag",
   "meaning": "0 for a live modem or solo-trainer game, $82 while watching a game film. Bit 7 is what checkFirePressedInteractive and isPlayerInputAllowed test to lock out player input; checkDiskId also skips the disk-serial copy when it is set.",
   "confidence": "medium"
  },
  {
   "addr": "0BA5",
   "scope": "abs",
   "name": "linkTypeFlag",
   "meaning": "Copy of the comm module's $E030; bit 7 selects comm build 1 (track 35, the solo-trainer AI opponent) instead of build 2 (modem/null modem). loadCommModule branches on it.",
   "confidence": "high"
  },
  {
   "addr": "0BA7",
   "scope": "abs",
   "name": "diskLoadDepth",
   "meaning": "Nesting counter bumped with INC/DEC around every disk load and overlay swap. Non-zero makes the IRQ discard keypresses (currentKey forced to $FF). The IRQ also tests bit 7 at $112F to skip the whole game half of the interrupt, but no traced writer ever makes it negative, so that early exit appears unreachable.",
   "confidence": "medium"
  },
  {
   "addr": "87FF",
   "scope": "abs",
   "name": "overlayVariantFlag",
   "meaning": "Bit 7 selects which $6F00-$87FF overlay variant should be resident (set = A, clear = B); read by reloadOverlayForVariant and loadOverlayBIfNeeded.",
   "confidence": "high"
  },
  {
   "addr": "9000",
   "scope": "abs",
   "name": "vicCtrl1Shadow",
   "meaning": "Shadow of VIC_CTRL1 ($D011), copied to the VIC in the first phase of every IRQ (together with $9001) so it is updated even while a disk load blocks the rest of the handler.",
   "confidence": "high"
  },
  {
   "addr": "9001",
   "scope": "abs",
   "name": "vicCtrl2Shadow",
   "meaning": "Shadow of VIC_CTRL2 ($D016).",
   "confidence": "high"
  },
  {
   "addr": "9002",
   "scope": "abs",
   "name": "vicMemPtrShadow",
   "meaning": "Shadow of VIC_MEM_PTR ($D018) - bitmap/screen base selection.",
   "confidence": "high"
  },
  {
   "addr": "9003",
   "scope": "abs",
   "name": "vicBorderShadow",
   "meaning": "Shadow of the border colour ($D020).",
   "confidence": "high"
  },
  {
   "addr": "9004",
   "scope": "abs",
   "name": "vicBg0Shadow",
   "meaning": "Shadow of background colour 0 ($D021); $9005 and $9006 shadow $D022/$D023 for multicolour bitmap mode.",
   "confidence": "high"
  },
  {
   "addr": "9008",
   "scope": "abs",
   "name": "spriteColorShadow",
   "meaning": "Eight sprite colour shadows $9008-$900F. A value below $10 is written straight to $D027+n; $10 and above packs two colours in the two nibbles and the IRQ alternates between them every other frame to make the sprite flash.",
   "confidence": "high"
  },
  {
   "addr": "9010",
   "scope": "abs",
   "name": "spriteXShadow",
   "meaning": "Eight sprite X-position shadows $9010-$9017, written to $D000+2n with the fine-scroll bias from $92F4 added.",
   "confidence": "high"
  },
  {
   "addr": "9018",
   "scope": "abs",
   "name": "spriteYShadow",
   "meaning": "Eight sprite Y-position shadows $9018-$901F, written to $D001+2n with the fine-scroll bias from $92F5 (minus 3) added.",
   "confidence": "high"
  },
  {
   "addr": "9020",
   "scope": "abs",
   "name": "spriteMsbXShadow",
   "meaning": "Shadow of VIC_SPR_HI_X ($D010).",
   "confidence": "high"
  },
  {
   "addr": "9022",
   "scope": "abs",
   "name": "spriteEnableShadow",
   "meaning": "Shadow of VIC_SPR_ENA ($D015); the IRQ also copies it to spriteEnableScratch ($10EF).",
   "confidence": "high"
  },
  {
   "addr": "9023",
   "scope": "abs",
   "name": "spriteExpandXShadow",
   "meaning": "Shadow of VIC_SPR_EXP_X ($D01D).",
   "confidence": "high"
  },
  {
   "addr": "9024",
   "scope": "abs",
   "name": "spriteExpandYShadow",
   "meaning": "Shadow of VIC_SPR_EXP_Y ($D017).",
   "confidence": "high"
  },
  {
   "addr": "90E9",
   "scope": "abs",
   "name": "lastKeyCode",
   "meaning": "Raw code of the key reported last frame; pollKeyboardEvent compares against it to implement auto-repeat.",
   "confidence": "high"
  },
  {
   "addr": "90EA",
   "scope": "abs",
   "name": "newKeyEvent",
   "meaning": "One-shot 'a new printable key was pressed this frame' code ($FF = none). Consumed by handleChatSend and the $C000 line editor, which clear it back to $FF.",
   "confidence": "high"
  },
  {
   "addr": "90EE",
   "scope": "abs",
   "name": "inputIdleFrames",
   "meaning": "Counts frames with the joystick centred and the fire button up, saturating at $FF; any input resets it to 0. Read by the map/console loops ($2042, $21C0, $24F3, $63E9) to time out prompts.",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreen",
   "meaning": "Currently displayed screen (0 = battlefield map, 3 = main menu, others for setup/console); the IRQ only blinks the cursor colour while it is 0.",
   "confidence": "high"
  },
  {
   "addr": "90FC",
   "scope": "abs",
   "name": "frameTimer90FC",
   "meaning": "General per-frame countdown decremented by updateFrameTimers; used around $21B8/$2302/$41BD.",
   "confidence": "low"
  },
  {
   "addr": "9163",
   "scope": "abs",
   "name": "exchangeRetryTimer",
   "meaning": "Countdown decremented every 4 frames; reloaded with 10 at $4E64 when a command exchange completes, so it paces the link packet exchange ($4DB1 tests it).",
   "confidence": "medium"
  },
  {
   "addr": "91C8",
   "scope": "abs",
   "name": "frameTimer91C8",
   "meaning": "Per-frame countdown decremented by updateFrameTimers, loaded at $3B1E and tested at $415A.",
   "confidence": "low"
  },
  {
   "addr": "91C9",
   "scope": "abs",
   "name": "joyCenteredMask",
   "meaning": "Auto-repeat mask ANDed into joyHoldCounter; the IRQ clears it to 0 whenever the stick is centred, and setJoystickRepeatDelay reloads it with $7F.",
   "confidence": "high"
  },
  {
   "addr": "9223",
   "scope": "abs",
   "name": "linkTimeoutCounter",
   "meaning": "Packet-exchange watchdog. checkLinkTimeout ($5A14) ORs in bit 7 to arm it; updateFrameTimers then counts it up towards $FF every 8 frames (16 while zp_B1 = $FF); a completed exchange clears it at $4E69. Reaching $E1 triggers the retry/'TRY AGAIN' path.",
   "confidence": "high"
  },
  {
   "addr": "9234",
   "scope": "abs",
   "name": "frameTimer9234",
   "meaning": "Per-frame countdown decremented by updateFrameTimers except when it holds the sentinel value $80, which freezes it.",
   "confidence": "medium"
  },
  {
   "addr": "923E",
   "scope": "abs",
   "name": "frameTimer923E",
   "meaning": "Per-frame countdown decremented by updateFrameTimers, loaded at $3407 and tested at $341F/$416E.",
   "confidence": "low"
  },
  {
   "addr": "923F",
   "scope": "abs",
   "name": "gameOverFlags",
   "meaning": "Set to $C0 when a game ends or a film finishes; bit 6 is what checkFirePressedInteractive / isPlayerInputAllowed test to refuse further player commands.",
   "confidence": "medium"
  },
  {
   "addr": "9248",
   "scope": "abs",
   "name": "lastTypedKey",
   "meaning": "Last printable key (PETSCII|$80) typed; used by the $C000 text/menu code and cleared to $FF when a key is consumed.",
   "confidence": "high"
  },
  {
   "addr": "9251",
   "scope": "abs",
   "name": "messageQueueHead",
   "meaning": "Write index of the 8-entry status-message queue at $9249-$9250; compared against messageQueueTail ($9252) to decide whether a message is pending. The comparison at $1400 in appendChatCharacter discards its result (dead code).",
   "confidence": "medium"
  },
  {
   "addr": "9252",
   "scope": "abs",
   "name": "messageQueueTail",
   "meaning": "Read index of the status-message queue at $9249-$9250, advanced by the $C000 message pump ($C624).",
   "confidence": "medium"
  },
  {
   "addr": "9253",
   "scope": "abs",
   "name": "incomingChatBuffer",
   "meaning": "36-byte buffer $9253-$9276 holding the chat message being received from the opponent, characters stored without bit 7.",
   "confidence": "high"
  },
  {
   "addr": "9277",
   "scope": "abs",
   "name": "incomingChatLength",
   "meaning": "Number of characters currently in incomingChatBuffer; $80 marks a completed message (RETURN received) so the next character starts a new one.",
   "confidence": "high"
  },
  {
   "addr": "9278",
   "scope": "abs",
   "name": "outgoingLineBuffer",
   "meaning": "36-byte line-edit buffer $9278-$929B, filled by the $C000 editor $C7D5 with a trailing cursor glyph $9E. Shared between the outgoing chat message and the SAVE GAME FILM name entry, which is why handleChatSend stands down while $92E6 is non-zero.",
   "confidence": "high"
  },
  {
   "addr": "929C",
   "scope": "abs",
   "name": "chatState",
   "meaning": "Chat/typing state: 0 = idle (link down), bit 7 set = typing a message to the opponent, bit 6 = the chat line is displayed, bits 0-5 = number of characters typed so far (max 35). Also poked by the $C000 message code ($C49C/$C4BA/$C4D1) and by $1BD3/$29AD/$5224/$5279/$7DBF.",
   "confidence": "medium"
  },
  {
   "addr": "92A0",
   "scope": "abs",
   "name": "rawKeyCode",
   "meaning": "Unfiltered key code returned by scanKeyboard this frame (before repeat filtering); read by the $C000 cursor-key handler at $C4B1.",
   "confidence": "high"
  },
  {
   "addr": "92A1",
   "scope": "abs",
   "name": "messageState",
   "meaning": "Status-line state byte; set to 1 by suspendCommForDiskAccess (drive busy notice) and cleared by resumeCommAfterDiskAccess.",
   "confidence": "medium"
  },
  {
   "addr": "92C8",
   "scope": "abs",
   "name": "spriteScrollEnable",
   "meaning": "When zero the IRQ writes the sprite shadows at their nominal positions; when non-zero it adds the fine-scroll offsets from $92F4/$92F5 so sprites track the smooth-scrolled tactical view.",
   "confidence": "medium"
  },
  {
   "addr": "92C9",
   "scope": "abs",
   "name": "gameEndCountdown",
   "meaning": "Counter incremented every 8 frames by updateFrameTimers while it is between 1 and $7F, stopping once it reaches $80; used to time the end-of-game sequence.",
   "confidence": "medium"
  },
  {
   "addr": "92E4",
   "scope": "abs",
   "name": "shiftHeldFlag",
   "meaning": "$80 when SHIFT (either key) was held during the last scan, else 0. Used for the shifted-key translation and, with $92E5, for the SHIFT+C=+F7 abort hotkey.",
   "confidence": "high"
  },
  {
   "addr": "92E5",
   "scope": "abs",
   "name": "commodoreHeldFlag",
   "meaning": "$80 when the C= key was held during the last scan, else 0.",
   "confidence": "high"
  },
  {
   "addr": "92E6",
   "scope": "abs",
   "name": "filmNameEntryLength",
   "meaning": "Non-zero while the SAVE GAME FILM slot name is being typed into the shared line buffer $9278; handleChatSend refuses to start typing a chat message while it is set.",
   "confidence": "medium"
  },
  {
   "addr": "92F0",
   "scope": "abs",
   "name": "diskIdIndex",
   "meaning": "Index (0 = 'OZ' game disk, 2 = 'EA' data disk) of the id checkDiskId last verified; also used to decide whether the disk serial should be copied to $FFDE.",
   "confidence": "high"
  },
  {
   "addr": "92F4",
   "scope": "abs",
   "name": "spriteScrollX",
   "meaning": "Fine horizontal scroll of the tactical view; the IRQ adds its low three bits to every sprite X shadow.",
   "confidence": "medium"
  },
  {
   "addr": "92F5",
   "scope": "abs",
   "name": "spriteScrollY",
   "meaning": "Fine vertical scroll of the tactical view; the IRQ adds (value & 7) - 3 to every sprite Y shadow.",
   "confidence": "medium"
  },
  {
   "addr": "92F8",
   "scope": "abs",
   "name": "frameTick92F8",
   "meaning": "Free-running frame tick incremented by updateFrameTimers; overlay B uses it (with a self-modified compare at $7807) as the clock for the missile-flight animation and resets it at $77CF.",
   "confidence": "high"
  },
  {
   "addr": "92FB",
   "scope": "abs",
   "name": "frameTick92FB",
   "meaning": "Free-running frame tick incremented by updateFrameTimers; overlay B's updateRadarSweep ($7894) uses it as the radar sweep clock and restartRadarSweep ($78D3) resets it.",
   "confidence": "high"
  },
  {
   "addr": "E02E",
   "scope": "abs",
   "name": "commTxCount",
   "meaning": "Number of bytes queued in the comm module's 16-byte transmit ring at $E091. handleChatSend requires it to be 0 before opening typing mode and force-terminates the message when it reaches $10 (full).",
   "confidence": "high"
  },
  {
   "addr": "E039",
   "scope": "abs",
   "name": "commRequest",
   "meaning": "Request byte written by suspendCommModule ($C0 = pause) / resumeCommModule (0).",
   "confidence": "medium"
  },
  {
   "addr": "E03A",
   "scope": "abs",
   "name": "commAckState",
   "meaning": "Acknowledge byte polled by suspend/resumeCommModule; bit 7 set means the module has stopped touching the hardware.",
   "confidence": "medium"
  },
  {
   "addr": "E03B",
   "scope": "abs",
   "name": "commActiveFlag",
   "meaning": "Non-zero when the communications module is running a link; guards the suspend/resume handshake and the whole chat send path.",
   "confidence": "high"
  },
  {
   "addr": "E013",
   "scope": "abs",
   "name": "commKeyVector",
   "meaning": "Two-byte vector inside the comm module ($E013/$E014) that its $E012/$E015 entries jump through; initCommModule points it at scanKeyboard ($0DB7).",
   "confidence": "high"
  },
  {
   "addr": "FFDE",
   "scope": "abs",
   "name": "diskSerial",
   "meaning": "Ten bytes copied from BAM offset $AB (disk name/id) by checkDiskId; the link protocol compares them so both players must run the same disk.",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "0F81",
   "length": 16,
   "type": "text",
   "name": "msgNotADataDisk",
   "description": "'NOT A DATA DISK!' - shown when the 'EA' data disk was expected but not found; the last byte carries bit 7 as the terminator."
  },
  {
   "addr": "0F91",
   "length": 17,
   "type": "text",
   "name": "msgInsertGameDisk",
   "description": "'INSERT GAME DISK.' - shown when the 'OZ' game disk is missing."
  },
  {
   "addr": "0FA2",
   "length": 11,
   "type": "text",
   "name": "msgDiskError",
   "description": "'DISK ERROR.' - shown by diskErrorExit when the BAM read fails."
  },
  {
   "addr": "0FAD",
   "length": 4,
   "type": "byteTable",
   "name": "diskIdTable",
   "description": "'OZEA': two two-character disk ids read as diskIdTable[X] / $0FAE[X], X=0 for the game disk ('OZ') and X=2 for the data disk ('EA'). The overlapping layout means index 1 would give 'ZE' (never used)."
  },
  {
   "addr": "10EF",
   "length": 2,
   "type": "byteTable",
   "name": "spriteEnableScratch",
   "description": "$10EF receives a copy of the sprite-enable shadow $9022 every IRQ and is never read back; $10F0 is untouched padding before the IRQ handler."
  },
  {
   "addr": "1299",
   "length": 3,
   "type": "unknown",
   "name": "padIrq1299",
   "description": "Three unreferenced bytes $00,$09,$13 between irqRti and updateFrameTimers - alignment padding or an assembler leftover."
  },
  {
   "addr": "131A",
   "length": 1,
   "type": "byteTable",
   "name": "cursorBlinkTimer",
   "description": "30-frame countdown used by blinkCursorColor; stored immediately before the routine that owns it."
  }
 ],
 "misclassified": [],
 "insights": [
  "Input model of the whole game, confirmed here: the KERNAL is banked out ($01=$35), so rasterIrqHandler ($10F1, raster line $FA) does all input itself. Every frame it calls pollKeyboardEvent -> comm entry $E015 -> scanKeyboard ($0DB7), and then - only if no key is pending - reads joystick 2 from CIA1_PRA (which scanKeyboard has just left configured as all-outputs-high). A pending keypress therefore suppresses joystick input for that frame, and joystick movement in either port makes scanKeyboard report 'no key'.",
  "keyMatrixCodeTable ($0D67) is indexed by row*8+column with row = the CIA1 PA line and column = the PB bit; the codes are PETSCII|$80 for printable keys, 0/1/2/3 for F1/F3/F5/F7, $88 DEL, $8D RETURN, $8B/$8E cursor, $A0 space, $FE for the shift/C= cells (skipped) and $FF for CTRL (aborts the scan).",
  "SHIFT + C= + F7 is the global abort hotkey: rasterIrqHandler detects it at $124F-$126B ($92E4 and $92E5 both negative and currentKey = 3), sets $0BA7=1 and inputLockoutTimer=$FF and JMPs to returnToMainMenu ($0AA3), which rebuilds the stack pointer - so that IRQ never RTIs.",
  "The IRQ is split in two halves. Phase 1 (VIC_CTRL1/CTRL2 from $9000/$9001 plus comm entries $E000 and $E00C) always runs; phase 2 (all other VIC shadows, the eight sprite colour/position/enable shadows, frame counter, input, timers, chat, sound) is guarded by $0BA7. But $0BA7 is only ever incremented from 0 around disk loads, so the bit-7 test at $112F never fails and the early exit at $1134 looks unreachable; the effective use of $0BA7 is the keypress suppression at $1246.",
  "Sprite colour shadows $9008-$900F encode flashing: a value < $10 is a plain colour, a value >= $10 packs two colours in its nibbles and the IRQ alternates between them on odd frames by comparing the current $D027+n against the low nibble. Positions come from $9010/$9018 with fine-scroll biases $92F4 & 7 and ($92F5 & 7) - 3 applied when $92C8 is non-zero, so sprites track the smooth-scrolled tactical view.",
  "Modem chat protocol: typing is opened with RETURN, one typed character per frame is queued to the opponent through comm entry $E009 (16-byte tx ring, depth at $E02E) and $8D (RETURN) ends the message. The receiving side pulls bytes with $E006 and appendChatCharacter builds them up in $9253-$9276 (length $9277, $80 = complete) while mirroring the text into the status line at $0548/$0549. The outgoing edit buffer $9278 is shared with the SAVE GAME FILM name entry, which is why handleChatSend stands down while $92E6 is non-zero.",
  "String terminator convention seen from the writer's side: appendChatCharacter stores the whole line plain and sets bit 7 only on the newest character, rewriting the earlier ones from the plain buffer each time. So bit 7 always marks exactly the current end of the line.",
  "updateFrameTimers ($129C) is the game's single timer tick. It also drives the link watchdog $9223: checkLinkTimeout ($5A14) arms it by ORing in bit 7, the tick counts it up to $FF every 8 frames (every 16 while zp_B1 = $FF, i.e. slower during actual play) and a completed packet exchange clears it at $4E69; $E1 is the retry threshold.",
  "All disk access in this chunk follows the same pattern: suspendCommForDiskAccess ($10CB) -> loadSectorsDelayed ($0F55, an 8-frame settle delay before every fast-load request) -> resumeCommAfterDiskAccess ($10D1), with $0BA7 bumped by the caller so keystrokes are dropped meanwhile.",
  "Dead code found: appendChatCharacter at $1400 loads messageQueueHead ($9251), compares it with messageQueueTail ($9252) and branches to $1408 - but the fall-through path is $1408 as well, so the test has no effect. Likewise zp_60 and zp_61 are written by the IRQ at $1216/$1210 and have no reader anywhere in the traced code.",
  "Corrections to the phase-1 knowledge base: $0EE5 (previously getSoloOrFlag923F, recorded as unreferenced) is called from $658B to gate attemptSystemRepair, and $0B9B is the film-playback flag ($82) rather than a solo-mode flag (the solo trainer sets $0B9B=0 and $0BA5=$FF), so the name checkFirePressedNotSolo at $0ED5 is misleading.",
  "$100D is a second entry point into checkDiskId's failure tail, jumped to from overlay A at $7E71 purely to display 'NOT A DATA DISK!' and return C=1."
 ]
}
