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

652 lines
48 KiB
ArmAsm

; ============================================================================
; $0400-$07FF - manual-check answer table, status message strings and shared scratch area
; ============================================================================
; Copied from $6F00 (track 27 sectors 13-16) by the boot loader. $0401 holds the 32 x 3-byte answers for
; the manual look-up check, $0461 the memory checksum routine the boot loader returns into, $0500-$0567
; the status message pointer tables and $056C.. the status message strings. Once the battle starts the
; three 50-byte arrays at $0400/$0432/$0464 are reused as unit scan lists and AI threat maps.
.setcpu "6502"
.include "c64.inc"
.include "zeropage.inc"
; ---- references to code/data outside this file ----
gameEntry := $0800
D_09C3 := $09C3
inputLockoutTimer := $0B7D
programChecksum := $0B7E
filmPlaybackMode := $0B9B
gameEndReason := $0BA8
resumeMainMenuScreen := $1414
enterMainMenu := $141D
enterMapScreen := $1FC3
runMapMainLoop := $1FD0
drawViewport := $26C1
initBitmapScreen := $2D41
clearMessageLineAndScreen := $2DDB
updateGameFrame := $40B2
drawOverviewMap := $5AD7
runMissileScreen := $7587
missileScreenResume := $759C
enterDroneScreen := $7FE6
resetDroneScreen := $7FF6
releaseDroneControl := $8230
msgGetOpponentOnPhoneAnd := $8800
spriteXMsbShadow := $9020
spriteEnableShadow := $9022
spriteXExpandShadow := $9023
spriteYExpandShadow := $9024
currentScreen := $90FB
stepDelayTimer := $90FC
unitOrderIndex := $9162
comcenStunStatus := $922A
chatState := $929C
gameEndCountdown := $92C9
droneArrivalFlags := $92F9
queueMessage := $C566
; Contents
; --------
; $0461 verifyProgramChecksum boot-time integrity check, and the address the boot loader falls into rather
; than calls.
; $06BA switchToRequestedScreen the game's screen dispatcher, called once per iteration from every screen's
; main loop to honour a pending F1/F3/F5/F7 keypress.
; $0709 runComcenStunnedBlackout the 'COMCEN STUNNED!' cut-scene: when your command centre has been knocked out,
; control is taken away, the screen goes blank and the engine keeps running unwatched for exactly one full round of all
; 100 units before the game hands control back.
.org $0400
; checksumXorConstant: 1 byte, $B0. The constant verifyProgramChecksum folds into the finished sum at
; $04A0 before storing it in programChecksum ($0B7E). It is deliberately OUTSIDE the checked range,
; which starts at $0402, so this byte alone shifts the reported build signature without changing the
; sum itself. The whole $0400-$07FF page is a copy of $6F00-$72FF (track 27 sectors 13-16), made by
; the boot loader's four-page block move at $C1B3-$C1C3 just before it drops into the checksum below.
; Once a battle starts the first half of the page is pure scratch and this byte becomes an array
; element - see the page map in the block comment at $04AE.
checksumXorConstant:
.byte $B0 ; 0400 $B0 = the checksum EOR constant; at run time this is also element 0 of the $0400 scratch arrays
; mapNameSeedTable: 32 entries x 3 bytes = 96 bytes ($0401-$0460), the manual look-up protection's
; question bank, used only by the copy-protection routine at $09A6.
; $09C2 executes 'LDA #seed' whose immediate operand at $09C3 is a SID noise byte the boot loader
; poked in; AND #$1F picks entry 0-31 and entry*3 indexes this table ($09CC/$09D3/$09DA). The three
; bytes are stored as the 24-bit map seed mapSeed/mapSeed1/mapSeed2 ($0B95-$0B97),
; generateBattlefieldMap draws that battlefield, and the player has to type the map's name from the
; manual; enterMapName packs what was typed back into a 24-bit number (zp_5A-zp_5C) and $09FC-$0A0F
; compares it with the very same three bytes. So there is no separate answer list: a map name IS the
; seed, written in base 32 with the 32-character alphabet at $890C. Three wrong answers and $0A1E
; wipes all of memory. Note that the checksum below starts at $0402, so the first byte of entry 0 is
; not covered by it. None of this data survives into the game: the labels inside the table ($0410,
; $0420, $0430, $0432,
; $0450) are there because other code uses those addresses as scratch arrays later on.
mapNameSeedTable:
.byte $0D,$0F,$AE,$97,$CD,$CE,$E4,$92; 0401 ........
.byte $24,$24,$D9,$40,$3C,$CC,$EC; 0409 $$.@<..
; Map-name entry 5 of the table above starts here. At run time $0410+g is the group summariser's
; 'worst movement cost any member of group g has to pay' (g = 0-15): cleared at $48ED, raised at $4942
; from terrainMoveCostTable[terrain class under the member].
D_0410:
.byte $CE,$1D,$AC,$F4,$89,$27,$54,$86; 0410 .....'T.
.byte $82,$BB,$B6,$86,$B2,$58,$C7,$E3; 0418 .....X..
; (This address falls inside map-name entry 10.) At run time $0420+g holds the lowest movement rate
; found in group g (g = 0-15): preset to $FF at
; $48F2 and lowered at $4960, so the whole group ends up moving at the speed of its slowest member.
D_0420:
.byte $6A,$5A,$B5,$94,$AF,$85,$CF,$DF; 0420 jZ......
.byte $3D,$DF,$84,$2E,$F3,$AA,$2E,$B0; 0428 =.......
; (The last byte of map-name entry 15.) At run time $0430+g is the copy of groupStatusTable[g] saved
; at $48E2 before the group table is rebuilt; only bits 6-7 of the old value are folded back into the
; new one at $4978.
D_0430:
.byte $4C,$B2 ; 0430 L.
; (Map-name entry 16 starts here.) At run time $0432+n is the squared-distance list that runs
; parallel to the unit-index list at $0400+n: collectUnitsInRadius ($37AA) fills both ($3806/$380E),
; and scanVisibleEnemies penalises an entry in place at $364F when the target is not in the unit's
; facing direction.
D_0432:
.byte $9C,$A0,$25,$39,$17,$9A,$26,$26; 0432 ..%9..&&
.byte $61,$6C,$6D,$B9,$15,$57,$CF,$25; 043A alm..W.%
.byte $32,$E7,$91,$8D,$90,$42,$5F,$2E; 0442 2....B_.
.byte $05,$C6,$B6,$2B,$1D,$68 ; 044A ...+.h
; At run time $0450+d is the row half of the map generator's flood-fill stack: $0400+d = column,
; $0450+d = row, $04A0+d = the direction bits still to try, d = 0..$4F (80 deep, $71C6-$72EF).
D_0450:
.byte $25,$27,$0E,$DB,$BC,$D3,$FB,$9B; 0450 %'......
.byte $87,$FE,$B9,$2F,$23,$C2,$E2,$D8; 0458 .../#...
.byte $81 ; 0460 .
; ----------------------------------------------------------------------
; verifyProgramChecksum - boot-time integrity check, and the address the boot loader falls into rather
; than calls. While loading, the loader rebuilt the stack (the RTS opcode $60 read from bootDone is
; pushed by the patched PHA at $C1B1, $04 was stored at $01FE by $C26C), so the RTS at $C218 pops the
; return address $0460 and execution resumes here at $0461. It sums $0402-$87FF - everything from the
; protection table up to the last byte of whichever $6F00-$87FF overlay is resident - with a
; carry-chained ADC/EOR/ROL word loop, parks the raw sum in $8800 (the first byte past the checked
; range) and stores sum EOR $B0 in programChecksum $0B7E, then enters the game. $0B7E is never tested
; locally: it is sent to the opponent in the settings packet ($ED89 in the modem driver) so both
; machines can notice they are running different builds.
; In: memory $0402-$87FF; $09C3 (the manual-check seed byte, zeroed for the duration so the sum does
; not depend on it); $0400 = $B0; a stack rigged to return to $0460
; Out: $8800 = the raw sum ($69 on this disk image), $0B7E = sum EOR $B0 ($D9 here), $09C3 restored,
; zp_19/zp_1A/zp_1B clobbered, the two self-modified operand high bytes at $047C and $0484 left
; pointing at page $88; ends with JMP $0800 (Y = 2, S = $FE)
; Called from: nobody - it is entered by the boot loader's obfuscated RTS. Recomputing the sum from
; the extracted image gives $69/$D9, while the byte mastered at $0B7E on this d64 is $DB and
; overlay A preloads the same $DB at $79CC, so this disk image has been altered somewhere in
; $0402-$87FF.
; ----------------------------------------------------------------------
verifyProgramChecksum:
lda D_09C3 ; 0461 read the manual-check seed byte - it is the immediate operand of the 'LDA #' at $09C2, poked in by the boot loader from SID noise
D_0464:
sta scratch19 ; 0464 park the seed in zp_19; it is zeroed below so that the sum is the same on every boot, and put back at $04A6 (the address $0464 itself becomes a scratch array later - see the page map at $04AE)
lda SID_OSC3_RANDOM ; 0466 read the SID voice-3 noise oscillator $D41B - free random bits
and #$7F ; 0469 keep bits 0-6
sta scratch1A ; 046B store it in zp_1A and never look at it again: no code in this routine reads zp_1A and every later user of it writes before reading, so these three instructions look like dead leftovers, an old random-seed grab
ldx #$84 ; 046D 132 pages to fold in: $04 up to and including $87, i.e. everything up to the end of the resident $6F00-$87FF overlay - at boot time that is the comcen-screens overlay the loader has just installed
lda #$00 ; 046F A = 0: the running sum starts empty (the same zero also clears the carry counter and the seed byte)
sta scratch1B ; 0471 zp_1B counts how many times the ADC carried out; the count is added back into the sum at $049B
tay ; 0473 Y = 0
iny ; 0474 Y = 1 ...
iny ; 0475 ... Y = 2: skip $0400 (the EOR constant) and $0401 (the first protection byte) so the sum starts at $0402
sta D_09C3 ; 0476 zero the seed byte inside the instruction at $09C2, otherwise every boot would checksum differently
clc ; 0479 clear the carry ONCE, outside the loop: the carry out of each word is deliberately carried into the next one
L_047A:
adc checksumXorConstant,y ; 047A add the even byte of the word; the operand high byte at $047C is bumped one page per outer pass, so this reads $0402, $0404 ... $87FE
bcc L_0481 ; 047D branch if this addition did not carry out
inc scratch1B ; 047F it did: count the carry instead of losing it
L_0481:
iny ; 0481 step to the odd byte of the word
L_0482:
eor checksumXorConstant,y ; 0482 mix the odd byte in with EOR (so byte order matters); its operand high byte at $0484 is bumped together with the other one
php ; 0485 save the ADC carry - the rotate below must not disturb it
asl a ; 0486 start of a hand-made ROL A ...
bcc L_048B ; 0487 ... branch if bit 7 was clear
ora #$01 ; 0489 ... bit 7 fell out of the top, so feed it back into bit 0
L_048B:
plp ; 048B restore the ADC carry for the next word (the address $048C is one of the trainer's accumulator arrays later - see $04AE)
D_048C:
iny ; 048C on to the next word
bne L_047A ; 048D 128 words = one page: keep going until Y wraps back to 0
inc L_047A+2 ; 048F self-modifying: bump the high byte of the ADC operand (the byte at $047C) to the next page
inc L_0482+2 ; 0492 self-modifying: bump the high byte of the EOR operand (the byte at $0484) the same way
dex ; 0495 one page done
bne L_047A ; 0496 loop over all 132 pages; both operands are left pointing at page $88 when this falls through
ldy #$02 ; 0498 Y = 2 on entry to the game - mainStart never reads Y, so this looks like a leftover
clc ; 049A clear carry for the final add
adc scratch1B ; 049B add the carry count back in, so no carry out of the chain is lost
sta msgGetOpponentOnPhoneAnd; 049D park the raw sum in $8800 - the byte immediately after the checked range. Nothing ever reads it back and the modem status strings are loaded over $8800 later, so this store is only a convenient scratch byte
D_04A0:
eor checksumXorConstant ; 04A0 fold in the $B0 at $0400, the one byte of this page that the sum deliberately skipped (this address is also the trainer's fourth accumulator array and the map generator's fill-stack direction array later)
sta programChecksum ; 04A3 $0B7E = the build signature handed to the opponent during the settings handshake; $D9 on this image, $DB on an unmodified one
lda scratch19 ; 04A6 take the seed byte back out of zp_19 ...
sta D_09C3 ; 04A8 ... and restore the immediate operand at $09C3 so the manual check still picks a random map
jmp gameEntry ; 04AB into the game proper: $0800 is a JMP to mainStart ($0A6C), which sets its own stack pointer to $FD
; scratchPageTail: 82 bytes of $00 filler on disk ($04AE-$04FF) - everything after the checksum
; routine. Nothing in $0400-$04FF is read as data by the running game: in a solo game the trainer's
; playbook (track 29 sectors 6-8) is loaded straight over $0200-$04FF, and in every game the page is
; scratch. The tenants of the page, all of which are only valid inside the routine that uses them,
; are:
; $0400+n / $0432+n / $0464+n unit scan lists (index, squared distance, direction) built by
; collectUnitsInRadius $37AA and scanVisibleEnemies $35FE
; $0400+unit / $0464+sp a per-unit deferral counter (a unit whose count is non-zero is put
; off) and the stack of units it is waiting on, while finishRound
; rebuilds the next round's process order ($4665-$4750)
; $0400+g / $0410+g / $0420+g / $0430+g per-group (0-15) member count, worst move cost, lowest
; move
; rate and saved status byte ($48DD-$4986)
; $0400+d / $0450+d / $04A0+d the map generator's flood-fill stack: column, row, remaining
; directions, up to 80 deep ($71C6-$72EF)
; $0464+i / $0478+i / $048C+i / $04A0+i the solo trainer's four 12-entry 16-bit accumulators -
; these
; are the high bytes, the low bytes live at $88D2/$88DE/$88EA/$88F6
; (all cleared together at $ED93)
; $04C8-$04E7 the 32-byte tile compose buffer described below
scratchPageTail:
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04AE ........
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04B6 ........
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04BE ........
.byte $00,$00 ; 04C6 ..
; Tile compose buffer, 32 bytes ($04C8-$04E7): one battlefield cell drawn at 16x16 pixels, i.e. the
; 2x2 character cells of the multicolour bitmap that the tactical viewport uses. Layout: bytes
; $00-$07 = top-left character, $08-$0F = top-right, $10-$17 = bottom-left, $18-$1F = bottom-right.
; drawViewport builds a cell here: the 4x4-pixel terrain tile is expanded, each pixel doubled in both
; directions ($27C2-$27E5 - the high nibble of each source byte becomes the left character, the low
; nibble the right one, and every row is written to two consecutive scan lines), the unit glyph is
; ORed on top ($28B5-$28BC), and the finished 32 bytes are blitted into the bitmap by drawViewport
; ($2700, the full 7x5 redraw) and drawViewportCell ($5D54, a single cell), both of which step the
; bitmap pointer on to the next character row after the first 16 bytes.
D_04C8:
.byte $00 ; 04C8 .
; +$01: the odd scan lines of the top-left character; each expanded terrain row is stored at both
; $04C8+x and $04C9+x so that it is two pixels tall.
D_04C9:
.byte $00,$00,$00,$00,$00,$00,$00; 04C9 .......
; +$08: first scan line of the top-right character (the low nibble of the same source byte).
D_04D0:
.byte $00 ; 04D0 .
; +$09: its odd scan lines, written together with $04D0+x.
D_04D1:
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04D1 ........
.byte $00,$00,$00,$00,$00,$00 ; 04D9 ......
; +$17: last scan line of the bottom-left character.
D_04DF:
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04DF ........
; +$1F: last scan line of the bottom-right character. $2861-$286C stores $FF into $04C8, $04D0, $04DF
; and $04E7 when the unit's stun/dug-in byte has bit 7 set, which draws a solid 16-pixel bar across
; the top and the bottom of the cell - the dug-in marker.
D_04E7:
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04E7 ........
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04EF ........
.byte $00,$00,$00,$00,$00,$00,$00,$00; 04F7 ........
.byte $00 ; 04FF .
; messagePtrLoTable: 36 bytes, low half of the status-message pointer table, indexed by message id
; $00-$23 (the high halves are at $0524 and the sound effect for each id at $0612). showMessageString
; is called with A/Y taken from the two tables at $C536 (message id 0, the chat line) and $C5E5 (the
; id pulled off the 8-entry message ring at $9249); the printed message sits on screen for $3C = 60
; frames. Ids $1E, $20 and $21 are $FFFF placeholders on disk and are filled in at run time by
; setMessageSlot ($C924/$C928, X = id, A/Y = string) - for instance overlay A assembles its rules line
; from the template at $0636, copies it to $0332 and registers that copy as id $1E ($8714-$8724). The
; 36 messages: 00 the chat line ($0548), 01 WORKING..., 02 END OF FILM., 03 GAME ABORTED., 04 OPPONENT
; READY., 05 UNIT RECYCLED., 06/07/08 three SPOTTED RECYCLER! variants, 09 NOT A GAME FILM!, 0A SETUP
; UNITS. PRESS F 1 WHEN DONE., 0B WAITING FOR OPPONENT..., 0C COMCEN STUNNED!, 0D SPOTTED ENEMY
; COMCEN!, 0E SPY UNDER ATTACK!, 0F SPOTTED ENEMY SPY!, 10 WAITING FOR CONNECTION..., 11 WE LOST ON
; POINTS!, 12 WE WON ON POINTS!, 13 IT WAS A DRAW., 14 WE WERE KNOCKED OUT!, 15 WE WON BY A KNOCKOUT!,
; 16 GAME OVER., 17 PICK UP PHONE THEN PRESS SPACE., 18 PRESS SPACE, WAIT, HANGUP PHONE., 19 TIMEOUT.
; RUN/STOP TO RESUME., 1A OPPONENT PICKING GAME/MAP., 1B RESUMED, 1C THE BATTLE BEGINS!, 1D LAST QTR
; WARNING!, 1E/20/21 run-time slots, 1F OPPONENT READY FOR NEW GAME., 22 PHONE TROUBLE., 23 TRY AGAIN.
messagePtrLoTable:
.byte $48,$6C,$07,$76,$58,$13,$21,$2A; 0500 Hl.vX.!* ids $00-$07: $0548 chat line, $056C WORKING..., $8707 END OF FILM., $0576 GAME ABORTED., $8858 OPPONENT READY., $8713 UNIT RECYCLED., $8721 and $872A SPOTTED RECYCLER! variants
.byte $31,$83,$93,$67,$42,$51,$66,$77; 0508 1..gBQfw ids $08-$0F: $8731 SPOTTED RECYCLER!, $0583 NOT A GAME FILM!, $0593 SETUP UNITS..., $8867 WAITING FOR OPPONENT..., $8742 COMCEN STUNNED!, $8751 SPOTTED ENEMY COMCEN!, $8766 SPY UNDER ATTACK!, $8777 SPOTTED ENEMY SPY!
.byte $7E,$89,$9B,$AC,$BA,$CE,$E3,$97; 0510 ~....... ids $10-$17: $887E WAITING FOR CONNECTION..., $8789 WE LOST ON POINTS!, $879B WE WON ON POINTS!, $87AC IT WAS A DRAW., $87BA WE WERE KNOCKED OUT!, $87CE WE WON BY A KNOCKOUT!, $87E3 GAME OVER., $8897 PICK UP PHONE THEN PRESS SPACE.
.byte $B6,$B4,$D6,$D0,$D7,$E9,$FF,$F0; 0518 ........ ids $18-$1F: $88B6 PRESS SPACE WAIT HANGUP PHONE., $05B4 TIMEOUT..., $88D6 OPPONENT PICKING GAME/MAP., $05D0 RESUMED, $05D7 THE BATTLE BEGINS!, $05E9 LAST QTR WARNING!, $FFFF run-time slot, $88F0 OPPONENT READY FOR NEW GAME.
.byte $FF,$FF,$FA,$08 ; 0520 .... ids $20-$23: two more $FFFF run-time slots, then $05FA PHONE TROUBLE. and $0608 TRY AGAIN.
; messagePtrHiTable: 36 bytes, the high halves of messagePtrLoTable, same indexing. The three page
; values show where each message lives: $05/$06 = in this file, $87 = in the resident $6F00-$87FF
; overlay (so those ids only print correctly while overlay B is in memory), $88 = in the message block
; loaded from track 29 sectors 9-10, and $FF = an unfilled run-time slot.
messagePtrHiTable:
.byte $05,$05,$87,$05,$88,$87,$87,$87; 0524 ........ high bytes for ids $00-$07
.byte $87,$05,$05,$88,$87,$87,$87,$87; 052C ........ high bytes for ids $08-$0F
.byte $88,$87,$87,$87,$87,$87,$87,$88; 0534 ........ high bytes for ids $10-$17
.byte $88,$05,$88,$05,$05,$05,$FF,$88; 053C ........ high bytes for ids $18-$1F - $FF at id $1E marks the empty run-time slot
.byte $FF,$FF,$05,$06 ; 0544 .... high bytes for ids $20-$23 - two more empty slots, then this file's last two strings
; chatMessageLine: message id $00, the 36-byte chat line - exactly the width of the message panel.
; $0548 is a leading marker glyph: $1E while there is text to show (set at $13FD when an incoming line
; has been unpacked and at $C67E), $9E - the same glyph with bit 7 set, so the string ends immediately
; - to blank the line ($C62A). $C5FC tests it and treats a negative value as 'no chat line'. On disk
; the whole line is 35 spaces followed by $A0 (space with the terminator bit).
chatMessageLine:
.byte " " ; 0548 marker glyph: $1E = there is a line to print, $9E = terminator, print nothing
; The 35 text bytes of the chat line, rebuilt character by character at $13EF from the raw receive
; buffer incomingChatBuffer ($9253): the newest character is stored with bit 7 set (it terminates the
; string) and the earlier ones are copied back over it as the line grows. $0BC5 stores $9E here to
; clear the line at the start of a game.
D_0549:
.byte $20,$20,$20,$20,$20,$20,$20,$20; 0549
.byte $20,$20,$20,$20,$20,$20,$20,$20; 0551
.byte $20,$20,$20,$20,$20,$20,$20,$20; 0559
.byte $20,$20,$20,$20,$20,$20,$20,$20; 0561
.byte $20,$20,$A0 ; 0569 .
; msgWorking: message id $01, 'WORKING...', 10 bytes with the terminator on the final '.'; sound $80 =
; silent. Used as the sticky message (persistentMessageId) while a disk load or an overlay swap is in
; progress ($2988, $7989, $C853).
msgWorking:
.byte "WORKING..",'.'|$80 ; 056C
; msgGameAborted: message id $03, 'GAME ABORTED.', 13 bytes, sound $03. Queued at $525F when the
; opponent or the player abandons the game.
msgGameAborted:
.byte "GAME ABORTED" ; 0576
.byte $AE ; 0582 the terminating '.' ($AE). It is broken out of the string above because the data-type map is keyed by address alone and the 1541 fast loader has a data block at its own $0582
; msgNotAGameFilm: message id $09, 'NOT A GAME FILM!', 16 bytes, sound $03. Queued at $81F9 when the
; block just loaded from a save slot is not a game film.
msgNotAGameFilm:
.byte "NOT A GAME FILM",'!'|$80; 0583
; msgSetupUnitsPressF1WhenDone: message id $0A, 'SETUP UNITS. PRESS F 1 WHEN DONE.', 33 bytes, silent
; ($80). Queued at $1FF2 when the pre-battle setup phase starts; F1 (screen 0) ends it.
msgSetupUnitsPressF1WhenDone:
.byte "SETUP UNITS. PRESS F 1 WHEN DONE",'.'|$80; 0593
; msgTimeoutRunStopToResume: message id $19, 'TIMEOUT. RUN/STOP TO RESUME.', 28 bytes, sound $19.
; Shown as a sticky message ($5287, $C72B) when the opponent has not answered for too long; RUN/STOP
; restarts the exchange.
msgTimeoutRunStopToResume:
.byte "TIMEOUT. RUN/STOP TO RESUME",'.'|$80; 05B4
; msgResumed: message id $1B, 'RESUMED', 7 bytes, sound $19. Queued at $52A6 when the link comes back
; after a timeout or a pause.
msgResumed:
.byte "RESUME",'D'|$80 ; 05D0
; msgTheBattleBegins: message id $1C, 'THE BATTLE BEGINS!', 18 bytes, sound $03. Queued at $0B72 when
; the setup phase ends and the clock starts.
msgTheBattleBegins:
.byte "THE BATTLE " ; 05D7
.byte $42 ; 05E2 'BEGINS!' - the tail of the string above, broken out because the 1541 fast loader has a data block at its own $05E2 and data types are keyed by address alone
.byte $45,$47,$49,$4E,$53,$A1 ; 05E3 EGINS.
; msgLastQtrWarning: message id $1D, 'LAST QTR WARNING!', 17 bytes, sound $19. Queued at $4621 when
; the game clock enters the last of its four quarters.
msgLastQtrWarning:
.byte "LAST QTR WA" ; 05E9
.byte "RNING",'!'|$80 ; 05F4 'RNING!' - the tail of the string above, broken out the same way by the fast loader's hiddenSignature at its own $05F4
; msgPhoneTrouble: message id $22, 'PHONE TROUBLE.', 14 bytes, sound $15. Shown at $5A50 when the
; modem link fails.
msgPhoneTrouble:
.byte "PHONE TROUBLE",'.'|$80 ; 05FA
; msgTryAgain: message id $23, 'TRY AGAIN.', 10 bytes, sound $03. Shown at $5A32, usually right after
; PHONE TROUBLE.
msgTryAgain:
.byte "TRY AGAIN",'.'|$80 ; 0608
; messageSoundTable: 36 bytes, the sound effect played when message id X is put on screen.
; $C543-$C55E loads it with Y = the message id and passes it to playSound, except when bit 7 is set
; ($80 = silent). The sounds group the messages: $03 neutral notice, $04 spotted/alert, $06 the two
; phone prompts,
; $0D we won, $0E we lost, $15 phone trouble, $19 clock/status (GAME OVER, TIMEOUT, RESUMED, LAST
; QTR).
messageSoundTable:
.byte $80,$80,$03,$03,$03,$04,$03,$03; 0612 ........ ids $00-$07: chat and WORKING... silent; END OF FILM / GAME ABORTED / OPPONENT READY $03; UNIT RECYCLED $04; the recycler sightings $03
.byte $04,$03,$80,$03,$04,$04,$04,$04; 061A ........ ids $08-$0F: SPOTTED RECYCLER $04, NOT A GAME FILM $03, SETUP UNITS silent, WAITING FOR OPPONENT $03, then $04 for COMCEN STUNNED, ENEMY COMCEN, SPY UNDER ATTACK and ENEMY SPY
.byte $80,$0E,$0D,$80,$0E,$0D,$19,$06; 0622 ........ ids $10-$17: WAITING FOR CONNECTION silent, lost $0E, won $0D, draw silent, knocked out $0E, knockout win $0D, GAME OVER $19, PICK UP PHONE $06
.byte $06,$19,$03,$19,$03,$19,$03,$80; 062A ........ ids $18-$1F: PRESS SPACE $06, TIMEOUT $19, OPPONENT PICKING $03, RESUMED $19, THE BATTLE BEGINS $03, LAST QTR WARNING $19, run-time slot $03, OPPONENT READY FOR NEW GAME silent
.byte $80,$03,$15,$03 ; 0632 .... ids $20-$23: run-time slot silent, run-time slot $03, PHONE TROUBLE $15, TRY AGAIN $03
; optionSummaryTemplate: 23 bytes, ' MENUS XXXX DAMAGE XXX.' with the terminator on the '.'. It is a
; fill-in-the-blanks rules line, not a constant string: appendOnOffWord ($872E in overlay A) copies
; one of the words from optionWordTable over the XXXX field at offset 7 and the XXX field at offset
; $13, writes '.'|$80 straight after the last word it substituted and adds ',' / ', ' padding, then
; $8714 copies the finished line to $0332 and $8722 registers that copy as message id $1E. Offsets: 0
; space, 1-6 'MENUS ', 7-10 the OFF/ON field, 11 space, 12-17 'DAMAGE', 18 space, 19-21 the
; FULL/HALF/NONE field, 22 '.'.
optionSummaryTemplate:
.byte " MENUS XXXX DAMAGE XXX",'.'|$80; 0636
; optionWordTable: 17 bytes, five bit-7 terminated words at fixed offsets: +$00 'OFF' ($064D), +$03
; 'ON' ($0650), +$05 'FULL' ($0652), +$09 'HALF' ($0656), +$0D 'NONE' ($065A). Only the first two are
; used. appendOnOffWord ($8734) is the single reader of the table in the whole program and it enters
; with Y = 0 (OFF) or Y = 3 (ON), for both fields of optionSummaryTemplate, and
; $8632 prints 'OFF' and then - through printStringAtPointer, which carries on from where the previous
; string ended - the 'ON' behind it, as the two items of a menu. FULL, HALF and NONE have no reader
; at all: they look like leftovers from a damage option that ended up being just on/off. (XREF.txt
; shows this address as eaCopyrightMessage; that is the 1541 drive's own $064D, a different address
; space entirely.)
optionWordTable:
.byte "OF",'F'|$80,"O",'N'|$80,"FUL",'L'|$80,"HAL",'F'|$80,"NON"; 064D
.byte 'E'|$80 ; 064D
; msgClicks: 10 bytes, ' CLICKS:' - the header of the score line drawn under the map. Clicks are
; Modem Wars' score unit. $C6BC prints it (A = $5E, Y = $06), then the game clock, then the next two
; strings in memory through printStringAtPointer, each followed by a 16-bit score from
; sideScoreLo/sideScoreHi ($92A2/$92A4).
msgClicks:
.byte " CLICKS",':'|$80 ; 065E
; msgUs: 7 bytes, ' US:'. Printed straight after the clock by the printStringAtPointer at $C6CB,
; which continues from wherever the previous string ended - which is why these three labels have to
; stay adjacent and in this order. The number that follows it comes from sideScore indexed by
; playerSide EOR 1 ($C6CE), so the score arrays look as if they are indexed by the side that conceded
; the points.
msgUs:
.byte " US",':'|$80 ; 0668
; msgThem: 8 bytes, ' THEM:', printed by the same routine at $C6D6 and followed by the score indexed
; by playerSide itself.
msgThem:
.byte " THEM",':'|$80 ; 066F
; msgTypeMapName: 40 bytes, three lines separated by $0D: 'TYPE MAP NAME' / ' (F7 RANDOM)' / ' (F5
; SYM/RND)'. Printed by the map chooser at $76C9 (A = $77, Y = $06) while the player types the
; base-32 name of the battlefield both sides have agreed on, or picks a random / symmetrical-random
; one. (The manual look-up check at $09EF prints its own prompt from $0A47, not this one.)
msgTypeMapName:
.byte "TYPE MAP NAME",$0D," (F7 RANDOM)",$0D," (F5 SYM/RND"; 0677
.byte ')'|$80 ; 0677
; msgBtnToAccept: 27 bytes, two lines separated by $0D: 'BTN TO ACCEPT' / '(F 1 TO FLIP)'. Printed at
; $76D9 while the generated map is on screen; the fire button accepts it and F1 shows it rotated 180
; degrees (mapFlipFlag $0B9D).
msgBtnToAccept:
.byte "BTN TO ACCEPT",$0D,"(F 1 TO FLIP",')'|$80; 069F
; ----------------------------------------------------------------------
; switchToRequestedScreen - the game's screen dispatcher, called once per iteration from every
; screen's main loop to honour a pending F1/F3/F5/F7 keypress. It returns at once while the chat
; editor has the keyboard, while no function key is pending, or when the key names the screen that is
; already showing; otherwise it makes the request current, throws the caller's stack away, turns every
; sprite off, rebuilds the bitmap display and jumps into the new screen's entry point - so it never
; returns in that case.
; In: zp_66 requestedScreenKey ($00 F1, $01 F3, $02 F5, $03 F7, $FF or a typed character = no
; request), $90FB currentScreen, $929C chatState, $0B9B bit 7 filmPlaybackMode
; Out: either RTS with nothing changed, or: $90FB updated, sprites and their shadows ($9020, $9022,
; $9023, $9024) cleared, $92F9 cleared unless a film is replaying, S = $FD, and a JMP to
; enterMapScreen ($1FC3, screen 0, with $9020 preset to $C4), runMissileScreen ($7587, screen 1),
; enterDroneScreen ($7FE6, screen 2) or enterMainMenu ($141D, anything else)
; Called from: the console loop $1463, the battlefield loop $2033 and, in the comcen-screens overlay,
; the missile screen $75C3 and the drone screen $8040. Screens 1 and 2 live in that overlay, so
; they can only be reached while it is resident. (XREF.txt also lists the map generator as
; calling $75C3 and $8040; that is an artefact of the two $6F00-$87FF overlays sharing one address
; range - in the map generator those two addresses hold unrelated code.)
; ----------------------------------------------------------------------
switchToRequestedScreen:
lda chatState ; 06BA $929C is non-zero while the chat line editor owns the keyboard
bne L_06C8 ; 06BD refuse to switch in the middle of typing a message
lda requestedScreenKey ; 06BF zp_66 = the last key seen by the IRQ poll; the four function keys decode to 0-3 in keyMatrixCodeTable, everything else has bit 7 set
bmi L_06C8 ; 06C1 nothing pending: $FF (no key) and any typed character ($80-$FF) are both negative
cmp currentScreen ; 06C3 $90FB = the screen already on show: 0 battlefield map, 1 comcen missile, 2 drone, 3 console/main menu
bne L_06C9 ; 06C6 a different screen was asked for - go and switch
L_06C8:
rts ; 06C8 same screen or nothing to do: back into the caller's main loop
L_06C9:
sta currentScreen ; 06C9 the requested screen becomes the current one
ldx #$FD ; 06CC $FD is the game's 'empty stack' value ...
txs ; 06CE ... reset it, because this routine jumps into the new screen instead of returning (mainStart does the same at $0AAC)
lda #$00 ; 06CF zero for all the sprite registers below
sta spriteEnableShadow ; 06D1 $9022 = the shadow of $D015 that the IRQ copies out every frame
sta VIC_SPR_ENA ; 06D4 and switch all eight sprites off immediately as well
sta spriteXMsbShadow ; 06D7 $9020 = shadow of $D010, the sprite X bit 8 flags
sta spriteXExpandShadow ; 06DA $9023 = shadow of $D01D, sprite X expansion
sta spriteYExpandShadow ; 06DD $9024 = shadow of $D017, sprite Y expansion
bit filmPlaybackMode ; 06E0 $0B9B bit 7 = a game film is being replayed
bmi L_06E8 ; 06E3 during a replay leave the drone flags alone - the recorded command stream has to play back exactly as it was taken down
sta droneArrivalFlags ; 06E5 clear the drone handshake flags $92F9 (bit 0 = a 'drone finished' command $92 is already queued, bits 4-5 = the launch/arrival states) so the new screen starts clean
L_06E8:
jsr initBitmapScreen ; 06E8 rebuild the whole bitmap display: VIC registers, video matrix, colour RAM and the window frames
lda currentScreen ; 06EB dispatch on the new screen number
bne L_06F8 ; 06EE not the battlefield map
lda #$C4 ; 06F0 $C4 = %11000100: preset the X bit-8 flag for sprites 2, 6 and 7 - the map screen's sprites that live in the right-hand half of the display
sta spriteXMsbShadow ; 06F2 into the $D010 shadow, before the map screen puts its sprites up
jmp enterMapScreen ; 06F5 screen 0 (F1): the battlefield map at $1FC3
L_06F8:
cmp #$02 ; 06F8 screen 2?
bne L_06FF ; 06FA no
jmp enterDroneScreen ; 06FC screen 2 (F5): the drone screen at $7FE6, in the comcen-screens overlay
L_06FF:
cmp #$01 ; 06FF screen 1?
bne L_0706 ; 0701 no - then it can only be 3
jmp runMissileScreen ; 0703 screen 1 (F3): the comcen missile screen at $7587, in the comcen-screens overlay
L_0706:
jmp enterMainMenu ; 0706 screen 3 (F7): the console with its STATS / REPAIR / MISC panels at $141D
; ----------------------------------------------------------------------
; runComcenStunnedBlackout - the 'COMCEN STUNNED!' cut-scene: when your command centre has been
; knocked out, control is taken away, the screen goes blank and the engine keeps running unwatched for
; exactly one full round of all 100 units before the game hands control back. It releases the drone,
; clears the screen, resets the stack, then loops on updateGameFrame with the input lockout re-armed
; and bit 6 of comcenStunStatus set, which makes the sprite enabler at $31A6 and the cell plotter at
; $5C7C give up so that nothing is drawn at all, until unitOrderIndex comes back to the value it had
; on entry; then it queues message $0C, unlocks the input, clears bits 4-6 and re-enters whichever
; screen was showing.
; In: $9162 unitOrderIndex (captured into the CMP operand at $073E), $922A comcenStunStatus, $90FB
; currentScreen, $92C9 gameEndCountdown and $0BA8 gameEndReason (abort conditions), $90FC
; stepDelayTimer
; Out: one blind engine round; $0B7D re-armed each step and cleared at the end; $922A left at bit 7 +
; countdown; message $0C queued through $C566; never returns - it jumps to runMapMainLoop ($1FD0,
; after redrawing the map and viewport), missileScreenResume ($759C), resetDroneScreen ($7FF6) or
; enterMainMenuKeepSelection ($1414)
; Called from: JMP at $4546 only, taken after a unit step when comcenStunStatus has reached $FF
; (written by $730D when the missile hits the comcen) and no film is being replayed. During a
; replay the cut-scene is skipped and the overlay queues the message on its own at $72F6 instead.
; ----------------------------------------------------------------------
runComcenStunnedBlackout:
jsr releaseDroneControl ; 0709 if a drone is still in the air, hand it back ($8230 in the comcen-screens overlay) - the player is about to lose control of everything
jsr clearMessageLineAndScreen; 070C wipe the message line and the screen windows: this is the blackout
ldx #$FD ; 070F this routine never returns to the caller of $4546 ...
txs ; 0711 ... so reset the stack to the game's empty value
lda unitOrderIndex ; 0712 $9162 = how far the round's 100-unit process order has got; $4537 has just bumped it
sta L_073D+1 ; 0715 self-modifying: this is the operand byte of the CMP at $073D ($073E), so the loop below ends when the round order comes back round to this same unit - exactly one full pass of all 100 units
lda #$CF ; 0718 $CF = %11001111: keep bit 7 (comcen stunned), set bit 6 (run blind) and load the low nibble with a full $F stun countdown. Just as important, it is no longer $FF, so the test at $453A cannot start this cut-scene again on the next unit step
sta comcenStunStatus ; 071A arm the blackout
L_071D:
lda comcenStunStatus ; 071D top of the blackout loop: re-read the status byte, because updateGameFrame may have changed it
ora #$40 ; 0720 bit 6 suppresses all drawing: the sprite enabler at $31A6 and the viewport plotter at $5C7C both give up when it is set, and updateGameFrame itself skips the input handling at $40BA
sta comcenStunStatus ; 0722 store it back
sta inputLockoutTimer ; 0725 the same $CF value doubles as the input lockout count: the IRQ decrements $0B7D every frame and ignores the keyboard and joystick while it is non-zero. It is re-armed on every step, so the player stays locked out for the whole round
L_0728:
lda gameEndCountdown ; 0728 $92C9 counts down the end-of-game sequence; non-zero means the game is already over
bne L_0746 ; 072B the game is ending - stop the blackout now
lda gameEndReason ; 072D $0BA8 = 0 while a game is running (1 aborted, $08 time up, $A0/$C0 a comcen was knocked out)
bne L_0746 ; 0730 the game has ended - stop
lda stepDelayTimer ; 0732 $90FC = frames still to wait before the engine may take its next unit step
bne L_0728 ; 0735 not yet: spin here, re-testing both abort conditions while the IRQ ticks the timer down
jsr updateGameFrame ; 0737 one engine step: one unit is processed, with the input locked out and nothing drawn
lda unitOrderIndex ; 073A where has the round order got to now?
L_073D:
cmp #$FF ; 073D the immediate operand was patched at $0715 with the index the round was at when the comcen went down
bne L_071D ; 073F keep stepping until it comes round again - the index runs 0..99 and is reset to 0 at $474D when the round finishes
lda #$0C ; 0741 message id $0C = 'COMCEN STUNNED!' (the string lives at $8742 in the comcen-screens overlay; sound $04)
jsr queueMessage ; 0743 put it in the 8-entry message ring at $9249
L_0746:
lda #$00 ; 0746 0 = no lockout ...
sta inputLockoutTimer ; 0748 ... give the keyboard and the joystick back to the player
lda comcenStunStatus ; 074B and stop running blind:
and #$8F ; 074E clear bits 4-6 (bit 6 was the blackout), keeping bit 7 'comcen stunned' and the low-nibble countdown that the console's REPAIR panel shows
sta comcenStunStatus ; 0750 store the tidied status byte
ldy currentScreen ; 0753 return to whichever screen was showing when the comcen was hit
bne L_0761 ; 0756 not the battlefield map
jsr drawOverviewMap ; 0758 screen 0: redraw the 40x40 overview panel that the blackout wiped ...
jsr drawViewport ; 075B ... and the 7x5-cell tactical viewport
jmp runMapMainLoop ; 075E back into the map's main loop, not its entry point - the screen has just been set up by hand
L_0761:
dey ; 0761 screen 1?
bne L_0767 ; 0762 no
jmp missileScreenResume ; 0764 the missile screen has its own re-entry point for exactly this case ($759C)
L_0767:
dey ; 0767 screen 2?
bne L_076D ; 0768 no - then it is the console
jmp resetDroneScreen ; 076A re-initialise the drone panel; the drone itself was released at $0709
L_076D:
jmp resumeMainMenuScreen ; 076D screen 3: back to the console, keeping the menu item that was selected
; tileVariantMaskTable: 40 bytes ($0770-$0797), indexed by map cell value - $40, and by cell - $20 for
; cells below $40 (randomTileVariant $7872 subtracts $40 and adds $20 back if that went negative), so
; the first 32 entries serve both the $20-$3F and the $40-$5F cell ranges and the last 8 serve
; $60-$67. The byte is a bit mask: the map generator ANDs a fresh scenario random with it and adds the
; result to the base tile, so a mask of 0 means 'always this exact tile', 1 means 'one of two' and 3
; means 'one of four'. Only eight entries are non-zero: cells $49, $4B and $65 (and $29, $2B) get
; mask 1, cells $4D,
; $51, $59, $5D, $61 (and $2D, $31, $39, $3D) get mask 3.
tileVariantMaskTable:
.byte $00,$00,$00,$00,$00,$00,$00,$00; 0770 ........ cells $40-$4D (and $20-$2D): all fixed except $49 and $4B (mask 1) and $4D (mask 3)
.byte $00,$01,$00,$01,$00,$03 ; 0778 ......
; $077E is also the base address of terrainClassTable below: everything that wants the terrain class
; of a
; cell reads $077E + cell value, so the class bytes really start at $077E+$21 = $079F. The 26 bytes
; here are still tile variant masks (cells $4E-$67), which is why the two tables have to sit exactly
; this far apart.
D_077E:
.byte $00,$00,$00,$03,$00,$00,$00,$00; 077E ........ cells $4E-$67 (and $2E-$3F): mask 3 at $51, $59, $5D and $61, mask 1 at $65, everything else fixed
.byte $00,$00,$00,$03,$00,$00,$00,$03; 0786 ........
.byte $00,$00,$00,$03,$00,$00,$00,$01; 078E ........
.byte $00,$00 ; 0796 ..
; tileVariantPad: the single $00 that pads the variant masks out to the glyph table. As terrain class
; data it would be the class of cell $1A, which never occurs.
tileVariantPad:
.byte $00 ; 0798 . $00 pad; read as terrain class data through the $077E base this would be the class of cell $1A, which never occurs
; unitGlyphBaseTable: 5 bytes plus a pad, the base glyph number of the 16x16 unit picture for each
; unit type: $2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN. The facing (unit flags AND 3) is
; added to it, then $2C is subtracted and the result doubled to index unitGlyphPointerTable, i.e.
; glyph = type*4 + facing ($289A-$28A8 for the tactical view, $7512 and $7538 for the radar screen,
; where the enemy version has bit 7 set). The trailing $00 at $079E is both the pad and, read as
; terrain class data through the $077E base, the class of cell $20 - class 0, blank ground.
unitGlyphBaseTable:
.byte $2C,$30,$34,$38,$3C,$00 ; 0799 ,048<. $2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN, then the $00 pad at $079E
; terrainClassTable: map cell value -> terrain class 0-8, 75 bytes covering cells $21-$6B. Every
; reader addresses it as $077E + cell (see the note there); $5BC8 gets to the same bytes as $07BE +
; (cell-$40) after clamping the index below $21. The class is what the rest of the engine works with:
; terrainMoveCostTable $405E (move points charged per step: 20, 65, 30, 40, 20, 40, 20, 20),
; terrainHeightTable $38B3 (0, 0, 0, 1, 2, 3, 4, 0 - line of sight and the combat height bonus),
; terrainSightBonusTable $35BB (0, 0, 0, 1, 1, 2, 2, 0 - extra range, doubled when spotting) and
; terrainColourTable $07EA are all indexed by it. Class 2 (woods) is also tested by name at $36B5 and
; $3729 because it blocks line of sight. Note that those four engine tables only have eight entries,
; so class 8 - the off-map border - runs off the end of them; nothing ever walks or shoots there.
terrainClassTable:
.byte $03,$03,$03,$03,$03,$03,$03,$03; 079F ........ cells $21-$34 -> class 3, the lower contour band: costs 40 move points, height 1, +1 sight
.byte $03,$03,$03,$03,$03,$03,$03,$03; 07A7 ........
.byte $03,$03,$03,$03 ; 07AF ....
.byte $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF; 07B3 ........ cells $35-$3F -> $FF, an invalid class: these cell values are never put on the map
.byte $FF,$FF,$FF ; 07BB ...
D_07BE:
.byte $00 ; 07BE . cell $40 -> class 0, plain open ground: 20 move points, no height, no sight bonus, drawn yellow
.byte $05,$05,$05,$05,$05,$05,$05,$05; 07BF ........ cells $41-$54 -> class 5, the upper contour band: 40 move points, height 3, +2 sight
.byte $05,$05,$05,$05,$05,$05,$05,$05; 07C7 ........
.byte $05,$05,$05,$05 ; 07CF ....
.byte $01,$01,$01,$01,$01,$01,$01,$01; 07D3 ........ cells $55-$5C -> class 1, river: 65 move points, the most expensive terrain in the game, drawn cyan
.byte $02,$02,$02,$02 ; 07DB .... cells $5D-$60 -> class 2, woods: 30 move points and the only terrain that blocks line of sight, drawn green
.byte $04,$04,$04,$04 ; 07DF .... cells $61-$64 -> class 4, hill: 20 move points, height 2, +1 sight
.byte $06,$06 ; 07E3 .. cells $65-$66 -> class 6, summit: 20 move points, height 4 (the best view in the game), +2 sight
.byte $07 ; 07E5 . cell $67 -> class 7, the recycler pad where damaged units are repaired ($3F27 stamps $67 into the map)
.byte $08,$08,$08,$08 ; 07E6 .... cells $68-$6B -> class 8, the off-map border: $278A builds these four values as $68 + ((row+col) AND 3) for any cell outside the 40x40 battlefield
; terrainColourTable: 9 bytes, the colour-RAM nibble for terrain class 0-8, read at $27AB (viewport)
; and
; $5BCB (overview map) straight after a class look-up: 7 yellow open ground, 3 cyan river, 5 green
; woods,
; 8 orange for the contour bands, hills and summits, 0 black for the recycler pad and the off-map
; border.
terrainColourTable:
.byte $07,$03,$05,$08,$08,$08,$08,$00; 07EA ........ class 0-8: $07 yellow, $03 cyan, $05 green, $08 orange x4, $00 black x2
.byte $00 ; 07F2 .
; table0400Tail: 13 bytes of $00 filler to the end of the page. The block move that installed the
; page copies four whole pages, so this is simply whatever followed the tables in the source file.
table0400Tail:
.byte $00,$00,$00,$00,$00,$00,$00,$00; 07F3 ........ unused filler
.byte $00,$00,$00,$00,$00 ; 07FB .....