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