; ============================================================================
; $C000-$CFFF - text/glyph engine, status message system and unit pictures (track 30 sectors 0-15)
; ============================================================================
; Loaded by $0F07 at start-up over the boot loader. Draws proportional text into the bitmap, manages the
; message queue and the status panel, and holds most of the game's vocabulary (unit names, group names,
; game types, menu text).

        .setcpu "6502"
        .include "c64.inc"
        .include "zeropage.inc"

; ---- references to code/data outside this file ----
contourRuleTable         := $0200
messagePtrLoTable        := $0500
messagePtrHiTable        := $0524
chatMessageLine          := $0548
messageSoundTable        := $0612
returnToMainMenu         := $0AA3
inputLockoutTimer        := $0B7D
filmPlaybackMode         := $0B9B
playerSide               := $0B9F
isSoloTrainer            := $0BA5
gameEndReason            := $0BA8
checkFirePressed         := $0EB1
endGameToMainMenu        := $1ADC
voicePauseReconnect      := $1BF7
drawViewport             := $26C1
calcCellPointersXY       := $2AA3
clearCurrentScreenWindows := $2DDE
enableSprites            := $31A3
serviceCommandExchange   := $4DA9
queueCmd2                := $561F
handleMenuSessionPacket  := $56BB
drawOverviewMap          := $5AD7
sndVoiceSoundId          := $67C8
sndVoice2SoundId         := $67CA
unpackRleTo0200          := $67CB
sprite5Shape             := $8B40
vicCtrl2Shadow           := $9001
sprite5ColourShadow      := $900D
sprite6ColourShadow      := $900E
sprite7ColourShadow      := $900F
sprite5XShadow           := $9015
sprite6XShadow           := $9016
sprite7XShadow           := $9017
sprite5YShadow           := $901D
sprite6YShadow           := $901E
sprite7YShadow           := $901F
spriteXMsbShadow         := $9020
spriteEnableShadow       := $9022
glyphBuildBuffer         := $90B8
newKeyEvent              := $90EA
textCentreWidth          := $90EF
currentScreen            := $90FB
stepDelayTimer           := $90FC
linkPollDelay            := $9163
shotLineTimer            := $91C8
gameClock                := $91CB
lastShownClock           := $9208
viewRedrawTimer          := $9234
comcenUnit               := $9236
lastTypedKey             := $9248
messageQueue             := $9249
messageQueueHead         := $9251
messageQueueTail         := $9252
incomingChatLength       := $9277
typedLineBuffer          := $9278
typedLineScrollSrc       := $9279
chatState                := $929C
rawKeyCode               := $92A0
persistentMessageId      := $92A1
sideScoreLo              := $92A2
sideScoreHi              := $92A4
savedTextWindow          := $92B6
setupPhaseMode           := $92C6
gameEndCountdown         := $92C9
decimalDigitBuffer       := $92DA
soundRequestSlots        := $92E7
soundRequestVoice1       := $92E8
soundRequestVoice2       := $92E9
soundRequestMultiVoice   := $92EA
lastQuarterFlag          := $92FA
titleColorRow02Cell14    := $985E
titleColorRow03Cell06    := $987E
unitDisplayFlagsTable    := $FB54
nextGameRandom           := $FD5D
irqVectorHi              := $FFFF

