{
 "chunk": "game_ovl_C000_C5EE",
 "unit": "game/ovl_C000",
 "routines": [
  {
   "addr": "C5EE",
   "name": "showNextQueuedMessage",
   "summary": "Picks the next status-line message. If the 8-entry message queue is not empty (tail $9252 != head $9251) it takes the id at the tail, advances the tail and leaves it in currentMessageId zp_7A. If the queue is empty it either re-arms the idle state (zp_7A = $80, chat line cleared with clearChatStatusLine, then persistentMessageId $92A1 decides: negative = redraw the CLICKS/US/THEM score line at L_C6B6, zero = blank the status line via sub_C39F, otherwise print that message with sub_C5E4) or, when a chat line is pending (chatLineMarker $0548 not negative and zp_7A != $80), selects message id 0 (the chat line) and suppresses its sound by patching $C54B to $FF.",
   "inputs": "D_9251/D_9252 queue indices, D_9249 queue, zp_7A, D_0548, D_92A1",
   "outputs": "zp_7A, D_9252, D_0548 (via sub_C62A), sound-suppress operand $C54B; may print the status line and clobber A/Y",
   "confidence": "high"
  },
  {
   "addr": "C62A",
   "name": "clearChatStatusLine",
   "summary": "Stores $9E (a bit-7 terminator that prints nothing) into chatLineMarker $0548, i.e. makes message slot 0 - whose text pointer is $0548 - an empty status line. Called from showNextQueuedMessage and by JMP from the game's start-up init at $0C63.",
   "inputs": "none",
   "outputs": "D_0548 = $9E, A = $9E",
   "confidence": "high"
  },
  {
   "addr": "C631",
   "name": "handleMessageReviewKeys",
   "summary": "Per-frame hook of the message service (called from sub_C4EF at $C4F3). CRSR up/down (lastKeyCode $9248 == $8B) enters/steps the message-history scrollback: the current queue tail is saved in messageReviewIndex $C630 with bit 7 set, then each press walks one entry backwards through the ring $9249 (sound 3 when there is no older entry), shows it immediately (message timer zp_72 = 0, sound suppressed via $C54B = $80) and sets $0548 = $1E so the chat line stays live; when zp_72 drops below $23 without another press the saved index is restored and review mode ends. CRSR left/right ($8E) is consumed and, only during battle (zp_B1 == 0), arms a 60-frame input lockout, sets zp_72 = $3C, clears bit 7 of zp_7B and falls into drawStatusScoreLine.",
   "inputs": "D_C630, D_9248, zp_72, D_9251/D_9252, D_9249, zp_7A, zp_7B, zp_B1",
   "outputs": "D_C630, D_9252, D_9248 = $FF, zp_7A, zp_7B, zp_72, D_0548, $C54B, inputLockoutTimer $0B7D; may draw the score line",
   "confidence": "high"
  },
  {
   "addr": "C6B6",
   "name": "drawStatusScoreLine",
   "summary": "Redraws the bottom status line as '   CLICKS:<n>    US:<n>   THEM:<n>'. Saves the text window (sub_C41B), selects the status-line window (sub_C3AB), prints the string trio at $065E/$0668/$066F with sub_C133/sub_C137, the game clock gameClockClicks $91CB and the two 16-bit point totals through printPlayerPoints (first the local side D_0B9F EOR 1, then the opponent D_0B9F), pads the line (sub_C07A) and restores the window (sub_C40E). Entered by JMP only - from $C521 (clock changed while the last-quarter score display flag $92FA is set) and from handleMessageReviewKeys.",
   "inputs": "D_91CB, D_0B9F, D_92A2/D_92A4, strings at $065E",
   "outputs": "status line redrawn; zp_39-zp_44 window state saved/restored via $92B6; zp_7E, D_92DA digit buffer clobbered",
   "confidence": "high"
  },
  {
   "addr": "C6E5",
   "name": "printPlayerPoints",
   "summary": "A = player side index (0/1). Loads that side's 16-bit point total from playerPointsLo $92A2,x / playerPointsHi $92A4,x and tail-calls the decimal printer sub_C17D.",
   "inputs": "A = side index; D_92A2, D_92A4",
   "outputs": "digits printed; X = side index, zp_18-zp_1F and D_92DA clobbered",
   "confidence": "high"
  },
  {
   "addr": "C6EF",
   "name": "resetMessageQueue",
   "summary": "Empties the message queue by copying the head index $9251 into the tail $9252, clears persistentMessageId $92A1 and the message timer zp_72, then falls through to showNextQueuedMessage so the status line is refreshed at once. Called from a dozen places whenever a screen changes or a game phase starts/ends.",
   "inputs": "D_9251",
   "outputs": "D_9252 = D_9251, D_92A1 = 0, zp_72 = 0, status line redrawn",
   "confidence": "high"
  },
  {
   "addr": "C6FF",
   "name": "showStatusMessage",
   "summary": "A = message id. Plays the sound associated with that id (sub_C557 looks it up in the per-message sound table at $0612; $80 = silent), stores the id in pendingMessageId zp_7B and prints the message text immediately through sub_C5E4, bypassing the queue. Used by the disk-error retry path ($082D), the link-status screens and the comm module.",
   "inputs": "A = message id; D_0612, D_0500/D_0524",
   "outputs": "zp_7B = id, message printed, sound queued",
   "confidence": "high"
  },
  {
   "addr": "C705",
   "name": "showStatusMessageNoSound",
   "summary": "Tail/alternate entry of showStatusMessage that skips the sound lookup: stores A in pendingMessageId zp_7B and prints that message. Reached by JMP from showWorkingMessage ($C858).",
   "inputs": "A = message id",
   "outputs": "zp_7B = id, message printed",
   "confidence": "high"
  },
  {
   "addr": "C70A",
   "name": "handlePauseKey",
   "summary": "RUN/STOP pause handling, called every frame from updateGameFrame ($40BF/$413D). Returns at once unless gamePhase zp_B1 == 0 and a game is running ($0BA8 == 0). In solo/film mode ($0B9B bit 7) RUN/STOP toggles commPauseFlags zp_BE; while paused persistentMessageId is $19 ('TIMEOUT. RUN/STOP TO RESUME.'), only handleMenuSessionPacket is serviced and two PLAs discard the caller's return address so the rest of the frame update is skipped. In live play the first RUN/STOP shows message 1 and sends command $8C reason 3; once zp_BE is set it saves the sprite-enable mask, clears the screen windows (unless currentScreen == 3), then either performs a voice pause (zp_BE bit 7 -> voicePauseReconnect) or spins calling serviceCommandExchange until zp_BE clears, sending $8C reason 4 (and showWaitingForOpponent) on each further RUN/STOP. On resume it redraws the overview map and viewport on screen 0 and restores the sprites.",
   "inputs": "zp_B1, D_0BA8, D_0B9B, zp_BE, D_9022, D_90FB, D_0B7D",
   "outputs": "zp_BE, D_92A1, D_9022 restored via enableSprites, commands $8C/3 and $8C/4 queued, screen redrawn; may return to the caller's caller",
   "confidence": "high"
  },
  {
   "addr": "C7A3",
   "name": "sendPauseCommand",
   "summary": "A = reason code. Stores it in cmdParam0 zp_73 and queues the 2-byte network command $8C via queueCmd2 ($561F). Reason 3 = request pause, 4 = request resume (reason 2 = ready for new game and 5 = voice pause/disconnect are sent elsewhere).",
   "inputs": "A = reason",
   "outputs": "zp_73, outgoing command ring",
   "confidence": "high"
  },
  {
   "addr": "C7AA",
   "name": "consumeRunStopKey",
   "summary": "Edge test for the RUN/STOP key: if lastKeyCode $9248 holds $93 it arms a 60-frame input lockout ($0B7D), clears the key, plays click sound 2 and returns A = 0 (Z = 1); otherwise it returns with Z = 0. All three call sites are inside handlePauseKey.",
   "inputs": "D_9248",
   "outputs": "A/Z, D_9248 = $FF, D_0B7D = $3C, sound 2",
   "confidence": "high"
  },
  {
   "addr": "C7C3",
   "name": "showWaitingForOpponent",
   "summary": "A = commPauseFlags. When the comm build flag $0BA5 bit 7 is clear and A bit 0 is set, sets persistentMessageId to $0B ('WAITING FOR OPPONENT...') and queues it with sub_C566. Also called from overlay A ($7C00).",
   "inputs": "A, D_0BA5",
   "outputs": "D_92A1 = $0B, message queued",
   "confidence": "high"
  },
  {
   "addr": "C7D5",
   "name": "handleTextEntryKey",
   "summary": "One-key line editor shared by the in-game chat (called from handleChatSend at $1387, inside the raster IRQ, with A = $A3) and the SAVE GAME FILM name field ($8029, A = $10). A = maximum length with bit 7 = 'scroll left when full', Y = cursor position in the 36-byte buffer $9278. It consumes newKeyEvent $90EA: RETURN ($8D) returns with bit 7 set in Y, DEL ($88) backspaces, codes $20-$5F are validated through the ASCII-to-glyph table ($985E,x; entry $3B means 'no glyph', which rejects backslash, caret and underscore) and stored, and the buffer is re-terminated with $9E. Overflow either scrolls the buffer one byte left or beeps (sound 3); an accepted key clicks (sound 2) and clears $90EA and $9248. Returns Z = 1 when nothing was accepted, otherwise A = the accepted key code.",
   "inputs": "A = max length/flags, Y = cursor position, D_90EA, D_985E table",
   "outputs": "A = key code or $FF, Z flag, Y advanced (bit 7 on RETURN), D_9278 buffer, D_90EA = $FF, D_9248 = $FF, sound; self-modifies $C813, $C817, $C84F",
   "confidence": "high"
  },
  {
   "addr": "C853",
   "name": "showWorkingMessage",
   "summary": "Sets persistentMessageId $92A1 to 1 and shows message 1 ('WORKING...') immediately through showStatusMessageNoSound. Called before slow operations (disk access, game start, pause request).",
   "inputs": "none",
   "outputs": "D_92A1 = 1, zp_7B = 1, message printed",
   "confidence": "high"
  },
  {
   "addr": "C85B",
   "name": "promptPressButtonWhenReady",
   "summary": "Prints 'PRESS BUTTON WHEN READY.' ($CBBE) on the status line, then loops stirring the game RNG (nextGameRandom) until checkFirePressed reports a fresh fire-button click, and finally restores/blanks the status line with sub_C39F. Only caller is waitForGameDiskInserted ($10E8), so the wait also randomises the map seed.",
   "inputs": "fire button (zp_5F/zp_79)",
   "outputs": "returns after a click; RNG advanced, status line redrawn",
   "confidence": "high"
  },
  {
   "addr": "C86D",
   "name": "exitIfGameEnded",
   "summary": "If gameEndFlag $92C9 has bit 7 set the routine never returns: it jumps to returnToMainMenu ($0AA3) when gamePhase zp_B1 is non-zero (setup/menu/film) or to endGameToMainMenu ($1ADC) during battle. Otherwise it simply returns. Called at the top of updateGameFrame ($40B2) and from the command-exchange loop ($4E41).",
   "inputs": "D_92C9, zp_B1",
   "outputs": "returns, or transfers control to the menu (stack is rebuilt there)",
   "confidence": "high"
  },
  {
   "addr": "C89A",
   "name": "playSound",
   "summary": "Public sound entry used by ~50 call sites: A = sound id 0-28, calls requestSound and restores X and Y afterwards through two self-modified operands ($C8A4, $C8A6). Because the save slots are static this entry is not re-entrant; the IRQ-side callers ($1370, $140E) deliberately call requestSound directly instead.",
   "inputs": "A = sound id",
   "outputs": "sound request queued; X and Y preserved",
   "confidence": "high"
  },
  {
   "addr": "C8A8",
   "name": "requestSound",
   "summary": "Core of the sound request queue. A = 0 silences everything: slots $92E7-$92E9 become $FF and slot $92EA becomes 0, so the IRQ dequeues sound id 0 and stops all voices. Otherwise the class byte from soundClassTable $C87D[id] decides: a negative class forces the request into slot (class AND 3) - $80/$81/$82 are voices 0/1/2 and $FF is the multi-voice slot 3 - while a positive class (0, 1 or 2) is a plain voice number that is only taken when that slot is idle ($FF) and the sound currently playing on that voice (sndVoiceSoundId $67C8,x) is either silent or has a non-negative (low priority) class. Ids >= $1D are discarded.",
   "inputs": "A = sound id 0-28; D_C87D, D_92E7-D_92EA, D_67C8",
   "outputs": "one of D_92E7-D_92EA set to the id (or all cleared); self-modifies $C8DC; A/X/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "C8E9",
   "name": "dequeueSoundRequest",
   "summary": "Scans the four request slots from 3 down to 0 for a pending id; the first non-negative one is returned in X and its slot is reset to $FF. X = $FF when all four are empty. Called once per frame from the raster IRQ at $1283, which then hands X to startSound ($6728).",
   "inputs": "D_92E7-D_92EA",
   "outputs": "X = sound id or $FF, that slot cleared; A/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "C8FA",
   "name": "updateComcenMoveSound",
   "summary": "Keeps the own command centre's engine sound in step with its movement. Runs only during live battle (gamePhase zp_B1 == 0 and playModeFlags $0B9B == 0). If the own comcen ($9236) is moving (unit idle flag $FB54 bit 7 clear) and SID voice 2 is silent it requests sound $14; if the comcen has become idle while voice 2 is still playing $14 or $16 it requests the stop sound $1C. Called for every unit step from processUnitStep ($42A0).",
   "inputs": "zp_B1, D_0B9B, D_9236, D_FB54, D_67CA (voice 2 current sound)",
   "outputs": "sound $14 or $1C requested",
   "confidence": "medium"
  },
  {
   "addr": "C924",
   "name": "setMessageSlot",
   "summary": "X = message id, A/Y = string address. Writes the run-time message pointer tables: low byte into $0500,x and high byte into $0524,x. Used for the dynamic slots $1E, $20 and $21 by the boot code ($0B1D/$0B26/$0C8E/$0C97), overlay A ($7BAC/$81C2/$821C/$86B5/$8724) and the comm-module tail ($EED6/$EEFF).",
   "inputs": "A = string low byte, Y = string high byte, X = message id",
   "outputs": "D_0500[x], D_0524[x]; A = high byte on exit",
   "confidence": "high"
  },
  {
   "addr": "CD0A",
   "name": "showUnitPictureSprites",
   "summary": "X = unit type 0-4 (GRUNT/RIDER/BOOMER/SPY/COMCEN). Fetches that type's RLE block address from unitPictureLoTable $CD05,x / unitPictureHiTable $CD00,x, unpacks it into $0200-$03FF with unpackRleTo0200 and copies the first 192 bytes to $8B40, which is the shape area of sprites 5, 6 and 7 (pointers $2D-$2F). Then sets those three sprite colours to white, X positions to 236/268/300 (the two MSBs via $9020 |= $C0), Y position to 162 and enables sprites 5-7 with mask $E0. Only caller is showUnitInfoPanel ($22F1), whose LDX operand at $22EF is patched with the unit type.",
   "inputs": "X = unit type 0-4",
   "outputs": "$0200-$03FF overwritten, $8B40-$8BFF = three 24x21 sprite frames, sprite shadows $900D-$900F, $9015-$9017, $901D-$901F, $9020, $9022; zp_B2/B3/1A/1C clobbered",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "72",
   "scope": "zp",
   "name": "messageTimer",
   "meaning": "Frames left before the current status-line message may be replaced; decremented every 4th frame by updateFrameTimers. 0 = show the next message now; handleMessageReviewKeys treats >= $23 as 'still fresh'.",
   "confidence": "high"
  },
  {
   "addr": "73",
   "scope": "zp",
   "name": "cmdParam0",
   "meaning": "First parameter byte of the outgoing network command built by queueCmd2; sendPauseCommand puts the pause reason (3 or 4) here.",
   "confidence": "high"
  },
  {
   "addr": "7A",
   "scope": "zp",
   "name": "currentMessageId",
   "meaning": "Status-line message currently selected; $80 = idle (nothing queued), bit 7 set on other values marks 'already displayed'. Set by showNextQueuedMessage and the message service at $C4EF.",
   "confidence": "high"
  },
  {
   "addr": "7B",
   "scope": "zp",
   "name": "pendingMessageId",
   "meaning": "Message id that the message service should print on the next pass; bit 7 set suppresses the reprint. Written by showStatusMessage, handleMessageReviewKeys and the score-line key path.",
   "confidence": "high"
  },
  {
   "addr": "B1",
   "scope": "zp",
   "name": "gamePhase",
   "meaning": "0 = live battle, non-zero = setup / film / menu ($F0 on the main menu, $FF at start-up). handlePauseKey and updateComcenMoveSound only run when it is 0; exitIfGameEnded picks the exit route from it.",
   "confidence": "high"
  },
  {
   "addr": "BE",
   "scope": "zp",
   "name": "commPauseFlags",
   "meaning": "Pause state: 0 = running, 1 = paused by RUN/STOP, bit 7 = voice pause (hang up, talk, reconnect). Toggled by handlePauseKey and by the $8C command handlers; cleared by voicePauseReconnect and endGameToMainMenu.",
   "confidence": "high"
  },
  {
   "addr": "0500",
   "scope": "abs",
   "name": "messageTextLo",
   "meaning": "Low bytes of the 36 status-message text pointers (ids $00-$23), indexed by message id; written by setMessageSlot for the run-time slots.",
   "confidence": "high"
  },
  {
   "addr": "0524",
   "scope": "abs",
   "name": "messageTextHi",
   "meaning": "High bytes of the 36 status-message text pointers; $0524+$24 = $0548, which is the text of slot 0 itself.",
   "confidence": "high"
  },
  {
   "addr": "0548",
   "scope": "abs",
   "name": "chatLineMarker",
   "meaning": "First byte of message slot 0's text (the chat line). $9E terminates immediately = blank status line; $1E prints nothing but continues into the incoming chat text mirrored at $0549. showNextQueuedMessage tests its sign to decide whether a chat line is pending.",
   "confidence": "high"
  },
  {
   "addr": "0612",
   "scope": "abs",
   "name": "messageSoundIdTable",
   "meaning": "36-byte table: sound id to play when message id X is shown; $80 = silent. Read by sub_C557 on behalf of showStatusMessage and the message service.",
   "confidence": "high"
  },
  {
   "addr": "065E",
   "scope": "abs",
   "name": "strStatusScoreLine",
   "meaning": "Three consecutive bit-7-terminated strings used by drawStatusScoreLine: '   CLICKS:' ($065E), '    US:' ($0668), '   THEM:' ($066F).",
   "confidence": "high"
  },
  {
   "addr": "0B7D",
   "scope": "abs",
   "name": "inputLockoutTimer",
   "meaning": "Frames during which joystick/keyboard input is ignored; set to $3C by consumeRunStopKey and by the score-line key.",
   "confidence": "high"
  },
  {
   "addr": "0B9B",
   "scope": "abs",
   "name": "playModeFlags",
   "meaning": "Bit 7 = solo trainer / film playback. handlePauseKey uses it to choose between the local pause loop and the network pause protocol.",
   "confidence": "high"
  },
  {
   "addr": "0B9F",
   "scope": "abs",
   "name": "opponentPlayerIndex",
   "meaning": "Side index (0/1) of the OPPONENT, not of the local player: drawStatusScoreLine prints D_0B9F EOR 1 under 'US:' and D_0B9F under 'THEM:', and computeGameResult ($2965/$298F) shows 'WE LOST'/'WE WERE KNOCKED OUT' when the winner index equals it.",
   "confidence": "medium"
  },
  {
   "addr": "0BA5",
   "scope": "abs",
   "name": "commBuildFlag",
   "meaning": "Bit 7 selects comm-module build 1 (track 35); when set, showWaitingForOpponent suppresses the 'WAITING FOR OPPONENT...' message.",
   "confidence": "high"
  },
  {
   "addr": "0BA8",
   "scope": "abs",
   "name": "noGameInProgress",
   "meaning": "Non-zero while no game is running (also carries the knock-out result bits); handlePauseKey returns immediately when it is set.",
   "confidence": "medium"
  },
  {
   "addr": "8B40",
   "scope": "abs",
   "name": "unitPictureSpriteShapes",
   "meaning": "192-byte shape area of sprites 5, 6 and 7 (pointers $2D/$2E/$2F in VIC bank 2); filled by showUnitPictureSprites with the three animation frames of a unit type.",
   "confidence": "high"
  },
  {
   "addr": "900D",
   "scope": "abs",
   "name": "spriteColourShadow",
   "meaning": "Shadow of VIC $D027-$D02E; $900D/$900E/$900F are sprites 5/6/7 and are set to 1 (white) for the unit picture.",
   "confidence": "high"
  },
  {
   "addr": "9015",
   "scope": "abs",
   "name": "spriteXShadow",
   "meaning": "Shadow of the sprite X registers ($9010+n); $9015/$9016/$9017 receive $EC/$0C/$2C so sprites 5-7 sit at X = 236, 268 and 300 in the info panel.",
   "confidence": "high"
  },
  {
   "addr": "901D",
   "scope": "abs",
   "name": "spriteYShadow",
   "meaning": "Shadow of the sprite Y registers ($9018+n); $901D-$901F receive $A2 = 162 for the unit picture row.",
   "confidence": "high"
  },
  {
   "addr": "9020",
   "scope": "abs",
   "name": "spriteXMsbShadow",
   "meaning": "Shadow of VIC $D010; showUnitPictureSprites ORs in $C0 to give sprites 6 and 7 X coordinates above 255.",
   "confidence": "high"
  },
  {
   "addr": "9022",
   "scope": "abs",
   "name": "spriteEnableShadow",
   "meaning": "Shadow of VIC $D015. handlePauseKey snapshots it before clearing the screen for a pause and re-ORs it through enableSprites afterwards.",
   "confidence": "high"
  },
  {
   "addr": "90EA",
   "scope": "abs",
   "name": "newKeyEvent",
   "meaning": "Raw key code of the most recent new key press, produced by pollKeyboardEvent in the IRQ; $FF = none. handleTextEntryKey is the consumer for typed text.",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreen",
   "meaning": "Active screen id (0 = battlefield, 1 = map/radar overlay, 2 = unit setup, 3 = menu). handlePauseKey skips the window clear on screen 3 and only redraws the overview/viewport on screen 0.",
   "confidence": "high"
  },
  {
   "addr": "91CB",
   "scope": "abs",
   "name": "gameClockClicks",
   "meaning": "Game clock in 'clicks' (the unit shown as CLICKS: on the status line); also the film time stamp. Printed by drawStatusScoreLine.",
   "confidence": "high"
  },
  {
   "addr": "9236",
   "scope": "abs",
   "name": "ownComcenUnit",
   "meaning": "Unit index of the local side's command centre (49 or 99); used by updateComcenMoveSound to test its idle flag.",
   "confidence": "high"
  },
  {
   "addr": "9248",
   "scope": "abs",
   "name": "lastKeyCode",
   "meaning": "Last typed key code (ASCII|$80 or a special code), $FF = consumed. Codes used in this chunk: $8B CRSR up/down = message history, $8E CRSR left/right = score line, $93 RUN/STOP = pause.",
   "confidence": "high"
  },
  {
   "addr": "9249",
   "scope": "abs",
   "name": "messageQueue",
   "meaning": "8-entry ring of pending status-message ids ($FF = empty), filled priority-sorted by sub_C566 and drained by showNextQueuedMessage; also walked backwards by the message-history scrollback.",
   "confidence": "high"
  },
  {
   "addr": "9251",
   "scope": "abs",
   "name": "messageQueueHead",
   "meaning": "Insert index of the message ring; queue is empty when it equals messageQueueTail.",
   "confidence": "high"
  },
  {
   "addr": "9252",
   "scope": "abs",
   "name": "messageQueueTail",
   "meaning": "Read index of the message ring; temporarily rewound by handleMessageReviewKeys while scrolling back through old messages.",
   "confidence": "high"
  },
  {
   "addr": "9278",
   "scope": "abs",
   "name": "textEntryBuffer",
   "meaning": "36-byte line buffer used by handleTextEntryKey for chat text and film names; always terminated with $9E. Note the scroll loop at $C81D reads one byte past the end ($929C = chatState).",
   "confidence": "high"
  },
  {
   "addr": "92A1",
   "scope": "abs",
   "name": "persistentMessageId",
   "meaning": "Message id the status line falls back to when the queue is empty; 0 = blank, negative = draw the CLICKS/US/THEM score line instead.",
   "confidence": "high"
  },
  {
   "addr": "92A2",
   "scope": "abs",
   "name": "playerPointsLo",
   "meaning": "Low bytes of the two players' point totals ($92A2 = side 0, $92A3 = side 1); printed by printPlayerPoints.",
   "confidence": "high"
  },
  {
   "addr": "92A4",
   "scope": "abs",
   "name": "playerPointsHi",
   "meaning": "High bytes of the two players' point totals ($92A4/$92A5).",
   "confidence": "high"
  },
  {
   "addr": "92C9",
   "scope": "abs",
   "name": "gameEndFlag",
   "meaning": "Bit 7 set when the game must be torn down; exitIfGameEnded turns it into a jump back to the main menu.",
   "confidence": "high"
  },
  {
   "addr": "92E7",
   "scope": "abs",
   "name": "soundRequestSlots",
   "meaning": "Four sound request slots $92E7-$92EA ($FF = empty). Slots 0-2 correspond to SID voices 0-2, slot 3 to whole-chip (three-voice) sounds. Written by requestSound, drained by dequeueSoundRequest from the raster IRQ.",
   "confidence": "high"
  },
  {
   "addr": "67C8",
   "scope": "abs",
   "name": "sndVoiceSoundId",
   "meaning": "Sound id currently playing on SID voice 0/1/2 ($67C8/$67C9/$67CA), maintained by the sound player; requestSound consults it for priority and updateComcenMoveSound checks voice 2 for $14/$16.",
   "confidence": "high"
  },
  {
   "addr": "985E",
   "scope": "abs",
   "name": "asciiGlyphTableBase",
   "meaning": "$987E minus $20, so that D_985E,x indexes the ASCII($20-$5F)-to-glyph table directly by character code. Entry $3B means 'no glyph in the game font' and is used by handleTextEntryKey to reject backslash, caret and underscore.",
   "confidence": "high"
  },
  {
   "addr": "FB54",
   "scope": "abs",
   "name": "unitIdleFlags",
   "meaning": "Per-unit flag byte array; bit 7 = unit is stationary/idle (tested by updateComcenMoveSound), bit 6 = in a group, bits 2-4 = group number.",
   "confidence": "high"
  },
  {
   "addr": "C630",
   "scope": "abs",
   "name": "messageReviewIndex",
   "meaning": "State byte of the message-history scrollback embedded in the code: bit 7 = review mode active, bits 0-6 = the queue read index to restore when it ends.",
   "confidence": "high"
  },
  {
   "addr": "C54B",
   "scope": "abs",
   "name": "suppressMessageSoundFlag",
   "meaning": "Self-modified operand of the LDA #$04 at $C54A in the message service: negative means 'do not play this message's sound', and it is reset to $04 after every message. Patched by showNextQueuedMessage ($FF) and handleMessageReviewKeys ($80).",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "C630",
   "length": 1,
   "type": "byteTable",
   "name": "messageReviewIndex",
   "description": "Single state byte for the message-history scrollback, stored between routines (bit 7 = active, low 7 bits = saved queue read index)."
  },
  {
   "addr": "C87D",
   "length": 29,
   "type": "byteTable",
   "name": "soundClassTable",
   "description": "Class/priority byte per sound id 0-28 used by requestSound. $00/$01/$02 = normal-priority request for SID voice 0/1/2 (only accepted if that voice is free or busy with another normal sound); $80/$81/$82 = force onto voice 0/1/2; $FF = force onto the whole-chip slot 3 (three-voice fanfares such as ids 13, 14 and 25). Contents: FF 00 00 00 00 00 FF 80 81 01 01 01 80 FF FF 82 82 82 82 82 02 FF 82 82 82 FF 80 81 82."
  },
  {
   "addr": "C92C",
   "length": 56,
   "type": "text",
   "name": "groupNameTable",
   "description": "Seven 8-byte group names printed with sub_C100 (index = unitIdleFlags bits 2-4): ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER. Used by printUnitStatusLines ($23B1), runJoinGroupMenu ($6162) and runGroupOptionsMenu ($634E). An index of 7 would fall into unitTypeNameTable and print GRUNT."
  },
  {
   "addr": "C964",
   "length": 40,
   "type": "text",
   "name": "unitTypeNameTable",
   "description": "Five 8-byte unit type names indexed by unitTypeTable bits 0-2: GRUNT, RIDER, BOOMER, SPY, COMCEN. Printed by showUnitInfoPanel ($22C9)."
  },
  {
   "addr": "C98C",
   "length": 70,
   "type": "text",
   "name": "unitStatusNameTable",
   "description": "Nine 8-byte status words: 0 READY, 1 RELOAD, 2 FIRED, 3 ' -   ', 4 REPAIR, 5 MOVE, 6 PINNED, 7 STUN'D, 8 DIGGIN. The WEAPON: line indexes it from $C98C, the ACTION: line from entry 3 ($C9A4) so entries 3-8 become ' -   '/REPAIR/MOVE/PINNED/STUN'D/DIGGIN. The last entry is only 6 bytes long because the next block starts right after it."
  },
  {
   "addr": "C9D2",
   "length": 186,
   "type": "text",
   "name": "unitOptionMenuText",
   "description": "Twelve $8D-terminated menu lines for the UNIT/GROUP OPTIONS menus, printed in sequence with sub_C133/sub_C137 from $5FCA and $6387: FORMATION, MOVE TO GOAL, CLOAKING ON, NO CLOAKING, CLEAR TARGET, SET TARGET, MEMBERSHIP, JOIN GROUP, DIRECT UNIT, SELF DESTRCT, SET BLITZ, BLITZ OFF."
  },
  {
   "addr": "CA8C",
   "length": 45,
   "type": "text",
   "name": "formationMenuText",
   "description": "Formation sub-menu, one string with embedded $0D line breaks: 'SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT)'."
  },
  {
   "addr": "CAB9",
   "length": 48,
   "type": "text",
   "name": "consoleMiscMenuText",
   "description": "Six $8D-terminated lines used by the command-console panels: ' ENERGY', ' BATTLE', ' RADAR', '  DRONE', '  DIG OUT', 'DIG IN'."
  },
  {
   "addr": "CAE9",
   "length": 33,
   "type": "text",
   "name": "unitInfoLabelText",
   "description": "Label column of the unit info panel, one string with $0D breaks and a $BA terminator: 'GROUP:' + three blank lines + 'ENERGY:' 'WEAPON:' 'ACTION:'. Printed by showUnitInfoPanel ($22E0)."
  },
  {
   "addr": "CB0A",
   "length": 21,
   "type": "text",
   "name": "consoleHeadingText",
   "description": "Two headings: '    UNIT' ($CB0A) and '   OPTIONS' + $0D ($CB13)."
  },
  {
   "addr": "CB1F",
   "length": 218,
   "type": "text",
   "name": "uiPromptStrings",
   "description": "Pool of short bit-7-terminated prompts and status words used by the unit/group menus, the command console tabs and the info panel: GROUP ($CB1F), WHICH GROUP: ($CB25), '  TOO FAR!' ($CB31), ADDED ($CB3B), REMOVED ($CB40), FINISHED ($CB47), SETTING ($CB4F), DESTINATION ($CB56), TARGET ($CB61), FUEL: ($CB67), MISC ($CB6C), STATS ($CB70), REPAIR ($CB75), CLOAKED ($CB7B), BLITZ ($CB82), DUG IN ($CB87), NOT IN GROUP ($CB8D), RE-POSITIONED ($CB99), NEW POSITION ($CBA6), ' EXIT ' ($CBB2), ENERGY ($CBB8), 'PRESS BUTTON WHEN READY.' ($CBBE, used by promptPressButtonWhenReady), 'REPAIR / WHICH:' ($CBD6), 'ATTEMPTING REPAIR...' ($CBE5). MISC/STATS/REPAIR are the three command-console tab labels drawn around $1655-$1675."
  },
  {
   "addr": "CBF9",
   "length": 24,
   "type": "text",
   "name": "gameHeaderText",
   "description": "Status header fragments: ' PLAY' ($CBF9), '1ST' ($CBFE), '2ND' ($CC01) and '\\r  SCENARIO\\r' ($CC04)."
  },
  {
   "addr": "CC11",
   "length": 14,
   "type": "text",
   "name": "rulesNameTable",
   "description": "Two 8-byte-stride entries selected with sub_C100 at $15C9: STANDARD ($CC11) and CUSTOM ($CC19, only 6 bytes because the next block follows)."
  },
  {
   "addr": "CC1F",
   "length": 122,
   "type": "text",
   "name": "mainMenuItemText",
   "description": "The six main-menu lines printed in sequence: 'COMPETE WITH MODEM OPPONENT', 'PRACTICE WITH SOLO TRAINER', 'WATCH GAME FILM', 'SAVE GAME FILM' ($CC1F block), 'LOAD GAME FILM' ($CC75) and 'DISCONNECT/SET VOICE' ($CC84)."
  },
  {
   "addr": "CC99",
   "length": 36,
   "type": "text",
   "name": "inGameMenuText",
   "description": "ENEMY ($CC99), MISSILES ($CC9E), 'ABORT GAME' ($CCA6) and 'VOICE PAUSE' ($CCB1) - the items of the in-game MISC options menu plus two panel captions."
  },
  {
   "addr": "CCBD",
   "length": 11,
   "type": "text",
   "name": "strGameTypeLabel",
   "description": "'GAME TYPE:' header printed before the scenario name at $15C5."
  },
  {
   "addr": "CCC8",
   "length": 56,
   "type": "text",
   "name": "scenarioNameTable",
   "description": "Seven 8-byte scenario names indexed by gameTypeOptions $0BA3 bits 0-2 (printed with sub_C100 at $15D9): SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER."
  },
  {
   "addr": "CD00",
   "length": 5,
   "type": "byteTable",
   "name": "unitPictureHiTable",
   "description": "High bytes of the five unit-picture RLE block addresses ($CD, $CD, $CE, $CE, $CF). Also probed as a build hook: $0C0D tests whether $CD00 holds $4C (JMP) and, if so, disables VIC IRQs and jumps there - in the shipped overlay it holds table data, so the hook is inert; showUnitInfoPanel ($22EA) only tests that $CD00 is negative (pictures present)."
  },
  {
   "addr": "CD05",
   "length": 5,
   "type": "byteTable",
   "name": "unitPictureLoTable",
   "description": "Low bytes of the five unit-picture RLE block addresses ($52, $D3, $3B, $CD, $3A) giving $CD52, $CDD3, $CE3B, $CECD and $CF3A."
  },
  {
   "addr": "CD52",
   "length": 129,
   "type": "sprite",
   "name": "unitPictureGrunt",
   "description": "RLE block (escape marker $05) for unit type 0 = GRUNT; the first 192 unpacked bytes are three 24x21 sprite frames (a helmeted figure, a running figure and a burst)."
  },
  {
   "addr": "CDD3",
   "length": 104,
   "type": "sprite",
   "name": "unitPictureRider",
   "description": "RLE block (escape marker $07) for unit type 1 = RIDER; three 24x21 sprite frames."
  },
  {
   "addr": "CE3B",
   "length": 146,
   "type": "sprite",
   "name": "unitPictureBoomer",
   "description": "RLE block (escape marker $08) for unit type 2 = BOOMER; three 24x21 sprite frames (launcher, missile in flight, emplacement)."
  },
  {
   "addr": "CECD",
   "length": 109,
   "type": "sprite",
   "name": "unitPictureSpy",
   "description": "RLE block (escape marker $05) for unit type 3 = SPY; three 24x21 sprite frames (antenna/aerial shapes)."
  },
  {
   "addr": "CF3A",
   "length": 163,
   "type": "sprite",
   "name": "unitPictureComcen",
   "description": "RLE block (escape marker $0A) for unit type 4 = COMCEN; three 24x21 sprite frames of the command centre building. Its 192nd unpacked byte (the unused pad of sprite 7) is fetched from $CFDD, which is already inside the leftover code fragment at the end of the page."
  }
 ],
 "misclassified": [
  {
   "addr": "CFDD",
   "length": 35,
   "actual": "code",
   "evidence": "$CFDD-$CFFF is byte-for-byte identical to $12DD-$12FF in game/main_0800 (AD 63 91 F0 03 CE 63 91 AD FC 90 F0 03 CE FC 90 AD C8 91 F0 03 CE C8 91 AD 34 92 F0 07 C9 80 F0 03 CE 34), i.e. the timer-decrement tail of updateFrameTimers ($129C). It is dead: nothing references it, the final DEC $9234 is truncated at the page boundary (only CE 34, the $92 is missing) and the BEQ at $CFF8 would branch to $D001. It is a leftover of the build, not reachable code, so leaving it as .byte is harmless - but it should be commented as such."
  }
 ],
 "insights": [
  "Status-line message system (all of it lives in this overlay): 36 message slots with pointer tables at $0500 (lo) / $0524 (hi) and a per-message sound id at $0612 ($80 = silent); slot 0's text is $0548, the chat line ($9E = blank, $1E = show the incoming chat text mirrored at $0549). sub_C566 inserts an id priority-sorted into the 8-entry ring $9249-$9250 (head $9251, tail $9252); showNextQueuedMessage ($C5EE) pops it into zp_7A; sub_C4EF prints it and reloads messageTimer zp_72 with $3C; persistentMessageId $92A1 is the fallback when the ring is empty (0 = blank line, negative = draw the score line instead); resetMessageQueue ($C6EF) flushes everything.",
  "Key codes decoded from keyMatrixCodeTable ($0D67, row-major PA0..PA7 x PB0..PB7): $88 INST/DEL, $8B CRSR up/down, $8C HOME, $8D RETURN, $8E CRSR left/right, $93 RUN/STOP, $A0 SPACE, $9B left-arrow, $83 pound, $FE modifier, $FF CTRL. In this chunk CRSR up/down scrolls back through the message history, CRSR left/right pops up the CLICKS/US/THEM score line during battle, and RUN/STOP is the pause key.",
  "Pause protocol: RUN/STOP sets commPauseFlags zp_BE. Live play sends command $8C with reason 3 (pause) and reason 4 (resume) through sendPauseCommand ($C7A3 -> queueCmd2); reason 5 is the voice pause sent by runInGameOptionsMenu, and reason 2 is 'ready for new game'. While paused, handlePauseKey ($C70A) owns the frame: it clears the screen windows, spins on serviceCommandExchange, shows message $0B 'WAITING FOR OPPONENT...' on each further RUN/STOP, and on release redraws the overview map and viewport and restores the sprite-enable mask. In solo/film mode it instead pops the caller's return address so the rest of updateGameFrame is skipped, with message $19 'TIMEOUT. RUN/STOP TO RESUME.' on screen. (The knowledge base credits $C768 to sub_C86D; it is actually inside sub_C70A.)",
  "Sound architecture detail: soundClassTable $C87D (29 entries) gives each sound id a class byte. Low two bits = target slot; $00/$01/$02 are 'normal priority on voice 0/1/2' and are only accepted when that request slot is idle and the voice is silent or playing another normal-priority sound, while $80/$81/$82 force voices 0-2 and $FF forces slot 3, the whole-chip slot used by the three-voice fanfares (13, 14, 25). requestSound(0) is the 'silence' request: it sets slots 0-2 to $FF and slot 3 to 0 so the IRQ dequeues id 0 and stops the chip. playSound ($C89A) preserves X/Y via self-modified operands and is therefore NOT re-entrant; the IRQ-side callers ($1370 in handleChatSend and $140E) call requestSound ($C8A8) directly to avoid corrupting it - but handleTextEntryKey, which also runs in the IRQ, does call $C89A, so a click or beep raised from the IRQ can clobber the X/Y save slots of a main-thread $C89A call.",
  "Unit pictures: five RLE blocks at $CD52/$CDD3/$CE3B/$CECD/$CF3A (pointer halves at $CD05 lo / $CD00 hi) hold three 24x21 frames each for GRUNT, RIDER, BOOMER, SPY and COMCEN, in unit-type order. showUnitPictureSprites ($CD0A) unpacks one into $0200, copies 192 bytes to $8B40 (sprite pointers $2D-$2F = sprites 5/6/7), colours them white and places them at X = 236/268/300, Y = 162, i.e. across the info panel. Each block's stream ends exactly where the next begins; only the first 192 of the 512 unpacked bytes are meaningful.",
  "The string pool uses deliberate table overlap to save bytes: the 9-entry unitStatusNameTable at $C98C is indexed from $C98C for the WEAPON: line and from entry 3 ($C9A4) for the ACTION: line; the last entry (DIGGIN) and the CUSTOM entry of rulesNameTable are truncated to 6 bytes because the following block starts immediately; and group index 7 of groupNameTable ($C92C) would read GRUNT out of the adjacent unitTypeNameTable.",
  "$CD00 is probed by $0C0D as an optional build hook: if it holds $4C (a JMP opcode) the game turns off VIC IRQs and jumps to $CD00. In the shipped overlay it holds the unit-picture high-byte table ($CD,$CD,$CE,$CE,$CF), so the hook never fires; showUnitInfoPanel only checks that $CD00 is negative to decide that unit pictures are available.",
  "$0B9F is the OPPONENT's side index, not the local player's: drawStatusScoreLine prints D_0B9F EOR 1 under 'US:' and D_0B9F under 'THEM:', and computeGameResult chooses 'WE LOST ON POINTS!' ($11) / 'WE WERE KNOCKED OUT!' ($14) exactly when the winner index equals $0B9F. The knowledge base entry for $28F1 states the opposite.",
  "handleTextEntryKey ($C7D5) is the shared line editor for the in-game chat (called from the IRQ at $1387 with A=$A3: 35 chars, scroll when full) and the SAVE GAME FILM name field ($8029, A=$10: 16 chars, no scroll). Character validation goes through the ASCII-to-glyph table indexed directly by character code as $985E,x (= $987E - $20); the three codes whose glyph entry is $3B (backslash, caret, underscore) are rejected with a beep. The scroll loop at $C81D copies 36 bytes from $9279 to $9278 and therefore reads one byte past the buffer, pulling chatState $929C into $929B.",
  "The overlay's last 35 bytes ($CFDD-$CFFF) are an unreferenced, truncated copy of the timer-decrement tail of updateFrameTimers ($12DD) - build leftover, never executed.",
  "Mis-traced labels: XREF.txt attributes two boot-loader call sites to this overlay ('sub_CBED call:C3D0', 'sub_CBF2 call:C3D9'). Those JSRs live in boot/loader_C000; in the runtime overlay $C3D0 is the operand byte of LDY #$00 and $C3D9 the operand of LDX #$24. The result is two spurious routine labels, sub_CBED and sub_CBF2, planted in the middle of the text string 'ATTEMPTING REPAIR...' at $CBE5 - they are data, not code."
 ]
}