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