modemwars/disassembly/game/textEngineC000.s
2026-08-23 02:09:40 -05:00

3238 lines
204 KiB
ArmAsm

; ============================================================================
; $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