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

5335 lines
357 KiB
ArmAsm

; ============================================================================
; $6F00-$87FF - random battlefield generator and setup phase (track 31 sectors 0-16 + track 32 sectors 0-7)
; ============================================================================
; Loaded by $1047. Generates the 40x40 map (river, hills, forest, end zones) from the game seed, derives
; the 5-character map id the two players compare, runs the unit setup screen and saves/loads game films.
; Shares its address range with the comcen screens overlay, which replaces it once the battle starts.
.setcpu "6502"
.include "c64.inc"
.include "zeropage.inc"
; ---- references to code/data outside this file ----
contourRuleTable := $0200
basicVectorHijack := $0300
play1Row07 := $0301
play1Row08 := $0302
play1Row10 := $0304
play1Row13 := $0307
storeRngSeedByte1 := $030F
play1Row22 := $0310
loadRngSeedByte2 := $0311
play1Row24 := $0312
storeRngSeedByte2 := $0313
play1Row26 := $0314
play1Row34 := $031C
play1Row42 := $0324
storeFontEndLo := $0325
storeFontEndHi := $0329
invertFontByteLoop := $032D
play2Grp02 := $032E
play2Grp06 := $0332
checksumXorConstant := $0400
play3Row42 := $0450
play4Col22 := $04A0
optionSummaryTemplate := $0636
optionWordTable := $064D
tileVariantMaskTable := $0770
D_077E := $077E
loadSectors := $0804
writeSectors := $0843
setLoadDest := $085C
startGameFromSetup := $0AC8
inputLockoutTimer := $0B7D
mapSeed := $0B95
mapSeed1 := $0B96
mapSeed2 := $0B97
prevMapSeed := $0B98
prevMapSeed1 := $0B99
prevMapSeed2 := $0B9A
filmPlaybackMode := $0B9B
linkSessionState := $0B9C
mapFlipFlag := $0B9D
prevMapFlipFlag := $0B9E
playerSide := $0B9F
setupPlayerSide := $0BA0
playsFirstFlag := $0BA1
savedPlaysFirstFlag := $0BA2
gameTypeOptions := $0BA3
prevGameTypeOptions := $0BA4
isSoloTrainer := $0BA5
diskLoadDepth := $0BA7
gameEndReason := $0BA8
mapNameText := $0BA9
mapNameTerminator := $0BAE
mapNameBackup := $0BAF
comcenUnitIndexBySide := $0C8A
showModemSetupPrompt := $0C8C
setJoystickRepeatDelay := $0D49
checkFirePressed := $0EB1
loadSectorsDelayed := $0F55
checkDiskId := $0FB1
L_100D := $100D
diskErrorExit := $1018
loadCommTailBuild2 := $1070
suspendCommForDiskAccess := $10CB
resumeCommAfterDiskAccess := $10D1
waitForGameDiskInserted := $10DC
clearMenuTextWindow := $156F
openCommLink := $1B5F
waitForConnection := $1B77
runMapMainLoop := $1FD0
D_206B := $206B
D_2084 := $2084
runTargetCursorLoop := $2185
getViewportOriginPointer := $25C7
readMapCell := $25FA
clearInfoPanel := $261C
clearScreenAndRedrawGameScreen := $2636
clearViewportTextArea := $26A1
drawViewport := $26C1
ownSideBranchOpcode := $2840
printOptionWord := $2A1A
positionCursorSprite := $3049
D_36D5 := $36D5
leftoverScanSideBranch := $379C
D_3B2C := $3B2C
D_3BD0 := $3BD0
initAllUnits := $3D8A
placeUnitOnMap := $3DDA
removeUnitFromMap := $3EBD
drawRecyclerOnMap := $3F27
initialFacingSideBranch := $3F9E
mirrorMapCoordinate := $3FA3
updateGameFrame := $40B2
L_4657 := $4657
zoneRowSideBranch := $4783
shuffleUnitOrder := $47ED
serviceCommandExchange := $4DA9
D_4DF4 := $4DF4
sendChooseSide := $553B
queueCmdBytePending := $55EC
queueCmd4 := $560A
queueCmd3 := $560F
queueCmd2 := $561F
sendOutgoingPacket := $562B
readyForNewGameHandshake := $572B
D_57C3 := $57C3
readFilmByte := $5874
writeFilmByte := $5893
copyPageUnderIo := $58B2
D_58C3 := $58C3
D_58C4 := $58C4
D_58C6 := $58C6
D_58C7 := $58C7
verifyFilmHeader := $58DE
waitForExchangeIdle := $5915
writeFilmSetupSnapshot := $5929
setFilmLimitToBufferEnd := $598D
initFilmStream := $5998
drawOverviewMap := $5AD7
D_5BF7 := $5BF7
clearOverviewWindow := $5E03
runMenuLoopWithFlag := $5FD1
unhighlightMenuRow := $60E5
toggleMenuRowHighlight := $60EA
groupEditorOwnSideBranch := $63E2
mapNameAlphabet := $890C
spriteEnableShadow := $9022
spriteBuildBuffer := $9025
newKeyEvent := $90EA
textCentreWidth := $90EF
selectedUnit := $90F8
currentScreen := $90FB
unitProcessOrder := $90FD
unitBaseOrder := $9164
gameClock := $91CB
menuItemCount := $91D4
menuSelectedItem := $91D5
menuFirstRow := $91D6
pickedCellCol := $91D8
pickedCellRow := $91D9
currentUnitCol := $91DA
currentUnitRow := $91DB
droneFuel := $9220
droneRadarOwnColour := $9224
droneRadarEnemyColour := $9225
overviewUnitPattern := $922B
comcenUnit := $9236
pendingRedrawGroup := $923A
pendingRedrawUnit := $923B
comcenBattleStatus := $9242
lastTypedKey := $9248
messageQueueHead := $9251
messageQueueTail := $9252
incomingChatLength := $9277
typedLineBuffer := $9278
chatState := $929C
lastOrderedGroupKey := $929D
pendingOwnCmdCount := $929F
persistentMessageId := $92A1
recyclerMode := $92A6
side1RecyclerMode := $92A7
recyclerCol := $92A8
side1RecyclerCol := $92A9
recyclerRow := $92AA
terrainPointsBySide := $92AC
dronesLeft := $92AE
opponentDronesLeft := $92AF
missilesLeft := $92B0
comcenSpeedClass := $92B4
menuBlinkState := $92C5
setupPhaseMode := $92C6
comcenStatusSnapshot := $92D6
stepIntervalFrames := $92E3
filmNameLength := $92E6
lastQuarterRound := $92EF
diskIdIndex := $92F0
setupToggleFlag := $92F3
lastQuarterFlag := $92FA
revealAllUnits := $92FC
defaultMainMenuItem := $92FD
soloTrainerAtGameStart := $92FE
printChar := $C01A
printNewline := $C041
bootToggleDataAck := $C07A
setCursorRow := $C097
updateCursorPointers := $C099
bootReceiveSectorData := $C0EF
printTableEntry := $C100
printStringCentred := $C121
printNextStringCentred := $C12B
printString := $C133
printStringAtPointer := $C137
printDecimal := $C17D
waitFrames := $C37F
clearMessageLine := $C39F
queueMessage := $C566
resetMessageQueue := $C6EF
showWaitingForOpponent := $C7C3
handleTextEntryKey := $C7D5
showWorkingMessage := $C853
playSound := $C89A
setMessageSlot := $C924
msgCustom := $CC19
exchangeFlags := $E01D
isLinkActive := $E03B
clearBattlefieldMap := $EC00
initTrainerAi := $EC0F
setViewOriginToHome := $EC1C
loadMapSeedIntoScenarioRng := $EC2B
makeMapPointSymmetric := $EC54
rotateMapAndMirrorCodes := $EC58
mirrorAllUnitCoordinates := $EC91
loadUnitStartTemplate := $ECF3
exchangePlayerNamesAndSettings := $ED43
measureRangeToTarget := $EE26
loadFilmStartSnapshot := $EE4A
toggleBattleDisplayMode := $EEDD
addRowDeltaToDroneRow := $EF77
diskNameSlot := $EFB6
D_F30B := $F30B
D_F333 := $F333
D_F334 := $F334
D_F600 := $F600
unitColTable := $F640
unitTypeTable := $F76C
side0ComcenType := $F79D
unitTerrainUnderAlias := $FA70
unitTerrainUnderTable := $FAF0
multiply8x8 := $FCCD
addRandomJitter := $FD1E
nextScenarioRandom := $FD4A
nextGameRandom := $FD5D
ownPlayerName := $FFDE
irqVectorHi := $FFFF
; Contents
; --------
; $6F04 generateBattlefieldMap entry point of the procedural battlefield generator.
; $6F36 runMapGeneratorPasses the eight terrain passes, entered by fall-through from
; $6F91 serviceLinkWhileGenerating lets the opponent link breathe during the long map build.
; $6F9E pickHillSummitSites decides how many hill summits the map gets and finds a blank cell for each.
; $701C findBlankSitesOnLanes gives every summit site from zp_2A to $0300-1 a column and a row.
; $708E advanceLaneIndex steps the lane cursor zp_2B down through the four shuffled lane columns in
; $7097 advanceRowCandidate steps the candidate-row cursor zp_29 down through $0325[0..zp_28],
; $70A0 shuffleLaneColumns builds a random permutation of 0-3 (identity, then four random transpositions)
; $70D9 paintHillSummits paints every site chosen by pickHillSummitSites as a blob of summit tiles.
; $710A growLowerHillsBesideSlopes second terrain layer.
; $71B0 growTerrainBlob the generic random blob painter used by every terrain pass.
; $72F6 checkAlignedBlockForForest constrains forest to whole 2x2 blocks on an even grid.
; $7340 placeForestClusters the last terrain pass.
; $73AD paintForestClusterSeeds paints eight small forest blobs covering the 4x2 cell block from
; $73D4 paintForestBlobAt grows one 4-cell forest blob at (zp_93,zp_92) if both coordinates are inside
; $73EE applyContourTiles draws the contour ring around a hill layer.
; $7444 applyContourRuleAtCell decides what one cell becomes, from which of its eight neighbours are
; $74FE clearMapMarkers Clean-up pass of the map generator: walks all 1600 cells of the 40x40
; $7548 generateRiver Draws the river across the whole battlefield, left edge to right edge, as the
; $759E chooseNextRiverSegment Picks the segment type of the next river cell into genTileType ($0312).
; $761C advanceRiverPosition Moves the river cursor one cell according to the segment just drawn: type 1
; $762E riverStepRight Step stub for every segment type that leaves the cell to the right (0, 2 and
; the
; $7632 riverStepUp Step stub for segment type 3, the elbow that turns the river upward: moves the
; $7636 riverStepDown Step stub for segment type 1, the elbow that turns the river downward: moves
; the
; $764A markRiverBanks Reserves the ground around the current river cell so the hill and forest passes
; $76AD mapNameEntryScreen The 'TYPE MAP NAME' page of the game setup.
; $7717 enterMapName Reads a five-character map name at column 4, row 16 and converts it into the
; $782D setMapNameCursor Moves the text cursor to the start of the map-name field: column 4 of the
; $7836 clearMapNameField Fills the five map-name characters $0BA9-$0BAD with spaces.
; $7841 printMapName Prints the five-character map name at its place on the setup screen.
; $7850 seedToMapName Spells a 24-bit map seed out as its five-character name: five times take the
; low
; $7872 randomTileVariant Picks a random visual variant of a terrain code so that large areas of the
; $78D1 startGameSession The whole 'start a game' sequence, from a cleared battle state to the first
; $7BB2 shuffleInitialUnitOrder builds the order in which the 100 units are processed each round.
; $7BC6 patchOwnSideBranches burns the local player's side into the engine's self-modifying
; $7BF9 sendPhaseSyncAndWait the 'I am ready' handshake that separates the setup phases.
; $7C1C waitForMapSeedCommand waits until the map seed sent by the other machine has arrived.
; $7C2E applyGameTypeDefaults loads the per-side game parameters of the chosen game type from the two
; $7CA2 removeUnitsUpToY marks unit slots Y down to 0 as 'not in this game': unitTypeTable ($F76C)
; $7CAE selectSideDefaultsTable points zp_48/zp_49 at the game-type default table of side X: $892C
; $7CBF readDefaultAndAdvanceRow reads the byte of the current 5-byte table row that belongs to the
; $7CD1 startFilmPlayback the game-start path taken when WATCH GAME FILM was chosen ($0B9B bit 7).
; $7D70 initUnitOrderLists fills the two 100-entry unit order lists unitBaseOrder ($9164) and
; $7D7D placeAllUnitsOnMap runs placeUnitOnMap for every unit 99 down to 0, which resets the unit's
; $7D8B setGameClockForType looks the length of the game up in roundsByGameType ($78C3: 63, 63, 31,
; $7D98 clearUnitsAndRecyclersFromMap takes every unit 99..0 off the map (removeUnitFromMap puts the
; $7DA7 clearRecyclerCell blanks side X's recycler cell: if that side has a recycler (recyclerCol bit
; $7DB8 waitForSpaceKey 'PRESS SPACE WHEN READY' wait used by the modem setup prompts.
; $7DE3 handleMainMenuChoice acts on the item the player picked in the main menu.
; $7E39 setModeFlagsAndStartGame shared tail of handleMainMenuChoice: X becomes isSoloTrainer $0BA5
; $7E4F checkDiskAndLoadFilmDirectory works out which disk is in the drive and loads its film
; $7E74 writeFilmDirectory writes the 512-byte film directory (30 entries of 16 bytes plus slack)
; $7E8B loadFilmDirectory reads track 18 sectors 2-3 (the 30 x 16-byte film names) into $0300-$04FF.
; $7EA0 drawFilmDirectoryPage draws one page of the film menu: the centred title 'SAVE GAME FILM' or
; $7F3F runLoadFilmMenu LOAD GAME FILM.
; $7FA6 setFilmDirectoryPagePtr points zp_48/zp_49 at the first directory entry of the current page
; $7FCB nextFilmDirectoryPage steps filmDirectoryPage on modulo 3 and redraws the list.
; $7FDD runSaveFilmMenu SAVE GAME FILM.
; $810C printFilmSlotEntry redraws the currently selected row of the film menu in colour A: moves the
; $8143 filmSlotToTrack turns the selected directory slot into the disk track its film occupies:
; $815E saveFilmToSlot writes the film in the buffer under I/O to the disk: rewrites the directory
; $81A5 promptInsertGameDisk end of the save/load film flow: shows 'INSERT GAME DISK AND PRESS
; $81B2 promptInsertDataDisk entry of promptInsertDiskAndWaitSpace with the word 'DATA' ($EFD0).
; $81B6 promptInsertDiskAndWaitSpace shows 'INSERT xxxx DISK AND PRESS SPACE.' and waits for the
; $81F4 checkFilmInMemory is the buffer under I/O at $D000 a valid game film? verifyFilmHeader
; $81FE waitFramesServicingComm waits about four seconds (240 IRQ frames counted down in zp_6C)
; $820B beginGameTypeSelection one-off prologue of the game-type selection screen, run only by the side that
; $821F runGameTypeMenu the GAME TYPE: selection screen.
; $8279 printGameTypeNamesLoop not a routine in variant A: this is the body of runGameTypeMenu's loop that
; $82A4 applyCustomPresetTail not a routine in variant A: the continuation inside runGameTypeMenu that ORs
; $82BF runMapTypeMenu second half of the setup menus: the STANDARD / CUSTOM map choice
; (gameTypeOptions
; $833C sendGameTypeCommand queues the 2-byte command $A4 (cmdSetGameType, handler $559C) carrying the
; $8343 printMenuTitle common menu prologue: preselects row X, prints the title string A/Y centred in
; the
; $8356 runMenuWidth10 runs the generic selector with a 10-character highlight bar; falls into
; $8358 runMenuWithWidth sets the highlight width, backs the window up one column so the bar has a
; leading
; $8364 resetSelectionAndView blanks the tactical viewport for a full-screen menu: sets $9242 bit 7 (the
; $8378 chooseRecycler RECYCLER setup for the scenarios that have one (game types 4-6).
; $843B printOptionWordLine prints option word A (0 FULL, 1 HALF, 2 NONE, from the 4-byte entries at $0652)
; $8441 clearInfoPanelSetRow sets the text and pad colours to 8 (orange), blanks the info panel window and
; $8451 editValueWithJoystick the generic numeric option editor of the custom setup.
; $84AA applyHandicapPoints last step of the game-start options.
; $84EC editTerrainPoints the CUSTOM option 'TERRAIN PTS' (0..2).
; $84F1 editTerrainPointsForSide entry into editTerrainPoints with the $92AC index already in A.
; $8511 editDronesAndMissiles the CUSTOM options 'DRONES' (0..9) and 'MISSILES' (0..36), edited one after
; $8518 editDronesAndMissilesBody not a real entry point in variant A: it is the JSR beginValueEdit inside
; $8547 runGameStartOptions the screen shown once per game after the setup has been saved and before unit
; $85BC editComcenSpeed the CUSTOM option 'COMCEN SPEED' (0..3).
; $85DB beginValueEdit common prologue of all four value editors: remembers the label string in two
; $8601 setValueEditWindow tail of beginValueEdit: marks the menu as a plain one (no unit attached) and
; sets
; $860B editOptionBitOnOff a two-row OFF/ON menu for one bit of gameTypeOptions, used by the game-type
; $862D runOnOffMenuAndApplyBit tail of editOptionBitOnOff that sets the item count and falls into the
; $8632 printOnOffMenuAndApplyBit inner tail of editOptionBitOnOff: prints the words OFF ($064D) and ON
; $8658 handleStarKeyToggle per-frame hook the main program calls while overlay A is resident and the game
; $8673 showToggleStateMessage shows the state of the '*' toggle.
; $86AA setDestroyModeMessage re-aims the '*' toggle at the unit-placement phase: the off-state persistent
; $86C3 showCustomSummaryMessage builds the status line 'CUSTOM:' [+ 'MAP,' when a named map is in use] +
; $8722 setPersistentMessage registers the string A/Y as message slot $1E (setMessageSlot writes the pointer
; $872E appendOnOffWord writes 'OFF,' (A = 0) or 'ON, ' (A != 0) into the template at $0636 starting at
; $8756 showScenarioBannerAndWaitForButton prints 'STANDARD' or 'CUSTOM' (from $CC11, chosen by $0BA3 bit
; $8795 printCentredTableEntry sets the centring field width to 13 columns and prints entry X of a table of
.org $6F00
; laneColumnTable - the four vertical 'lanes' of the battlefield along which hill summits are placed:
; columns 6, 15, 25 and 34 of the 40-column map. shuffleLaneColumns copies them into
; $0329-$032C in a random order so successive summits land in different parts of the map.
; (The label is only marked sub_6F00 because the comcen-screens overlay has code at this address.)
laneColumnTable:
.byte $06,$0F,$19,$22 ; 6F00 ..." lane columns 6, 15, 25, 34 (indexed 0-3 by the shuffled permutation)
; ----------------------------------------------------------------------
; generateBattlefieldMap - entry point of the procedural battlefield generator. Derives the map-style
; flags for the chosen game type, decides whether the map is point-symmetric, puts 'WORKING...' on the
; status line, blanks all 1600 map cells to $40 and re-seeds the scenario RNG from the 24-bit map
; seed, then falls into runMapGeneratorPasses. The battlefield is never loaded from disk: seed + game
; type completely determine the terrain, which is why both machines produce the identical map and why
; a map can be named by its seed.
; In: gameTypeOptions $0BA3 bits 0-2 = game type 0-6, mapSeed $0B95-$0B97; needs the contour rule
; table at $0200 (track 18 s15) and the $EC00 comm-tail helpers resident
; Out: $032E map style, $032D symmetry flags, map $F000-$F63F built, scenario RNG $5A-$5C advanced,
; map name text $0BA9-$0BAD, generator scratch $0300-$032C, $0400-$04EF and $9025-$9028 clobbered
; Called from: manualLookupCheck ($09E0) to show the map the manual asks about, and from the setup
; screens of this overlay ($76AD map editor, $76FF, $7A02 game start, $7D23 film playback)
; Choose the map style from the game type: QB SNEAK and DEFENDER get special terrain.
; ----------------------------------------------------------------------
generateBattlefieldMap:
lda gameTypeOptions ; 6F04 gameTypeOptions from the settings block
and #$07 ; 6F07 bits 0-2 = game type (0 SCRIMAGE .. 6 DEFENDER)
tay ; 6F09 keep the type in Y for the two comparisons below
beq storeMapStyleFlags ; 6F0A type 0 (SCRIMAGE): A is already 0 = ordinary random map
lda #$40 ; 6F0C style $40 = DEFENDER map (river always, exactly 4 hills, 1-4 forest clusters)
cpy #$06 ; 6F0E game type 6 = DEFENDER?
beq storeMapStyleFlags ; 6F10 yes - keep style $40
lda #$80 ; 6F12 style $80 = QB SNEAK map (no river, at most 1 hill, 10 forest clusters)
cpy #$01 ; 6F14 game type 1 = QB SNEAK?
beq storeMapStyleFlags ; 6F16 yes - keep style $80
lda #$00 ; 6F18 every other game type: no special style
storeMapStyleFlags:
sta play2Grp02 ; 6F1A $032E mapGenGameFlags: bit 7 = QB SNEAK style, bit 6 = DEFENDER style
; ----------------------------------------------------------------------
; A map whose name ends in '=' (seed bits 0-4 all set) is built point-symmetric.
; ----------------------------------------------------------------------
ldy #$00 ; 6F1D assume an asymmetric map
lda mapSeed ; 6F1F low byte of the 24-bit map seed
and #$1F ; 6F22 bits 0-4 are the last character of the 5-letter map name
cmp #$1F ; 6F24 $1F = the alphabet's last symbol '='
bne storeMapSymmetryFlag ; 6F26 not a symmetric map code - leave the flag clear
ldy #$80 ; 6F28 bit 7 = build a point-symmetric battlefield
storeMapSymmetryFlag:
sty invertFontByteLoop ; 6F2A $032D mapSymmetryFlags (bit 6 is set later if the river crosses the centre) (also trainerPlaybook0200:play2Grp01)
jsr showWorkingMessage ; 6F2D show message 1 'WORKING...' - generating the map takes several seconds
jsr clearBattlefieldMap ; 6F30 fill the whole 40x40 map $F000-$F63F with blank terrain $40 (also trainerSetupEC00:mainGroupByPlanTable)
jsr loadMapSeedIntoScenarioRng; 6F33 scenario RNG $5A-$5C := mapSeed; returns the three seed bytes in A/Y/X
; ----------------------------------------------------------------------
; runMapGeneratorPasses - the eight terrain passes, entered by fall-through from
; generateBattlefieldMap. Order: map name from the seed; river with its keep-clear markers; hill
; summit sites; summit blobs; erase the $80 markers; upper contour ring; lower hill bodies; erase the
; $81 markers; lower contour ring; forest clusters. A symmetric map is finished by copying the top
; half onto the bottom half rotated 180 degrees, with a three-cell patch that joins the two halves of
; the river at the centre. The opponent link is serviced between passes.
; In: A/Y/X = the three map seed bytes, $032D symmetry flags, $032E map style, RNG $5A-$5C
; Out: the finished map at $F000-$F63F, map name text $0BA9-$0BAD, generator scratch $0300-$032C;
; ends with JMP resetMessageQueue, which clears 'WORKING...' and returns to the caller
; Called from: nothing in this overlay (fall-through only); the JSR $7125 the XREF lists belongs to
; the comcen-screens overlay that shares this address range
; Pass 1-2: name the map, then carve the river.
; ----------------------------------------------------------------------
runMapGeneratorPasses:
jsr seedToMapName ; 6F36 write the seed as the 5-character map name at $0BA9 (A/Y/X still hold it)
jsr generateRiver ; 6F39 carve the river ($55-$5C) and reserve its banks with the $80/$81 markers
; ----------------------------------------------------------------------
; Pass 3-5: hill summits, then the upper slope ring around them.
; ----------------------------------------------------------------------
jsr pickHillSummitSites ; 6F3C choose how many summits to place in the top half and on which lanes
jsr serviceLinkWhileGenerating; 6F3F give the modem/trainer link a turn - generation must not stall the exchange
jsr paintHillSummits ; 6F42 paint each summit as a blob of tile $65/$66
lda #$80 ; 6F45 $80 = the outer (wide) river keep-clear marker
jsr clearMapMarkers ; 6F47 turn those markers back into blank $40 before the contour pass reads them
jsr serviceLinkWhileGenerating; 6F4A service the link again
lda #$00 ; 6F4D contour offset 0: rule-table tiles are used unchanged ($41-$54)
sta play1Row42 ; 6F4F $0324 = amount subtracted from every rule-table tile code
lda #$65 ; 6F52 $65 = summit tile, terrain class 6
jsr applyContourTiles ; 6F54 draw the upper slope contour $41-$54 around everything of class >= 6
jsr serviceLinkWhileGenerating; 6F57 service the link
; ----------------------------------------------------------------------
; Pass 6-7: widen the hills into bodies and draw their lower contour ring.
; ----------------------------------------------------------------------
jsr growLowerHillsBesideSlopes; 6F5A grow lower-hill bodies ($61-$64) beside the upper slopes
lda #$81 ; 6F5D $81 = the inner river keep-clear marker
jsr clearMapMarkers ; 6F5F erase those markers too - the banks are now safe from the lower ring
lda #$20 ; 6F62 contour offset $20: rule tiles $41-$54 become the lower ring $21-$34
sta play1Row42 ; 6F64 $0324
lda #$61 ; 6F67 $61 = lower hill tile, terrain class 4
jsr applyContourTiles ; 6F69 draw the lower contour around everything of class >= 4
jsr serviceLinkWhileGenerating; 6F6C service the link
; ----------------------------------------------------------------------
; Pass 8: forest, then the optional 180-degree mirror for symmetric maps.
; ----------------------------------------------------------------------
jsr placeForestClusters ; 6F6F scatter the forest clusters ($5D-$60)
lda invertFontByteLoop ; 6F72 mapSymmetryFlags (also trainerPlaybook0200:play2Grp01)
bpl mapGeneratorDone ; 6F75 bit 7 clear - asymmetric map, nothing more to do
jsr makeMapPointSymmetric ; 6F77 overwrite the bottom half with the 180-degree rotation of the top half (also trainerSetupEC00:checkForTheBombGameType)
bit invertFontByteLoop ; 6F7A bit 6 = the river jogged up to row 19 at column 20 (set by chooseNextRiverSegment) (also trainerPlaybook0200:play2Grp01)
bvc mapGeneratorDone ; 6F7D the river does not meet itself at the centre - done
lda #$58 ; 6F7F $58 = river bend left/up
sta D_F333 ; 6F81 cell (19,20) = $F000 + 20*40 + 19
lda #$55 ; 6F84 $55 = river bend down/right
sta D_F30B ; 6F86 cell (19,19) = $F000 + 19*40 + 19
joinRiverClearCentre:
lda #$40 ; 6F89 $40 = blank terrain
sta D_F334 ; 6F8B cell (20,20): erase the mirrored stub so the two river halves join cleanly
mapGeneratorDone:
jmp resetMessageQueue ; 6F8E clear the 'WORKING...' message and return to generateBattlefieldMap's caller
; ----------------------------------------------------------------------
; serviceLinkWhileGenerating - lets the opponent link breathe during the long map build. Does nothing
; while a game film is being replayed (no opponent) or while the battle loop is running (the frame
; handler already services the link there); otherwise runs one command-exchange step.
; In: filmPlaybackMode $0B9B bit 7, gamePhase zp_B1 bit 7
; Out: whatever serviceCommandExchange ($4DA9) does - packets sent/received, messages updated
; Called from: runMapGeneratorPasses ($6F3F, $6F4A, $6F57, $6F6C) and the map-name screen ($773A)
; ----------------------------------------------------------------------
serviceLinkWhileGenerating:
lda filmPlaybackMode ; 6F91 filmPlaybackMode: bit 7 = watching a recorded game
bmi linkServiceNotNeeded ; 6F94 film playback - there is nobody on the other end
lda gamePhase ; 6F96 gamePhase: bit 7 set in the setup/menu phases
bpl linkServiceNotNeeded ; 6F98 bit 7 clear = the battle loop is running and already services the link
jmp serviceCommandExchange ; 6F9A exchange one round of commands with the opponent (tail call)
linkServiceNotNeeded:
rts ; 6F9D nothing to do
; ----------------------------------------------------------------------
; pickHillSummitSites - decides how many hill summits the map gets and finds a blank cell for each.
; The count is rolled as (rnd AND 3) + (rnd AND 1), re-rolled to 1-4 if it came out exactly 1;
; DEFENDER always gets 4 and QB SNEAK at most 1. Each site gets a blob size of 2-5 cells, and four
; candidate rows in the range 7-14 are rolled. findBlankSitesOnLanes then places the top-half sites;
; afterwards the count is doubled, the candidate rows are mirrored to 39-row and the same routine runs
; again by fall-through, so every summit gets a point-mirrored twin in the bottom half.
; In: $032E map style bits 6/7, scenario RNG
; Out: $0300 = total number of sites placed, $0307[0..7] blob sizes, $0314[i]/$031C[i] site
; column/row, $0325[0..3] candidate rows, $0329-$032C lane columns; zp_18, zp_28-zp_2B
; Called from: runMapGeneratorPasses ($6F3C)
; Roll the number of summits for the top half of the map.
; ----------------------------------------------------------------------
pickHillSummitSites:
lda #$00 ; 6F9E start the top-half pass at site 0
sta scratch2A ; 6FA0 zp_2A = index of the site being placed; the first pass starts at site 0
lda #$04 ; 6FA2 DEFENDER gets exactly 4 summits
bit play2Grp02 ; 6FA4 mapGenGameFlags
bvs storeHillSiteCount ; 6FA7 bit 6 (DEFENDER style) - keep the fixed count of 4
jsr nextScenarioRandom ; 6FA9 first roll
and #$03 ; 6FAC 0-3
sta scratch18 ; 6FAE remember it
jsr nextScenarioRandom ; 6FB0 second roll
and #$01 ; 6FB3 0-1
clc ; 6FB5 prepare the add
adc scratch18 ; 6FB6 count = (rnd AND 3) + (rnd AND 1) = 0-4, biased towards 1-3
cmp #$01 ; 6FB8 did it come out as a single lone summit?
bne limitHillsForQbSneak ; 6FBA no - keep the count
jsr nextScenarioRandom ; 6FBC yes - re-roll rather than build a one-hill map
and #$03 ; 6FBF 0-3
clc ; 6FC1 prepare the add
adc #$01 ; 6FC2 1-4 summits instead
limitHillsForQbSneak:
bit play2Grp02 ; 6FC4 mapGenGameFlags
bpl storeHillSiteCount ; 6FC7 bit 7 clear - not QB SNEAK, keep the count
and #$01 ; 6FC9 QB SNEAK: 0 or 1 summit only, the field has to stay open
storeHillSiteCount:
sta basicVectorHijack ; 6FCB $0300 = summit count (the generator uses the $0200-$03FF setup block as scratch RAM) (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
cmp #$00 ; 6FCE redundant: every path above already set Z from A
bne setRowCandidateLimit ; 6FD0 at least one summit
noHillSitesAtAll:
jmp storeHillSitesPlaced ; 6FD2 no hills at all - store 0 through the common exit and return
; ----------------------------------------------------------------------
; Roll one blob size per site (mirrored to the twin) and four candidate rows 7-14.
; ----------------------------------------------------------------------
setRowCandidateLimit:
sta cellRowB ; 6FD5 zp_28 = highest index of the candidate-row list...
dec cellRowB ; 6FD7 ...= count-1, so 0-3 (there are four candidate rows)
ldy #$00 ; 6FD9 Y walks the top-half sites 0..count-1
ldx basicVectorHijack ; 6FDB X walks their bottom-half twins, which start at index count (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
hillBlobSizeLoop:
jsr nextScenarioRandom ; 6FDE roll the size of this summit blob
and #$03 ; 6FE1 0-3
clc ; 6FE3 prepare the add
adc #$02 ; 6FE4 blob size 2-5 cells
sta play1Row13,y ; 6FE6 $0307[y] = size of top-half site y
sta play1Row13,x ; 6FE9 $0307[count+y] = the mirrored twin gets the same size
iny ; 6FEC next top-half site
inx ; 6FED next bottom-half twin
cpy basicVectorHijack ; 6FEE all sites given a size? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
bcc hillBlobSizeLoop ; 6FF1 one size per site
ldy #$03 ; 6FF3 four candidate rows, filled backwards 3..0
hillRowCandidateLoop:
jsr nextScenarioRandom ; 6FF5 roll one candidate row
and #$07 ; 6FF8 0-7
clc ; 6FFA prepare the add
adc #$07 ; 6FFB row 7-14: summits stay clear of the top edge and of the centre line
sta storeFontEndLo,y ; 6FFD $0325[y] = candidate row list used by findBlankSitesOnLanes (also trainerPlaybook0200:play1Row43)
sta play1Row34,y ; 7000 preload $031C[y] as well so an unplaced site still holds a sane row
dey ; 7003 next candidate-row slot
bpl hillRowCandidateLoop ; 7004 four candidate rows in all
; ----------------------------------------------------------------------
; Place the top half, then mirror everything into the bottom half.
; ----------------------------------------------------------------------
jsr findBlankSitesOnLanes ; 7006 resolve the top-half sites onto the four lanes
asl basicVectorHijack ; 7009 double the placed count - each site also gets a point-mirrored twin (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
beq noHillSitesAtAll ; 700C nothing could be placed - store 0 and return
ldy #$03 ; 700E mirror the four candidate rows
mirrorHillRowCandidates:
lda play1Row34,y ; 7010 row of site y (the accepted row, or the preloaded candidate)
jsr mirrorMapCoordinate ; 7013 39 - row: the same distance from the bottom edge
sta storeFontEndLo,y ; 7016 candidate rows 25-32 for the bottom half (also trainerPlaybook0200:play1Row43)
dey ; 7019 next of the four candidate rows
bpl mirrorHillRowCandidates ; 701A falls into findBlankSitesOnLanes for sites count..2*count-1
; ----------------------------------------------------------------------
; findBlankSitesOnLanes - gives every summit site from zp_2A to $0300-1 a column and a row. The four
; lane columns are reshuffled first; each site then takes the next lane and the next candidate row and
; tries up to 21 jittered columns (+/-6 around the lane) until it finds a blank ($40) cell. If a whole
; row fails it moves on to the next candidate row, and if every row fails the site is dropped ($0300
; and zp_2A are decremented). Entered by JSR for the top half and by fall-through for the mirrored
; bottom half, which is why zp_2A is not reset here.
; In: zp_2A = first site index, $0300 = requested count, zp_28 = highest candidate-row index,
; $0325[] candidate rows, map at $F000
; Out: $0314[i] site column, $031C[i] site row, $0300 = sites actually placed, $0329-$032C lanes;
; zp_1D/zp_1E/zp_1F/zp_29/zp_2B scratch
; Called from: pickHillSummitSites ($7006, and by fall-through from $701A)
; ----------------------------------------------------------------------
findBlankSitesOnLanes:
jsr shuffleLaneColumns ; 701C randomise the order of the four lane columns 6/15/25/34
lda #$03 ; 701F lane index 3 = the last entry of the shuffled lane list
sta scratch2B ; 7021 zp_2B = lane index; advanceLaneIndex counts down 3,2,1,0,3,...
lda cellRowB ; 7023 highest valid candidate-row index (= count-1)
sta scanListIndex ; 7025 zp_29 = start at the top of the candidate-row list
; ----------------------------------------------------------------------
; Per site: try candidate rows in turn until a blank cell is found.
; ----------------------------------------------------------------------
startSitePlacement:
lda scanListIndex ; 7027 current candidate-row index
sta scratch1F ; 7029 zp_1F remembers which candidate row this site started with
tryRowCandidate:
lda #$14 ; 702B 21 column attempts per candidate row (counter $14 down through 0)
sta scratch1E ; 702D zp_1E = attempts left
tryJitteredLaneColumn:
ldy scratch2B ; 702F current lane
lda storeFontEndHi,y ; 7031 its column: 6, 15, 25 or 34 (also trainerPlaybook0200:play1Row47)
ldy #$03 ; 7034 Y = 3 jitter steps, i.e. an offset of 0, +/-2, +/-4 or +/-6 columns
jitterLaneColumn:
jsr addRandomJitter ; 7036 scatter the site sideways around its lane column
cmp #$28 ; 7039 still inside the 40-column map?
bcs tryJitteredLaneColumn ; 703B off the map - roll again (a rejected roll does not use up an attempt)
ldy scratch2A ; 703D index of the site being placed
sta play1Row26,y ; 703F $0314[i] = provisional column
tax ; 7042 X = column for readMapCell
ldy scanListIndex ; 7043 current candidate-row slot
lda storeFontEndLo,y ; 7045 the candidate row for this attempt (also trainerPlaybook0200:play1Row43)
sta scratch1D ; 7048 zp_1D keeps it until the cell is known to be free
tay ; 704A Y = row for readMapCell
jsr readMapCell ; 704B zp_48/49 -> map cell (column,row)
ldy #$00 ; 704E offset 0 = the cell itself
lda (mapCellPtr),y ; 7050 what is already there
cmp #$40 ; 7052 $40 = blank terrain (the river and its markers are not)
beq hillSiteAccepted ; 7054 free - take this site
dec scratch1E ; 7056 one attempt used up
bpl tryJitteredLaneColumn ; 7058 try another column on the same row
jsr advanceRowCandidate ; 705A this row is hopeless - step to the next candidate row
lda scanListIndex ; 705D the candidate row we have wrapped round to
cmp scratch1F ; 705F have we cycled back to the row we started on?
rowCandidateWrapTest:
bne tryRowCandidate ; 7061 no - give that row its own 21 attempts
dec basicVectorHijack ; 7063 every candidate row failed: drop this site from the count (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
dec scratch2A ; 7066 and let the next site re-use its index
jmp advanceToNextHillSite ; 7068 site dropped - skip the accept path
hillSiteAccepted:
ldy scratch2A ; 706B index of the site just placed
lda scratch1D ; 706D the row that worked
sta play1Row34,y ; 706F $031C[i] = site row
; ----------------------------------------------------------------------
; Advance lane and row candidate so consecutive summits are spread out.
; ----------------------------------------------------------------------
advanceToNextHillSite:
jsr advanceLaneIndex ; 7072 next site uses the next lane column
lda scratch1F ; 7075 the candidate row this site started from
sta scanListIndex ; 7077 restore the candidate row this site started from...
jsr advanceRowCandidate ; 7079 ...then step it once, so the next site starts on a different row
inc scratch2A ; 707C next site index
lda scratch2A ; 707E site index just used
cmp basicVectorHijack ; 7080 all requested sites handled? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
bcs storeHillSitesPlaced ; 7083 all sites handled - store the real count
placeNextHillSite:
jmp startSitePlacement ; 7085 place the next site
storeHillSitesPlaced:
lda scratch2A ; 7088 zp_2A = number of sites actually placed
sta basicVectorHijack ; 708A $0300 corrected, so the painters only walk real sites (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
hillSitesDone:
rts ; 708D returns with $0300 = the number of sites that really exist
; ----------------------------------------------------------------------
; advanceLaneIndex - steps the lane cursor zp_2B down through the four shuffled lane columns in
; $0329-$032C, wrapping 0 back to 3.
; In: zp_2B
; Out: zp_2B = next lane index 0-3
; Called from: findBlankSitesOnLanes ($7072)
; ----------------------------------------------------------------------
advanceLaneIndex:
dec scratch2B ; 708E next lane
bpl laneIndexDone ; 7090 still a valid index
lda #$03 ; 7092 lane index 3
sta scratch2B ; 7094 wrap round to the last of the four lanes
laneIndexDone:
rts ; 7096 return the wrapped lane index
; ----------------------------------------------------------------------
; advanceRowCandidate - steps the candidate-row cursor zp_29 down through $0325[0..zp_28], wrapping 0
; back to zp_28 (= summit count - 1).
; In: zp_29, zp_28
; Out: zp_29 = next candidate-row index
; Called from: findBlankSitesOnLanes ($705A, $7079)
; ----------------------------------------------------------------------
advanceRowCandidate:
dec scanListIndex ; 7097 next candidate row
bpl rowCandidateDone ; 7099 still a valid index
lda cellRowB ; 709B highest candidate index
sta scanListIndex ; 709D wrap round to the top of the list
rowCandidateDone:
rts ; 709F return the wrapped candidate-row index
; ----------------------------------------------------------------------
; shuffleLaneColumns - builds a random permutation of 0-3 (identity, then four random transpositions)
; in the sprite build buffer $9025-$9028, which is free while the map is generated, and writes
; laneColumnTable[perm[y]] into $0329-$032C. Summits are therefore distributed over the four vertical
; lanes of the map in a different order for every map.
; In: scenario RNG, laneColumnTable $6F00
; Out: $0329-$032C = the four lane columns in random order; $9025-$9028 and zp_18 clobbered
; Called from: findBlankSitesOnLanes ($701C)
; ----------------------------------------------------------------------
shuffleLaneColumns:
lda #$03 ; 70A0 load the swap count
sta scratch18 ; 70A2 zp_18 = swap counter: 3,2,1,0 = four swaps
tay ; 70A4 Y = 3
laneIdentityLoop:
tya ; 70A5 A = Y
sta spriteBuildBuffer,y ; 70A6 $9025[y] = y: start from the identity permutation
dey ; 70A9 next slot
bpl laneIdentityLoop ; 70AA four entries 3..0
laneSwapLoop:
jsr nextScenarioRandom ; 70AC pick the first index...
and #$03 ; 70AF 0-3
tay ; 70B1 Y = first slot to swap
jsr nextScenarioRandom ; 70B2 ...and the second (may be the same, which is a no-op swap)
and #$03 ; 70B5 0-3
tax ; 70B7 X = second slot to swap (may be the same)
lda spriteBuildBuffer,y ; 70B8 perm[y]
pha ; 70BB save it on the stack
lda spriteBuildBuffer,x ; 70BC perm[x]
sta spriteBuildBuffer,y ; 70BF perm[y] = perm[x]
pla ; 70C2 the old perm[y]
sta spriteBuildBuffer,x ; 70C3 exchange perm[y] and perm[x]
dec scratch18 ; 70C6 one swap done
bpl laneSwapLoop ; 70C8 four swaps in all
ldy #$03 ; 70CA Y = 3: fill the lane list backwards
laneColumnFillLoop:
ldx spriteBuildBuffer,y ; 70CC permuted index 0-3
lda laneColumnTable,x ; 70CF lane column 6, 15, 25 or 34
sta storeFontEndHi,y ; 70D2 $0329-$032C = shuffled lane columns (also trainerPlaybook0200:play1Row47)
dey ; 70D5 next lane slot
bpl laneColumnFillLoop ; 70D6 four lanes
rts ; 70D8 the four lane columns are ready
; ----------------------------------------------------------------------
; paintHillSummits - paints every site chosen by pickHillSummitSites as a blob of summit tiles. Each
; blob is 2-5 cells of tile $65 (randomTileVariant may pick $66) grown from the site with a horizontal
; spread of 200/256 and a vertical spread of 32/256, so summits are wide and flat rather than round.
; Does nothing when no sites were placed.
; In: $0300 site count, $0307[] blob sizes, $0314[]/$031C[] site coordinates
; Out: summit tiles written to the map; $0301 = $65, $0304 = 0, zp_2A, zp_A7/zp_A8
; Called from: runMapGeneratorPasses ($6F42)
; ----------------------------------------------------------------------
paintHillSummits:
lda basicVectorHijack ; 70D9 number of summit sites (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
beq summitsDone ; 70DC no hills on this map
lda #$00 ; 70DE clear A for the two stores below
sta play1Row10 ; 70E0 $0304 blob flags = 0: no 2x2 block constraint
sta scratch2A ; 70E3 start at site 0
lda #$65 ; 70E5 $65 = summit tile, terrain class 6
sta play1Row07 ; 70E7 $0301 = the tile growTerrainBlob paints
summitBlobLoop:
ldy scratch2A ; 70EA index of the site being painted
lda play1Row26,y ; 70EC site column
sta blobOriginCol ; 70EF zp_A7 = blob origin column
lda play1Row34,y ; 70F1 site row
sta blobOriginRow ; 70F4 zp_A8 = blob origin row
lda play1Row13,y ; 70F6 A = 2-5 cells to place
ldx #$C8 ; 70F9 X = horizontal spread 200/256 - summits stretch east/west
ldy #$20 ; 70FB Y = vertical spread 32/256 - and stay thin north/south
jsr growTerrainBlob ; 70FD paint this summit
inc scratch2A ; 7100 next site
lda scratch2A ; 7102 site index just painted
cmp basicVectorHijack ; 7104 all sites painted? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
bcc summitBlobLoop ; 7107 every site, top-half original and mirrored twin alike
summitsDone:
rts ; 7109 no more sites
; ----------------------------------------------------------------------
; growLowerHillsBesideSlopes - second terrain layer. Scans every interior cell (column and row 1-38)
; and, when a blank cell touches an upper slope tile (terrain class 5, $41-$54), grows a 5-cell blob
; of lower-hill tile $61 from it. The chance is 1 in 4 for the north and south neighbours and 1 in 2
; for west and east, so the hill bodies spread sideways like the summits. The first matching neighbour
; wins. Skipped entirely when the map has no hills.
; In: $0300 summit count, the map, terrainClassTable $077E, scenario RNG
; Out: lower-hill tiles written to the map; $0301 = $61, $0304 = 0, zp_A7/zp_A8 = the scan cell
; Called from: runMapGeneratorPasses ($6F5A)
; ----------------------------------------------------------------------
growLowerHillsBesideSlopes:
lda basicVectorHijack ; 710A were any summits placed? (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06)
bne lowerHillScanInit ; 710D there are hills to widen
jmp lowerHillScanDone ; 710F no hills - nothing to widen
lowerHillScanInit:
lda #$00 ; 7112 clear A for the two stores below
sta play1Row10 ; 7114 $0304 blob flags = 0: no 2x2 block constraint
sta scratch2A ; 7117 clears zp_2A as a side effect; nothing in this routine reads it
lda #$61 ; 7119 $61 = lower hill tile ($61-$64 variants), terrain class 4
sta play1Row07 ; 711B $0301 = the tile growTerrainBlob paints
lda #$01 ; 711E row 1
sta blobOriginRow ; 7120 scan starts at row 1 (the outermost ring is never used)
; ----------------------------------------------------------------------
; Scan loop over the interior cells. zp_A7/zp_A8 hold the cell; zp_48/49 is set to the top-left
; corner of its 3x3 block so the neighbours are at fixed offsets $01 N, $28 W, $29 self, $2A E,
; $51 S.
; ----------------------------------------------------------------------
lowerHillRowStart:
lda #$01 ; 7122 column 1
sta blobOriginCol ; 7124 each row starts at column 1
lowerHillCellScan:
ldx blobOriginCol ; 7126 X = column...
dex ; 7128 ...minus 1
ldy blobOriginRow ; 7129 Y = row...
dey ; 712B ...minus 1
jsr readMapCell ; 712C zp_48/49 -> cell (col-1,row-1), the top-left of the 3x3 block
ldy #$29 ; 712F offset $29 = the cell itself
lda (mapCellPtr),y ; 7131 the terrain code of the scan cell
cmp #$40 ; 7133 only blank cells can start a lower hill
lowerHillCellOccupied:
bne lowerHillNextColumn ; 7135 occupied - move on
ldy #$01 ; 7137 offset $01 = north neighbour (col,row-1)
lda (mapCellPtr),y ; 7139 north neighbour's cell
bmi lowerHillTestWest ; 713B bit 7 set = a unit or a river keep-clear marker, not terrain
tay ; 713D use the terrain code as the class-table index
lda D_077E,y ; 713E terrain class of the north neighbour
lowerHillNorthClassTest:
cmp #$05 ; 7141 class 5 = upper slope contour $41-$54
bne lowerHillTestWest ; 7143 not a slope - try the west neighbour
jsr nextScenarioRandom ; 7145 roll for the north slope
cmp #$40 ; 7148 1 chance in 4 for a north slope
bcc growLowerHillHere ; 714A won - grow a hill body here
lowerHillTestWest:
ldy #$28 ; 714C offset $28 = west neighbour (col-1,row)
lda (mapCellPtr),y ; 714E west neighbour's cell
bmi lowerHillTestEast ; 7150 unit or marker - skip
tay ; 7152 use the terrain code as the class-table index
lda D_077E,y ; 7153 terrain class of the west neighbour
cmp #$05 ; 7156 upper slope?
bne lowerHillTestEast ; 7158 no - try the east neighbour
jsr nextScenarioRandom ; 715A roll for the west slope
cmp #$80 ; 715D 1 chance in 2 for a side slope - hills widen east/west
bcc growLowerHillHere ; 715F won - grow a hill body here
lowerHillTestEast:
ldy #$2A ; 7161 offset $2A = east neighbour (col+1,row)
lowerHillReadEast:
lda (mapCellPtr),y ; 7163 east neighbour's cell
bmi lowerHillTestSouth ; 7165 unit or marker - skip
tay ; 7167 use the terrain code as the class-table index
lda D_077E,y ; 7168 terrain class of the east neighbour
cmp #$05 ; 716B upper slope?
bne lowerHillTestSouth ; 716D no - try the south neighbour
jsr nextScenarioRandom ; 716F roll for the east slope
cmp #$80 ; 7172 1 chance in 2
bcc growLowerHillHere ; 7174 won - grow a hill body here
lowerHillTestSouth:
ldy #$51 ; 7176 offset $51 = south neighbour (col,row+1)
lda (mapCellPtr),y ; 7178 south neighbour's cell
bmi lowerHillNextColumn ; 717A unit or marker - give up on this cell
tay ; 717C use the terrain code as the class-table index
lda D_077E,y ; 717D terrain class of the south neighbour
cmp #$05 ; 7180 upper slope?
bne lowerHillNextColumn ; 7182 no - nothing to do at this cell
jsr nextScenarioRandom ; 7184 roll for the south slope
cmp #$40 ; 7187 1 chance in 4 (inverted test: falls through to grow)
bcs lowerHillNextColumn ; 7189 lost the roll
growLowerHillHere:
ldx #$40 ; 718B X = horizontal spread 64/256
ldy #$10 ; 718D Y = vertical spread 16/256 - the body hugs the slope
lda #$05 ; 718F A = 5 cells
jsr growTerrainBlob ; 7191 zp_A7/zp_A8 already hold the scan cell = the blob origin
lowerHillNextColumn:
inc blobOriginCol ; 7194 next column
lda blobOriginCol ; 7196 the column just finished
cmp #$27 ; 7198 columns 1-38 only
bcs lowerHillNextRow ; 719A row finished
jmp lowerHillCellScan ; 719C scan the next cell of this row
lowerHillNextRow:
inc blobOriginRow ; 719F next row
lda blobOriginRow ; 71A1 the row just finished
cmp #$27 ; 71A3 rows 1-38 only
bcs lowerHillScanDone ; 71A5 whole map scanned
jmp lowerHillRowStart ; 71A7 scan the next row
lowerHillScanDone:
rts ; 71AA the whole interior has been scanned
; unusedBitTable71AB - $08,$04,$02,$01,$00. Dead in this overlay: nothing between $6F00 and $87FF
; reads it (the $71AC reference in XREF belongs to the comcen-screens overlay). It looks like a
; leftover direction-bit table for growTerrainBlob, which builds the same bits with immediate operands
; instead.
unusedBitTable71AB:
.byte $08,$04,$02,$01,$00 ; 71AB ..... unused direction-bit constants
; ----------------------------------------------------------------------
; growTerrainBlob - the generic random blob painter used by every terrain pass. Starting at
; (zp_A7,zp_A8) it performs a depth-first walk with an explicit 80-entry stack in page 4 ($0400
; columns, $0450 rows, $04A0 tried-direction flags) and paints tile $0301 - passed through
; randomTileVariant - on blank ($40) cells until A cells have been placed, the stack empties or the
; stack overflows. From each cell the four directions are tried once (flag bits 0 N, 1 W, 2 E, 3 S in
; zp_6B); a neighbour is taken when it is on the map, blank, and a scenario random byte is below the X
; (west/east) or Y (north/south) threshold, so X and Y control the horizontal and vertical spread out
; of 256. A newly pushed cell has the direction back towards the origin pre-marked as tried, which
; keeps the blob growing outwards. With $0304 bit 6 set (forest) a cell is only painted when
; checkAlignedBlockForForest approves its 2x2 block.
; In: A = cells to paint, X = horizontal spread 0-255, Y = vertical spread 0-255,
; zp_A7/zp_A8 = origin, $0301 = base tile, $0304 = blob flags
; Out: map cells painted; zp_18, zp_1A, zp_6B, zp_A5/zp_A6, zp_B2 stack level, zp_B3 painted count,
; $91DA/$91DB and $0400-$04EF clobbered. NOTE: $0400-$04EF is the page that also holds the
; copy-protection answer table at $0401 - harmless, because manualLookupCheck reads its answer
; before it calls the generator
; Called from: paintHillSummits ($70FD), growLowerHillsBesideSlopes ($7191), paintForestBlobAt ($73EA)
; Set up the walk: thresholds, counters and stack entry 0 = the origin cell.
; ----------------------------------------------------------------------
growTerrainBlob:
stx currentUnitCol ; 71B0 X = horizontal accept threshold, parked in the engine's currentUnitCol (free here)
sty currentUnitRow ; 71B3 Y = vertical accept threshold, parked in currentUnitRow
sta scratch18 ; 71B6 zp_18 = how many cells this blob should get
lda #$00 ; 71B8 clear A for the three stores below
sta glyphPtrHi ; 71BA zp_B3 = cells painted so far
sta play4Col22 ; 71BC $04A0[0] = tried-direction flags of the origin entry
sta glyphPtr ; 71BF zp_B2 = stack fill level (index of the next free entry)
lda play1Row10 ; 71C1 blob flags: bit 6 = only paint inside a clean 2x2 block
sta targetDirection ; 71C4 zp_6B = flags of the cell currently being expanded
lda blobOriginCol ; 71C6 the walk starts at the blob origin
sta viewOriginCol ; 71C8 zp_A5 = column of the current cell
sta checksumXorConstant ; 71CA $0400[0] = origin column (also trainerPlaybook0200:play3Col12)
lda blobOriginRow ; 71CD origin row
sta viewOriginRow ; 71CF zp_A6 = row of the current cell
sta play3Row42 ; 71D1 $0450[0] = origin row
jsr getViewportOriginPointer; 71D4 zp_48/49 -> the origin cell (copes with negative coordinates)
ldy #$00 ; 71D7 offset 0 = the cell itself
jmp blobPaintCell ; 71D9 paint the origin, then push it as the first stack entry
; ----------------------------------------------------------------------
; Pop the top stack entry and try its four directions in turn. zp_48/49 is aimed one row above the
; cell, so N/W/E/S are at offsets $00/$27/$29/$50.
; ----------------------------------------------------------------------
blobPopStack:
dec glyphPtr ; 71DC drop the fill level to the entry on top of the stack
bpl blobResumeCell ; 71DE there is still an entry to expand
jmp blobFinished ; 71E0 stack empty - this blob is finished
blobResumeCell:
ldy glyphPtr ; 71E3 y = the entry now on top of the stack
lda play4Col22,y ; 71E5 $04A0[y] = directions already tried from this cell
ora play1Row10 ; 71E8 re-apply the global blob flags (bit 6 survives every push/pop)
sta targetDirection ; 71EB zp_6B = the flags of the cell being expanded
ldx checksumXorConstant,y ; 71ED $0400[y] = its column (also trainerPlaybook0200:play3Col12)
lda play3Row42,y ; 71F0 $0450[y] = its row
tay ; 71F3 A -> Y so it can be decremented
dey ; 71F4 aim one row above the cell...
sty viewOriginRow ; 71F5 ...so the north neighbour sits at offset 0
stx viewOriginCol ; 71F7 zp_A5 = the cell's column
jsr getViewportOriginPointer; 71F9 zp_48/49 -> (column, row-1)
lda targetDirection ; 71FC tried-direction flags
and #$01 ; 71FE bit 0 = north already tried
bne blobTryWest ; 7200 yes - go on to the west neighbour
lda targetDirection ; 7202 tried-direction flags
blobMarkNorthTried:
ora #$01 ; 7204 bit 0 = north
sta targetDirection ; 7206 mark north as tried
lda viewOriginRow ; 7208 row-1 above the top edge of the map?
bmi blobTryWest ; 720A yes - try west
ldy #$00 ; 720C offset $00 = north neighbour
lda (mapCellPtr),y ; 720E the north neighbour's cell
cmp #$40 ; 7210 only blank terrain can be painted
bne blobTryWest ; 7212 not blank - cannot grow there
jsr nextScenarioRandom ; 7214 roll for the vertical spread
cmp currentUnitRow ; 7217 accept while the random byte is below the vertical spread
bcs blobTryWest ; 721A lost the roll - try west
blobGrowWest:
jmp blobPaintCell ; 721C grow into the north neighbour (zp_A5/zp_A6 and Y already point at it)
blobTryWest:
inc viewOriginRow ; 721F undo the -1: back to the cell's own row
blobStepWest:
dec viewOriginCol ; 7221 step one column west
lda targetDirection ; 7223 tried-direction flags
and #$02 ; 7225 bit 1 = west already tried
bne blobTryEast ; 7227 west already tried from this cell
lda targetDirection ; 7229 tried-direction flags
ora #$02 ; 722B bit 1 = west
sta targetDirection ; 722D mark west as tried
lda viewOriginCol ; 722F column-1 off the left edge?
bmi blobTryEast ; 7231 column -1 is off the map
ldy #$27 ; 7233 offset $27 = west neighbour (-1 column, +1 row from the pointer)
lda (mapCellPtr),y ; 7235 the west neighbour's cell
cmp #$40 ; 7237 blank?
bne blobTryEast ; 7239 not blank
jsr nextScenarioRandom ; 723B roll for the horizontal spread
cmp currentUnitCol ; 723E accept while the random byte is below the horizontal spread
bcc blobGrowWest ; 7241 grow into the west neighbour
blobTryEast:
inc viewOriginCol ; 7243 back to the cell's own column...
inc viewOriginCol ; 7245 two columns east: the cell's column + 1
lda targetDirection ; 7247 tried-direction flags
and #$04 ; 7249 bit 2 = east already tried
bne blobTrySouth ; 724B east already tried
lda targetDirection ; 724D tried-direction flags
ora #$04 ; 724F bit 2 = east
sta targetDirection ; 7251 mark east as tried
lda viewOriginCol ; 7253 the column the east neighbour sits in
cmp #$28 ; 7255 column 40 is off the right edge
bcs blobTrySouth ; 7257 column 40 is off the map
ldy #$29 ; 7259 offset $29 = east neighbour (+1 column, +1 row)
lda (mapCellPtr),y ; 725B the east neighbour's cell
blobTestEastBlank:
cmp #$40 ; 725D blank?
bne blobTrySouth ; 725F not blank
jsr nextScenarioRandom ; 7261 roll for the horizontal spread
cmp currentUnitCol ; 7264 horizontal spread again
bcc blobPaintCell ; 7267 grow into the east neighbour
blobTrySouth:
dec viewOriginCol ; 7269 back to the cell's own column
inc viewOriginRow ; 726B step one row south
lda targetDirection ; 726D tried-direction flags
and #$08 ; 726F bit 3 = south already tried
blobSouthAlreadyTried:
bne blobCellExhausted ; 7271 south already tried
lda targetDirection ; 7273 tried-direction flags
ora #$08 ; 7275 bit 3 = south
sta targetDirection ; 7277 mark south as tried
lda viewOriginRow ; 7279 the row the south neighbour sits in
cmp #$28 ; 727B row 40 is off the bottom edge
bcs blobCellExhausted ; 727D row 40 is off the map
ldy #$50 ; 727F offset $50 = south neighbour (+2 rows)
lda (mapCellPtr),y ; 7281 the south neighbour's cell
cmp #$40 ; 7283 blank?
bne blobCellExhausted ; 7285 not blank
jsr nextScenarioRandom ; 7287 roll for the vertical spread
blobTestSouthChance:
cmp currentUnitRow ; 728A vertical spread again
bcc blobPaintCell ; 728D grow into the south neighbour
blobCellExhausted:
jmp blobPopStack ; 728F all four directions used up - pop the next cell
; ----------------------------------------------------------------------
; Paint the accepted cell (Y = its offset from zp_48/49) and push it on the stack.
; ----------------------------------------------------------------------
blobPaintCell:
lda (mapCellPtr),y ; 7292 the candidate cell
cmp #$40 ; 7294 blank terrain?
bne blobPushCell ; 7296 not blank (only possible for the origin) - push it without painting
bit targetDirection ; 7298 bit 6 = the 2x2 forest constraint
bvc blobWriteTile ; 729A bit 6 clear - paint unconditionally
jsr checkAlignedBlockForForest; 729C forest: the aligned 2x2 block must contain only blank and forest
bcs blobPushCell ; 729F block rejected - do not paint, but still expand from here
blobWriteTile:
lda play1Row07 ; 72A1 base tile for this pass
jsr randomTileVariant ; 72A4 add a random variant (Y is preserved through a self-modified LDY at $7891+1)
sta (mapCellPtr),y ; 72A7 paint the cell
inc glyphPtrHi ; 72A9 one more cell of this blob placed
lda glyphPtrHi ; 72AB cells placed so far
cmp scratch18 ; 72AD reached the requested number of cells?
bcs blobFinished ; 72AF yes - blob finished
blobPushCell:
ldy glyphPtr ; 72B1 y = the parent entry that is being expanded
blobSaveParentFlags:
lda targetDirection ; 72B3 the parent's flags with the direction just used marked as tried
sta play4Col22,y ; 72B5 $04A0[parent] = its updated tried-direction flags
iny ; 72B8 the child entry goes one slot higher
lda viewOriginCol ; 72B9 the accepted cell's column
sta checksumXorConstant,y ; 72BB $0400[child] = column (also trainerPlaybook0200:play3Col12)
lda viewOriginRow ; 72BE and its row
sta play3Row42,y ; 72C0 $0450[child] = row
; ----------------------------------------------------------------------
; Pre-mark the direction back towards the blob origin as already tried, so a blob never folds back on
; itself.
; ----------------------------------------------------------------------
lda #$00 ; 72C3 clear A
sta scratch1A ; 72C5 zp_1A = the child's preset flags
lda #$02 ; 72C7 $02 = 'west already tried'
ldx viewOriginCol ; 72C9 the child's column
cpx blobOriginCol ; 72CB child column against the blob origin column
beq blobRowBias ; 72CD same column - no horizontal bias
bcs blobStoreColumnBias ; 72CF child is east of the origin - block growth back west
lda #$04 ; 72D1 $04 = 'east already tried' (child is west of the origin)
blobStoreColumnBias:
sta scratch1A ; 72D3 horizontal bias
blobRowBias:
lda #$01 ; 72D5 $01 = 'north already tried'
ldx viewOriginRow ; 72D7 the child's row
cpx blobOriginRow ; 72D9 child row against the blob origin row
beq blobStoreChildFlags ; 72DB same row - no vertical bias
bcs blobMergeRowBias ; 72DD child is below the origin - block growth back north
lda #$08 ; 72DF $08 = 'south already tried' (child is above the origin)
blobMergeRowBias:
ora scratch1A ; 72E1 combine with the horizontal bias
sta scratch1A ; 72E3 zp_1A = the child's preset tried-direction flags
blobStoreChildFlags:
lda scratch1A ; 72E5 the child's preset tried-direction flags
sta play4Col22,y ; 72E7 $04A0[child] = its preset flags
iny ; 72EA point one past the child entry
sty glyphPtr ; 72EB zp_B2 = new fill level = child + 1
cpy #$50 ; 72ED 80 entries is all that fits ($0400/$0450/$04A0 x 80 = $0400-$04EF)
bcc blobExpandChild ; 72EF still room on the stack
rts ; 72F1 stack full - abandon this blob
blobExpandChild:
jmp blobPopStack ; 72F2 expand the cell just pushed
blobFinished:
rts ; 72F5 blob finished: cell count reached, or the stack ran dry
; ----------------------------------------------------------------------
; checkAlignedBlockForForest - constrains forest to whole 2x2 blocks on an even grid. It takes the
; block containing (zp_A5,zp_A6) - both coordinates rounded down to even - and returns C = 0 only if
; all four cells are terrain class 0 (blank $40) or class 2 (forest $5D-$60). On success zp_48/49 is
; re-pointed at the caller's own cell with Y = 0, so the caller can store straight through it. Note
; that the river keep-clear markers $80/$81 index past terrainClassTable into the zero filler at
; $07F3, so they read as class 0 here; growTerrainBlob's own $40 test still refuses to paint them.
; In: zp_A5/zp_A6 = the cell, map, terrainClassTable $077E
; Out: C = 0 accept / C = 1 reject; zp_48/49, A/X/Y clobbered
; Called from: growTerrainBlob ($729C) when $0304 bit 6 is set
; ----------------------------------------------------------------------
checkAlignedBlockForForest:
lda viewOriginCol ; 72F6 column of the cell about to be painted
and #$FE ; 72F8 round the column down to the even grid
tax ; 72FA X = block column
lda viewOriginRow ; 72FB its row
and #$FE ; 72FD round the row down as well
tay ; 72FF Y = block row
forestBlockTestNw:
jsr readMapCell ; 7300 A = the block's top-left cell, zp_48/49 -> it, Y = 0
tax ; 7303 use the terrain code as the class-table index
lda D_077E,x ; 7304 terrain class of that cell
beq forestBlockTestNe ; 7307 class 0 = blank: acceptable
cmp #$02 ; 7309 class 2 = forest: also acceptable
bne forestBlockReject ; 730B anything else (river, hill, contour, unit) rejects the block
forestBlockTestNe:
iny ; 730D offset 1 = top-right cell
lda (mapCellPtr),y ; 730E the top-right cell of the block
forestBlockClassNe:
tax ; 7310 use the terrain code as the class-table index
forestBlockLookupNe:
lda D_077E,x ; 7311 its terrain class
beq forestBlockTestSw ; 7314 blank
forestBlockCompareNe:
cmp #$02 ; 7316 or forest
bne forestBlockReject ; 7318 not blank or forest - reject the block
forestBlockTestSw:
ldy #$28 ; 731A offset $28 = bottom-left cell
lda (mapCellPtr),y ; 731C the bottom-left cell of the block
forestBlockClassSw:
tax ; 731E use the terrain code as the class-table index
lda D_077E,x ; 731F its terrain class
beq forestBlockTestSe ; 7322 blank
cmp #$02 ; 7324 or forest
bne forestBlockReject ; 7326 not blank or forest - reject the block
forestBlockTestSe:
iny ; 7328 offset $29 = bottom-right cell
lda (mapCellPtr),y ; 7329 the bottom-right cell of the block
tax ; 732B use the terrain code as the class-table index
lda D_077E,x ; 732C its terrain class
beq forestBlockAccept ; 732F blank
cmp #$02 ; 7331 or forest
bne forestBlockReject ; 7333 not blank or forest - reject the block
forestBlockAccept:
ldx viewOriginCol ; 7335 the caller's own column
ldy viewOriginRow ; 7337 and row
jsr readMapCell ; 7339 re-point zp_48/49 at the caller's own cell (leaves Y = 0)
clc ; 733C C = 0: this block may be forested
forestBlockAcceptRts:
rts ; 733D C = 0 from $733C
forestBlockReject:
sec ; 733E C = 1: the block holds something else
rts ; 733F C = 1 set at $733E
; ----------------------------------------------------------------------
; placeForestClusters - the last terrain pass. Rolls a number of forest clusters (3-10 normally, with
; a 1-in-32 chance of no forest at all; exactly 10 for QB SNEAK; 1-4 for DEFENDER), gives each cluster
; a random centre at column/row 4-35 and scatters 6 seed points around it (+/-16 columns, +/-8 rows,
; re-rolled while off the map). Every seed becomes a 4x2 patch of small blobs through
; paintForestClusterSeeds. Blob flags $40 make all of it respect the 2x2 forest grid.
; In: $032E map style, scenario RNG
; Out: forest tiles $5D-$60 in the map; $0301, $0302, $0304, zp_2A cluster counter, zp_2B seed
; counter, zp_92-zp_95, zp_A5/zp_A6
; Called from: runMapGeneratorPasses ($6F6F)
; ----------------------------------------------------------------------
placeForestClusters:
lda #$40 ; 7340 blob flag bit 6: forest only grows inside a clean aligned 2x2 block
sta play1Row10 ; 7342 $0304 blob flags
lda #$41 ; 7345 $41 = upper contour tile
sta play1Row08 ; 7347 dead store: nothing in this overlay ever reads $0302 (XREF has no reader)
lda #$5D ; 734A $5D = forest tile ($5D-$60 variants), terrain class 2
storeForestTile:
sta play1Row07 ; 734C $0301 = the tile growTerrainBlob paints
lda #$09 ; 734F QB SNEAK: 9 -> ten clusters, a deliberately cluttered field
bit play2Grp02 ; 7351 mapGenGameFlags
bmi limitForestForDefender ; 7354 bit 7 = QB SNEAK style - keep the fixed count
jsr nextScenarioRandom ; 7356 roll the cluster count
and #$1F ; 7359 0-31
beq forestClustersDone ; 735B 1 chance in 32 of a completely bare map
and #$07 ; 735D 0-7...
clc ; 735F prepare the add
adc #$02 ; 7360 ...+2, so 3-10 clusters in total
limitForestForDefender:
bit play2Grp02 ; 7362 mapGenGameFlags
bvc storeForestClusterCount ; 7365 bit 6 clear - not DEFENDER
and #$03 ; 7367 DEFENDER: at most 3, i.e. 1-4 clusters (the map has to stay open)
storeForestClusterCount:
sta scratch2A ; 7369 zp_2A = clusters left minus one
; ----------------------------------------------------------------------
; Per cluster: pick a centre, then scatter six seed patches around it.
; ----------------------------------------------------------------------
forestClusterLoop:
jsr nextScenarioRandom ; 736B roll the cluster centre column
forestCentreFromRandom:
and #$1F ; 736E 0-31
clc ; 7370 prepare the add
adc #$04 ; 7371 cluster centre column 4-35
sta lineEndCol ; 7373 zp_95 = centre column, the base for the column jitter
sta viewOriginRow ; 7375 vestigial: growTerrainBlob overwrites zp_A6 at once, and this is a column, not a row
jsr nextScenarioRandom ; 7377 roll the cluster centre row
and #$1F ; 737A 0-31
clc ; 737C prepare the add
adc #$04 ; 737D cluster centre row 4-35
sta lineEndRow ; 737F zp_94 = centre row, the base for the row jitter
sta viewOriginCol ; 7381 vestigial in the same way (a row written to the view-origin column)
lda #$05 ; 7383 six seeds per cluster (5 down through 0)
sta scratch2B ; 7385 zp_2B = six seed points per cluster (5 down through 0)
forestSeedRowRetry:
lda lineEndRow ; 7387 centre row
ldy #$04 ; 7389 Y = 4 jitter steps = an offset of 0, +/-2, +/-4, +/-6 or +/-8 rows
jsr addRandomJitter ; 738B scatter the seed row around the cluster centre
cmp #$28 ; 738E still on the map?
bcs forestSeedRowRetry ; 7390 off the map - roll the row again
sta lineCurRow ; 7392 zp_92 = seed row
forestSeedColumnRetry:
lda lineEndCol ; 7394 centre column
jitterForestSeedColumn:
ldy #$08 ; 7396 Y = 8 jitter steps = up to +/-16 columns, so clusters are wide
jsr addRandomJitter ; 7398 scatter the seed column around the cluster centre
cmp #$28 ; 739B still on the map?
bcs forestSeedColumnRetry ; 739D off the map - roll the column again
sta lineCurCol ; 739F zp_93 = seed column
jsr paintForestClusterSeeds ; 73A1 paint the 4x2 patch of blobs around this seed
dec scratch2B ; 73A4 one seed done
bpl forestSeedRowRetry ; 73A6 six seeds per cluster
dec scratch2A ; 73A8 one cluster done
bpl forestClusterLoop ; 73AA next cluster
forestClustersDone:
rts ; 73AC all forest placed
; ----------------------------------------------------------------------
; paintForestClusterSeeds - paints eight small forest blobs covering the 4x2 cell block from column
; zp_93-2 to zp_93+1 and rows zp_92 to zp_92+1, walking it in the order (c,r), (c+1,r), (c+1,r+1),
; (c,r+1), (c-2,r), (c-1,r), (c-1,r+1) and finally (c-2,r+1) by fall-through. Because the block is
; four columns wide it always covers two whole 2x2 forest blocks.
; In: zp_93 = seed column, zp_92 = seed row, $0301/$0304 from placeForestClusters
; Out: forest painted; on exit zp_93 = c-2 and zp_92 = r+1 (they are not restored - the caller
; re-rolls both for the next seed)
; Called from: placeForestClusters ($73A1)
; ----------------------------------------------------------------------
paintForestClusterSeeds:
jsr paintForestBlobAt ; 73AD seed (c,r)
forestSeedStepEast:
inc lineCurCol ; 73B0 -> column c+1
jsr paintForestBlobAt ; 73B2 (c+1,r)
inc lineCurRow ; 73B5 -> row r+1
forestSeedPaintSouthEast:
jsr paintForestBlobAt ; 73B7 (c+1,r+1)
dec lineCurCol ; 73BA -> column c
forestSeedPaintSouth:
jsr paintForestBlobAt ; 73BC (c,r+1)
dec lineCurCol ; 73BF -> column c-1
dec lineCurCol ; 73C1 -> column c-2
dec lineCurRow ; 73C3 -> row r
jsr paintForestBlobAt ; 73C5 (c-2,r)
inc lineCurCol ; 73C8 -> column c-1
jsr paintForestBlobAt ; 73CA (c-1,r)
inc lineCurRow ; 73CD -> row r+1
jsr paintForestBlobAt ; 73CF (c-1,r+1)
dec lineCurCol ; 73D2 -> column c-2, then fall through for the eighth seed (c-2,r+1)
; ----------------------------------------------------------------------
; paintForestBlobAt - grows one 4-cell forest blob at (zp_93,zp_92) if both coordinates are inside the
; 40x40 map; out-of-range coordinates (including the $FF a DEC produced at column 0) are silently
; ignored. Spread is 220/256 in both axes, so the blobs are compact and round.
; In: zp_93 column, zp_92 row, $0301 tile, $0304 flags
; Out: up to 4 forest cells; zp_A7/zp_A8 = the blob origin
; Called from: paintForestClusterSeeds (all eight seeds, $73AD-$73D2 and by fall-through)
; ----------------------------------------------------------------------
paintForestBlobAt:
lda lineCurCol ; 73D4 seed column
cmp #$28 ; 73D6 columns 0-39 only
bcs forestSeedOffMap ; 73D8 off the map (or wrapped to $FF by a DEC) - ignore this seed
sta blobOriginCol ; 73DA zp_A7 = blob origin column
lda lineCurRow ; 73DC seed row
cmp #$28 ; 73DE rows 0-39 only
bcs forestSeedOffMap ; 73E0 off the map - ignore
sta blobOriginRow ; 73E2 zp_A8 = blob origin row
growForestSeedBlob:
ldy #$DC ; 73E4 Y = vertical spread 220/256
ldx #$DC ; 73E6 X = horizontal spread 220/256 - forest grows evenly in all directions
lda #$04 ; 73E8 A = 4 cells per seed
jsr growTerrainBlob ; 73EA grow the blob
forestSeedOffMap:
rts ; 73ED seed outside the map, or blob finished
; ----------------------------------------------------------------------
; applyContourTiles - draws the contour ring around a hill layer. A is the layer's interior tile ($65
; summit or $61 lower hill); zp_8E becomes its terrain class (6 or 4) and zp_91 the class of its ring
; (5 or 3). All 1600 cells are scanned in order: a cell that already holds a ring tile of this layer
; is erased to blank and re-evaluated, cells of the interior class or higher and river cells (class 1)
; are left alone, and everything else is handed to applyContourRuleAtCell. When that returns C = 1 it
; has changed a cell that earlier cells depended on, so the scan is backed up two columns and two rows
; and continues from there until the whole map is stable.
; In: A = interior tile, $0324 = contour offset (0 for $41-$54, $20 for $21-$34), contour rule
; table at $0200, terrainClassTable $077E, map
; Out: contour tiles in the map; $0312 = interior tile, zp_8E, zp_91, zp_2D/zp_2E scan position,
; zp_8C/zp_8D random-bit state, zp_21
; Called from: runMapGeneratorPasses ($6F54 with $65, $6F69 with $61)
; ----------------------------------------------------------------------
applyContourTiles:
sta play1Row24 ; 73EE $0312 = the interior tile this pass belongs to
tay ; 73F1 use the tile code as the class-table index
lda D_077E,y ; 73F2 its terrain class: 6 for the summit, 4 for the lower hill
sta lineMajorLen ; 73F5 zp_8E = the 'high ground' class for the neighbour test
sta lineStepsLeft ; 73F7 zp_91 starts as the interior class...
dec lineStepsLeft ; 73F9 zp_91 = class of this layer's contour ring (5 or 3)
lda #$00 ; 73FB clear A for the three stores below
sta lineErrRow ; 73FD zp_8C = 0: force a fresh random byte on the first irregular rule
sta contourScanCol ; 73FF column 0
sta contourScanRow ; 7401 start the scan at cell (0,0)
; ----------------------------------------------------------------------
; Scan every cell of the map. zp_2E/zp_2D are the cell; zp_48/49 addresses the top-left corner of its
; 3x3 block, so the cell itself is at offset $29.
; ----------------------------------------------------------------------
contourScanCell:
ldx contourScanCol ; 7403 X = scan column
ldy contourScanRow ; 7405 Y = scan row
dex ; 7407 step back one column to the block corner
dey ; 7408 address the block corner (column-1,row-1)
stx viewOriginCol ; 7409 zp_A5 = column-1
sty viewOriginRow ; 740B zp_A6 = row-1
jsr getViewportOriginPointer; 740D zp_48/49 -> the corner, clipped correctly for negative coordinates
ldy #$29 ; 7410 offset $29 = the cell itself
contourReadCell:
lda (mapCellPtr),y ; 7412 the cell's terrain code
bmi contourNextColumn ; 7414 bit 7 = a unit or a river keep-clear marker - never touched
tax ; 7416 use it as the class-table index
lda D_077E,x ; 7417 terrain class of the cell
contourCompareRingClass:
cmp lineStepsLeft ; 741A is it already a ring tile of this layer?
contourCellIsOldRing:
beq contourEraseOldRing ; 741C yes - erase it so the rule table can redraw it
bcs contourNextColumn ; 741E class above the ring = hill interior or higher - leave it
cmp #$01 ; 7420 class 1 = river
bne contourApplyRule ; 7422 not the river - run the rule
beq contourNextColumn ; 7424 river cells are never overwritten by a contour
contourEraseOldRing:
lda #$40 ; 7426 $40 = blank
sta (mapCellPtr),y ; 7428 erase the stale ring tile
contourApplyRule:
jsr applyContourRuleAtCell ; 742A let the neighbourhood rule table decide what this cell becomes
bcs contourScanCell ; 742D C = 1: a neighbour changed, restart from the backed-up position
contourNextColumn:
inc contourScanCol ; 742F next column
ldx contourScanCol ; 7431 the column just finished
cpx #$28 ; 7433 columns 0-39
bcc contourScanCell ; 7435 not at the right edge yet
inc contourScanRow ; 7437 next row
ldx #$00 ; 7439 column 0 of the next row
stx contourScanCol ; 743B back to column 0
ldy contourScanRow ; 743D the row just finished
cpy #$28 ; 743F rows 0-39
bcc contourScanCell ; 7441 not at the bottom yet
rts ; 7443 the whole map is stable
; ----------------------------------------------------------------------
; applyContourRuleAtCell - decides what one cell becomes, from which of its eight neighbours are high
; ground. It builds an 8-bit mask (bit 0 NW, 1 N, 2 NE, 3 W, 4 E, 5 SW, 6 S, 7 SE) of the neighbours
; whose terrain class is >= zp_8E, clears the bits that would point off the map, and uses the mask as
; the index into the 256-byte rule table at $0200 (track 18 sector 15):
; $00 leave the cell alone, C = 0
; $01-$7F paint that tile minus the contour offset $0324, with a random variant, C = 0
; $80-$8F irregular: 50% chance (bit taken from a random byte, 7 bits per byte) to erase the
; neighbour named by the low 3 bits when it is of the interior class; otherwise fill
; the cell with the interior tile. Either way C = 1
; $90-$FE same 50% erase attempt, but if it does not fire the cell is left alone, C = 0
; $FF fill the cell with the interior tile $0312, C = 1
; C = 1 also backs the scan position zp_2E/zp_2D up by two columns and two rows (floored at 0) so
; applyContourTiles re-examines the cells around the change.
; In: zp_2E/zp_2D = the cell, zp_48/49 -> its 3x3 block corner, zp_8E interior class, $0312
; interior tile, $0324 contour offset, rule table $0200, terrainClassTable $077E
; Out: C flag, map cells changed, zp_2D/zp_2E, zp_21 mask, zp_18 rule value, zp_8C/zp_8D random bits
; Called from: applyContourTiles ($742A)
; Build the high-ground neighbour mask, bit 7 (SE) down to bit 0 (NW).
; ----------------------------------------------------------------------
applyContourRuleAtCell:
lda #$00 ; 7444 clear the neighbour mask
sta scratch21 ; 7446 zp_21 = neighbour mask
ldx #$07 ; 7448 walk the eight neighbours backwards: 7 = SE .. 0 = NW
contourNeighbourLoop:
ldy neighbourOffsetTable,x ; 744A offset of neighbour X from the block corner
lda (mapCellPtr),y ; 744D that neighbour's terrain code
bmi contourNextNeighbour ; 744F bit 7 = unit or keep-clear marker - not high ground
tay ; 7451 use it as the class-table index
lda D_077E,y ; 7452 terrain class of the neighbour
cmp lineMajorLen ; 7455 high ground = class >= the interior class of this pass
bcc contourNextNeighbour ; 7457 lower - leave the bit clear
lda scratch21 ; 7459 mask built so far
ora bitMaskTable,x ; 745B bit X: NW,N,NE,W,E,SW,S,SE = bits 0-7
sta scratch21 ; 745E zp_21 = mask so far
contourNextNeighbour:
dex ; 7460 next neighbour
bpl contourNeighbourLoop ; 7461 all eight neighbours
; ----------------------------------------------------------------------
; Clear the mask bits that point off the edge of the 40x40 map.
; ----------------------------------------------------------------------
ldx contourScanCol ; 7463 the cell's own column
ldy contourScanRow ; 7465 its row
lda scratch21 ; 7467 the raw mask
cpx #$00 ; 7469 on the left edge?
bne contourClipRightEdge ; 746B not on the left edge
and #$D6 ; 746D $D6 clears bits 0/3/5 = NW, W, SW
contourClipRightEdge:
cpx #$27 ; 746F on the right edge (column 39)?
bne contourClipTopEdge ; 7471 not on the right edge
and #$6B ; 7473 $6B clears bits 2/4/7 = NE, E, SE
contourClipTopEdge:
cpy #$00 ; 7475 on the top edge?
bne contourClipBottomEdge ; 7477 not on the top edge
and #$F8 ; 7479 $F8 clears bits 0/1/2 = NW, N, NE
contourClipBottomEdge:
cpy #$27 ; 747B on the bottom edge (row 39)?
bne contourLookupRule ; 747D not on the bottom edge
and #$1F ; 747F $1F clears bits 5/6/7 = SW, S, SE
contourLookupRule:
sta scratch21 ; 7481 zp_21 = the clipped mask
tay ; 7483 use it as the rule-table index
; ----------------------------------------------------------------------
; Look the neighbourhood up in the 256-byte contour rule table loaded to $0200 from track 18 sector
; 15.
; ----------------------------------------------------------------------
lda contourRuleTable,y ; 7484 rule byte for this exact neighbourhood (also trainerPlaybook0200:trainerPlay0)
beq contourLeaveCell ; 7487 $00 = this neighbourhood needs no contour - leave the cell (C = 0)
bpl contourRingTileFromRule ; 7489 $01-$7F = a contour tile code - paint it
cmp #$FF ; 748B $FF = the cell is surrounded, fill it with the hill interior
beq contourFillWithInterior ; 748D fill the cell with the interior tile and force a rescan
sta scratch18 ; 748F $80-$FE: irregular rule; zp_18 keeps the value
dec lineErrRow ; 7491 any random bits left in zp_8D?
bpl contourTakeRandomBit ; 7493 yes
lda #$06 ; 7495 seven bits per random byte: counter 6 down through 0
sta lineErrRow ; 7497 seven bits are taken from each random byte
jsr nextScenarioRandom ; 7499 fetch a fresh batch of random bits
sta lineErrCol ; 749C zp_8D = fresh random bits
contourTakeRandomBit:
lsr lineErrCol ; 749E shift out the next bit
bcc contourMaybeFillCell ; 74A0 bit clear - do not erase a neighbour this time
lda scratch18 ; 74A2 the rule value
and #$07 ; 74A4 low 3 bits of the rule value = which neighbour it wants removed
tax ; 74A6 neighbour index 0-7
ldy neighbourOffsetTable,x ; 74A7 its offset from the block corner
lda (mapCellPtr),y ; 74AA that neighbour's terrain code
bmi contourMaybeFillCell ; 74AC unit or marker - leave it alone
tax ; 74AE use the terrain code as the class-table index
lda D_077E,x ; 74AF terrain class of that neighbour
cmp lineMajorLen ; 74B2 only a cell of the interior class may be nibbled away
bne contourMaybeFillCell ; 74B4 not a cell of the hill interior - leave it alone
lda #$40 ; 74B6 $40 = blank
sta (mapCellPtr),y ; 74B8 erase the neighbour - this is what makes hill edges irregular
jmp contourBackUpScan ; 74BA C = 1 and back the scan up
contourMaybeFillCell:
lda scratch18 ; 74BD the rule value again
cmp #$90 ; 74BF rule values $90-$FE...
bcs contourLeaveCell ; 74C1 ...leave the cell untouched when the erase did not fire (C = 0)
contourFillWithInterior:
lda play1Row24 ; 74C3 values $80-$8F (and $FF): fill the cell with the hill interior tile
jsr contourWriteTile ; 74C6 write it into the centre cell
; ----------------------------------------------------------------------
; A cell changed under the scan: back up two columns and two rows so applyContourTiles re-evaluates
; the neighbourhood, and return C = 1.
; ----------------------------------------------------------------------
contourBackUpScan:
lda contourScanCol ; 74C9 current scan column
sec ; 74CB prepare the subtract
sbc #$02 ; 74CC two columns back
bpl contourStoreScanCol ; 74CE still positive
lda #$00 ; 74D0 but never before column 0
contourStoreScanCol:
sta contourScanCol ; 74D2 zp_2E = backed-up scan column
lda contourScanRow ; 74D4 current scan row
contourBackUpRow:
sec ; 74D6 prepare the subtract
sbc #$02 ; 74D7 two rows back
bpl contourStoreScanRow ; 74D9 still positive
lda #$00 ; 74DB but never before row 0
contourStoreScanRow:
sta contourScanRow ; 74DD zp_2D = backed-up scan row
sec ; 74DF C = 1: applyContourTiles restarts its scan here
contourRescanReturn:
rts ; 74E0 C = 1 set at $74DF
contourRingTileFromRule:
sec ; 74E1 prepare the subtract
sbc play1Row42 ; 74E2 tile = rule value - $0324: offset 0 keeps $41-$54, offset $20 gives $21-$34
contourWriteTile:
ldy #$29 ; 74E5 offset $29 = the cell itself
jsr randomTileVariant ; 74E7 pick a random variant of the tile (Y is preserved)
sta (mapCellPtr),y ; 74EA paint the contour tile
contourLeaveCell:
clc ; 74EC C = 0: the scan carries on normally
rts ; 74ED C = 0 set at $74EC
; Two lookup tables shared by the contour/edge pass applyContourRuleAtCell ($7444, just above this
; chunk). That routine keeps zp_48/zp_49 pointing at the top-left cell of the 3x3 block around the
; cell it is examining, so the centre cell is at offset +$29 (one row of 40 = $28, plus one column).
neighbourOffsetTable:
.byte $00,$01,$02,$28,$2A ; 74EE ...(* neighbour offsets NW=+$00, N=+$01, NE=+$02, W=+$28 (one row down), E=+$2A
L_74F3:
.byte $50 ; 74F3 ...SW=+$50 (two rows down), S=+$51, SE=+$52; label L_74F3 only exists because variant B has code here
.byte $51,$52 ; 74F4 QR
; bitMaskTable: byteTable, 8 bytes. $01,$02,$04,$08,$10,$20,$40,$80 - single-bit masks used by the
; terrain fill pass at $745B.
bitMaskTable:
.byte $01,$02,$04,$08,$10,$20,$40,$80; 74F6 ..... @. bit n of the neighbour mask built at $745B, in the same NW,N,NE,W,E,SW,S,SE order as the offsets above
; ----------------------------------------------------------------------
; clearMapMarkers - Clean-up pass of the map generator: walks all 1600 cells of the 40x40 battlefield
; ($F000-$F63F) and turns every cell that still holds the reservation marker in A back into blank
; terrain $40. markRiverBanks ($764A) stamps $80 (river outskirts) and $81 (river bank) into empty
; cells so the hill/forest passes leave the river valley alone; this routine removes them again once
; those passes are done. The map is scanned as six whole pages through a self-modified absolute,Y
; load/store pair plus the 64-byte tail at $F600.
; In: A = marker byte to erase ($80 or $81); the map at $F000-$F63F
; Out: every cell equal to A becomes $40; A and Y clobbered, X untouched. Self-modifies the
; compare operands at $751C/$753C and the page bytes of the load/store at $7517-$7518 and
; $7522-$7523
; Called from: generateBattlefieldMap ($6F47 with $80, $6F5F with $81)
; ----------------------------------------------------------------------
clearMapMarkers:
sta markerScanTest+1 ; 74FE patch the marker value into the CMP #imm at $751B (main-page test)
sta markerTailTest+1 ; 7501 patch the same marker into the CMP #imm at $753B (last-64-cells test)
lda #$00 ; 7504 map starts at $F000, so the low byte of both patched operands is 0
sta markerScanLoad+1 ; 7506 low byte of the self-modified LDA abs,Y at $7516
sta markerScanStore+1 ; 7509 low byte of the self-modified STA abs,Y at $7521
startMarkerScanAtF000:
lda #$F0 ; 750C first page of the map is $F0xx (label only reachable from variant B's code)
sta markerScanLoad+2 ; 750E high byte of the self-modified LDA abs,Y
sta markerScanStore+2 ; 7511 high byte of the self-modified STA abs,Y
ldy #$00 ; 7514 start at the first cell of the page
; ----------------------------------------------------------------------
; Page loop: 256 cells at a time from $F000 through $F5FF. The absolute address of both the load and
; the store is the self-modified operand; the assembled placeholder is $FFFF (rendered as
; irqVectorHi), never the address actually used.
; ----------------------------------------------------------------------
markerScanLoad:
lda irqVectorHi,y ; 7516 read map cell (operand patched to the current page, $F000-$F500)
bpl markerScanNext ; 7519 plain terrain has bit 7 clear - nothing to undo
markerScanTest:
cmp #$FF ; 751B compare against the marker being erased (operand written at $74FE)
bne markerScanNext ; 751D some other bit-7 cell (a unit marker $80|index cannot occur yet) - leave it
lda #$40 ; 751F $40 = blank terrain
markerScanStore:
sta irqVectorHi,y ; 7521 write the blank back (operand patched to the current page)
markerScanNext:
iny ; 7524 next cell in this page
bne markerScanLoad ; 7525 keep going until the page wraps back to offset 0
inc markerScanLoad+2 ; 7527 step the load address to the next page
inc markerScanStore+2 ; 752A step the store address with it
lda markerScanStore+2 ; 752D look at the page just reached
cmp #$F6 ; 7530 $F600 is past the last full page of the map
bcc markerScanLoad ; 7532 not there yet - scan the next 256 cells
; ----------------------------------------------------------------------
; Tail: the map is 1600 = 6*256 + 64 bytes, so the last 64 cells at $F600-$F63F are handled separately
; (backwards, so DEY/BPL can end the loop).
; ----------------------------------------------------------------------
ldy #$3F ; 7534 last cell of the 64-byte tail ($F63F)
markerTailLoop:
lda D_F600,y ; 7536 read cell $F600+Y
bpl markerTailNext ; 7539 plain terrain (bit 7 clear) - nothing to undo here
markerTailTest:
cmp #$FF ; 753B compare against the marker being erased (operand written at $7501)
bne markerTailNext ; 753D not this marker - leave the cell alone
lda #$40 ; 753F $40 = blank terrain
sta D_F600,y ; 7541 restore the blank cell
markerTailNext:
dey ; 7544 walk down to cell 0 of the tail
bpl markerTailLoop ; 7545 loop while the index is still positive
rts ; 7547 map is clean again
; ----------------------------------------------------------------------
; generateRiver - Draws the river across the whole battlefield, left edge to right edge, as the first
; terrain feature of a generated map. A river cell is terrain code $55 + segment type (0-7 ->
; $55-$5C); the segment type also decides whether the cursor then steps right, up or down. Around
; every cell it reserves the banks with markRiverBanks so the later hill and forest passes cannot
; build on top of the water.
; In: mapGenGameFlags $032E (bit 7 = QB SNEAK, bit 6 = DEFENDER), mapSymmetryFlags $032D (bit 7 =
; point-symmetric map), scenario RNG $5A-$5C via nextScenarioRandom ($FD4A), map at $F000
; Out: river tiles $55-$5C written into the map plus $80/$81 bank markers around them; genCol
; $030F, genRow $0310, riverCurSegment $0311 and genTileType $0312 left at the right edge;
; mapSymmetryFlags bit 6 may be set by chooseNextRiverSegment. Returns with no river drawn at
; all for QB SNEAK, and with probability 1/8 for the game types other than DEFENDER
; Called from: runMapGeneratorPasses ($6F39)
; ----------------------------------------------------------------------
generateRiver:
bit play2Grp02 ; 7548 map-style flags derived from the game type
bmi noRiverOnThisMap ; 754B bit 7 = QB SNEAK: that scenario is played on dry land, no river
D_754D:
ldy #$14 ; 754D default start row for a symmetric map: row 20, the centre line
D_754F:
lda invertFontByteLoop ; 754F map symmetry flags (also trainerPlaybook0200:play2Grp01)
sub_7552:
bmi startRiverAtRow ; 7552 bit 7 = point-symmetric map (seed ends in '='): keep the fixed centre row
jsr nextScenarioRandom ; 7554 otherwise roll for the start row
L_7557:
and #$0F ; 7557 0-15...
clc ; 7559 clear carry for the add
adc #$0C ; 755A ...plus 12 = a start row of 12-27, well inside the 40-row map
tay ; 755C keep the start row in Y
bit play2Grp02 ; 755D map-style flags again, this time for bit 6
bvs startRiverAtRow ; 7560 bit 6 = DEFENDER: that scenario always has a river
jsr nextScenarioRandom ; 7562 for every other game type roll once more
and #$07 ; 7565 3 random bits
bne startRiverAtRow ; 7567 7 chances in 8: draw the river
noRiverOnThisMap:
rts ; 7569 1 chance in 8 (and always for QB SNEAK): leave the map dry
; ----------------------------------------------------------------------
; Walk the river from column 0 to column 39. genTileType ($0312) holds the segment chosen for the
; *next* cell; it starts as 5, a plain horizontal straight, so the river enters the map flat.
; ----------------------------------------------------------------------
startRiverAtRow:
sty play1Row22 ; 756A genRow = start row
lda #$00 ; 756D column 0 = the left edge of the map
sta storeRngSeedByte1 ; 756F genCol = 0, the left edge of the map (also trainerPlaybook0200:play1Row21)
lda #$05 ; 7572 segment type 5 = a horizontal straight variant ($5A)
sta play1Row24 ; 7574 genTileType = the segment to draw at the first cell
riverCellLoop:
ldy play1Row22 ; 7577 current row...
ldx storeRngSeedByte1 ; 757A ...and column of the river cursor (also trainerPlaybook0200:play1Row21)
jsr readMapCell ; 757D mapCellPtr ($48/$49) = &map[row][col] (the returned cell value is discarded)
lda play1Row24 ; 7580 segment type chosen for this cell
sta loadRngSeedByte2 ; 7583 riverCurSegment = it, so markRiverBanks/advanceRiverPosition see the same segment (also trainerPlaybook0200:play1Row23)
D_7586:
clc ; 7586 clear carry before the tile base is added
L_7587:
adc #$55 ; 7587 river terrain codes are $55 + segment type, i.e. $55-$5C
ldy #$00 ; 7589 mapCellPtr already points at the cell itself
sta (mapCellPtr),y ; 758B paint the river cell
jsr chooseNextRiverSegment ; 758D decide the segment for the next cell (may also jog the cursor on a symmetric map)
jsr markRiverBanks ; 7590 reserve the 6x6 block of banks around this cell with $80/$81
jsr advanceRiverPosition ; 7593 step the cursor right, up or down according to riverCurSegment
lda storeRngSeedByte1 ; 7596 column reached... (also trainerPlaybook0200:play1Row21)
cmp #$28 ; 7599 ...40 = one past the right edge?
bcc riverCellLoop ; 759B no - draw the next river cell
rts ; 759D the river has crossed the whole map
; ----------------------------------------------------------------------
; chooseNextRiverSegment - Picks the segment type of the next river cell into genTileType ($0312).
; Segment types: 0 = elbow leaving right after an up step ($55), 1 = elbow leaving downward ($56), 2 =
; elbow leaving right after a down step ($57), 3 = elbow leaving upward ($58), 4-7 = horizontal
; straight variants ($59-$5C). Only types 1 and 3 move the cursor vertically, so the river runs flat
; with the occasional one-row jog. Rules: 1 is always followed by 2 and 3 by 0 (the elbow that turns
; the flow back to the right); an elbow is always followed by a straight; a straight is followed by
; another straight about half the time, otherwise by a jog. Columns 0-1 and 38-39 are forced flat,
; and column 37 may not start a jog (the elbow after it would fall off the map). On a point-symmetric
; map the river must run along row 20 and meet its own 180-degree mirror image in the middle, so at
; column 20 it steps back two columns and up one row with probability 7/8.
; In: riverCurSegment $0311, genCol $030F, genRow $0310, mapSymmetryFlags $032D, scenario RNG
; Out: genTileType $0312 = next segment 0-7; on a symmetric map genCol/genRow may be moved and
; mapSymmetryFlags bit 6 set to record that the jog was made
; Called from: generateRiver ($758D)
; Symmetric maps: the river is drawn only once and the caller mirrors the map afterwards, so the line
; has to arrive at the centre of the map in a shape that joins its own rotated copy.
; ----------------------------------------------------------------------
chooseNextRiverSegment:
ldx storeRngSeedByte1 ; 759E column of the cell just drawn (also trainerPlaybook0200:play1Row21)
lda invertFontByteLoop ; 75A1 map symmetry flags (also trainerPlaybook0200:play2Grp01)
bpl chooseFromCurrentSegment; 75A4 bit 7 clear = ordinary random map: use the normal segment rules
cpx #$14 ; 75A6 symmetric map: has the river reached the centre column 20?
bne forceHorizontalSegment ; 75A8 no - force a horizontal straight so it stays on row 20
jsr nextScenarioRandom ; 75AA at the centre, roll for the jog
and #$07 ; 75AD 3 random bits
beq forceHorizontalSegment ; 75AF 1 chance in 8: no jog, stay flat
lda invertFontByteLoop ; 75B1 map symmetry flags (also trainerPlaybook0200:play2Grp01)
ora #$40 ; 75B4 bit 6 = 'the centre jog has been made' (read by the symmetry fix-up)
sta invertFontByteLoop ; 75B6 write them back with the jog recorded (also trainerPlaybook0200:play2Grp01)
checkCentreRow:
ldy play1Row22 ; 75B9 current row
cpy #$14 ; 75BC only jog when the river is still on the centre row 20
bne forceHorizontalSegment ; 75BE otherwise just keep it flat
dec storeRngSeedByte1 ; 75C0 back up two columns... (also trainerPlaybook0200:play1Row21)
dec storeRngSeedByte1 ; 75C3 ...so the two cells about to be redrawn form the joint (also trainerPlaybook0200:play1Row21)
jogRiverUpOneRow:
dec play1Row22 ; 75C6 and move up to row 19, above the mirror line
bne forceHorizontalSegment ; 75C9 always taken (row 19 is not 0) - draw a straight there
; ----------------------------------------------------------------------
; Ordinary maps: the next segment follows from the current one. Elbows 1 and 3 must be closed by
; their partner elbow, everything else picks randomly between straights and jogs.
; ----------------------------------------------------------------------
chooseFromCurrentSegment:
lda loadRngSeedByte2 ; 75CB segment just drawn (also trainerPlaybook0200:play1Row23)
cmp #$01 ; 75CE was it the 'turn downward' elbow?
bne checkUpElbow ; 75D0 not the downward elbow - test the next case
lda #$02 ; 75D2 yes - the cell below it must be the elbow that turns the flow right again
bne clampSegmentToColumn ; 75D4 always taken (A = 2)
checkUpElbow:
cmp #$03 ; 75D6 was it the 'turn upward' elbow?
bne chooseStraightOrJog ; 75D8 not the upward elbow either - fall through to the random choice
lda #$00 ; 75DA yes - the cell above it must be elbow 0, which turns the flow right again
beq clampSegmentToColumn ; 75DC always taken (A = 0)
chooseStraightOrJog:
cmp #$03 ; 75DE types 0-2 are elbows, 4-7 are straights
bcs rollNextSegment ; 75E0 current cell is a straight - it may be followed by a jog
jsr nextScenarioRandom ; 75E2 current cell was an elbow: the next one must be a straight
and #$03 ; 75E5 pick one of the four straight variants...
makeStraightSegment:
clc ; 75E7 clear carry for the add
adc #$04 ; 75E8 ...i.e. segment 4-7
bne clampSegmentToColumn ; 75EA always taken (the value is 4-7)
rollNextSegment:
jsr nextScenarioRandom ; 75EC random byte
bpl maskSegmentType ; 75EF random bit 7 clear: leave the value alone, so a jog stays possible
ora #$04 ; 75F1 ...force bit 2, which restricts the result to a straight 4-7
maskSegmentType:
and #$07 ; 75F3 keep 3 bits: a segment type 0-7
beq chooseStraightOrJog ; 75F5 type 0 is an elbow that cannot start a jog - draw a straight instead
rejectClosingElbow:
cmp #$02 ; 75F7 type 2 likewise closes a jog rather than starting one
beq chooseStraightOrJog ; 75F9 reject it the same way (A is 0 or 2, so the retry always takes the straight path)
; ----------------------------------------------------------------------
; Edge guard: a jog needs room for its closing elbow, so the first two and the last three columns are
; always drawn flat.
; ----------------------------------------------------------------------
clampSegmentToColumn:
ldx storeRngSeedByte1 ; 75FB column of the cell just drawn (also trainerPlaybook0200:play1Row21)
cpx #$02 ; 75FE columns 0-1: the river must enter the map horizontally
bcc forceHorizontalSegment ; 7600 column 0 or 1 - force a horizontal straight
cpx #$25 ; 7602 is the cell in column 37, the last one that could start a jog?
bcc storeNextSegment ; 7604 columns 2-36: accept whatever was chosen
bne forceHorizontalSegment ; 7606 columns 38-39: force a straight so the river leaves the map horizontally
cmp #$01 ; 7608 column 37: reject a downward jog...
beq forceHorizontalSegment ; 760A downward jog rejected - draw a straight instead
cmp #$03 ; 760C ...and an upward jog, since the closing elbow would land in column 39
L_760E:
bne storeNextSegment ; 760E not a jog - the straight that was chosen is fine in column 37
forceHorizontalSegment:
jsr nextScenarioRandom ; 7610 random byte
and #$03 ; 7613 one of the four...
clc ; 7615 clear carry for the add
adc #$04 ; 7616 ...horizontal straight variants 4-7
storeNextSegment:
sta play1Row24 ; 7618 genTileType = segment type of the next river cell
rts ; 761B back to generateRiver
; ----------------------------------------------------------------------
; advanceRiverPosition - Moves the river cursor one cell according to the segment just drawn: type 1
; (elbow turning downward) steps a row down, type 3 (elbow turning upward) a row up, every other type
; one column right. Implemented as a jump through a two-part address table, so the JMP operand at
; $762C/$762D is rewritten on every call.
; In: riverCurSegment $0311, tables riverStepLo $763A / riverStepHi $7642
; Out: genCol $030F or genRow $0310 stepped by one; the operand of the JMP at $762B is self-modified
; Called from: generateRiver ($7593)
; ----------------------------------------------------------------------
advanceRiverPosition:
ldy loadRngSeedByte2 ; 761C segment type of the cell just drawn (also trainerPlaybook0200:play1Row23)
lda riverStepLo,y ; 761F low byte of the step routine for that type
sta riverStepJump+1 ; 7622 patch it into the JMP operand at $762C
lda riverStepHi,y ; 7625 high byte (always $76)
sta riverStepJump+2 ; 7628 patch it into $762D
riverStepJump:
jmp irqVectorHi ; 762B jump to riverStepRight/Up/Down; the assembled operand $FFFF is only a placeholder
; ----------------------------------------------------------------------
; riverStepRight - Step stub for every segment type that leaves the cell to the right (0, 2 and the
; straights 4-7): advances the river cursor one column.
; In: none
; Out: genCol $030F + 1
; Called from: the patched JMP at $762B in advanceRiverPosition
; ----------------------------------------------------------------------
riverStepRight:
inc storeRngSeedByte1 ; 762E river cursor moves one column right (also trainerPlaybook0200:play1Row21)
rts ; 7631 back to generateRiver, which called advanceRiverPosition
; ----------------------------------------------------------------------
; riverStepUp - Step stub for segment type 3, the elbow that turns the river upward: moves the cursor
; one row up (towards row 0).
; In: none
; Out: genRow $0310 - 1
; Called from: the patched JMP at $762B in advanceRiverPosition
; ----------------------------------------------------------------------
riverStepUp:
dec play1Row22 ; 7632 river cursor moves one row up
rts ; 7635 back to generateRiver, which called advanceRiverPosition
; ----------------------------------------------------------------------
; riverStepDown - Step stub for segment type 1, the elbow that turns the river downward: moves the
; cursor one row down.
; In: none
; Out: genRow $0310 + 1
; Called from: the patched JMP at $762B in advanceRiverPosition
; ----------------------------------------------------------------------
riverStepDown:
inc play1Row22 ; 7636 river cursor moves one row down
rts ; 7639 back to generateRiver, which called advanceRiverPosition
; Split address table of the three step stubs, indexed by segment type 0-7 and read by
; advanceRiverPosition ($761F/$7625). Only type 1 ($7636 = down) and type 3 ($7632 = up) move the
; river vertically; every other type maps to $762E (right).
riverStepLo:
.byte $2E,$36,$2E,$32,$2E ; 763A .6.2. low bytes for types 0-4: right, down, right, up, right
L_763F:
.byte $2E ; 763F low bytes for types 5-7: all three straights step right
.byte $2E,$2E ; 7640 ..
; riverStepHi: addrTable, 8 bytes. high bytes ($76) of the river step routine addresses.
riverStepHi:
.byte $76,$76,$76,$76,$76,$76,$76,$76; 7642 vvvvvvvv high bytes: all three stubs live in page $76
; ----------------------------------------------------------------------
; markRiverBanks - Reserves the ground around the current river cell so the hill and forest passes
; that follow keep the river valley clear. Away from the map edges it addresses the 6x6 block whose
; top-left corner is (genCol & ~1) - 2, (genRow & ~1) - 2, writes $80 into every blank cell of the
; 20-cell perimeter and $81 into the 16 cells of the 4x4 core (upgrading a $80 already there). Only
; cells that are still blank ($40 or lower) are touched, so terrain already placed - including the
; river itself - survives. In columns 0-1 and 38-39 only the four cells of the river's own column get
; $81. clearMapMarkers ($74FE) erases both marker values again at the end of the generator.
; In: genCol $030F, genRow $0310, map addressed through readMapCell ($25FA -> mapCellPtr zp_48/49),
; tables riverZoneOuterOffsets $7894 and riverZoneInnerOffsets $78A8
; Out: $80/$81 written into blank map cells; mapCellPtr points at the block origin; A/X/Y clobbered
; Called from: generateRiver ($7590)
; ----------------------------------------------------------------------
markRiverBanks:
ldx storeRngSeedByte1 ; 764A column of the river cell just drawn (also trainerPlaybook0200:play1Row21)
cpx #$02 ; 764D columns 0 and 1 have no room for a 6x6 block to their left
bcs checkRightEdgeColumn ; 764F inside the map - do the full 6x6 reservation
; ----------------------------------------------------------------------
; Edge columns (0, 1, 38, 39): reserve only the four cells directly above and below the river in the
; river's own column, using the first four entries of riverZoneOuterOffsets ($28, $50, $78, $A0 = one,
; two, three and four rows down).
; ----------------------------------------------------------------------
markEdgeColumnOnly:
lda play1Row22 ; 7651 row of the river cell
and #$FE ; 7654 snap it down to an even row (the generator works in 2x2 blocks)
tay ; 7656 Y = the even row
dey ; 7657 two rows above...
dey ; 7658 ...the even row, so the four offsets cover evenRow-1 to evenRow+2
jsr readMapCell ; 7659 mapCellPtr = &map[evenRow-2][genCol] (X still holds the column)
ldx #$03 ; 765C four offsets, walked backwards
markEdgeCellLoop:
ldy riverZoneOuterOffsets,x ; 765E offset of the cell: +1, +2, +3 or +4 rows in the same column
lda (mapCellPtr),y ; 7661 read that cell
bmi writeBankMarkerEdge ; 7663 bit 7 set = already an $80/$81 reservation marker - upgrade it
cmp #$41 ; 7665 $40 and below is blank ground
bcs markEdgeCellNext ; 7667 real terrain is already here - leave it alone
writeBankMarkerEdge:
lda #$81 ; 7669 $81 = river bank, the strongest reservation
sta (mapCellPtr),y ; 766B reserve the cell as river bank
markEdgeCellNext:
dex ; 766D next of the four offsets
bpl markEdgeCellLoop ; 766E loop over all four offsets
rts ; 7670 done with the edge column
; ----------------------------------------------------------------------
; Interior columns 2-37: build the 6x6 block, mark its 20-cell perimeter $80 and its 4x4 core $81.
; ----------------------------------------------------------------------
checkRightEdgeColumn:
cpx #$26 ; 7671 is the river within two columns of the right edge (38 or 39)?
bcs markEdgeColumnOnly ; 7673 yes - too close to the right edge for a 6x6 block
txa ; 7675 column of the river cell
and #$FE ; 7676 snap the column down to an even one
tax ; 7678 X = the even column
lda play1Row22 ; 7679 row of the river cell
and #$FE ; 767C snap the row down to an even one
tay ; 767E Y = the even row
dex ; 767F step two columns left...
dex ; 7680 ...to the left edge of the 6x6 block
dey ; 7681 step two rows up...
dey ; 7682 ...to the top edge of the 6x6 block
jsr readMapCell ; 7683 mapCellPtr = &map[evenRow-2][evenCol-2]
ldx #$13 ; 7686 20 perimeter offsets, walked backwards
markOuterCellLoop:
ldy riverZoneOuterOffsets,x ; 7688 offset of the next perimeter cell inside the 6x6 block
lda (mapCellPtr),y ; 768B read it
cmp #$41 ; 768D anything above $40 - real terrain or an existing marker - is left as it is
bcs markOuterCellNext ; 768F already terrain or a marker - leave it
lda #$80 ; 7691 $80 = river outskirts, the weaker reservation
sta (mapCellPtr),y ; 7693 reserve the perimeter cell
markOuterCellNext:
dex ; 7695 next perimeter offset
bpl markOuterCellLoop ; 7696 loop over all 20 perimeter offsets
; ----------------------------------------------------------------------
; The 4x4 core of the block, i.e. the cells that actually touch the water, gets the stronger $81
; marker; an $80 written by a neighbouring river cell is upgraded to $81 here.
; ----------------------------------------------------------------------
markInnerCells:
ldx #$0F ; 7698 16 core offsets, walked backwards
markInnerCellLoop:
ldy riverZoneInnerOffsets,x ; 769A offset of the next core cell
lda (mapCellPtr),y ; 769D read it
bmi writeBankMarkerInner ; 769F already a marker ($80 or $81) - upgrade to $81
cmp #$41 ; 76A1 $40 and below is blank ground
bcs markInnerCellNext ; 76A3 real terrain is already here - leave it
writeBankMarkerInner:
lda #$81 ; 76A5 $81 = river bank
sta (mapCellPtr),y ; 76A7 reserve the core cell as river bank
markInnerCellNext:
dex ; 76A9 next core offset
bpl markInnerCellLoop ; 76AA loop over all 16 core offsets
rts ; 76AC back to generateRiver
; ----------------------------------------------------------------------
; mapNameEntryScreen - The 'TYPE MAP NAME' page of the game setup. A map name is simply the game's
; 24-bit map seed written in base 32 with the alphabet at $890C, so typing a name picks a battlefield
; and the two players only have to agree on those five characters. The routine draws the current map,
; then loops: enterMapName returns a new seed (C=0) - adopt it, regenerate and redraw the map, ask
; again; or the fire button (C=1) - print 'ACCEPTED' and return to the setup menu.
; In: mapSeed $0B95-$0B97, mapFlipFlag $0B9D, keyboard and joystick
; Out: mapSeed and mapFlipFlag updated, the map at $F000 regenerated and drawn in the overview
; window, text colour zp_3D / pad colour zp_42 = 9
; Called from: the setup menu, by JMP from $8339 (the MAP item)
; ----------------------------------------------------------------------
mapNameEntryScreen:
jsr generateBattlefieldMap ; 76AD build the 40x40 map from the current seed
lda mapFlipFlag ; 76B0 is the map shown rotated 180 degrees?
beq drawMapAndClearName ; 76B3 flip flag clear - show the map as generated
jsr rotateMapAndMirrorCodes ; 76B5 yes - rotate the map and mirror the direction-dependent terrain codes
drawMapAndClearName:
jsr drawOverviewMap ; 76B8 paint the whole map into the 20x20 overview window
jsr clearMapNameField ; 76BB blank the five name characters $0BA9-$0BAD
; ----------------------------------------------------------------------
; Prompt loop. Each pass reprints both prompts and calls enterMapName; it ends only when the player
; presses the fire button.
; ----------------------------------------------------------------------
mapNamePromptLoop:
lda #$09 ; 76BE colour 9 = brown, the setup-panel text colour
sta textColour ; 76C0 text colour
sta textPadColour ; 76C2 and the colour of the padding around centred text
jsr clearInfoPanel ; 76C4 clear the info panel under the tactical view
lda #$77 ; 76C7 string low byte...
ldy #$06 ; 76C9 ...and high byte: msgTypeMapName $0677, 'TYPE MAP NAME / (F7 RANDOM) / (F5 SYM/RND)'
jsr printString ; 76CB print the three-line prompt
printAcceptPrompt:
lda #$00 ; 76CE leftmost column of the panel
sta textCursorCol ; 76D0 set it as the cursor column
lda #$13 ; 76D2 row 19 of the text grid
jsr setCursorRow ; 76D4 move the cursor there
lda #$9F ; 76D7 string low byte...
ldy #$06 ; 76D9 ...and high byte: msgBtnToAccept $069F, 'BTN TO ACCEPT / (F 1 TO FLIP)'
jsr printString ; 76DB print the two-line prompt
jsr enterMapName ; 76DE run the interactive name entry
bcs acceptCurrentMap ; 76E1 C=1: the fire button was pressed - accept the map on screen
lda scenarioSeed0 ; 76E3 C=0: enterMapName left a fresh 24-bit seed in $5A-$5C
sta mapSeed ; 76E5 adopt it as the map seed...
lda scenarioSeed1 ; 76E8 seed byte 1...
sta mapSeed1 ; 76EA ...into mapSeed1
lda scenarioSeed2 ; 76ED seed byte 2...
sta mapSeed2 ; 76EF ...into mapSeed2
jsr clearBattlefieldMap ; 76F2 wipe the map back to blank terrain $40 (also trainerSetupEC00:mainGroupByPlanTable)
lda #$00 ; 76F5 colour 0
sta textColour ; 76F7 text colour 0 while redrawing
sta mapFlipFlag ; 76F9 a freshly named map is always shown unflipped
jsr clearOverviewWindow ; 76FC clear the 20x20 overview window
jsr generateBattlefieldMap ; 76FF generate the new battlefield from the new seed
redrawOverviewAfterNewMap:
jsr drawOverviewMap ; 7702 draw it
jmp mapNamePromptLoop ; 7705 and ask for another name
; ----------------------------------------------------------------------
; Accept: the map currently on screen is the one that will be played.
; ----------------------------------------------------------------------
acceptCurrentMap:
jsr clearInfoPanel ; 7708 clear the info panel
lda #$0F ; 770B row 15
jsr setCursorRow ; 770D move the cursor to row 15
lda #$ED ; 7710 string low byte...
L_7712:
ldy #$EF ; 7712 ...and high byte: 'ACCEPTED' at $EFED in the $EC00 comm-module tail; tail-call back to the menu
jmp printStringCentred ; 7714 centre the word in the panel and return to the setup menu
; ----------------------------------------------------------------------
; enterMapName - Reads a five-character map name at column 4, row 16 and converts it into the 24-bit
; map seed in $5A-$5C. The field is underlined with five $25 glyphs and the cursor cell is marked by
; writing colour $11 into the cell's colour-RAM byte. Only the 32 characters of the alphabet at $890C
; (space, A-Z, + - * / =) are accepted, five bits each, first character in the high bits; $88 (DEL)
; erases; any other key just beeps. F1 flips the displayed map, F7 takes a random seed from the game
; RNG and F5 does the same but forces the low five bits to 1 so the name ends in '=' - the marker for
; a point-symmetric map. The fire button ends entry with C=1. The character counter lives at $0313;
; the symbol commandDispatchLoop comes from the code that occupies $0300-$03FF in the other overlay,
; it has nothing to do with commands here.
; In: key events from newKeyEvent $90EA, function key requestedScreenKey zp_66, the fire button via
; checkFirePressed ($0EB1), game RNG $57-$59, alphabet $890C
; Out: C=0 - $5A-$5C hold the chosen 24-bit seed and $0BA9-$0BAD its five characters; C=1 - the fire
; button was pressed and nothing was chosen. $0313 = characters typed, mapFlipFlag $0B9D may
; have been toggled, zp_18 scratch, text cursor moved
; Called from: mapNameEntryScreen ($76DE) and the manual look-up copy protection ($09F9)
; ----------------------------------------------------------------------
enterMapName:
jsr printMapName ; 7717 print the (blank) name field and leave the cursor after it
lda #$04 ; 771A column 4 again - printMapName left the cursor on the row below the field
sta textCursorCol ; 771C cursor back to column 4, the first character of the field
jsr updateCursorPointers ; 771E recompute the bitmap and colour-RAM pointers for that cell
lda #$25 ; 7721 glyph $25 = the underline character
sta textFillGlyph ; 7723 make it the fill glyph
lda #$05 ; 7725 five character cells to underline
jsr bootReceiveSectorData ; 7727 $C0EF is printFillChars: underline the five character cells (also textEngineC000:printFillChars)
lda #$00 ; 772A glyph 0 = the normal blank
sta textFillGlyph ; 772C restore the normal blank glyph
sta storeRngSeedByte2 ; 772E $0313 = number of characters typed so far = 0 (also trainerPlaybook0200:play1Row25)
sta scenarioSeed0 ; 7731 clear the 24-bit seed accumulator...
sta scenarioSeed1 ; 7733 seed byte 1
sta scenarioSeed2 ; 7735 ...all three bytes
jsr clearMapNameField ; 7737 and blank the five name characters
; ----------------------------------------------------------------------
; Input loop: one pass per frame. It services the opponent link, handles F1 (flip), tests the fire
; button and then consumes at most one key event.
; ----------------------------------------------------------------------
mapNameInputLoop:
jsr serviceLinkWhileGenerating; 773A keep the modem/trainer exchange alive while the player types
lda requestedScreenKey ; 773D function key from the IRQ poll: 0-3 = F1/F3/F5/F7, $FF = none
bne checkAcceptButton ; 773F not F1 (function key code 0) - no flip
toggleMapFlip:
lda mapFlipFlag ; 7741 F1: flip the map end for end
eor #$01 ; 7744 toggle bit 0
sta mapFlipFlag ; 7746 store the new flip state
jsr rotateMapAndMirrorCodes ; 7749 rotate the map 180 degrees and mirror the direction-dependent terrain codes
jsr drawOverviewMap ; 774C redraw the overview window
lda #$00 ; 774F A = 0 so the fire test below starts from a known value (A is not used by it)
checkAcceptButton:
jsr checkFirePressed ; 7751 fresh press of the fire button? (returns A=0 on a new press)
bne handleTypedKey ; 7754 no press - carry on reading keys
lda #$01 ; 7756 sound 1 = the click checkFirePressed has just played; this repeats it
jsr playSound ; 7758 a second click on top of the one checkFirePressed already played
sec ; 775B C=1 tells the caller 'accepted, no new name'
rts ; 775C return to the caller with 'accepted'
; ----------------------------------------------------------------------
; Key handling. newKeyEvent ($90EA) holds the last key the IRQ saw, $FF when there is none; it is
; reset to $FF once the key has been dealt with.
; ----------------------------------------------------------------------
handleTypedKey:
lda newKeyEvent ; 775D last key event
cmp #$FF ; 7760 $FF means the IRQ has not seen a key
beq checkNameComplete ; 7762 no key this frame - go on to the completion tests
ldy storeRngSeedByte2 ; 7764 characters typed so far (also trainerPlaybook0200:play1Row25)
bne checkDeleteKey ; 7767 not the first one - keep what is on screen
pha ; 7769 first keystroke: save the key
jsr setMapNameCursor ; 776A cursor back to the start of the field
jsr bootToggleDataAck ; 776D and wipe the underlined field (also textEngineC000:clearToEndOfLine)
pla ; 7770 restore the key
checkDeleteKey:
cmp #$88 ; 7771 $88 = the DEL key
bne lookUpKeyInAlphabet ; 7773 not DEL - try the alphabet
ldy storeRngSeedByte2 ; 7775 characters typed so far (also trainerPlaybook0200:play1Row25)
beq keyRejected ; 7778 field already empty - just beep
dec storeRngSeedByte2 ; 777A one character less (also trainerPlaybook0200:play1Row25)
jsr printChar ; 777D printChar sees $88 as a backspace: step back, erase the glyph, step back again
ldy #$00 ; 7780 offset 0 = the cell the cursor has just stepped back to
lda #$11 ; 7782 colour $11 = the cursor block
sta (screenPtr),y ; 7784 mark the freed cell as the cursor (screenPtr points into colour RAM here)
iny ; 7786 offset 1 = the cell to its right
lda textColour ; 7787 normal text colour
sta (screenPtr),y ; 7789 restore the cell to the right, which held the cursor block before the delete
ldy storeRngSeedByte2 ; 778B index of the freed character (also trainerPlaybook0200:play1Row25)
lda #$20 ; 778E space
storeBlankInNameField:
sta mapNameText,y ; 7790 blank it in the name buffer $0BA9-$0BAD
bne playKeyClick ; 7793 always taken ($20 is not 0) - play the key click
; ----------------------------------------------------------------------
; Ordinary character: look it up in the 32-entry map-name alphabet at $890C. A key that is not in the
; alphabet is rejected with the error beep.
; ----------------------------------------------------------------------
lookUpKeyInAlphabet:
sta scratch18 ; 7795 keep the raw key (bit 7 may be set by the keyboard scanner)
and #$7F ; 7797 strip bit 7 to get plain ASCII
ldy #$1F ; 7799 search the alphabet backwards from index 31
alphabetSearchLoop:
cmp mapNameAlphabet,y ; 779B mapNameAlphabet[y]: space, A-Z, +, -, *, /, =
beq storeNameCharacter ; 779E found it - Y is the 5-bit code of the character
dey ; 77A0 try the previous alphabet entry
bpl alphabetSearchLoop ; 77A1 keep searching down to index 0
keyRejected:
lda #$03 ; 77A3 sound 3 = the reject beep
bne playKeySound ; 77A5 always taken (A = 3) - play the reject beep
storeNameCharacter:
ldy storeRngSeedByte2 ; 77A7 index of the next free character (also trainerPlaybook0200:play1Row25)
sta mapNameText,y ; 77AA store the character in the name buffer
inc storeRngSeedByte2 ; 77AD one character more (also trainerPlaybook0200:play1Row25)
lda scratch18 ; 77B0 the raw key as typed
jsr printChar ; 77B2 draw it into the field
lda storeRngSeedByte2 ; 77B5 characters typed so far (also trainerPlaybook0200:play1Row25)
cmp #$05 ; 77B8 are all five characters of the field filled now?
bcs playKeyClick ; 77BA yes - no cursor block past the end of the field
ldy #$00 ; 77BC offset 0 = the cell the cursor has just advanced to
lda #$11 ; 77BE colour $11 = the cursor block
sta (screenPtr),y ; 77C0 mark the next cell as the cursor
playKeyClick:
lda #$02 ; 77C2 sound 2 = the key click
playKeySound:
jsr playSound ; 77C4 play it
lda #$FF ; 77C7 $FF = no key pending
sta newKeyEvent ; 77C9 mark the key event as consumed
; ----------------------------------------------------------------------
; After every frame: five characters means the name is complete, otherwise F5/F7 may still pick a
; random one. nextGameRandom is stepped every pass so the random seed depends on how long the player
; took.
; ----------------------------------------------------------------------
checkNameComplete:
ldy storeRngSeedByte2 ; 77CC characters typed (also trainerPlaybook0200:play1Row25)
cpy #$05 ; 77CF five = a complete map name
bcs packNameIntoSeed ; 77D1 pack it into the 24-bit seed
jsr nextGameRandom ; 77D3 stir the game RNG once per frame
lda gameRngState0 ; 77D6 low byte of the new RNG state
ldy requestedScreenKey ; 77D8 function key: 0-3 = F1/F3/F5/F7, $FF = none
bmi waitForMoreInput ; 77DA $FF - no function key, keep waiting
checkRandomKeys:
cpy #$02 ; 77DC F5 has function key code 2, F7 has 3
bcs checkSymmetricRandom ; 77DE one of them - take a random map
waitForMoreInput:
jmp mapNameInputLoop ; 77E0 F1 or F3 - back to the input loop
checkSymmetricRandom:
bne storeRandomSeed ; 77E3 F7 (3) gives a completely random map
ora #$1F ; 77E5 F5 (2) sets the low five bits, so the name ends in '=' = a point-symmetric map
storeRandomSeed:
sta scenarioSeed0 ; 77E7 seed byte 0 = the RNG low byte
ldy gameRngState1 ; 77E9 RNG byte 1
sty scenarioSeed1 ; 77EB seed byte 1
ldx gameRngState2 ; 77ED RNG byte 2
stx scenarioSeed2 ; 77EF seed byte 2
jsr seedToMapName ; 77F1 spell the seed out as five characters in $0BA9-$0BAD (A/Y/X already hold it)
jmp showNameAndAccept ; 77F4 show it and return C=0
; ----------------------------------------------------------------------
; Pack the five typed characters into the 24-bit seed: index of the character in the 32-entry
; alphabet, five bits each, first character in the most significant bits.
; ----------------------------------------------------------------------
packNameIntoSeed:
ldy #$00 ; 77F7 start at the first character, and use Y as the zero to clear the accumulator
clearSeedAccumulator:
sty scenarioSeed0 ; 77F9 seed = 0...
sty scenarioSeed1 ; 77FB seed byte 1
sty scenarioSeed2 ; 77FD ...all 24 bits
packNameCharLoop:
lda mapNameText,y ; 77FF character Y of the name
ldx #$1F ; 7802 search the alphabet backwards from index 31
alphabetIndexLoop:
cmp mapNameAlphabet,x ; 7804 compare the character with alphabet entry X
beq shiftSeedLeftFive ; 7807 found - X is its 5-bit code
dex ; 7809 try the previous alphabet entry
bpl alphabetIndexLoop ; 780A search all 32 alphabet entries
inx ; 780C unreachable: every character in the buffer was validated when it was typed...
brk ; 780D ...so this BRK (which would crash) is really an assertion
shiftSeedLeftFive:
stx scratch18 ; 780E keep the 5-bit character code
ldx #$05 ; 7810 shift the accumulator five bits left
shiftSeedBitLoop:
asl scenarioSeed0 ; 7812 24-bit shift left by one...
rol scenarioSeed1 ; 7814 carry into seed byte 1...
rol scenarioSeed2 ; 7816 ...and on into seed byte 2
dex ; 7818 count the five shifts
bne shiftSeedBitLoop ; 7819 five single-bit shifts in all
lda scenarioSeed0 ; 781B low byte of the accumulator
and #$E0 ; 781D redundant: the five ASLs already left bits 0-4 clear
ora scratch18 ; 781F merge the character code into the low five bits
sta scenarioSeed0 ; 7821 store it back
iny ; 7823 next character of the name
cpy #$05 ; 7824 all five characters packed?
bcc packNameCharLoop ; 7826 keep packing until all five are in
showNameAndAccept:
jsr printMapName ; 7828 print the finished name in the field
D_782B:
clc ; 782B C=0: the caller gets a new seed in $5A-$5C
rts ; 782C return to the caller with the new seed
; ----------------------------------------------------------------------
; setMapNameCursor - Moves the text cursor to the start of the map-name field: column 4 of the current
; text window, row 16.
; In: the text window set up by the caller (left column zp_3B)
; Out: textCursorCol zp_3F = 4, textCursorRow zp_40 = $10, cursor pointers zp_B4-zp_B7 recomputed
; Called from: enterMapName ($776A) and printMapName ($7841)
; ----------------------------------------------------------------------
setMapNameCursor:
lda #$04 ; 782D the map-name field starts at column 4
sta textCursorCol ; 782F set it as the cursor column
lda #$10 ; 7831 row 16
jmp setCursorRow ; 7833 sets the row and recomputes the cell pointers
; ----------------------------------------------------------------------
; clearMapNameField - Fills the five map-name characters $0BA9-$0BAD with spaces.
; In: none
; Out: mapNameText $0BA9-$0BAD = $20, Y = $FF
; Called from: mapNameEntryScreen ($76BB), enterMapName ($7737) and the manual look-up copy
; protection ($09F6)
; ----------------------------------------------------------------------
clearMapNameField:
ldy #$04 ; 7836 five characters, index 4 down to 0
clearNameCharLoop:
lda #$20 ; 7838 space
sta mapNameText,y ; 783A mapNameText[y]
dey ; 783D walk down to character 0
L_783E:
bpl clearNameCharLoop ; 783E until index 0 has been written
rts ; 7840 the field is blank
; ----------------------------------------------------------------------
; printMapName - Prints the five-character map name at its place on the setup screen. The byte after
; the name, $0BAE, is used as the string terminator: $8D is a carriage return ($0D) with bit 7 set, so
; the name is printed and the cursor drops to the next row.
; In: mapNameText $0BA9-$0BAD
; Out: mapNameTerminator $0BAE = $8D, the name drawn at column 4 row 16, cursor on row 17
; Called from: enterMapName ($7717 and $7828)
; ----------------------------------------------------------------------
printMapName:
jsr setMapNameCursor ; 7841 cursor to column 4, row 16
lda #$8D ; 7844 $8D = CR with bit 7 = 'last character of the string'
setMapNameTerminator:
sta mapNameTerminator ; 7846 terminate the name after five characters
lda #$A9 ; 7849 string low byte...
ldy #$0B ; 784B ...and high byte: mapNameText $0BA9
jmp printString ; 784D print it
; ----------------------------------------------------------------------
; seedToMapName - Spells a 24-bit map seed out as its five-character name: five times take the low
; five bits as an index into the 32-character alphabet at $890C and shift the seed right by five, so
; the last character carries the lowest bits. This is the exact inverse of the packing loop at
; $77F7, and it is why a map's name and its seed are the same number.
; In: A = seed byte 0, Y = seed byte 1, X = seed byte 2 (the same order as $5A/$5B/$5C)
; Out: mapNameText $0BA9-$0BAD; zp_18-zp_1A used as the shift register; X and Y clobbered
; Called from: runMapGeneratorPasses ($6F36, to name the map it has just built) and enterMapName
; ($77F1, after an F5/F7 random seed)
; ----------------------------------------------------------------------
seedToMapName:
sta scratch18 ; 7850 shift register byte 0
sty scratch19 ; 7852 byte 1
stx scratch1A ; 7854 byte 2
ldy #$04 ; 7856 fill the name from the last character backwards
seedCharLoop:
lda scratch18 ; 7858 low byte of the remaining seed
and #$1F ; 785A the low five bits are one character
tax ; 785C use the 5-bit code as an index into the alphabet
lda mapNameAlphabet,x ; 785D mapNameAlphabet: space, A-Z, +, -, *, /, =
sta mapNameText,y ; 7860 store it in the name buffer
shiftSeedRightFive:
ldx #$05 ; 7863 shift the seed right by five bits
shiftSeedRightBit:
lsr scratch1A ; 7865 24-bit shift right by one...
ror scratch19 ; 7867 carry down into byte 1...
ror scratch18 ; 7869 ...and on into byte 0
dex ; 786B count the five shifts
bne shiftSeedRightBit ; 786C five single-bit shifts in all
dey ; 786E next character to the left
L_786F:
bpl seedCharLoop ; 786F loop over all five characters
rts ; 7871 the five characters are in $0BA9-$0BAD
; ----------------------------------------------------------------------
; randomTileVariant - Picks a random visual variant of a terrain code so that large areas of the same
; terrain do not look tiled. The mask table at $0770 (tileVariantMaskTable, part of the terrain rules
; loaded from track 18) is indexed by tile code - $40, or tile code - $20 for the codes below $40, and
; gives 0, 1 or 3 = the number of extra variants; the result is the base code plus a random value
; masked with it. Y is preserved through a self-modified LDY #imm.
; In: A = base terrain code, tileVariantMaskTable $0770, scenario RNG
; Out: A = the code with random variant bits added; Y preserved, X untouched. Self-modifies the
; operands at $788D (AND #mask), $7890 (ADC #base) and $7892 (LDY #savedY)
; Called from: the hill/forest fill pass ($72A4) and the contour pass ($74E7)
; ----------------------------------------------------------------------
randomTileVariant:
nop ; 7872 leftover NOP - probably a patched-out instruction
sta addBaseTileCode+1 ; 7873 remember the base tile code in the ADC #imm at $788F
sec ; 7876 set carry for the subtraction
sbc #$40 ; 7877 terrain codes $40 and up index the mask table directly
adjustLowTileIndex:
bcs lookUpVariantMask ; 7879 code was $40 or higher - the index is code - $40
adc #$20 ; 787B code below $40: add $20 back, giving an index of code - $20
lookUpVariantMask:
sty restoreCallerY+1 ; 787D save Y in the LDY #imm at $7891 so the caller's Y survives
tay ; 7880 index into the variant mask table
lda tileVariantMaskTable,y ; 7881 number of extra variants as a bit mask: 0, 1 or 3
beq combineVariantWithBase ; 7884 0 = this terrain has only one look, A is already the answer
sta maskRandomVariant+1 ; 7886 patch the mask into the AND #imm at $788C
jsr nextScenarioRandom ; 7889 random byte
maskRandomVariant:
and #$FF ; 788C keep only as many random bits as the terrain has variants (operand patched above)
combineVariantWithBase:
clc ; 788E clear carry for the add
addBaseTileCode:
adc #$FF ; 788F add the base tile code back (operand patched at $7873)
restoreCallerY:
ldy #$FF ; 7891 restore the caller's Y (operand patched at $787D)
D_7893:
rts ; 7893 A is the terrain code to paint
; The 6x6 reservation block used by markRiverBanks, expressed as offsets from its top-left cell. The
; map row stride is $28 (40 cells). Entries 0-3 are the left column (rows 1-4) and are also used on
; their own near the map edges, where they address the four cells above and below the river in its own
; column; entries 4-9 are the top row, 10-13 the right column (rows 1-4) and 14-19 the bottom row.
; Together they are the 20-cell perimeter, marked $80.
riverZoneOuterOffsets:
.byte $28,$50,$78,$A0 ; 7894 (Px. left column, rows 1-4 ($28 = one row): also the edge-column-only case
D_7898:
.byte $00 ; 7898 top row, columns 0-5, then the right column rows 1-2 ($2D = row 1 col 5, $55 = row 2 col 5)
.byte $01,$02,$03,$04,$05,$2D,$55; 7899 .....-U
.byte $7D ; 78A0 right column rows 3-4, then the bottom row ($C8 = row 5 col 0) columns 0-5
.byte $A5,$C8,$C9,$CA,$CB,$CC,$CD; 78A1 .......
; The inner 4x4 cells (rows 1-4, columns 1-4) of the same 6x6 block: the ground that actually touches
; the water, marked $81 by markRiverBanks.
riverZoneInnerOffsets:
.byte $29,$2A,$2B,$2C,$51,$52,$53,$54; 78A8 )*+,QRST rows 1 and 2, columns 1-4
.byte $79 ; 78B0 rows 3 and 4, columns 1-4
.byte $7A,$7B,$7C,$A1,$A2,$A3,$A4; 78B1 z{|....
; Which of the four 400-byte unit start-position templates at $F000 (+0, +400, +800, +1200) each game
; type uses, indexed by gameTypeOptions & 7: SCRIMAGE, QB SNEAK and THE BOMB share template 2,
; FACE-OFF uses 1, SLUGGERS 3, FULL WAR and DEFENDER 0.
unitLayoutByGameType:
.byte $02 ; 78B8 . game type 0 SCRIMAGE -> template 2
L_78B9:
.byte $02 ; 78B9 types 1 QB SNEAK, 2 THE BOMB -> 2; 3 FACE-OFF -> 1; 4 SLUGGERS -> 3; 5 FULL WAR, 6 DEFENDER -> 0
.byte $02,$01,$03,$00,$00 ; 78BA .....
; Translates the COMCEN SPEED option (0-3, as edited by editComcenSpeed) into the engine's internal
; speed class. Applied to both entries of comcenSpeedClass $92B4/$92B5 at $7B6E.
comcenSpeedClassMap:
.byte $04,$00,$01,$05 ; 78BF .... setting 0 -> class 4, 1 -> 0, 2 -> 1, 3 -> 5
; Game length in rounds for each game type (gameTypeOptions & 7), loaded into gameClock $91CB by
; setGameClockForType ($7D8B). A quarter of this value is the last-quarter warning round $92EF.
roundsByGameType:
.byte $3F,$3F,$1F,$7F ; 78C3 ??.. SCRIMAGE 63, QB SNEAK 63, THE BOMB 31, FACE-OFF 127
L_78C7:
.byte $7F ; 78C7 SLUGGERS 127, FULL WAR 254, DEFENDER 254
.byte $FE,$FE ; 78C8 ..
; Default gameTypeOptions byte for each of the seven items of the game-type menu, read at $828F. Bits
; 0-2 are the game type itself; bit 3 (UNIT MENUS) and bit 4 (DAMAGE) are preset for the four long
; scenarios.
gameTypeOptionsByMenuItem:
.byte $00 ; 78CA . item 0 SCRIMAGE -> $00
sub_78CB:
.byte $01 ; 78CB QB SNEAK $01, THE BOMB $02, FACE-OFF $13, SLUGGERS $14, FULL WAR $1D, DEFENDER $1E
.byte $02,$13,$14,$1D,$1E ; 78CC .....
; ----------------------------------------------------------------------
; startGameSession - The whole 'start a game' sequence, from a cleared battle state to the first frame
; of play. It resets the per-battle variables, then either branches off to the game-film replay path
; or negotiates the live game: agree which player is side 0 and which is side 1, let side 0 pick the
; game type and the map and send the map seed as command $8B (or $A6 for a custom map), exchange
; player names and check the opponent's build, load the unit start template, record the whole setup in
; the film header at $D000, generate and (for side 1) rotate the battlefield, run the unit deployment
; phase, and finally set the clock, place the units and the recyclers and jump into the battle.
; In: the settings block $0B95-$0BB4 (map seed, film mode, link state, side, game type, trainer
; flag), the comm module behind the $E000 jump table, the unit start templates at $F000
; Out: the complete battle state - map, units, clock, film header and message slots; playerSide
; $0B9F resolved to 0 or 1 and the side-dependent branch opcodes patched. Does not return:
; ends in JMP finaliseRecyclerSetup ($EE26) or JMP startFilmPlayback ($7CD1)
; Called from: startGameFromSetup ($0B40)
; Reset everything the previous battle may have left behind. $FF is the 'none/unset' marker for the
; redraw requests, the clock, the last ordered group and both recycler positions; unitCol[0] =
; $FF additionally tells loadUnitStartTemplate ($ECF3) that the unit arrays still have to be filled
; from the template on disk.
; ----------------------------------------------------------------------
startGameSession:
jsr setFilmLimitToBufferEnd ; 78D1 film recording may use the whole buffer up to $DFF0
lda #$FF ; 78D4 $FF = 'none' for everything that follows
sta gamePhase ; 78D6 gamePhase != 0: not in the battle loop yet
sta pendingRedrawGroup ; 78D8 no group redraw pending
sta pendingRedrawUnit ; 78DB no unit redraw pending
sta gameClock ; 78DE game clock not started
sta lastOrderedGroupKey ; 78E1 no group has been given an order yet
sta unitColTable ; 78E4 unitCol[0] = $FF marks the unit arrays as 'still the disk template'
clearRecyclerPositions:
sta recyclerCol ; 78E7 side 0 has no recycler placed yet
sta side1RecyclerCol ; 78EA nor side 1
lda #$32 ; 78ED 50 = a comcen at full energy
sta comcenStatusSnapshot ; 78EF comcen status snapshot starts at full
sta lastQuarterRound ; 78F2 last-quarter warning round; overwritten with rounds/4 at $7BA3
lda #$02 ; 78F5 colour 2 = red
sta enemyUnitColour ; 78F7 the opponent's units are drawn red
lda #$09 ; 78F9 colour 9 = brown
sta droneRadarEnemyColour ; 78FB enemy blips on the drone radar
lda #$06 ; 78FE colour 6 = blue
setOwnUnitColour:
sta ownUnitColour ; 7900 the local player's units are drawn blue
lda #$0B ; 7902 colour 11 = dark grey
sta droneRadarOwnColour ; 7904 own blips on the drone radar
lda #$55 ; 7907 %01010101 = all four pixels of an overview blip in multicolour pattern 1
sta overviewUnitPattern ; 7909 bit pattern used for a unit in the 20x20 overview map
lda #$00 ; 790C 0 for the three variables below
sta gameEndReason ; 790E no end-of-game reason yet
sta D_57C3 ; 7911 reset the base of the film's delta timestamps (operand of the SBC in writeFilmTimestamp)
sta D_4DF4 ; 7914 and the matching base in the film replay decoder (operand of the ADC at $4DF3)
lda #$03 ; 7917 screen id 3
sta currentScreen ; 7919 screen 3 while the game starts up
checkFilmPlayback:
lda filmPlaybackMode ; 791C WATCH GAME FILM?
bpl startLiveGame ; 791F bit 7 clear - not a replay, start a live game
jmp startFilmPlayback ; 7921 yes - the replay path sets everything up from the film header instead
; ----------------------------------------------------------------------
; Live game. First decide which side this machine plays. playerSide $0B9F is used both as a
; *request* (0 = 'I want side 0', $FF = 'I want side 1', anything else = 'no preference') and,
; after cmdChooseSide ($5511) has compared the two requests, as the answer (0 or 1).
; ----------------------------------------------------------------------
startLiveGame:
lda isSoloTrainer ; 7924 solo trainer flag (bit 7)
sta revealAllUnits ; 7927 in a trainer game the opponent's units are always visible
lda #$04 ; 792A the engine steps once every four frames
sta stepIntervalFrames ; 792C the engine advances the battle every 4th frame
lda playerSide ; 792F side left over from the previous game, or $80|random after a tie
bpl alternateSideFromLastGame; 7932 a real side (0 or 1) - alternate from it
requestSideFromLink:
lda #$01 ; 7934 1 = 'no preference': let the tie-break value below decide
sta playerSide ; 7936 store the 'no preference' marker
bne openLinkForSession ; 7939 always taken (A = 1)
alternateSideFromLastGame:
inc playsFirstFlag ; 793B count games played on the same setup
ldy playsFirstFlag ; 793E read the counter back
cpy #$02 ; 7941 has the same setup now been played twice?
bcc encodeSidePreference ; 7943 not yet - keep the same side as last time
ldy #$00 ; 7945 wrap the counter to 0
sty playsFirstFlag ; 7947 restart the count
eor #$01 ; 794A ...and swap sides
encodeSidePreference:
cmp #$00 ; 794C is the requested side 0?
beq storeSidePreference ; 794E yes - ask for side 0
lda #$FF ; 7950 side 1 is requested as $FF, the highest value, so cmdChooseSide awards it side 1
storeSidePreference:
sta playerSide ; 7952 this is the side we will ask for
; ----------------------------------------------------------------------
; Bring the link up. A session that is already running only needs the 'ready for a new game'
; handshake; otherwise the modem is opened and we wait for carrier. On the solo-trainer build both
; calls return at once.
; ----------------------------------------------------------------------
openLinkForSession:
lda linkSessionState ; 7955 link session state ($FF = a session is already established)
bpl openLinkFresh ; 7958 no session yet - dial/answer
jsr readyForNewGameHandshake; 795A exchange the [$8C,02] READY packets with the opponent
lda isLinkActive ; 795D is the comm module still live? (also trainerAiE000:isLinkActive)
bne resolveSidePreference ; 7960 yes - skip the connection wait
openLinkFresh:
jsr openCommLink ; 7962 'PICK UP PHONE THEN PRESS SPACE.' and open the link
jsr waitForConnection ; 7965 'WAITING FOR CONNECTION...' until carrier; leaves its countdown in zp_18
resolveSidePreference:
lda playerSide ; 7968 did we ask for side 0?
beq sendSideChoice ; 796B yes - send that
cmp #$FF ; 796D or for side 1?
beq sendSideChoice ; 796F yes - send that
lda scratch18 ; 7971 no preference: use the carrier-wait countdown as a random tie-break value
sta playerSide ; 7973 that is what we will ask for
sendSideChoice:
jsr sendChooseSide ; 7976 queue command $9D with our request (also copied to $90F8 for the comparison)
D_7979:
jsr sendOutgoingPacket ; 7979 push the packet out now
; ----------------------------------------------------------------------
; Wait until cmdChooseSide ($5511) has seen the opponent's request and written the agreed side; that
; handler sets gamePhase to $DF, clearing bit 5, which is the flag this loop watches.
; ----------------------------------------------------------------------
waitForSideAgreement:
jsr waitForExchangeIdle ; 797C run frames until the packet exchange is idle
lda gamePhase ; 797F phase flags
and #$20 ; 7981 bit 5 = 'side selection still running'
bne waitForSideAgreement ; 7983 still not agreed - keep exchanging
lda #$FF ; 7985 $FF = setup phase, no drawing yet
markSetupPhase:
sta gamePhase ; 7987 gamePhase = $FF: setup, not battle
lda #$01 ; 7989 1 = a game session is now running on the link
sta linkSessionState ; 798B a game session is now established on the link
jsr patchOwnSideBranches ; 798E patch the eleven self-modified 'is this unit mine?' branches for our side
; ----------------------------------------------------------------------
; Side 0 chooses the game type and the map and tells side 1 about it; side 1 just shows 'OPPONENT
; PICKING GAME/MAP.' and waits for the $8B/$A6 command.
; ----------------------------------------------------------------------
chooseOrAwaitGameAndMap:
lda playerSide ; 7991 are we side 0, the player who picks?
beq pickGameTypeAndMap ; 7994 side 0 - we are the player who picks the game and the map
lda #$1A ; 7996 message $1A = 'OPPONENT PICKING GAME/MAP.'
sta persistentMessageId ; 7998 keep it on screen until something replaces it
jsr queueMessage ; 799B queue it for display
jmp afterMapSeedExchange ; 799E and wait for the map seed to arrive
pickGameTypeAndMap:
jsr beginGameTypeSelection ; 79A1 run the GAME TYPE / MAP TYPE / TYPE MAP NAME setup screens
jsr showWorkingMessage ; 79A4 'WORKING...'
lda mapSeed ; 79A7 map seed byte 0...
sta cmdParam0 ; 79AA ...as command argument 0
lda mapSeed1 ; 79AC seed byte 1
sta cmdParam1 ; 79AF argument 1
lda mapSeed2 ; 79B1 seed byte 2
sta cmdParam2 ; 79B4 argument 2
lda #$8B ; 79B6 command $8B = set map seed
ldy mapFlipFlag ; 79B8 is the map flipped?
beq queueMapSeedCommand ; 79BB map not flipped - keep plain command $8B
lda #$A6 ; 79BD command $A6 = set map seed and mark it a custom/flipped map
queueMapSeedCommand:
jsr queueCmd4 ; 79BF queue the command with its three argument bytes
afterMapSeedExchange:
jsr waitForMapSeedCommand ; 79C2 both sides wait here until gamePhase bit 7 is cleared by cmdSetMapSeed
lda #$FF ; 79C5 $FF = 'no input allowed'
sta inputLockoutTimer ; 79C7 block the joystick/keyboard for the rest of the setup
clearPendingScreenKey:
sta requestedScreenKey ; 79CA and forget any pending F1-F7 screen request
lda #$DB ; 79CC $DB is this build's version stamp
sta selectedUnit ; 79CE exchangePlayerNamesAndSettings overwrites $90F8 with the EOR of the peer's version bytes
jsr exchangePlayerNamesAndSettings; 79D1 swap the ten-character player names and the settings block over the link (also trainerAiE000:L_ED43)
lda #$7F ; 79D4 $7F
sta gamePhase ; 79D6 gamePhase $7F: setup phase, drawing enabled
checkOpponentVersion:
lda selectedUnit ; 79D8 did the version handshake come back as our own stamp?
cmp #$DB ; 79DB compare it with our own version stamp
beq loadTemplateAndDefaults ; 79DD yes - the opponent runs the same build
lda #$5E ; 79DF string low byte...
ldy #$89 ; 79E1 ...and high byte: 'OPPONENT USING NON-STANDARD GAME.' at $895E
jsr showModemSetupPrompt ; 79E3 show it plus 'PRESS SPACE WHEN READY.' and wait for the space bar
; ----------------------------------------------------------------------
; Both sides now know the game type and the map, so they can build the same starting position and
; write the same setup into the film header at $D000.
; ----------------------------------------------------------------------
loadTemplateAndDefaults:
jsr loadUnitStartTemplate ; 79E6 copy the unit start-position template for this game type into $F640 (also trainerAiE000:L_ECF3, trainerSetupEC00:copyPlanRecordLoop)
jsr applyGameTypeDefaults ; 79E9 load the per-side scenario parameters (drones, missiles, comcen speed, recycler mode)
jsr sendPhaseSyncAndWait ; 79EC command $9E: wait until both machines have got this far
jsr showWorkingMessage ; 79EF 'WORKING...'
ldy #$2B ; 79F2 film header offset $2B = the map seed
ldx #$00 ; 79F4 start with seed byte 0
recordMapSeedLoop:
lda mapSeed,x ; 79F6 seed byte X
jsr writeFilmByte ; 79F9 write it into the film header
iny ; 79FC next film header offset
inx ; 79FD next seed byte
cpx #$03 ; 79FE all three seed bytes written?
bcc recordMapSeedLoop ; 7A00 keep writing the seed into the film header
jsr generateBattlefieldMap ; 7A02 build the 40x40 battlefield from the seed (this also spells out the map name)
lda playerSide ; 7A05 our side
sta setupPlayerSide ; 7A08 remember the side for the setup screens
; ----------------------------------------------------------------------
; With CUSTOM map type (gameTypeOptions bit 7) the map is not one of the maps printed in the manual,
; so its name is marked with a trailing '!' and hidden from the player who did not choose it - side 1
; sees '#####' instead of the five characters.
; ----------------------------------------------------------------------
lda gameTypeOptions ; 7A0B game options
bpl terminateMapName ; 7A0E bit 7 clear = STANDARD map type, name stays as generated
lda #$21 ; 7A10 '!' replaces the carriage return after the name
sta mapNameTerminator ; 7A12 so the name prints as five characters plus '!'
lda playerSide ; 7A15 our side
beq terminateMapName ; 7A18 side 0 chose the map and may see its name
ldy #$04 ; 7A1A five characters
lda #$23 ; 7A1C '#'
hideMapNameLoop:
sta mapNameText,y ; 7A1E blank out the name for the player who did not pick it
dey ; 7A21 all five characters
bpl hideMapNameLoop ; 7A22 loop over all five characters
terminateMapName:
lda mapNameTerminator ; 7A24 the byte after the name
ora #$80 ; 7A27 set bit 7 so the text engine stops there
sta mapNameTerminator ; 7A29 write the terminator back
; ----------------------------------------------------------------------
; Alternation bookkeeping: playsFirstFlag $0BA1 makes the two players swap sides every second game,
; but only while the setup stays the same. Changing the game type, the map seed or the flip resets it
; to 1. The previous map's name is cached at $0BAF so a repeated map keeps the name the player typed
; rather than the one derived from the seed.
; ----------------------------------------------------------------------
lda gameTypeOptions ; 7A2C game options
cmp prevGameTypeOptions ; 7A2F same options as the last game?
storePrevGameTypeOptions:
sta prevGameTypeOptions ; 7A32 remember them either way
beq compareWithPreviousMap ; 7A35 unchanged - keep alternating
lda #$01 ; 7A37 1 = 'first game with this setup'
sta playsFirstFlag ; 7A39 options changed: restart the side alternation
compareWithPreviousMap:
lda mapSeed ; 7A3C seed byte 0...
cmp prevMapSeed ; 7A3F ...same as last game?
bne rememberNewMap ; 7A42 no - a new map
lda mapSeed1 ; 7A44 seed byte 1
cmp prevMapSeed1 ; 7A47 seed byte 1 - same as last game?
bne rememberNewMap ; 7A4A different - a new map
lda mapSeed2 ; 7A4C seed byte 2
cmp prevMapSeed2 ; 7A4F seed byte 2 - same as last game?
bne rememberNewMap ; 7A52 different - a new map
ldy #$05 ; 7A54 six bytes: the five name characters plus the terminator
restoreMapNameLoop:
lda mapNameBackup,y ; 7A56 same map as last time: restore the name it was given then
sta mapNameText,y ; 7A59 into the live name buffer
dey ; 7A5C all six bytes
bpl restoreMapNameLoop ; 7A5D loop over all six bytes
bmi checkFlipChanged ; 7A5F always taken
rememberNewMap:
lda mapSeed ; 7A61 seed byte 0
sta prevMapSeed ; 7A64 remember the new seed...
lda mapSeed1 ; 7A67 seed byte 1
sta prevMapSeed1 ; 7A6A store it
lda mapSeed2 ; 7A6D seed byte 2
sta prevMapSeed2 ; 7A70 ...all three bytes
lda #$01 ; 7A73 1 = 'first game with this setup'
sta playsFirstFlag ; 7A75 new map: restart the side alternation
saveMapNameStart:
ldy #$05 ; 7A78 six bytes: name plus terminator
saveMapNameLoop:
lda mapNameText,y ; 7A7A cache the name so a rematch on the same map shows it again
sta mapNameBackup,y ; 7A7D into the backup buffer $0BAF
dey ; 7A80 all six bytes
bpl saveMapNameLoop ; 7A81 loop over all six bytes
checkFlipChanged:
lda mapFlipFlag ; 7A83 map flip
cmp prevMapFlipFlag ; 7A86 was the map flipped the same way last game?
beq recordFilmHeader ; 7A89 unchanged - keep alternating sides
sta prevMapFlipFlag ; 7A8B remember the new flip
lda #$01 ; 7A8E 1 = 'first game with this setup'
sta playsFirstFlag ; 7A90 flip changed: restart the side alternation
; ----------------------------------------------------------------------
; Write the setup into the film header at $D000, so a recorded game can be replayed exactly: +4
; trainer flag, +5 side, +6..+$B map name, +$C game options, +$11..$24 both player names, +$2B..$2D
; map seed, +$2E play order and flip. startFilmPlayback ($7CD1) reads them back.
; ----------------------------------------------------------------------
recordFilmHeader:
lda playsFirstFlag ; 7A93 play-order counter
sta savedPlaysFirstFlag ; 7A96 keep the play order for the end-of-game screen
lda playsFirstFlag ; 7A99 play-order counter again
asl a ; 7A9C play order in bits 1-2...
ora mapFlipFlag ; 7A9D ...and the map flip in bit 0
ldy #$2E ; 7AA0 film header offset $2E
jsr writeFilmByte ; 7AA2 record it
ldy #$04 ; 7AA5 film header offset 4
lda isSoloTrainer ; 7AA7 was this a solo-trainer game?
jsr writeFilmByte ; 7AAA record it
iny ; 7AAD offset 5
lda playerSide ; 7AAE which side the recording player was on
jsr writeFilmByte ; 7AB1 record it
iny ; 7AB4 offset 6
ldx #$00 ; 7AB5 start with the first character
recordMapNameLoop:
lda mapNameText,x ; 7AB7 map name character X
recordFilmByteAndAdvance:
jsr writeFilmByte ; 7ABA record it
iny ; 7ABD next film header offset
inx ; 7ABE next character
cpx #$06 ; 7ABF five characters plus the terminator
bcc recordMapNameLoop ; 7AC1 loop over the six name bytes
lda gameTypeOptions ; 7AC3 the game options byte
jsr writeFilmByte ; 7AC6 offset $C = the game options byte
ldy #$11 ; 7AC9 offset $11
ldx #$00 ; 7ACB start with the first name byte
recordPlayerNamesLoop:
lda ownPlayerName,x ; 7ACD $FFDE holds both ten-character player names
jsr writeFilmByte ; 7AD0 record it
iny ; 7AD3 next film header offset
inx ; 7AD4 next name byte
cpx #$14 ; 7AD5 20 bytes = both ten-character player names
bcc recordPlayerNamesLoop ; 7AD7 loop over all 20 name bytes
; ----------------------------------------------------------------------
; Each player must see his own end zone at the bottom of the screen, so side 1 plays on a map that is
; rotated 180 degrees and on unit coordinates that are mirrored with it. A flipped map inverts that
; decision, which is what the EOR tests.
; ----------------------------------------------------------------------
lda playerSide ; 7AD9 our side
eor mapFlipFlag ; 7ADC combined with the requested flip
beq checkMirrorUnits ; 7ADF side 0 on an unflipped map: leave the map as generated
jsr rotateMapAndMirrorCodes ; 7AE1 otherwise rotate it 180 degrees and mirror the direction-dependent terrain codes
checkMirrorUnits:
lda playerSide ; 7AE4 our side
beq initUnitsAndRunOptions ; 7AE7 side 0 keeps the template coordinates
jsr mirrorAllUnitCoordinates; 7AE9 side 1 mirrors all six unit coordinate arrays to match the rotated map (also trainerAiE000:smoothThreatRing, trainerSetupEC00:usePlanFour)
initUnitsAndRunOptions:
jsr setViewOriginToHome ; 7AEC view origin to (17,28), i.e. the middle of our own end
jsr initAllUnits ; 7AEF reset all 100 unit records from their start positions and put them on the map
lda #$FF ; 7AF2 $FF = clock not running
sta gameClock ; 7AF4 clock stays unset until the battle starts
jsr runGameStartOptions ; 7AF7 STANDARD/CUSTOM scenario banner and the custom option editors
lda #$00 ; 7AFA 0 for both variables below
sta textColour ; 7AFC back to text colour 0
sta setupToggleFlag ; 7AFE '*' toggle off
jsr clearInfoPanel ; 7B01 clear the info panel
jsr sendPhaseSyncAndWait ; 7B04 command $9E: wait until the opponent has finished his options too
lda #$00 ; 7B07 0
sta comcenBattleStatus ; 7B09 no comcen alert pending
jsr drawOverviewMap ; 7B0C draw the battlefield overview
jsr resetMessageQueue ; 7B0F empty the message queue
; ----------------------------------------------------------------------
; Solo trainer only: the $EC00-$EFFF tail is swapped for the trainer's opening-play chooser (track 29
; sectors 0-3), run once, and then the normal comm tail is loaded back over it.
; ----------------------------------------------------------------------
lda isSoloTrainer ; 7B12 solo trainer?
bpl checkSetupPhaseNeeded ; 7B15 no - a modem game, nothing to load
inc diskLoadDepth ; 7B17 mark that a disk load is in progress (nested loads are counted)
jsr setGameClockForType ; 7B1A the trainer setup wants the game length already set
jsr showWorkingMessage ; 7B1D 'WORKING...'
ldx #$00 ; 7B20 destination low byte...
ldy #$EC ; 7B22 ...and high byte: $EC00
jsr setLoadDest ; 7B24 tell the fast loader where the sectors go
ldy #$1D ; 7B27 track 29
ldx #$00 ; 7B29 first sector 0
lda #$04 ; 7B2B four sectors = $EC00-$EFFF
jsr loadSectors ; 7B2D load the solo-trainer setup overlay
jsr initTrainerAi ; 7B30 let it pick this game's opening play
jsr loadCommTailBuild2 ; 7B33 restore the comm-module tail at $EC00 from track 34 sectors 12-15
dec diskLoadDepth ; 7B36 disk load finished
; ----------------------------------------------------------------------
; Unit deployment phase. SCRIMAGE (type 0) and THE BOMB (type 2) start from the fixed template and
; skip it; every other game type lets both players drag their units around the map first. Note that
; this test looks at the game type directly and not at setupPhaseMode $92C6, which
; applyGameTypeDefaults derived from the same game type.
; ----------------------------------------------------------------------
checkSetupPhaseNeeded:
lda gameTypeOptions ; 7B39 the game options byte
and #$07 ; 7B3C bits 0-2 = the game type
beq waitForPacketExchange ; 7B3E type 0 SCRIMAGE: no deployment phase
cmp #$02 ; 7B40 is the game type 2, THE BOMB?
beq waitForPacketExchange ; 7B42 type 2 THE BOMB: none either
runUnitSetupPhase:
lda #$3F ; 7B44 gamePhase $3F = unit deployment
sta gamePhase ; 7B46 store the deployment phase
jsr shuffleInitialUnitOrder ; 7B48 shuffle the unit processing order (both machines get the same order from the map seed)
jsr setDestroyModeMessage ; 7B4B point the '*' toggle at 'DESTROY OFF' and the 'SETUP UNITS.' prompt
jsr runMapMainLoop ; 7B4E run the battlefield screen until the player presses F1
lda #$00 ; 7B51 0 = all sprites off
sta spriteEnableShadow ; 7B53 turn the cursor sprites off
; ----------------------------------------------------------------------
; Do not start the battle until every order the player queued during deployment has actually been sent
; to the opponent.
; ----------------------------------------------------------------------
drainOwnCommandQueue:
lda pendingOwnCmdCount ; 7B56 commands of ours still waiting to be sent
checkQueueEmpty:
beq syncAfterSetup ; 7B59 queue empty - synchronise and go
jsr updateGameFrame ; 7B5B run one more frame, which services the link
jmp drainOwnCommandQueue ; 7B5E look again
syncAfterSetup:
jsr sendPhaseSyncAndWait ; 7B61 command $9E: both machines have finished deploying
; ----------------------------------------------------------------------
; Final start-up: wait for one complete packet exchange, fix up the derived settings, snapshot the
; starting position into the film and put everything on the map.
; ----------------------------------------------------------------------
waitForPacketExchange:
bit exchangeFlags ; 7B64 comm exchange flag byte $E01D (also trainerAiE000:commInterfaceBlock)
bvc waitForPacketExchange ; 7B67 spin until bit 6 says a full packet exchange has completed
jsr setGameClockForType ; 7B69 gameClock = the number of rounds for this game type
ldx #$01 ; 7B6C index 1 = the opponent, then 0 = us
mapComcenSpeedLoop:
ldy comcenSpeedClass,x ; 7B6E COMCEN SPEED setting 0-3 for that side
lda comcenSpeedClassMap,y ; 7B71 translate it into the engine speed class 4, 0, 1 or 5
sta comcenSpeedClass,x ; 7B74 store it back over the setting
dex ; 7B77 then side 0, our own
bpl mapComcenSpeedLoop ; 7B78 loop over both sides
jsr clearUnitsAndRecyclersFromMap; 7B7A take every unit and both recyclers off the map again
jsr suspendCommForDiskAccess; 7B7D pause the comm module: the snapshot banks RAM in under the I/O area
jsr writeFilmSetupSnapshot ; 7B80 copy the settings and the unit arrays into the film header at $D034-$D367
jsr placeAllUnitsOnMap ; 7B83 put all 100 units back on the map
setBattleClock:
jsr setGameClockForType ; 7B86 gameClock = the number of rounds, also returned in A
sta gameClock ; 7B89 redundant: setGameClockForType has already stored the value in gameClock
jsr initFilmStream ; 7B8C film command stream starts at $D368
; ----------------------------------------------------------------------
; Common tail, also the point the film replay path jumps back into ($7D6D). Both recyclers are now
; drawn (the replay path drew the opponent's just before jumping here), the unit order is shuffled one
; last time with gamePhase temporarily 0 so the battle-mode path of the round-order rebuild runs, and
; the last-quarter warning round is computed.
; ----------------------------------------------------------------------
beginBattle:
ldy playerSide ; 7B8F our own side
jsr drawRecyclerOnMap ; 7B92 draw our recycler on the map
lda #$00 ; 7B95 0
sta gamePhase ; 7B97 gamePhase 0 = battle, so shuffleInitialUnitOrder rebuilds the round order the battle way
jsr shuffleInitialUnitOrder ; 7B99 reshuffle from the map seed, identically on both machines
dec gamePhase ; 7B9C back to $FF until the battle loop clears it
jsr setGameClockForType ; 7B9E A = the game length in rounds
lsr a ; 7BA1 halve...
sub_7BA2:
lsr a ; 7BA2 divide by four
sta lastQuarterRound ; 7BA3 the last quarter of the game starts at this round ('LAST QTR' warning)
ldx #$20 ; 7BA6 message slot $20
lda #$ED ; 7BA8 string low byte...
ldy #$87 ; 7BAA ...and high byte: 'DRONE ALERT!' at $87ED in this overlay
jsr setMessageSlot ; 7BAC install it as message $20
jmp measureRangeToTarget ; 7BAF hand over to the comm-module tail, which finishes the recyclers and starts the battle (also modemDriverE000:finaliseRecyclerSetup)
; ----------------------------------------------------------------------
; shuffleInitialUnitOrder - builds the order in which the 100 units are processed each round. Re-seeds
; the scenario RNG from the shared 24-bit map seed, resets unitBaseOrder ($9164) and unitProcessOrder
; ($90FD) to the identity permutation and then performs 51 random swaps of unitBaseOrder. Because the
; RNG is seeded from the map seed both machines shuffle identically without exchanging a single byte.
; Ends by jumping into the round-order rebuild at L_4657 (the tail of finishRound), whose RTS returns
; to our caller.
; In: mapSeed $0B95-$0B97; scenario RNG state zp_5A-zp_5C
; Out: $9164 shuffled, $90FD reset to 0..99, RNG advanced, zp_18 used as the swap counter;
; continues in L_4657 (recycle dead units, rebuild the group tables, age visibility)
; Called from: $7B48 and $7B99, the two game-start paths of this overlay
; ----------------------------------------------------------------------
shuffleInitialUnitOrder:
jsr loadMapSeedIntoScenarioRng; 7BB2 copy the 24-bit map seed $0B95-$0B97 into the scenario RNG state zp_5A-zp_5C
jsr initUnitOrderLists ; 7BB5 start from the identity permutation 0..99 in both order lists
lda #$32 ; 7BB8 $32 = 50, so the loop below runs 51 times (counter $32..$00)
sta scratch18 ; 7BBA zp_18 = number of swaps still to do
shuffleSwapLoop:
jsr shuffleUnitOrder ; 7BBC one swap: exchange two randomly chosen entries of unitBaseOrder ($47ED)
dec scratch18 ; 7BBF count this swap off
bpl shuffleSwapLoop ; 7BC1 loop until the counter goes negative - 51 swaps in all
jmp L_4657 ; 7BC3 continue in the round-order rebuild at the end of finishRound; its RTS returns to our caller
; ----------------------------------------------------------------------
; patchOwnSideBranches - burns the local player's side into the engine's self-modifying 'is this unit
; mine?' tests. Each of those tests compares a unit index with $32 (50) and is followed by a branch
; whose opcode byte is a variable: BCC ($90) means 'own units are 0-49' (side 0), BCS ($B0) means 'own
; units are 50-99' (side 1). Seven sites get that opcode and four sites, phrased the other way round
; ('skip if NOT mine'), get the inverted one. Finally caches this side's comcen unit index.
; In: playerSide $0B9F, perSideValueTable $0C8A ($31/$63 = unit 49/99)
; Out: branch opcodes at $63E2 $2840 $3F9E $36D5 $379C $4783 $5BF7 and the inverted opcodes at
; $206B $2084 $3B2C $3BD0; comcenUnit $9236
; Called from: $798E (start of a normal game) and $7D20 (start of film playback)
; ----------------------------------------------------------------------
patchOwnSideBranches:
lda #$90 ; 7BC6 $90 = BCC: for side 0 'own unit' means index < 50
ldy playerSide ; 7BC8 which side is the local player on?
beq L_7BCF ; 7BCB side 0 keeps the BCC
lda #$B0 ; 7BCD $B0 = BCS: for side 1 'own unit' means index >= 50
L_7BCF:
sta groupEditorOwnSideBranch; 7BCF runGroupEditor $63E0 'cpy #$32': marks the picked unit with bit 7 when it is not ours
sta ownSideBranchOpcode ; 7BD2 renderMapTile $283E 'cpy #$32': the other side's units are drawn in the enemy colour
sta initialFacingSideBranch ; 7BD5 initialFacingForUnit $3F9C 'cpx #$32': own units start facing up (0), enemies down (1)
sta D_36D5 ; 7BD8 scanVisibleEnemies $36D3 'cpy #$32': only units of the opposite side enter the spotted list
sta leftoverScanSideBranch ; 7BDB $379C is two leftover bytes ($90 $FA) in front of collectAllUnitsInRadiusDead - a dead branch, this store changes nothing
sta zoneRowSideBranch ; 7BDE moveUnitToCell $4781 'cpx #$32': the column is mirrored before scoring an end zone for the other side
sta D_5BF7 ; 7BE1 buildOverviewQuadrant $5BF5: inverts the mini-map pattern for the other side (that test uses the map-cell form, 'cpy #$B2' = $80|50)
eor #$20 ; 7BE4 $90 EOR $20 = $B0 and back - the four sites below test 'skip if NOT mine'
sta D_206B ; 7BE6 runMapMainLoop $2069 'cmp #$32': the info panel only opens for our own units
sta D_2084 ; 7BE9 runMapMainLoop $2082 'cmp #$32': only our own units can be picked with the joystick
sta D_3B2C ; 7BEC applyHitToUnit $3B2A 'cpy #$32': the spy/comcen hit reactions are for our units only
sta D_3BD0 ; 7BEF applyHitToUnit $3BCE 'cpy #$32': decides whether endGame is entered with X=0 (we lost) or X=1
lda comcenUnitIndexBySide,y ; 7BF2 $0C8A: comcen index of side Y - 49 for side 0, 99 for side 1
sta comcenUnit ; 7BF5 cache it in comcenUnit $9236 for the rest of the game
rts ; 7BF8 done
; ----------------------------------------------------------------------
; sendPhaseSyncAndWait - the 'I am ready' handshake that separates the setup phases. Queues the
; 1-byte command $9E and then keeps running frames until both ready bits of gamePhase zp_B1 are clear:
; the $9E handler ($520C) clears the sending side's bit on both machines (bit 0 = us, bit 1 = the
; opponent). While waiting it runs a full game frame during the battle, or only services the link
; when gamePhase says we are in a menu/setup phase.
; In: gamePhase zp_B1 (bits 0/1 = side not ready, bits 6/7 = not in the battle), the link
; Out: zp_B1 low two bits cleared by the command handler, persistentMessageId $92A1 = 0
; Called from: $79EC, $7B04 and $7B61 - the three setup-phase boundaries
; ----------------------------------------------------------------------
sendPhaseSyncAndWait:
lda #$9E ; 7BF9 $9E = 'setup phase ready' (during the battle the same command means 'comcen repaired')
jsr queueCmdBytePending ; 7BFB queue it for the opponent and count it as one of our own pending commands
lda gamePhase ; 7BFE pass the ready/pause bits to the message code in A
L_7C00:
jsr showWaitingForOpponent ; 7C00 on a modem game with bit 0 still set this shows message $0B 'WAITING FOR OPPONENT'
; ----------------------------------------------------------------------
; Wait loop: re-read the ready bits every frame until both sides have executed the $9E.
; ----------------------------------------------------------------------
phaseSyncWaitLoop:
lda gamePhase ; 7C03 current ready/pause bits
and #$C0 ; 7C05 bits 6-7 set = menu or setup phase, the battle is not running
beq L_7C0F ; 7C07 battle phase: run a whole game frame
L_7C09:
jsr waitForExchangeIdle ; 7C09 setup phase: just service the link until the packet exchange is idle
jmp L_7C12 ; 7C0C and re-test
L_7C0F:
jsr updateGameFrame ; 7C0F battle phase: one complete game update (input, movement, drawing, link)
L_7C12:
lda gamePhase ; 7C12 read the ready bits again
and #$03 ; 7C14 bit 0 = our $9E not executed yet, bit 1 = the opponent's not seen yet
bne phaseSyncWaitLoop ; 7C16 keep waiting while either side is still not ready
sta persistentMessageId ; 7C18 A is 0 here: drop the 'WAITING FOR OPPONENT' status message
rts ; 7C1B both sides are ready
; ----------------------------------------------------------------------
; waitForMapSeedCommand - waits until the map seed sent by the other machine has arrived. gamePhase
; zp_B1 bit 7 is set by the caller and cleared by the handler of the map-seed commands
; $8B / $A6 ($51EA), so this loop simply services the link until that command has been executed.
; In: gamePhase zp_B1 bit 7, the link
; Out: loops until zp_B1 bit 7 is clear; persistentMessageId $92A1 = 0
; Called from: $79C2 (the side that waits for its opponent to choose the map)
; ----------------------------------------------------------------------
waitForMapSeedCommand:
jsr waitForExchangeIdle ; 7C1C service the link until the current packet exchange goes idle
lda gamePhase ; 7C1F bit 7 is only cleared by the $8B/$A6 map-seed command handler at $51EA
bmi waitForMapSeedCommand ; 7C21 keep polling while the seed has not arrived
lda #$00 ; 7C23 the seed is in: clear the status message
sta persistentMessageId ; 7C25 persistentMessageId $92A1 = 0
rts ; 7C28 both machines now hold the same map seed
; setupPhaseModeTable - one byte per game-type class (the folded column 0-4 computed by
; applyGameTypeDefaults), copied into setupPhaseMode $92C6: 0 = no setup phase at all (SCRIMAGE, QB
; SNEAK), 1 = both sides deploy their units and terrain (THE BOMB, FACE-OFF, SLUGGERS, FULL WAR), $80
; = only side 0 deploys (DEFENDER).
setupPhaseModeTable:
.byte $00,$00,$01,$01,$80 ; 7C29 ..... classes 0-4: 0, 0, 1, 1, $80
; ----------------------------------------------------------------------
; applyGameTypeDefaults - loads the per-side game parameters of the chosen game type from the two 5x5
; default tables at $892C (side 0) and $8945 (side 1) in the $8800 overlay. The game type
; $0BA3 bits 0-2 is first folded into a table column: 0 SCRIMAGE, 1 QB SNEAK, 2 THE BOMB /
; FACE-OFF / SLUGGERS, 3 FULL WAR, 4 DEFENDER; the same column selects the setup-phase mode. Table
; rows 0-1 (terrain points, recycler mode) are stored by absolute side, rows 2-4 (comcen speed class,
; drones, missiles) by relative index (0 = us, 1 = the opponent). Afterwards the two special rosters
; are applied: QB SNEAK takes units 0-6 out of play, THE BOMB takes out units 0-98, re-creates unit 49
; as a COMCEN and gives each side an extra drone.
; In: gameTypeOptions $0BA3 bits 0-2, playerSide $0B9F, tables $892C/$8945, setupPhaseModeTable
; Out: setupPhaseMode $92C6; terrainPointsBySide $92AC and recyclerMode $92A6 per absolute side;
; comcenSpeedClass $92B4, dronesLeft $92AE, missilesLeft $92B0 per relative index; unit slots
; emptied in $F76C/$FAF0; zp_18, zp_48/zp_49; self-modifies $7C85+1 and $7CBF+1
; Called from: $79E9, once, after the game type has been agreed
; ----------------------------------------------------------------------
applyGameTypeDefaults:
lda gameTypeOptions ; 7C2E the agreed game type and option bits
and #$07 ; 7C31 bits 0-2 = type 0 SCRIMAGE .. 6 DEFENDER
tay ; 7C33 keep the type in Y while it is folded into a table column
sty L_7C85+1 ; 7C34 self-modifying: the raw game type becomes the operand of the LDA #$FF at $7C85
cpy #$02 ; 7C37 types 0 and 1 have a column of their own
bcc L_7C44 ; 7C39 SCRIMAGE and QB SNEAK: column = type
bne L_7C3E ; 7C3B types 3-6 go straight to the common DEY
iny ; 7C3D type 2 (THE BOMB) is bumped to 3 so the DEY puts it on column 2
L_7C3E:
dey ; 7C3E types 2-6: one column less
cpy #$03 ; 7C3F columns 0-2 are final
bcc L_7C44 ; 7C41 THE BOMB and FACE-OFF share column 2
dey ; 7C43 SLUGGERS, FULL WAR and DEFENDER lose one more: columns 2, 3 and 4
L_7C44:
sty readDefaultAndAdvanceRow+1; 7C44 self-modifying: the column index becomes the LDY operand of readDefaultAndAdvanceRow ($7CC0)
lda setupPhaseModeTable,y ; 7C47 setup-phase mode for this class of game
sta setupPhaseMode ; 7C4A $92C6: 0 = no setup phase, 1 = both sides deploy, $80 = only side 0 deploys
ldx playerSide ; 7C4D first pass: X = the local player's absolute side
lda #$00 ; 7C50 relative index 0 = us
sta scratch18 ; 7C52 zp_18 = 0 (us) then 1 (the opponent)
; ----------------------------------------------------------------------
; Two passes over the 5-row default table, ours first. Rows 0-1 are stored under the absolute side
; number (they describe a side of the battlefield), rows 2-4 under the relative index (they describe a
; player: 0 = this machine, 1 = the other one).
; ----------------------------------------------------------------------
sideDefaultsLoop:
jsr selectSideDefaultsTable ; 7C54 point zp_48/zp_49 at the 5x5 default table of the side in X
jsr readDefaultAndAdvanceRow; 7C57 row 0 of the selected column = terrain points
sta terrainPointsBySide,x ; 7C5A $92AC+side: terrain points this side may place in the setup phase
jsr readDefaultAndAdvanceRow; 7C5D row 1 = recycler mode
sta recyclerMode,x ; 7C60 $92A6+side: whether and how that side gets a recycler
ldx scratch18 ; 7C63 rows 2-4 are indexed by the relative side instead
jsr readDefaultAndAdvanceRow; 7C65 row 2 = comcen speed class
sta comcenSpeedClass,x ; 7C68 $92B4+rel (later mapped through comcenSpeedClassMap at $7B6E)
jsr readDefaultAndAdvanceRow; 7C6B row 3 = number of drones
D_7C6E:
sta dronesLeft,x ; 7C6E $92AE+rel; the D_7C6E label only exists because the comcen-screens overlay has code here
jsr readDefaultAndAdvanceRow; 7C71 row 4 = number of missiles
sta missilesLeft,x ; 7C74 $92B0+rel
lda playerSide ; 7C77 second pass: the opponent's absolute side
eor #$01 ; 7C7A side EOR 1
tax ; 7C7C X = opponent side for the next pass
inc scratch18 ; 7C7D relative index 1 = the opponent
lda scratch18 ; 7C7F two passes only
cmp #$02 ; 7C81 0 = us, 1 = them
bcc sideDefaultsLoop ; 7C83 go round again for the opponent
; ----------------------------------------------------------------------
; Roster adjustments for the two game types that are not played with all 50 units a side.
; ----------------------------------------------------------------------
L_7C85:
lda #$FF ; 7C85 operand patched at $7C34: A = the raw game type 0-6
cmp #$01 ; 7C87 type 1 = QB SNEAK
beq removeQbSneakUnits ; 7C89 QB SNEAK is played with a handful of units only
cmp #$02 ; 7C8B type 2 = THE BOMB
bne L_7CAD ; 7C8D every other type keeps the full roster
ldy #$62 ; 7C8F THE BOMB: take units 0-98 out of play ($62 = 98)
jsr removeUnitsUpToY ; 7C91 empty those unit slots
lda #$04 ; 7C94 type 4 = COMCEN
sta side0ComcenType ; 7C96 $F79D = unitTypeTable+49: put side 0's comcen back into play
inc dronesLeft ; 7C99 one extra drone for us ...
inc opponentDronesLeft ; 7C9C ... and one for the opponent: THE BOMB is fought with comcens and drones alone
rts ; 7C9F done
removeQbSneakUnits:
ldy #$06 ; 7CA0 QB SNEAK: take units 0-6 out of play
; ----------------------------------------------------------------------
; removeUnitsUpToY - marks unit slots Y down to 0 as 'not in this game': unitTypeTable ($F76C) = $FF
; (bit 7 = dead / no such unit) and unitTerrainUnderTable ($FAF0) = $FF ('not standing on the map').
; Entered with Y = 6 for QB SNEAK and Y = $62 for THE BOMB. The RTS at $7CAD is also the shared exit
; of applyGameTypeDefaults.
; In: Y = highest unit index to remove
; Out: $F76C[0..Y] = $FAF0[0..Y] = $FF, Y = $FF
; Called from: $7C91 (applyGameTypeDefaults) and by fall-through from $7CA0
; ----------------------------------------------------------------------
removeUnitsUpToY:
lda #$FF ; 7CA2 $FF = empty unit slot
sta unitTypeTable,y ; 7CA4 unitTypeTable[Y] = $FF: bit 7 marks it dead, the whole byte marks it absent
sta unitTerrainUnderTable,y ; 7CA7 unitTerrainUnderTable[Y] = $FF: this unit occupies no map cell
dey ; 7CAA next lower unit
bpl removeUnitsUpToY ; 7CAB down to unit 0
L_7CAD:
rts ; 7CAD shared exit of applyGameTypeDefaults
; ----------------------------------------------------------------------
; selectSideDefaultsTable - points zp_48/zp_49 at the game-type default table of side X: $892C for
; side 0 and $8945 for side 1. Both live in the $8800 modem-strings overlay and hold 5 rows of 5
; bytes; the row is the parameter and the column the game-type class.
; In: X = side 0/1
; Out: zp_48/zp_49 = address of row 0 of that table
; Called from: $7C54 (applyGameTypeDefaults)
; ----------------------------------------------------------------------
selectSideDefaultsTable:
lda #$2C ; 7CAE low byte of $892C = side 0's defaults
ldy #$89 ; 7CB0 high byte (the $8800 strings overlay)
cpx #$00 ; 7CB2 side 0?
beq L_7CBA ; 7CB4 yes: keep $892C
lda #$45 ; 7CB6 low byte of $8945 = side 1's defaults, 25 bytes further on
ldy #$89 ; 7CB8 high byte
L_7CBA:
sta mapCellPtr ; 7CBA zp_48 = pointer to the current table row
sty mapCellPtrHi ; 7CBC zp_49 = high byte
rts ; 7CBE ready for readDefaultAndAdvanceRow
; ----------------------------------------------------------------------
; readDefaultAndAdvanceRow - reads the byte of the current 5-byte table row that belongs to the chosen
; game-type column and then advances zp_48/zp_49 to the next row. The LDY operand at
; $7CC0 is the column index, written by applyGameTypeDefaults at $7C44.
; In: zp_48/zp_49 = current row, column index patched into $7CC0
; Out: A = the table value, zp_48/zp_49 += 5
; Called from: $7C57, $7C5D, $7C65, $7C6B, $7C71 (all inside applyGameTypeDefaults)
; ----------------------------------------------------------------------
readDefaultAndAdvanceRow:
ldy #$FF ; 7CBF operand patched at $7C44: Y = game-type column 0-4
lda (mapCellPtr),y ; 7CC1 the default for this parameter and this game type
pha ; 7CC3 hold it while the pointer moves on
lda mapCellPtr ; 7CC4 advance the row pointer ...
clc ; 7CC6 clear carry before the add
adc #$05 ; 7CC7 ... by 5 bytes = one table row
sta mapCellPtr ; 7CC9 store it back
bcc L_7CCF ; 7CCB no page crossing
inc mapCellPtrHi ; 7CCD carry into the high byte
L_7CCF:
pla ; 7CCF get the value back
rts ; 7CD0 A = the default value
; ----------------------------------------------------------------------
; startFilmPlayback - the game-start path taken when WATCH GAME FILM was chosen ($0B9B bit 7).
; Everything the film needs is in its 52-byte header in the RAM under I/O at $D000, written by
; $7A78-$7AD8 when the game was recorded; this routine reads it back with readFilmByte and
; rebuilds the game from it: link type, side, game type, both player names, map seed, map orientation
; and who plays first. The battlefield itself is not stored - it is regenerated from the seed and
; rotated if necessary. Then the recorded starting state is restored, the units are placed, the
; screen is drawn and the common game-start tail at L_7B8F is entered.
; In: film header at $D000 (zp_BC/zp_BD = $D000, left there by checkFilmInMemory)
; Out: $0BA8=0, $92FC=0, $92FE, $0B9F, $0BA3, $FFDE-$FFF1, $0B95-$0B97, $0B9D, $0BA1, $0BA9-$0BAE,
; $90FB=0, $92FA=$FF, map at $F000 regenerated, all units placed; jumps to L_7B8F
; Called from: $7921 (JMP, from the game-start dispatcher)
; ----------------------------------------------------------------------
startFilmPlayback:
lda #$00 ; 7CD1 no game-over reason yet
sta gameEndReason ; 7CD3 $0BA8 = 0: the game has not ended
sta revealAllUnits ; 7CD6 $92FC = 0: start the replay with the normal one-sided view
ldy #$04 ; 7CD9 header byte 4
; ----------------------------------------------------------------------
; Read the recorded game parameters out of the film header at $D000 (RAM under I/O).
; ----------------------------------------------------------------------
jsr readFilmByte ; 7CDB read one header byte through zp_BC/zp_BD with the ROMs and I/O banked out
sta soloTrainerAtGameStart ; 7CDE $92FE: was the recorded game played against the trainer or over a modem?
iny ; 7CE1 header byte 5
jsr readFilmByte ; 7CE2 read it
sta playerSide ; 7CE5 the side the film was recorded from - the replay is watched through that player's eyes
ldy #$0C ; 7CE8 header byte $0C
jsr readFilmByte ; 7CEA read it
sta gameTypeOptions ; 7CED $0BA3: game type and option bits of the recorded game
ldy #$11 ; 7CF0 header bytes $11-$24 = the two 10-byte player names
ldx #$00 ; 7CF2 X walks the name block at $FFDE
filmNameByteLoop:
jsr readFilmByte ; 7CF4 next name byte
sta ownPlayerName,x ; 7CF7 $FFDE = our name, $FFE8 = the opponent's
iny ; 7CFA next header offset
inx ; 7CFB next destination byte
cpx #$14 ; 7CFC $14 = 20 bytes = two 10-character names
bcc filmNameByteLoop ; 7CFE keep copying
ldy #$2B ; 7D00 header bytes $2B-$2D = the 24-bit map seed
ldx #$00 ; 7D02 X = seed byte 0-2
filmSeedByteLoop:
jsr readFilmByte ; 7D04 next seed byte
sta mapSeed,x ; 7D07 $0B95-$0B97: the number the map name is written from
iny ; 7D0A next header offset
inx ; 7D0B next seed byte
cpx #$03 ; 7D0C three bytes
bcc filmSeedByteLoop ; 7D0E keep copying
jsr readFilmByte ; 7D10 header byte $2E packs two flags
tax ; 7D13 keep the packed byte in X
and #$01 ; 7D14 bit 0 = map orientation
sta mapFlipFlag ; 7D16 $0B9D: the map was shown rotated 180 degrees
txa ; 7D19 the packed byte again
lsr a ; 7D1A shift the orientation bit out
and #$03 ; 7D1B bits 1-2 = who plays first
sta playsFirstFlag ; 7D1D $0BA1
; ----------------------------------------------------------------------
; Rebuild the battlefield: the film stores only the seed, never the 1600 map cells.
; ----------------------------------------------------------------------
jsr patchOwnSideBranches ; 7D20 burn the film's side into the engine's self-modifying side tests
jsr generateBattlefieldMap ; 7D23 regenerate the identical 40x40 map from the seed
lda playerSide ; 7D26 the generator always builds the map for side 0 ...
eor mapFlipFlag ; 7D29 ... so compare the viewing side with the recorded orientation
beq L_7D31 ; 7D2C orientations agree: leave the map alone
jsr rotateMapAndMirrorCodes ; 7D2E turn the battlefield 180 degrees and mirror the direction-dependent terrain codes
L_7D31:
ldy #$06 ; 7D31 header bytes 6-$0B = the 6-byte map-id text
ldx #$00 ; 7D33 X walks $0BA9-$0BAE
filmMapIdByteLoop:
jsr readFilmByte ; 7D35 next character of the map name
sta mapNameText,x ; 7D38 restore it: generateBattlefieldMap has just overwritten $0BA9 with its own copy
iny ; 7D3B next header offset
inx ; 7D3C next character
cpx #$06 ; 7D3D six characters
bcc filmMapIdByteLoop ; 7D3F keep copying
jsr loadFilmStartSnapshot ; 7D41 restore the recorded starting state (unit arrays, scores, clock) from $D034 onwards (also trainerSetupEC00:checkGroupStartsAtOnce)
lda gameTypeOptions ; 7D44 the recorded option bits
and #$40 ; 7D47 bit 6 = CUSTOM rules
beq L_7D4E ; 7D49 standard rules: nothing to announce
jsr showCustomSummaryMessage; 7D4B put the 'CUSTOM: ...' summary on the status line
; ----------------------------------------------------------------------
; Put the restored units on the map and draw the battlefield screen.
; ----------------------------------------------------------------------
L_7D4E:
jsr placeAllUnitsOnMap ; 7D4E write $80|index into the map cell of every living unit
jsr setViewOriginToHome ; 7D51 move the viewport over the player's own end of the field
lda #$00 ; 7D54 screen id 0 ...
sta currentScreen ; 7D56 ... = the battlefield screen
jsr clearScreenAndRedrawGameScreen; 7D59 clear the bitmap and redraw the frames, panels and console
jsr drawOverviewMap ; 7D5C draw the 20x20 overview map in the left panel
lda #$FF ; 7D5F bit 7 set ...
sta lastQuarterFlag ; 7D61 ... lets the status line keep the clock and score up to date ($C50D)
lda playerSide ; 7D64 our side
eor #$01 ; 7D67 EOR 1 = the opponent's side
tay ; 7D69 drawRecyclerOnMap takes the side in Y
jsr drawRecyclerOnMap ; 7D6A put the opponent's recycler glyph back on the map
jmp beginBattle ; 7D6D join the common game-start tail (the film's own recycler is drawn there)
; ----------------------------------------------------------------------
; initUnitOrderLists - fills the two 100-entry unit order lists unitBaseOrder ($9164) and
; unitProcessOrder ($90FD) with the identity permutation, 99 down to 0.
; In: -
; Out: $9164[i] = $90FD[i] = i for i = 0..99, Y = $FF
; Called from: $7BB5 (shuffleInitialUnitOrder), before the base list is shuffled
; ----------------------------------------------------------------------
initUnitOrderLists:
ldy #$63 ; 7D70 $63 = 99, the last unit index
fillOrderListLoop:
tya ; 7D72 entry Y gets the value Y
sta unitBaseOrder,y ; 7D73 $9164: the shuffled base order built by shuffleInitialUnitOrder
sta unitProcessOrder,y ; 7D76 $90FD: the working order the round loop walks
dey ; 7D79 next lower unit
bpl fillOrderListLoop ; 7D7A down to unit 0
rts ; 7D7C both lists are now 0..99
; ----------------------------------------------------------------------
; placeAllUnitsOnMap - runs placeUnitOnMap for every unit 99 down to 0, which resets the unit's
; energy, move points and flags, saves the terrain under it and writes $80|index into its map cell (a
; dead unit becomes an empty slot instead). The callee wants the index in zp_18 as well as in X.
; In: the unit column/row arrays, the map at $F000
; Out: every living unit is on the map, zp_18 = $FF
; Called from: $7B83 (game start) and $7D4E (film playback)
; ----------------------------------------------------------------------
placeAllUnitsOnMap:
lda #$63 ; 7D7D start at unit 99
sta scratch18 ; 7D7F placeUnitOnMap reads the index from zp_18 as well as from X
placeNextUnit:
ldx scratch18 ; 7D81 X = the unit to place
jsr placeUnitOnMap ; 7D83 reset the unit and write $80|index into its map cell, saving the terrain underneath
dec scratch18 ; 7D86 next lower unit
bpl placeNextUnit ; 7D88 down to unit 0
rts ; 7D8A all 100 slots handled
; ----------------------------------------------------------------------
; setGameClockForType - looks the length of the game up in roundsByGameType ($78C3: 63, 63, 31, 127,
; 127, 254, 254 rounds for types 0-6) and starts the game clock with it.
; In: gameTypeOptions $0BA3 bits 0-2
; Out: gameClock $91CB = A = the number of rounds
; Called from: $7B1A, $7B69, $7B86 and $7B9E (which shifts A right twice to get the quarter length)
; ----------------------------------------------------------------------
setGameClockForType:
lda gameTypeOptions ; 7D8B the game type and option bits
and #$07 ; 7D8E bits 0-2 = type 0-6
tay ; 7D90 index the length table
lda roundsByGameType,y ; 7D91 $78C3: 63, 63, 31, 127, 127, 254, 254 rounds
sta gameClock ; 7D94 $91CB counts one down per round
rts ; 7D97 A still holds the length for the caller
; ----------------------------------------------------------------------
; clearUnitsAndRecyclersFromMap - takes every unit 99..0 off the map (removeUnitFromMap puts the
; terrain that was hidden under it back) and then blanks both recycler cells, leaving a map that holds
; terrain only. Falls into clearRecyclerCell for side 1.
; In: the unit arrays, recyclerCol/recyclerRow $92A8/$92AA
; Out: the map at $F000-$F63F contains no units and no recyclers
; Called from: $7B7A, just before the film snapshot is written and the units are re-placed
; ----------------------------------------------------------------------
clearUnitsAndRecyclersFromMap:
ldx #$63 ; 7D98 start at unit 99
removeNextUnit:
jsr removeUnitFromMap ; 7D9A restore the terrain under the unit and mark it as not on the map
dex ; 7D9D next lower unit
bpl removeNextUnit ; 7D9E down to unit 0
ldx #$00 ; 7DA0 side 0 ...
jsr clearRecyclerCell ; 7DA2 ... blank its recycler cell
ldx #$01 ; 7DA5 side 1: fall into the same code
; ----------------------------------------------------------------------
; clearRecyclerCell - blanks side X's recycler cell: if that side has a recycler (recyclerCol bit 7
; clear) the map byte at (recyclerCol, recyclerRow) is set to the empty terrain code $40.
; In: X = side 0/1, recyclerCol $92A8, recyclerRow $92AA
; Out: one map cell = $40, zp_48/zp_49 point at it
; Called from: $7DA2 and by fall-through from $7DA5
; ----------------------------------------------------------------------
clearRecyclerCell:
lda recyclerCol,x ; 7DA7 $92A8+side; bit 7 set means this side has no recycler placed
bmi L_7DB7 ; 7DAA no recycler: nothing to blank
ldy recyclerRow,x ; 7DAC Y = recycler row
tax ; 7DAF X = recycler column
jsr readMapCell ; 7DB0 zp_48/zp_49 = address of that map cell; Y comes back as 0
lda #$40 ; 7DB3 $40 = empty terrain
sta (mapCellPtr),y ; 7DB5 wipe the recycler out of the map
L_7DB7:
rts ; 7DB7 done
; ----------------------------------------------------------------------
; waitForSpaceKey - 'PRESS SPACE WHEN READY' wait used by the modem setup prompts. Clears the
; keyboard and chat state and then loops servicing the link and stirring the game RNG until the space
; bar ($A0) turns up in lastTypedKey; afterwards it empties the message queue and locks the input out
; for half a second. Stirring the RNG on every pass is what makes the map seed depend on how long the
; player took to press space.
; In: lastTypedKey $9248 (written by the IRQ keyboard scan)
; Out: $9248 = $FF, chatState $929C = 0, incomingChatLength $9277 = 0, inputLockoutTimer $0B7D =
; 30 frames, message queue emptied, game RNG zp_57-zp_59 advanced
; Called from: showModemSetupPrompt ($0C9D, JMP), so this RTS returns to that routine's caller
; ----------------------------------------------------------------------
waitForSpaceKey:
lda #$FF ; 7DB8 $FF = no key pending
sta lastTypedKey ; 7DBA throw away whatever was typed before
lda #$00 ; 7DBD zero for the three variables below
sta chatState ; 7DBF leave chat entry mode
sta inputLockoutTimer ; 7DC2 no input lockout while we wait
sta incomingChatLength ; 7DC5 discard a half-received chat line
; ----------------------------------------------------------------------
; Wait loop: keep the link alive and stir the random generator until space is pressed.
; ----------------------------------------------------------------------
waitSpaceLoop:
jsr serviceCommandExchange ; 7DC8 service the lock-step command exchange
jsr nextGameRandom ; 7DCB step the 24-bit game RNG - this is what randomises the map seed
ldy lastTypedKey ; 7DCE the last key the IRQ saw
cpy #$A0 ; 7DD1 $A0 = the space bar
bne waitSpaceLoop ; 7DD3 keep waiting
jsr resetMessageQueue ; 7DD5 space pressed: empty the message queue and refresh the status line
lda #$1E ; 7DD8 $1E = 30 frames
sta inputLockoutTimer ; 7DDA ignore input for half a second so the key is not seen twice
lda #$FF ; 7DDD $FF = no key pending
sta lastTypedKey ; 7DDF consume the space
rts ; 7DE2 returns into showModemSetupPrompt's caller
; ----------------------------------------------------------------------
; handleMainMenuChoice - acts on the item the player picked in the main menu. 0 COMPETE WITH MODEM
; OPPONENT and 1 PRACTICE WITH SOLO TRAINER set the opponent-module flag and start a game; 2 WATCH
; GAME FILM needs a valid film in $D000 and starts the game in playback mode; 3 SAVE GAME FILM (also
; needs a valid film) and 4 LOAD GAME FILM ask for the data disk, load its film directory and run the
; save or load slot menu, then ask for the game disk again.
; In: menuSelectedItem $91D5, isSoloTrainer $0BA5, the film buffer at $D000
; Out: $0BA5, $0B9B, defaultMainMenuItem $92FD = 2, filmDirectoryPage $7FA1 = 0, filmMenuMode
; $7FA2; items 0-2 leave through startGameFromSetup ($0AC8)
; Called from: runMainMenu ($1C78, JMP)
; ----------------------------------------------------------------------
handleMainMenuChoice:
ldy menuSelectedItem ; 7DE3 the item the selector returned
sty L_7E0E+1 ; 7DE6 self-modifying: the item number becomes the operand of the LDA #$FF at $7E0E
cpy #$03 ; 7DE9 items 0-2 start a game
bcc L_7E2B ; 7DEB handle those below
bne L_7DF4 ; 7DED item 4 = LOAD GAME FILM: there is nothing in memory to check
jsr checkFilmInMemory ; 7DEF item 3 = SAVE GAME FILM: refuse unless $D000 holds a valid film
bcs L_7E2A ; 7DF2 no film: 'NOT A GAME FILM!' has been shown, give up
; ----------------------------------------------------------------------
; Save/load film: get the data disk in the drive and read its film directory.
; ----------------------------------------------------------------------
L_7DF4:
jsr promptInsertDataDisk ; 7DF4 show 'INSERT DATA DISK AND PRESS SPACE.'
jsr suspendCommForDiskAccess; 7DF7 pause the modem/trainer module - the fast loader runs with interrupts off
jsr checkDiskAndLoadFilmDirectory; 7DFA identify the disk and read the 30-entry film directory to $0300
php ; 7DFD keep the C flag across the resume
jsr resumeCommAfterDiskAccess; 7DFE restart the comm module and clear the status line
plp ; 7E01 get the result back
bcs L_7E2A ; 7E02 no usable disk in the drive: the message has already been shown
lda #$02 ; 7E04 item 2 = WATCH GAME FILM ...
sta defaultMainMenuItem ; 7E06 ... becomes the pre-selected main-menu item next time round
lda #$00 ; 7E09 start on ...
sta filmDirectoryPage ; 7E0B ... the first directory page
L_7E0E:
lda #$FF ; 7E0E operand patched at $7DE6: A = the menu item (3 or 4)
sec ; 7E10 prepare the subtraction
sbc #$03 ; 7E11 item 3 -> 0, item 4 -> 1
sta filmMenuMode ; 7E13 $7FA2: 0 = save film, 1 = load film
jsr drawFilmDirectoryPage ; 7E16 draw the ten slots of the current page
lda filmMenuMode ; 7E19 which of the two menus?
bne L_7E24 ; 7E1C 1 = load
jsr runSaveFilmMenu ; 7E1E SAVE: pick a slot, type a name and write the film to disk
jmp promptInsertGameDisk ; 7E21 then ask for the game disk again
L_7E24:
jsr runLoadFilmMenu ; 7E24 LOAD: pick a slot and read the film into $D000
jmp promptInsertGameDisk ; 7E27 then ask for the game disk again
L_7E2A:
rts ; 7E2A nothing to do
; ----------------------------------------------------------------------
; Items 0-2: choose the opponent module / playback mode and start a game.
; ----------------------------------------------------------------------
L_7E2B:
cpy #$02 ; 7E2B item 2 = WATCH GAME FILM
bne L_7E42 ; 7E2D items 0 and 1 below
jsr checkFilmInMemory ; 7E2F a film can only be watched if one is loaded
bcs L_7E2A ; 7E32 invalid film: back to the menu
ldx isSoloTrainer ; 7E34 keep the current opponent module - a film can be watched either way
lda #$82 ; 7E37 $0B9B = $82: bit 7 = playing a film back, bits 0-1 = 2 = show both sides
; ----------------------------------------------------------------------
; setModeFlagsAndStartGame - shared tail of handleMainMenuChoice: X becomes isSoloTrainer $0BA5 ($FF =
; solo trainer, 0 = modem opponent) and A becomes filmPlaybackMode $0B9B, then the game is started.
; In: X = new $0BA5, A = new $0B9B
; Out: $0BA5, $0B9B; jumps to startGameFromSetup ($0AC8)
; Called from: $7E47 and $7E4D (branches), and by fall-through from $7E37
; ----------------------------------------------------------------------
setModeFlagsAndStartGame:
stx isSoloTrainer ; 7E39 $0BA5: $FF = play the solo trainer, 0 = play a modem opponent
sta filmPlaybackMode ; 7E3C $0B9B: bit 7 = watch a game film
jmp startGameFromSetup ; 7E3F leave the menu and start the game
L_7E42:
lda #$00 ; 7E42 item 0 COMPETE WITH MODEM OPPONENT: no film ...
tax ; 7E44 ... and X = 0 = the modem driver
cpy #$01 ; 7E45 item 1 = PRACTICE WITH SOLO TRAINER?
bne setModeFlagsAndStartGame; 7E47 no: start the modem game
lda #$00 ; 7E49 solo trainer: no film either
ldx #$FF ; 7E4B $FF selects the trainer module at $E000
bne setModeFlagsAndStartGame; 7E4D always taken (X is $FF)
; ----------------------------------------------------------------------
; checkDiskAndLoadFilmDirectory - works out which disk is in the drive and loads its film directory.
; The game disk ('OZ') is tried first and silently, because films may also be kept on it; otherwise a
; data disk ('EA') is required, and a data disk must additionally have every block allocated in its
; BAM ($0203-$028F all zero) so that CBM DOS can never overwrite the raw film tracks. diskIdIndex
; $92F0 is left at 0 for the game disk and 2 for a data disk, which later decides whether the
; directory has one page or three. Falls into loadFilmDirectory.
; In: the disk in the drive
; Out: C=0 and the directory at $0300-$04FF, or C=1 with a message shown; $92F0, BAM at $0200
; Called from: $7DFA (handleMainMenuChoice)
; ----------------------------------------------------------------------
checkDiskAndLoadFilmDirectory:
ldx #$00 ; 7E4F X = 0: look for the 'OZ' game disk
lda #$80 ; 7E51 bit 7 = fail silently, this is only a first guess
jsr checkDiskId ; 7E53 read the BAM and compare the disk id
bcc L_7E6D ; 7E56 the game disk can hold films too: use it
ldx #$02 ; 7E58 X = 2: look for the 'EA' data disk
lda #$00 ; 7E5A this time complain if it is not there
jsr checkDiskId ; 7E5C read the BAM and compare the disk id
bcs L_7E70 ; 7E5F neither disk: 'NOT A DATA DISK!' has been shown, return C=1
ldy #$03 ; 7E61 the BAM's per-track allocation entries start at $0203
; ----------------------------------------------------------------------
; A real data disk has been fully allocated by the format utility, so every BAM entry must be zero (no
; free blocks anywhere). Otherwise this is an ordinary CBM disk whose files would be destroyed by
; writing raw tracks.
; ----------------------------------------------------------------------
checkBamAllocatedLoop:
lda contourRuleTable,y ; 7E63 a byte of the BAM allocation map (the $0200 buffer, contourRuleTable's address) (also trainerPlaybook0200:trainerPlay0)
bne L_7E71 ; 7E66 any non-zero byte means free blocks exist: not a data disk
iny ; 7E68 next BAM byte
cpy #$90 ; 7E69 $0203-$028F covers all 35 tracks
bcc checkBamAllocatedLoop ; 7E6B keep checking
L_7E6D:
jmp loadFilmDirectory ; 7E6D the disk is usable: read its film directory
L_7E70:
rts ; 7E70 C=1: no usable disk
L_7E71:
jmp L_100D ; 7E71 'NOT A DATA DISK!' ($0F81) and C=1
; ----------------------------------------------------------------------
; writeFilmDirectory - writes the 512-byte film directory (30 entries of 16 bytes plus slack) from
; $0300-$04FF back to track 18 sectors 2-3. A write error ends in diskErrorExit.
; In: the directory at $0300
; Out: track 18 sectors 2-3 rewritten, C=0
; Called from: $8164 (saveFilmToSlot), before the film itself is written
; ----------------------------------------------------------------------
writeFilmDirectory:
lda #$08 ; 7E74 8 frames ...
jsr waitFrames ; 7E76 ... to let the drive settle after the previous access
ldx #$00 ; 7E79 destination low byte $00 ...
ldy #$03 ; 7E7B ... high byte $03: the buffer at $0300
jsr setLoadDest ; 7E7D tell the fast loader where the data is
lda #$02 ; 7E80 2 sectors ...
tax ; 7E82 ... starting at sector 2 ...
ldy #$12 ; 7E83 ... of track 18, the directory track
jsr writeSectors ; 7E85 the drive encrypts and writes both sectors
bcs filmDirectoryDiskError ; 7E88 write failed
rts ; 7E8A directory saved
; ----------------------------------------------------------------------
; loadFilmDirectory - reads track 18 sectors 2-3 (the 30 x 16-byte film names) into $0300-$04FF. A
; read error ends in diskErrorExit. Tail of checkDiskAndLoadFilmDirectory.
; In: -
; Out: $0300-$04FF = film directory, C=0
; Called from: $7E6D (JMP)
; ----------------------------------------------------------------------
loadFilmDirectory:
ldx #$00 ; 7E8B destination low byte $00 ...
ldy #$03 ; 7E8D ... high byte $03: the buffer at $0300
jsr setLoadDest ; 7E8F tell the fast loader where to put the data
lda #$02 ; 7E92 2 sectors ...
tax ; 7E94 ... from sector 2 ...
ldy #$12 ; 7E95 ... of track 18
jsr loadSectorsDelayed ; 7E97 wait 8 frames, then read both sectors
bcs filmDirectoryDiskError ; 7E9A read failed
rts ; 7E9C directory loaded
filmDirectoryDiskError:
jmp diskErrorExit ; 7E9D show 'DISK ERROR.' and return C=1
; ----------------------------------------------------------------------
; drawFilmDirectoryPage - draws one page of the film menu: the centred title 'SAVE GAME FILM' or 'LOAD
; GAME FILM', then ten entries printed as a right-aligned slot number, a space and the 16-character
; film name, then ' (MORE)' when a three-page data disk is in the drive and finally ' (EXIT)'. Ends
; by setting up the menu selector: item count, window column 11 / width 18 (the name field, so that
; the highlight bar and the redraws leave the slot numbers alone), first row 9 and the highlight EOR
; mask.
; In: filmDirectoryPage $7FA1, filmMenuMode $7FA2, diskIdIndex $92F0, directory at $0300
; Out: the menu drawn; menuItemCount $91D4, menuSelectedItem $91D5 = 0, menuFirstRow $91D6 = 9,
; zp_3B = 11, zp_3C = 18, zp_3D = $60, zp_48/zp_49 = entry pointer, $90F8 and $9220 counters
; Called from: $7E16 and $7FDA (nextFilmDirectoryPage, JMP)
; ----------------------------------------------------------------------
drawFilmDirectoryPage:
jsr clearMenuTextWindow ; 7EA0 clear the menu window (screen column 2, 36 wide, from row 7)
lda #$66 ; 7EA3 'SAVE GAME FILM' at $CC66 ...
ldy #$CC ; 7EA5 ... in the text overlay
ldx filmMenuMode ; 7EA7 0 = save, 1 = load
beq L_7EB0 ; 7EAA save: keep that title
lda #$75 ; 7EAC 'LOAD GAME FILM' at $CC75 ...
ldy #$CC ; 7EAE ... in the text overlay
L_7EB0:
jsr printStringCentred ; 7EB0 centre the title in the 36-column menu window
lda #$09 ; 7EB3 indent the list ...
sta textWindowLeft ; 7EB5 ... to screen column 9
jsr printNewline ; 7EB7 down to the first list row (row 8)
lda #$16 ; 7EBA 22 columns: 'nn ' plus a 16-character name
sta textWindowWidth ; 7EBC also the wrap width used while printing
lda #$01 ; 7EBE dead store: setFilmDirectoryPagePtr overwrites $90F8 two instructions later
sta selectedUnit ; 7EC0 (leftover initialisation of the slot number)
jsr setFilmDirectoryPagePtr ; 7EC3 zp_48/zp_49 = first entry of this page, $90F8 = the slot number it is called
lda #$09 ; 7EC6 9 ...
sta droneFuel ; 7EC8 ... = ten entries per page (the counter runs 9..0); $9220 is only the drone fuel during a battle
; ----------------------------------------------------------------------
; Entry loop: prints 'nn name' for the ten slots of this page, advancing zp_48/zp_49 by one 16-byte
; directory entry each time.
; ----------------------------------------------------------------------
printDirectoryEntryLoop:
lda selectedUnit ; 7ECB the slot number 1-30 printed in front of the name
cmp #$0A ; 7ECE numbers below 10 are one digit ...
bcs L_7ED7 ; 7ED0 ... two digits need no padding
lda #$A0 ; 7ED2 $A0 = space
jsr printChar ; 7ED4 pad one-digit numbers so all names start in the same column
L_7ED7:
lda selectedUnit ; 7ED7 the slot number
ldy #$00 ; 7EDA Y = middle byte of the 24-bit value printDecimal takes (the top byte zp_7E is left at 0)
jsr printDecimal ; 7EDC print it in decimal without leading zeros
lda #$A0 ; 7EDF $A0 = space
jsr printChar ; 7EE1 separate the number from the name
lda mapCellPtr ; 7EE4 pointer to this directory entry ...
ldy mapCellPtrHi ; 7EE6 ... low/high
jsr printString ; 7EE8 print the 16-character name (its last byte carries bit 7)
jsr printNewline ; 7EEB next screen line
lda mapCellPtr ; 7EEE advance the entry pointer ...
clc ; 7EF0 clear carry for the 16-bit add
adc #$10 ; 7EF1 ... by $10 = one 16-byte directory entry
sta mapCellPtr ; 7EF3 store it back
lda mapCellPtrHi ; 7EF5 high byte
adc #$00 ; 7EF7 carry only
sta mapCellPtrHi ; 7EF9 store it back
inc selectedUnit ; 7EFB next slot number
dec droneFuel ; 7EFE one entry done
bpl printDirectoryEntryLoop ; 7F01 ten entries per page
; ----------------------------------------------------------------------
; Trailing menu items and the selector setup.
; ----------------------------------------------------------------------
lda #$0B ; 7F03 11 items ...
sta menuItemCount ; 7F05 ... = ten slots plus (EXIT)
lda diskIdIndex ; 7F08 0 = the game disk, 2 = a data disk
beq L_7F1C ; 7F0B the game disk holds only one page of ten slots
inc menuItemCount ; 7F0D a data disk has three pages, so there is a 12th item: (MORE)
lda #$03 ; 7F10 three blank columns ...
jsr bootReceiveSectorData ; 7F12 ... so that '(MORE)' lines up with the names ($C0EF is printFillChars in the text overlay) (also textEngineC000:printFillChars)
lda #$81 ; 7F15 ' (MORE)' at $EF81 ...
ldy #$EF ; 7F17 ... in the comm-module tail
jsr printString ; 7F19 print it (the string ends with a CR)
L_7F1C:
lda #$03 ; 7F1C three blank columns ...
jsr bootReceiveSectorData ; 7F1E ... for the last item too (also textEngineC000:printFillChars)
lda #$B2 ; 7F21 ' (EXIT)' at $CAB2 ...
ldy #$CA ; 7F23 ... the tail of the formation-menu string block
jsr printString ; 7F25 print it
lda #$0B ; 7F28 the selector window starts at screen column 11 ...
sta textWindowLeft ; 7F2A ... i.e. one column in front of the names
lda #$12 ; 7F2C 18 characters wide ...
sta textWindowWidth ; 7F2E ... = the space plus the 16-character name, and the width of the highlight bar
lda #$09 ; 7F30 the first slot sits on screen row 9 ...
sta menuFirstRow ; 7F32 ... menuFirstRow, to which the item number is added
lda #$00 ; 7F35 start the bar ...
sta menuSelectedItem ; 7F37 ... on the first slot
lda #$60 ; 7F3A $60 = the EOR mask that flips a row between the two text colours
sta textColour ; 7F3C zp_3D is both the plot colour and the highlight mask
rts ; 7F3E the page is ready for the selector
; ----------------------------------------------------------------------
; runLoadFilmMenu - LOAD GAME FILM. Runs the standard menu selector over the directory page; (MORE)
; turns to the next page and starts again, (EXIT) leaves. Picking a slot converts it to a disk track
; and reads that track's 16 sectors one at a time into $0200, copying each page into the film buffer
; $D000-$DFFF that lives under the I/O area, and finally validates the header.
; In: menuSelectedItem $91D5, diskIdIndex $92F0, filmDirectoryPage $7FA1, the disk
; Out: $D000-$DFFF = the film, $91D5 = the track it came from, zp_18/zp_19, the copy pointers
; $58C3/$58C4 (source) and $58C6/$58C7 (destination)
; Called from: $7E24 (handleMainMenuChoice) and $7F54 (itself, after a page turn)
; ----------------------------------------------------------------------
runLoadFilmMenu:
sec ; 7F3F C=1: ordinary menu behaviour (the bar pauses before it reaches the last row)
jsr runMenuLoopWithFlag ; 7F40 run the selector until fire or RETURN
lda menuSelectedItem ; 7F43 the chosen item
cmp #$0A ; 7F46 items 0-9 are film slots
bcc L_7F57 ; 7F48 load that slot
bne L_7F9E ; 7F4A item 11 is always (EXIT)
lda diskIdIndex ; 7F4C item 10 is (MORE) only when a data disk is in the drive
beq L_7F9E ; 7F4F on the game disk item 10 is (EXIT)
jsr nextFilmDirectoryPage ; 7F51 turn to the next page and redraw it
jmp runLoadFilmMenu ; 7F54 and run the selector again
; ----------------------------------------------------------------------
; Load one film: 16 sectors of one track, each read into $0200 and then copied into the film buffer
; under the I/O area at $D000.
; ----------------------------------------------------------------------
L_7F57:
jsr suspendCommForDiskAccess; 7F57 pause the comm module for the disk access
jsr filmSlotToTrack ; 7F5A slot -> track number
sta scratch19 ; 7F5D zp_19 = the track to read
lda #$00 ; 7F5F copyPageUnderIo destination low byte = $00 ...
sta D_58C6 ; 7F61 ... (operand of the STA at $58C5)
lda #$D0 ; 7F64 ... high byte $D0: the film buffer under I/O
sta D_58C7 ; 7F66 the destination page walks $D000..$DF00
lda #$00 ; 7F69 copyPageUnderIo source low byte = $00 ...
sta D_58C3 ; 7F6B ... (operand of the LDA at $58C2)
lda #$02 ; 7F6E ... high byte $02: the sector buffer at $0200
sta D_58C4 ; 7F70 the source stays at $0200 for every sector
lda #$00 ; 7F73 start with ...
sta scratch18 ; 7F75 ... sector 0 in zp_18
loadFilmSectorLoop:
ldx #$00 ; 7F77 destination low byte $00 ...
ldy #$02 ; 7F79 ... high byte $02: read the sector to $0200
jsr setLoadDest ; 7F7B tell the fast loader where to put it
lda #$01 ; 7F7E one sector ...
ldy scratch19 ; 7F80 ... from the film's track ...
ldx scratch18 ; 7F82 ... at the current sector number
jsr loadSectorsDelayed ; 7F84 wait 8 frames, then read it
jsr copyPageUnderIo ; 7F87 copy those 256 bytes into the film buffer with the I/O area banked out
inc D_58C7 ; 7F8A next destination page ($D000, $D100, ...)
inc scratch18 ; 7F8D next sector
lda scratch18 ; 7F8F sector counter
cmp #$10 ; 7F91 16 sectors = the whole 4 KB film buffer
bcc loadFilmSectorLoop ; 7F93 keep reading
jsr resumeCommAfterDiskAccess; 7F95 restart the comm module and clear the status line
jsr checkFilmInMemory ; 7F98 does the loaded data have a valid film header?
bcc L_7F9E ; 7F9B valid
rts ; 7F9D invalid: checkFilmInMemory has already shown 'NOT A GAME FILM!'
L_7F9E:
jmp resetMessageQueue ; 7F9E valid: clear the message queue and return
; filmDirectoryPage - which of the three directory pages is on screen (0-2). A game disk only ever
; shows page 0; a data disk holds 30 films in three pages of ten.
filmDirectoryPage:
.byte $00 ; 7FA1 . current page 0-2
; filmMenuMode - 0 = SAVE GAME FILM, 1 = LOAD GAME FILM (set from the main-menu item - 3).
filmMenuMode:
.byte $00 ; 7FA2 . 0 = save, 1 = load
; filmPageFirstSlotTable - the slot number printed for the first entry of each page: page 0 starts at
; 1, page 1 at 11 and page 2 at 21, so the 30 slots are numbered 1-30 on screen.
filmPageFirstSlotTable:
.byte $01,$0B,$15 ; 7FA3 ... first slot number of pages 0, 1 and 2
; ----------------------------------------------------------------------
; setFilmDirectoryPagePtr - points zp_48/zp_49 at the first directory entry of the current page ($0300
; + page * $A0, ten entries of 16 bytes per page) and puts the slot number that entry is shown as (1,
; 11 or 21) into $90F8 for the numbering loop.
; In: filmDirectoryPage $7FA1, filmPageFirstSlotTable $7FA3
; Out: zp_48/zp_49 = entry pointer, $90F8 = first slot number
; Called from: $7EC3 (drawFilmDirectoryPage) and $7FDD (runSaveFilmMenu)
; ----------------------------------------------------------------------
setFilmDirectoryPagePtr:
lda #$00 ; 7FA6 low byte of $0300 ...
sta mapCellPtr ; 7FA8 ... the directory buffer
lda #$03 ; 7FAA high byte $03 ...
sta mapCellPtrHi ; 7FAC ... zp_48/zp_49 = $0300
ldy filmDirectoryPage ; 7FAE current page 0-2
lda filmPageFirstSlotTable,y; 7FB1 1, 11 or 21
sta selectedUnit ; 7FB4 $90F8 = the slot number of the first entry on this page
tya ; 7FB7 page number into A
beq L_7FCA ; 7FB8 page 0 needs no offset
addDirectoryPageOffsetLoop:
lda mapCellPtr ; 7FBA add one page ...
clc ; 7FBC clear carry for the 16-bit add
adc #$A0 ; 7FBD ... $A0 = 10 entries of 16 bytes
sta mapCellPtr ; 7FBF store it back
lda mapCellPtrHi ; 7FC1 high byte
adc #$00 ; 7FC3 carry only
sta mapCellPtrHi ; 7FC5 store it back
dey ; 7FC7 one page done
bne addDirectoryPageOffsetLoop; 7FC8 repeat for each page skipped
L_7FCA:
rts ; 7FCA zp_48/zp_49 now points at entry 0 of the page
; ----------------------------------------------------------------------
; nextFilmDirectoryPage - steps filmDirectoryPage on modulo 3 and redraws the list.
; In: filmDirectoryPage $7FA1
; Out: $7FA1 advanced (wrapping 2 -> 0); jumps to drawFilmDirectoryPage
; Called from: $7F51 (load menu) and $80CC (save menu), both on the (MORE) item
; ----------------------------------------------------------------------
nextFilmDirectoryPage:
inc filmDirectoryPage ; 7FCB step to the next page
D_7FCE:
lda filmDirectoryPage ; 7FCE read it back (the D_7FCE label only exists because the comcen-screens overlay has a table here)
cmp #$03 ; 7FD1 three pages of ten slots
bcc L_7FDA ; 7FD3 still in range
lda #$00 ; 7FD5 wrap ...
sta filmDirectoryPage ; 7FD7 ... back to page 0
L_7FDA:
jmp drawFilmDirectoryPage ; 7FDA redraw the ten entries of the new page
; ----------------------------------------------------------------------
; runSaveFilmMenu - SAVE GAME FILM. A private copy of the generic menu selector (runMenuLoopWatchUnit
; $5FDC) with a text field built into it: while a slot 0-9 is highlighted every key goes to the line
; editor handleTextEntryKey (at most 16 characters into the buffer
; $9278) and the typed text is echoed on that row with a block cursor behind it. Moving the bar
; up or down throws the typed text away; fire or RETURN accepts. (MORE) turns the page, (EXIT)
; leaves, and a slot with typed text gets the name copied into its 16-byte directory entry (padded
; with spaces, bit 7 on the last byte) before saveFilmToSlot writes directory and film.
; In: menuItemCount $91D4, menuSelectedItem $91D5, menuFirstRow $91D6, filmDirectoryPage $7FA1,
; the keyboard through $90EA, the joystick zp_5D, the frame timers zp_6C and zp_6E
; Out: Y = $91D5 (after a save that is the disk track, not the slot), filmNameLength $92E6 = 0,
; $92C5, the typed name in $9278 and in the directory entry, the film on disk;
; zp_4A/zp_4B = pointer to the selected directory entry; self-modifies $809B+1 and $809F+1
; Called from: $7E1E (handleMainMenuChoice) and $80CF (itself, after a page turn)
; ----------------------------------------------------------------------
runSaveFilmMenu:
jsr setFilmDirectoryPagePtr ; 7FDD zp_48/zp_49 = the first entry of this page
ldx menuItemCount ; 7FE0 number of menu items
dex ; 7FE3 count - 1 ...
dex ; 7FE4 ... - 1 = the row the bar has to slow down in front of
stx L_809B+1 ; 7FE5 self-modifying: that row becomes the operand of the CMP #$FF at $809B
lda #$14 ; 7FE8 $14 = 20 frames ...
sta joyRepeatTimer ; 7FEA ... of input lockout before the stick is read
; ----------------------------------------------------------------------
; Entered again after every move of the bar: clear the typed text, work out the address of the
; selected directory entry and draw the row highlighted.
; ----------------------------------------------------------------------
saveMenuRedrawRow:
lda #$00 ; 7FEC start with ...
sta filmNameLength ; 7FEE ... an empty name ...
sta menuBlinkState ; 7FF1 ... the row highlighted ($92C5 = 0) ...
sta fillEndPtrHi ; 7FF4 ... and the high byte of the entry pointer cleared
L_7FF6:
lda menuSelectedItem ; 7FF6 the selected item
cmp #$0A ; 7FF9 items 0-9 are slots ...
bcs L_8010 ; 7FFB ... (MORE) and (EXIT) have no directory entry
ldy #$04 ; 7FFD shift four times ...
L_7FFF:
asl a ; 7FFF ... to multiply the item number by 16 ...
rol fillEndPtrHi ; 8000 ... into A (low) and zp_4B (high)
dey ; 8002 four shifts
bne L_7FFF ; 8003 keep shifting
clc ; 8005 add the page pointer ...
adc mapCellPtr ; 8006 ... low byte
sta fillEndPtr ; 8008 zp_4A = address of the entry
lda fillEndPtrHi ; 800A high byte ...
adc mapCellPtrHi ; 800C ... plus the carry
sta fillEndPtrHi ; 800E zp_4B = high byte of the entry pointer
L_8010:
lda #$01 ; 8010 colour $01 = the highlighted row (black on white)
jsr printFilmSlotEntry ; 8012 draw the row (name, or (MORE)/(EXIT))
lda #$9E ; 8015 $9E = the line-editor terminator ...
sta typedLineBuffer ; 8017 ... so the typed-name buffer starts out empty
; ----------------------------------------------------------------------
; Input loop: one pass per frame. Keys go into the name field, the highlight blinks every 20 frames
; and the joystick moves the bar or confirms the choice.
; ----------------------------------------------------------------------
saveMenuInputLoop:
jsr serviceCommandExchange ; 801A keep the lock-step command exchange running while the player types
lda menuSelectedItem ; 801D the selected item
cmp #$0A ; 8020 only slots 0-9 have a name to type
bcs L_805E ; 8022 (MORE)/(EXIT): skip the text field
ldy filmNameLength ; 8024 Y = current cursor position in the name buffer
lda #$10 ; 8027 A = 16 = maximum length, bit 7 clear = beep instead of scrolling when full
jsr handleTextEntryKey ; 8029 let the shared line editor handle one key press
beq L_805E ; 802C nothing was accepted this frame
tya ; 802E Y comes back with bit 7 set when RETURN was pressed
bpl L_8034 ; 802F an ordinary character: echo the line
jmp saveMenuAccept ; 8031 RETURN accepts the entry just like the fire button
L_8034:
sty filmNameLength ; 8034 remember the new cursor position
lda #$01 ; 8037 colour $01 = the highlighted row ...
ldy menuBlinkState ; 8039 ... but $92C5 tells which half of the blink we are in
beq L_8040 ; 803C row currently highlighted: keep $01
lda #$61 ; 803E $61 = the un-highlighted colour ($01 EOR the $60 mask)
L_8040:
sta textColour ; 8040 the colour the text engine writes for each cell
lda #$00 ; 8042 back to ...
sta textCursorCol ; 8044 ... column 0 of the name field (screen column 11)
jsr updateCursorPointers ; 8046 recompute the bitmap and colour pointers for that cell
jsr bootToggleDataAck ; 8049 erase the rest of the row (also textEngineC000:clearToEndOfLine)
lda #$A0 ; 804C $A0 = space
jsr printChar ; 804E one blank column in front of the name
lda #$78 ; 8051 the typed-name buffer $9278 ...
ldy #$92 ; 8053 ... low/high
jsr printString ; 8055 print what has been typed so far
ldy #$00 ; 8058 the cell the cursor now sits in
lda #$11 ; 805A $11 = both colour nibbles white = a solid block
L_805C:
sta (screenPtr),y ; 805C draw the block cursor behind the text
L_805E:
lda menuBlinkTimer ; 805E blink timer, counted down by the IRQ
bne L_8069 ; 8060 not time yet
lda #$60 ; 8062 $60 = the EOR mask ...
sta textColour ; 8064 ... used as the toggle colour
jsr toggleMenuRowHighlight ; 8066 flip the row between $01 and $61 and restart the 20-frame timer
L_8069:
lda joyDirection ; 8069 the joystick direction latched by the IRQ (active low)
cmp #$0F ; 806B $0F = centred
bne L_8074 ; 806D the stick is being pushed: do not test fire
jsr checkFirePressed ; 806F edge-detect a new fire-button press
beq saveMenuAccept ; 8072 fire pressed: accept the entry
L_8074:
ldy joyRepeatTimer ; 8074 auto-repeat delay still running?
bne L_8098 ; 8076 yes: ignore the stick this frame
lda joyDirection ; 8078 the joystick nibble again
ora #$0C ; 807A mask the left/right bits out, only up and down move the bar
ldy #$FF ; 807C -1 = move up
cmp #$0E ; 807E $0E = up (bit 0 low)
beq L_8088 ; 8080 move up
ldy #$01 ; 8082 +1 = move down
cmp #$0D ; 8084 $0D = down (bit 1 low)
bne L_8098 ; 8086 neither: nothing to do
L_8088:
tya ; 8088 A = the step, -1 or +1
sty L_809F+1 ; 8089 self-modifying: the direction becomes the operand of the LDY #$FF at $809F
clc ; 808C prepare the add
L_808D:
adc menuSelectedItem ; 808D new selection = old selection + step
jsr setJoystickRepeatDelay ; 8090 start the auto-repeat delay (16 frames first, then 6)
cmp menuItemCount ; 8093 past the last item?
bcc L_809B ; 8096 no: take the new selection
L_8098:
jmp saveMenuInputLoop ; 8098 out of range: leave the bar where it is
L_809B:
cmp #$FF ; 809B operand patched at $7FE5: the row in front of the last one
bne L_80A7 ; 809D an ordinary row: take it
L_809F:
ldy #$FF ; 809F operand patched at $8089: the direction we are moving in
bmi L_80A7 ; 80A1 moving up: no extra delay
ldy #$1E ; 80A3 moving down into the last-but-one row: pause ...
sty joyRepeatTimer ; 80A5 ... 30 frames, so the bar does not run into (MORE)/(EXIT) by accident
L_80A7:
pha ; 80A7 hold the new selection
lda #$61 ; 80A8 $61 = the un-highlighted colour
jsr printFilmSlotEntry ; 80AA redraw the row we are leaving (this also wipes the typed text)
pla ; 80AD the new selection
sta menuSelectedItem ; 80AE make it current
lda #$02 ; 80B1 sound 2 ...
jsr playSound ; 80B3 ... the menu click
jmp saveMenuRedrawRow ; 80B6 start again with an empty name field on the new row
; ----------------------------------------------------------------------
; The choice has been accepted with fire or RETURN: turn the page, leave, or write the film into the
; selected slot.
; ----------------------------------------------------------------------
saveMenuAccept:
lda #$08 ; 80B9 8 frames ...
jsr waitFrames ; 80BB ... so the button press is not seen twice
lda menuSelectedItem ; 80BE the accepted item
cmp #$0A ; 80C1 items 0-9 are film slots
bcc L_80D2 ; 80C3 save into that slot
bne L_8103 ; 80C5 item 11 is always (EXIT)
lda diskIdIndex ; 80C7 0 = game disk: item 10 is (EXIT) too
beq L_8103 ; 80CA leave the menu
jsr nextFilmDirectoryPage ; 80CC (MORE): turn to the next page ...
jmp runSaveFilmMenu ; 80CF ... and run the selector again
L_80D2:
lda filmNameLength ; 80D2 how many characters were typed?
beq L_80FB ; 80D5 nothing typed: keep the old name
and #$7F ; 80D7 mask the RETURN bit off (it cannot be set here - defensive)
sta scratch18 ; 80D9 zp_18 = length of the typed name
ldy #$0F ; 80DB start at the last byte of the 16-byte entry
lda #$A0 ; 80DD $A0 = space
bne L_80E4 ; 80DF always taken - enter the loop at the test
padDirectoryNameLoop:
sta (fillEndPtr),y ; 80E1 pad this position with a space
dey ; 80E3 next lower position
L_80E4:
cpy scratch18 ; 80E4 pad everything from the end down to the typed length
bcs padDirectoryNameLoop ; 80E6 keep padding
tya ; 80E8 Y = length - 1
bmi L_80FB ; 80E9 nothing typed at all
copyTypedNameLoop:
lda typedLineBuffer,y ; 80EB a character of the typed name
sta (fillEndPtr),y ; 80EE into the directory entry
dey ; 80F0 next lower character
bpl copyTypedNameLoop ; 80F1 copy the whole name
ldy #$0F ; 80F3 the last byte of the entry ...
lda (fillEndPtr),y ; 80F5 ... gets ...
ora #$80 ; 80F7 ... bit 7 set: that is the string terminator
sta (fillEndPtr),y ; 80F9 store it back
L_80FB:
lda #$01 ; 80FB colour $01 = highlighted
jsr printFilmSlotEntry ; 80FD redraw the row from the directory entry, so the saved name is shown
jsr saveFilmToSlot ; 8100 write the directory and the 16 sectors of the film to disk
L_8103:
lda #$00 ; 8103 clear ...
sta filmNameLength ; 8105 ... the typed length
ldy menuSelectedItem ; 8108 Y = the selection ($91D5, which filmSlotToTrack has replaced with the track)
rts ; 810B back to the main menu
; ----------------------------------------------------------------------
; printFilmSlotEntry - redraws the currently selected row of the film menu in colour A: moves the
; cursor to menuFirstRow + menuSelectedItem, column 0 of the name window (screen column 11), clears
; the rest of the line and prints a space followed by the entry text - the 16-byte name at zp_4A/zp_4B
; for slots 0-9, ' (MORE)' for item 10 on a data disk, ' (EXIT)' otherwise. The slot number printed by
; drawFilmDirectoryPage sits in columns 9-10 and is left alone.
; In: A = colour byte, menuSelectedItem $91D5, menuFirstRow $91D6, diskIdIndex $92F0, zp_4A/zp_4B
; Out: one menu row redrawn; zp_3D = A, zp_3F = 0
; Called from: $8012, $80AA and $80FD (all in runSaveFilmMenu)
; ----------------------------------------------------------------------
printFilmSlotEntry:
sta textColour ; 810C the colour the text engine writes for each cell ($01 highlighted, $61 normal)
lda menuSelectedItem ; 810E the selected item
clc ; 8111 clear carry for the add
adc menuFirstRow ; 8112 + the first menu row = the screen row of this entry
ldy #$00 ; 8115 column 0 ...
sty textCursorCol ; 8117 ... of the name window (screen column 11)
jsr setCursorRow ; 8119 move the cursor there and recompute the cell pointers
jsr bootToggleDataAck ; 811C erase the old contents of the row (also textEngineC000:clearToEndOfLine)
lda #$A0 ; 811F $A0 = space
jsr printChar ; 8121 one blank column in front of the name
L_8124:
lda #$B2 ; 8124 ' (EXIT)' at $CAB2 ...
ldy #$CA ; 8126 ... the default text for the last item
ldx menuSelectedItem ; 8128 the selected item
cpx #$0A ; 812B items 0-9 are film slots
bcc L_813C ; 812D print the name of the entry
L_812F:
bne L_8140 ; 812F item 11 is always (EXIT)
ldx diskIdIndex ; 8131 0 = game disk
beq L_8140 ; 8134 on the game disk item 10 is (EXIT) as well
lda #$81 ; 8136 ' (MORE)' at $EF81 ...
ldy #$EF ; 8138 ... in the comm-module tail
bne L_8140 ; 813A always taken
L_813C:
lda fillEndPtr ; 813C pointer to the 16-byte directory entry ...
ldy fillEndPtrHi ; 813E ... low/high
L_8140:
jmp printString ; 8140 print it and return through printString
; ----------------------------------------------------------------------
; filmSlotToTrack - turns the selected directory slot into the disk track its film occupies: page * 10
; + item + 2, plus one more when the result would be the directory track 18. Slots 0-29 therefore
; live on tracks 2-17 and 19-32, sixteen sectors each. The track replaces menuSelectedItem $91D5 and
; is also returned in A.
; In: filmDirectoryPage $7FA1, menuSelectedItem $91D5
; Out: A = $91D5 = track number 2-32
; Called from: $7F5A (load) and $8161 (save)
; ----------------------------------------------------------------------
filmSlotToTrack:
lda filmDirectoryPage ; 8143 the page the slot is on
beq L_814D ; 8146 page 0: no offset
ldy #$0A ; 8148 ten slots ...
jsr multiply8x8 ; 814A ... per page; A comes back holding the low byte of page * 10
L_814D:
clc ; 814D add ...
adc menuSelectedItem ; 814E ... the item number 0-9 ...
adc #$02 ; 8151 ... and 2, because tracks 0 and 1 are not used for films
cmp #$12 ; 8153 $12 = 18 = the directory track
bcc L_815A ; 8155 below it: use the track as it is
L_8157:
clc ; 8157 at or above track 18 ...
adc #$01 ; 8158 ... skip the directory track
L_815A:
sta menuSelectedItem ; 815A the selection now holds the track number
rts ; 815D A = the track
; ----------------------------------------------------------------------
; saveFilmToSlot - writes the film in the buffer under I/O to the disk: rewrites the directory (with
; the new name in it) and then copies the 16 pages of $D000-$DFFF one at a time into the
; $0200 sector buffer and writes each as one sector 0-15 of the film's track.
; In: menuSelectedItem $91D5 = slot, the film at $D000-$DFFF, the directory at $0300
; Out: track sectors 0-15 written, $91D5 = the track used, zp_18/zp_19, the copy pointers
; $58C3/$58C4 (source) and $58C6/$58C7 (destination)
; Called from: $8100 (runSaveFilmMenu)
; ----------------------------------------------------------------------
saveFilmToSlot:
jsr suspendCommForDiskAccess; 815E pause the comm module for the disk access
jsr filmSlotToTrack ; 8161 slot -> track number
jsr writeFilmDirectory ; 8164 write the 2 directory sectors first, so the name is on disk even if the film write fails
L_8167:
lda #$00 ; 8167 copyPageUnderIo source low byte = $00 ...
sta D_58C3 ; 8169 ... (operand of the LDA at $58C2)
lda #$D0 ; 816C ... high byte $D0: the film buffer under I/O
sta D_58C4 ; 816E the source page walks $D000..$DF00
lda #$00 ; 8171 copyPageUnderIo destination low byte = $00 ...
sta D_58C6 ; 8173 ... (operand of the STA at $58C5)
lda #$02 ; 8176 ... high byte $02: the sector buffer
sta D_58C7 ; 8178 the destination stays at $0200
lda #$00 ; 817B start with ...
sta scratch18 ; 817D ... sector 0 in zp_18
lda menuSelectedItem ; 817F the track filmSlotToTrack worked out
L_8182:
sta scratch19 ; 8182 zp_19 = the track to write
writeFilmSectorLoop:
jsr copyPageUnderIo ; 8184 copy one page of the film into $0200 with the I/O area banked out
ldx #$00 ; 8187 source low byte $00 ...
ldy #$02 ; 8189 ... high byte $02: write from $0200
jsr setLoadDest ; 818B tell the fast loader where the data is
ldy scratch19 ; 818E the film's track ...
ldx scratch18 ; 8190 ... and the current sector number
lda #$01 ; 8192 one sector at a time
jsr writeSectors ; 8194 the drive encrypts and writes it
inc D_58C4 ; 8197 next source page ($D000, $D100, ...)
inc scratch18 ; 819A next sector
lda scratch18 ; 819C sector counter
cmp #$10 ; 819E 16 sectors = the whole 4 KB film
bcc writeFilmSectorLoop ; 81A0 keep writing
jmp resumeCommAfterDiskAccess; 81A2 restart the comm module and clear the status line
; ----------------------------------------------------------------------
; promptInsertGameDisk - end of the save/load film flow: shows 'INSERT GAME DISK AND PRESS SPACE.',
; then makes sure the 'OZ' disk really is in the drive and restarts the comm module.
; In: -
; Out: the game disk is in the drive, the comm module runs again
; Called from: $7E21 and $7E27 (JMP, after the save and load menus)
; ----------------------------------------------------------------------
promptInsertGameDisk:
lda #$D4 ; 81A5 low byte of 'GAME' at $EFD4 ...
L_81A7:
ldy #$EF ; 81A7 ... in the comm-module tail
jsr promptInsertDiskAndWaitSpace; 81A9 patch it into the message and wait for space
jsr waitForGameDiskInserted ; 81AC keep asking until checkDiskId finds the 'OZ' disk
jmp resumeCommAfterDiskAccess; 81AF restart the comm module and clear the status line
; ----------------------------------------------------------------------
; promptInsertDataDisk - entry of promptInsertDiskAndWaitSpace with the word 'DATA' ($EFD0).
; In: -
; Out: see promptInsertDiskAndWaitSpace
; Called from: $7DF4 (handleMainMenuChoice)
; ----------------------------------------------------------------------
promptInsertDataDisk:
lda #$D0 ; 81B2 low byte of 'DATA' at $EFD0 ...
ldy #$EF ; 81B4 ... in the comm-module tail; fall through
; ----------------------------------------------------------------------
; promptInsertDiskAndWaitSpace - shows 'INSERT xxxx DISK AND PRESS SPACE.' and waits for the space
; bar. The four characters at $EFB6 inside the message template at $EFAF are overwritten with the
; word passed in A/Y ('DATA' or 'GAME'), the template is installed as message slot $1E and shown as
; the persistent status message; the link keeps being serviced while waiting.
; In: A/Y = address of a 4-character disk name ($EFD0 'DATA' or $EFD4 'GAME')
; Out: $EFB6-$EFB9 patched, message slot $1E = $EFAF, $92A1, lastTypedKey $9248 = $FF, the
; message line erased; self-modifies the operand of the LDA at $81C7
; Called from: $81A9 (promptInsertGameDisk) and by fall-through from promptInsertDataDisk
; ----------------------------------------------------------------------
promptInsertDiskAndWaitSpace:
sta patchDiskNameLoop+1 ; 81B6 self-modifying: the word's low byte becomes the operand of the LDA at $81C7 ...
sty patchDiskNameLoop+2 ; 81B9 ... and its high byte the second operand byte
ldx #$1E ; 81BC message slot $1E is the free 'dynamic' slot
lda #$AF ; 81BE 'INSERT DATA DISK AND PRESS SPACE.' at $EFAF ...
ldy #$EF ; 81C0 ... in the comm-module tail
jsr setMessageSlot ; 81C2 install that string as message $1E
ldy #$03 ; 81C5 four characters to patch
patchDiskNameLoop:
lda irqVectorHi,y ; 81C7 operand patched at $81B6/$81B9: reads 'DATA' or 'GAME' (assembled as an LDA $FFFF,y)
sta diskNameSlot,y ; 81CA overwrite the word inside the message template
dey ; 81CD next character
bpl patchDiskNameLoop ; 81CE all four
L_81D0:
lda #$1E ; 81D0 message $1E ...
sta persistentMessageId ; 81D2 ... becomes the persistent status message
jsr queueMessage ; 81D5 put it on the status line
lda #$FF ; 81D8 $FF = no key pending
sta lastTypedKey ; 81DA forget the last key
waitSpaceForDiskLoop:
jsr serviceCommandExchange ; 81DD keep the link alive while the player swaps disks
lda lastTypedKey ; 81E0 the last key the IRQ saw
cmp #$A0 ; 81E3 $A0 = the space bar
bne waitSpaceForDiskLoop ; 81E5 keep waiting
lda #$02 ; 81E7 sound 2 ...
jsr playSound ; 81E9 ... the acknowledging click
lda #$00 ; 81EC no persistent message ...
sta persistentMessageId ; 81EE ... any more
jmp clearMessageLine ; 81F1 erase the message line and return
; ----------------------------------------------------------------------
; checkFilmInMemory - is the buffer under I/O at $D000 a valid game film? verifyFilmHeader recomputes
; the header checksum, compares it with header byte $33 and checks the version byte
; $2F against $0B94. A valid film returns C=0 (and leaves zp_BC/zp_BD = $D000 for the readers
; that follow); otherwise message 9 'NOT A GAME FILM!' is shown for about four seconds and C=1 comes
; back.
; In: the film header at $D000
; Out: C=0 and A = header byte 4 when valid, C=1 otherwise; zp_BC/zp_BD = $D000
; Called from: $7DEF, $7E2F (main menu) and $7F98 (after loading a film)
; ----------------------------------------------------------------------
checkFilmInMemory:
jsr verifyFilmHeader ; 81F4 check the header checksum and version byte
bcc L_820A ; 81F7 C=0: it is a film
lda #$09 ; 81F9 message 9 = 'NOT A GAME FILM!'
jsr queueMessage ; 81FB queue it, then fall into the four-second wait
; ----------------------------------------------------------------------
; waitFramesServicingComm - waits about four seconds (240 IRQ frames counted down in zp_6C) while
; still servicing the command exchange, then returns C=1. In this overlay it is only ever reached by
; falling out of checkFilmInMemory, so that the 'NOT A GAME FILM!' message stays readable before the
; caller carries on. (The main program's call at $563C is to the drone routine that the
; comcen-screens overlay has at this same address, not to this code.)
; In: -
; Out: C=1, joyRepeatTimer zp_6C = 0
; Called from: fall-through from $81FB
; ----------------------------------------------------------------------
waitFramesServicingComm:
lda #$F0 ; 81FE $F0 = 240 frames ...
sta joyRepeatTimer ; 8200 ... about four seconds in one of the IRQ's frame countdowns
waitFramesLinkLoop:
jsr serviceCommandExchange ; 8202 keep the link alive while the message is on screen
lda joyRepeatTimer ; 8205 the IRQ counts the timer down
bne waitFramesLinkLoop ; 8207 wait until it reaches zero
sec ; 8209 C=1 = 'no usable film'
L_820A:
rts ; 820A shared RTS: also the C=0 exit of checkFilmInMemory
; ----------------------------------------------------------------------
; beginGameTypeSelection - one-off prologue of the game-type selection screen, run only by the side
; that picks the scenario (side 0). Throws away any queued status message, forces the map screen,
; tells showToggleStateMessage that the '*' toggle is OFF-with-no-message, installs 'CUSTOM OFF'
; ($EFD8) as message slot $1E and falls into runGameTypeMenu.
; In: nothing (message slot $1E and the patch operand at $868D are the things it sets up)
; Out: D_90FB = 0, $868D = 0 (off-state persistent message id), message slot $1E -> $EFD8
; Called from: startGameSession $79A1 (side 0 only; side 1 waits with message $1A instead)
; ----------------------------------------------------------------------
beginGameTypeSelection:
jsr resetMessageQueue ; 820B drop everything still queued in the status-message ring before the setup screen appears
lda #$00 ; 820E 0 = the normal map/battlefield screen (not a comcen panel)
sta currentScreen ; 8210 currentScreen $90FB = 0
sta L_868C+1 ; 8213 patch the LDX operand at $868D: with the toggle OFF show persistent message id 0 (none)
ldx #$1E ; 8216 X = message slot $1E, the reusable 'toggle state' slot
lda #$D8 ; 8218 low byte of 'CUSTOM OFF' at $EFD8 (in the comm module's $EC00 tail)
ldy #$EF ; 821A high byte of $EFD8
jsr setMessageSlot ; 821C register $EFD8 as message $1E, then fall into runGameTypeMenu
; ----------------------------------------------------------------------
; runGameTypeMenu - the GAME TYPE: selection screen. Aims the ON/OFF patch at the word inside 'CUSTOM
; OFF', derives the '*' toggle ($92F3 bit 7) from gameTypeOptions bit 6, redraws the screen, lists the
; seven scenario names and runs the selector. A new scenario takes its options byte from
; gameTypeOptionsByMenuItem ($78CA); re-choosing the same scenario under CUSTOM rules keeps the byte
; the player already tuned. With CUSTOM rules the two OFF/ON sub-menus 'UNIT MENUS' (bit 3) and
; 'DAMAGE' (bit 4) follow, then it falls into runMapTypeMenu.
; In: D_0BA3 gameTypeOptions, D_0BA5 isSoloTrainer, D_92F3, joystick/menu input
; Out: D_0BA3, D_92F3, D_91D4 = 7, D_91D5, zp_3D/zp_42 = 9, zp_B1 = $BF on the custom path;
; self-modifies $829E (previous game type), $8319 (chosen game type) and $869A/$869B
; Called from: fall-through from beginGameTypeSelection. The JMP $2E07 listed in XREF is the main
; program jumping to overlay variant B's routine at this address, not this code.
; Aim the '*' toggle machinery at the 'CUSTOM OFF' message and seed the toggle from the options byte.
; ----------------------------------------------------------------------
runGameTypeMenu:
lda #$DF ; 821F low byte of $EFDF - the 'OFF' inside 'CUSTOM OFF'
sta L_8699+1 ; 8221 patch the STA operand at $869A: showToggleStateMessage writes ON/OFF there
lda #$EF ; 8224 high byte of $EFDF
sta L_8699+2 ; 8226 patch $869B, the high half of that STA operand
lda gameTypeOptions ; 8229 gameTypeOptions $0BA3
and #$40 ; 822C bit 6 = CUSTOM rules
asl a ; 822E shift bit 6 up into bit 7
sta setupToggleFlag ; 822F setupToggleFlag $92F3 bit 7 = '*' toggle; here it means 'CUSTOM rules on'
jsr clearScreenAndRedrawGameScreen; 8232 grey the bitmap and redraw the whole game screen frame
jsr clearOverviewWindow ; 8235 blank the 20x20 character overview window on the left
lda #$09 ; 8238 text colour code 9 (brown)
sta textColour ; 823A colour byte written for every printed character cell
sta textPadColour ; 823C same colour for the blanks that pad a centred string
jsr resetSelectionAndView ; 823E no unit selected, viewport parked off the map so it stays blank
lda #$0C ; 8241 text cursor row 12
jsr clearInfoPanelSetRow ; 8243 clear the info panel in colour 8 and put the cursor on row 12
lda isSoloTrainer ; 8246 isSoloTrainer $0BA5, bit 7 = PRACTICE WITH SOLO TRAINER
bpl L_824E ; 8249 branch when this is a modem game
jsr toggleBattleDisplayMode ; 824B solo game: flip the beginner/standard battle display and queue the 'F 1 FOR ...' prompt (also trainerSetupEC00:storeHeadingOffset)
L_824E:
lda setupToggleFlag ; 824E the CUSTOM toggle just derived from $0BA3 bit 6
beq L_8256 ; 8251 branch when CUSTOM rules are off - nothing to announce
jsr showToggleStateMessage ; 8253 show 'CUSTOM ON' as the persistent status message
; ----------------------------------------------------------------------
; Validate the stored game type (0-6), preselect it and print the seven scenario names.
; ----------------------------------------------------------------------
L_8256:
lda gameTypeOptions ; 8256 gameTypeOptions
and #$07 ; 8259 bits 0-2 = game type
cmp #$07 ; 825B only 0-6 exist (0 SCRIMAGE .. 6 DEFENDER)
bcc L_8264 ; 825D branch if the stored type is valid
lda #$00 ; 825F out of range: fall back to SCRIMAGE
sta gameTypeOptions ; 8261 and reset the whole options byte
L_8264:
sta L_829D+1 ; 8264 patch the CPY operand at $829E with the game type in force before the menu
tax ; 8267 X = preselected menu row = current game type
lda #$BD ; 8268 'GAME TYPE:' title at $CCBD
ldy #$CC ; 826A high byte of $CCBD
jsr printMenuTitle ; 826C print it centred, set the menu column and record the first item row
lda #$07 ; 826F seven scenarios
sta menuItemCount ; 8271 menuItemCount $91D4
sta scratch18 ; 8274 zp_18 also counts the print loop
L_8276:
jsr printStringAtPointer ; 8276 print the next scenario name; printStringAtPointer walks on to the following 8-byte entry
; ----------------------------------------------------------------------
; printGameTypeNamesLoop - not a routine in variant A: this is the body of runGameTypeMenu's loop that
; prints the seven scenario names at $CCC8, one per line, counting down zp_18. The label exists only
; because overlay variant B has a real routine (advanceDroneCamera) at this address.
; In: zp_18 = names left, string pointer zp_A2/zp_A3 already inside the $CCC8 table
; Out: seven lines of text, zp_18 = 0
; Called from: nothing in variant A (fall-through / JMP $8280 inside runGameTypeMenu)
; ----------------------------------------------------------------------
printGameTypeNamesLoop:
dec scratch18 ; 8279 one name printed
beq L_8283 ; 827B after the seventh do not add another newline
jsr printNewline ; 827D next line
jmp L_8276 ; 8280 print the next name
; ----------------------------------------------------------------------
; Run the selector, then turn the chosen row into a gameTypeOptions byte.
; ----------------------------------------------------------------------
L_8283:
jsr runMenuWidth10 ; 8283 run the menu selector with a 10-character highlight bar; A = chosen row
tay ; 8286 Y = chosen game type 0-6
sty L_8318+1 ; 8287 patch the LDY operand at $8319 so runMapTypeMenu knows which scenario was picked
lda #$00 ; 828A 0 - reset the row for the next menu
sta menuSelectedItem ; 828C reset menuSelectedItem for the next menu
lda gameTypeOptionsByMenuItem,y; 828F default options byte for this scenario (gameTypeOptionsByMenuItem $78CA)
bit setupToggleFlag ; 8292 test the CUSTOM toggle in $92F3
bmi L_829D ; 8295 branch when CUSTOM rules are on
sta gameTypeOptions ; 8297 STANDARD rules: take the preset unchanged
jmp L_82F7 ; 829A and skip the two custom sub-menus
L_829D:
cpy #$FF ; 829D compare the chosen row with the game type in force (operand patched at $8264)
bne applyCustomPresetTail ; 829F a different scenario -> use the table preset in A
lda gameTypeOptions ; 82A1 same scenario re-chosen -> keep the options byte the player already tuned (bits 3/4/7)
; ----------------------------------------------------------------------
; applyCustomPresetTail - not a routine in variant A: the continuation inside runGameTypeMenu that ORs
; the CUSTOM bit ($40) into the scenario's options byte, stores it and runs the two OFF/ON sub-menus
; 'UNIT MENUS' (bit 3) and 'DAMAGE' (bit 4). Variant B has a real routine
; (readDroneHeadingFromJoystick) at this address.
; In: A = options byte to install
; Out: D_0BA3 with bit 6 set and bits 3/4 from the sub-menus, zp_B1 = $BF
; Called from: nothing in variant A (fall-through / BNE $829F)
; ----------------------------------------------------------------------
applyCustomPresetTail:
ora #$40 ; 82A4 bit 6 = CUSTOM rules
sta gameTypeOptions ; 82A6 gameTypeOptions $0BA3
lda #$BF ; 82A9 $BF - non-battle phase with bits 7 and 6 set for the setup menus
sta gamePhase ; 82AB gamePhase zp_B1
ldx #$08 ; 82AD X = mask $08 = UNIT MENUS
lda #$9B ; 82AF 'UNIT MENUS' label at $EF9B
ldy #$EF ; 82B1 high byte of $EF9B
jsr editOptionBitOnOff ; 82B3 OFF/ON menu for bit 3
D_82B6:
ldx #$10 ; 82B6 X = mask $10 = DAMAGE
D_82B8:
lda #$A7 ; 82B8 'DAMAGE' label at $EFA7
D_82BA:
ldy #$EF ; 82BA high byte of $EFA7
D_82BC:
jsr editOptionBitOnOff ; 82BC OFF/ON menu for bit 4, then fall into runMapTypeMenu
; ----------------------------------------------------------------------
; runMapTypeMenu - second half of the setup menus: the STANDARD / CUSTOM map choice (gameTypeOptions
; bit 7). Sends the finished options byte to the opponent (command $A4), loads the scenario's unit
; start-position template, and forces playsFirstFlag when the options changed since the last game. A
; CUSTOM map jumps to the 'TYPE MAP NAME' screen; a STANDARD map takes a fresh 24-bit map seed from
; the shared game RNG - with the low 5 bits forced to 1 (a name ending in '=' = point-symmetric map)
; for every scenario except QB SNEAK and DEFENDER, which get an asymmetric seed and only when this
; side has 1ST PLAY (otherwise the previous map is kept so the two sides can swap roles on it).
; In: D_0BA3, D_0BA4 prevGameTypeOptions, D_0BA1 playsFirstFlag, zp_57-zp_59 game RNG, menu input
; Out: D_0BA3 bit 7, D_0BA1, D_0B95-D_0B97 mapSeed, zp_B1 = $FF, zp_73, command $A4 queued
; Called from: fall-through from editOptionBitOnOff / runGameTypeMenu. The JSR $82BF at $3A30
; (resolveAttack) targets overlay variant B's drawShotLineInViewport, not this code.
; ----------------------------------------------------------------------
runMapTypeMenu:
jsr resetSelectionAndView ; 82BF hide the viewport and clear the selection state
lda #$0E ; 82C2 text cursor row 14
jsr clearInfoPanelSetRow ; 82C4 clear the info panel, cursor to row 14
lda #$77 ; 82C7 'MAP TYPE' title at $EF77
ldy #$EF ; 82C9 high byte of $EF77
ldx #$00 ; 82CB preselect row 0 = STANDARD
L_82CD:
bit gameTypeOptions ; 82CD bit 7 of gameTypeOptions = custom (named) map
bpl L_82D3 ; 82D0 branch when a random map is in force
inx ; 82D2 preselect row 1 = CUSTOM
L_82D3:
jsr printMenuTitle ; 82D3 print the title, set the menu column, record the first item row
lda #$02 ; 82D6 two items
sta menuItemCount ; 82D8 menuItemCount $91D4
lda #$11 ; 82DB 'STANDARD' at $CC11
ldy #$CC ; 82DD high byte of $CC11
jsr printString ; 82DF print item 0
jsr printNewline ; 82E2 next line
jsr printStringAtPointer ; 82E5 print item 1 - 'CUSTOM' follows immediately at $CC19
jsr runMenuWidth10 ; 82E8 run the selector, 10-character bar
lda gameTypeOptions ; 82EB current options byte
and #$7F ; 82EE clear bit 7 (map type)
ldy menuSelectedItem ; 82F0 0 = STANDARD, 1 = CUSTOM
beq L_82F7 ; 82F3 branch when STANDARD was chosen
ora #$80 ; 82F5 set bit 7 = custom (named) map
L_82F7:
sta gameTypeOptions ; 82F7 store the finished gameTypeOptions
jsr sendGameTypeCommand ; 82FA tell the opponent the whole options byte (command $A4)
jsr loadUnitStartTemplate ; 82FD copy this scenario's 400-byte unit start template into the live unit arrays (also trainerAiE000:L_ECF3, trainerSetupEC00:copyPlanRecordLoop)
lda #$FF ; 8300 $FF = menu / setup phase
sta gamePhase ; 8302 gamePhase zp_B1
lda gameTypeOptions ; 8304 the new options byte
cmp prevGameTypeOptions ; 8307 compare with the options of the previous game
beq L_8311 ; 830A branch when nothing changed - keep the alternating 1ST PLAY flag
lda #$01 ; 830C 1 = this side has 1ST PLAY
sta playsFirstFlag ; 830E a new scenario always gives this side 1ST PLAY
; ----------------------------------------------------------------------
; Map seed. A CUSTOM map is typed in by the player; a STANDARD map is drawn from the shared 24-bit
; game RNG state zp_57-zp_59, so both machines end up with the same battlefield.
; ----------------------------------------------------------------------
L_8311:
ldy menuSelectedItem ; 8311 map type chosen above
bne L_8339 ; 8314 CUSTOM map -> let the player type the five-character map name
lda gameRngState0 ; 8316 byte 0 of the shared game RNG state
L_8318:
ldy #$FF ; 8318 Y = chosen game type (operand patched at $8287)
cpy #$06 ; 831A 6 = DEFENDER (asymmetric scenario)
beq L_8322 ; 831C DEFENDER - handle it with QB SNEAK
cpy #$01 ; 831E 1 = QB SNEAK (asymmetric scenario)
bne L_8329 ; 8320 any other scenario -> symmetric map
L_8322:
ldx playsFirstFlag ; 8322 asymmetric scenario: does this side have 1ST PLAY?
beq L_8338 ; 8325 no -> leave mapSeed alone, so the same map is replayed with the roles swapped
bne L_832B ; 8327 yes -> store the RNG state unchanged (asymmetric map)
L_8329:
ora #$1F ; 8329 force the low 5 bits, i.e. a map name ending in '=' - the generator then mirrors the map
L_832B:
sta mapSeed ; 832B mapSeed byte 0 $0B95
lda gameRngState1 ; 832E byte 1 of the RNG state
sta mapSeed1 ; 8330 mapSeed byte 1
lda gameRngState2 ; 8333 byte 2 of the RNG state
sta mapSeed2 ; 8335 mapSeed byte 2
L_8338:
rts ; 8338 done - the caller at $79A7 sends the seed to the opponent as command $8B/$A6
L_8339:
jmp mapNameEntryScreen ; 8339 custom map: 'TYPE MAP NAME' screen (which comes back through startGameSession)
; ----------------------------------------------------------------------
; sendGameTypeCommand - queues the 2-byte command $A4 (cmdSetGameType, handler $559C) carrying the
; chosen gameTypeOptions byte, so the opponent's $0BA3 ends up identical.
; In: A = gameTypeOptions byte
; Out: zp_73 = A, command $A4 queued, D_929F incremented
; Called from: runMapTypeMenu $82FA
; ----------------------------------------------------------------------
sendGameTypeCommand:
sta cmdParam0 ; 833C cmdParam0 = the options byte
L_833E:
lda #$A4 ; 833E command $A4 = cmdSetGameType
jmp queueCmd2 ; 8340 queue command + 1 argument
; ----------------------------------------------------------------------
; printMenuTitle - common menu prologue: preselects row X, prints the title string A/Y centred in the
; current window, then moves the text window to column $1C (28) and records the row the first item
; will be printed on as menuFirstRow.
; In: X = row to preselect, A/Y = bit-7 terminated title string
; Out: D_91D5 = X, D_91D6 = first item row, zp_3B = $1C, cursor pointers updated
; Called from: $826C (GAME TYPE:), $82D3 (MAP TYPE), $8588 (' SCENARIO'), $862A (OFF/ON menus)
; ----------------------------------------------------------------------
printMenuTitle:
stx menuSelectedItem ; 8343 menuSelectedItem = the preselected row
jsr printStringCentred ; 8346 print the title centred in the current window width
lda #$1C ; 8349 column 28 - left edge of the menu column
sta textWindowLeft ; 834B textWindowLeft zp_3B
jsr updateCursorPointers ; 834D recompute the bitmap/colour pointers for the new column
lda textCursorRow ; 8350 the row the cursor now sits on
sta menuFirstRow ; 8352 menuFirstRow $91D6 - the selector highlights menuFirstRow + menuSelectedItem
rts ; 8355 menu title and first row are set up
; ----------------------------------------------------------------------
; runMenuWidth10 - runs the generic selector with a 10-character highlight bar; falls into
; runMenuWithWidth.
; In: menu state D_91D4/D_91D5/D_91D6
; Out: A = D_91D5 = chosen row
; Called from: $8283 (game type), $82E8 (map type), $8599 (STANDARD/CUSTOM scenario)
; ----------------------------------------------------------------------
runMenuWidth10:
lda #$0A ; 8356 highlight bar 10 characters wide
; ----------------------------------------------------------------------
; runMenuWithWidth - sets the highlight width, backs the window up one column so the bar has a leading
; blank, and runs the generic menu selector with C = 0 (no extra slow-down row before the last item).
; In: A = highlight width, D_91D4 item count, D_91D5 preselected row, D_91D6 first row
; Out: A = D_91D5 = chosen row, zp_3C = width, zp_3B decremented
; Called from: runMenuWidth10 (fall-through) and editOptionBitOnOff $8641
; ----------------------------------------------------------------------
runMenuWithWidth:
sta textWindowWidth ; 8358 textWindowWidth zp_3C is also the width of the highlight bar
dec textWindowLeft ; 835A start the bar one column left of the text
clc ; 835C C = 0 -> no slow-down row in the selector
jsr runMenuLoopWithFlag ; 835D joystick/fire driven selection loop
lda menuSelectedItem ; 8360 return the chosen row
rts ; 8363 A = the chosen row
; ----------------------------------------------------------------------
; resetSelectionAndView - blanks the tactical viewport for a full-screen menu: sets $9242 bit 7 (the
; overview then draws plain terrain instead of unit blips), clears the selected unit, forces the map
; screen and parks the view origin at (45,45), which is outside the 40x40 map.
; In: nothing
; Out: D_9242 = $FE, D_90F8 = $FE, D_90FB = 0, zp_A5 = zp_A6 = $2D
; Called from: $823E, $82BF, $8398, $8422, $854B, $8614
; ----------------------------------------------------------------------
resetSelectionAndView:
lda #$FE ; 8364 $FE - bit 7 set, low bits 'no unit'
sta comcenBattleStatus ; 8366 $9242 bit 7 makes the overview map draw the terrain under each unit
sta selectedUnit ; 8369 selectedUnit $90F8 = $FE = nothing selected
lda #$00 ; 836C 0 = map screen
sta currentScreen ; 836E currentScreen 0 = map screen
lda #$2D ; 8371 45 - past the last map row/column (0-39)
sta viewOriginCol ; 8373 view origin column
sta viewOriginRow ; 8375 view origin row: the 7x5 viewport now shows nothing
rts ; 8377 viewport is now blank
; ----------------------------------------------------------------------
; chooseRecycler - RECYCLER setup for the scenarios that have one (game types 4-6). Solo games force
; both sides to NONE. With CUSTOM options a FULL/HALF/NONE menu is shown, otherwise the scenario
; default stands and NONE returns at once. For FULL or HALF the player drives the target cursor over
; the map until he picks a cell whose terrain is blank ($40, looked up under a unit if one stands
; there); the result goes out as command $9C (mode, column, row), NONE as $9C with column/row = $FF.
; In: D_0BA5, D_0B9F playerSide, D_92A6 recyclerMode[side], D_8450 customSetupChosen, player input
; Out: D_92A6/D_92A7 (solo), D_91D5, zp_73-zp_75, command $9C queued, D_9022 = 0, zp_BB = $FF;
; self-modifies the LDA operand at $8433 with the chosen mode
; Called from: runGameStartOptions $85B6 (only for game types >= 4)
; ----------------------------------------------------------------------
chooseRecycler:
lda isSoloTrainer ; 8378 isSoloTrainer $0BA5
bpl L_8385 ; 837B modem game -> keep the per-side settings
lda #$02 ; 837D 2 = NONE
sta recyclerMode ; 837F side 0 recycler mode $92A6
sta side1RecyclerMode ; 8382 side 1 recycler mode $92A7 - the solo trainer never uses a recycler
L_8385:
ldx playerSide ; 8385 playerSide 0/1
ldy recyclerMode,x ; 8388 this side's recycler mode
sty menuSelectedItem ; 838B preselect it in the menu
lda customSetupChosen ; 838E customSetupChosen $8450 - nonzero when CUSTOM was picked at game start
bne L_8398 ; 8391 CUSTOM -> show the RECYCLER menu
cpy #$02 ; 8393 2 = NONE
bne L_83D1 ; 8395 a preset FULL or HALF still has to be placed on the map
rts ; 8397 NONE and no menu -> nothing to do at all
; ----------------------------------------------------------------------
; The RECYCLER menu: title plus the three option words FULL / HALF / NONE from $0652.
; ----------------------------------------------------------------------
L_8398:
jsr resetSelectionAndView ; 8398 hide the viewport behind the menu
lda #$0E ; 839B text cursor row 14
jsr clearInfoPanelSetRow ; 839D clear the info panel, cursor to row 14
lda #$92 ; 83A0 'RECYCLER' at $EF92
ldy #$EF ; 83A2 high byte of $EF92
jsr printStringCentred ; 83A4 print it centred
lda #$1E ; 83A7 column 30
sta textWindowLeft ; 83A9 textWindowLeft zp_3B
jsr updateCursorPointers ; 83AB recompute the cursor pointers for the new column
lda textCursorRow ; 83AE row of the first item
sta menuFirstRow ; 83B0 menuFirstRow $91D6
lda #$03 ; 83B3 three items
sta menuItemCount ; 83B5 menuItemCount $91D4
lda #$00 ; 83B8 option word 0 = FULL
jsr printOptionWordLine ; 83BA print it and a newline
lda #$01 ; 83BD option word 1 = HALF
jsr printOptionWordLine ; 83BF print it and a newline
lda #$02 ; 83C2 option word 2 = NONE
jsr printOptionWordLine ; 83C4 print it and a newline
dec textWindowLeft ; 83C7 highlight bar starts one column left of the words
lda #$06 ; 83C9 bar 6 characters wide
sta textWindowWidth ; 83CB textWindowWidth zp_3C
clc ; 83CD C = 0 -> no slow-down row
jsr runMenuLoopWithFlag ; 83CE run the selector; Y comes back holding the chosen row
; ----------------------------------------------------------------------
; Turn the chosen mode into command $9C. NONE needs no map cell, FULL/HALF make the player pick one.
; ----------------------------------------------------------------------
L_83D1:
sty L_8432+1 ; 83D1 patch the LDA operand at $8433 - the mode becomes cmdParam0
cpy #$02 ; 83D4 2 = NONE
bne L_83E0 ; 83D6 FULL or HALF -> place it on the map
lda #$FF ; 83D8 $FF = no cell
sta cmdParam1 ; 83DA cmdParam1 = column
sta cmdParam2 ; 83DC cmdParam2 = row
bmi L_8432 ; 83DE always taken ($FF is negative) - jump straight to the send
L_83E0:
lda #$0F ; 83E0 text cursor row 15
jsr clearInfoPanelSetRow ; 83E2 clear the info panel, cursor to row 15
lda #$89 ; 83E5 'LOCATING' at $EF89
ldy #$EF ; 83E7 high byte of $EF89
L_83E9:
jsr printStringCentred ; 83E9 print 'LOCATING' centred
jsr printNextStringCentred ; 83EC and the string that follows it, 'RECYCLER' at $EF92
jsr setViewOriginToHome ; 83EF put the view origin over this side's home end of the map
jsr positionCursorSprite ; 83F2 show the cursor sprite over the centre cell
jsr drawViewport ; 83F5 draw the 7x5 cell tactical view
jsr drawOverviewMap ; 83F8 draw the whole-map overview
; ----------------------------------------------------------------------
; Pick loop: the recycler may only stand on empty terrain, so a cell holding anything else beeps and
; the cursor comes back.
; ----------------------------------------------------------------------
L_83FB:
lda #$FF ; 83FB $FF = free cell pick (result reported, no command executed)
sta cursorTargetMode ; 83FD cursorTargetMode zp_BB
jsr runTargetCursorLoop ; 83FF drive the cursor until fire; the cell lands in $91D8/$91D9
ldx pickedCellCol ; 8402 chosen column
ldy pickedCellRow ; 8405 chosen row
jsr readMapCell ; 8408 A = the map byte of that cell
bpl L_8411 ; 840B bit 7 clear -> plain terrain, use it directly
tay ; 840D bit 7 set: Y = $80|unitIndex
L_840E:
lda unitTerrainUnderAlias,y ; 840E $FA70 + $80 + unit = unitTerrainUnderTable[unit], the terrain hidden by that unit
L_8411:
cmp #$40 ; 8411 $40 = blank terrain
beq L_841D ; 8413 blank -> accept the cell
lda #$03 ; 8415 sound 3 - cell rejected
jsr playSound ; 8417 play it
L_841A:
jmp L_83FB ; 841A ask for another cell
L_841D:
lda #$00 ; 841D 0 - no sprites
sta spriteEnableShadow ; 841F turn every sprite off again
jsr resetSelectionAndView ; 8422 park the view off the map
jsr clearViewportTextArea ; 8425 clear the text area under the viewport
lda pickedCellCol ; 8428 chosen column
sta cmdParam1 ; 842B cmdParam1
lda pickedCellRow ; 842D chosen row
sta cmdParam2 ; 8430 cmdParam2
L_8432:
lda #$FF ; 8432 operand patched at $83D1 with the recycler mode
sta cmdParam0 ; 8434 cmdParam0 = mode
L_8436:
lda #$9C ; 8436 command $9C = cmdSetRecycler
jmp queueCmd4 ; 8438 queue command + 3 arguments
; ----------------------------------------------------------------------
; printOptionWordLine - prints option word A (0 FULL, 1 HALF, 2 NONE, from the 4-byte entries at
; $0652) followed by a newline.
; In: A = word index 0-2
; Out: one line of text at the cursor
; Called from: chooseRecycler $83BA, $83BF, $83C4
; ----------------------------------------------------------------------
printOptionWordLine:
jsr printOptionWord ; 843B print the 4-character word from $0652 + A*4
jmp printNewline ; 843E and end the line
; ----------------------------------------------------------------------
; clearInfoPanelSetRow - sets the text and pad colours to 8 (orange), blanks the info panel window and
; puts the cursor on row A. Used before every setup panel in this overlay.
; In: A = text cursor row
; Out: zp_3D = zp_42 = 8, panel cleared, zp_40 = A, cursor pointers updated
; Called from: $8243, $82C4, $839D, $83E2, $8572, $85E6, $8619, $8758
; ----------------------------------------------------------------------
clearInfoPanelSetRow:
pha ; 8441 remember the wanted row across the clear
lda #$08 ; 8442 colour code 8 (orange)
sta textColour ; 8444 text colour zp_3D
sta textPadColour ; 8446 pad colour zp_42
jsr clearInfoPanel ; 8448 set the info-panel text window and blank it
pla ; 844B the wanted row again
L_844C:
jmp setCursorRow ; 844C position the cursor on it
; Two one-byte variables of the value editors, sitting between the routines that use them.
editValue:
.byte $00 ; 844F . current value of the joystick value editor (limits and meaning depend on the caller)
; customSetupChosen: byteTable, 1 bytes. nonzero = CUSTOM options chosen at game start
customSetupChosen:
.byte $00 ; 8450 . 0 = STANDARD scenario options, 1 = CUSTOM chosen on the game-start screen ($40 briefly at $8564)
; ----------------------------------------------------------------------
; editValueWithJoystick - the generic numeric option editor of the custom setup. It repeatedly prints
; editValue * Y in decimal at column 1 of the one-row value window, keeps the link and the blinking
; highlight alive, and applies joystick up as +1 and down as -1, rejecting anything that is not below
; the limit (a decrement below 0 wraps to $FF and is rejected the same way). Returns when the fire
; button is clicked.
; In: A = exclusive upper limit, Y = display multiplier, D_844F = starting value, window set up by
; beginValueEdit, zp_5D joystick, zp_6C repeat timer, zp_6E blink timer
; Out: D_844F = edited value, D_92C5 = 1, number printed, sound 2 on every change;
; self-modifies the CMP operand at $8493 (limit) and the LDY operand at $846A (multiplier)
; Called from: applyHandicapPoints $84DB, editTerrainPoints $8500, editDronesAndMissiles $851F/$8536,
; editComcenSpeed $85CA
; ----------------------------------------------------------------------
editValueWithJoystick:
sta L_8492+1 ; 8451 patch the CMP operand at $8493 with the exclusive upper limit
sty L_8469+1 ; 8454 patch the LDY operand at $846A with the display multiplier
lda #$01 ; 8457 1 = un-highlighted
sta menuBlinkState ; 8459 menuBlinkState 1 = the row is currently un-highlighted
; ----------------------------------------------------------------------
; Redraw pass: un-highlight the row, print value * multiplier, then wait in the input loop.
; ----------------------------------------------------------------------
L_845C:
jsr unhighlightMenuRow ; 845C take the highlight off so the number can be redrawn
lda #$01 ; 845F column 1 of the value window (window left is 29, so absolute column 30)
L_8461:
sta textCursorCol ; 8461 textCursorCol zp_3F
jsr updateCursorPointers ; 8463 recompute the cursor pointers
lda editValue ; 8466 current value
L_8469:
ldy #$FF ; 8469 multiplier (operand patched at $8454)
jsr multiply8x8 ; 846B 16-bit multiply: A = product low, Y = product high
jsr printDecimal ; 846E print it as decimal without leading zeros
jsr bootToggleDataAck ; 8471 blank the rest of the line so a shorter number leaves no digits behind (also textEngineC000:clearToEndOfLine)
L_8474:
jsr serviceCommandExchange ; 8474 keep the lock-step command exchange running while the player thinks
lda menuBlinkTimer ; 8477 frames left before the next highlight flip
bne L_847E ; 8479 not yet
jsr toggleMenuRowHighlight ; 847B flip the highlight of the value row
L_847E:
lda joyRepeatTimer ; 847E joystick auto-repeat delay still running?
bne L_84A4 ; 8480 yes -> only look at the fire button
lda joyDirection ; 8482 active-low joystick nibble (bit 0 up, bit 1 down, bit 2 left, bit 3 right)
ora #$0C ; 8484 force the left/right bits high so they are ignored
cmp #$0F ; 8486 $0F = neither up nor down
beq L_84A4 ; 8488 stick centred (or only sideways) -> no change
asl a ; 848A up -> $1C, down -> $1A
sec ; 848B borrow into the subtraction
sbc #$1B ; 848C $1C-$1B = +1 (up), $1A-$1B = -1 (down)
clc ; 848E no carry into the add
adc editValue ; 848F apply the step to the current value
L_8492:
cmp #$FF ; 8492 limit (operand patched at $8451); a decrement below 0 wraps to $FF and fails here too
bcs L_849E ; 8494 out of range -> leave the value alone
sta editValue ; 8496 store the new value
lda #$02 ; 8499 sound 2 - the value changed
jsr playSound ; 849B play it
L_849E:
jsr setJoystickRepeatDelay ; 849E 16 frames before the first repeat, 6 between repeats
jmp L_845C ; 84A1 redraw the number
L_84A4:
jsr checkFirePressed ; 84A4 new fire click?
bne L_8474 ; 84A7 no -> keep servicing the link and blinking
rts ; 84A9 value confirmed
; ----------------------------------------------------------------------
; applyHandicapPoints - last step of the game-start options. Works out the default handicap of the
; two asymmetric scenarios (QB SNEAK gives side 0 a 2, DEFENDER gives side 1 a $14) and, under CUSTOM
; rules, lets the player edit it (0..50, displayed multiplied by 10). A nonzero result is sent as
; command $A5 kind 0, which cmdSetupOption turns into value*10 points for the sender's side; zero is
; not sent.
; In: D_0BA3 bits 0-2 game type, D_0B9F playerSide, D_92F3 bit 7 CUSTOM toggle
; Out: zp_74 = 0, zp_75 = value, command $A5 queued (only when nonzero); info panel used
; Called from: JMP from runGameStartOptions $85B9
; ----------------------------------------------------------------------
applyHandicapPoints:
ldx #$00 ; 84AA default handicap 0
lda gameTypeOptions ; 84AC gameTypeOptions
and #$07 ; 84AF bits 0-2 = game type
cmp #$01 ; 84B1 1 = QB SNEAK
bne L_84BC ; 84B3 not QB SNEAK
lda playerSide ; 84B5 playerSide
bne L_84C7 ; 84B8 only side 0 (the side on offence) gets the QB SNEAK handicap
ldx #$02 ; 84BA 2 -> 20 points
L_84BC:
cmp #$06 ; 84BC 6 = DEFENDER (A is the game type, or playerSide 0 after the branch above)
bne L_84C7 ; 84BE not DEFENDER
L_84C0:
lda playerSide ; 84C0 playerSide
beq L_84C7 ; 84C3 only side 1 gets the DEFENDER handicap
ldx #$14 ; 84C5 $14 = 20 -> 200 points
L_84C7:
bit setupToggleFlag ; 84C7 CUSTOM toggle $92F3 bit 7
bmi L_84D0 ; 84CA custom rules -> let the player edit the number
txa ; 84CC A = the scenario default
L_84CD:
bne L_84E1 ; 84CD zero -> send nothing at all
rts ; 84CF no handicap in this scenario for this side
L_84D0:
lda #$52 ; 84D0 'HANDICAP' at $EF52
ldy #$EF ; 84D2 high byte of $EF52
jsr beginValueEdit ; 84D4 X still holds the default, which becomes the starting value
lda #$33 ; 84D7 limit $33 = 51, so 0..50
ldy #$0A ; 84D9 displayed multiplied by 10 (0..500 points)
L_84DB:
jsr editValueWithJoystick ; 84DB edit it with the joystick
L_84DE:
lda editValue ; 84DE the edited value
L_84E1:
sta cmdParam2 ; 84E1 cmdParam2 = the handicap
lda #$00 ; 84E3 kind 0 = handicap points
sta cmdParam1 ; 84E5 cmdParam1
lda #$A5 ; 84E7 command $A5 = cmdSetupOption
jmp queueCmd3 ; 84E9 queue command + 2 arguments
; ----------------------------------------------------------------------
; editTerrainPoints - the CUSTOM option 'TERRAIN PTS' (0..2). $92AC is indexed with the complement of
; the side, so the entry this routine edits (index playerSide EOR 1) is exactly the one $47A9 uses
; when one of this player's own units reaches enemy ground. Sent as command $A5 kind 1, which
; cmdSetupOption stores at the same complemented index on both machines.
; In: D_0B9F playerSide, D_92AC
; Out: zp_74 = 1, zp_75 = value, command $A5 queued
; Called from: runGameStartOptions $85A7
; ----------------------------------------------------------------------
editTerrainPoints:
lda playerSide ; 84EC playerSide
eor #$01 ; 84EF $92AC is indexed 1-side, so this picks this player's own entry
; ----------------------------------------------------------------------
; editTerrainPointsForSide - entry into editTerrainPoints with the $92AC index already in A. Nothing
; in variant A reaches it (the XREF caller $83DE belongs to overlay variant B, where $84F1 is
; plotDiamondMarker).
; In: A = $92AC index (0 or 1)
; Out: as editTerrainPoints
; Called from: fall-through only in variant A
; ----------------------------------------------------------------------
editTerrainPointsForSide:
tay ; 84F1 Y = the table index
ldx terrainPointsBySide,y ; 84F2 current TERRAIN PTS setting = starting value
lda #$5A ; 84F5 'TERRAIN PTS' at $EF5A
ldy #$EF ; 84F7 high byte of $EF5A
jsr beginValueEdit ; 84F9 set up the one-row value window
lda #$03 ; 84FC limit 3, so 0..2
ldy #$01 ; 84FE displayed as it is
jsr editValueWithJoystick ; 8500 edit it with the joystick
lda editValue ; 8503 the edited value
sta cmdParam2 ; 8506 cmdParam2
lda #$01 ; 8508 kind 1 = terrain points
sta cmdParam1 ; 850A cmdParam1
lda #$A5 ; 850C command $A5 = cmdSetupOption
jmp queueCmd3 ; 850E queue command + 2 arguments
; ----------------------------------------------------------------------
; editDronesAndMissiles - the CUSTOM options 'DRONES' (0..9) and 'MISSILES' (0..36), edited one after
; the other and sent together as command $90; during the setup phase cmdMissileStrike simply stores
; the two numbers as the sender's dronesLeft/missilesLeft.
; In: D_92AE dronesLeft[own], D_92B0 missilesLeft[own]
; Out: zp_74 = drones, zp_75 = missiles, command $90 queued; self-modifies the LDA operand at $853A
; Called from: runGameStartOptions $85AA
; ----------------------------------------------------------------------
editDronesAndMissiles:
ldx dronesLeft ; 8511 own drone count = starting value
lda #$65 ; 8514 'DRONES' at $EF65
ldy #$EF ; 8516 high byte of $EF65
; ----------------------------------------------------------------------
; editDronesAndMissilesBody - not a real entry point in variant A: it is the JSR beginValueEdit inside
; editDronesAndMissiles. The label only exists because the main program's JSR $8518 at $1460/$14D3
; targets overlay variant B's drawStatsTab.
; In: A/Y = label string, X = starting value (already loaded by editDronesAndMissiles)
; Out: as editDronesAndMissiles
; Called from: fall-through only in variant A
; ----------------------------------------------------------------------
editDronesAndMissilesBody:
jsr beginValueEdit ; 8518 set up the one-row value window for DRONES
lda #$0A ; 851B limit $0A, so 0..9
ldy #$01 ; 851D displayed as it is
jsr editValueWithJoystick ; 851F edit it
lda editValue ; 8522 the drone count
sta L_8539+1 ; 8525 patch the LDA operand at $853A so it survives the second edit
ldx missilesLeft ; 8528 own missile count = starting value
lda #$9E ; 852B 'MISSILES' at $CC9E
L_852D:
ldy #$CC ; 852D high byte of $CC9E
jsr beginValueEdit ; 852F set up the value window again
lda #$25 ; 8532 limit $25 = 37, so 0..36
ldy #$01 ; 8534 displayed as it is
L_8536:
jsr editValueWithJoystick ; 8536 edit it
L_8539:
lda #$FF ; 8539 drone count (operand patched at $8525)
sta cmdParam1 ; 853B cmdParam1 = drones
lda editValue ; 853D missile count
sta cmdParam2 ; 8540 cmdParam2 = missiles
lda #$90 ; 8542 command $90 = cmdMissileStrike (a setup-phase store here, a missile launch during battle)
jmp queueCmd3 ; 8544 queue command + 2 arguments
; ----------------------------------------------------------------------
; runGameStartOptions - the screen shown once per game after the setup has been saved and before unit
; placement. It draws the 'STANDARD/CUSTOM <scenario> SCENARIO' banner and waits for a click, then,
; if the scenario carries CUSTOM rules ($0BA3 bit 6), offers a STANDARD/CUSTOM choice; picking CUSTOM
; runs the COMCEN SPEED, TERRAIN PTS, DRONES and MISSILES editors. Scenarios 4-6 then place the
; recycler, and it finishes in applyHandicapPoints.
; In: D_0BA3 gameTypeOptions, D_0B9F playerSide, menu/text engine state
; Out: zp_B1 = $7F, D_8450 customSetupChosen, D_92F3 bit 7, setup commands $A5/$90/$9C queued
; Called from: startGameSession $7AF7
; ----------------------------------------------------------------------
runGameStartOptions:
lda #$7F ; 8547 $7F = setup phase with drawing enabled
sta gamePhase ; 8549 gamePhase zp_B1
jsr resetSelectionAndView ; 854B park the viewport off the map
L_854E:
lda playerSide ; 854E playerSide
bne L_8559 ; 8551 side 1 needs the whole screen redrawn
jsr drawOverviewMap ; 8553 side 0 only refreshes the overview map
jmp L_855C ; 8556 skip the side-1 redraw
L_8559:
jsr clearScreenAndRedrawGameScreen; 8559 grey the bitmap and redraw the game screen frame
L_855C:
jsr showScenarioBannerAndWaitForButton; 855C show the scenario banner and wait for the fire button
lda gameTypeOptions ; 855F gameTypeOptions
and #$40 ; 8562 bit 6 = CUSTOM rules
sta customSetupChosen ; 8564 $40 or 0 (overwritten with 0/1 by the menu below)
asl a ; 8567 shift bit 6 up into bit 7
L_8568:
sta setupToggleFlag ; 8568 setupToggleFlag $92F3 bit 7
beq L_85AD ; 856B STANDARD rules -> skip the whole custom block
jsr showCustomSummaryMessage; 856D put 'CUSTOM: ... MENUS x, DAMAGE y.' on the status line
lda #$0D ; 8570 text cursor row 13
jsr clearInfoPanelSetRow ; 8572 clear the info panel, cursor to row 13
lda gameTypeOptions ; 8575 gameTypeOptions
and #$07 ; 8578 bits 0-2 = game type
tax ; 857A X = scenario index
lda #$C8 ; 857B scenario name table at $CCC8 (8-byte entries)
ldy #$CC ; 857D high byte of $CCC8
jsr printCentredTableEntry ; 857F print the scenario name centred in 13 columns
lda #$04 ; 8582 ' SCENARIO' at $CC04
ldy #$CC ; 8584 high byte of $CC04
ldx #$00 ; 8586 preselect row 0 = STANDARD
jsr printMenuTitle ; 8588 print it as the menu title
lda #$02 ; 858B two items
sta menuItemCount ; 858D menuItemCount $91D4
jsr printStringAtPointer ; 8590 'STANDARD' follows at $CC11
jsr printNewline ; 8593 next line
jsr printStringAtPointer ; 8596 'CUSTOM' follows at $CC19
jsr runMenuWidth10 ; 8599 run the selector
sta customSetupChosen ; 859C 0 = STANDARD, 1 = CUSTOM
lda customSetupChosen ; 859F redundant reload - A already holds the choice
beq L_85AD ; 85A2 STANDARD -> keep the scenario defaults
jsr editComcenSpeed ; 85A4 COMCEN SPEED
jsr editTerrainPoints ; 85A7 TERRAIN PTS
jsr editDronesAndMissiles ; 85AA DRONES and MISSILES
L_85AD:
lda gameTypeOptions ; 85AD gameTypeOptions
and #$07 ; 85B0 bits 0-2 = game type
cmp #$04 ; 85B2 types 4-6 are SLUGGERS, FULL WAR and DEFENDER
bcc L_85B9 ; 85B4 the other scenarios have no recycler
jsr chooseRecycler ; 85B6 choose and place the recycler
L_85B9:
jmp applyHandicapPoints ; 85B9 finish with the handicap
; ----------------------------------------------------------------------
; editComcenSpeed - the CUSTOM option 'COMCEN SPEED' (0..3). Sent as command $A5 kind 3, which
; cmdSetupOption stores in $92B4 of the sender; at $7B6E the setting is mapped through the table at
; $78BF into the comcen's actual step rate, and $34D5 refuses to move a comcen whose class is 4.
; In: D_92B4 comcenSpeedClass[own]
; Out: zp_74 = 3, zp_75 = value, command $A5 queued
; Called from: runGameStartOptions $85A4
; ----------------------------------------------------------------------
editComcenSpeed:
ldx comcenSpeedClass ; 85BC own comcen speed class = starting value
lda #$6B ; 85BF 'COMCEN SPEED' at $EF6B
ldy #$EF ; 85C1 high byte of $EF6B
jsr beginValueEdit ; 85C3 set up the one-row value window
lda #$04 ; 85C6 limit 4, so 0..3
ldy #$01 ; 85C8 displayed as it is
jsr editValueWithJoystick ; 85CA edit it with the joystick
lda editValue ; 85CD the edited value
sta cmdParam2 ; 85D0 cmdParam2
lda #$03 ; 85D2 kind 3 = comcen speed
sta cmdParam1 ; 85D4 cmdParam1
lda #$A5 ; 85D6 command $A5 = cmdSetupOption
jmp queueCmd3 ; 85D8 queue command + 2 arguments
; ----------------------------------------------------------------------
; beginValueEdit - common prologue of all four value editors: remembers the label string in two
; self-modified operands, seeds editValue, clears the info panel to row 15, prints the label centred,
; then moves the text window to column $1D (29) and makes the next line the single menu row. Falls
; into setValueEditWindow.
; In: A/Y = bit-7 terminated label, X = starting value
; Out: D_844F = X, D_91D5 = 0, D_91D6 = the value row, D_90F8 = $FE, zp_3B = $1D, zp_3C = 5;
; self-modifies $85EA/$85EC with the label pointer
; Called from: $84D4 (HANDICAP), $84F9 (TERRAIN PTS), $8518 (DRONES), $852F (MISSILES), $85C3
; (COMCEN SPEED)
; ----------------------------------------------------------------------
beginValueEdit:
sta L_85E9+1 ; 85DB patch the LDA operand at $85EA with the label's low byte
sty L_85EB+1 ; 85DE patch the LDY operand at $85EC with its high byte
stx editValue ; 85E1 editValue = the starting value
lda #$0F ; 85E4 text cursor row 15
jsr clearInfoPanelSetRow ; 85E6 clear the info panel, cursor to row 15
L_85E9:
lda #$FF ; 85E9 label low byte (patched at $85DB)
L_85EB:
ldy #$FF ; 85EB label high byte (patched at $85DE)
jsr printStringCentred ; 85ED print the label centred
lda #$1D ; 85F0 column 29 - where the number goes
sta textWindowLeft ; 85F2 textWindowLeft zp_3B
jsr printNewline ; 85F4 the value goes on the next line
L_85F7:
lda textCursorRow ; 85F7 that row
sta menuFirstRow ; 85F9 menuFirstRow $91D6 - the single row the editor blinks
lda #$00 ; 85FC 0 = the only row
sta menuSelectedItem ; 85FE menuSelectedItem 0 - a one-row menu
; ----------------------------------------------------------------------
; setValueEditWindow - tail of beginValueEdit: marks the menu as a plain one (no unit attached) and
; sets the highlight bar to 5 characters. The separate label exists only because $8601 is a real
; routine (printNumberRightAligned) in overlay variant B; in A it is reached by fall-through alone.
; In: nothing
; Out: D_90F8 = $FE, zp_3C = 5
; Called from: fall-through only in variant A
; ----------------------------------------------------------------------
setValueEditWindow:
lda #$FE ; 8601 $FE = plain menu, no unit behind it (keeps the REPAIR-menu special case out of the blinker)
sta selectedUnit ; 8603 selectedUnit $90F8
lda #$05 ; 8606 highlight bar 5 characters wide
sta textWindowWidth ; 8608 textWindowWidth zp_3C
rts ; 860A the value window is ready
; ----------------------------------------------------------------------
; editOptionBitOnOff - a two-row OFF/ON menu for one bit of gameTypeOptions, used by the game-type
; screen for 'UNIT MENUS' (mask $08) and 'DAMAGE' (mask $10). The label pointer and the mask go into
; self-modified operands, the current state of the bit preselects the row, and the chosen row writes
; the bit back into $0BA3.
; In: A/Y = label string, X = bit mask, D_0BA3
; Out: D_0BA3 with that bit updated, D_91D4 = 2, D_91D5 = chosen row;
; self-modifies $8622 (mask), $8627/$8629 (label pointer)
; Called from: runGameTypeMenu $82B3 (UNIT MENUS) and $82BC (DAMAGE)
; ----------------------------------------------------------------------
editOptionBitOnOff:
sta L_8626+1 ; 860B patch the LDA operand at $8627 with the label's low byte
sty L_8628+1 ; 860E patch the LDY operand at $8629 with its high byte
stx L_8621+1 ; 8611 patch the AND operand at $8622 with the bit mask
jsr resetSelectionAndView ; 8614 park the viewport off the map
lda #$0E ; 8617 text cursor row 14
jsr clearInfoPanelSetRow ; 8619 clear the info panel, cursor to row 14
ldx #$00 ; 861C preselect row 0 = OFF
lda gameTypeOptions ; 861E gameTypeOptions
L_8621:
and #$FF ; 8621 mask (operand patched at $8611)
beq L_8626 ; 8623 bit clear -> leave OFF preselected
inx ; 8625 bit set -> preselect row 1 = ON
L_8626:
lda #$FF ; 8626 label low byte (patched at $860B)
L_8628:
ldy #$FF ; 8628 label high byte (patched at $860E)
jsr printMenuTitle ; 862A print the label as the menu title
; ----------------------------------------------------------------------
; runOnOffMenuAndApplyBit - tail of editOptionBitOnOff that sets the item count and falls into the
; printing part. Only a real entry point in overlay variant B (setTextColumn).
; In: menu title already printed, mask in the operand at $8622
; Out: D_0BA3 updated
; Called from: fall-through only in variant A
; ----------------------------------------------------------------------
runOnOffMenuAndApplyBit:
lda #$02 ; 862D two items
sta menuItemCount ; 862F menuItemCount $91D4
; ----------------------------------------------------------------------
; printOnOffMenuAndApplyBit - inner tail of editOptionBitOnOff: prints the words OFF ($064D) and ON
; ($0650), runs the 5-wide selector and clears or sets the mask bit in gameTypeOptions. The main
; program's JSR $8632 at $176B targets overlay variant B's drawComcenSchematic, not this code.
; In: menu state, mask in the operand at $8622, D_0BA3
; Out: D_0BA3 bit cleared or set, D_91D5 = chosen row
; Called from: fall-through only in variant A
; ----------------------------------------------------------------------
printOnOffMenuAndApplyBit:
lda #$4D ; 8632 'OFF' at $064D
ldy #$06 ; 8634 high byte of $064D
jsr printString ; 8636 print row 0
jsr printNewline ; 8639 next line
jsr printStringAtPointer ; 863C 'ON' follows at $0650
lda #$05 ; 863F highlight bar 5 characters wide
jsr runMenuWithWidth ; 8641 run the selector
tay ; 8644 Y = 0 OFF, 1 ON
lda #$FF ; 8645 $FF - build the inverse of the mask
eor L_8621+1 ; 8647 invert the mask (operand at $8622)
and gameTypeOptions ; 864A clear that bit in gameTypeOptions
cpy #$00 ; 864D was ON chosen?
beq L_8654 ; 864F no - leave the bit clear
ora L_8621+1 ; 8651 yes - set it again
L_8654:
sta gameTypeOptions ; 8654 store the updated options byte
rts ; 8657 the option bit is updated
; ----------------------------------------------------------------------
; handleStarKeyToggle - per-frame hook the main program calls while overlay A is resident and the game
; is not in the battle phase. A '*' keypress ($AA) that is not part of a chat line is consumed and
; flips bit 7 of setupToggleFlag; the routine then falls into showToggleStateMessage and returns C =
; 1. Any other key returns C = 0. The flag means CUSTOM rules on the scenario setup screen and
; DESTROY mode during unit placement (where clicking one of your own units sends $91 self destruct at
; $209F).
; In: D_929C chatState, D_9248 lastTypedKey, D_92F3 setupToggleFlag
; Out: C = 1 with D_92F3 bit 7 flipped and D_9248 = $FF when '*' was pressed, otherwise C = 0
; Called from: main program trampoline $25C4 (JMP, battlefield loop when zp_B1 != 0) and runMenuLoop
; $6002 (JSR, when zp_B1 == $FF)
; ----------------------------------------------------------------------
handleStarKeyToggle:
lda chatState ; 8658 chatState $929C, bit 7 set while a chat line is being typed
bmi L_8664 ; 865B typing -> the '*' belongs to the text
lda lastTypedKey ; 865D last typed key (PETSCII with bit 7 set)
cmp #$AA ; 8660 $AA = '*'
beq L_8666 ; 8662 it is the toggle key
L_8664:
clc ; 8664 C = 0: key not handled here
rts ; 8665 C = 0, key not handled
L_8666:
lda #$FF ; 8666 $FF = no key pending
sta lastTypedKey ; 8668 consume the '*' so nothing else sees it
lda setupToggleFlag ; 866B setupToggleFlag
eor #$80 ; 866E flip bit 7
sta setupToggleFlag ; 8670 store it back and fall into showToggleStateMessage
; ----------------------------------------------------------------------
; showToggleStateMessage - shows the state of the '*' toggle. It empties the message queue, patches
; the three characters 'ON'+terminator or 'OFF' into the message text at the address held in the
; operand at
; $869A/$869B ($EFDF inside 'CUSTOM OFF', or $EFEA inside 'DESTROY OFF'), makes $1E the persistent
; message while the mode is on (and the id in the operand at $868D - 0 on the setup screen, $0A =
; 'SETUP UNITS. PRESS F 1 WHEN DONE.' during placement - while it is off) and queues message $1E.
; In: D_92F3 bit 7; operands $868D and $869A/$869B set by $820B/$821F or setDestroyModeMessage
; Out: D_9252 = D_9251 (queue emptied), D_92A1, zp_48/zp_49, zp_72 = 0, message $1E queued, C = 1
; Called from: fall-through from handleStarKeyToggle, and directly from runGameTypeMenu $8253
; ----------------------------------------------------------------------
showToggleStateMessage:
lda messageQueueHead ; 8673 message queue write index
sta messageQueueTail ; 8676 read index = write index -> the queue is empty
lda #$00 ; 8679 0 - show the next message immediately
sta messageTimer ; 867B messageTimer 0 so the new message appears at once
lda #$50 ; 867D 'ON' at $0650
ldy #$06 ; 867F high byte of $0650
ldx #$1E ; 8681 persistent message id $1E while the mode is on
bit setupToggleFlag ; 8683 setupToggleFlag bit 7
bmi L_868E ; 8686 toggle on -> keep 'ON' and id $1E
lda #$4D ; 8688 'OFF' at $064D
ldy #$06 ; 868A high byte of $064D
L_868C:
ldx #$FF ; 868C off-state persistent message id (operand patched at $8213 = 0, or at $86AC = $0A)
L_868E:
stx persistentMessageId ; 868E persistentMessageId $92A1
sta mapCellPtr ; 8691 zp_48 = word pointer low
sty mapCellPtrHi ; 8693 zp_49 = word pointer high
ldy #$02 ; 8695 copy three bytes (for 'ON' that is 'O', 'N'|$80 and the stray 'F' of the next word)
L_8697:
lda (mapCellPtr),y ; 8697 read the word
; ----------------------------------------------------------------------
; The STA operand below is a variable: $820B/$821F point it at $EFDF ('CUSTOM OFF') and
; setDestroyModeMessage points it at $EFEA ('DESTROY OFF'). The $FFFF assembled here is just the byte
; pattern that happens to be on disk.
; ----------------------------------------------------------------------
L_8699:
sta irqVectorHi,y ; 8699 destination patched at $869A/$869B ($EFDF inside 'CUSTOM OFF' or $EFEA inside 'DESTROY OFF'); the $FFFF assembled here is only the byte pattern on disk
dey ; 869C next byte down
bpl L_8697 ; 869D three bytes copied?
lda #$00 ; 869F 0 again
sta messageTimer ; 86A1 redundant - messageTimer was already zeroed at $867B
lda #$1E ; 86A3 message id $1E
jsr queueMessage ; 86A5 queue it
sec ; 86A8 C = 1: the key was handled
rts ; 86A9 C = 1, key handled
; ----------------------------------------------------------------------
; setDestroyModeMessage - re-aims the '*' toggle at the unit-placement phase: the off-state persistent
; message becomes id $0A ('SETUP UNITS. PRESS F 1 WHEN DONE.'), message slot $1E becomes 'DESTROY OFF'
; ($EFE2) and the ON/OFF patch address becomes $EFEA, the 'OFF' inside that string. The setup screen
; does the same at $820B/$821F with 'CUSTOM OFF' ($EFD8/$EFDF) and off-message 0.
; In: nothing
; Out: $868D = $0A, message slot $1E -> $EFE2, $869A/$869B = $EFEA
; Called from: startGameSession $7B4B, just before the unit placement loop
; ----------------------------------------------------------------------
setDestroyModeMessage:
lda #$0A ; 86AA message id $0A = 'SETUP UNITS. PRESS F 1 WHEN DONE.'
sta L_868C+1 ; 86AC patch the LDX operand at $868D
ldx #$1E ; 86AF message slot $1E
lda #$E2 ; 86B1 'DESTROY OFF' at $EFE2
ldy #$EF ; 86B3 high byte of $EFE2
jsr setMessageSlot ; 86B5 register it as message $1E
lda #$EA ; 86B8 $EFEA = the 'OFF' inside 'DESTROY OFF'
sta L_8699+1 ; 86BA patch $869A
lda #$EF ; 86BD high byte of $EFEA
sta L_8699+2 ; 86BF patch $869B
rts ; 86C2 the '*' toggle now drives the DESTROY message
; ----------------------------------------------------------------------
; showCustomSummaryMessage - builds the status line 'CUSTOM:' [+ 'MAP,' when a named map is in use] +
; ' MENUS OFF,/ON, DAMAGE OFF./ON.' in the buffer at $0332 and registers it as the persistent
; message. The prefix is copied character by character from 'CUSTOM' ($CC19) and 'MAP TYPE' ($EF77);
; the option part is punched into the template ' MENUS XXXX DAMAGE XXX.' at $0636 by appendOnOffWord
; from bits 3 and 4 of gameTypeOptions, and the finished template is then appended behind the prefix.
; In: D_0BA3 bits 3, 4 and 7; the template at $0636 and the words at $064D
; Out: text in $0332.., the $0636 template overwritten in place, message slot $1E = $0332,
; D_92A1 = $1E, message queued; self-modifies the ADC operand at $8712 with the prefix length
; Called from: startFilmPlayback $7D4B and runGameStartOptions $856D
; ----------------------------------------------------------------------
showCustomSummaryMessage:
ldy #$00 ; 86C3 start of the output buffer
L_86C5:
lda msgCustom,y ; 86C5 'CUSTOM' at $CC19
and #$7F ; 86C8 strip the bit-7 string terminator
sta play2Grp06,y ; 86CA message buffer at $0332
iny ; 86CD next output position
cpy #$06 ; 86CE six characters of 'CUSTOM'
bcc L_86C5 ; 86D0 keep copying until six are done
lda #$3A ; 86D2 ':'
sta play2Grp06,y ; 86D4 append it to the buffer
iny ; 86D7 next output position
bit gameTypeOptions ; 86D8 gameTypeOptions bit 7 = custom (named) map
bpl L_86F3 ; 86DB random map -> no 'MAP,' in the summary
ldx #$00 ; 86DD start at the first character of 'MAP TYPE'
L_86DF:
lda addRowDeltaToDroneRow,x ; 86DF 'MAP TYPE' at $EF77 (also modemDriverE000:msgMapType, trainerAiE000:storeScanReferencePoint)
and #$7F ; 86E2 strip any terminator bit
sta play2Grp06,y ; 86E4 append to the buffer
inx ; 86E7 next source character
iny ; 86E8 next output position
cpx #$03 ; 86E9 only the first three characters, 'MAP'
bcc L_86DF ; 86EB keep copying until three are done
lda #$2C ; 86ED ','
sta play2Grp06,y ; 86EF append it to the buffer
iny ; 86F2 next output position
L_86F3:
sty L_8711+1 ; 86F3 patch the ADC operand at $8712 with the length of the prefix built so far
; ----------------------------------------------------------------------
; Fill the two XXXX fields of the ' MENUS XXXX DAMAGE XXX.' template at $0636, put the terminator
; right behind the last word, then copy the whole template backwards onto the end of the prefix.
; ----------------------------------------------------------------------
ldx #$07 ; 86F6 template index 7 = the first X of ' MENUS XXXX'
lda gameTypeOptions ; 86F8 gameTypeOptions
D_86FB:
and #$08 ; 86FB bit 3 = UNIT MENUS
jsr appendOnOffWord ; 86FD write 'OFF,' or 'ON, ' there
ldx #$13 ; 8700 template index 19 = the XXX of ' DAMAGE XXX'
lda gameTypeOptions ; 8702 gameTypeOptions
and #$10 ; 8705 bit 4 = DAMAGE
jsr appendOnOffWord ; 8707 write 'OFF,' or 'ON, ' there
lda #$AE ; 870A '.' with bit 7 set = end of string
sta optionSummaryTemplate,x ; 870C put it right behind the last word written
txa ; 870F X = index of that terminator
clc ; 8710 no carry into the add
L_8711:
adc #$FF ; 8711 + prefix length (operand patched at $86F3)
tay ; 8713 Y = destination index of the last template byte
L_8714:
lda optionSummaryTemplate,x ; 8714 copy the template backwards so source and destination cannot collide
sta play2Grp06,y ; 8717 into the message buffer
dey ; 871A next destination byte down
dex ; 871B next template byte down
bpl L_8714 ; 871C down to template index 0
lda #$32 ; 871E the finished string at $0332
ldy #$03 ; 8720 high byte of $0332
; ----------------------------------------------------------------------
; setPersistentMessage - registers the string A/Y as message slot $1E (setMessageSlot writes the
; pointer tables at $0500/$0524), makes $1E the persistent status message and queues it.
; In: A/Y = bit-7 terminated string
; Out: D_0500[$1E]/D_0524[$1E] = A/Y, D_92A1 = $1E, message queued, X = $1E
; Called from: showScenarioBannerAndWaitForButton $8787, plus fall-through from
; showCustomSummaryMessage
; ----------------------------------------------------------------------
setPersistentMessage:
ldx #$1E ; 8722 message slot $1E
jsr setMessageSlot ; 8724 setMessageSlot leaves X alone
txa ; 8727 A = $1E
sta persistentMessageId ; 8728 persistentMessageId $92A1 - the line reappears after any transient message
jmp queueMessage ; 872B queue it for display
; ----------------------------------------------------------------------
; appendOnOffWord - writes 'OFF,' (A = 0) or 'ON, ' (A != 0) into the template at $0636 starting at
; index X: copies the bit-7 terminated word from $064D (OFF) or $064D+3 (ON), adds a comma and, for
; ON, a trailing space so both forms occupy four characters. X comes back pointing at the comma.
; In: A = option state (0 = off), X = destination index inside $0636
; Out: four characters written at $0636+X, X = index of the comma
; Called from: showCustomSummaryMessage $86FD (UNIT MENUS) and $8707 (DAMAGE)
; ----------------------------------------------------------------------
appendOnOffWord:
pha ; 872E remember the state for the trailing-space decision
tay ; 872F Y = 0 selects 'OFF' at $064D
D_8730:
beq L_8734 ; 8730 off -> keep Y = 0
ldy #$03 ; 8732 on -> 'ON' at $064D+3
L_8734:
lda optionWordTable,y ; 8734 next character of the word
bmi L_8740 ; 8737 bit 7 set = last character
sta optionSummaryTemplate,x ; 8739 store it in the template
inx ; 873C next template position
iny ; 873D next source character
bne L_8734 ; 873E always - continue with the next character
L_8740:
and #$7F ; 8740 strip the terminator bit
sta optionSummaryTemplate,x ; 8742 store the last character
inx ; 8745 next template position
lda #$2C ; 8746 ','
sta optionSummaryTemplate,x ; 8748 store it
pla ; 874B the saved state
beq L_8755 ; 874C 'OFF,' is already four characters
inx ; 874E one position past the comma
lda #$20 ; 874F ' ' so that 'ON, ' is four too
sta optionSummaryTemplate,x ; 8751 store the padding blank
dex ; 8754 leave X on the comma
L_8755:
rts ; 8755 X points at the comma
; ----------------------------------------------------------------------
; showScenarioBannerAndWaitForButton - prints 'STANDARD' or 'CUSTOM' (from $CC11, chosen by $0BA3 bit
; 6), the scenario name (from $CCC8, bits 0-2) and ' SCENARIO' ($CC04), each centred in 13 columns,
; puts 'PRESS BUTTON WHEN READY.' ($CBBE) on the status line and then services the link until the fire
; button is clicked.
; In: D_0BA3, the fire button, the command exchange
; Out: info panel text, D_92A1 = $1E, status line cleared on exit
; Called from: runGameStartOptions $855C
; ----------------------------------------------------------------------
showScenarioBannerAndWaitForButton:
lda #$0F ; 8756 text cursor row 15
jsr clearInfoPanelSetRow ; 8758 clear the info panel, cursor to row 15
ldx #$00 ; 875B entry 0 = STANDARD
lda gameTypeOptions ; 875D gameTypeOptions
and #$40 ; 8760 bit 6 = CUSTOM rules
beq L_8765 ; 8762 standard rules
inx ; 8764 entry 1 = CUSTOM
L_8765:
lda #$11 ; 8765 rules name table at $CC11 (8-byte entries)
ldy #$CC ; 8767 high byte of $CC11
jsr printCentredTableEntry ; 8769 print it centred in 13 columns
jsr printNewline ; 876C next line
lda gameTypeOptions ; 876F gameTypeOptions
and #$07 ; 8772 bits 0-2 = game type
tax ; 8774 X = scenario index
lda #$C8 ; 8775 scenario name table at $CCC8 (8-byte entries)
ldy #$CC ; 8777 high byte of $CCC8
jsr printCentredTableEntry ; 8779 print the scenario name centred
lda #$04 ; 877C ' SCENARIO' at $CC04 (it starts and ends with a CR)
ldy #$CC ; 877E high byte of $CC04
jsr printStringCentred ; 8780 print it centred
lda #$BE ; 8783 'PRESS BUTTON WHEN READY.' at $CBBE
ldy #$CB ; 8785 high byte of $CBBE
jsr setPersistentMessage ; 8787 show it as the persistent status message
L_878A:
jsr serviceCommandExchange ; 878A keep the two machines in lock step while waiting
jsr checkFirePressed ; 878D new fire click?
bne L_878A ; 8790 no -> keep servicing
jmp clearMessageLine ; 8792 erase the status line and return
; ----------------------------------------------------------------------
; printCentredTableEntry - sets the centring field width to 13 columns and prints entry X of a table
; of 8-byte bit-7 terminated strings whose base is in A/Y. Used for the STANDARD/CUSTOM words and for
; the seven scenario names.
; In: A/Y = table base, X = entry index
; Out: D_90EF = $0D, string printed centred (printStringAtPointer clears D_90EF again)
; Called from: $857F, $8769, $8779
; ----------------------------------------------------------------------
printCentredTableEntry:
pha ; 8795 keep the table base low byte across the store
lda #$0D ; 8796 13 columns
sta textCentreWidth ; 8798 textCentreWidth $90EF - the field the next string is centred in
pla ; 879B table base low byte again
jmp printTableEntry ; 879C print entry X (base + X*8)
; Result and alert message strings shared with overlay variant B. From $879F on the bytes are
; identical in both variants; only $8789-$879E differ, so in variant A messages $11 ('WE LOST ON
; POINTS!') and $12 ('WE WON ON POINTS!') are broken while messages $13-$16 and 'DRONE ALERT!' still
; read correctly. They are only ever shown at game end, when variant B is resident.
msgWeWonOnPointsTail:
.byte "ON ON POINTS",'!'|$80 ; 879F tail of message $12 'WE WON ON POINTS!'; its first four bytes at $879B are code here
; msgItWasADraw: text, 14 bytes. 'IT WAS A DRAW.' = message $13 (game result)
msgItWasADraw:
.byte "IT WAS A DRAW",'.'|$80 ; 87AC message $13 (pointer $0500/$0524 index $13 = $87AC)
; msgWeWereKnockedOut: text, 20 bytes. 'WE WERE KNOCKED OUT!' = message $14
msgWeWereKnockedOut:
.byte "WE WERE KNOCKED OUT",'!'|$80; 87BA message $14
; msgWeWonByKnockout: text, 21 bytes. 'WE WON BY A KNOCKOUT!' = message $15. Comm build 1 (T35, solo
; trainer) reuses $87CE+unit as a per-unit work table ($EB29/$EB53 store the game clock there), so
; with the trainer resident these strings are clobbered.
msgWeWonByAKnockout:
.byte "WE WON BY A KNOCKOUT",'!'|$80; 87CE message $15; the solo trainer overlay (T35) reuses $87CE+unit as a per-unit work table ($EB29/$EB53 store the game clock there), so these strings are clobbered in a solo game
; msgGameOver: text, 10 bytes. 'GAME OVER.' = message $16
msgGameOver:
.byte "GAME OVER",'.'|$80 ; 87E3 message $16
; msgDroneAlert: text, 12 bytes. 'DRONE ALERT!' - installed as message slot $20 by overlay A at $7BA6
; (sub_C924 X=$20)
msgDroneAlert:
.byte "DRONE ALERT",'!'|$80 ; 87ED installed as run-time message slot $20 by overlay A at $7BA6
; overlayTailPadding: unknown, 6 bytes. six zero bytes of padding before the variant flag
overlayTailPadding:
.byte $00,$00,$00,$00,$00,$00 ; 87F9 ...... six unused bytes between the last string and the identity flag; referenced by nothing
; overlayVariantFlag: byteTable, 1 bytes. $00 = variant A, $FF = variant B (bit 7 tested by the main
; program at $1036/$1080)
overlayVariantFlag:
.byte $00 ; 87FF . $00 = variant A (map generator and setup) is resident; bit 7 is tested at $1036/$1080