; Contents
; --------
;    $C01A  printChar                    the game's single character output routine.
;    $C030  restoreRegsAXY               reloads A, X and Y from the zp_54/zp_55/zp_56 save slots filled by saveRegsAXY.
;    $C03D  handleControlChar            the only two control codes printChar understands: $0D carriage return (falls
;    $C041  printNewline                 carriage return: cursor column back to 0 and the row down by one (by two when
;    $C06A  clearTextWindow              homes the cursor to the top left of the current window and falls into
;    $C075  clearToWindowBottom          fills with the blank glyph zp_3E from the cursor cell down to the window
;    $C07A  clearToEndOfLine             the same fill loop with the stop row set to cursor row + 1, i.e.
;    $C097  setCursorRow                 cursor row = A, then recompute the cell pointers (falls into
;    $C099  updateCursorPointers         recomputes the bitmap and colour pointers of the cursor cell: absolute
;    $C0C0  plotCharAndAdvance           draws glyph A at the cursor cell, then advances the cursor one column (two
;    $C0D7  setFullScreenWindow          resets the text window to the whole screen: top row 0, left column 0,
;    $C0E8  saveRegsAXY                  copies A, X and Y into the zp_54/zp_55/zp_56 scratch triple so restoreRegsAXY
;                                        can
;    $C0EF  printFillChars               prints the current blank glyph zp_3E A times through plotCharAndAdvance; a
;                                        count
;    $C100  printTableEntry              prints entry X of a table of fixed 8-byte, bit-7-terminated strings whose base
;    $C121  printStringCentred           sets the centring field width to the current window width and tail-calls
;    $C12B  printNextStringCentred       like printStringCentred, but continues with the string that follows the
;    $C133  printString                  sets the string pointer zp_A2/zp_A3 from A/Y and falls into
;                                        printStringAtPointer.
;    $C137  printStringAtPointer         prints the bit-7-terminated string at zp_A2/zp_A3.
;    $C17D  printDecimal                 prints an unsigned 24-bit value in decimal without leading zeros: converts it
;                                        into
;    $C183  convertToDecimalDigits       converts the 24-bit value in A (low) / Y (middle) / zp_7E (high) to
;    $C1E2  printDecimalDigits           prints the digit buffer at $92DA from index zp_1E up to and including index
;    $C1F0  plotGlyphCell                draws glyph A into the bitmap cell at zp_B4/zp_B5 and writes the colour byte
;                                        zp_3D
;    $C24B  plotGlyphDoubleSize          draws the glyph at zp_B2/zp_B3 magnified 2x2 into four character cells
;    $C307  plotGlyphLeftHalf            quadrant helper of plotGlyphDoubleSize for a left-hand cell: returns at once
;    $C315  plotGlyphRightHalf           the same for a right-hand cell: returns at once when the mask suppresses
;    $C320  expandGlyphNibbles           inner loop of the 2x magnifier: takes 4 glyph rows from the work buffer $90B8
;    $C34B  colourDoubleSizeCell         writes the colour byte zp_3D into the colour cells of the 2x2 block just
;    $C367  colourCellPair               writes zp_3D into the two horizontally adjacent colour cells at (zp_B6),0 and
;    $C37F  waitFrames                   busy-waits A frames by polling the raster register: per frame it waits until
;    $C397  waitRasterBelowScreen        spins until the raster is at line $E0 (224) or below, i.e.
;    $C39F  clearMessageLine             blanks the status bar at the bottom of the screen: saves the caller's text
;    $C3AB  setMessageLineWindow         defines the text window as the one-row status bar: left column 2, width
;    $C3D8  showMessageString            prints the bit-7-terminated string A/Y centred in the 36-column status bar and
;    $C3E0  printOnMessageLine           worker behind showMessageString: remembers the string pointer in the operands
;    $C3F9  advanceFillPointer           loop helper of the block fill/clear routines: compares the 16-bit pointer
;    $C40E  restoreTextWindow            copies the 12 saved bytes at $92B6 back into zp_39-zp_44 (window geometry,
;    $C41B  saveTextWindow               saves zp_39-zp_44 (the whole text engine state) into the single slot at $92B6
;                                        and
;    $C428  splitDecimalDigits           splits the byte in A into three decimal digits by repeated subtraction:
;    $C451  checkUiContinue              the shared 'may this modal panel keep running' test used by the menu selector,
;    $C470  checkSetupUiContinue         tail of checkUiContinue, also called on its own by the map main loop during
;    $C493  serviceChatDisplay           first half of updateStatusMessage: it owns the status bar while the player is
;    $C4DA  drawTypedChatLine            draws the line being typed to the opponent: the bit-7-terminated buffer at
;    $C4EF  updateStatusMessage          per-frame service of the status bar, called from every polling loop.
;    $C557  playMessageSound             plays the sound effect assigned to message id Y: the sound number comes from
;    $C560  queueMessageUnlessFilm       queues message id A like queueMessage, but drops it silently while a game
;    $C566  queueMessage                 inserts message id A into the 8-entry ring at $9249, keeping it sorted so that
;    $C5C9  decMessageIndex              steps the message-queue index in Y back one slot, wrapping from 0 to 7.
;    $C5CF  incMessageIndex              steps the message-queue index in Y on one slot, wrapping from 7 to 0.
;    $C5D7  redrawStatusMessage          puts the status bar back after a screen has been rebuilt: the message that
;    $C5E4  showMessageById              looks message id A up in the pointer tables at $0500 (low) / $0524 (high) and
;    $C5EE  showNextQueuedMessage        chooses what the status line should show next and leaves the answer in
;                                        pendingMessageId zp_7A; the actual printing is done by updateStatusMessage on its next pass.
;    $C62A  clearChatStatusLine          empties message slot 0 (the chat line, text at $0548) by storing $9E in its
;                                        first byte.
;    $C631  handleMessageReviewKeys      per-frame keyboard hook of the status-message service.
;    $C6B6  drawStatusScoreLine          redraws the bottom status line as '   CLICKS:<clock>    US:<points>
;                                        THEM:<points>'.
;    $C6E5  printPlayerPoints            prints one side's 16-bit point total as decimal.
;    $C6EF  resetMessageQueue            throws away every queued status message: the read index is set equal to the
;                                        write index (empty ring), the fallback message and the display timer are cleared, and it falls into
;                                        showNextQueuedMessage so the status line is refreshed immediately.
;    $C6FF  showStatusMessage            prints message id A on the status line immediately, bypassing the queue, and
;                                        plays the sound the per-message table $0612 assigns to it ($80 = silent).
;    $C705  showStatusMessageNoSound     the tail of showStatusMessage, also used as an entry point of its own when the
;                                        message should be silent: records A as the message now on the status line and prints it.
;    $C70A  handlePauseKey               RUN/STOP pause handling, called once per frame from updateGameFrame.
;    $C7A3  sendPauseCommand             queues the 2-byte network command $8C (pause/session control) with A as its
;                                        reason byte.
;    $C7AA  consumeRunStopKey            edge test for RUN/STOP.
;    $C7C3  showWaitingForOpponent       if this is a real modem game and the pause is our own (bit 0 of the flags
;                                        passed in A), makes message $0B 'WAITING FOR OPPONENT...' the persistent status message and queues it.
;    $C7D5  handleTextEntryKey           the one-key line editor shared by the in-game chat line and the SAVE GAME FILM
;                                        name field.
;    $C853  showWorkingMessage           puts message 1 'WORKING...' on the status line at once and makes it the
;                                        persistent message, so it stays there until something clears it.
;    $C85B  promptPressButtonWhenReady   shows 'PRESS BUTTON WHEN READY.' centred on the status line and busy-waits for
;                                        a fresh fire-button click, stirring the 24-bit game RNG on every pass.
;    $C86D  exitIfGameEnded              if the end-of-game display has been up long enough this routine never returns:
;                                        it transfers control to the main menu instead.
;    $C89A  playSound                    the public sound entry used by about fifty call sites: queues sound id A and
;                                        gives X and Y back to the caller.
;    $C8A8  requestSound                 the core of the sound request queue.
;    $C8E9  dequeueSoundRequest          takes one pending sound request out of the four slots, scanning from slot 3
;                                        (the whole-chip slot, highest priority) down to slot 0.
;    $C8FA  updateComcenMoveSound        keeps the engine sound of the local player's command centre in step with its
;                                        movement.
;    $C924  setMessageSlot               installs a string address into the run-time message pointer tables, so a
;                                        message id can be re-pointed at different text.
;    $CD0A  showUnitPictureSprites       draws the picture of one unit type in the info panel.

        .org $C000


; Six small look-up tables that the boot loader kept here; only bitMaskTable_C008 is still used by the
; game.  The stale labels bootEntry/bootLoaderStatus/bootLoadSectors belong to boot/fastLoaderC000,
; which occupies the same addresses in the other build of this page.
bitPairHighTable:
        .byte   $00,$40,$80             ; C000  .@.  bitPairHighTable: a 2-bit value shifted up into bits 7-6 ($00,$40,$80,$C0); unreferenced in this overlay
D_C003:
        .byte   $C0                     ; C003  fourth entry of bitPairHighTable (the label here is the boot loader's, not the text engine's)

; solidColourByteTable: byteTable, 4 bytes. $00,$55,$AA,$FF - the byte that fills a whole multicolour
; bitmap row with colour 0-3. No reference in the disassembly.
solidColourByteTable:
        .byte   $00,$55,$AA,$FF         ; C004  .U..  solidColourByteTable: byte that paints a whole multicolour bitmap row in colour 0,1,2 or 3; unreferenced here

; bitMaskTable_C008: byteTable, 8 bytes. $80,$40,$20,$10,$08,$04,$02,$01 - single bit masks, read as
; L_C008,x by the sprite/bit plotting code at $30B9 (ORA, set bit) and $3217 (LDA, test/EOR).
bitMaskTable_C008:
        .byte   $80,$40,$20,$10,$08,$04,$02,$01; C008  .@ .....  bitMaskTable_C008: single-bit masks $80..$01 indexed by bit number; read as L_C008,x by the sprite bit plotter at $30B9 (ORA = set) and $3217 (LDA = test/EOR)

; pixelPairMaskTable: byteTable, 4 bytes. $C0,$30,$0C,$03 - the four multicolour pixel-pair masks of a
; bitmap byte. No reference in the disassembly.
pixelPairMaskTable:
        .byte   $C0,$30,$0C,$03         ; C010  .0..  pixelPairMaskTable: the four multicolour pixel-pair masks of a bitmap byte; unreferenced here

; pixelPairClearMaskTable: byteTable, 4 bytes. $3F,$CF,$F3,$FC - complements of pixelPairMaskTable. No
; reference in the disassembly.
pixelPairClearMaskTable:
        .byte   $3F,$CF,$F3,$FC         ; C014  ?...  pixelPairClearMaskTable: complements of pixelPairMaskTable (clear one pixel pair); unreferenced here

; unusedC018: unknown, 2 bytes. $A9,$8D - two bytes between the mask tables and printChar; no
; reference, meaning unknown.
unusedC018:
        .byte   $A9,$8D                 ; C018  ..  two orphan bytes ($A9,$8D = 'lda #$8D' in the boot loader build); nothing in the overlay reads them

; ----------------------------------------------------------------------
; printChar - the game's single character output routine.  Unless raw-glyph mode is on it strips the
; bit-7 string terminator, sends codes below $20 to handleControlChar and translates $20-$5F through
; the 64-entry ASCII-to-glyph table at $987E, then draws the glyph and advances the cursor.  A, X and
; Y are preserved, so callers can print inside their own loops.
; In:  A = character ($20-$5F ASCII, $08/$0D control, or a raw glyph index when rawGlyphMode zp_43
;      bit 7 is set); text window zp_39-zp_44; cursor cell pointers zp_B4-zp_B7
; Out: one glyph drawn into the bitmap plus its colour byte, cursor advanced (zp_3F/zp_40 and
;      zp_B2-zp_B7 updated); A/X/Y restored from zp_54-zp_56
; Called from: printStringAtPointer ($C16E/$C174), printDecimalDigits ($C1E7), drawTypedChatLine
;      ($C4E9) and ~15 sites in the main program and the $6F00 overlay
; ----------------------------------------------------------------------
printChar:
        jsr     saveRegsAXY             ; C01A  stash A/X/Y in zp_54-zp_56 so printing never disturbs the caller's registers
        bit     rawGlyphMode            ; C01D  raw glyph mode? (zp_43 bit 7 set = A is already a glyph number)
        bmi     L_C02D                  ; C01F  yes - plot A unchanged, no ASCII translation and no control codes
        and     #$7F                    ; C021  drop bit 7, the marker that ends a string
        cmp     #$20                    ; C023  below the first printable character?
        bcc     L_C037                  ; C025  $00-$1F is a control code (CR or backspace)
        sbc     #$20                    ; C027  make it 0-based; carry is set on this path so exactly $20 is subtracted
        tay                             ; C029  use the character as the table index
        lda     titleColorRow03Cell06,y ; C02A  asciiToGlyphTable[c-$20] = glyph number 0-$3B (the table only covers $20-$5F)  (also graphicsData9300:asciiToGlyphTable)
L_C02D:
        jsr     plotCharAndAdvance      ; C02D  draw the glyph and step the cursor one cell to the right

; ----------------------------------------------------------------------
; restoreRegsAXY - reloads A, X and Y from the zp_54/zp_55/zp_56 save slots filled by saveRegsAXY.
; Tail of printChar; the viewport renderer borrows the same triple to recover a cell coordinate.
; In:  zp_54 (A), zp_55 (X), zp_56 (Y)
; Out: A, X and Y restored; N/Z reflect A
; Called from: printChar ($C03A), map viewport code $5CA1/$5D11/$5D17/$5DE7
; ----------------------------------------------------------------------
restoreRegsAXY:
        ldy     savedRegY               ; C030  the caller's Y
        ldx     savedRegX               ; C032  the caller's X
sub_C034:
        lda     savedRegA               ; C034  A last, so N/Z describe the character just printed
        rts                             ; C036  back to the caller

L_C037:
        jsr     handleControlChar       ; C037  $00-$1F: handle carriage return / backspace
        jmp     restoreRegsAXY          ; C03A  then restore A/X/Y and return


; ----------------------------------------------------------------------
; handleControlChar - the only two control codes printChar understands: $0D carriage return (falls
; into printNewline) and $08 backspace (step back one column, draw the blank glyph 0 over that cell,
; step back onto it again).  Every other code below $20 is ignored, which is how the $1E marker at the
; head of the chat line prints as nothing.
; In:  A = control code
; Out: cursor moved, possibly one cell erased, cell pointers recomputed
; Called from: printChar ($C037)
; ----------------------------------------------------------------------
handleControlChar:
        cmp     #$0D                    ; C03D  carriage return?
        bne     L_C056                  ; C03F  no - test for backspace

; ----------------------------------------------------------------------
; printNewline - carriage return: cursor column back to 0 and the row down by one (by two when
; double-size text is selected), then recompute the cell pointers.  The window is never scrolled: the
; compare against the window bottom at $C04D-$C051 branches to the next instruction either way.
; In:  window/cursor state zp_39-zp_44
; Out: zp_3F = 0, zp_40 advanced, zp_B4-zp_B7 recomputed
; Called from: handleControlChar (fall through), plotCharAndAdvance ($C0D1) and ~20 sites in the
;      main program and the $6F00 overlay
; ----------------------------------------------------------------------
printNewline:
        lda     #$00                    ; C041  column 0 =
        sta     textCursorCol           ; C043  back to the left edge of the window
        inc     textCursorRow           ; C045  down one text row
        bit     doubleSizeText          ; C047  double-size characters? (zp_44 bit 7)
        bpl     L_C04D                  ; C049  no - one row is enough
        inc     textCursorRow           ; C04B  a double-size glyph is two rows tall, so skip a second row
L_C04D:
        lda     textCursorRow           ; C04D  vestigial: the new cursor row ...
        cmp     textWindowBottom        ; C04F  ... compared against the window bottom row ...
        bcc     L_C053                  ; C051  ... but both outcomes continue at $C053, so a window never scrolls
L_C053:
        jmp     updateCursorPointers    ; C053  recompute the bitmap/colour pointers of the new cursor cell

L_C056:
        cmp     #$08                    ; C056  backspace?
        bne     L_C069                  ; C058  no - ignore this control code
        dec     textCursorCol           ; C05A  step the cursor one column back
L_C05C:
        jsr     updateCursorPointers    ; C05C  point the cell pointers at it
        lda     #$00                    ; C05F  glyph 0 is the blank glyph
        jsr     plotCharAndAdvance      ; C061  erase that cell (printing leaves the cursor one column further right again)
        dec     textCursorCol           ; C064  so step back onto the erased cell
        jmp     updateCursorPointers    ; C066  and recompute its cell pointers

L_C069:
        rts                             ; C069  unknown control code: do nothing


; ----------------------------------------------------------------------
; clearTextWindow - homes the cursor to the top left of the current window and falls into
; clearToWindowBottom, so the whole window is filled with the blank glyph zp_3E in the colour zp_3D.
; The standard 'clear this panel' call of the whole game.
; In:  window zp_39-zp_3E
; Out: window filled, cursor left at its top left cell
; Called from: main program $1572/$1596/$1768/$261F/$2648/$26B5/$5E11/$6552/$659D
; ----------------------------------------------------------------------
clearTextWindow:
        lda     #$00                    ; C06A  column 0 =
        sta     textCursorCol           ; C06C  left edge of the window
        lda     textWindowTop           ; C06E  first row of the window =
        sta     textCursorRow           ; C070  cursor row
        jsr     updateCursorPointers    ; C072  compute the cell pointers of that top-left cell

; ----------------------------------------------------------------------
; clearToWindowBottom - fills with the blank glyph zp_3E from the cursor cell down to the window
; bottom row (exclusive), then puts the cursor back where it started.
; In:  cursor zp_3F/zp_40, window zp_3A/zp_3C, blank glyph zp_3E, colour zp_3D
; Out: cells filled; cursor unchanged on exit
; Called from: clearTextWindow (fall through), main program $24B5
; ----------------------------------------------------------------------
clearToWindowBottom:
        lda     textWindowBottom        ; C075  stop when the cursor reaches the row below the window
        jmp     L_C07F                  ; C077  into the shared fill loop


; ----------------------------------------------------------------------
; clearToEndOfLine - the same fill loop with the stop row set to cursor row + 1, i.e. it blanks the
; rest of the current line only.  Used to pad the status bar after a message and by the overlay
; screens.  Both entries patch the stop row into the CMP operand at $C08F+1.
; In:  cursor zp_3F/zp_40, window width zp_3C, blank glyph zp_3E, colour zp_3D
; Out: rest of the line filled; cursor restored; the CMP operand at $C090 is rewritten
; Called from: clearMessageLine ($C3A5), printOnMessageLine ($C3F6), drawStatusScoreLine ($C6DF),
;      $6F00 overlay $776D/$8049/$811C/$8471
; ----------------------------------------------------------------------
clearToEndOfLine:
        lda     textCursorRow           ; C07A  current row ...
        clc                             ; C07C  clear carry for the +1
        adc     #$01                    ; C07D  ... plus one: stop as soon as the fill wraps onto the next line
L_C07F:
        sta     L_C08F+1                ; C07F  patch the stop row into the CMP at $C08F
        lda     textCursorRow           ; C082  remember the cursor row ...
        pha                             ; C084  ... on the stack
        lda     textCursorCol           ; C085  and the cursor column ...
D_C087:
        pha                             ; C087  ... too, both are restored when the fill is done
L_C088:
        lda     textFillGlyph           ; C088  the current blank/fill glyph
sub_C08A:
        jsr     plotCharAndAdvance      ; C08A  print it; the cursor advances and wraps to the next row at the window edge
        lda     textCursorRow           ; C08D  which row is the cursor on now?
L_C08F:
        cmp     #$FF                    ; C08F  patched: stop row (window bottom, or cursor row + 1 for clearToEndOfLine)
        bcc     L_C088                  ; C091  keep filling while the cursor is still above the stop row
        pla                             ; C093  restore the starting column ...
        sta     textCursorCol           ; C094  put the cursor column back
        pla                             ; C096  ... and the starting row (falls into setCursorRow, which stores it)

; ----------------------------------------------------------------------
; setCursorRow - cursor row = A, then recompute the cell pointers (falls into updateCursorPointers).
; The usual 'move to row A' call; the column is left alone.
; In:  A = absolute screen row
; Out: zp_40 = A, zp_B4-zp_B7 recomputed
; Called from: clearToWindowBottom (fall through) and ~15 sites in the main program and the $6F00
;      overlay
; ----------------------------------------------------------------------
setCursorRow:
        sta     textCursorRow           ; C097  cursor row = A

; ----------------------------------------------------------------------
; updateCursorPointers - recomputes the bitmap and colour pointers of the cursor cell: absolute column
; = cursor column + window left edge, row = cursor row, through calcCellPointersXY ($2AA3). In
; multicolour mode ($D016 shadow $9001 bit 4 set) it adds $4C to the high byte, turning the
; $8C00 video-matrix pointer into a $D800 colour-RAM pointer.  X and Y survive in self-modified
; immediate operands ($C0B9 and $C0BB) instead of on the stack.
; In:  zp_3F cursor column, zp_40 cursor row, zp_3B window left, $9001
; Out: zp_B4/zp_B5 = bitmap cell address, zp_B6/zp_B7 = colour cell address; X/Y preserved
; Called from: everywhere in this file plus ~30 sites in the main program and both $6F00 overlays
; ----------------------------------------------------------------------
updateCursorPointers:
        stx     L_C0B8+1                ; C099  preserve X in the LDX operand at $C0B9
        sty     L_C0BA+1                ; C09C  preserve Y in the LDY operand at $C0BB
        clc                             ; C09F  clear carry for the column add
        lda     textCursorCol           ; C0A0  cursor column is relative to the window ...
        adc     textWindowLeft          ; C0A2  ... so add the window's left edge to get the screen column
        tax                             ; C0A4  X = screen column
        ldy     textCursorRow           ; C0A5  Y = screen row (rows are absolute already)
        jsr     calcCellPointersXY      ; C0A7  -> zp_B4/B5 = $A000 + row*320 + col*8, zp_B6/B7 = $8C00 + row*40 + col
        lda     vicCtrl2Shadow          ; C0AA  shadow of $D016 ...
        and     #$10                    ; C0AD  ... bit 4 = multicolour bitmap mode?
        beq     L_C0B8                  ; C0AF  hires: the colour byte belongs in the video matrix at $8C00, leave the pointer alone
        lda     screenPtrHi             ; C0B1  multicolour: the colour byte belongs in colour RAM ...
        clc                             ; C0B3  clear carry for the add
        adc     #$4C                    ; C0B4  ... so add $4C00 ($8C00 -> $D800)
        sta     screenPtrHi             ; C0B6  zp_B6/zp_B7 now addresses colour RAM
L_C0B8:
        ldx     #$FF                    ; C0B8  patched by $C099: the caller's X
L_C0BA:
        ldy     #$FF                    ; C0BA  patched by $C09C: the caller's Y
        rts                             ; C0BC  done

        lda     #$04                    ; C0BD  dead: unreachable 'lda #$04 / rts' left over from an earlier build of this page
        rts                             ; C0BF  dead (see $C0BD)


; ----------------------------------------------------------------------
; plotCharAndAdvance - draws glyph A at the cursor cell, then advances the cursor one column (two in
; double-size mode), wrapping to the next line when it reaches the window width, and recomputes the
; cell pointers.
; In:  A = glyph number, window/cursor state zp_39-zp_44
; Out: glyph drawn, cursor advanced, zp_B4-zp_B7 updated
; Called from: printChar ($C02D), handleControlChar ($C061), the window fill loop ($C08A),
;      printFillChars ($C0F6)
; ----------------------------------------------------------------------
plotCharAndAdvance:
        jsr     plotGlyphCell           ; C0C0  draw the glyph into the cell the cursor points at
        inc     textCursorCol           ; C0C3  one column to the right
        bit     doubleSizeText          ; C0C5  double-size characters? (zp_44 bit 7)
        bpl     L_C0CB                  ; C0C7  no
        inc     textCursorCol           ; C0C9  a double-size glyph is two cells wide
L_C0CB:
        ldy     textCursorCol           ; C0CB  the new cursor column ...
        cpy     textWindowWidth         ; C0CD  ... has it reached the right edge of the window?
        bcc     L_C0D4                  ; C0CF  no - stay on this line
        jsr     printNewline            ; C0D1  wrap to column 0 of the next line
L_C0D4:
        jmp     updateCursorPointers    ; C0D4  recompute the cell pointers for the new cursor position


; ----------------------------------------------------------------------
; setFullScreenWindow - resets the text window to the whole screen: top row 0, left column 0, bottom
; $19 (row 25, exclusive) and width $28 (40 columns).
; In:  none
; Out: zp_39 = 0, zp_3A = $19, zp_3B = 0, zp_3C = $28
; Called from: saveTextWindow ($C425), main program $1647/$2364/$2622/$2645/$2D41, $6F00 overlay
;      $6FF5/$70B3/$70DD/$7A78/$7B2B/$7B44
; ----------------------------------------------------------------------
setFullScreenWindow:
        lda     #$00                    ; C0D7  row 0 =
        sta     textWindowTop           ; C0D9  top of the window
        lda     #$00                    ; C0DB  column 0 =
        sta     textWindowLeft          ; C0DD  left edge of the window
        lda     #$19                    ; C0DF  row 25 (exclusive) =
        sta     textWindowBottom        ; C0E1  bottom of the window, i.e. all 25 rows
        lda     #$28                    ; C0E3  40 columns =
        sta     textWindowWidth         ; C0E5  full screen width
        rts                             ; C0E7  done


; ----------------------------------------------------------------------
; saveRegsAXY - copies A, X and Y into the zp_54/zp_55/zp_56 scratch triple so restoreRegsAXY can put
; them back.  There is only one slot, so the pair must not be nested.
; In:  A, X, Y
; Out: zp_54 = A, zp_55 = X, zp_56 = Y
; Called from: printChar ($C01A), map viewport code $5C81
; ----------------------------------------------------------------------
saveRegsAXY:
        sta     savedRegA               ; C0E8  A save slot
        stx     savedRegX               ; C0EA  X save slot
        sty     savedRegY               ; C0EC  Y save slot
L_C0EE:
        rts                             ; C0EE  done


; ----------------------------------------------------------------------
; printFillChars - prints the current blank glyph zp_3E A times through plotCharAndAdvance; a count of
; 0 does nothing.  Used to pad fields, to draw the left pad of a centred string and to draw horizontal
; bars.
; In:  A = repeat count, zp_3E = fill glyph, zp_3D = colour
; Out: A cells filled, cursor advanced past them
; Called from: printStringAtPointer ($C15B), main program $15BD/$15CE/$15EC/$165E/$166E/$1728/
;      $1733/$19DB, $6F00 overlay $7727/$7F12/$7F1E/$8623
; ----------------------------------------------------------------------
printFillChars:
        cmp     #$00                    ; C0EF  count of 0?
        beq     L_C0FF                  ; C0F1  then there is nothing to draw
L_C0F3:
        pha                             ; C0F3  keep the remaining count on the stack
        lda     textFillGlyph           ; C0F4  the blank/fill glyph
L_C0F6:
        jsr     plotCharAndAdvance      ; C0F6  draw it and advance the cursor
        pla                             ; C0F9  get the count back
L_C0FA:
        sec                             ; C0FA  set carry so the subtract does not borrow
        sbc     #$01                    ; C0FB  one cell done
        bne     L_C0F3                  ; C0FD  keep going until the count runs out
L_C0FF:
        rts                             ; C0FF  done


; ----------------------------------------------------------------------
; printTableEntry - prints entry X of a table of fixed 8-byte, bit-7-terminated strings whose base
; address is passed in A (low) / Y (high): computes base + X*8 with a 16-bit shift and tail-calls
; printString.  Used for the unit type names ($C964), group names ($C92C), menu items and the
; STANDARD/CUSTOM words.
; In:  A/Y = table base, X = entry index
; Out: the string is printed at the cursor; zp_18/zp_19/zp_1A clobbered
; Called from: main program $15C9/$15D9/$22CD/$23B5/$242C/$2486/$5FCE/$6168/$6352, $6F00 overlay
;      $879C
; ----------------------------------------------------------------------
printTableEntry:
        sta     scratch18               ; C100  table base, low byte
        lda     #$00                    ; C102  zero ...
        sta     scratch1A               ; C104  clear the high byte of the offset being built
        sty     scratch19               ; C106  table base, high byte
        txa                             ; C108  A = entry index
        asl     a                       ; C109  index * 2 ...
        rol     scratch1A               ; C10A  ... carrying into the offset high byte
        asl     a                       ; C10C  index * 4 ...
        rol     scratch1A               ; C10D  ... carrying into the offset high byte
        asl     a                       ; C10F  index * 8 - the table entries are 8 bytes each
        rol     scratch1A               ; C110  ... and once more
        clc                             ; C112  clear carry for the 16-bit add
        adc     scratch18               ; C113  base low + offset low ...
        sta     scratch18               ; C115  = pointer low byte
        lda     scratch1A               ; C117  offset high ...
        adc     scratch19               ; C119  ... plus base high
        tay                             ; C11B  Y = pointer high byte
        lda     scratch18               ; C11C  A = pointer low byte
        jmp     printString             ; C11E  print the bit-7 terminated string found there


; ----------------------------------------------------------------------
; printStringCentred - sets the centring field width to the current window width and tail-calls
; printString, so the string is padded on the left to sit centred in the window.  A survives the store
; across the stack.
; In:  A/Y = string pointer, zp_3C = window width
; Out: $90EF = window width (cleared again by printStringAtPointer); string printed centred
; Called from: main program $4C49/$5F5D/$638B/$6452/$64A8/$64B8/$64FA, $6F00 overlay $7714/$7EB0/
;      $8346/$83A4/$83E9/$85ED/$8780
; ----------------------------------------------------------------------
printStringCentred:
        pha                             ; C121  keep the pointer low byte
        lda     textWindowWidth         ; C122  the window width ...
        sta     textCentreWidth         ; C124  ... is the field the string is centred in
        pla                             ; C127  pointer low byte back
        jmp     printString             ; C128  print it centred


; ----------------------------------------------------------------------
; printNextStringCentred - like printStringCentred, but continues with the string that follows the one
; just printed: zp_A2/zp_A3 already points past the previous terminator, so this prints the next line
; of a multi-string block, centred.
; In:  zp_A2/zp_A3 = string pointer, zp_3C = window width
; Out: $90EF = window width; the next string is printed centred
; Called from: main program $6503, $6F00 overlay $83EC
; ----------------------------------------------------------------------
printNextStringCentred:
        lda     textWindowWidth         ; C12B  the window width ...
        sta     textCentreWidth         ; C12D  ... is the field to centre in
        jmp     printStringAtPointer    ; C130  keep printing where the previous string ended


; ----------------------------------------------------------------------
; printString - sets the string pointer zp_A2/zp_A3 from A/Y and falls into printStringAtPointer. The
; main text output call of the game; strings end with a character that has bit 7 set.
; In:  A = pointer low, Y = pointer high
; Out: string printed at the cursor, zp_A2/zp_A3 left just past the terminator
; Called from: ~25 sites in the main program, ~20 in the $6F00 overlays, and $C11E/$C128/$C3F3/
;      $C6C0 in this file
; ----------------------------------------------------------------------
printString:
        sta     stringPtr               ; C133  string pointer, low byte
        sty     stringPtrHi             ; C135  string pointer, high byte

; ----------------------------------------------------------------------
; printStringAtPointer - prints the bit-7-terminated string at zp_A2/zp_A3.  When the centring width
; $90EF is non-zero it first measures the string and prints (width - length)/2 blanks in the pad
; colour zp_42 (the text colour is saved across the pad), then prints every character through
; printChar, terminator included, and finally clears $90EF so centring is a one-shot setting.
; In:  zp_A2/zp_A3 = string, $90EF = field width or 0, zp_42 = pad colour, zp_3E = blank glyph
; Out: text drawn, zp_A2/zp_A3 past the terminator, $90EF = 0; the SBC operand at $C159 is patched
; Called from: printString (fall through), printNextStringCentred ($C130), drawStatusScoreLine
;      ($C6CB/$C6D6), main program $1A2C/$1A3D/$1A45/$5EA9, $6F00 overlay
; $8276/$82E5/$8590/$8596/$863C
; ----------------------------------------------------------------------
printStringAtPointer:
        lda     textCentreWidth         ; C137  is a centring width set?
        beq     L_C161                  ; C13A  no - print left aligned at the cursor
        lda     textColour              ; C13C  save the text colour ...
        pha                             ; C13E  ... across the padding
        lda     textPadColour           ; C13F  the pad blanks use their own colour
D_C141:
        sta     textColour              ; C141  the pad colour is the text colour while the blanks are drawn

; ----------------------------------------------------------------------
; Measure the string: count characters until one with bit 7 set (the terminator) has been counted.
; ----------------------------------------------------------------------
        ldy     #$00                    ; C143  start at the first character
        ldx     #$00                    ; C145  X counts the characters
L_C147:
        lda     (stringPtr),y           ; C147  next character of the string
        pha                             ; C149  keep it for the bit-7 test
        and     #$7F                    ; C14A  strip the terminator bit (the value itself is not needed)
        iny                             ; C14C  next character position
        inx                             ; C14D  count this character, terminator included
        pla                             ; C14E  test the original character ...
        bpl     L_C147                  ; C14F  ... loop until bit 7 was set
        stx     L_C158+1                ; C151  patch the length into the SBC at $C158
        lda     textCentreWidth         ; C154  field width ...
        sec                             ; C157  set carry for the subtract
L_C158:
        sbc     #$FF                    ; C158  patched: string length including the terminator
        lsr     a                       ; C15A  half of the slack: only the left side is padded
        jsr     printFillChars          ; C15B  print that many blanks in the pad colour
        pla                             ; C15E  restore the text colour for the string itself
        sta     textColour              ; C15F  the string itself uses the caller's colour again

; ----------------------------------------------------------------------
; Print loop: fetch a character, advance the pointer, print it; the character with bit 7 set is
; printed as well and ends the string.
; ----------------------------------------------------------------------
L_C161:
        ldy     #$00                    ; C161  always read at offset 0 ...
        lda     (stringPtr),y           ; C163  ... the character the pointer points at
        inc     stringPtr               ; C165  advance the string pointer ...
        bne     L_C16B                  ; C167  no carry into the pointer high byte
        inc     stringPtrHi             ; C169  ... across a page boundary if need be
L_C16B:
        tay                             ; C16B  test bit 7 without disturbing A
        bmi     L_C174                  ; C16C  bit 7 set: this is the last character
        jsr     printChar               ; C16E  print it
        jmp     L_C161                  ; C171  and go on with the next

L_C174:
        jsr     printChar               ; C174  print the terminating character too (printChar masks its bit 7 off)
        lda     #$00                    ; C177  centring is a one-shot setting ...
        sta     textCentreWidth         ; C179  ... so clear it for the next string
        rts                             ; C17C  done


; ----------------------------------------------------------------------
; printDecimal - prints an unsigned 24-bit value in decimal without leading zeros: converts it into
; the digit buffer at $92DA and then prints the digits.
; In:  A = value low, Y = value middle, zp_7E = value high
; Out: digits printed; $92DA filled, zp_7E cleared, zp_18-zp_1F clobbered
; Called from: drawStatusScoreLine ($C6C8), printPlayerPoints ($C6EC), main program $1739/$22DA/
;      $23CE/$618D, $6F00 overlay $7EDC/$846E/$862A
; ----------------------------------------------------------------------
printDecimal:
        jsr     convertToDecimalDigits  ; C17D  value -> ASCII digits in $92DA
        jmp     printDecimalDigits      ; C180  print them, most significant first


; ----------------------------------------------------------------------
; convertToDecimalDigits - converts the 24-bit value in A (low) / Y (middle) / zp_7E (high) to decimal
; by repeated division by 10: a 24-iteration shift/subtract loop leaves the quotient in zp_18-zp_1A
; and the digit in zp_1B.  Digits are stored as $B0 + digit (ASCII with bit 7 set) backwards from the
; end of the buffer at $92DA, so zp_1E ends up pointing at the most significant one.  A value of 0
; still produces one digit.
; In:  A/Y/zp_7E = 24-bit value
; Out: $92DA+1..$92DA+8 digits, zp_1E = index of the first digit, zp_7E = 0, zp_18-zp_1F clobbered
; Called from: printDecimal ($C17D)
; ----------------------------------------------------------------------
convertToDecimalDigits:
        sta     scratch18               ; C183  dividend, low byte
        sty     scratch19               ; C185  dividend, middle byte
        lda     decimalValueHigh        ; C187  the caller's high byte ...
        sta     scratch1A               ; C189  ... completes the 24-bit dividend
        ldy     #$08                    ; C18B  the digit buffer holds 8 digits ($92DA+1..+8) ...
        sty     scratch1E               ; C18D  ... and is filled from its last slot backwards

; ----------------------------------------------------------------------
; Outer loop: one division by 10 per pass, lowest digit first, until the quotient is zero.
; ----------------------------------------------------------------------
L_C18F:
        lda     #$00                    ; C18F  clear the 24-bit remainder ...
        sta     scratch1B               ; C191  remainder, low byte
        sta     scratch1C               ; C193  remainder, middle byte
        sta     scratch1D               ; C195  remainder, high byte
L_C197:
        lda     #$18                    ; C197  24 bits of dividend to shift through
        sta     scratch1F               ; C199  bit counter for this division
        clc                             ; C19B  the first quotient bit shifts in as 0

; ----------------------------------------------------------------------
; Restoring division by 10: shift dividend:remainder left one bit, subtract 10 from the remainder and
; keep the result only if it did not borrow; the carry becomes the next quotient bit.
; ----------------------------------------------------------------------
L_C19C:
        rol     scratch18               ; C19C  shift the dividend left, its top bit ...
        rol     scratch19               ; C19E  dividend, middle byte
        rol     scratch1A               ; C1A0  ... entering the remainder
        rol     scratch1B               ; C1A2  remainder, low byte
        rol     scratch1C               ; C1A4  remainder, middle byte
        rol     scratch1D               ; C1A6  remainder, high byte
        sec                             ; C1A8  no borrow into the trial subtraction
        lda     scratch1B               ; C1A9  trial subtraction: remainder - 10, low byte
        sbc     #$0A                    ; C1AB  10 = the divisor
        tay                             ; C1AD  hold the result in Y ...
        lda     scratch1C               ; C1AE  middle byte
        sbc     #$00                    ; C1B0  only the borrow
        tax                             ; C1B2  ... and in X ...
L_C1B3:
        lda     scratch1D               ; C1B3  high byte
D_C1B5:
        sbc     #$00                    ; C1B5  only the borrow
        bcc     L_C1BF                  ; C1B7  it borrowed: this quotient bit is 0, keep the old remainder
        sty     scratch1B               ; C1B9  no borrow: the subtraction stands ...
        stx     scratch1C               ; C1BB  middle byte of the reduced remainder
        sta     scratch1D               ; C1BD  ... and the carry makes this quotient bit a 1
L_C1BF:
        dec     scratch1F               ; C1BF  one of the 24 bits done
        bne     L_C19C                  ; C1C1  24 shifts in all
        rol     scratch18               ; C1C3  shift the last quotient bit in: zp_18-zp_1A = value / 10
        rol     scratch19               ; C1C5  quotient, middle byte
        rol     scratch1A               ; C1C7  quotient, high byte
        lda     scratch1B               ; C1C9  remainder 0-9 = this decimal digit
        clc                             ; C1CB  clear carry for the add
        adc     #$B0                    ; C1CC  +$B0 = '0'..'9' with bit 7 set (printChar masks it off again)
        ldy     scratch1E               ; C1CE  the current end of the digit buffer
        sta     decimalDigitBuffer,y    ; C1D0  store the digit there
        dec     scratch1E               ; C1D3  the next digit goes one slot to the left
        lda     scratch18               ; C1D5  is the quotient ...
        ora     scratch19               ; C1D7  ... zero in all ...
        ora     scratch1A               ; C1D9  ... three bytes?
        bne     L_C18F                  ; C1DB  no - divide again for the next digit
        sta     decimalValueHigh        ; C1DD  A is 0 here, so this also clears the caller's high byte zp_7E
        inc     scratch1E               ; C1DF  point zp_1E at the most significant digit
        rts                             ; C1E1  done


; ----------------------------------------------------------------------
; printDecimalDigits - prints the digit buffer at $92DA from index zp_1E up to and including index 8,
; i.e. the significant digits produced by convertToDecimalDigits.
; In:  zp_1E = index of the first digit, buffer $92DA
; Out: the digits are printed at the cursor
; Called from: printDecimal ($C180)
; ----------------------------------------------------------------------
printDecimalDigits:
        ldy     scratch1E               ; C1E2  start at the most significant digit
L_C1E4:
        lda     decimalDigitBuffer,y    ; C1E4  digit as '0'+d with bit 7 set
        jsr     printChar               ; C1E7  print it
        iny                             ; C1EA  next digit
        cpy     #$09                    ; C1EB  index 8 is the last (units) digit
        bcc     L_C1E4                  ; C1ED  loop over the remaining digits
        rts                             ; C1EF  done


; ----------------------------------------------------------------------
; plotGlyphCell - draws glyph A into the bitmap cell at zp_B4/zp_B5 and writes the colour byte zp_3D
; through zp_B6/zp_B7.  The glyph source is $98BE + A*8 in multicolour mode or $9A96 + A*8 in hires
; mode: two fonts $1D8 bytes apart with the same shapes, one drawn in bit pairs %11 (colour from
; colour RAM) and one in single bits (colour from the video matrix).  With double-size text selected
; it hands the glyph to plotGlyphDoubleSize instead.  The copy loop at $C227 reads the bitmap byte
; that is already there and the following opcode at $C22B is patched by setBitmapDrawMode ($2A41) to
; LDA/ORA/EOR/CMP (zp),Y, giving replace / OR / EOR / transparent drawing.
; In:  A = glyph number, zp_B4-zp_B7 cell pointers, zp_3D colour, zp_44 double-size flag, $9001
; Out: 8 bitmap bytes and one colour byte written, zp_B2/zp_B3 = glyph pointer; A preserved
; Called from: plotCharAndAdvance ($C0C0)
; ----------------------------------------------------------------------
plotGlyphCell:
        pha                             ; C1F0  keep the glyph number - it is handed back to the caller
        ldy     #$00                    ; C1F1  zero ...
        sty     glyphPtr                ; C1F3  clear the glyph pointer ...
        sty     glyphPtrHi              ; C1F5  ... before shifting the offset into it
        asl     a                       ; C1F7  glyph * 2 ...
        rol     glyphPtrHi              ; C1F8  ... carrying into the pointer high byte
        asl     a                       ; C1FA  glyph * 4 ...
        rol     glyphPtrHi              ; C1FB  ... carrying into the pointer high byte
        asl     a                       ; C1FD  glyph * 8 - a glyph is 8 bytes
        rol     glyphPtrHi              ; C1FE  ... carrying into the pointer high byte
L_C200:
        clc                             ; C200  clear carry for the base add
        adc     #$BE                    ; C201  add the multicolour font base $98BE ...
        sta     glyphPtr                ; C203  glyph pointer, low byte
D_C205:
        lda     glyphPtrHi              ; C205  offset high byte ...
        adc     #$98                    ; C207  ... high byte
        sta     glyphPtrHi              ; C209  glyph pointer, high byte
        lda     vicCtrl2Shadow          ; C20B  shadow of $D016 ...
        and     #$10                    ; C20E  ... bit 4 = multicolour bitmap?
        bne     L_C21F                  ; C210  multicolour: the $98BE font is the right one
        lda     glyphPtr                ; C212  hires: add $01D8 to reach the identical ...
        clc                             ; C214  clear carry for the add
        adc     #$D8                    ; C215  low byte of $01D8 ...
        sta     glyphPtr                ; C217  glyph pointer, low byte
        lda     glyphPtrHi              ; C219  ... hires font at $9A96 (59 glyphs further on)
        adc     #$01                    ; C21B  high byte of $01D8 ...
        sta     glyphPtrHi              ; C21D  glyph pointer, high byte
L_C21F:
        bit     doubleSizeText          ; C21F  double-size text? (zp_44 bit 7)
        bpl     L_C227                  ; C221  no - draw it at normal size
        pla                             ; C223  restore the glyph number for the caller
        jmp     plotGlyphDoubleSize     ; C224  the 2x2 magnifier draws it instead


; ----------------------------------------------------------------------
; Copy the 8 rows of the glyph into the cell, bottom row first.
; ----------------------------------------------------------------------
L_C227:
        ldy     #$07                    ; C227  8 bitmap rows, counted down
L_C229:
        lda     (bitmapPtr),y           ; C229  read the bitmap byte that is already there ...
D_C22B:
        lda     (glyphPtr),y            ; C22B  ... patched by setBitmapDrawMode ($2A41) to LDA/ORA/EOR/CMP (zp),Y = replace/OR/EOR/transparent
        sta     (bitmapPtr),y           ; C22D  store the combined row back into the bitmap
        dey                             ; C22F  next row up
        bpl     L_C229                  ; C230  loop over all 8 rows of the cell
        iny                             ; C232  Y = 0: the cell's single colour byte
        lda     (screenPtr),y           ; C233  dead load - nothing patches this instruction, so the colour is always replaced
        lda     textColour              ; C235  the text colour ...
        sta     (screenPtr),y           ; C237  ... into colour RAM (multicolour) or the video matrix (hires)
        pla                             ; C239  hand the glyph number back
        rts                             ; C23A  done


; pixelDoubleTable: 16 bytes, indexed by a nibble; each entry is that nibble with every pixel doubled
; ($1 -> $03, $F -> $FF).  Read by expandGlyphNibbles for the 2x magnifier.
pixelDoubleTable:
        .byte   $00,$03,$0C,$0F         ; C23B  ....  nibble $0-$3 doubled
sub_C23F:
        .byte   $30                     ; C23F  nibble $4-$B doubled (the label here is the boot loader's)
        .byte   $33,$3C,$3F,$C0,$C3,$CC,$CF; C240  3<?....
        .byte   $F0                     ; C247  nibble $C-$D doubled
        .byte   $F3                     ; C248  .
L_C249:
        .byte   $FC                     ; C249  nibble $E-$F doubled
        .byte   $FF                     ; C24A  .

; ----------------------------------------------------------------------
; plotGlyphDoubleSize - draws the glyph at zp_B2/zp_B3 magnified 2x2 into four character cells
; starting at zp_B4/zp_B5 (top-left, +8 top-right, +$138 more = one bitmap row down for bottom-left,
; +8 bottom-right) and colours them through colourDoubleSizeCell.  The 8 source bytes are copied to
; the work buffer $90B8 first; each quadrant expands 4 source rows and one nibble of each row.  The
; mask zp_BA suppresses quadrants (bit 0 top cells, bit 1 bottom cells, bit 2 left cells, bit 3 right
; cells) so a zoom rectangle can keep half-size edges - note zp_BA is only ever written by
; magnifyWindow2x, so double-size text uses whatever mask it left behind.  A, X and Y are preserved in
; self-modified operands ($C301, $C303, $C305).
; In:  zp_B2/zp_B3 = glyph, zp_B4-zp_B7 = destination cell, zp_3D = colour, zp_BA = quadrant mask
; Out: up to 4 cells (32 bitmap bytes) and their colour bytes written; $90B8-$90BF and zp_1A-zp_1D
;      clobbered; the operands at $C33A/$C33B/$C33D/$C33E are rewritten as it goes
; Called from: plotGlyphCell ($C224), magnifyWindow2x ($2E7B/$2EA5/$2EEA/$2F14)
; ----------------------------------------------------------------------
plotGlyphDoubleSize:
        sta     L_C304+1                ; C24B  preserve A in the LDA operand at $C305
        sty     L_C302+1                ; C24E  preserve Y in the LDY operand at $C303
        stx     L_C300+1                ; C251  preserve X in the LDX operand at $C301
        ldy     #$07                    ; C254  8 glyph rows ...
L_C256:
        lda     (glyphPtr),y            ; C256  ... read from the font ...
        sta     glyphBuildBuffer,y      ; C258  ... into the work buffer, where the expander can index them freely
        dey                             ; C25B  next row up
        bpl     L_C256                  ; C25C  until all 8 rows are in the buffer
        lda     #$00                    ; C25E  source rows 0-3 = the top half of the glyph
        sta     scratch1B               ; C260  row offset into the work buffer

; ----------------------------------------------------------------------
; Point the two STA operands in expandGlyphNibbles at the top-left cell: $C33A gets the cell address
; and $C33D that address + 1, so every expanded row is written to two consecutive bitmap rows.
; ----------------------------------------------------------------------
        lda     bitmapPtr               ; C262  destination cell, low byte ...
        sta     L_C339+1                ; C264  ... into the STA operand at $C33A
        clc                             ; C267  clear carry for the +1
        adc     #$01                    ; C268  the next bitmap row of the same cell ...
        sta     L_C33C+1                ; C26A  ... into the STA operand at $C33D
        lda     bitmapPtrHi             ; C26D  destination cell, high byte ...
        sta     L_C339+2                ; C26F  ... into the STA at $C33B
        adc     #$00                    ; C272  carry of the low byte
        sta     L_C33C+2                ; C274  ... and $C33E
        lda     magnifyEdgeMask         ; C277  quadrant mask ...
        and     #$01                    ; C279  ... bit 0 = skip the top pair of cells
        bne     L_C280                  ; C27B  top row suppressed - jump straight to the bottom half
        jsr     plotGlyphLeftHalf       ; C27D  top-left cell: the high nibble of source rows 0-3

; ----------------------------------------------------------------------
; Step both destination addresses on by 8 bytes = one character cell to the right.
; ----------------------------------------------------------------------
L_C280:
        clc                             ; C280  clear carry for the 16-bit add
        lda     L_C339+1                ; C281  first destination ...
        adc     #$08                    ; C284  ... plus 8 = the next cell
        sta     L_C339+1                ; C286  back into the STA operand
        lda     L_C339+2                ; C289  its high byte ...
        adc     #$00                    ; C28C  carry into the high byte
        sta     L_C339+2                ; C28E  back into the STA operand
        lda     L_C33C+1                ; C291  second destination ... (relies on the carry left clear above)
        adc     #$08                    ; C294  ... plus 8
        sta     L_C33C+1                ; C296  back into the second STA operand
        lda     L_C33C+2                ; C299  its high byte ...
        adc     #$00                    ; C29C  carry of the low byte
        sta     L_C33C+2                ; C29E  back into the STA operand
        lda     magnifyEdgeMask         ; C2A1  quadrant mask ...
        and     #$01                    ; C2A3  ... top pair of cells still enabled?
        bne     L_C2AA                  ; C2A5  no
        jsr     plotGlyphRightHalf      ; C2A7  top-right cell: the low nibble of source rows 0-3
L_C2AA:
        lda     #$04                    ; C2AA  source rows 4-7 = the bottom half of the glyph
        sta     scratch1B               ; C2AC  row offset into the work buffer

; ----------------------------------------------------------------------
; Step both destinations on by $138; together with the +8 above that is +320 = one character row down,
; i.e. the bottom-left cell.
; ----------------------------------------------------------------------
        clc                             ; C2AE  clear carry for the 16-bit add
        lda     L_C339+1                ; C2AF  first destination ...
        adc     #$38                    ; C2B2  ... plus $138 ...
        sta     L_C339+1                ; C2B4  back into the STA operand
        lda     L_C339+2                ; C2B7  its high byte ...
        adc     #$01                    ; C2BA  ... = +320 with the +8 above: one character row down
        sta     L_C339+2                ; C2BC  back into the STA operand
        clc                             ; C2BF  clear carry for the second 16-bit add
        lda     L_C33C+1                ; C2C0  second destination ...
        adc     #$38                    ; C2C3  ... plus $138 ...
        sta     L_C33C+1                ; C2C5  back into the STA operand
        lda     L_C33C+2                ; C2C8  its high byte ...
        adc     #$01                    ; C2CB  ... = one character row down
        sta     L_C33C+2                ; C2CD  back into the STA operand
        lda     magnifyEdgeMask         ; C2D0  quadrant mask ...
        and     #$02                    ; C2D2  ... bit 1 = skip the bottom pair of cells
        bne     L_C2FD                  ; C2D4  bottom row suppressed - go and colour what was drawn
        jsr     plotGlyphLeftHalf       ; C2D6  bottom-left cell: the high nibble of source rows 4-7
        clc                             ; C2D9  clear carry for the 16-bit add
sub_C2DA:
        lda     L_C339+1                ; C2DA  first destination ...
        adc     #$08                    ; C2DD  ... plus 8 = the bottom-right cell
        sta     L_C339+1                ; C2DF  back into the STA operand
        lda     L_C339+2                ; C2E2  its high byte ...
        adc     #$00                    ; C2E5  carry of the low byte
        sta     L_C339+2                ; C2E7  back into the STA operand
        lda     L_C33C+1                ; C2EA  second destination ... (again relying on the clear carry)
        adc     #$08                    ; C2ED  ... plus 8
        sta     L_C33C+1                ; C2EF  back into the second STA operand
        lda     L_C33C+2                ; C2F2  its high byte ...
        adc     #$00                    ; C2F5  carry of the low byte
        sta     L_C33C+2                ; C2F7  back into the STA operand
        jsr     plotGlyphRightHalf      ; C2FA  bottom-right cell: the low nibble of source rows 4-7
L_C2FD:
        jsr     colourDoubleSizeCell    ; C2FD  write the colour byte into all four cells
L_C300:
        ldx     #$FF                    ; C300  patched by $C251: the caller's X
L_C302:
        ldy     #$FF                    ; C302  patched by $C24E: the caller's Y
L_C304:
        lda     #$FF                    ; C304  patched by $C24B: the caller's A (the glyph number)
        rts                             ; C306  done


; ----------------------------------------------------------------------
; plotGlyphLeftHalf - quadrant helper of plotGlyphDoubleSize for a left-hand cell: returns at once
; when the mask suppresses left cells, otherwise selects the high nibble of each source row and runs
; the expander.
; In:  zp_BA quadrant mask, zp_1B = source row offset (0 top half, 4 bottom half)
; Out: 8 bitmap bytes of one cell written; zp_1D = $F0
; Called from: plotGlyphDoubleSize ($C27D top-left, $C2D6 bottom-left)
; ----------------------------------------------------------------------
plotGlyphLeftHalf:
        lda     magnifyEdgeMask         ; C307  quadrant mask ...
        and     #$04                    ; C309  ... bit 2 = left-hand cells suppressed?
        beq     L_C30E                  ; C30B  no - draw it
        rts                             ; C30D  suppressed: nothing to do

L_C30E:
        lda     #$F0                    ; C30E  the high nibble is the left half of the glyph row
        sta     scratch1D               ; C310  $F0 = keep the high nibble of every source row
L_C312:
        jmp     expandGlyphNibbles      ; C312  expand it into the destination cell


; ----------------------------------------------------------------------
; plotGlyphRightHalf - the same for a right-hand cell: returns at once when the mask suppresses right
; cells, otherwise selects the low nibble of each source row and falls into the expander.
; In:  zp_BA quadrant mask, zp_1B = source row offset
; Out: 8 bitmap bytes of one cell written; zp_1D = $0F
; Called from: plotGlyphDoubleSize ($C2A7 top-right, $C2FA bottom-right)
; ----------------------------------------------------------------------
plotGlyphRightHalf:
        lda     magnifyEdgeMask         ; C315  quadrant mask ...
        and     #$08                    ; C317  ... bit 3 = right-hand cells suppressed?
        beq     L_C31C                  ; C319  no - draw it
        rts                             ; C31B  suppressed: nothing to do

L_C31C:
        lda     #$0F                    ; C31C  the low nibble is the right half of the glyph row
        sta     scratch1D               ; C31E  $0F = keep the low nibble of every source row

; ----------------------------------------------------------------------
; expandGlyphNibbles - inner loop of the 2x magnifier: takes 4 glyph rows from the work buffer $90B8
; starting at offset zp_1B, masks each with zp_1D (shifting the high nibble down), doubles its pixels
; horizontally through pixelDoubleTable and stores the result to two bitmap rows one after the other
; (the self-modified absolute addresses at $C33A and $C33D), doubling it vertically as well.
; In:  $90B8 glyph rows, zp_1B = 0 or 4, zp_1D = $F0 or $0F, destination operands $C33A/$C33D
; Out: 8 bitmap bytes of one cell written; zp_1A = destination row counter
; Called from: plotGlyphLeftHalf ($C312), plotGlyphRightHalf (fall through)
; ----------------------------------------------------------------------
expandGlyphNibbles:
        ldy     scratch1B               ; C320  first source row: 0 for the top half, 4 for the bottom half
D_C322:
        ldx     #$00                    ; C322  zero ...
        stx     scratch1A               ; C324  destination row within the cell = 0
L_C326:
        lda     glyphBuildBuffer,y      ; C326  one row of the glyph
        and     scratch1D               ; C329  keep only the half being magnified
        bit     scratch1D               ; C32B  was that the high nibble ($F0 is negative)?
        bpl     L_C333                  ; C32D  no - the low nibble is already in place
        lsr     a                       ; C32F  shift the high nibble down, 1 ...
        lsr     a                       ; C330  ... 2 ...
        lsr     a                       ; C331  ... 3 ...
        lsr     a                       ; C332  ... 4 places, into bits 0-3
L_C333:
        tax                             ; C333  index the doubling table with the nibble
        lda     pixelDoubleTable,x      ; C334  the same 4 pixels, each one twice as wide
        ldx     scratch1A               ; C337  X = destination row within the cell
L_C339:
        sta     irqVectorHi,x           ; C339  patched destination: cell address + X
L_C33C:
        sta     irqVectorHi,x           ; C33C  patched destination: cell address + 1 + X - the same row again, doubling vertically
        iny                             ; C33F  next source row
        inc     scratch1A               ; C340  two destination rows ...
        inc     scratch1A               ; C342  ... per source row
        ldx     scratch1A               ; C344  destination row counter
        cpx     #$08                    ; C346  8 bitmap rows = 4 source rows
        bcc     L_C326                  ; C348  loop over the 4 source rows
        rts                             ; C34A  done


; ----------------------------------------------------------------------
; colourDoubleSizeCell - writes the colour byte zp_3D into the colour cells of the 2x2 block just
; drawn: the top pair unless the mask suppresses top cells, then the pointer is advanced by 40 (one
; screen row) and the bottom pair is written unless the mask suppresses bottom cells.
; In:  zp_B6/zp_B7 = colour cell pointer, zp_3D = colour, zp_BA = quadrant mask
; Out: up to 4 colour bytes written; zp_B6/zp_B7 advanced by one screen row
; Called from: plotGlyphDoubleSize ($C2FD)
; ----------------------------------------------------------------------
colourDoubleSizeCell:
        ldy     #$00                    ; C34B  redundant - colourCellPair sets Y itself
        lda     magnifyEdgeMask         ; C34D  quadrant mask ...
        and     #$01                    ; C34F  ... bit 0 = top cells suppressed?
        bne     L_C356                  ; C351  yes - try the bottom pair
        jsr     colourCellPair          ; C353  colour the top-left and top-right cells
L_C356:
        lda     magnifyEdgeMask         ; C356  quadrant mask ...
        and     #$02                    ; C358  ... bit 1 = bottom cells suppressed?
        bne     L_C37E                  ; C35A  yes - nothing more to colour
        clc                             ; C35C  clear carry for the +40
        lda     screenPtr               ; C35D  advance the colour pointer ...
D_C35F:
        adc     #$28                    ; C35F  ... by 40 = one character row down
        sta     screenPtr               ; C361  colour pointer, low byte
        bcc     colourCellPair          ; C363  no carry - colour the bottom pair
        inc     screenPtrHi             ; C365  carry into the high byte, then fall into colourCellPair

; ----------------------------------------------------------------------
; colourCellPair - writes zp_3D into the two horizontally adjacent colour cells at (zp_B6),0 and
; (zp_B6),1, skipping the left one when the mask suppresses left cells and the right one when it
; suppresses right cells.
; In:  zp_B6/zp_B7, zp_3D = colour, zp_BA = quadrant mask
; Out: up to 2 colour bytes written
; Called from: colourDoubleSizeCell ($C353 and the fall-through at $C363/$C365)
; ----------------------------------------------------------------------
colourCellPair:
        ldy     #$00                    ; C367  the left cell of the pair
        lda     magnifyEdgeMask         ; C369  quadrant mask ...
        and     #$04                    ; C36B  ... bit 2 = left cells suppressed?
        bne     L_C373                  ; C36D  yes - skip it
        lda     textColour              ; C36F  the text colour ...
D_C371:
        sta     (screenPtr),y           ; C371  ... into the left cell
L_C373:
        iny                             ; C373  the right cell of the pair
        lda     magnifyEdgeMask         ; C374  quadrant mask ...
        and     #$08                    ; C376  ... bit 3 = right cells suppressed?
        bne     L_C37E                  ; C378  yes - skip it
        lda     textColour              ; C37A  the text colour ...
        sta     (screenPtr),y           ; C37C  ... into the right cell
L_C37E:
        rts                             ; C37E  done


; ----------------------------------------------------------------------
; waitFrames - busy-waits A frames by polling the raster register: per frame it waits until
; $D012 reads $F0 (line 240, in the lower border) and then until the raster leaves that line.
; Returns at once for A = 0.  Used for the pauses in the boot and menu sequences, in sound effects and
; in the modem dialogue.
; In:  A = number of frames
; Out: returns A frames later with A = 0
; Called from: main program $0CAF/$0F58/$1028/$1BA8/$1C14/$1C65/$409E/$5A3C, $6F00 overlay $7E76/
;      $80BB, modem driver $EE03
; ----------------------------------------------------------------------
waitFrames:
        cmp     #$00                    ; C37F  a count of 0 ...
        beq     L_C396                  ; C381  ... means no wait at all
L_C383:
        pha                             ; C383  keep the remaining frame count on the stack
L_C384:
        lda     VIC_RASTER              ; C384  current raster line ...
        cmp     #$F0                    ; C387  ... is it line $F0 (240), below the visible screen?
        bne     L_C384                  ; C389  no - keep polling
L_C38B:
        cmp     VIC_RASTER              ; C38B  wait while the raster is still on line $F0 ...
        beq     L_C38B                  ; C38E  ... one whole frame has passed once it moves on
        pla                             ; C390  frame count back
        sec                             ; C391  set carry so the subtract does not borrow
        sbc     #$01                    ; C392  one frame done
        bne     L_C383                  ; C394  wait for the rest
L_C396:
        rts                             ; C396  done


; ----------------------------------------------------------------------
; waitRasterBelowScreen - spins until the raster is at line $E0 (224) or below, i.e. in the lower
; border, so the caller can rewrite the bitmap without tearing.
; In:  none
; Out: returns with A = the raster line, which is >= $E0
; Called from: main program $3CFA
; ----------------------------------------------------------------------
waitRasterBelowScreen:
        lda     VIC_RASTER              ; C397  current raster line
        cmp     #$E0                    ; C39A  below the visible screen (line 224)?
        bcc     waitRasterBelowScreen   ; C39C  not yet - keep spinning
        rts                             ; C39E  done


; ----------------------------------------------------------------------
; clearMessageLine - blanks the status bar at the bottom of the screen: saves the caller's text
; window, selects the one-row message window, clears it and restores the window.
; In:  $90FB current screen, $9001 display mode
; Out: the status line is filled with the blank glyph; the text window is restored from $92B6
; Called from: showNextQueuedMessage ($C61B), promptPressButtonWhenReady ($C86A), main program
;      $102B/$10D9/$1B96/$2DDB/$576A, $6F00 overlay $81F1/$8792
; ----------------------------------------------------------------------
clearMessageLine:
        jsr     saveTextWindow          ; C39F  stash the caller's text window
        jsr     setMessageLineWindow    ; C3A2  switch to the one-row status bar
        jsr     clearToEndOfLine        ; C3A5  blank it
        jmp     restoreTextWindow       ; C3A8  and put the caller's window back


; ----------------------------------------------------------------------
; setMessageLineWindow - defines the text window as the one-row status bar: left column 2, width
; $24 (36 columns, i.e. columns 2-37), cursor at its left end, row $17 (23) - or $16 (22) on the
; console/main-menu screen, where the bar sits one row higher.  Text and pad colour become 0 in
; multicolour mode (colour RAM nibble) and 7 in hires mode (video matrix byte).
; In:  $90FB current screen (0 map, 1 missile, 2 drone, 3 console/menu), $9001 display mode
; Out: zp_3A-zp_3D, zp_3F, zp_40 and zp_42 set; cell pointers recomputed
; Called from: clearMessageLine ($C3A2), printOnMessageLine ($C3EC), drawStatusScoreLine ($C6B9)
; ----------------------------------------------------------------------
setMessageLineWindow:
        lda     #$24                    ; C3AB  36 columns wide ...
        sta     textWindowWidth         ; C3AD  = the window width
        lda     #$02                    ; C3AF  ... starting at column 2, so the bar covers columns 2-37
        sta     textWindowLeft          ; C3B1  = the window's left edge
        lda     #$00                    ; C3B3  cursor to the left end of the bar
        sta     textCursorCol           ; C3B5  = the cursor column
        ldx     #$17                    ; C3B7  row 23 on the battlefield, missile and drone screens
        ldy     currentScreen           ; C3B9  which screen is on show?
        cpy     #$03                    ; C3BC  3 = the console / main menu screen
        bcc     L_C3C2                  ; C3BE  no - keep row 23
D_C3C0:
        ldx     #$16                    ; C3C0  on the console screen the bar sits one row higher, on row 22
L_C3C2:
        stx     textWindowBottom        ; C3C2  the window is that single row ...
        stx     textCursorRow           ; C3C4  ... and the cursor starts on it
        ldy     #$07                    ; C3C6  colour 7 for the hires screens
        lda     vicCtrl2Shadow          ; C3C8  shadow of $D016 ...
        and     #$10                    ; C3CB  ... bit 4 = multicolour bitmap?
        beq     L_C3D1                  ; C3CD  hires - keep 7
        ldy     #$00                    ; C3CF  multicolour: colour RAM nibble 0
L_C3D1:
        sty     textColour              ; C3D1  text colour ...
        sty     textPadColour           ; C3D3  ... and the colour of the padding blanks
        jmp     updateCursorPointers    ; C3D5  compute the cell pointers of the first cell of the bar


; ----------------------------------------------------------------------
; showMessageString - prints the bit-7-terminated string A/Y centred in the 36-column status bar and
; restores the caller's text window.  The standard 'put this text in the status bar' call, used for
; disk errors, queued messages and the clock line.
; In:  A/Y = string pointer
; Out: the status line is rewritten; the text window is restored
; Called from: updateStatusMessage ($C53C), showMessageById ($C5EB), promptPressButtonWhenReady
;      ($C85F), main program $0816/$1023
; ----------------------------------------------------------------------
showMessageString:
        ldx     #$24                    ; C3D8  centre across all 36 columns of the bar
        jsr     printOnMessageLine      ; C3DA  draw the string there
        jmp     restoreTextWindow       ; C3DD  and put the caller's window back


; ----------------------------------------------------------------------
; printOnMessageLine - worker behind showMessageString: remembers the string pointer in the operands
; at $C3F0/$C3F2, takes the centring width from X, saves the text window, selects the status bar,
; prints the string and pads the rest of the line.  It leaves the window set to the status bar, so the
; caller must restore it.
; In:  A/Y = string pointer, X = centring width (0 = left aligned)
; Out: the status line is drawn; the text window is still the message window on return
; Called from: showMessageString ($C3DA), drawTypedChatLine ($C4E0)
; ----------------------------------------------------------------------
printOnMessageLine:
        sta     L_C3EF+1                ; C3E0  patch the pointer low byte into the LDA at $C3EF
        sty     L_C3F1+1                ; C3E3  and the high byte into the LDY at $C3F1
        stx     textCentreWidth         ; C3E6  X = the field width to centre in, 0 = left aligned
        jsr     saveTextWindow          ; C3E9  stash the caller's text window
        jsr     setMessageLineWindow    ; C3EC  switch to the status bar
L_C3EF:
        lda     #$FF                    ; C3EF  patched: string pointer low
L_C3F1:
        ldy     #$FF                    ; C3F1  patched: string pointer high
        jsr     printString             ; C3F3  print it (centred if a width was given)
        jmp     clearToEndOfLine        ; C3F6  pad the rest of the bar with blanks


; ----------------------------------------------------------------------
; advanceFillPointer - loop helper of the block fill/clear routines: compares the 16-bit pointer
; zp_48/zp_49 with the last address zp_4A/zp_4B (carry set once the pointer has reached it), then
; steps the pointer and a 16-bit counter that nothing ever reads.  Callers store a byte and loop while
; the carry is clear.
; In:  zp_48/zp_49 = current address, zp_4A/zp_4B = last address
; Out: C = 1 when the pointer had reached the limit; zp_48/zp_49 and zp_4E/zp_4F incremented
; Called from: memory clear $0C44, the setup block at $0333, modem driver $EC16/$EC8B
; ----------------------------------------------------------------------
advanceFillPointer:
        lda     mapCellPtr              ; C3F9  16-bit compare of the fill pointer ...
        cmp     fillEndPtr              ; C3FB  low bytes ...
        lda     mapCellPtrHi            ; C3FD  ... against the last address to fill ...
        sbc     fillEndPtrHi            ; C3FF  ... leaving carry set once it has been reached
        inc     mapCellPtr              ; C401  step to the next byte ...
        bne     L_C407                  ; C403  no carry into the high byte
        inc     mapCellPtrHi            ; C405  ... across a page boundary if need be
L_C407:
        inc     fillCounter             ; C407  count the bytes filled ...
        bne     L_C40D                  ; C409  no carry into the high byte
        inc     fillCounterHi           ; C40B  ... in a 16-bit counter nothing ever reads
L_C40D:
        rts                             ; C40D  carry tells the caller whether to stop


; ----------------------------------------------------------------------
; restoreTextWindow - copies the 12 saved bytes at $92B6 back into zp_39-zp_44 (window geometry,
; colours, cursor position and mode flags) and recomputes the cell pointers.  Counterpart of
; saveTextWindow; there is only one save slot, so the pair cannot be nested.
; In:  $92B6-$92C1
; Out: zp_39-zp_44 restored, zp_B4-zp_B7 recomputed
; Called from: clearMessageLine ($C3A8), showMessageString ($C3DD), drawTypedChatLine ($C4EC),
;      handlePauseKey ($C761/$C79A), drawStatusScoreLine ($C6E2), main program $173C/$19D3/$19DE
; ----------------------------------------------------------------------
restoreTextWindow:
        ldx     #$0B                    ; C40E  12 bytes: zp_39 up to zp_44
L_C410:
        lda     savedTextWindow,x       ; C410  byte of the saved window state ...
        sta     textWindowTop,x         ; C413  ... back into the zero-page text variables
        dex                             ; C415  next byte
        bpl     L_C410                  ; C416  all 12 of them
        jmp     updateCursorPointers    ; C418  recompute the cell pointers for the restored cursor


; ----------------------------------------------------------------------
; saveTextWindow - saves zp_39-zp_44 (the whole text engine state) into the single slot at $92B6 and
; then hands the caller a full-screen window.  Used before drawing the status bar, the repair window
; and anything else that must not disturb the caller's window.
; In:  zp_39-zp_44
; Out: $92B6-$92C1 = the saved state; the window is reset to 0,0,25,40
; Called from: clearMessageLine ($C39F), printOnMessageLine ($C3E9), drawStatusScoreLine ($C6B6),
;      handlePauseKey ($C75B/$C791), main program $16FF/$19E1
; ----------------------------------------------------------------------
saveTextWindow:
        ldx     #$0B                    ; C41B  12 bytes: zp_39 up to zp_44
L_C41D:
        lda     textWindowTop,x         ; C41D  text engine variable ...
        sta     savedTextWindow,x       ; C41F  ... into the save slot
        dex                             ; C422  next byte
        bpl     L_C41D                  ; C423  all 12 of them
        jmp     setFullScreenWindow     ; C425  leave the caller with a full-screen window


; ----------------------------------------------------------------------
; splitDecimalDigits - splits the byte in A into three decimal digits by repeated subtraction: units
; into decimalOnes, tens into decimalTens, hundreds into decimalHundreds.  The digits are used as
; indices for drawTallGlyph by the drone fuel gauge and the missile counter.
; In:  A = value 0-255
; Out: decimalHundreds/decimalTens/decimalOnes ($C44E-$C450); X clobbered
; Called from: $6F00 overlay $6F9C (drone fuel) and $7ABD (missile count)
; ----------------------------------------------------------------------
splitDecimalDigits:
        ldx     #$FF                    ; C428  X will count the tens
        sec                             ; C42A  no borrow into the first subtraction
L_C42B:
        inx                             ; C42B  one more ten ...
        sbc     #$0A                    ; C42C  ... subtract it
        bcs     L_C42B                  ; C42E  keep going until it borrows
        adc     #$0A                    ; C430  put the last ten back: A = value mod 10
        sta     decimalOnes             ; C432  the units digit
        txa                             ; C435  A = value / 10
        ldx     #$FF                    ; C436  X will count the hundreds
        sec                             ; C438  no borrow into the first subtraction
L_C439:
        inx                             ; C439  one more hundred ...
        sbc     #$0A                    ; C43A  ... subtract ten from the tens value
        bcs     L_C439                  ; C43C  keep going until it borrows
        adc     #$0A                    ; C43E  put the last ten back: A = tens digit
        sta     decimalTens             ; C440  the tens digit
        txa                             ; C443  A = value / 100
        bne     L_C44A                  ; C444  dead leading-zero test: ...
        lda     #$00                    ; C446  ... A is already 0 on this path ...
        beq     L_C44A                  ; C448  ... and both branches continue at $C44A
L_C44A:
        sta     decimalHundreds         ; C44A  the hundreds digit
        rts                             ; C44D  done


; The three digits produced by splitDecimalDigits, stored in the code page itself.
decimalHundreds:
        .byte   $00                     ; C44E  hundreds digit; read by the drone fuel display ($6FD6)
decimalTens:
        .byte   $00                     ; C44F  tens digit; read at $6FDE (fuel) and $7AD5 (missiles left)
decimalOnes:
        .byte   $00                     ; C450  units digit; read at $6FE6 (fuel) and $7ADD (missiles left)

; ----------------------------------------------------------------------
; checkUiContinue - the shared 'may this modal panel keep running' test used by the menu selector, the
; unit info panel and the group editor.  Returns A = 1 to carry on.  It returns A = 0 and arms a
; 60-frame input lockout when the end-of-game sequence has started (and we are not watching a film),
; when a function key has asked for another screen, when SPACE was typed, or when the setup-phase test
; at checkSetupUiContinue says stop; a pause or gamePhase bits 6/7 keep the panel alive.
; In:  $0B9B film flag, $92C9 end-of-game counter, zp_BE pause state, zp_B1 game phase,
;      zp_66 requested screen key, $9248 last typed key
; Out: A = 1 continue / 0 abort; $0B7D = $3C (60 frames of input lockout) on abort
; Called from: main program $21D9/$2367/$4BE4/$6079/$622B/$63CB/$6400/$64BE/$65F7
; ----------------------------------------------------------------------
checkUiContinue:
        lda     filmPlaybackMode        ; C451  watching a game film?
        bmi     L_C45F                  ; C454  yes - the film drives everything, skip the end/pause tests
        lda     gameEndCountdown        ; C456  is the end-of-game sequence running?
        bne     L_C48B                  ; C459  yes - close the panel
        lda     pauseState              ; C45B  paused or disconnected?
        bne     L_C488                  ; C45D  yes - keep the panel on screen
L_C45F:
        lda     gamePhase               ; C45F  game phase ...
        and     #$C0                    ; C461  ... bits 6/7 mark the setup and menu phases ...
        bne     L_C488                  ; C463  ... where the panel always stays up
        lda     requestedScreenKey      ; C465  has a function key asked for another screen? ($FF = none)
        bpl     L_C48B                  ; C467  yes - close the panel
        lda     lastTypedKey            ; C469  last typed key ...
        cmp     #$A0                    ; C46C  ... SPACE ($20 with bit 7 set)?
        beq     L_C48B                  ; C46E  yes - SPACE closes the panel

; ----------------------------------------------------------------------
; checkSetupUiContinue - tail of checkUiContinue, also called on its own by the map main loop during
; the setup phase: the panel stays up while the battle is running (phase 0), while this game type has
; no setup phase, or while only side 0 deploys and we are side 0; otherwise it stays up only when both
; handshake bits of gamePhase are set.
; In:  zp_B1 game phase, $92C6 setup phase mode (0 none, 1 both sides, $80 side 0 only), $0B9F side
; Out: A = 1 continue / 0 abort; $0B7D = $3C on abort
; Called from: checkUiContinue (fall through), main program $202D
; ----------------------------------------------------------------------
checkSetupUiContinue:
        lda     gamePhase               ; C470  game phase: 0 while the battle runs
        beq     L_C488                  ; C472  during the battle the panel stays up
        lda     setupPhaseMode          ; C474  does this game type have a unit setup phase?
        beq     L_C488                  ; C477  no - stay up
        bpl     L_C480                  ; C479  positive: both sides deploy their units
        lda     playerSide              ; C47B  $80 = only side 0 deploys ...
        beq     L_C488                  ; C47E  ... and we are side 0, so stay up
L_C480:
        lda     gamePhase               ; C480  game phase ...
        and     #$03                    ; C482  ... low two bits are the setup handshake ...
        cmp     #$03                    ; C484  ... both sides ready?
        bne     L_C48B                  ; C486  no - close the panel and wait
L_C488:
        lda     #$01                    ; C488  A = 1: the caller's panel may keep running
        rts                             ; C48A  back to the caller's panel loop

L_C48B:
        lda     #$3C                    ; C48B  60 frames ...
        sta     inputLockoutTimer       ; C48D  ... during which the IRQ ignores keyboard and joystick
        lda     #$00                    ; C490  A = 0: the caller must leave its panel
        rts                             ; C492  back to the caller's panel loop


; ----------------------------------------------------------------------
; serviceChatDisplay - first half of updateStatusMessage: it owns the status bar while the player is
; typing to the opponent.  A positive non-zero chatState means the line has just been sent, so the
; typed line is drawn one last time and the bar is handed back to the message system.  While typing
; (bit 7 set) holding CRSR up/down ($8B) peeks at the line the opponent is typing (message 0) and
; releasing it redraws our own; bit 6 of chatState means 'the bar no longer shows the typed buffer'.
; In both typing cases two bytes are pulled off the stack so that updateStatusMessage returns to its
; own caller and never touches the message queue.
; In:  $929C chat state (bit 7 typing, bit 6 bar dirty, bits 0-5 characters typed), $92A0 held key,
;      zp_7A pending message id
; Out: $929C updated, status bar redrawn, zp_7A/zp_7B/zp_72 updated; may discard updateStatusMessage's
;      return address
; Called from: updateStatusMessage ($C4F0)
; ----------------------------------------------------------------------
serviceChatDisplay:
        lda     chatState               ; C493  chat state
        beq     L_C4D9                  ; C496  no chat activity - carry on with the ordinary message handling
        bmi     L_C4B1                  ; C498  bit 7 set: still typing
        lda     #$00                    ; C49A  the line has been sent ...
        sta     chatState               ; C49C  ... so leave typing mode
        jsr     drawTypedChatLine       ; C49F  show the finished line once more
        lda     pendingMessageId        ; C4A2  the pending message id ...
        and     #$7F                    ; C4A4  ... without its 'consumed' bit ...
        sta     shownMessageId          ; C4A6  ... is what the bar now shows
        lda     #$81                    ; C4A8  $81 = bit 7 'nothing pending', but not the fully idle value $80
        sta     pendingMessageId        ; C4AA  so the message system sees nothing pending
        lda     #$00                    ; C4AC  expire the display timer ...
        sta     messageTimer            ; C4AE  ... so the next queued message can take the bar
        rts                             ; C4B0  return normally: updateStatusMessage carries on with the queue

L_C4B1:
        ldy     rawKeyCode              ; C4B1  which key is being held?
        cpy     #$8B                    ; C4B4  CRSR up/down = 'show me the other side's line'
        bne     L_C4C5                  ; C4B6  not held - make sure our own line is on the bar
        ora     #$40                    ; C4B8  remember that the bar no longer shows the typed buffer (A is still chatState)
        sta     chatState               ; C4BA  bit 6 = the bar is not showing the typed line
        lda     #$00                    ; C4BD  message 0 = the incoming chat line at $0548
        jsr     showMessageById         ; C4BF  print it on the bar
        jmp     L_C4D7                  ; C4C2  and take the bar for the chat

L_C4C5:
        lda     chatState               ; C4C5  chat state ...
        and     #$40                    ; C4C8  ... bit 6: is the bar showing something else?
        beq     L_C4D7                  ; C4CA  no - the typed line is already there
        lda     chatState               ; C4CC  clear the flag ...
        and     #$BF                    ; C4CF  clear bit 6 ...
        sta     chatState               ; C4D1  ... the bar shows the typed line again
        jsr     drawTypedChatLine       ; C4D4  ... and put the typed line back on the bar
L_C4D7:
        pla                             ; C4D7  discard updateStatusMessage's return address ...
        pla                             ; C4D8  ... so that while typing the bar belongs to the chat alone
L_C4D9:
        rts                             ; C4D9  nothing to do


; ----------------------------------------------------------------------
; drawTypedChatLine - draws the line being typed to the opponent: the bit-7-terminated buffer at
; $9278 is printed left aligned on the status bar, then the colour is set to $11 and one blank is
; printed as the typing cursor (on the hires menu screen the video-matrix byte $11 paints that whole
; cell white, so the blank shows up as a solid block).
; In:  $9278 typed line buffer (terminated with $9E, which prints nothing)
; Out: the status bar shows the typed line with a cursor; zp_3D = $11; the text window is restored
; Called from: serviceChatDisplay ($C49F, $C4D4)
; ----------------------------------------------------------------------
drawTypedChatLine:
        lda     #$78                    ; C4DA  the typed line buffer at $9278 ...
        ldy     #$92                    ; C4DC  $9278, high byte
        ldx     #$00                    ; C4DE  ... printed left aligned, no centring
        jsr     printOnMessageLine      ; C4E0  draw it on the status bar
        lda     #$11                    ; C4E3  $11 = white in both nibbles ...
        sta     textColour              ; C4E5  ... so the next cell reads as a solid block
        lda     #$A0                    ; C4E7  a space (bit 7 set, printChar strips it)
        jsr     printChar               ; C4E9  print it as the typing cursor
        jmp     restoreTextWindow       ; C4EC  put the caller's text window back


; ----------------------------------------------------------------------
; updateStatusMessage - per-frame service of the status bar, called from every polling loop.  It runs
; the chat display and the message-review keys, returns while the current message still has time on
; the clock, and otherwise retires that message (showNextQueuedMessage), takes the pending id, prints
; its string from the pointer tables at $0500/$0524, restarts the $3C-tick timer and plays the
; message's sound.  With nothing pending it either shows an arrived chat line or, in the last quarter
; of a battle, refreshes the CLICKS/US/THEM line whenever the game clock has ticked.
; In:  zp_72 timer, zp_7A pending id, zp_7B shown id, $9277 chat length, $92FA last-quarter flag,
;      zp_B1 phase, $91CB game clock, $9208 last drawn clock, tables $0500/$0524/$0612
; Out: status bar updated, zp_72 = $3C, zp_7A marked consumed, $9208 = clock; the sound-suppress
;      operand at $C54B is rewritten
; Called from: main program $0CCA/$1B9A/$1BDC/$40EB/$4131/$4DA9/$5716/$573E
; ----------------------------------------------------------------------
updateStatusMessage:
        nop                             ; C4EF  spare byte; nothing patches it
        jsr     serviceChatDisplay      ; C4F0  chat first - while typing it pops our return address and takes the bar
        jsr     handleMessageReviewKeys ; C4F3  CRSR keys: message scrollback and the score line
        lda     messageTimer            ; C4F6  how long has the message on the bar still got?
        beq     L_C4FB                  ; C4F8  expired - time for the next one
L_C4FA:
        rts                             ; C4FA  still on display: leave the bar alone

L_C4FB:
        ldx     shownMessageId          ; C4FB  the message currently on the bar
        bmi     L_C502                  ; C4FD  bit 7 set: the bar is free, nothing to retire
        jsr     showNextQueuedMessage   ; C4FF  retire it and pick the next one out of the queue
L_C502:
        ldx     pendingMessageId        ; C502  the id the queue handed us ...
        stx     shownMessageId          ; C504  ... is now the one on display
        bpl     L_C536                  ; C506  a real message id (bit 7 clear) - print it
        lda     incomingChatLength      ; C508  have chat characters arrived from the opponent?
        bne     L_C524                  ; C50B  yes - the bar shows the chat line
        lda     lastQuarterFlag         ; C50D  $92FA bit 7: has the last quarter started?
        bpl     L_C4FA                  ; C510  no - leave the bar blank
        lda     gamePhase               ; C512  game phase: only during the battle ...
        bne     L_C4FA                  ; C514  ... is the clock/score line drawn
        lda     gameClock               ; C516  the game clock (clicks left) ...
        cmp     lastShownClock          ; C519  ... against the value last drawn
        beq     L_C4FA                  ; C51C  unchanged - nothing to redraw
        sta     lastShownClock          ; C51E  remember the clock we are about to draw
        jmp     drawStatusScoreLine     ; C521  redraw '  CLICKS:n   US:n   THEM:n'

L_C524:
        bpl     L_C531                  ; C524  positive: characters are still arriving, keep the line live
        lda     pendingMessageId        ; C526  the incoming line is complete: the bar keeps ...
        and     #$7F                    ; C528  ... the last id ...
        sta     shownMessageId          ; C52A  ... as the one on display
        lda     #$00                    ; C52C  the chat text has been taken ...
        sta     incomingChatLength      ; C52E  ... so clear the incoming counter
L_C531:
        stx     L_C54A+1                ; C531  X is negative here, which suppresses the sound at $C54A
        ldx     #$00                    ; C534  message 0 = the chat line, whose text pointer is $0548
L_C536:
        lda     messagePtrLoTable,x     ; C536  message text pointer, low byte ($0500 + id)
        ldy     messagePtrHiTable,x     ; C539  message text pointer, high byte ($0524 + id)
        jsr     showMessageString       ; C53C  print it centred on the status bar
        lda     #$3C                    ; C53F  $3C ticks (the timer counts down every 4th frame)
        sta     messageTimer            ; C541  start the display timer
        lda     pendingMessageId        ; C543  the id just printed ...
        tay                             ; C545  ... in Y for the sound look-up
        ora     #$80                    ; C546  set bit 7 = consumed, nothing pending any more
        sta     pendingMessageId        ; C548  store the marked id back
L_C54A:
        lda     #$04                    ; C54A  patched flag: negative = play no sound for this message
        bmi     L_C551                  ; C54C  suppressed
        jsr     playMessageSound        ; C54E  play the sound this message id is given in $0612
L_C551:
        lda     #$04                    ; C551  re-arm the flag positive ...
        sta     L_C54A+1                ; C553  ... so the next message sounds again ($04 is just a positive value)
        rts                             ; C556  done for this frame


; ----------------------------------------------------------------------
; playMessageSound - plays the sound effect assigned to message id Y: the sound number comes from the
; per-message table at $0612 and a negative entry ($80) means the message is silent.
; In:  Y = message id 0-$23, table $0612
; Out: the sound is queued through playSound ($C89A)
; Called from: updateStatusMessage ($C54E), showStatusMessage ($C701)
; ----------------------------------------------------------------------
playMessageSound:
        lda     messageSoundTable,y     ; C557  sound id assigned to this message
        bmi     L_C55F                  ; C55A  $80 = a silent message
        jsr     playSound               ; C55C  queue the sound effect
L_C55F:
        rts                             ; C55F  done


; ----------------------------------------------------------------------
; queueMessageUnlessFilm - queues message id A like queueMessage, but drops it silently while a game
; film is being replayed.  Used for the combat messages ('UNIT RECYCLED', 'COMCEN STUNNED!', ...).
; In:  A = message id, $0B9B bit 7 = film playback
; Out: the message is queued, or ignored during film playback
; Called from: main program $36F1/$3B3D/$3EA5/$3F8E
; ----------------------------------------------------------------------
queueMessageUnlessFilm:
        bit     filmPlaybackMode        ; C560  is a game film being replayed?
        bpl     queueMessage            ; C563  no - queue the message normally
        rts                             ; C565  during a replay combat messages are dropped


; ----------------------------------------------------------------------
; queueMessage - inserts message id A into the 8-entry ring at $9249, keeping it sorted so that higher
; ids are shown first: it claims the slot after the head, scans from the tail for the first entry that
; is not larger than A, shifts the entries in between one slot forward and drops the new id in.  A
; full ring silently discards the message.  If the insert lands on the tail the display timer is
; zeroed so the message appears at once, and a status bar that was free is marked busy so that
; updateStatusMessage pops the queue on its next pass.  Y is preserved.
; In:  A = message id, $9249-$9250 ring, $9251 head (write index), $9252 tail (read index)
; Out: the ring is updated; zp_72 and zp_7B possibly updated; operands at $C5B0, $C5B4 and $C5BB
;      are rewritten
; Called from: queueMessageUnlessFilm ($C563), handlePauseKey ($C730), showWaitingForOpponent
;      ($C7D1), ~14 sites in the main program, the $6F00 overlays, $0743 and the modem driver
; ----------------------------------------------------------------------
queueMessage:
        sta     L_C5B3+1                ; C566  patch the new id into the LDA at $C5B3
        sty     L_C5BA+1                ; C569  preserve Y in the LDY operand at $C5BA
        ldy     messageQueueHead        ; C56C  head = the slot after the last queued message
        jsr     incMessageIndex         ; C56F  one on, wrapping at 8
        cpy     messageQueueTail        ; C572  would that run into the tail, i.e. is the ring full?
        bne     L_C57B                  ; C575  no - there is room
        lda     #$05                    ; C577  (A is never used again - leftover)
        bne     L_C5BA                  ; C579  ring full: drop this message
L_C57B:
        sty     messageQueueHead        ; C57B  claim the new slot as the head
        ldy     messageQueueTail        ; C57E  start at the entry that will be shown next

; ----------------------------------------------------------------------
; Find the insertion slot: the queue runs from the tail to the head in descending id order, so the new
; id goes in front of the first entry that is not larger than it.
; ----------------------------------------------------------------------
L_C581:
        cmp     messageQueue,y          ; C581  compare the new id with this entry
        bcs     L_C591                  ; C584  new id is the larger - insert here
        jsr     incMessageIndex         ; C586  next entry
        cpy     messageQueueHead        ; C589  reached the head?
        bne     L_C581                  ; C58C  no - keep looking
        jsr     decMessageIndex         ; C58E  nothing smaller found: append at the end of the queue
L_C591:
        sty     L_C5AF+1                ; C591  patch the insertion slot into the CPY at $C5AF
        cpy     messageQueueTail        ; C594  is the new message going to the front of the queue?
        bne     L_C59D                  ; C597  no
        lda     #$00                    ; C599  then expire the display timer ...
        sta     messageTimer            ; C59B  ... so it appears as soon as possible
L_C59D:
        ldy     messageQueueHead        ; C59D  start at the head

; ----------------------------------------------------------------------
; Open the insertion slot: walk backwards from the head, copying each entry one slot forward, until
; the insertion slot is reached.
; ----------------------------------------------------------------------
L_C5A0:
        jsr     decMessageIndex         ; C5A0  the entry before it ...
        lda     messageQueue,y          ; C5A3  ... is read ...
        jsr     incMessageIndex         ; C5A6  step forward again ...
        sta     messageQueue,y          ; C5A9  ... and written one slot forward
        jsr     decMessageIndex         ; C5AC  step back
L_C5AF:
        cpy     #$FF                    ; C5AF  patched: the insertion slot
        bne     L_C5A0                  ; C5B1  keep shifting until it is free
L_C5B3:
        lda     #$FF                    ; C5B3  patched: the new message id
        sta     messageQueue,y          ; C5B5  into the slot that was opened for it
        lda     #$0C                    ; C5B8  (A is never used again - leftover)
L_C5BA:
        ldy     #$FF                    ; C5BA  patched: the caller's Y
        lda     shownMessageId          ; C5BC  what is on the status bar?
        bpl     L_C5C8                  ; C5BE  a message is on display; it will pop the queue when its timer expires
        and     #$7F                    ; C5C0  the bar is free: clear that bit ...
        sta     shownMessageId          ; C5C2  ... so updateStatusMessage pops the queue on its next pass
        bne     L_C5C8                  ; C5C4  was the bar completely idle ($80)?
        sta     messageTimer            ; C5C6  then show the new message immediately
L_C5C8:
        rts                             ; C5C8  done


; ----------------------------------------------------------------------
; decMessageIndex - steps the message-queue index in Y back one slot, wrapping from 0 to 7.
; In:  Y = ring index 0-7
; Out: Y = (Y - 1) mod 8
; Called from: queueMessage ($C58E/$C5A0/$C5AC), handleMessageReviewKeys ($C66A)
; ----------------------------------------------------------------------
decMessageIndex:
        dey                             ; C5C9  one slot back
        bpl     L_C5CE                  ; C5CA  still inside the ring
        ldy     #$07                    ; C5CC  wrapped past 0: the ring has 8 slots
L_C5CE:
        rts                             ; C5CE  Y = the previous slot


; ----------------------------------------------------------------------
; incMessageIndex - steps the message-queue index in Y on one slot, wrapping from 7 to 0.
; In:  Y = ring index 0-7
; Out: Y = (Y + 1) mod 8
; Called from: queueMessage ($C56F/$C586/$C5A6), showNextQueuedMessage ($C621)
; ----------------------------------------------------------------------
incMessageIndex:
        iny                             ; C5CF  one slot on
        cpy     #$08                    ; C5D0  the ring has 8 slots
        bcc     L_C5D6                  ; C5D2  still inside it
        ldy     #$00                    ; C5D4  wrap back to the first slot
L_C5D6:
        rts                             ; C5D6  Y = the next slot


; ----------------------------------------------------------------------
; redrawStatusMessage - puts the status bar back after a screen has been rebuilt: the message that was
; on display (its id with the 'bar free' bit masked off), or message 0 (the chat line) when chat text
; is buffered, or else the next message from the queue.
; In:  zp_7B shown message id, $9277 incoming chat length
; Out: the status bar is redrawn
; Called from: main program $1562/$268C, $6F00 overlays $7131/$7A68
; ----------------------------------------------------------------------
redrawStatusMessage:
        lda     shownMessageId          ; C5D7  the message that was on the bar
        bpl     L_C5E2                  ; C5D9  a real id - draw it again
        lda     #$00                    ; C5DB  otherwise consider message 0, the chat line
        ldx     incomingChatLength      ; C5DD  is any chat text buffered?
        beq     showNextQueuedMessage   ; C5E0  no - take the next message from the queue instead
L_C5E2:
        and     #$7F                    ; C5E2  strip the 'bar is free' bit to get the plain id

; ----------------------------------------------------------------------
; showMessageById - looks message id A up in the pointer tables at $0500 (low) / $0524 (high) and
; tail-calls showMessageString, so the text appears centred on the status bar straight away without
; going through the queue.
; In:  A = message id 0-$23
; Out: the status bar is redrawn; X = the message id
; Called from: serviceChatDisplay ($C4BF), redrawStatusMessage (fall through at $C5E2 and the branch
;      at $C619), showStatusMessageNoSound ($C707)
; ----------------------------------------------------------------------
showMessageById:
        tax                             ; C5E4  X = message id
        lda     messagePtrLoTable,x     ; C5E5  text pointer low byte, 36 messages
        ldy     messagePtrHiTable,x     ; C5E8  text pointer high byte
        jmp     showMessageString       ; C5EB  print it centred on the status bar


; ----------------------------------------------------------------------
; showNextQueuedMessage - chooses what the status line should show next and leaves the answer in
; pendingMessageId zp_7A; the actual printing is done by updateStatusMessage on its next pass.  If the
; 8-entry message ring $9249 is not empty it pops the entry at the read index.  If the ring is empty
; it either falls back to the chat line (message id 0) when one is waiting, or goes idle: zp_7A = $80,
; the chat line is blanked, and persistentMessageId $92A1 then decides - negative redraws the
; CLICKS/US/THEM score line, zero blanks the status line, anything else is printed as a message id.
; In:  messageQueueTail $9252 (read index), messageQueueHead $9251 (write index), messageQueue $9249,
;     pendingMessageId zp_7A, chatMessageLine $0548 (marker byte of message 0), persistentMessageId
;     $92A1
; Out: zp_7A = the next message id or $80, $9252 advanced, $0548 possibly blanked, the sound-suppress
;     operand L_C54A+1 possibly set negative; the status line may be redrawn; A/Y clobbered
; Called from: updateStatusMessage ($C4FF), redrawStatusMessage ($C5E0 falls in), resetMessageQueue
;     ($C6FC)
; ----------------------------------------------------------------------
showNextQueuedMessage:
        ldy     messageQueueTail        ; C5EE  read index of the 8-entry message ring
        cpy     messageQueueHead        ; C5F1  compare it with the write index
        bne     popQueuedMessage        ; C5F4  differ = at least one message is queued, go and pop it
        lda     #$80                    ; C5F6  $80 = the 'nothing pending' marker of pendingMessageId
        cmp     pendingMessageId        ; C5F8  is the status line already idle?
        beq     statusLineGoIdle        ; C5FA  yes - keep it idle (A is already the $80 that gets stored there)
        ldy     chatMessageLine         ; C5FC  marker byte of message 0 at $0548: $1E = chat text present, $9E = empty
        bmi     statusLineGoIdle        ; C5FF  bit 7 set ($9E) = no chat line waiting, so go idle
        lda     #$00                    ; C601  message id 0 = the chat line at $0548
        sta     pendingMessageId        ; C603  show the chat line next
        ldy     #$FF                    ; C605  $FF = negative
        sty     L_C54A+1                ; C607  patch the 'lda #$04' operand at $C54A in updateStatusMessage negative so this message is printed without its sound
        bmi     storePendingMessageId   ; C60A  always taken (Y = $FF); A is still 0, so the store at $C627 just repeats it

; ----------------------------------------------------------------------
; Queue empty and no chat line: park the status line.  A = $80 here.
; ----------------------------------------------------------------------
statusLineGoIdle:
        sta     pendingMessageId        ; C60C  A = $80: nothing is pending any more
        jsr     clearChatStatusLine     ; C60E  make message 0 print as an empty line ($0548 = $9E)
        lda     persistentMessageId     ; C611  $92A1, the message the status line falls back to when the ring drains
        bpl     showPersistentMessage   ; C614  positive = a plain message id (0 = blank line)
        jmp     drawStatusScoreLine     ; C616  negative = show the CLICKS / US / THEM score line instead

showPersistentMessage:
        bne     showMessageById         ; C619  non-zero = print that message id right now
        jmp     clearMessageLine        ; C61B  zero = simply blank the status line


; ----------------------------------------------------------------------
; Queue not empty: pop the entry the read index points at.
; ----------------------------------------------------------------------
popQueuedMessage:
        lda     messageQueue,y          ; C61E  take the oldest id out of the ring
        jsr     incMessageIndex         ; C621  advance the read index modulo 8
        sty     messageQueueTail        ; C624  store the new read index
storePendingMessageId:
        sta     pendingMessageId        ; C627  hand the id to updateStatusMessage
        rts                             ; C629  the caller prints it, this routine only selects


; ----------------------------------------------------------------------
; clearChatStatusLine - empties message slot 0 (the chat line, text at $0548) by storing $9E in its
; first byte.  printChar throws away every code below $20, and $9E is $1E with the string-terminator
; bit 7 set, so a string that starts with it prints absolutely nothing: the status line comes out
; blank.
; In:  none
; Out: chatMessageLine $0548 = $9E, A = $9E
; Called from: showNextQueuedMessage ($C60E) and by JMP from the game's start-up init ($0C63)
; ----------------------------------------------------------------------
clearChatStatusLine:
        lda     #$9E                    ; C62A  $1E | $80: a control code (nothing is drawn) that also carries the end-of-string bit
        sta     chatMessageLine         ; C62C  message slot 0 now terminates on its first byte = an empty status line
        rts                             ; C62F  A = $9E on exit, which the start-up init at $0C63 relies on


; messageReviewIndex - the single state byte of the message-history scrollback. bit 7 = review mode
; active, bits 0-6 = the real messageQueueTail saved when review started, so it can be put back when
; the player stops scrolling.  0 = not reviewing.
messageReviewIndex:
        .byte   $00                     ; C630  .  review state: $00 = off, $80|savedReadIndex = scrolling back through old messages

; ----------------------------------------------------------------------
; handleMessageReviewKeys - per-frame keyboard hook of the status-message service.  CRSR up/down (key
; code $8B) enters and then steps the message history: the real ring read index is saved in
; messageReviewIndex with bit 7 set and each further press walks one entry backwards through the
; 8-entry ring, showing it at once (messageTimer = 0, sound suppressed).  When the player stops
; pressing, the saved read index is restored and review mode ends.  CRSR left/right ($8E) is consumed
; and, during the battle only, pops up the CLICKS/US/THEM score line for 60 ticks by falling into
; drawStatusScoreLine.
; In:  messageReviewIndex $C630, lastTypedKey $9248, messageTimer zp_72, messageQueue/Head/Tail
;     $9249/$9251/$9252, pendingMessageId zp_7A, shownMessageId zp_7B, gamePhase zp_B1
; Out: $C630, $9252, $9248 = $FF (key consumed), zp_7A, zp_7B, zp_72, chatMessageLine $0548 = $1E, the
;     sound-suppress operand L_C54A+1 = $80, inputLockoutTimer $0B7D; may draw the score line
; Called from: updateStatusMessage ($C4F3), i.e. from every polling loop in the game
; ----------------------------------------------------------------------
handleMessageReviewKeys:
        lda     messageReviewIndex      ; C631  review state byte
        bmi     messageReviewActive     ; C634  bit 7 set = already scrolling back, skip the entry test
        lda     lastTypedKey            ; C636  last key the IRQ decoded ($FF = none)
        cmp     #$8B                    ; C639  $8B = CRSR up/down, the scrollback key
        bne     checkScoreLineKey       ; C63B  not that key - try the score-line key instead
        lda     messageQueueTail        ; C63D  remember where the ring read index really is
        ora     #$80                    ; C640  bit 7 marks review mode as active
        sta     messageReviewIndex      ; C642  save it so the ring can be rewound afterwards
        bmi     stepMessageReviewBack   ; C645  always taken (bit 7 was just set): show the previous message

; ----------------------------------------------------------------------
; Review mode is running.  Wait until the message on screen has had its minimum airtime ($3C down to
; $23), then either step further back or leave review mode.
; ----------------------------------------------------------------------
messageReviewActive:
        lda     messageTimer            ; C647  ticks left on the message currently displayed
        cmp     #$23                    ; C649  $3C is loaded on display; below $23 means it has been up for about 25 ticks (100 frames)
        bcs     messageReviewDone       ; C64B  still fresh - do nothing, which rate-limits the scrollback and delays the exit
        lda     lastTypedKey            ; C64D  has CRSR up/down been pressed again?
        cmp     #$8B                    ; C650  $8B = CRSR up/down
        beq     stepMessageReviewBack   ; C652  yes - step one more entry backwards
        lda     messageReviewIndex      ; C654  no further press: recover the saved read index
        and     #$7F                    ; C657  strip the 'review active' bit 7
        sta     messageQueueTail        ; C659  put the ring read index back where it was
        sta     messageReviewIndex      ; C65C  and clear bit 7, leaving review mode
        jmp     messageReviewDone       ; C65F  done for this frame


; ----------------------------------------------------------------------
; Step one entry backwards through the ring and display it immediately.
; ----------------------------------------------------------------------
stepMessageReviewBack:
        ldy     #$FF                    ; C662  $FF = 'no key'
        sty     lastTypedKey            ; C664  consume the key so it is not seen again next frame
        ldy     messageQueueTail        ; C667  current ring read index
        jsr     decMessageIndex         ; C66A  step one entry backwards modulo 8
        cpy     messageQueueHead        ; C66D  have we walked back onto the write index?
        beq     beepNoOlderMessage      ; C670  yes - there is no older message, beep
        lda     messageQueue,y          ; C672  id stored in that ring slot
        bpl     showReviewedMessage     ; C675  a real id (bit 7 clear) - show it
beepNoOlderMessage:
        lda     #$03                    ; C677  sound 3 = the error/refusal beep
        jmp     playSound               ; C679  tail call: beep and return

showReviewedMessage:
        lda     #$1E                    ; C67C  $1E = 'message 0 has text'; the code itself prints nothing
        sta     chatMessageLine         ; C67E  keep the chat line marked as printable so the status line returns to it when review ends
        lda     pendingMessageId        ; C681  id currently selected
        ora     #$80                    ; C683  set bit 7 = 'the status line is free'
        sta     shownMessageId          ; C685  so updateStatusMessage prints the reviewed message immediately
        sty     messageQueueTail        ; C687  the ring read index now points at the reviewed entry
        lda     messageQueue,y          ; C68A  fetch that entry again
        sta     pendingMessageId        ; C68D  and make it the message to display
        lda     #$00                    ; C68F  zero the display timer
        sta     messageTimer            ; C691  so the next updateStatusMessage pass prints it at once
        lda     #$80                    ; C693  $80 = negative
        sta     L_C54A+1                ; C695  patch the 'lda #$04' operand at $C54A so the reviewed message is shown silently
messageReviewDone:
        rts                             ; C698  nothing more to do this frame


; ----------------------------------------------------------------------
; Not the scrollback key: CRSR left/right pops the score line up during the battle.
; ----------------------------------------------------------------------
checkScoreLineKey:
        lda     lastTypedKey            ; C699  last key again
        cmp     #$8E                    ; C69C  $8E = CRSR left/right, the 'show the score' key
        bne     messageReviewDone       ; C69E  some other key - ignore it
        lda     #$FF                    ; C6A0  $FF = 'no key'
        sta     lastTypedKey            ; C6A2  consume the key
        lda     gamePhase               ; C6A5  zp_B1 is 0 only while the battle is running
        bne     messageReviewDone       ; C6A7  in setup, film or menu phases there is no score line to show
        lda     #$3C                    ; C6A9  $3C = 60
        sta     inputLockoutTimer       ; C6AB  lock the keyboard and joystick out for 60 frames
        sta     messageTimer            ; C6AE  and keep the score line up for 60 message ticks
        lda     shownMessageId          ; C6B0  id on the status line
        and     #$7F                    ; C6B2  clear bit 7 = 'the line is occupied' so the score line is not overwritten at once
        sta     shownMessageId          ; C6B4  store it back, then fall into drawStatusScoreLine

; ----------------------------------------------------------------------
; drawStatusScoreLine - redraws the bottom status line as '   CLICKS:<clock>    US:<points>
; THEM:<points>'.  The three labels are three consecutive bit-7 terminated strings at
; $065E/$0668/$066F, so after the first one printString's pointer is simply run on with
; printStringAtPointer.  sideScore is indexed by the side that was scored against, so entry
; [playerSide EOR 1] is the local player's own point total.
; In:  gameClock $91CB, playerSide $0B9F, sideScoreLo/Hi $92A2/$92A4, the label strings at $065E
; Out: status line redrawn; the caller's text window is saved to $92B6 and restored; decimalValueHigh
;     zp_7E, the digit buffer $92DA and zp_18-zp_1F are clobbered
; Called from: by JMP only - updateStatusMessage ($C521, when the clock changed while the last-quarter
;     flag $92FA is set) and handleMessageReviewKeys (fall-through at $C6B4)
; ----------------------------------------------------------------------
drawStatusScoreLine:
        jsr     saveTextWindow          ; C6B6  stash the caller's text window in $92B6 and select the full screen
        jsr     setMessageLineWindow    ; C6B9  switch to the one-row status bar window (columns 2-37 of row 22 or 23)
        lda     #$5E                    ; C6BC  low byte of '   CLICKS:' at $065E
        ldy     #$06                    ; C6BE  high byte of $065E
        jsr     printString             ; C6C0  print it, leaving the string pointer just past its terminator
        lda     gameClock               ; C6C3  $91CB, the game clock in clicks (it counts down to 0)
        ldy     #$00                    ; C6C6  middle byte 0; the high byte zp_7E was left 0 by the previous conversion
        jsr     printDecimal            ; C6C8  print it as decimal without leading zeros
        jsr     printStringAtPointer    ; C6CB  carry straight on with the next string in memory, '    US:' at $0668
        lda     playerSide              ; C6CE  $0B9F = the side this machine plays
        eor     #$01                    ; C6D1  the other side, which is the index of the points WE scored
        jsr     printPlayerPoints       ; C6D3  print our 16-bit point total
        jsr     printStringAtPointer    ; C6D6  carry on with '   THEM:' at $066F
        lda     playerSide              ; C6D9  our own side index = the points scored against us
        jsr     printPlayerPoints       ; C6DC  print the opponent's point total
        jsr     clearToEndOfLine        ; C6DF  pad the rest of the 36-column status line with the blank glyph
        jmp     restoreTextWindow       ; C6E2  restore the caller's text window and return


; ----------------------------------------------------------------------
; printPlayerPoints - prints one side's 16-bit point total as decimal.  The two point totals are
; indexed by the side that was scored against, so A = playerSide EOR 1 gives our score and A =
; playerSide gives the opponent's.
; In:  A = score index 0/1; sideScoreLo $92A2, sideScoreHi $92A4
; Out: digits printed at the cursor; X = the index, zp_18-zp_1F, zp_7E and the digit buffer $92DA
;     clobbered
; Called from: drawStatusScoreLine ($C6D3, $C6DC)
; ----------------------------------------------------------------------
printPlayerPoints:
        tax                             ; C6E5  use the side index as a table index
        lda     sideScoreLo,x           ; C6E6  low byte of that side's point total
        ldy     sideScoreHi,x           ; C6E9  high byte (printDecimal takes A = low, Y = middle, zp_7E = high)
        jmp     printDecimal            ; C6EC  tail call the decimal printer; zp_7E is left 0 by it for the next call


; ----------------------------------------------------------------------
; resetMessageQueue - throws away every queued status message: the read index is set equal to the
; write index (empty ring), the fallback message and the display timer are cleared, and it falls into
; showNextQueuedMessage so the status line is refreshed immediately.
; In:  messageQueueHead $9251
; Out: messageQueueTail $9252 = head, persistentMessageId $92A1 = 0, messageTimer zp_72 = 0, status
;     line redrawn
; Called from: a dozen places that change screen or game phase - $0AB5, $0B6A, $0CA0, $0CE1 (jmp),
;     $1BE9, $28F4, $5258 in the main program and $6F8E, $7B0F, $7DD5, $7F9E, $820B in the $6F00
;     overlays
; ----------------------------------------------------------------------
resetMessageQueue:
        lda     messageQueueHead        ; C6EF  ring write index
        sta     messageQueueTail        ; C6F2  make the read index equal to it: the queue is now empty
        lda     #$00                    ; C6F5  0
        sta     persistentMessageId     ; C6F7  no fallback message either (0 = blank status line)
        sta     messageTimer            ; C6FA  clear the display timer so the next message appears at once
        jmp     showNextQueuedMessage   ; C6FC  pick whatever should be shown now (normally a blank line)


; ----------------------------------------------------------------------
; showStatusMessage - prints message id A on the status line immediately, bypassing the queue, and
; plays the sound the per-message table $0612 assigns to it ($80 = silent).
; In:  A = message id 0-$23; messageSoundTable $0612, messagePtrLoTable/HiTable $0500/$0524
; Out: shownMessageId zp_7B = the id, the text is drawn centred in the status line, a sound request
;     may be queued
; Called from: the disk-error retry path ($082D), the end-of-game and link-status screens ($1AC4,
;     $1B81, $1BC9, $5A34, $5A52), the modem driver ($EDFE) and handlePauseKey ($C728)
; ----------------------------------------------------------------------
showStatusMessage:
        pha                             ; C6FF  keep the id for the print below
        tay                             ; C700  playMessageSound indexes its table with Y
        jsr     playMessageSound        ; C701  look the id up in $0612 and start that sound unless it is $80 = silent
        pla                             ; C704  get the message id back

; ----------------------------------------------------------------------
; showStatusMessageNoSound - the tail of showStatusMessage, also used as an entry point of its own
; when the message should be silent: records A as the message now on the status line and prints it.
; In:  A = message id
; Out: shownMessageId zp_7B = A, message printed
; Called from: by JMP from showWorkingMessage ($C858); fall-through from showStatusMessage
; ----------------------------------------------------------------------
showStatusMessageNoSound:
        sta     shownMessageId          ; C705  bit 7 clear = the status line is now busy with this id
        jmp     showMessageById         ; C707  look the id up in $0500/$0524 and draw it centred


; ----------------------------------------------------------------------
; handlePauseKey - RUN/STOP pause handling, called once per frame from updateGameFrame.  It does
; nothing unless the battle is actually running (gamePhase zp_B1 = 0 and gameEndReason $0BA8 = 0).  In
; a solo or film game RUN/STOP simply toggles bit 0 of pauseState zp_BE; while paused the persistent
; message is $19 'TIMEOUT. RUN/STOP TO RESUME.', only the menu-level link poll is serviced, and two
; PLAs throw away this routine's return address so the rest of the frame update is skipped.  In a live
; modem game the first RUN/STOP shows 'WORKING...' and sends command $8C reason 3 (request pause); the
; opponent's answer sets zp_BE, after which this routine owns the frame: it saves and clears the
; screen, then either performs a voice pause or spins on serviceCommandExchange until zp_BE clears,
; sending $8C reason 4 (request resume) on every further RUN/STOP.  On release it redraws the
; battlefield and puts the sprites back.
; In:  gamePhase zp_B1, gameEndReason $0BA8, filmPlaybackMode $0B9B, pauseState zp_BE,
;     spriteEnableShadow $9022, currentScreen $90FB, inputLockoutTimer $0B7D
; Out: zp_BE, persistentMessageId $92A1, commands $8C/3 and $8C/4 queued, screen cleared and redrawn,
;     sprite mask restored through enableSprites; may return to its caller's caller
; Called from: updateGameFrame ($40BF and $413D)
; ----------------------------------------------------------------------
handlePauseKey:
        lda     gamePhase               ; C70A  zp_B1 is 0 only during the battle itself
        bne     pauseKeyDone            ; C70C  in setup, film or menu phases RUN/STOP is not the pause key
        lda     gameEndReason           ; C70E  $0BA8 is 0 only while a game is actually running
        bne     pauseKeyDone            ; C711  the game is already over or was never started - nothing to pause
        lda     filmPlaybackMode        ; C713  $0B9B bit 7 = a game film is being replayed (also set for the solo trainer)
        bpl     livePauseEntry          ; C716  bit 7 clear = a live opponent, use the negotiated pause protocol

; ----------------------------------------------------------------------
; Solo trainer / film playback: no opponent to negotiate with, so RUN/STOP just toggles bit 0 of
; pauseState and the frame update is skipped while it is set.
; ----------------------------------------------------------------------
        jsr     consumeRunStopKey       ; C718  was RUN/STOP pressed this frame? (Z = 1 if yes, key consumed)
        bne     checkSoloPauseActive    ; C71B  not pressed - just check whether we are still paused
        lda     pauseState              ; C71D  pause flags
        eor     #$01                    ; C71F  toggle bit 0 = our own pause request
        sta     pauseState              ; C721  store the new state
        bne     L_C72B                  ; C723  non-zero = we have just paused
        sta     persistentMessageId     ; C725  A = 0: no fallback message any more
        jmp     showStatusMessage       ; C728  show message 0 (the chat line, silent) to wipe the TIMEOUT text off the status line

L_C72B:
        lda     #$19                    ; C72B  message $19 = 'TIMEOUT. RUN/STOP TO RESUME.'
        sta     persistentMessageId     ; C72D  make it the fallback message so it stays on screen
        jsr     queueMessage            ; C730  and queue it so it appears right now
checkSoloPauseActive:
        lda     pauseState              ; C733  are we paused?
        beq     pauseKeyDone            ; C735  no - let updateGameFrame carry on normally
        jsr     handleMenuSessionPacket ; C737  paused: only the menu-level link poll keeps running
        pla                             ; C73A  discard this routine's return address...
        pla                             ; C73B  ...so the RTS below returns into updateGameFrame's caller and the rest of the frame is skipped
pauseKeyDone:
        rts                             ; C73C  normal exit


; ----------------------------------------------------------------------
; Live modem game.  zp_BE is 0 until the pause has been agreed with the opponent, so the first
; RUN/STOP only sends the request; the command interpreter sets zp_BE when the answer arrives and the
; next call falls into the pause loop below.
; ----------------------------------------------------------------------
livePauseEntry:
        lda     pauseState              ; C73D  pause flags: bit 0 own pause, bit 1 opponent pause, bit 7 voice pause
        bne     enterPauseLoop          ; C73F  already in a pause - run the pause loop
        jsr     consumeRunStopKey       ; C741  was RUN/STOP pressed?
        bne     pauseKeyExit            ; C744  no - carry on with the frame
        jsr     showWorkingMessage      ; C746  put 'WORKING...' on the status line while the request is in flight
        lda     #$03                    ; C749  reason 3 = request pause
        jmp     sendPauseCommand        ; C74B  queue the 2-byte command $8C,3 for the opponent and return

enterPauseLoop:
        lda     spriteEnableShadow      ; C74E  current sprite-enable mask
        sta     restorePauseSprites+1   ; C751  patch it into the 'lda #$FF' operand at $C79D so it can be restored on resume
        lda     currentScreen           ; C754  0 battlefield, 1 missile screen, 2 drone screen, 3 console/menu
        cmp     #$03                    ; C757  screen 3 is the console/menu
        beq     choosePauseKind         ; C759  the menu screen keeps its windows
        jsr     saveTextWindow          ; C75B  save the caller's text window
        jsr     clearCurrentScreenWindows; C75E  blank the windows of the current screen (this also zeroes $9022)
        jsr     restoreTextWindow       ; C761  and restore the text window
choosePauseKind:
        lda     pauseState              ; C764  pause flags again
        bpl     pauseSpinLoop           ; C766  bit 7 clear = an ordinary pause
        jsr     voicePauseReconnect     ; C768  bit 7 = voice pause: hang up, let the players talk, then dial back
        jmp     resumeFromPause         ; C76B  and go straight to the redraw


; ----------------------------------------------------------------------
; Pause spin loop: keep the link alive and watch for RUN/STOP until the pause flags clear (which
; happens when the command interpreter receives the opponent's resume).  serviceCommandExchange also
; keeps the status line and the IRQ fed.
; ----------------------------------------------------------------------
pauseSpinLoop:
        jsr     serviceCommandExchange  ; C76E  keep exchanging command packets with the opponent while paused
        lda     pauseState              ; C771  still paused?
        beq     resumeFromPause         ; C773  no - both sides have resumed
        lda     inputLockoutTimer       ; C775  keys are ignored while this 60-frame lockout runs
        bne     pauseSpinLoop           ; C778  still locked out - keep spinning
        jsr     consumeRunStopKey       ; C77A  RUN/STOP pressed again?
        bne     pauseSpinLoop           ; C77D  no - keep spinning
        lda     pauseState              ; C77F  pass the pause flags to the message helper
        jsr     showWaitingForOpponent  ; C781  show 'WAITING FOR OPPONENT...' if the pause is ours and the link is real
        lda     #$04                    ; C784  reason 4 = request resume
        jsr     sendPauseCommand        ; C786  queue command $8C,4
        jmp     pauseSpinLoop           ; C789  and go back to spinning until the opponent agrees


; ----------------------------------------------------------------------
; Resume: the battlefield screen was blanked above, so rebuild it.
; ----------------------------------------------------------------------
resumeFromPause:
        lda     currentScreen           ; C78C  which screen are we on?
        bne     restorePauseSprites     ; C78F  only the battlefield map (screen 0) was cleared and needs rebuilding
        jsr     saveTextWindow          ; C791  save the text window around the redraw
        jsr     drawOverviewMap         ; C794  redraw the 40x40 overview map in the left panel
        jsr     drawViewport            ; C797  redraw the 7x5 tactical viewport
        jsr     restoreTextWindow       ; C79A  restore the text window
restorePauseSprites:
        lda     #$FF                    ; C79D  operand patched at $C751 with the sprite-enable mask saved before the screen was cleared
        jsr     enableSprites           ; C79F  OR the sprites back into $9022
pauseKeyExit:
        rts                             ; C7A2  back to updateGameFrame


; ----------------------------------------------------------------------
; sendPauseCommand - queues the 2-byte network command $8C (pause/session control) with A as its
; reason byte.  Reason 3 = request pause, 4 = request resume; reason 2 (ready for a new game) and 5
; (voice pause / hang up) are sent from elsewhere.
; In:  A = reason code
; Out: cmdParam0 zp_73 = A, two bytes appended to the outgoing command ring
; Called from: handlePauseKey ($C74B by JMP, $C786 by JSR)
; ----------------------------------------------------------------------
sendPauseCommand:
        sta     cmdParam0               ; C7A3  the command's single argument byte
        lda     #$8C                    ; C7A5  $8C = the pause / session-control command
        jmp     queueCmd2               ; C7A7  append [$8C, reason] to the outgoing command ring and return


; ----------------------------------------------------------------------
; consumeRunStopKey - edge test for RUN/STOP.  If the last decoded key was $93 (RUN/STOP) it clears
; the key, arms a 60-frame input lockout, plays the click sound and returns A = 0 (Z = 1).  Otherwise
; it returns with the key code still in A and Z = 0.
; In:  lastTypedKey $9248
; Out: Z = 1 when RUN/STOP was pressed; $9248 = $FF, inputLockoutTimer $0B7D = $3C, sound 2 queued
; Called from: handlePauseKey only ($C718, $C741, $C77A)
; ----------------------------------------------------------------------
consumeRunStopKey:
        lda     lastTypedKey            ; C7AA  last key code the IRQ decoded ($FF = none)
        cmp     #$93                    ; C7AD  $93 = RUN/STOP in the game's own key table
        bne     runStopKeyReturn        ; C7AF  some other key - return with Z = 0
        lda     #$3C                    ; C7B1  $3C = 60 frames
        sta     inputLockoutTimer       ; C7B3  ignore the keyboard and joystick for about a second
        lda     #$FF                    ; C7B6  $FF = 'no key'
        sta     lastTypedKey            ; C7B8  consume the key so the press is only acted on once
        lda     #$02                    ; C7BB  sound 2 = the key click
        jsr     playSound               ; C7BD  click (X and Y are preserved by playSound)
        lda     #$00                    ; C7C0  A = 0 so the caller's BNE falls through
runStopKeyReturn:
        rts                             ; C7C2  Z = 1 means 'RUN/STOP was pressed'


; ----------------------------------------------------------------------
; showWaitingForOpponent - if this is a real modem game and the pause is our own (bit 0 of the flags
; passed in A), makes message $0B 'WAITING FOR OPPONENT...' the persistent status message and queues
; it.  In a solo game there is nobody to wait for, so it does nothing.
; In:  A = pauseState flags, isSoloTrainer $0BA5 (bit 7 set = solo trainer)
; Out: persistentMessageId $92A1 = $0B and the message queued, or nothing
; Called from: handlePauseKey ($C781) and the setup overlay ($7C00)
; ----------------------------------------------------------------------
showWaitingForOpponent:
        bit     isSoloTrainer           ; C7C3  $0BA5 bit 7 = the solo trainer is the opponent
        bmi     waitingMessageDone      ; C7C6  solo game - there is no remote player to wait for
        and     #$01                    ; C7C8  bit 0 = we are the side that asked for the pause
        beq     waitingMessageDone      ; C7CA  the opponent paused us - do not claim to be waiting
        lda     #$0B                    ; C7CC  message $0B = 'WAITING FOR OPPONENT...'
        sta     persistentMessageId     ; C7CE  keep it on the line until something else replaces it
        jsr     queueMessage            ; C7D1  and queue it so it is shown now
waitingMessageDone:
        rts                             ; C7D4  shared exit of both 'nothing to say' paths


; ----------------------------------------------------------------------
; handleTextEntryKey - the one-key line editor shared by the in-game chat line and the SAVE GAME FILM
; name field.  It consumes at most one key event per call: RETURN ($8D) ends the line (bit 7 is set in
; the returned cursor position), INST/DEL ($88) backspaces, and printable codes $20-$5F are checked
; against the ASCII-to-glyph table before being stored in the 36-byte buffer at $9278, which is always
; re-terminated with $9E.  When the line is full the buffer either scrolls one character left (bit 7
; of A) or the key is refused with a beep.
; In:  A = maximum length, bit 7 = 'scroll left when full'; Y = cursor position in $9278; newKeyEvent
;     $90EA; the ASCII-to-glyph table $985E (= $987E - $20, indexed by the character code itself)
; Out: A = the accepted key code or $FF, Z = 1 when nothing was accepted; Y advanced (bit 7 set on
;     RETURN); typedLineBuffer $9278 updated; $90EA and lastTypedKey $9248 = $FF; sound 2 (accepted)
;     or 3 (refused); self-modifies the operands at $C813, $C817 and $C84F
; Called from: handleChatSend ($1387, inside the raster IRQ, A = $A3 = 35 characters with scrolling)
;     and the film-name field of the setup overlay ($8029, A = $10 = 16 characters, no scrolling)
; Note: because this runs inside the IRQ for the chat line, its calls to playSound can corrupt the
; static X/Y save slots of a playSound call in progress on the main thread.
; ----------------------------------------------------------------------
handleTextEntryKey:
        sta     textEntryScrollCheck+1  ; C7D5  keep the raw parameter (bit 7 = may scroll) in the LDX operand at $C816
        and     #$7F                    ; C7D8  strip the scroll flag
        sta     textEntryLengthCheck+1  ; C7DA  leaving the maximum length, which becomes the CPY operand at $C812
        lda     newKeyEvent             ; C7DD  key event produced this frame ($FF = none)
        cmp     #$FF                    ; C7E0  $FF = nothing typed
        beq     textEntryReturn         ; C7E2  no key - return with Z = 1 and A = $FF
        sta     textEntryReturnCode+1   ; C7E4  remember the key code as this call's return value (operand of the LDA at $C84E)
        cmp     #$8D                    ; C7E7  $8D = RETURN
        bne     textEntryCheckDelete    ; C7E9  not RETURN - test the other special keys
        tya                             ; C7EB  cursor position
        ora     #$80                    ; C7EC  set bit 7 of the cursor position as the 'line finished' flag
        tay                             ; C7EE  bit 7 set tells the caller the line has been completed
        jmp     textEntryAccepted       ; C7EF  click and finish without touching the buffer

textEntryCheckDelete:
        cmp     #$88                    ; C7F2  $88 = INST/DEL
        bne     textEntryCheckPrintable ; C7F4  not a delete either - treat it as a printable character
        cpy     #$00                    ; C7F6  already at the start of the line?
        beq     textEntryRefuse         ; C7F8  nothing to delete - refuse with a beep
        dey                             ; C7FA  step the cursor back over the last character
        jmp     terminateTypedLine      ; C7FB  re-terminate the buffer there and click

textEntryCheckPrintable:
        and     #$7F                    ; C7FE  key codes carry bit 7; strip it to get plain ASCII
        tax                             ; C800  keep the character code for the table lookup and the store
        sec                             ; C801  prepare the range test
        sbc     #$20                    ; C802  codes below space are control keys
        bcc     textEntryNoKey          ; C804  not printable - swallow it silently, no beep
        cmp     #$40                    ; C806  $20..$5F is the printable range ($40 codes)
        bcs     textEntryRefuse         ; C808  above 'Z' - refuse with a beep
        lda     titleColorRow02Cell14,x ; C80A  $985E,x = asciiToGlyphTable[code - $20]  (also graphicsData9300:asciiToGlyphTableBiased)
        cmp     #$3B                    ; C80D  $3B is the 'no glyph' entry (backslash, caret and underscore)
        beq     textEntryRefuse         ; C80F  the font cannot draw it - refuse with a beep
        txa                             ; C811  get the character code back
textEntryLengthCheck:
        cpy     #$FF                    ; C812  operand = the maximum line length patched at $C7DA
        bcc     storeTypedChar          ; C814  still room in the line - just store it
textEntryScrollCheck:
        ldx     #$FF                    ; C816  operand = the caller's parameter patched at $C7D5; bit 7 = 'scroll when full'
        bpl     textEntryRefuse         ; C818  no scrolling allowed - the line is full, beep

; ----------------------------------------------------------------------
; The line is full but scrolling is allowed (chat line): shift the whole 36-byte buffer one character
; to the left, then store the new character at the end.
; ----------------------------------------------------------------------
        pha                             ; C81A  keep the character while the buffer is shifted
        ldx     #$00                    ; C81B  start at the first byte of the buffer
scrollTypedLineLoop:
        lda     typedLineScrollSrc,x    ; C81D  read one byte further on ($9279 = buffer + 1)
        sta     typedLineBuffer,x       ; C820  and write it one place to the left
        inx                             ; C823  next byte
        cpx     #$24                    ; C824  $24 = 36, the length of the line buffer
        bcc     scrollTypedLineLoop     ; C826  keep shifting; note the last pass reads $929C (chatState) one byte past the buffer, but that byte is overwritten by the terminator below
        dey                             ; C828  the whole line moved left, so the cursor did too
        pla                             ; C829  recover the character
storeTypedChar:
        sta     typedLineBuffer,y       ; C82A  store it at the cursor
        iny                             ; C82D  and advance the cursor
        bne     terminateTypedLine      ; C82E  always taken (Y can never wrap to 0 here)
textEntryRefuse:
        lda     #$03                    ; C830  sound 3 = the refusal beep
        jsr     playSound               ; C832  beep
textEntryNoKey:
        lda     #$FF                    ; C835  $FF = 'no key accepted'
        sta     textEntryReturnCode+1   ; C837  overwrite the return value stored at $C7E4
        bne     textEntryConsumeKey     ; C83A  always taken
terminateTypedLine:
        lda     #$9E                    ; C83C  $9E = the terminator that prints nothing
        sta     typedLineBuffer,y       ; C83E  terminate the line at the cursor
textEntryAccepted:
        lda     #$02                    ; C841  sound 2 = the key click
        jsr     playSound               ; C843  click
textEntryConsumeKey:
        lda     #$FF                    ; C846  $FF = 'no key'
        sta     newKeyEvent             ; C848  consume the key event...
        sta     lastTypedKey            ; C84B  ...and the decoded key, so no other handler sees it
textEntryReturnCode:
        lda     #$FF                    ; C84E  operand = the accepted key code, or $FF when nothing was taken
textEntryReturn:
        cmp     #$FF                    ; C850  Z = 1 tells the caller that nothing was accepted
        rts                             ; C852  A = the key code (or $FF), Y = the new cursor position


; ----------------------------------------------------------------------
; showWorkingMessage - puts message 1 'WORKING...' on the status line at once and makes it the
; persistent message, so it stays there until something clears it.  Message 1's sound entry is $80,
; i.e. silent.
; In:  none
; Out: persistentMessageId $92A1 = 1, shownMessageId zp_7B = 1, message drawn
; Called from: before every slow operation - $10CE (jmp) and $1AA4 in the main program, $6F2D, $79A4,
;     $79EF, $7B1D in the setup overlay, and handlePauseKey ($C746)
; ----------------------------------------------------------------------
showWorkingMessage:
        lda     #$01                    ; C853  message id 1 = 'WORKING...' ($056C)
        sta     persistentMessageId     ; C855  keep it on the line until the queue or a screen change replaces it
        jmp     showStatusMessageNoSound; C858  print it now, without a sound lookup


; ----------------------------------------------------------------------
; promptPressButtonWhenReady - shows 'PRESS BUTTON WHEN READY.' centred on the status line and
; busy-waits for a fresh fire-button click, stirring the 24-bit game RNG on every pass.  Because the
; number of passes depends on the player, this is also what randomises the map seed before a game.
; In:  the fire button (through checkFirePressed), RNG state $57-$59
; Out: returns after a click; the RNG has been advanced an unpredictable number of times and the
;     status line is blanked
; Called from: waitForGameDiskInserted ($10E8)
; ----------------------------------------------------------------------
promptPressButtonWhenReady:
        lda     #$BE                    ; C85B  low byte of 'PRESS BUTTON WHEN READY.' at $CBBE
        ldy     #$CB                    ; C85D  high byte of $CBBE
        jsr     showMessageString       ; C85F  draw it centred in the 36-column status line
waitForFireLoop:
        jsr     nextGameRandom          ; C862  advance the pseudo-random shift register - the player's reaction time is the entropy
        jsr     checkFirePressed        ; C865  Z = 1 as soon as a fresh press is seen (it also plays click sound 1)
        bne     waitForFireLoop         ; C868  no click yet - keep stirring
        jmp     clearMessageLine        ; C86A  blank the status line and return


; ----------------------------------------------------------------------
; exitIfGameEnded - if the end-of-game display has been up long enough this routine never returns: it
; transfers control to the main menu instead.  gameEndCountdown $92C9 is set to a start value when the
; game ends and counted UP by the IRQ until it reaches $80, so bit 7 set means 'the result has been
; shown, get out'.
; In:  gameEndCountdown $92C9, gamePhase zp_B1
; Out: returns normally, or jumps to the menu (which rebuilds the stack, so the caller is abandoned)
; Called from: the top of updateGameFrame ($40B2) and the command-exchange loop ($4E41)
; ----------------------------------------------------------------------
exitIfGameEnded:
        lda     gameEndCountdown        ; C86D  $92C9: 0 = no game over pending, otherwise it counts up to $80
        bpl     gameStillRunning        ; C870  still counting (or nothing pending) - just go back
        lda     gamePhase               ; C872  zp_B1 is 0 only during the battle
        beq     endBattleToMenu         ; C874  in the battle the session has to be tidied up first
        jmp     returnToMainMenu        ; C876  setup, film or menu phase: reset the stack and show the main menu

endBattleToMenu:
        jmp     endGameToMainMenu       ; C879  battle: close the film, free the link and then go to the menu

gameStillRunning:
        rts                             ; C87C  the game is still running


; soundClassTable - one class/priority byte per sound id 0-28, read by requestSound.
;   $00/$01/$02 = ordinary request for SID voice 0/1/2: only accepted when that request
;                 slot is free and the voice is silent or busy with another ordinary sound.
;   $80/$81/$82 = force the sound onto voice 0/1/2, overriding whatever plays there.
;   $FF         = force it into request slot 3, the whole-chip slot used by the three-voice
;                 fanfares (ids $0D, $0E, $19).
; requestSound only uses the low two bits as the slot number, so $FF selects slot 3. Entry 0 is never
; used as a class: requestSound(0) is the 'silence everything' request.
soundClassTable:
        .byte   $FF,$00,$00,$00,$00,$00,$FF,$80; C87D  ........  ids 0-7: 0 unused (silence request), 1-5 ordinary voice 0, 6 whole-chip, 7 forced voice 0
        .byte   $81                     ; C885  ids 8-$0F: 8 forced voice 1, 9-$0B ordinary voice 1, $0C forced voice 0, $0D/$0E whole-chip fanfares, $0F forced voice 2
        .byte   $01,$01,$01,$80,$FF,$FF,$82; C886  .......
        .byte   $82                     ; C88D  ids $10-$17: $10-$13 forced voice 2, $14 ordinary voice 2 (comcen engine), $15 whole-chip, $16/$17 forced voice 2
        .byte   $82,$82,$82,$02,$FF,$82,$82; C88E  .......
        .byte   $82                     ; C895  ids $18-$1C: $18 forced voice 2, $19 whole-chip fanfare, $1A forced voice 0, $1B forced voice 1, $1C forced voice 2 (comcen engine stop)
        .byte   $FF,$80,$81,$82         ; C896  ....

; ----------------------------------------------------------------------
; playSound - the public sound entry used by about fifty call sites: queues sound id A and gives X and
; Y back to the caller.  X and Y are preserved in two self-modified operands rather than on the stack,
; so this entry is NOT re-entrant - the IRQ-side callers at $1370 and $140E deliberately call
; requestSound directly.
; In:  A = sound id 0-28
; Out: the sound is queued in one of the four request slots; X and Y unchanged, A clobbered
; Called from: everywhere - see XREF; within this file $C55C, $C679 (jmp), $C7BD, $C832, $C843, $C910
;     (jmp), $C920 (jmp)
; ----------------------------------------------------------------------
playSound:
        sty     playSoundRestoreY+1     ; C89A  save Y in the operand of the LDY at $C8A3
        stx     playSoundRestoreX+1     ; C89D  save X in the operand of the LDX at $C8A5
        jsr     requestSound            ; C8A0  queue the request
playSoundRestoreY:
        ldy     #$FF                    ; C8A3  operand patched at $C89A: the caller's Y
playSoundRestoreX:
        ldx     #$FF                    ; C8A5  operand patched at $C89D: the caller's X
        rts                             ; C8A7  only A is clobbered


; ----------------------------------------------------------------------
; requestSound - the core of the sound request queue.  A = 0 is the 'silence everything' request:
; slots 0-2 become $FF (empty) and slot 3 gets id 0, so the raster IRQ dequeues sound 0 and stops the
; chip.  Otherwise the class byte from soundClassTable decides where the request goes: a negative
; class forces it into slot (class AND 3), while a positive class is a plain voice number that is only
; taken when that request slot is free and the voice is either silent or playing another low-priority
; sound.  Ids of $1D and above are dropped.
; In:  A = sound id 0-28; soundClassTable $C87D, the four request slots $92E7-$92EA, sndVoiceSoundId
;     $67C8
; Out: one of $92E7-$92EA holds the id (or all four are reset); self-modifies the operand at $C8DC;
;     A/X/Y clobbered
; Called from: playSound ($C8A0) and directly from the two IRQ-side call sites $1370 (jmp) and $140E
; ----------------------------------------------------------------------
requestSound:
        tay                             ; C8A8  Y = the sound id for the table lookups below
        bne     classifySoundRequest    ; C8A9  id 0 is the request to silence everything
        sty     soundRequestMultiVoice  ; C8AB  slot 3 (the whole-chip slot) receives id 0 = 'stop all voices'
        dey                             ; C8AE  Y = $FF
        sty     soundRequestVoice2      ; C8AF  voice 2 request slot: empty
        sty     soundRequestVoice1      ; C8B2  voice 1 request slot: empty
        sty     soundRequestSlots       ; C8B5  voice 0 request slot: empty
        rts                             ; C8B8  the IRQ will pick up the id 0 in slot 3 on its next pass

classifySoundRequest:
        lda     soundClassTable,y       ; C8B9  class/priority byte of this sound (ids >= $1D read past the table, but the range test at $C8E1 drops them before anything is stored)
        sta     queueSoundInSlot+1      ; C8BC  patch it into the LDA operand at $C8DB - the slot number is derived from it below
        bmi     queueSoundInSlot        ; C8BF  negative class = forced, no priority test needed
        tax                             ; C8C1  X = the voice number 0-2
        lda     soundRequestSlots,x     ; C8C2  is a request already waiting for that voice?
        bmi     checkVoiceBusy          ; C8C5  $FF = free, so only the voice itself has to be checked
        tax                             ; C8C7  X = the sound id already queued there
        lda     soundClassTable,x       ; C8C8  its class
        bmi     soundRequestDone        ; C8CB  the queued sound is a forced one - do not displace it
        ldx     queueSoundInSlot+1      ; C8CD  restore X = our own voice number
checkVoiceBusy:
        lda     sndVoiceSoundId,x       ; C8D0  $67C8+voice = the id the SID voice is playing right now (0 = silent)
        beq     queueSoundInSlot        ; C8D3  silent - the request can be taken
        tax                             ; C8D5  X = the sound currently playing
        lda     soundClassTable,x       ; C8D6  its class
        bmi     soundRequestDone        ; C8D9  a forced sound is playing - do not interrupt it
queueSoundInSlot:
        lda     #$FF                    ; C8DB  operand = the class byte patched at $C8BC
        and     #$03                    ; C8DD  the low two bits are the request slot: 0-2 = SID voices, 3 = the whole-chip slot ($FF AND 3)
        tax                             ; C8DF  X = slot
        tya                             ; C8E0  A = the sound id again
        cmp     #$1D                    ; C8E1  29 sounds are defined (ids 0-$1C)
        bcs     soundRequestDone        ; C8E3  out of range - drop it
        sta     soundRequestSlots,x     ; C8E5  queue the id; the raster IRQ dequeues it with dequeueSoundRequest
soundRequestDone:
        rts                             ; C8E8  shared exit: either the request was queued or it was dropped


; ----------------------------------------------------------------------
; dequeueSoundRequest - takes one pending sound request out of the four slots, scanning from slot 3
; (the whole-chip slot, highest priority) down to slot 0.  The first non-empty slot is emptied and its
; id returned in X; X = $FF when all four are empty.
; In:  the four request slots $92E7-$92EA ($FF = empty)
; Out: X = the sound id or $FF, that slot reset to $FF; A/Y clobbered
; Called from: the raster IRQ ($1283), which hands X straight to startSound ($6728)
; ----------------------------------------------------------------------
dequeueSoundRequest:
        ldy     #$03                    ; C8E9  start at slot 3, the whole-chip slot
scanSoundSlotsLoop:
        ldx     soundRequestSlots,y     ; C8EB  id waiting in this slot
        bmi     nextSoundSlot           ; C8EE  $FF = empty, try the next slot down
        lda     #$FF                    ; C8F0  $FF = empty
        sta     soundRequestSlots,y     ; C8F2  take the request out of the slot
        rts                             ; C8F5  X = the id to start

nextSoundSlot:
        dey                             ; C8F6  next slot down (2, 1, 0)
        bpl     scanSoundSlotsLoop      ; C8F7  keep scanning while the index is still non-negative
        rts                             ; C8F9  X = $FF: nothing to start this frame


; ----------------------------------------------------------------------
; updateComcenMoveSound - keeps the engine sound of the local player's command centre in step with its
; movement.  It only acts during a live battle.  When the comcen is still moving and SID voice 2 is
; silent it starts the engine loop (sound $14); when the comcen has arrived and voice 2 is still
; running the engine ($14) or its variant ($16) it plays the engine-stop sound ($1C).
; In:  gamePhase zp_B1, filmPlaybackMode $0B9B, comcenUnit $9236, unitDisplayFlagsTable $FB54 (bit 7 =
;     the unit has reached its destination), sndVoiceSoundId+2 $67CA (the sound on SID voice 2)
; Out: sound $14 or $1C requested through playSound
; Called from: processUnitStep ($42A0), i.e. once per unit movement step
; ----------------------------------------------------------------------
updateComcenMoveSound:
        lda     gamePhase               ; C8FA  zp_B1 is 0 only during the battle
        ora     filmPlaybackMode        ; C8FC  $0B9B is 0 only in a live game (not film playback, not solo)
        bne     comcenSoundDone         ; C8FF  any other state - stay quiet
        ldy     comcenUnit              ; C901  unit index of our own comcen (49 for side 0, 99 for side 1)
        lda     unitDisplayFlagsTable,y ; C904  its UI flag byte; bit 7 = the unit is standing on its destination
        bmi     comcenHasStopped        ; C907  bit 7 set = the comcen has stopped
        lda     sndVoice2SoundId        ; C909  the sound SID voice 2 is playing (0 = silent)
        bne     comcenSoundDone         ; C90C  voice 2 is busy - do not start the engine over it
        lda     #$14                    ; C90E  sound $14 = the comcen engine loop
        jmp     playSound               ; C910  start it (its class $02 means it yields to forced sounds)

comcenHasStopped:
        lda     sndVoice2SoundId        ; C913  the sound on voice 2
        cmp     #$14                    ; C916  $14 = the engine loop
        beq     requestComcenStopSound  ; C918  yes - stop it
        cmp     #$16                    ; C91A  $16 = the other comcen movement sound
        bne     comcenSoundDone         ; C91C  voice 2 is playing something else - leave it alone
requestComcenStopSound:
        lda     #$1C                    ; C91E  sound $1C = the engine-stop sound (forced onto voice 2)
        jmp     playSound               ; C920  play it

comcenSoundDone:
        rts                             ; C923  nothing to do: wrong game phase, or voice 2 is busy with another sound


; ----------------------------------------------------------------------
; setMessageSlot - installs a string address into the run-time message pointer tables, so a message id
; can be re-pointed at different text.  Used for the dynamic slots $1E, $20 and $21.
; In:  X = message id, A = string low byte, Y = string high byte
; Out: messagePtrLoTable $0500[X] = A, messagePtrHiTable $0524[X] = Y; A = the high byte on exit
; Called from: the start-up code ($0B1D, $0B26, $0C8E, $0C97), the setup overlay ($7BAC, $81C2, $821C,
;     $86B5, $8724) and the modem driver tail ($EED6, $EEFF)
; ----------------------------------------------------------------------
setMessageSlot:
        sta     messagePtrLoTable,x     ; C924  low byte of the string address into slot X
        tya                             ; C927  high byte
        sta     messagePtrHiTable,x     ; C928  into the parallel table $24 bytes further on
        rts                             ; C92B  A = the high byte, which some callers rely on


; -------------------------------------------------------------------
; The game's vocabulary: $C92C-$CCFF is one pool of text, most of it in fixed 8-byte-stride tables
; that printTableEntry ($C100) indexes with A/Y = base address and X = entry number, the rest bit-7
; terminated strings printed in sequence with printString ($C133) / printStringAtPointer ($C137).
; Several tables are deliberately truncated because the next block starts immediately - see the notes
; on each one.
; -------------------------------------------------------------------
; groupNameTable - seven 8-byte group names, index = the group id in bits 2-4 of the unit's UI flag
; byte $FB54: 0 ALPHA, 1 BAKER, 2 CHARLIE, 3 DELTA, 4 EASY, 5 FOXTROT, 6 GROVER.  Read by
; printUnitStatusLines ($23B1), runJoinGroupMenu ($6162) and runGroupOptionsMenu ($634E).  Index 7
; does not exist and would run into unitTypeNameTable below, printing GRUNT.
groupNameTable:
        .byte   "ALPHA ",$A0,$00        ; C92C

; msgBaker - message text "BAKER  " (the last character carries bit 7 as the terminator).
msgBaker:
        .byte   "BAKER ",$A0            ; C934
        .byte   $00                     ; C93B  .

; msgCharlie - message text "CHARLIE" (the last character carries bit 7 as the terminator).
msgCharlie:
        .byte   "CHARLI",'E'|$80        ; C93C
        .byte   $00                     ; C943  .

; msgDelta - message text "DELTA  " (the last character carries bit 7 as the terminator).
msgDelta:
        .byte   "DELTA ",$A0            ; C944
        .byte   $00                     ; C94B  .

; msgEasy - message text "EASY   " (the last character carries bit 7 as the terminator).
msgEasy:
        .byte   "EASY  ",$A0            ; C94C
        .byte   $00                     ; C953  .

; msgFoxtrot - message text "FOXTROT" (the last character carries bit 7 as the terminator).
msgFoxtrot:
        .byte   "FOXTRO",'T'|$80        ; C954
        .byte   $00                     ; C95B  .

; msgGrover - message text "GROVER " (the last character carries bit 7 as the terminator).
msgGrover:
        .byte   "GROVER",$A0            ; C95C
        .byte   $00                     ; C963  .

; unitTypeNameTable - five 8-byte unit type names indexed by the unit type in bits 0-2 of
; unitTypeTable $F76C: 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN. Each name is right-aligned inside
; its 8-byte slot with leading spaces, so no padding is needed at the print site (showUnitInfoPanel
; $22C9).
unitTypeNameTable:
        .byte   "GRUN",'T'|$80          ; C964

; msgRider - message text "   RIDER" (the last character carries bit 7 as the terminator).
msgRider:
        .byte   "   RIDE",'R'|$80       ; C969

; msgBoomer - message text "   BOOMER" (the last character carries bit 7 as the terminator).
msgBoomer:
        .byte   "   BOOME",'R'|$80      ; C971

; msgSpy - message text "  SPY" (the last character carries bit 7 as the terminator).
msgSpy:
        .byte   "  SP",'Y'|$80          ; C97A

; msgComcen - message text "     COMCEN" (the last character carries bit 7 as the terminator).
msgComcen:
        .byte   "     COMCE",'N'|$80    ; C97F

; msgReady - message text "  READY " (the last character carries bit 7 as the terminator).
msgReady:
        .byte   "  "                    ; C98A

; unitStatusNameTable - nine 8-byte status words used by the two status lines of
; the unit info panel: 0 READY, 1 RELOAD, 2 FIRED, 3 ' -   ', 4 REPAIR, 5 MOVE,
; 6 PINNED, 7 STUN'D, 8 DIGGIN.  The WEAPON: line indexes the table from $C98C (entries 0-2 plus
; STUN'D), the ACTION: line from entry 3 at $C9A4, so its
; indices 0-5 read ' -   '/REPAIR/MOVE/PINNED/STUN'D/DIGGIN.  The last entry is
; only 6 bytes long because unitOptionMenuText starts right after it.
unitStatusNameTable:
        .byte   "READY",$A0,$00,$00     ; C98C

; msgReload - message text "RELOAD" (the last character carries bit 7 as the terminator).
msgReload:
        .byte   "RELOA",'D'|$80         ; C994
        .byte   $00,$00                 ; C99A  ..

; msgFired - message text "FIRED " (the last character carries bit 7 as the terminator).
msgFired:
        .byte   "FIRED",$A0             ; C99C
        .byte   $00,$00,$20,$2D,$20,$20,$20,$A0; C9A2  .. -   .
        .byte   $00,$00                 ; C9AA  ..

; msgRepair - message text "REPAIR" (the last character carries bit 7 as the terminator).
msgRepair:
        .byte   "REPAI",'R'|$80         ; C9AC
        .byte   $00,$00                 ; C9B2  ..

; msgMove - message text "MOVE  " (the last character carries bit 7 as the terminator).
msgMove:
        .byte   "MOVE ",$A0             ; C9B4
        .byte   $00,$00                 ; C9BA  ..

; msgPinned - message text "PINNED" (the last character carries bit 7 as the terminator).
msgPinned:
        .byte   "PINNE",'D'|$80         ; C9BC
        .byte   $00,$00,$53,$54,$55,$4E,$27,$C4; C9C2  ..STUN'.
        .byte   $00,$00                 ; C9CA  ..

; msgDiggin - message text "DIGGIN" (the last character carries bit 7 as the terminator).
msgDiggin:
        .byte   "DIGGI",'N'|$80         ; C9CC

; unitOptionMenuText - the twelve lines of the UNIT OPTIONS and GROUP OPTIONS menus, each terminated
; with $8D (carriage return with bit 7 set), so the menu printers at $5FCA and $6387 just call
; printStringAtPointer repeatedly and the pointer walks from one line to the next: FORMATION, MOVE TO
; GOAL, CLOAKING ON, NO CLOAKING, CLEAR TARGET, SET TARGET, MEMBERSHIP, JOIN GROUP, DIRECT UNIT, SELF
; DESTRCT, SET BLITZ, BLITZ OFF.
unitOptionMenuText:
        .byte   "FORMATION",$8D         ; C9D2

; msgMoveToGoal - message text "      MOVE TO GOAL " (the last character carries bit 7 as the
; terminator).
msgMoveToGoal:
        .byte   "      MOVE TO GOAL",$8D; C9DC

; msgCloakingOn - message text "   CLOAKING ON
" (the last character carries bit 7 as the terminator).
msgCloakingOn:
        .byte   "   CLOAKING ON",$8D    ; C9EF

; msgNoCloaking - message text "    NO CLOAKING " (the last character carries bit 7 as the
; terminator).
msgNoCloaking:
        .byte   "    NO CLOAKING",$8D   ; C9FE

; msgClearTarget - message text "    CLEAR TARGET " (the last character carries bit 7 as the
; terminator).
msgClearTarget:
        .byte   "    CLEAR TARGET",$8D  ; CA0E

; msgSetTarget - message text "   SET TARGET
" (the last character carries bit 7 as the terminator).
msgSetTarget:
        .byte   "   SET TARGET",$8D     ; CA1F

; msgMembership - message text "     MEMBERSHIP " (the last character carries bit 7 as the
; terminator).
msgMembership:
        .byte   "     MEMBERSHIP",$8D   ; CA2D

; msgJoinGroup - message text "     JOIN GROUP
" (the last character carries bit 7 as the terminator).
msgJoinGroup:
        .byte   "     JOIN GROUP",$8D   ; CA3D

; msgDirectUnit - message text "     DIRECT UNIT " (the last character carries bit 7 as the
; terminator).
msgDirectUnit:
        .byte   "     DIRECT UNIT",$8D  ; CA4D

; msgSelfDestrct - message text "    SELF DESTRCT " (the last character carries bit 7 as the
; terminator).
msgSelfDestrct:
        .byte   "    SELF DESTRCT",$8D  ; CA5E

; msgSetBlitz - message text "   SET BLITZ
" (the last character carries bit 7 as the terminator).
msgSetBlitz:
        .byte   "   SET BLITZ",$8D      ; CA6F

; msgBlitzOff - message text "      BLITZ OFF
" (the last character carries bit 7 as the terminator).
msgBlitzOff:
        .byte   "      BLITZ OFF",$8D   ; CA7C

; formationMenuText - the formation sub-menu as a single string with embedded
; $0D line breaks, so one printString call draws all five rows:
; SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT).
formationMenuText:
        .byte   "SWEEP LEFT",$0D,"SWEEP RIGHT",$0D,"REVERSE",$0D,"CUSTOM"; CA8C
        .byte   $0D," (EXIT",')'|$80    ; CA8C

; consoleMiscMenuText - six $8D-terminated lines printed in sequence by the command console: '
; ENERGY', ' BATTLE', ' RADAR', '  DRONE', '  DIG OUT', 'DIG IN'.  The leading spaces do the centring
; inside the narrow console column.
consoleMiscMenuText:
        .byte   " ENERGY",$8D           ; CAB9

; msgBattle - message text " BATTLE " (the last character carries bit 7 as the terminator).
msgBattle:
        .byte   " BATTLE",$8D           ; CAC1

; msgRadar - message text " RADAR " (the last character carries bit 7 as the terminator).
msgRadar:
        .byte   " RADAR",$8D            ; CAC9

; msgDrone3 - message text "  DRONE " (the last character carries bit 7 as the terminator).
msgDrone3:
        .byte   "  DRONE",$8D           ; CAD0

; msgDigOut - message text "  DIG OUT " (the last character carries bit 7 as the terminator).
msgDigOut:
        .byte   "  DIG OUT",$8D         ; CAD8

; msgDigIn - message text "DIG IN " (the last character carries bit 7 as the terminator).
msgDigIn:
        .byte   "DIG IN",$8D            ; CAE2

; unitInfoLabelText - the label column of the unit info panel as one string with
; $0D line breaks and a bit-7 terminator: 'GROUP:', three blank lines (where the
; unit picture sprites sit), 'ENERGY:', 'WEAPON:', 'ACTION:'.  Printed by showUnitInfoPanel ($22E0);
; the values are printed alongside by printUnitStatusLines ($2384).
unitInfoLabelText:
        .byte   "GROUP:",$0D,$0D,$0D,$0D,"ENERGY:",$0D,"WEAPON:",$0D; CAE9
        .byte   "ACTION",':'|$80        ; CAE9

; consoleHeadingText - two $8D-terminated headings drawn above the console menus:
; '    UNIT' ($CB0A) and '   OPTIONS' followed by a blank line ($CB13).
consoleHeadingText:
        .byte   "    UNIT",$8D          ; CB0A

; msgOptions - message text "   OPTIONS

" (the last character carries bit 7 as the terminator).
msgOptions:
        .byte   "   OPTIONS",$0D,$8D    ; CB13

; uiPromptStrings - a pool of short bit-7 terminated words and prompts shared by the unit/group menus,
; the three command-console tabs and the info panel.  They are addressed individually, not as a table:
; 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, 'REPAIR/WHICH:'
; $CBD6 and 'ATTEMPTING REPAIR...' $CBE5.  MISC/STATS/REPAIR are the three console tab labels drawn
; around $1655-$1675.
uiPromptStrings:
        .byte   "GROUP",$8D             ; CB1F

; msgWhichGroup - message text "WHICH GROUP:" (the last character carries bit 7 as the terminator).
msgWhichGroup:
        .byte   "WHICH GROUP",':'|$80   ; CB25

; msgTooFar - message text "  TOO FAR!" (the last character carries bit 7 as the terminator).
msgTooFar:
        .byte   "  TOO FAR",'!'|$80     ; CB31

; msgAdded - message text "ADDED" (the last character carries bit 7 as the terminator).
msgAdded:
        .byte   "ADDE",'D'|$80          ; CB3B

; msgRemoved - message text "REMOVED" (the last character carries bit 7 as the terminator).
msgRemoved:
        .byte   "REMOVE",'D'|$80        ; CB40

; msgFinished - message text "FINISHED" (the last character carries bit 7 as the terminator).
msgFinished:
        .byte   "FINISHE",'D'|$80       ; CB47

; msgSetting - message text "SETTING" (the last character carries bit 7 as the terminator).
msgSetting:
        .byte   "SETTIN",'G'|$80        ; CB4F

; msgDestination - message text "DESTINATION" (the last character carries bit 7 as the terminator).
msgDestination:
        .byte   "DESTINATIO",'N'|$80    ; CB56

; msgTarget - message text "TARGET" (the last character carries bit 7 as the terminator).
msgTarget:
        .byte   "TARGE",'T'|$80         ; CB61
        .byte   $46,$55,$45,$4C,$BA,$4D,$49,$53; CB67  FUEL.MIS
        .byte   $C3                     ; CB6F  .

; msgStats - message text "STATS" (the last character carries bit 7 as the terminator).
msgStats:
        .byte   "STAT",'S'|$80          ; CB70

; msgRepair2 - message text "REPAIR" (the last character carries bit 7 as the terminator).
msgRepair2:
        .byte   "REPAI",'R'|$80         ; CB75

; msgCloaked - message text "CLOAKED" (the last character carries bit 7 as the terminator).
msgCloaked:
        .byte   "CLOAKE",'D'|$80        ; CB7B

; msgBlitz - message text "BLITZ" (the last character carries bit 7 as the terminator).
msgBlitz:
        .byte   "BLIT",'Z'|$80          ; CB82

; msgDugIn - message text "DUG IN" (the last character carries bit 7 as the terminator).
msgDugIn:
        .byte   "DUG I",'N'|$80         ; CB87

; msgNotInGroup - message text "NOT IN GROUP" (the last character carries bit 7 as the terminator).
msgNotInGroup:
        .byte   "NOT IN GROU",'P'|$80   ; CB8D

; msgRePositioned - message text "RE-POSITIONED" (the last character carries bit 7 as the terminator).
msgRePositioned:
        .byte   "RE-POSITIONE",'D'|$80  ; CB99

; msgNewPosition - message text "NEW POSITION" (the last character carries bit 7 as the terminator).
msgNewPosition:
        .byte   "NEW POSITIO",'N'|$80   ; CBA6

; msgExit - message text " EXIT " (the last character carries bit 7 as the terminator).
msgExit:
        .byte   " EXIT",$A0             ; CBB2

; msgEnergy2 - message text "ENERGY" (the last character carries bit 7 as the terminator).
msgEnergy2:
        .byte   "ENERG",'Y'|$80         ; CBB8

; msgPressButtonWhenReady - message text "PRESS BUTTON WHEN READY." (the last character carries bit 7
; as the terminator).
msgPressButtonWhenReady:
        .byte   "PRESS BUTTON WHEN READY",'.'|$80; CBBE

; msgRepair3 - message text, 2 lines separated by $0D; the last character carries bit 7:
;     "REPAIR"
;     "WHICH:"
msgRepair3:
        .byte   "REPAIR",$0D,"WHICH:",$0D,$8D; CBD6

; 'ATTEMPTING REPAIR...' - shown by the REPAIR tab while it tries to un-stun the comcen ($17DA).
; NOTE: the two labels inside this string, at $CBED and $CBF2, are spurious.  They come from XREF
; entries 'call:C3D0' and 'call:C3D9', which belong to the boot loader that used to occupy
; $C000-$C3FF; in this overlay
; $C3D0 and $C3D9 are operand bytes of LDY #$00 and LDX #$24 inside
; printOnMessageLine.  Nothing in the running game calls $CBED or $CBF2 - they are the middle of a
; string, not code.
msgAttemptingRepair:
        .byte   "ATTEMPTI"              ; CBE5
strAttemptingRepairMid:
        .byte   $4E,$47,$20,$52,$45     ; CBED  NG RE
strAttemptingRepairEnd:
        .byte   $50,$41,$49,$52,$2E,$2E,$AE; CBF2  PAIR...

; gameHeaderText - the fragments of the play/scenario header on the console: ' PLAY' ($CBF9), '1ST'
; ($CBFE) and '2ND' ($CC01) - the last two are 3-byte entries selected as a pair - and a $0D-wrapped '
; SCENARIO' ($CC04).
gameHeaderText:
        .byte   " PLA",'Y'|$80,"1S",'T'|$80,"2N",'D'|$80; CBF9

; msgScenario - message text "   SCENARIO  " (the last character carries bit 7 as the terminator).
msgScenario:
        .byte   $0D,"  SCENARIO",$0D,$8D; CC04

; rulesNameTable - two entries on the usual 8-byte stride, selected with printTableEntry at $15C9: 0
; STANDARD, 1 CUSTOM.  The CUSTOM entry is only 6 bytes long because mainMenuItemText starts
; immediately after it, which is safe because it is the last entry.
rulesNameTable:
        .byte   "STANDAR",'D'|$80       ; CC11

; msgCustom - message text "CUSTOM" (the last character carries bit 7 as the terminator).
msgCustom:
        .byte   "CUSTO",'M'|$80         ; CC19

; mainMenuItemText - the main menu, printed line by line: 'COMPETE WITH MODEM OPPONENT', 'PRACTICE
; WITH SOLO TRAINER', 'WATCH GAME FILM' and 'SAVE GAME FILM' are one $0D-separated run ending in $8D
; at $CC74, then 'LOAD GAME FILM' ($CC75) and 'DISCONNECT/SET VOICE' ($CC84) follow as separate
; $8D-terminated lines.
mainMenuItemText:
        .byte   "COMPETE WITH MODEM OPPONENT",$0D; CC1F
        .byte   "PRACTICE WITH SOLO TRAINER",$0D,"WATCH GAME FILM",$0D; CC1F
        .byte   "SAVE GAME FILM",$8D    ; CC1F

; msgLoadGameFilm - message text "LOAD GAME FILM " (the last character carries bit 7 as the
; terminator).
msgLoadGameFilm:
        .byte   "LOAD GAME FILM",$8D    ; CC75

; msgDisconnectSetVoice - message text "DISCONNECT/SET VOICE " (the last character carries bit 7 as
; the terminator).
msgDisconnectSetVoice:
        .byte   "DISCONNECT/SET VOICE",$8D; CC84

; inGameMenuText - ENEMY ($CC99) and MISSILES ($CC9E) are panel captions, 'ABORT GAME' ($CCA6) and
; 'VOICE PAUSE' ($CCB1) are the two items of the in-game MISC options menu.  The modem driver reads
; this block too ($ED6F).
inGameMenuText:
        .byte   "ENEM",'Y'|$80          ; CC99

; msgMissiles - message text "MISSILES" (the last character carries bit 7 as the terminator).
msgMissiles:
        .byte   "MISSILE",'S'|$80       ; CC9E

; msgAbortGame - message text "ABORT GAME " (the last character carries bit 7 as the terminator).
msgAbortGame:
        .byte   "ABORT GAME",$8D        ; CCA6

; msgVoicePause - message text "VOICE PAUSE " (the last character carries bit 7 as the terminator).
msgVoicePause:
        .byte   "VOICE PAUSE",$8D       ; CCB1

; strGameTypeLabel - 'GAME TYPE:' printed at $15C5 just before the scenario name.
strGameTypeLabel:
        .byte   "GAME TYPE:",$8D        ; CCBD

; gameTypeNameTable - the seven scenario names, 8 bytes each, indexed by the game type in $0BA3 bits
; 0-2. Note the first entry is spelt SCRIMAGE with one M in the shipped game; the manual spells it
; SCRIMMAGE. The other six (QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER) match the
; manual exactly.
scenarioNameTable:
        .byte   "SCRIMAG",'E'|$80       ; CCC8

; msgQbSneak - message text "QB SNEAK" (the last character carries bit 7 as the terminator).
msgQbSneak:
        .byte   "QB SNEA",'K'|$80       ; CCD0

; msgTheBomb - message text "THE BOMB" (the last character carries bit 7 as the terminator).
msgTheBomb:
        .byte   "THE BOM",'B'|$80       ; CCD8

; msgFaceOff - message text "FACE-OFF" (the last character carries bit 7 as the terminator).
msgFaceOff:
        .byte   "FACE-OF",'F'|$80       ; CCE0

; msgSluggers - message text "SLUGGERS" (the last character carries bit 7 as the terminator).
msgSluggers:
        .byte   "SLUGGER",'S'|$80       ; CCE8

; msgFullWar - message text "FULL WAR" (the last character carries bit 7 as the terminator).
msgFullWar:
        .byte   "FULL WA",'R'|$80       ; CCF0

; msgDefender - message text "DEFENDER" (the last character carries bit 7 as the terminator).
msgDefender:
        .byte   "DEFENDE",'R'|$80       ; CCF8

; unitPictureHiTable / unitPictureLoTable - the addresses of the five unit-picture RLE blocks, split
; into a high-byte table at $CD00 and a low-byte table at $CD05, both indexed by unit type 0-4.  They
; resolve to $CD52 GRUNT, $CDD3 RIDER,
; $CE3B BOOMER, $CECD SPY, $CF3A COMCEN.
; $CD00 doubles as an inert build hook: the start-up code at $0C0D tests whether
; it holds $4C (a JMP opcode) and would turn the VIC IRQ off and jump there.  In the shipped overlay
; it holds $CD, so the hook never fires; showUnitInfoPanel ($22EA) only tests that $CD00 is negative,
; meaning 'unit pictures are loaded'.
unitPictureHiTable:
        .byte   $CD,$CD,$CE,$CE,$CF     ; CD00  .....  high bytes for types 0-4: $CD52, $CDD3, $CE3B, $CECD, $CF3A

; unitPictureLoTable: byteTable, 5 bytes. Low bytes of the five unit-picture RLE block addresses ($52,
; $D3, $3B, $CD, $3A) giving $CD52, $CDD3, $CE3B, $CECD and $CF3A.
unitPictureLoTable:
        .byte   $52,$D3,$3B,$CD,$3A     ; CD05  R.;.:  low bytes of the same five addresses

; ----------------------------------------------------------------------
; showUnitPictureSprites - draws the picture of one unit type in the info panel.  It unpacks that
; type's RLE block into the scratch page $0200-$03FF, copies the first 192 bytes (three 24x21 sprite
; shapes) to $8B40 - the shape area addressed by sprite pointers $2D/$2E/$2F, i.e. sprites 5, 6 and 7
; - and then positions and enables those three sprites side by side across the panel.
; In:  X = unit type 0-4 (GRUNT/RIDER/BOOMER/SPY/COMCEN)
; Out: $0200-$03FF overwritten with the unpacked block, $8B40-$8BFF = the three sprite frames, sprite
;     shadows $900D-$900F (colour), $9015-$9017 (X), $901D-$901F (Y), $9020 (X MSBs) and $9022
;     (enable) updated; zp_B2/zp_B3/zp_1A/zp_1C clobbered by the unpacker
; Called from: showUnitInfoPanel ($22F1), whose LDX operand at $22EF is patched with the unit type
; ----------------------------------------------------------------------
showUnitPictureSprites:
        lda     unitPictureLoTable,x    ; CD0A  low byte of this type's RLE block
        ldy     unitPictureHiTable,x    ; CD0D  high byte of it
        jsr     unpackRleTo0200         ; CD10  expand the run-length stream into $0200-$03FF (512 bytes)
        ldy     #$00                    ; CD13  copy from the start of the unpacked page

; ----------------------------------------------------------------------
; Copy the first three sprite frames (3 x 64 bytes) out of the unpacked page into the shape area of
; sprites 5, 6 and 7; the remaining 320 unpacked bytes are padding and are ignored.
; ----------------------------------------------------------------------
copyUnitPictureLoop:
        lda     contourRuleTable,y      ; CD15  unpacked byte (the RLE destination is the contour-rule page $0200)  (also trainerPlaybook0200:trainerPlay0)
        sta     sprite5Shape,y          ; CD18  $8B40 = $8000 + $2D*64, the shape block sprite pointer $2D selects
        iny                             ; CD1B  next byte
        cpy     #$C0                    ; CD1C  $C0 = 192 = three 64-byte sprite shapes
        bcc     copyUnitPictureLoop     ; CD1E  keep copying
        lda     #$01                    ; CD20  VIC colour 1 = white
        sta     sprite5ColourShadow     ; CD22  sprite 5 colour
        sta     sprite6ColourShadow     ; CD25  sprite 6 colour
        sta     sprite7ColourShadow     ; CD28  sprite 7 colour
        lda     #$EC                    ; CD2B  X = 236: the left-hand picture, still below 256 so its MSB stays clear
        sta     sprite5XShadow          ; CD2D  sprite 5 X position
        lda     #$0C                    ; CD30  X = 268 ($10C): low byte $0C with the MSB set below
        sta     sprite6XShadow          ; CD32  sprite 6 X position
        lda     #$2C                    ; CD35  X = 300 ($12C): low byte $2C with the MSB set below
        sta     sprite7XShadow          ; CD37  sprite 7 X position
        lda     #$A2                    ; CD3A  Y = 162, the picture row of the info panel
        sta     sprite5YShadow          ; CD3C  sprite 5 Y position
        sta     sprite6YShadow          ; CD3F  sprite 6 Y position
        sta     sprite7YShadow          ; CD42  sprite 7 Y position
        lda     spriteXMsbShadow        ; CD45  shadow of VIC_SPR_HI_X $D010
        ora     #$C0                    ; CD48  bits 6 and 7 = the X MSBs of sprites 6 and 7
        sta     spriteXMsbShadow        ; CD4A  store it back
        lda     #$E0                    ; CD4D  $E0 = sprites 5, 6 and 7
        jmp     enableSprites           ; CD4F  OR them into the sprite-enable shadow $9022 and return


; -------------------------------------------------------------------
; The five unit pictures, in unit-type order, as run-length coded blocks for unpackRleTo0200 ($67CB).
; The first byte of a block is its escape marker; in the stream 'marker, count, value' emits value
; count times and every other byte is a literal.  Each block unpacks to 512 bytes but only the first
; 192 matter: three 24x21 monochrome sprite frames of 63 bytes plus one pad byte, shown side by side
; in the info panel by showUnitPictureSprites.  Each block's stream ends exactly where the next one
; begins.
; -------------------------------------------------------------------
; unitPictureGrunt - type 0, escape marker $05: a helmeted figure, a running figure and a burst.
unitPictureGrunt:
        .byte   $05,$05,$10             ; CD52  .....#.#.....#.#...#....
        .byte   $00,$3C,$00             ; CD55  ..........####..........
        .byte   $00,$FF,$00             ; CD58  ........########........
        .byte   $01,$99,$80             ; CD5B  .......##..##..##.......
        .byte   $01,$A5,$80             ; CD5E  .......##.#..#.##.......
        .byte   $03,$E7,$C0             ; CD61  ......#####..#####......
        .byte   $07,$FF,$E0             ; CD64  .....##############.....
        .byte   $07,$3C,$E0             ; CD67  .....###..####..###.....
        .byte   $06,$00,$60             ; CD6A  .....##..........##.....
        .byte   $06,$00,$60             ; CD6D  .....##..........##.....
        .byte   $06,$00,$60             ; CD70  .....##..........##.....
        .byte   $03,$00,$C0             ; CD73  ......##........##......
        .byte   $03,$00,$C0             ; CD76  ......##........##......
        .byte   $03,$00,$C0             ; CD79  ......##........##......
        .byte   $07,$00,$E0             ; CD7C  .....###........###.....
        .byte   $05,$17,$00             ; CD7F  .....#.#...#.###........
        .byte   $1F,$80,$00             ; CD82  ...######...............
        .byte   $27,$C0,$00             ; CD85  ..#..#####..............
        .byte   $6F,$E0,$03             ; CD88  .##.#######...........##
        .byte   $FF,$F0,$03             ; CD8B  ############..........##
        .byte   $FD,$70,$00             ; CD8E  ######.#.###............

        .byte   $3B,$B0,$00             ; CD91  ..###.###.##............
        .byte   $07,$C0,$00             ; CD94  .....#####..............
        .byte   $07,$80,$00             ; CD97  .....####...............
        .byte   $07,$00,$00             ; CD9A  .....###................
        .byte   $06,$00,$00             ; CD9D  .....##.................
        .byte   $07,$00,$00             ; CDA0  .....###................
        .byte   $03,$80,$00             ; CDA3  ......###...............
        .byte   $03,$80,$00             ; CDA6  ......###...............
        .byte   $0F,$C0,$05             ; CDA9  ....######...........#.#
        .byte   $14,$00,$18             ; CDAC  ...#.#.............##...
        .byte   $00,$00,$18             ; CDAF  ...................##...
        .byte   $00,$00,$18             ; CDB2  ...................##...
        .byte   $00,$00,$3C             ; CDB5  ..................####..
        .byte   $00,$00,$FF             ; CDB8  ................########
        .byte   $00,$00,$BD             ; CDBB  ................#.####.#
        .byte   $00,$03,$99             ; CDBE  ..............###..##..#
        .byte   $C0,$03,$FF             ; CDC1  ##............##########
        .byte   $C0,$03,$FF             ; CDC4  ##............##########
        .byte   $C0,$03,$FF             ; CDC7  ##............##########
        .byte   $C0,$01,$FF             ; CDCA  ##.............#########
        .byte   $80,$00,$7E             ; CDCD  #................######.

        .byte   $05,$10,$00             ; CDD0  .....#.#...#............


; unitPictureRider - type 1, escape marker $07: three 24x21 frames of the mounted/wheeled unit.
unitPictureRider:
        .byte   $07,$07,$1F             ; CDD3  .....###.....###...#####
        .byte   $00,$1C,$00             ; CDD6  ...........###..........
        .byte   $00,$36,$00             ; CDD9  ..........##.##.........
        .byte   $00,$3E,$00             ; CDDC  ..........#####.........
        .byte   $00,$1C,$00             ; CDDF  ...........###..........
        .byte   $0E,$1C,$38             ; CDE2  ....###....###....###...
        .byte   $0F,$7F,$78             ; CDE5  ....####.#######.####...
        .byte   $0F,$C1,$F8             ; CDE8  ....######.....######...
        .byte   $0F,$00,$78             ; CDEB  ....####.........####...
        .byte   $0D,$00,$58             ; CDEE  ....##.#.........#.##...
        .byte   $07,$26,$00             ; CDF1  .....###..#..##.........
        .byte   $38,$00,$03             ; CDF4  ..###.................##
        .byte   $FC,$00,$00             ; CDF7  ######..................
        .byte   $7C,$00,$00             ; CDFA  .#####..................
        .byte   $30,$F0,$00             ; CDFD  ..##....####............
        .byte   $7D,$98,$0F             ; CE00  .#####.##..##.......####
        .byte   $CF,$68,$1A             ; CE03  ##..####.##.#......##.#.
        .byte   $01,$68,$16             ; CE06  .......#.##.#......#.##.
        .byte   $01,$98,$0C             ; CE09  .......##..##.......##..
        .byte   $00,$F0,$07             ; CE0C  ........####.........###
        .byte   $16,$00,$03             ; CE0F  ...#.##...............##

        .byte   $00,$60,$03             ; CE12  .........##...........##
        .byte   $08,$60,$03             ; CE15  ....#....##...........##
        .byte   $08,$60,$00             ; CE18  ....#....##.............
        .byte   $88,$80,$00             ; CE1B  #...#...#...............
        .byte   $5D,$00,$00             ; CE1E  .#.###.#................
        .byte   $3E,$00,$00             ; CE21  ..#####.................
        .byte   $3E,$00,$00             ; CE24  ..#####.................
        .byte   $3E,$00,$0E             ; CE27  ..#####.............###.
        .byte   $5D,$38,$0F             ; CE2A  .#.###.#..###.......####
        .byte   $F7,$F8,$0F             ; CE2D  ####.########.......####
        .byte   $00,$78,$0E             ; CE30  .........####.......###.
        .byte   $00,$38,$0E             ; CE33  ..........###.......###.
        .byte   $00,$38,$07             ; CE36  ..........###........###
        .byte   $09,$00                 ; CE39  ....#..#........


; unitPictureBoomer - type 2, escape marker $08: launcher, missile in flight and emplacement.
unitPictureBoomer:
        .byte   $08,$08,$0A             ; CE3B  ....#.......#.......#.#.
        .byte   $00,$3C,$00             ; CE3E  ..........####..........
        .byte   $00,$66,$00             ; CE41  .........##..##.........
        .byte   $00,$3C,$00             ; CE44  ..........####..........
        .byte   $00,$18,$00             ; CE47  ...........##...........
        .byte   $00,$18,$00             ; CE4A  ...........##...........
        .byte   $00,$18,$00             ; CE4D  ...........##...........
        .byte   $00,$18,$00             ; CE50  ...........##...........
        .byte   $00,$18,$00             ; CE53  ...........##...........
        .byte   $00,$3C,$00             ; CE56  ..........####..........
        .byte   $00,$5A,$00             ; CE59  .........#.##.#.........
        .byte   $00,$DB,$00             ; CE5C  ........##.##.##........
        .byte   $0F,$FF,$F0             ; CE5F  ....################....
        .byte   $0B,$DB,$D0             ; CE62  ....#.####.##.####.#....
        .byte   $0F,$FF,$F0             ; CE65  ....################....
        .byte   $0A,$3C,$50             ; CE68  ....#.#...####...#.#....
        .byte   $0E,$00,$70             ; CE6B  ....###..........###....
        .byte   $08,$13,$00             ; CE6E  ....#......#..##........
        .byte   $02,$00,$00             ; CE71  ......#.................
        .byte   $04,$00,$00             ; CE74  .....#..................
        .byte   $0E,$00,$00             ; CE77  ....###.................

        .byte   $13,$00,$00             ; CE7A  ...#..##................
        .byte   $01,$80,$00             ; CE7D  .......##...............
        .byte   $00,$C0,$00             ; CE80  ........##..............
        .byte   $00,$60,$00             ; CE83  .........##.............
        .byte   $00,$3F,$C0             ; CE86  ..........########......
        .byte   $00,$19,$E0             ; CE89  ...........##..####.....
        .byte   $00,$1F,$F0             ; CE8C  ...........#########....
        .byte   $00,$3F,$F8             ; CE8F  ..........###########...
        .byte   $00,$6A,$AC             ; CE92  .........##.#.#.#.#.##..
        .byte   $00,$51,$14             ; CE95  .........#.#...#...#.#..
        .byte   $00,$6A,$AC             ; CE98  .........##.#.#.#.#.##..
        .byte   $00,$3F,$F8             ; CE9B  ..........###########...
        .byte   $08,$14,$00             ; CE9E  ....#......#.#..........
        .byte   $3F,$00,$00             ; CEA1  ..######................
        .byte   $0C,$00,$00             ; CEA4  ....##..................
        .byte   $0C,$00,$00             ; CEA7  ....##..................
        .byte   $0C,$00,$00             ; CEAA  ....##..................
        .byte   $0C,$00,$00             ; CEAD  ....##..................
        .byte   $0C,$00,$00             ; CEB0  ....##..................
        .byte   $0C,$00,$07             ; CEB3  ....##...............###
        .byte   $3F,$38,$05             ; CEB6  ..######..###........#.#

        .byte   $ED,$E8,$07             ; CEB9  ###.##.####.#........###
        .byte   $FF,$F8,$05             ; CEBC  #############........#.#
        .byte   $FF,$E8,$07             ; CEBF  ###########.#........###
        .byte   $FF,$F8,$05             ; CEC2  #############........#.#
        .byte   $FF,$E8,$07             ; CEC5  ###########.#........###
        .byte   $3F,$38,$08             ; CEC8  ..######..###.......#...
        .byte   $09,$00                 ; CECB  ....#..#........


; unitPictureSpy - type 3, escape marker $05: three antenna/aerial frames.
unitPictureSpy:
        .byte   $05,$05,$19             ; CECD  .....#.#.....#.#...##..#
        .byte   $00,$41,$00             ; CED0  .........#.....#........
        .byte   $00,$22,$00             ; CED3  ..........#...#.........
        .byte   $00,$14,$00             ; CED6  ...........#.#..........
        .byte   $00,$08,$00             ; CED9  ............#...........
        .byte   $03,$1C,$60             ; CEDC  ......##...###...##.....
        .byte   $03,$FF,$E0             ; CEDF  ......#############.....
        .byte   $00,$00,$00             ; CEE2  ........................
        .byte   $01,$00,$40             ; CEE5  .......#.........#......
        .byte   $00,$08,$00             ; CEE8  ............#...........
        .byte   $01,$00,$40             ; CEEB  .......#.........#......
        .byte   $05,$23,$00             ; CEEE  .....#.#..#...##........
        .byte   $50,$00,$00             ; CEF1  .#.#....................
        .byte   $50,$00,$00             ; CEF4  .#.#....................
        .byte   $20,$00,$00             ; CEF7  ..#.....................
        .byte   $20,$00,$0C             ; CEFA  ..#.................##..
        .byte   $70,$60,$0F             ; CEFD  .###.....##.........####
        .byte   $FF,$F0,$00             ; CF00  ############............
        .byte   $00,$00,$04             ; CF03  .....................#..
        .byte   $05,$04,$00             ; CF06  .....#.#.....#..........
        .byte   $20,$02,$05             ; CF09  ..#...........#......#.#

        .byte   $18,$00,$03             ; CF0C  ...##.................##
        .byte   $80,$E0,$02             ; CF0F  #.......###...........#.
        .byte   $80,$A0,$03             ; CF12  #.......#.#...........##
        .byte   $80,$E0,$00             ; CF15  #.......###.............
        .byte   $80,$80,$00             ; CF18  #.......#...............
        .byte   $D5,$80,$00             ; CF1B  ##.#.#.##...............
        .byte   $77,$00,$00             ; CF1E  .###.###................
        .byte   $1C,$00,$00             ; CF21  ...###..................
        .byte   $08,$00,$00             ; CF24  ....#...................
        .byte   $08,$00,$00             ; CF27  ....#...................
        .byte   $08,$00,$00             ; CF2A  ....#...................
        .byte   $1C,$00,$00             ; CF2D  ...###..................
        .byte   $14,$00,$00             ; CF30  ...#.#..................
        .byte   $1C,$00,$00             ; CF33  ...###..................
        .byte   $08,$05,$0A             ; CF36  ....#........#.#....#.#.
        .byte   $00                     ; CF39  ........


; unitPictureComcen - type 4, escape marker $0A: three frames of the command centre building.  The
; block runs out at $CFDC, so the 192nd unpacked byte - the unused pad byte of sprite 7 - is the first
; byte of the leftover code fragment that follows.
unitPictureComcen:
        .byte   $0A,$0A,$0A             ; CF3A  ....#.#.....#.#.....#.#.
        .byte   $00,$82,$00             ; CF3D  ........#.....#.........
        .byte   $00,$85,$00             ; CF40  ........#....#.#........
        .byte   $00,$82,$00             ; CF43  ........#.....#.........
        .byte   $00,$8F,$80             ; CF46  ........#...#####.......
        .byte   $03,$FF,$E0             ; CF49  ......#############.....
        .byte   $07,$55,$70             ; CF4C  .....###.#.#.#.#.###....
        .byte   $06,$AA,$B0             ; CF4F  .....##.#.#.#.#.#.##....
        .byte   $07,$FF,$F0             ; CF52  .....###############....
        .byte   $05,$FF,$D0             ; CF55  .....#.###########.#....
        .byte   $06,$FF,$B0             ; CF58  .....##.#########.##....
        .byte   $07,$55,$70             ; CF5B  .....###.#.#.#.#.###....
        .byte   $06,$FF,$B0             ; CF5E  .....##.#########.##....
        .byte   $07,$FF,$F0             ; CF61  .....###############....
        .byte   $05,$FF,$D0             ; CF64  .....#.###########.#....
        .byte   $04,$41,$10             ; CF67  .....#...#.....#...#....
        .byte   $07,$C1,$F0             ; CF6A  .....#####.....#####....
        .byte   $0A,$11,$00             ; CF6D  ....#.#....#...#........
        .byte   $10,$10,$00             ; CF70  ...#.......#............
        .byte   $08,$10,$00             ; CF73  ....#......#............
        .byte   $10,$10,$00             ; CF76  ...#.......#............

        .byte   $7E,$10,$00             ; CF79  .######....#............
        .byte   $3F,$FE,$00             ; CF7C  ..#############.........
        .byte   $7F,$FF,$15             ; CF7F  .###############...#.#.#
        .byte   $7F,$FF,$3F             ; CF82  .###############..######
        .byte   $FF,$EB,$7F             ; CF85  ###########.#.##.#######
        .byte   $FF,$D7,$FF             ; CF88  ##########.#.###########
        .byte   $FF,$EB,$FF             ; CF8B  ###########.#.##########
        .byte   $FF,$FF,$6A             ; CF8E  ################.##.#.#.
        .byte   $BE,$AB,$2F             ; CF91  #.#####.#.#.#.##..#.####
        .byte   $ED,$FD,$1D             ; CF94  ###.##.#######.#...###.#
        .byte   $73,$AE,$1A             ; CF97  .###..###.#.###....##.#.
        .byte   $B3,$56,$0F             ; CF9A  #.##..##.#.#.##.....####
        .byte   $E1,$FC,$0A             ; CF9D  ###....#######......#.#.
        .byte   $0B,$00,$3E             ; CFA0  ....#.##..........#####.
        .byte   $00,$00,$7F             ; CFA3  .................#######
        .byte   $00,$00,$FF             ; CFA6  ................########
        .byte   $80,$01,$FF             ; CFA9  #..............#########
        .byte   $C0,$03,$55             ; CFAC  ##............##.#.#.#.#
        .byte   $60,$06,$AA             ; CFAF  .##..........##.#.#.#.#.
        .byte   $B0,$07,$FF             ; CFB2  #.##.........###########
        .byte   $F0,$07,$FC             ; CFB5  ####.........#########..

        .byte   $F0,$07,$FB             ; CFB8  ####.........########.##
        .byte   $70,$07,$FB             ; CFBB  .###.........########.##
        .byte   $70,$05,$FC             ; CFBE  .###.........#.#######..
        .byte   $D0,$07,$FF             ; CFC1  ##.#.........###########
        .byte   $F0,$05,$B6             ; CFC4  ####.........#.##.##.##.
        .byte   $D0,$07,$FF             ; CFC7  ##.#.........###########
        .byte   $F0,$05,$FF             ; CFCA  ####.........#.#########
        .byte   $D0,$07,$AF             ; CFCD  ##.#.........####.#.####
        .byte   $F0,$05,$FF             ; CFD0  ####.........#.#########
        .byte   $D0,$07,$FF             ; CFD3  ##.#.........###########
        .byte   $F0,$01,$FF             ; CFD6  ####...........#########
        .byte   $C0,$00,$00             ; CFD9  ##......................
        .byte   $00                     ; CFDC  ........


; ----------------------------------------------------------------------
; -------------------------------------------------------------------
; DEAD CODE.  The last 35 bytes of the overlay are a byte-for-byte copy of the tail of
; updateFrameTimers ($12DD-$12FF in the main program) that the build system left in the sector.
; Nothing references $CFDD, the game's timers are decremented by the copy at $12DD inside the raster
; IRQ, and the fragment is not even complete: its relative branches were assembled for $12xx, so here
; they point at $D001 (VIC_SPR0_Y), and the final DEC runs off the end of the page after two of its
; three bytes.
; -------------------------------------------------------------------
; ----------------------------------------------------------------------
leftoverFrameTimerTail:
        lda     linkPollDelay           ; CFDD  leftover: link poll delay $9163
        beq     L_CFE5                  ; CFE0  leftover: skip if it has already expired
        dec     linkPollDelay           ; CFE2  leftover: count it down
L_CFE5:
        lda     stepDelayTimer          ; CFE5  leftover: unit step delay $90FC
        beq     L_CFED                  ; CFE8  leftover: skip if zero
        dec     stepDelayTimer          ; CFEA  leftover: count it down
L_CFED:
        lda     shotLineTimer           ; CFED  leftover: shot-line display timer $91C8
        beq     L_CFF5                  ; CFF0  leftover: skip if zero
        dec     shotLineTimer           ; CFF2  leftover: count it down
L_CFF5:
        lda     viewRedrawTimer         ; CFF5  leftover: viewport redraw timer $9234
        beq     VIC_SPR0_Y              ; CFF8  leftover: the branch target of the $12DD original lands at $D001 here (VIC_SPR0_Y), not at any code
        cmp     #$80                    ; CFFA  leftover: $80 means 'hold the redraw indefinitely', so it is not counted down
        beq     VIC_SPR0_Y              ; CFFC  leftover: same stray branch to $D001
        .byte   $CE                     ; CFFE  leftover: the first two bytes of 'dec $9234'; the third byte would be at $D000, so the copy is truncated by the end of the page
        .byte   $34                     ; CFFF  4
