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