; ============================================================================ ; $0800-$6EFF - main game program (tracks 22-27, sectors 0-17 each) ; ============================================================================ ; Entry point $0800 (JMP $0A6C). Contains the loader API, start-up sequence, raster interrupt, input ; handling, the battlefield and console screens, the unit simulation and the command interpreter. .setcpu "6502" .include "c64.inc" .include "zeropage.inc" ; ---- references to code/data outside this file ---- D_0104 := $0104 bamDiskIdChar0 := $02A2 bamDiskIdChar1 := $02A3 bamDiskSerial := $02AB basicVectorHijack := $0300 checksumXorConstant := $0400 mapNameSeedTable := $0401 play3Col28 := $0410 play3Col44 := $0420 play3Row10 := $0430 play3Row12 := $0432 play4Grp12 := $0464 play4Row12 := $04C8 play4Row13 := $04C9 play4Row20 := $04D0 play4Row21 := $04D1 play4Row35 := $04DF play4Row43 := $04E7 chatMessageLine := $0548 D_0549 := $0549 switchToRequestedScreen := $06BA runComcenStunnedBlackout := $0709 D_077E := $077E unitGlyphBaseTable := $0799 D_07BE := $07BE terrainColourTable := $07EA generateBattlefieldMap := $6F04 applyDroneBlast := $72A1 steerDrone := $731E drawRadarGlyph := $7396 getRadarCellGlyph := $74C3 radarSideBranchOpcode := $750A missileLineFromComcen := $7717 clearMapNameField := $7836 startGameSession := $78D1 scrollRadarOnComcenMove := $7959 mapCellToRadarGrid := $7A13 isRadarGridCellInRange := $7A2B redrawRadarMapWindow := $7A6B waitForSpaceKey := $7DB8 handleMainMenuChoice := $7DE3 getDroneMapPosition := $81FE showDroneAlert := $820F fillDroneViewWindow := $821F drawShotLineInViewport := $82BF drawStatsTab := $8518 drawComcenSchematic := $8632 handleStarKeyToggle := $8658 filmViewWordTable := $86FB msgShowViewWord := $8726 msgSpeedDigit := $8730 overlayVariantFlag := $87FF sprite0Shape := $8A00 sprite1Shape := $8A40 sprite2Shape := $8A80 sprite2ShapeRow7 := $8A95 sprite3Shape := $8AC0 sprite4Shape := $8B00 D_8C00 := $8C00 D_8D00 := $8D00 D_8E00 := $8E00 D_8F00 := $8F00 vicCtrl1Shadow := $9000 vicCtrl2Shadow := $9001 vicMemPtrShadow := $9002 vicBorderShadow := $9003 vicBg0Shadow := $9004 vicBg1Shadow := $9005 vicBg2Shadow := $9006 spriteColourShadow := $9008 sprite1ColourShadow := $9009 sprite2ColourShadow := $900A sprite3ColourShadow := $900B sprite4ColourShadow := $900C spriteXShadow := $9010 sprite1XShadow := $9011 sprite2XShadow := $9012 sprite3XShadow := $9013 sprite4XShadow := $9014 spriteYShadow := $9018 sprite1YShadow := $9019 sprite2YShadow := $901A sprite3YShadow := $901B sprite4YShadow := $901C spriteXMsbShadow := $9020 spriteEnableShadow := $9022 spriteXExpandShadow := $9023 spriteYExpandShadow := $9024 spriteBuildBuffer := $9025 outgoingCmdRing := $9065 outgoingCmdRingEnd := $908C outgoingCmdWriteIndex := $908D outgoingCmdReadIndex := $908E mergedCmdBuffer := $908F mergedCmdReadIndex := $90B7 glyphBuildBuffer := $90B8 prevKeyCode := $90E9 newKeyEvent := $90EA inputIdleFrames := $90EE textCentreWidth := $90EF lastStepRowDelta := $90F4 lastStepColDelta := $90F5 drawCellSaveCol := $90F6 drawCellSaveRow := $90F7 selectedUnit := $90F8 menuTabIndex := $90F9 currentUnit := $90FA currentScreen := $90FB stepDelayTimer := $90FC unitProcessOrder := $90FD unitOrderIndex := $9162 linkPollDelay := $9163 unitBaseOrder := $9164 shotLineTimer := $91C8 joyRepeatMask := $91C9 joyHoldCounter := $91CA gameClock := $91CB activeSpriteIndex := $91CC menuItemIdList := $91CE menuItemCount := $91D4 menuSelectedItem := $91D5 menuFirstRow := $91D6 infoPanelUnit := $91D7 pickedCellCol := $91D8 pickedCellRow := $91D9 currentUnitCol := $91DA currentUnitRow := $91DB boomerTargetTable := $91DC fireTargetUnit := $91F0 fireTargetDistance := $91F1 circleStepsLeft := $91F2 spritePlotMode := $91F3 groupStatusTable := $91F4 highlightedUnit := $9204 highlightedGroupKey := $9205 selectedGroupKey := $9206 unusedFlag9207 := $9207 lastShownClock := $9208 uiLevel := $9209 groupBoxActive := $920A droneHeading := $920B droneControlFlags := $920D exchangeAppliedFlag := $920E frameWidth := $9221 frameHeight := $9222 linkTimeoutCounter := $9223 cellBackgroundColour := $9226 scanUnitTerrain := $9227 scanUnitType := $9228 scanUnitTerrainHeight := $9229 comcenStunStatus := $922A overviewUnitPattern := $922B currentUnitCloakBits := $922C groupBoxRightOffset := $922E groupBoxLeftOffset := $922F groupBoxBottomOffset := $9230 groupBoxTopOffset := $9231 viewRedrawTimer := $9234 comcenUnit := $9236 radarOwnUnitColour := $9237 radarEnemyUnitColour := $9238 radarTerrainColour := $9239 pendingRedrawGroup := $923A pendingRedrawUnit := $923B commandSliceIndex := $923C attackerType := $923D markerSpriteTimer := $923E gameOverFlags := $923F hitDamage := $9240 comcenSystemStatus := $9241 comcenBattleStatus := $9242 comcenRadarStatus := $9243 comcenDroneStatus := $9244 comcenCloakedFlag := $9245 rleMarkerByte := $9246 rleUnpackFlag := $9247 lastTypedKey := $9248 messageQueue := $9249 messageQueueHead := $9251 messageQueueTail := $9252 incomingChatBuffer := $9253 incomingChatScrollSrc := $9254 incomingChatLength := $9277 typedLineBuffer := $9278 chatState := $929C lastOrderedGroupKey := $929D pendingGroupOrderCount := $929E pendingOwnCmdCount := $929F rawKeyCode := $92A0 persistentMessageId := $92A1 sideScoreLo := $92A2 side1ScoreLo := $92A3 sideScoreHi := $92A4 side1ScoreHi := $92A5 recyclerMode := $92A6 recyclerCol := $92A8 recyclerRow := $92AA terrainPointsBySide := $92AC dronesLeft := $92AE missilesLeft := $92B0 comcenRevealTimer := $92B2 comcenSpeedClass := $92B4 opponentComcenSpeedClass := $92B5 exchangeActivityFlag := $92C2 savedViewOriginCol := $92C3 savedViewOriginRow := $92C4 menuBlinkState := $92C5 manualCheckTries := $92C7 screenShakeTimer := $92C8 gameEndCountdown := $92C9 comcenStatusSnapshot := $92D6 stepIntervalFrames := $92E3 shiftHeldFlag := $92E4 commodoreHeldFlag := $92E5 filmNameLength := $92E6 mergedCmdLength := $92EB mergedCmdSplitIndex := $92EC mergedCmdOwnerFlag := $92ED currentCommandLength := $92EE lastQuarterRound := $92EF diskIdIndex := $92F0 filmEndPtr := $92F1 filmEndPtrHi := $92F2 setupToggleFlag := $92F3 spriteScrollFineX := $92F4 spriteScrollFineY := $92F5 droneViewOriginCol := $92F6 droneViewOriginRow := $92F7 missileFlightTick := $92F8 droneArrivalFlags := $92F9 lastQuarterFlag := $92FA radarSweepTick := $92FB revealAllUnits := $92FC defaultMainMenuItem := $92FD soloTrainerAtGameStart := $92FE bitmapRowLoTable := $9300 bitmapRowLoTableFrom3 := $9303 bitmapRowHiTable := $9319 bitmapRowHiTableFrom3 := $931C bitmapColLoTable := $9332 bitmapColLoTableFrom3 := $9335 bitmapColHiTable := $935A bitmapColHiTableFrom3 := $935D screenRowLoTable := $9382 screenRowLoTableFrom3 := $9385 screenRowHiTable := $939B screenRowHiTableFrom3 := $939E fontColorTable := $93B4 titleColorRow00Cell16 := $9810 windowFrameTileSets := $9824 windowFrameTileSetRaised := $982D windowFrameTileSetOctagon := $9836 windowFrameTileSetPanel := $983F windowFrameTileSetSolid := $9848 windowFrameTileSetBlank := $9851 windowFrameTileSetDither := $985A windowFrameTileSetBevelA := $9863 windowFrameTileSetBevelB := $986C windowFrameTileSetBevelC := $9875 titleScreenRow16Col18 := $9E92 titleScreenRow16Col36 := $9EA4 titleScreenRow20Col02 := $9F22 titleScreenRow20Col23 := $9F37 titleScreenRow20Col33 := $9F41 titleScreenRow21Col03 := $9F4B titleScreenRow21Col13 := $9F55 titleScreenRow21Col23 := $9F5F mapRowAddrLoTable := $BFB0 mapRowAddrHiTable := $BFD8 bootEntry := $C000 bootReadSectorLoop := $C008 printChar := $C01A restoreRegsAXY := $C030 printNewline := $C041 bootDriveDataBit := $C06A bootWaitClkAck := $C075 setCursorRow := $C097 updateCursorPointers := $C099 setFullScreenWindow := $C0D7 saveRegsAXY := $C0E8 bootReceiveSectorData := $C0EF printTableEntry := $C100 printStringCentred := $C121 printNextStringCentred := $C12B printString := $C133 printStringAtPointer := $C137 printDecimal := $C17D D_C22B := $C22B plotGlyphDoubleSize := $C24B waitFrames := $C37F leftoverSetC397 := $C397 clearMessageLine := $C39F showMessageString := $C3D8 advanceFillPointer := $C3F9 restoreTextWindow := $C40E saveTextWindow := $C41B checkUiContinue := $C451 checkSetupUiContinue := $C470 updateStatusMessage := $C4EF queueMessageUnlessFilm := $C560 queueMessage := $C566 redrawStatusMessage := $C5D7 clearChatStatusLine := $C62A resetMessageQueue := $C6EF showStatusMessage := $C6FF handlePauseKey := $C70A handleTextEntryKey := $C7D5 showWorkingMessage := $C853 promptPressButtonWhenReady := $C85B exitIfGameEnded := $C86D playSound := $C89A requestSound := $C8A8 dequeueSoundRequest := $C8E9 updateComcenMoveSound := $C8FA setMessageSlot := $C924 unitPictureHiTable := $CD00 showUnitPictureSprites := $CD0A D_D900 := $D900 D_DA00 := $DA00 D_DB00 := $DB00 commJumpTable := $E000 commLinkControl := $E003 getLinkByte := $E006 putLinkByte := $E009 pollLinkStatus := $E00C isOriginateMode := $E011 D_E013 := $E013 D_E014 := $E014 commKeyEntry := $E015 exchangeFlags := $E01D rxPacketLength := $E01E txPacketLength := $E01F rxPacketBuffer := $E020 rxPacketArg1 := $E021 txPacketBuffer := $E027 txPacketArg1 := $E028 hostOutCount := $E02E commBuildId := $E030 nmiChainVector := $E031 nmiChainVectorHi := $E032 ciaIcrShadow := $E033 nmiSuspendRequest := $E039 nmiSuspendAck := $E03A isLinkActive := $E03B linkStatus := $E03C linkStatusSample := $E03D runTrainerRoundHook := $E079 trainerUnitStepHook := $E0E8 handleDisplayToggleKey := $EEB9 unitColTable := $F640 unitFlagsTableAlias := $F688 unitRowTable := $F6A4 unitTypeTableAlias := $F6EC unitFlagsTable := $F708 unitTypeTable := $F76C unitDestColTable := $F7D0 unitPathColTableAlias := $F818 unitDestRowTable := $F834 unitPathColTable := $F898 unitPathRowTable := $F8FC unitEnergyTableAlias := $F944 unitStunTable := $F960 unitEnergyTable := $F9C4 unitMovePointsTable := $FA28 unitTerrainUnderAlias := $FA70 unitHitFlashTable := $FA8C unitTerrainUnderTable := $FAF0 unitDisplayFlagsTable := $FB54 initLineStepper := $FBB8 stepLine := $FBFA multiply8x8 := $FCCD divideSigned16by8 := $FCEC nextScenarioRandom := $FD4A nextGameRandom := $FD5D ownPlayerName := $FFDE opponentPlayerName := $FFE8 nmiVector := $FFFA nmiVectorHi := $FFFB irqVector := $FFFE irqVectorHi := $FFFF ; Contents ; -------- ; $0800 gameEntry the game's front door. ; $0804 loadSectors the game's own copy of the fast loader. ; $0843 writeSectors the write half of the fast loader: sends A consecutive sectors from the address ; set by setLoadDest to track Y starting at sector X, one page per sector. ; $085C setLoadDest names the memory the next transfer works on. ; $0863 writeOneSector writes a single sector: command $60, then the track and the sector number, then ; the 256 bytes. ; $088F sendSectorData pushes the 256 bytes at (wrSrcPtr) to the drive one handshaked byte at a time, ; then falls into finishTransfer to read back the status byte. ; $089C sendByteToDrive the slow, fully handshaked, full-duplex byte exchange with the drive; the other ; end is exchangeByteWithHost ($034B) in the drive code. ; $08EB readOneSector reads one sector into the load destination. ; $08FE readSectorBody the body of a sector read. ; $0943 finishTransfer shared tail of a read and a write: puts the sprite enable register back the way ; it was and collects the drive's status byte, which arrives on the ordinary handshaked path. ; $0950 receiveSectorData the fast half of the protocol and the only cycle-counted code in the game. ; $09A6 manualLookupCheck the manual look-up copy protection, and it is disabled in this build: the JSR ; that called it at $0A9B was assembled as BIT ($2C instead of $20), so it is never entered. ; $0A6C mainStart the cold start of the game, run once. ; $0AA3 returnToMainMenu the panic exit back to the main menu, used whenever the game has to abandon ; whatever it was doing: the end of a game, a lost link, the SHIFT + C= + F7 abort in the raster IRQ, and the ; fall-through from mainStart. ; $0AC8 startGameFromSetup the hand-over from the setup screens to the battle, entered by JMP from the ; setup overlay ($7E3F) once both sides have agreed on a game. ; $0B4F loadCommTailBuild1 not a routine of its own: the tail of startGameFromSetup, entered with X/Y ; already set at $0B4B. ; $0BB5 initIrqAndInputState brings the interrupt and input state back to a known point. ; $0BD8 disableCiaInterrupts masks every interrupt source in both CIAs and acknowledges whatever was already ; pending. ; $0BE7 initInterruptVectors finishes the interrupt set-up: it points the BRK escape of the raster handler ; at the normal IRQ exit and parks both the hardware NMI vector and the comm module's private copy of it on the bare RTI ; at $1298. ; $0C02 setNmiVectorRti points the hardware NMI vector at the bare RTI at $1298. ; $0C0D jumpToCd00IfPatched dead code. ; $0C1C resetGameVariables wipes the game's variables back to a cold state: zero page $15-$7F and $8C-$FF, ; the whole variable block $9008-$92FF, the eight-entry message queue and the last-key bytes. ; $0C66 installRasterIrq installs rasterIrqHandler as the interrupt routine. ; $0C8C showModemSetupPrompt puts a caller-supplied line above 'PRESS SPACE WHEN READY.' and waits for the ; space bar. ; $0CA0 resetMessagesAndBeep clears the message queue, shows message slot $20 immediately, beeps twice a ; third of a second apart and makes slot $21 the line the status bar falls back to once slot $20 has had its time. ; $0CBD waitForAnswerOriginateKey the ANSWER/ORIGINATE question at the start of a modem game. ; $0CE8 buildOwnerNameString assembles the 21-character line that names the two players on the menu panel, ; out of the two 10-byte name fields at the top of memory: ownPlayerName ($FFDE), copied out of the disk's BAM by ; checkDiskId, and opponentPlayerName ($FFE8), which the modem driver fills in from the other machine. ; $0D49 setJoystickRepeatDelay the joystick auto-repeat used by every menu and by the map scrolling. ; $0DB7 scanKeyboard the game's own keyboard matrix scanner; there is no KERNAL to ask, since the ; ROMs are banked out. ; $0E45 testKeyMask tests one named key for scanKeyboard: it selects the row given in Y, reads the ; columns twice until two reads agree, and masks out the single column bit the caller passed in A. ; $0E56 pollKeyboardEvent the keyboard front end, run once per frame from the raster IRQ (and only from ; there). ; $0EB1 checkFirePressed the game's 'has the player clicked?' primitive; almost every interactive loop ; spins on it with BNE. ; $0ED5 checkFirePressedInteractive checkFirePressed plus a 'may the player act at all?' gate, used by the loops ; that turn a click into an order. ; $0EE5 isPlayerInputAllowed the gate half of checkFirePressedInteractive without the button test: it jumps ; into that routine at $0EDA with A = 0, so it answers 'may the local player still act?' - Z=1 when no film is being ; watched and the game has not ended. ; $0EEA isFireHeld the level-triggered version of checkFirePressed: no ring-buffer check, no ; debounce reload and no click sound, so it keeps answering 0 for as long as the button is held (once it has been seen ; released). ; $0EFE stopAllSound Silences every SID voice by requesting sound id 0, which requestSound turns ; into ; $0F03 loadStartupOverlays One-shot start-up loader: fast-loads track 30 sectors 0-15 into $C000-$CFFF ; $0F16 loadCommModule Loads the opponent module behind the $E000 jump table and initialises it. ; $0F31 loadCommBuild2 Loads the modem-driver build of the comm module with interrupts off. ; $0F41 loadCommFinish Shared tail of the two comm-module load paths: does the final loadSectors with ; the ; $0F44 initCommModule Cold-initialises the freshly loaded comm module and grafts the game's own ; keyboard ; $0F55 loadSectorsDelayed Wrapper around loadSectors that first burns 8 raster frames through the $C000 ; $0F5F suspendCommModule Asks the comm module to pause before the game touches the disk: the fast loader ; $0F71 resumeCommModule Counterpart of suspendCommModule: when the link is up it clears the request ; byte ; $0FB1 checkDiskId Verifies which disk is in the drive. ; $1018 diskErrorExit Loads the address of msgDiskError ('DISK ERROR.') and falls into ; $101C showDiskMessageAndFail Common failure exit of the disk routines: error beep, put the message on ; $1030 ensureOverlayAResident Makes sure the $6F00-$87FF overlay variant A (map generator, setup and menu ; $1047 loadOverlayA Reads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00-$7FFF ; and ; $1060 loadStringsAndCommTail Reads the modem status-message string block (track 29 sectors 9-10) into ; $1070 loadCommTailBuild2 Reads the last page group of the modem driver (track 34 sectors 12-15) into ; $1080 loadOverlayBIfNotResident Loads the $6F00 overlay variant B only when it is not already in memory. ; $1086 loadOverlayB Reads the $6F00-$87FF overlay variant B (the comcen missile and drone screens): ; track ; $10AB loadUnitTemplatesAndSetup Reads track 18 sectors 8-14 into $F000-$F6FF, then falls into ; $10BB loadSetupBlock Reads the saved setup block (track 18 sectors 15-16) into $0200-$03FF: the ; terrain ; $10CB suspendCommForDiskAccess Standard prologue for disk access: pause the comm module, then set ; $10D1 resumeCommAfterDiskAccess Epilogue for disk access: restart the comm module, forget the persistent ; $10DC waitForGameDiskInserted Blocks until the OZ game disk is in the drive. ; $10F1 rasterIrqHandler The game's only interrupt handler, entered through the RAM vector $FFFE at ; raster ; $1293 irqExit Pulls Y, X and A back off the stack and falls into irqRti. ; $1298 irqRti A bare RTI. ; $129C updateFrameTimers Per-frame housekeeping called from the raster IRQ. ; $130A stepRandomSeed Called from the IRQ every frame. ; $131B blinkCursorColour Blinks the map cursor. ; $1331 handleChatSend The outgoing half of the modem chat ('message to opponent'), run every frame ; from ; $1399 receiveChatCharacter The incoming half of the chat, called every frame from the IRQ. ; $13C4 appendChatCharacter Appends character A to the 36-byte incoming message buffer at $9253 and ; $1414 resumeMainMenuScreen Re-entry into the console screen (screen 3, the F7 screen with the STATS / ; $141D enterMainMenu Full entry into the console screen (screen 3): picks the default tab, draws the ; $1439 mainMenuLoop The console screen's main loop (screen 3, tabs STATS / REPAIR / MISC). ; $14F0 snapshotComcenStatus Remembers what the REPAIR tab is currently showing: copies the three comcen ; $1507 selectDefaultTab Chooses which console tab should be showing. ; $153B showMainMenu Builds the console screen from scratch: resets the glyph plotter's parameter ; block, ; $1565 drawMenuFrame Draws the console's two nested frames from the predefined window table: window ; $18 ; $156F clearMenuTextWindow Sets the console panel's text colour and window bounds and blanks the window. ; $1575 setMenuTextWindow Picks the console text colour from the comcen stun state (red on the REPAIR tab ; $1578 setMenuWindowBounds Sets the text window that the console panels print into: rows 7 to 21, left ; $158F clearMenuWindowPlain Blanks the whole console-menu panel with text colour 0 (black text, no ; highlight EOR mask). ; $1599 drawGameSetupSummary Draws the fixed header of the console STATS tab (menu tab 0): the rules word ; STANDARD/CUSTOM, the game-type name, an optional '. ; $1643 drawMenuTabBar Draws the three console tabs of screen 3: prints the labels STATS, REPAIR and ; MISC on row 3 starting at column 10, each in its own colour, then paints the 2x2 tab box behind each of them in its ; normal colour, and finally resets the bitmap/colour draw modes to 'replace'. ; $168C setTabHighlightColour Redraws console tab X in its bright colour: looks the colour up in ; tabHighlightColourTable and joins setTabNormalColour's common tail. ; $1691 setTabNormalColour Redraws console tab X: stores the chosen colour and the tab index into the two ; self-modified operands, sets the character-block origin to row 1 / tabColumnTable[X], draws the 2x2 tab-box shape from ; $9804 and then fills those 2x2 screen-RAM colour cells with the tab colour. ; $16B7 blinkSelectedTab Called once per pass of mainMenuLoop. ; $16D6 printEnergyLabel Prints the fixed word 'ENERGY' of the REPAIR tab at window column $1C, row 8 ; (absolute column 30 inside the menu panel whose left edge is 2), in the current menu text colour. ; $16EB setMenuTextColour Chooses the text colour byte for the console menu screen: normally $61 (white ; text, highlight EOR mask 6 matching the blue panel), but $21 while the REPAIR tab is open and our own comcen is ; stunned, because drawRepairTab then paints the panel red instead of blue. ; $16FF printEnergyValue Prints the comcen's energy as a percentage at column $22, row 9 of the REPAIR ; tab, right-aligned in a three-character field, but only when the value has changed since the last call. ; $175A drawRepairTab Builds the whole REPAIR tab: clears the panel in blue ($66) or, if our comcen ; is stunned, red ($22); unpacks the comcen schematic picture into the bitmap; paints a 20x10 colour block behind it; ; resets the blink and energy bookkeeping; draws the four system markers (only when not stunned); prints the ENERGY ; label and value; and, when stunned, prints 'COMCEN STUNNED!'. ; $17DA repairTabAction Fire-button handler of the REPAIR tab (entry 1 of menuItemHandlerTable at ; $1535, reached through the self-modified JSR at $148D). ; $188B toggleSystemMarker One blink step for the marker of the comcen system currently highlighted in the ; REPAIR WHICH menu: flips blinkPhase and then either draws the marker (new phase 0) or erases it with the schematic ; background colour $16 (new phase 1). ; $189D drawSystemMarker Draws the status marker of comcen system Y on the REPAIR tab schematic. ; $1919 eraseSystemMarker Fills the whole 3x3 colour-cell block of system Y's marker with the colour byte ; in X. ; $192D sendEnergyRepairCommand Queues the 3-byte command $9A (cmdAddEnergy) with the comcen's unit index and ; the repaired amount, so that both machines add the same energy to that unit. ; $193C attemptSystemRepair Resolves one repair attempt on the comcen system highlighted in the REPAIR ; WHICH menu. ; $19B0 blinkRepairMarker Blinks the marker of the selected comcen system for about one second while a ; repair attempt resolves. ; $19C9 printAttemptingRepair Positions the REPAIR panel's own message row (window column 10, screen column ; 12, screen row 20) and prints 'ATTEMPTING REPAIR...' there, then restores the text window the caller had. ; $19D6 clearRepairMessage Erases the REPAIR panel's message row by printing 20 copies of the blank glyph ; zp_3E over it, then restores the caller's text window. ; $19E1 setRepairMessageWindow Shared prologue of printAttemptingRepair and clearRepairMessage: saves the ; caller's 12-byte text state, picks the menu text colour, applies the menu panel geometry (left column 2, width $24, ; rows 7..$16) and places the cursor at window column 10 = screen column 12 of screen row 20. ; $19F3 printOptionsList Prints the body of the MISC tab. ; $1A49 miscTabAction Fire-button handler of the MISC tab (entry 2 of menuItemHandlerTable at $1535) ; and, through the JMP at $0AC5, the top of the whole main-menu loop at cold start. ; $1ADC endGameToMainMenu Tears a finished game session down and prepares the main menu. ; $1B33 hangUpModem Convenience entry: hang up and leave linkSessionState at 0 (no session). ; $1B35 hangUpModemSetState Drops the modem link and stores A as the new linkSessionState. ; $1B5F openCommLink Brings the link up if it is not already. ; $1B74 openLinkAndWaitForConnection openCommLink followed by waitForConnection (fall-through). ; $1B77 waitForConnection On the modem build, pins the status message 'WAITING FOR CONNECTION...', spins ; in waitForCarrier until the modem reports carrier, then clears the voice-pause bit of pauseState and blanks the ; message line. ; $1B9A waitForCarrier Polling loop that runs until the comm module reports carrier (bit 6 of the ; debounced carrier state $E03C). ; $1BC1 showMessageWaitForSpace Pins status message A on the message line and blocks until the player presses ; the space bar. ; $1BF7 voicePauseReconnect The VOICE PAUSE round trip: hang up so both players can talk on the phone, wait ; five seconds, then bring the line back up and wait for carrier. ; $1C08 hangUpAndPause Hangs up for a voice pause and then waits about five seconds. ; $1C17 disconnectFromOpponent Ends a link session cleanly: tells the opponent with command $8C reason 5 ; (disconnect), pushes the packet out immediately and hangs up. ; $1C2C runMainMenu Runs the main menu whose text printOptionsList has just drawn: five items, or ; six when a link session is up and DISCONNECT/SET VOICE is offered. ; $1C7B isAsymmetricGameType Tests the game type held in gameTypeOptions bits 0-2 and returns Z=1 for ; $1C87 restoreSettingsAfterFilm Undoes what WATCH GAME FILM overwrote. ; $1CC1 setViewVerticalLimit Sets viewRowLimit, the exclusive map row limit the centre cell of the ; $1CD3 scrollViewBy Moves the 7x5-cell tactical view by one map cell: X = column delta, Y = row ; delta, ; $1D4E scrollViewToUnit Scrolls the battlefield view until unit X sits under the centre cursor, one map ; $1D97 setLineTargetToUnit Points the line stepper's end point at unit X. ; $1DE4 scrollViewBitmap Shifts the whole 7x5-cell tactical view one map cell (16 pixels = 2 characters) ; $1F4E keepGroupBoxInsideMap Nudges the view origin one cell at a time until the whole group formation ; $1FC3 enterMapScreen Entry point of the battlefield screen (game mode 0). ; $1FD0 runMapMainLoop The interactive loop of the battlefield screen and of the unit-placement phase. ; $20F4 scrollViewportByDirection Turns a direction code 0-7 into a column/row step and scrolls the view ; $2185 runTargetCursorLoop Modal loop that lets the player drive the cursor to a cell and then acts on ; $2264 previewPathToCursor Idle-time animation: while the player leaves the controls alone with a move ; $22A2 showUnitInfoPanel Fills the info panel under the tactical view (text window rows 12-20, columns ; $2367 checkUserInterrupt The 'may this passive display keep running?' test used by the idle loops of the ; info panel and the path preview. ; $2384 printUnitStatusLines Fills in the value column of the unit info panel for selectedUnit ($90F8): the ; group name on row 13, the energy percentage on row 17, the weapon state on row 18, the action on row 19 and one flag ; word (BLITZ / CLOAKED / DUG IN) on row 20, then adds up to two '*' zone markers in front of the unit name on row 12. ; $24E4 animatePathMarkerToDestination Sets the line-stepper target to the ordered destination of selectedUnit and ; falls into animatePathMarkerStep, so the mini-map marker crawls from the unit towards where it has been sent. ; $24F3 animatePathMarkerStep Advances the mini-map path marker by one cell every 4 frames, but only after ; the player has been idle for 90 frames. ; $2540 resetPathMarker Puts the path marker back at the start: sprite 1 off, marker cell and pass ; counter back to the selected unit, and sprite 1's shape reloaded. ; $256F resetPathMarkerToUnit Reloads the marker's pass counter with 5 and moves the marker cell onto the ; selected unit's map position, then falls into positionMarkerSprite to place sprite 1 there. ; $2585 positionMarkerSprite Converts a map cell to mini-map sprite coordinates (column*4+$1F, row*4+$39, or ; the group-box mapping when the mini-map is zoomed) and stores them in sprite 1's position shadows, which the raster ; IRQ copies to the VIC. ; $258F recenterViewportOnUnit Makes sure the selected unit sits under the battlefield cursor. ; $25B7 consumeSpaceKey Eats a pending SPACE. ; $25C4 callSetupOverlayHook Trampoline into overlay variant A at $8658 (handleStarKeyToggle), the per-frame ; '*' key handler of the non-battle phases: it toggles setupToggleFlag $92F3 bit 7 (DESTROY mode during unit placement, ; CUSTOM rules on the setup screen) and reports C=1 when it consumed the key. ; $25C7 getViewportOriginPointer Builds a map pointer for the top-left cell of the 7x5 battlefield view. ; $25E3 getViewportRowPointer Map pointer for the cell (viewOriginCol, Y). ; $25FA readMapCell The map accessor of the whole game: forms mapCellPtr = rowAddress[Y] + X from ; the 40-entry row address tables at $BFB0/$BFD8 (built by the start-up code at $0339 as $F000 + 40*row) and returns the ; byte of cell (column X, row Y) of the 40x40 battlefield. ; $260D readCellUnderCursor Reads the map cell under the battlefield cursor. ; $261C clearInfoPanel Selects the unit info panel as the text window and blanks it with the text ; engine's window clear, using the current textColour zp_3D and fill glyph zp_3E. ; $2622 setInfoPanelWindow Defines the text window that covers the unit info panel: columns 25-38, rows 12 ; to 20 (row 21 is the exclusive limit). ; $2636 clearScreenAndRedrawGameScreen Rebuilds the battlefield display from scratch: sets the VIC up for the ; multicolour bitmap and wipes it grey, then falls into redrawGameScreen. ; $2639 redrawGameScreen Draws the whole battlefield screen: the three panel frames (mini-map, tactical ; view, info panel), the bottom status bar, the 40x40 overview map, the 7x5 tactical view and finally the current status ; message. ; $268F drawMiniMapFrame Draws window 0: the 22x22 character frame at column 0, row 0 that surrounds the ; 20x20 strategic mini-map. ; $2698 drawViewportFrame Draws window 1: the 16x12 character frame at column 24, row 0 around the ; tactical view, then falls into clearViewportTextArea to blank the interior. ; $26A1 clearViewportTextArea Declares the interior of the tactical view (rows 1-10, columns 25-38, i.e. ; $26B8 drawInfoPanelFrame Draws window 2: the 16x11 character frame at column 24, row 11 around the unit ; info panel. ; $26C1 drawViewport Redraws the tactical view: 5 rows of 7 map cells, each cell a 16x16 pixel ; multicolour tile occupying 2x2 character cells, into character rows 1-10 and columns 25-38. ; $2772 renderMapTile Builds the 16x16 pixel multicolour tile of one map cell in the 32-byte buffer ; at $04C8-$04E7 and writes the cell's four screen-RAM colour bytes. ; $28E1 drawMainWindowFrame Draws the frame of the bottom bar: window 3 on the battlefield screen, window 4 ; on every other screen. ; $28F1 computeGameResult Turns the state at the end of a game into the two final point totals and the ; result message. ; $29B3 setOtherPlayerPoints Writes Y as the 16-bit point total of the side OTHER than X and returns with X ; $29C1 shakeScreenScroll One frame of the screen-shake effect used when a comcen is hit. ; $2A08 patchUnitSideBranches Re-points the two 'is this unit mine?' branches at the viewpoint currently ; $2A1A printOptionWord Prints one of the three 4-byte DAMAGE option words held at $0652 in the message ; $2A38 setBitmapDrawMode Chooses how a glyph's eight bitmap bytes are combined with what is already in ; the ; $2A47 setColourDrawMode The colour-plane twin of setBitmapDrawMode: chooses how drawColourByte is ; combined ; $2A56 resetDrawModes Puts both planes back to mode 0 (plain replace) after a highlight or overlay ; effect. ; $2A64 fillScreenColourRect Paints an X columns by Y rows rectangle of the multicolour bitmap's colour ; cells ; $2A9F calcCellPointers Loads the character cell coordinates from scratch18 (row) and scratch19 ; (column) and ; $2AA3 calcCellPointersXY Turns a character cell (Y = row, X = column) into the two pointers every ; plotter ; $2AC3 drawCharAt Draws character A at screen row scratch18, column scratch19: computes the cell ; pointers and ; $2AC8 plotGlyph Draws one 8x8 glyph of the game's own font (114 glyphs at $9426, 8 bytes each) ; into the ; $2B18 drawCharBlock Draws a rectangular block of characters at windowOriginRow/windowOriginCol. ; $2B75 drawWindowInnerCorners Draws the small hollow-ring glyph $38 (font bytes at $95E6) in the four cells ; $2BB7 drawTallGlyph Draws one of the ten two-cell-tall symbols whose character-code pairs live at ; $9810 ; $2BCE xorColourBlock2x2 Flashes one battlefield tile by EORing the colour byte A into the 2x2 ; screen-RAM ; $2C7F drawWindowFrame Draws one of the 27 predefined windows selected by X. ; $2CA1 drawFrameBox Draws a rectangular frame from a 9-byte tile set at ; windowOriginRow/windowOriginCol, ; $2D1D drawFrameColumn Repeats the tile at offset Y of the current tile set down column scratch19, ; starting ; $2D2F drawFrameRow Repeats the tile at offset Y of the current tile set along row scratch18, ; starting at ; $2D41 initBitmapScreen Builds the game's display from scratch: a full-screen text window, grey border ; and ; $2DBD setTerrainTilePtr Points glyphPtr at the 8-byte terrain tile definition number A in the table at ; $2DDB clearMessageLineAndScreen Wipes the status/message line first, then falls into ; $2DDE clearCurrentScreenWindows Re-clears the content areas of whichever screen is active, dispatching on ; $2E0F magnifyWindow2x Redraws the character rectangle at windowOriginRow/windowOriginCol, frameWidth ; by ; $2F34 setMagnifyCellPointers Helper of magnifyWindow2x. ; $2F6A initSpritePointers Rewrites the eight sprite pointers in the last bytes of the video matrix, ; $2FA5 clearAllSpriteData Zeroes $8A00-$8BFF, the 64-byte shape blocks of all eight sprites, in one ; 256-pass ; $2FB2 clearSpriteShape Zeroes the 64-byte shape of sprite A at $8A00 + A*64. ; $2FCF setCursorShapeSquare Gives sprite 0, the map cursor, the 18-byte (6 rows of 24 pixels) hollow square ; $2FD3 installCursorShape Shared tail of setCursorShapeSquare and setCursorShapeCross: copies the 18-byte ; $2FF3 setCursorShapeCross The same as setCursorShapeSquare but installs the thick plus/cross shape at ; $9E80 ; $2FFA setCursorShapeBlock Loads the small 4x4 solid block at $9E92 into the sprite named by ; $3023 mapCellToSpriteXY Converts a battlefield cell (X = column 0-39, Y = row 0-39) into the sprite ; $303A mapCellToSpriteXYRaw The plain cell-to-sprite conversion for the overview map: spriteX = ; $3049 positionCursorSprite Puts the sprite named by activeSpriteIndex ($91CC) over the cursor cell of ; $306C drawLineInPathSprite Draws a straight line into the shape of sprite 3 ($8AC0), which is the game's ; $308A plotPathCross Marks the end of a path/shot line by plotting the four orthogonal neighbours of ; the ; $30A1 plotPathSpritePixel Sets one pixel in the shape of sprite 3 at $8AC0. ; $30C0 buildGroupFormationSprite Builds the 'formation ghost' shown while a whole group is being ordered ; $31A3 enableSprites ORs the sprite mask in A into spriteEnableShadow ($9022), the shadow the raster ; IRQ ; $31AF plotSpritePixel Sets, clears or toggles one pixel of a sprite shape. ; $31CA setSpritePixel Entry point of plotSpritePixel that forces the ORA (set) operation. ; $3239 drawCircleInSprite Draws a circle of radius X (1-10) centred on pixel (12,10) - the middle of a ; $3266 computeCircleOctantPoints Expands one octant point (dx,dy) of a circle into the coordinates of all ; $32BF plotCircleOctantPoints Plots the eight mirrored points prepared by computeCircleOctantPoints into ; $32EA hideSprite1 Clears bit 1 of the sprite enable shadow and writes the result straight to ; $32F6 hideSprite0Dead Dead code: a byte-for-byte copy of hideSprite1 that clears bit 0 (sprite 0) ; $3302 showCrosshairSprite Loads the 63-byte diamond crosshair (the second shape of the pair at $9EA4, ; $3306 showCursorBoxSprite Same as showCrosshairSprite but copies the first shape of the pair ($9EA4- ; $3340 loadDirectionShapeSprite Copies sprite shape number A (0-7, one per facing direction) from the ; $3383 showComcenCrossMarker Puts the small cross marker at $9F22 into rows 7-13 of sprite 2, colours it ; $33B1 gridCellToSpriteCoords Converts a cell of overlay B's 21x17 radar grid (one map cell per 8x8 ; $33C8 showExplosionMarker Shows the explosion marker for a weapon effect at map cell (X,Y). ; $3405 startExplosionMarkerTimer Two-instruction tail shared by both paths of showExplosionMarker: arms ; $340B getUnitSpriteCentreOnGrid Maps a battlefield cell onto overlay B's radar grid and returns the ; $341F updateExplosionMarker Keeps the explosion marker (sprite 4) on the right cell of the drone camera ; $3456 drawComcenRingSprite Builds the comcen marker of the drone screen: two concentric rings (radius 2 ; $3458 drawRingsInSprite4 Shared body of drawComcenRingSprite: clears sprite 4 and draws A concentric ; $347F orderSelectedUnitToCursor Issues a MOVE order for the selected unit to the cell under the cursor: ; $34A4 updateUnitAtDestinationFlag Sets bit 7 of unitDisplayFlags ($FB54) for the unit in cmdParam0 when ; $34C6 applyMoveOrder Executor of command $80 MOVE (unit, column, row). ; $34D5 setUnitDestination Stores a destination for unit Y. ; $355D clampMapCoord Clamps a signed map coordinate to the battlefield: negative becomes 0, anything ; from ; $3568 markAllUnitsVisible Sets bits 6 and 7 (spotted this cycle, currently visible) of unitEnergy for ; $3576 ageUnitVisibility Run once per unit-processing cycle, before the spotting passes: every unit that ; $35C3 scanVisibleEnemies Builds the list of enemy units that unit Y can see (X bit 7 clear, the spotting ; $3746 checkLineOfSightStep Feeds the terrain class of one more cell into the running line-of-sight test. ; $375D initLineBetweenUnits Points the line walker at unit Y's cell and falls into initLineFromUnit, which ; sets the start to unit X's cell. ; $3767 initLineFromUnit Sets the line walker start to unit X's cell, runs initLineStepper ($FBB8) and ; reports the principal direction of the line: 0 = north or 1 = south for a row-major line, 2 = east or 3 = west for a ; column-major one. ; $378B setScanFilterOppositeSide Configures the unit filter of collectUnitsInRadius so that only units of the ; side opposite to unit Y are collected, by writing the compare value and the branch opcode of the test at $37FE/$3800. ; $379E collectAllUnitsInRadiusDead Unreferenced alternate entry of collectUnitsInRadius that turns the side filter ; into 'accept every unit' (compare against unit 0 with a BCC) before falling into the scan. ; $37AA collectUnitsInRadius Scans the square of half-width A around map cell (X,Y), clipped to the 40x40 ; battlefield, and lists every cell holding a unit that passes the side filter patched by setScanFilterOppositeSide: the ; unit index goes into $0400[n] and its squared distance from the centre into $0432[n]. ; $3870 distanceSquared Squared distance between two map cells: |dCol|^2 + |dRow|^2 taken from ; squaresTable. ; $38C2 resolveAttack Resolves one shot of unit X at unit Y. ; $3A5A drawShotLineSprite Draws the shot on the 4-pixel-per-cell overview map using hardware sprite 3. ; $3B22 applyHitToUnit Takes A points of damage off unit Y and deals with everything that follows. ; $3B52 applyHitToUnitNoAlert Alternate entry of applyHitToUnit that skips the block above, i.e. ; $3BDF reduceUnitEnergyBy Stores A as the damage in hitDamage $9240 and falls into reduceUnitEnergy. ; $3BE2 reduceUnitEnergy Subtracts hitDamage $9240 from the energy of unit Y, keeping the two visibility ; flags in bits 6-7 and stopping at 0. ; $3BFC recordUnitHit Records the visible effect of a hit on unit Y: the damage, capped at 20, is ; stored as $80|damage in the hit-effect byte $FA8C that drives the flash and the PINNED status word, the same number of ; movement points is taken off the signed counter $FA28 (clamped at -127), and bit 1 of the display flags asks for the ; console entry to be refreshed. ; $3C20 orderAttackUnitAtCursor Fire-button action of the attack cursor. ; $3C50 setBoomerTargetCmd Execution half of command $87 SET TARGET. ; $3C5D countBoomersBelow Counts the BOOMERs (type 2) with an index below X, which is the ordinal of ; boomer X among all boomers and therefore its slot in boomerTargetTable. ; $3C74 getBoomerTarget Reads the target lock of unit X. ; $3C85 selectBoomerTarget Picks what boomer Y shoots at this round. ; $3CF2 animateRingBurst Expanding-then-contracting ring drawn into hardware sprite 1. ; $3D57 rollComcenSystemDamage Rolls for collateral damage to the local comcen's three repairable systems ; after it has been hit. ; $3D8A initAllUnits Resets all 100 unit records from their start positions and puts every living ; unit back on the battlefield map, then clears the 16 per-group state bytes. ; $3DA0 initUnitRecord Resets the order state of one unit: its path position and its destination are ; snapped to the cell it stands on, the destination is marked 'idle', the zone-scored bits and the stun counter are ; cleared, the group membership is kept and the facing and side bits are rebuilt. ; $3DDA placeUnitOnMap Second half of the unit reset: restores the dug-in bit from the type byte, ; gives the unit a fresh movement allowance and full energy, copies the stationary and group bits into the display ; flags, remembers the terrain of the cell it stands on and writes $80|index into that map cell. ; $3E23 recycleDeadUnits Once per round each side that owns a recycler gets one destroyed robot back. ; $3EBD removeUnitFromMap Lifts unit X off the battlefield map: the terrain byte the unit was hiding is ; written back into its cell, the unit is marked as 'not on the map' (unitTerrainUnderTable = $FF) and its ; hit-flash/pinned countdown is cleared. ; $3EC2 removeUnitLeavingCell Same as removeUnitFromMap but the caller chooses what stays behind in the ; unit's cell (A) instead of the saved terrain. ; $3EE2 applyBlitzStepCost Charges a blitzing unit for the step it has just taken: it loses one energy ; point and gains 25 movement points (SET BLITZ = 'move extra fast'), and its next shot gets the charge bonus ; (unitFlagsTable bit 7). ; $3F27 drawRecyclerOnMap Puts the recycler glyph ($67) on the battlefield map at side Y's recycler ; position and hands the caller everything drawMapCell needs to show it. ; $3F57 checkEnemyRecyclerSpotted Reveals the opponent's recycler the first time one of our own uncloaked robots ; gets within 3 cells of it: it draws the recycler glyph on the map, queues message 8 'SPOTTED RECYCLER!' and then ; patches its own first byte to RTS so the test never runs again. ; $3F97 initialFacingForUnit Returns the facing a unit starts with: 0 (up, towards the enemy) for the local ; player's units and 1 (down) for the opponent's, since each player sees his own side at the bottom of the map. ; $3FA3 mirrorMapCoordinate Returns 39 - A, mirroring a map column or row across the 40x40 battlefield. ; $3FAC haltStunnedUnit Cancels the orders of a unit that is stunned (a drone hit stuns everything ; around it): its waypoint and destination are snapped back to the cell it stands on, it is marked stationary and it is ; dropped out of its group so the group does not wait for it. ; $3FE8 haltAllUnitsAndReveal Game-over clean-up: repairs the three comcen system status bytes, stops all 100 ; units (stationary, nothing fired, nothing pinned, waypoint snapped back onto the unit) and reveals the whole board by ; marking every unit as spotted. ; $4027 endGame Ends the battle once the caller has stored the reason in gameEndReason $0BA8 ; ($08 = the clock ran out, $A0/$C0 = one side's comcen was knocked out). ; $403B markComcenRevealed Launching a radar sweep or a drone gives the launching comcen away: it sets ; that side's comcen-reveal timer to 6 rounds and forces the visibility bits of that comcen so the other player can see ; it on the map. ; $406F waitFramesOrInput Interruptible pause that keeps the battle running: it advances the game one ; frame at a time for up to A frames and returns early on a fresh fire-button press, on the joystick leaving its centre ; position, or when the game-end countdown starts. ; $4094 runGameFrames Advances the game by A frames: each pass waits for one raster frame and then ; runs one updateGameFrame. ; $40B2 updateGameFrame The heartbeat of the game, called once per frame from every main and ; user-interface loop. ; $4213 processUnitStep Processes exactly one unit of the round order; this is an internal entry of ; updateGameFrame reached by the branch at $4206, not by a JSR, and it ends by falling into the shared tail at L_4537 ; which bumps unitOrderIndex. ; $4559 tryStepTowardTarget walks the line stepper one cell from the current cell ($93,$92) toward the ; $45E6 finishRound end of a round, reached by JMP from updateGameFrame ($4210) once all 100 units ; have ; $4751 moveUnitToCell puts currentUnit into the cell ($93,$92) that tryStepTowardTarget just ; accepted: ; $47ED shuffleUnitOrder exchanges two randomly chosen entries of unitBaseOrder ($9164, the 100 unit ; $4805 randomUnitIndex returns a unit index 0-99 drawn uniformly from the scenario random generator: ; $480F followDirectedUnit keeps the battlefield view locked on the unit the console is following. ; $4860 updateHitFlashTimers ages the hit markers of all 100 units. ; $48A2 drawUnitAtPosition redraws unit Y where it stands: builds the map byte $80|unit and falls into ; $48A5 drawValueAtUnit draws cell value A at the position of unit Y. ; $48B1 updateIdleFlag recomputes bit 7 of unitDestCol ($F7D0) for currentUnit: it is set when the ; $48DD rebuildGroupTables recomputes the 16 entries of groupStatusTable ($91F4), one per group key ; $4987 averageGroupMoveVector averages the outstanding movement of one group. ; $4A02 followGroupMovement Gives the unit that is joining a group the movement the rest of the group ; already has: averageGroupMoveVector ($4987) returns the average (destination - position) vector of the members and the ; unit's new destination becomes its own cell plus that vector. ; $4A35 sendGroupMoveToCursor Fire-button action of the battlefield target cursor while a whole group is ; selected (uiLevel $9209 >= 2). ; $4A52 moveGroupKeepingFormation Executor of command $81, run in lock step on both machines. ; $4AA3 addSelectedUnitToGroup The 'ADDED' half of the group membership editor. ; $4AD4 joinUnitToGroup Executor of command $88. ; $4AF2 removeSelectedUnitFromGroup The 'REMOVED' half of the membership editor. ; $4B04 isPointWithinGroupRange Range test that stops a group from spreading over more than 8 cells. ; $4B5E computeGroupBounds Scans all 100 unit slots and accumulates the member count and the bounding box ; of one group. ; $4BBF showTooFarMessage Error feedback shared by the two group range checks: selects and clears the ; 14-column info panel (screen columns 25-38, rows 12-21), plays sound 3 (the rejection beep) and prints 'TOO FAR!' from ; the $C000 text overlay on screen row 16, in whatever colour the caller left in textColour. ; $4BD3 orderGroupNewPosition NEW POSITION action of the custom-formation editor. ; $4C59 setGroupMemberDestination Executor of command $98. ; $4C6B syncLocalGroupFlags Copies the authoritative group bits of unit X (unitFlagsTable & $7C: group ; number, side and member bit) into the local flag copy unitDisplayFlagsTable, keeping that byte's own bits 0, 1 and 7. ; $4C84 rotateGroupFormation Executor of command $A3 - the SWEEP LEFT / SWEEP RIGHT / REVERSE items of the ; FORMATION menu. ; $4D9F getCommandLength Looks the total length (command byte plus its parameters) of command A up in ; commandLengthTable and leaves it in currentCommandLength as well as in A. ; $4DA9 serviceCommandExchange Per-frame entry point of the lock-step command exchange, called from the main ; loop of every screen and from the overlays. ; $4DB7 processCommandExchange The heart of the lock-step protocol. ; $4E91 executeTurnCommands Runs every command of one exchange. ; $4F05 mergeIncomingPacket Appends the packet just received from the opponent ($E020, $E01E bytes) to the ; merged command buffer, marking the block as remote. ; $4F5E advancePacketCursor Helper of both packet mergers: keeps the command byte in A, looks its length up ; and leaves packetReadIndex pointing at the next command in the packet. ; $4F6B mergeOutgoingPacket Appends the copy of the packet this machine just sent ($E027, $E01F bytes) to ; the merged buffer, marking the block as own, so the local commands are executed at exactly the same point in the ; sequence as on the opponent's machine. ; $4FC4 readCmdArgs3 Argument reader for the 4-byte commands $80, $81, $8A, $8B, $98, $9C and $A6: ; takes the first parameter into cmdParam0 and falls through into readCmdArgs2, which picks up the other two. ; $4FD0 readCmdArgs2 Argument reader for the 3-byte commands $87, $88, $8E, $90, $9A, $A3 and $A5, ; and the tail of readCmdArgs3. ; $4FE4 mirrorCmdCoordsIfRemote Point mirror for the map coordinates carried by a command. ; $4FF7 readCmdArg1 Argument reader for all the 2-byte commands - the per-unit and per-group orders ; $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2 and $A4. ; $5005 mirrorCmdCoordsIfRemoteDec Coordinate mirror for the drone, which covers a 2x2 block of cells and is ; addressed by its top-left corner. ; $5011 cmdMoveUnit Handler of command $80 MOVE (4 bytes: unit, column, row), the ordinary move ; order given by clicking a cell with a single unit selected. ; $501A cmdMoveGroupRelative Handler of command $81 (4 bytes: unit, column, row) - the move order given ; while a whole group is selected. ; $5023 cmdCloakOn Handler of command $82 CLOAK ON (2 bytes: unit). ; $504B cmdCloakOff Handler of command $84 CLOAK OFF (2 bytes: unit); also used as a subroutine by ; cmdBlitzOn. ; $5062 cmdBlitzOff Handler of command $95 BLITZ OFF (2 bytes: unit); also used as a subroutine by ; cmdCloakOn. ; $5070 cmdBlitzOn Handler of command $93 BLITZ ON (2 bytes: unit), the mirror image of ; cmdCloakOn. ; $5098 cmdGroupCloakOn Handler of command $83 (2 bytes: group key). ; $50AA cmdGroupBlitzOn Handler of command $94 (2 bytes: group key). ; $50BE cmdGroupBlitzOff Handler of command $96 (2 bytes: group key) and, at $50C1, the blitz half of ; the patch block that configures the shared group routine. ; $50D2 cmdGroupCloakOff Handler of command $85 (2 bytes: group key) and, at $50D4, the cloak half of ; the patch block. ; $50EB applyGroupCloakOrBlitz The shared, heavily self-modified body of the four group cloak/blitz handlers; ; it is only ever reached by branching, never by JSR. ; $5151 updateComcenCloakLock The common tail of every cloak and blitz handler and the only writer of ; comcenCloakedFlag: it copies bit 7 of the local player's own comcen entry in unitPathColTable, so $9245 bit 7 is set ; exactly while that comcen is cloaked. ; $515D cmdClearTarget Handler of command $86 CLEAR TARGET (2 bytes: BOOMER unit). ; $516B cmdSetTarget Handler of command $87 SET TARGET (3 bytes: BOOMER unit, target unit). ; $5185 cmdJoinGroup Handler of command $88 JOIN GROUP (3 bytes: unit, group byte). ; $519B cmdLeaveGroup Handler of command $89 (2 bytes: unit): clears bit 6 ('in a group') of the ; unit's ; $51AB cmdPlaceUnit Handler of command $8A (4 bytes: unit, column, row): teleports a unit during ; the deployment ; $51EA cmdSetCustomMapSeed Handler of command $A6 (4 bytes: three seed bytes): sets mapFlipFlag ($0B9D) = ; 1 and ; $51EE cmdSetMapSeed Handler of command $8B (4 bytes: three seed bytes): mapFlipFlag = 0, the three ; arguments are ; $520C cmdComcenRepairedOrSetupReady Handler of the 1-byte command $9E, which means two different things. ; $523E cmdSessionControl Handler of command $8C (2 bytes: reason). ; $52AE getPauseClearMask Returns the AND mask that clears the resuming side's bit of a two-bit ; ready/pause byte. ; $52C8 cmdDroneMove Handler of command $8D (4 bytes: heading, column, row), the opponent's live ; drone telemetry. ; $532F saveRegsForCmdReturn Two-instruction helper for the handlers that are called from the middle of the ; packet ; $5336 cmdDroneDetonate Handler of command $8E (3 bytes: column, row). ; $537B cmdDroneRelease Handler of the 1-byte command $8F, queued by releaseDroneControl ($8230) when ; the player ; $538B cmdDroneShotDown Handler of the 1-byte command $92. ; $53A3 cmdDroneLaunch Handler of the 1-byte command $97: droneArrivalFlags = 0 for a fresh drone in ; the air. ; $53C0 cmdMissileStrike Handler of command $90 (3 bytes), which carries two different payloads. ; $542E cmdSelfDestruct Handler of command $91 (2 bytes: unit; bit 7 of the argument means 'the whole ; group this ; $5459 destroyUnit Removes unit cmdParam0 from play. ; $5470 cmdMoveKeepGroup Handler of command $98 MOVE TO GOAL (4 bytes: unit, column, row), the order ; issued to ; $5479 cmdAddEnergy Handler of command $9A (3 bytes: unit, amount), sent by the COMCEN energy ; repair on the ; $549E cmdHaltGroup Handler of command $9B (2 bytes: group byte), the group HALT order. ; $54E1 cmdSetRecycler Handler of command $9C (4 bytes: mode, column, row), sent by overlay A's ; chooseRecycler ; $5511 cmdChooseSide Handler of command $9D (2 bytes: the sender's side-preference token), the ; side-selection ; $553B sendChooseSide The sending half of the $9D exchange: copies playerSide into cmdParam0 and into ; $5548 cmdDigIn Handler of command $9F DIG IN (2 bytes: unit): calls setUnitDigBits with $20, ; i.e. ; $554C cmdDigOut Handler of command $A0 DIG OUT (2 bytes: unit): setUnitDigBits with 0 clears ; the dig-progress ; $555A setUnitDigBits Shared prologue of cmdDigIn and cmdDigOut: patches the new dig bits into the ; ORA operand ; $5562 applyDigBitsToUnit The two-line worker shared by the single-unit and the group dig handlers: ; $556D cmdGroupDigIn Handler of command $A1 (2 bytes: group byte): patches the dig bits to $20 and ; runs the ; $5571 cmdGroupDigOut Handler of command $A2 (2 bytes: group byte) and the entry of the shared group ; dig loop: ; $5596 cmdSetFormation Handler of command $A3 (3 bytes: group leader unit, rotation mode 0-2): reads ; the two ; $559C cmdSetGameType Handler of command $A4 (2 bytes): stores the argument into gameTypeOptions ; ($0BA3), which ; $55A5 cmdSetupOption Handler of command $A5 (3 bytes: kind, value), the generic carrier of overlay ; A's ; $55EC queueCmdBytePending Entry used by the 1-byte commands ($8F, $92, $97, $9E): counts one command in ; $55EF queueCmdByte Appends A to the 40-byte outgoing command ring at $9065, wrapping the write ; index $908D at ; $560A queueCmd4 Queues a 4-byte command: the code in A followed by cmdParam0, cmdParam1 and ; cmdParam2 (used for ; $560F queueCmd3 Queues a 3-byte command: the code in A followed by cmdParam1 and cmdParam2 (the ; commands of ; $561F queueCmd2 Queues a 2-byte command: the code in A followed by cmdParam0 (the commands of ; length 2 are the ; $562B sendOutgoingPacket Builds the packet for the next lock-step exchange and hands it to the comm ; module. ; $56B1 pollMenuSession Reached while a link is up but no game is running (commSessionState $0B9C bit ; 7): if the ; $56BB handleMenuSessionPacket Menu-level receive path: re-arms the 10-frame link poll delay and returns ; unless a ; $56F1 completeExchange Makes sure the packet exchange currently in flight finishes. ; $572B readyForNewGameHandshake Run before a new game over an existing link: finishes any pending exchange, ; puts ; $577C buildReadyPacket Fills the comm module's transmit buffer with the 2-byte packet $8C,$02 (session ; control, ; $578A recordExchangeToFilm Appends one lock-step exchange to the game film held in the RAM under the I/O ; area. ; $57BE writeFilmTimestamp Writes the time stamp of one game-film record at (filmPtr),Y. ; $57E7 advanceFilmPtr Adds the record length in Y to the 16-bit film write pointer filmPtr ($BC/$BD), ; so the next record starts where this one ended. ; $57F2 finalizeFilm Closes a game film that was being recorded: appends the end-of-film marker ; record ($FE = the game played itself out, $FF = it was aborted) with a time stamp if the buffer still has room, then ; rewinds and fills in the header at $D000 - total film length, format id, final clock, both 16-bit scores and the ; checksum over $D000-$D032 - and finally leaves filmPtr pointing at the end of the recording again. ; $585C setFilmPtrToEnd Points filmPtr ($BC/$BD) at filmEndPtr ($92F1/$92F2), i.e. ; $5867 saveFilmEndAndRewind Remembers the current film position as the end of the recording (filmEndPtr = ; filmPtr) and then rewinds filmPtr to the start of the buffer at $D000, ready for header access. ; $5874 readFilmByte Reads one byte of the game film out of the 4 KB of RAM that lies underneath the ; I/O area ($D000-$DFFF). ; $5893 writeFilmByte The write half of readFilmByte: stores A at (filmPtr),Y in the RAM under the ; I/O area, with the same 'sei / kill the CIA2 NMI / $01=$34 / restore' dance. ; $58B2 copyPageUnderIo Copies one 256-byte page with the I/O area banked out, used to move the unit ; tables in and out of the film's start-of-game snapshot. ; $58D5 rewindFilmPtr Sets filmPtr ($BC/$BD) to $D000, the first byte of the film / saved-game buffer ; in the RAM under the I/O area. ; $58DE verifyFilmHeader Sanity check on a film that has just been loaded from disk: recomputes the ; checksum over header bytes $D000-$D032 and compares it with the stored one at $D033, then checks that the format id at ; $D02F matches this build's gameVersionByte ($0B94). ; $5900 computeFilmHeaderChecksum Adds up the 51 header bytes $D000-$D032 into scratch18 ($18). ; $5915 waitForExchangeIdle Spins on the per-frame command exchange until the packet exchange in flight has ; completed, which the exchange code signals by clearing linkTimeoutCounter ($9223); checkLinkTimeout sets bit 7 of that ; counter while a packet is outstanding. ; $591E compareFilmPtrToEnd 16-bit unsigned compare of filmPtr against filmEndPtr. ; $5929 writeFilmSetupSnapshot Writes the start-of-game snapshot into the film header so that a replay can ; rebuild the battlefield exactly: the 20 setup bytes $92A2-$92B5 (scores, recycler mode and position, terrain points, ; drones, missiles, comcen speeds) go to header offsets $34-$47, the eight 100-byte unit arrays $F640-$F95F (column, ; row, flags, type, destination column/row, waypoint column/row) are copied to $D048-$D367, and the game clock is stored ; at $D030. ; $5983 getFilmSnapshotDestAddr Returns the constant address $D268 in A (low) and X (high): the highest ; destination page of the unit snapshot inside the film header, and the page just below the start of the command stream ; at $D368. ; $5988 getFilmSnapshotSrcAddr Returns the constant address $F860 in A (low) and X (high): the highest source ; page of the unit snapshot, i.e. ; $598D setFilmLimitToBufferEnd Sets filmEndPtr to $DFF0, the highest address a recording may reach. ; $5998 initFilmStream Prepares the film for recording once the snapshot is in place: writes the ; offset of the first command record ($0368) into header bytes $D002/$D003 and sets filmPtr to $D368, the first byte ; after the 800-byte unit snapshot. ; $59B3 isCmdRingNearlyFull Works out how much room is left in the 40-byte outgoing command ring at ; $9065-$908C and reports C=1 when fewer than 5 bytes are free, which is less than the longest command plus its ; arguments. ; $59D0 finishFilmPlayback End of a game film: reached when the replay runs past the last record, when the ; record length byte is $FF or when the aborted-game end marker is read. ; $5A14 checkLinkTimeout Watchdog used while the game is waiting for the opponent's packet. ; $5AD7 drawOverviewMap Draws the whole strategic overview: the 40x40 battlefield squeezed into the ; 20x20 character window at screen (1,1)-(20,20), four map cells (a 2x2 block) per character. ; $5B6C drawOverviewQuarterLine Puts the field markings back on top of an overview character. ; $5BAD buildOverviewQuadrant Colour-aware entry of the overview cell renderer. ; $5BD4 buildOverviewQuadrantNoColour ORs the 2x4 pixel pattern of map cell A into quadrant X of the 8-byte ; character buffer at $90B8, without touching the colour. ; $5C7C drawMapCell Redraws a single battlefield cell everywhere it can currently be seen. ; $5D1A drawViewportCell Draws one map cell as a 16x16 pixel tile (2x2 characters) in the 7x5 tactical ; view on the right of the battlefield screen. ; $5D7F drawScreen1Cell Redraws one map cell on screen 1, the comcen radar/scanner map, where the ; window is 21 cells wide by 17 tall at character (3,3) and every cell is a single 8x8 glyph. ; $5E03 clearOverviewWindow Points the text window at the 20x20 overview panel (left column 1, top row 1, ; $5E17 handleOwnUnitClick Entry taken by the battlefield main loop when the player clicks on one of his ; $5E24 runUnitOptionsMenu Single-unit branch of handleOwnUnitClick, also entered by JMP from the group ; $5FA6 exitUnitMenu Shared tail of every unit/group menu path: drops back to UI level 0 and ; releases the ; $5FB1 addMenuItem Appends item id X to menuItemIdList[menuItemCount++] and prints the item's name ; on the ; $5FD1 runMenuLoopWithFlag Alternative entry for the console and setup menus: with C = 1 it behaves ; $5FD7 runMenuLoop Menu selector without the unit-idle watch: patches menuIdleSnapshot+1 ($602A) ; to 0 and ; $5FDC runMenuLoopWatchUnit The generic vertical menu selector used by every in-game option menu. ; $60E5 unhighlightMenuRow Removes the highlight from the selected menu row, but only if it is currently ; $60EA toggleMenuRowHighlight Flips the highlight of menu row menuFirstRow + menuSelectedItem by EORing ; $612F runJoinGroupMenu The JOIN GROUP picker. ; $61CF runGroupOptionsMenu Group branch of handleOwnUnitClick. ; $633C printGroupTitleAndName Header of the group menu: prints GROUP (printGroupTitle) and, on the next ; $6358 printGroupTitle Moves the text cursor to column 4 of the info panel and prints the string GROUP ; $6366 clearGroupHighlight Drops the viewport's group highlight and repaints the 7x5 tactical view. ; $636E printSettingHeader Header of the group editor: clears the info panel in colour 4, moves to screen ; $6387 printFormationCentred Prints the word FORMATION ($C9D2) centred in the current text window; also ; $638E runGroupEditor Interactive editor for the group in selectedGroupKey, in MEMBERSHIP mode (zp_BB ; >= ; $64DA sendHaltGroupCmd Queues the 2-byte command $9B with the key of the currently selected group. ; $64E4 printSettingHeaderForLevel Clears the info panel in the colour that belongs to the current UI ; $64F6 printSettingWord Prints the word SETTING ($CB4F) centred in the current text window and ends ; $6500 promptForDestination runSetDestinationScreen - The SETTING / DESTINATION screen. ; $650E countGroupMembers Counts the live units that belong to the group whose key is in A. ; $652E runRepairWhichMenu Draws and runs the 'REPAIR WHICH:' menu of the REPAIR console tab. ; $65A0 runInGameOptionsMenu Runs the MISC-tab options menu while a battle is in progress. ; $65E7 waitForGroupRepositionsDone Before a group screen edits a group, wait until the re-position ; $65FF runFormationOptionsMenu The FORMATION sub-menu of the group options. ; $6637 soundPlayerTick The per-frame SID sound driver. ; $6651 updateSoundVoice Advances one SID voice by one frame. ; $66C7 runSoundScript Interprets one voice's script bytecode until it sets a note duration (or ends). ; $671C fetchSoundScriptByte Reads the next byte of a voice's script through the zero-page pointer at ; $6728 startSound Starts sound A (0-28) on the voices its table row names. ; $67CB unpackRleTo0200 Run-length decoder that always unpacks into the two pages $0200-$03FF. ; $681A fetchRleByte Returns the next byte of the packed stream and advances the pointer. .org $0800 ; ---------------------------------------------------------------------- ; gameEntry - the game's front door. The boot loader does not return to its caller: it rebuilt the ; stack so that its final RTS lands in verifyProgramChecksum ($0461), and that routine ends with JMP ; $0800. This is also where the setup block jumps back to after a reset of the game state. ; In: nothing; $01 = $35 (I/O visible, no ROMs), interrupts pointing at the RTI in the boot loader. ; Out: never returns - control passes to mainStart. ; Called from: boot/eaBootFile $02D5, and game/scratchAndMessages0400 $04AB (the tail of ; verifyProgramChecksum). ; ---------------------------------------------------------------------- gameEntry: jmp mainStart ; 0800 the three bytes at $0800 are the published entry point, so the real start-up code can live anywhere ; loaderStatus - sticky error flag of the fast loader. Bit 7 means 'the DISK ERROR message is ; currently on the status line'; the low bits keep the drive's error code from that failure ($02 ; header not found, $03 no sync, $05 checksum, $08 write protected / wrong disk id). Only loadSectors ; reads or writes it. loaderStatus: .byte $00 ; 0803 . $00 = no error outstanding ; ---------------------------------------------------------------------- ; loadSectors - the game's own copy of the fast loader. Reads A consecutive sectors of track Y, ; starting at sector X, into the address last given to setLoadDest, one sector per page. Every ; overlay, the battlefield map, the setup block and the trainer's playbook arrive through here. A ; drive error is not fatal: 'DISK ERROR.' goes up on the status line and the same sector is retried ; forever, so the player can close the drive door or put the right disk back in and the load simply ; continues. ; In: A = number of sectors, Y = track, X = first sector, loadDestLo/loadDestHi = destination page. ; Out: the sectors in memory, loadDestHi advanced one page per sector, C=0, the caller's I flag ; restored; ldTrack/ldSector/ldCount clobbered. ; Called from: $0B58, $0F10, $0F38, $0F41, $0F5C in this file, game/mapGenerator6F00 $7B2D and ; game/trainerAiE000 $E0B4/$E0D6/$ECB3. ; ---------------------------------------------------------------------- loadSectors: php ; 0804 remember the caller's interrupt flag - the transfer itself is cycle counted sei ; 0805 no interrupts from here to the RTS: a raster IRQ in the middle of a sector would lose bytes sta ldCount ; 0806 sectors still to transfer ; ---------------------------------------------------------------------- ; One pass per sector: ask the drive for it, retry until it answers 'status 0', then move the ; destination on one page and step to the next sector of the same track. ; ---------------------------------------------------------------------- loadSectorsLoop: jsr readOneSector ; 0808 fetch one sector into (loadDestLo/Hi); returns C=1 and the drive's error code in A on failure bcc loadSectorOk ; 080B branch when the drive reported no error ora #$80 ; 080D keep the error code but set bit 7 = 'the error message is on the screen' sta loaderStatus ; 080F remember that so the message can be taken down after a successful retry lda #$A2 ; 0812 low byte of msgDiskError ($0FA2 = 'DISK ERROR.') ldy #$0F ; 0814 high byte of msgDiskError jsr showMessageString ; 0816 print it centred on the status line (showMessageString $C3D8) ldy ldTrack ; 0819 reload the track of the sector that failed... ldx ldSector ; 081B ...and its sector number jmp loadSectorsLoop ; 081D retry the same sector forever - the loader never gives up ; ---------------------------------------------------------------------- ; A sector came in cleanly. If an earlier attempt had put DISK ERROR on the status line, take it down ; again and put the message the game wants there back. ; ---------------------------------------------------------------------- loadSectorOk: lda loaderStatus ; 0820 did any earlier attempt put DISK ERROR on the screen? bpl loadSectorNext ; 0823 no message to take down - go straight on and #$7F ; 0825 clear the 'message showing' bit, leaving the last error code behind for a post mortem sta loaderStatus ; 0827 store the cleared flag lda persistentMessageId ; 082A persistentMessageId = the message the status line should fall back to jsr showStatusMessage ; 082D reprint it over the DISK ERROR text (showStatusMessage $C6FF) ; ---------------------------------------------------------------------- ; Advance to the next sector of the run: one sector forward on the disk, one page forward in memory. ; ---------------------------------------------------------------------- loadSectorNext: inc ldSector ; 0830 next sector of this track inc loadDestHi ; 0832 next destination page - one sector is exactly 256 bytes ldx ldSector ; 0835 sector number for the next request ldy ldTrack ; 0837 track number for the next request (never changes inside one call) dec ldCount ; 0839 one sector fewer to go bne loadSectorsLoop ; 083B loop until the whole run has been read plp ; 083D restore the caller's interrupt flag clc ; 083E C=0: a read can only ever end successfully, it retries instead of failing rts ; 083F back to the caller ; ---------------------------------------------------------------------- ; Failure exit of writeSectors: the drive refused a sector (write protected, wrong disk id, or it ; could not find the header). Unlike a read this is not retried - the caller shows a message and ; gives up. ; ---------------------------------------------------------------------- writeSectorsFail: plp ; 0840 restore the caller's interrupt flag sec ; 0841 C=1 = the save failed rts ; 0842 back to the caller ; ---------------------------------------------------------------------- ; writeSectors - the write half of the fast loader: sends A consecutive sectors from the address set ; by setLoadDest to track Y starting at sector X, one page per sector. The drive encrypts each block ; with the same rolling XOR that a read decrypts with (cryptSectorBuffer $0531 in the drive code). ; This is the only path that ever sends command $60, and it is used solely to save a game film and the ; setup block to the save slots on track 18. ; In: A = number of sectors, Y = track, X = first sector, loadDestLo/loadDestHi = source page. ; Out: C=0 when every sector went out, C=1 on the first refusal; loadDestHi advanced. ; Called from: game/mapGenerator6F00 $7E85 (save game film) and $8194 (save setup). ; ---------------------------------------------------------------------- writeSectors: php ; 0843 remember the caller's interrupt flag sei ; 0844 interrupts off for the whole transfer sta ldCount ; 0845 sectors still to write writeSectorsLoop: jsr writeOneSector ; 0847 write one sector; C=1 if the drive refused it bcs writeSectorsFail ; 084A give up on the first error instead of retrying inc ldSector ; 084C next sector of the track inc loadDestHi ; 084E next source page ldx ldSector ; 0851 sector number for the next request ldy ldTrack ; 0853 track number for the next request dec ldCount ; 0855 one sector fewer to go bne writeSectorsLoop ; 0857 loop until the whole run has been written plp ; 0859 restore the caller's interrupt flag clc ; 085A C=0 = everything was written rts ; 085B back to the caller ; ---------------------------------------------------------------------- ; setLoadDest - names the memory the next transfer works on. It does not use a variable: it patches ; the operand bytes loadDestLo ($09A2) and loadDestHi ($09A3), which readSectorBody and writeOneSector ; then copy into the zero-page pointers the byte loops actually use. Only the page matters in ; practice, since every transfer is a whole number of 256-byte sectors. ; In: X = address low byte, Y = address high byte. ; Out: loadDestLo/loadDestHi patched. ; Called from: every overlay load in this file ($0B4F, $0F07, $0F1A, $0FC8, $104B, $1064, $1074, ; $108D, $10AF, $10BF), game/mapGenerator6F00 ($7B24, $7E7D, $7E8F, $7F7B, $818B) and the trainer ; ($E12C, $ECAA). ; ---------------------------------------------------------------------- setLoadDest: stx loadDestLo ; 085C low byte of the destination (or source) address sty loadDestHi ; 085F high byte; loadSectors/writeSectors increment this one as they advance page by page rts ; 0862 back to the caller ; ---------------------------------------------------------------------- ; writeOneSector - writes a single sector: command $60, then the track and the sector number, then the ; 256 bytes. CIA1 timer A is stopped across the transfer, exactly as on the read path, and started ; again afterwards - on this side it is only belt and braces, since the CIA interrupts were masked at ; start-up. ; In: Y = track, X = sector, loadDestLo/loadDestHi = the page to write. ; Out: C=1 when the drive's status byte is non-zero (through finishTransfer); ldTrack/ldSector set; ; wrSrcPtr points at the source page. ; Called from: writeSectors $0847. ; ---------------------------------------------------------------------- writeOneSector: sei ; 0863 no interrupts - the handshake with the drive must not be broken sty ldTrack ; 0864 keep the track where the caller's loop can find it again stx ldSector ; 0866 same for the sector number lda loadDestLo ; 0868 the page setLoadDest patched in... sta wrSrcPtr ; 086B ...becomes the low byte of the zero-page source pointer lda loadDestHi ; 086D high byte of the source page sta wrSrcPtrHi ; 0870 sendSectorData reads the 256 bytes through (wrSrcPtr),y lda #$60 ; 0872 $60 = the drive's 'write a sector' command (handleWriteCommand $0490 in the drive code) jsr sendByteToDrive ; 0874 send the command byte lda ldTrack ; 0877 the track to write to jsr sendByteToDrive ; 0879 send it lda ldSector ; 087C the sector to write to jsr sendByteToDrive ; 087E send it ldx #$00 ; 0881 CRA bit 0 = 0... stx CIA1_CRA ; 0883 ...stops CIA1 timer A, the 60 Hz tick the KERNAL left running, so its underflow cannot disturb the exchange jsr sendSectorData ; 0886 hand over the 256 bytes and collect the status byte ldx #$01 ; 0889 CRA = $01: timer A on, continuous mode... stx CIA1_CRA ; 088B ...restart it now that the drive is done rts ; 088E C still holds finishTransfer's verdict ; ---------------------------------------------------------------------- ; sendSectorData - pushes the 256 bytes at (wrSrcPtr) to the drive one handshaked byte at a time, then ; falls into finishTransfer to read back the status byte. The drive receives them the slow way too ; (writeSectorToDisk $04A0), so no cycle counting is needed on this side. ; In: wrSrcPtr/wrSrcPtrHi = the page to send. ; Out: 256 bytes sent; C=1 if the drive's status byte is non-zero. ; Called from: writeOneSector $0886. ; ---------------------------------------------------------------------- sendSectorData: ldy #$00 ; 088F start at the first byte of the page L_0891: lda (wrSrcPtr),y ; 0891 next byte of the sector jsr sendByteToDrive ; 0893 clock it out (the byte coming back is ignored) iny ; 0896 advance through the page bne L_0891 ; 0897 loop until Y wraps, i.e. all 256 bytes are gone jmp finishTransfer ; 0899 read the drive's verdict and return it in C ; ---------------------------------------------------------------------- ; sendByteToDrive - the slow, fully handshaked, full-duplex byte exchange with the drive; the other ; end is exchangeByteWithHost ($034B) in the drive code. Eight times over it samples the drive's bit ; off DATA IN, pulls CLK to acknowledge, puts its own bit on DATA OUT, releases CLK and waits for the ; drive to pull CLK back - one bit each way per handshake, so neither side has to be cycle accurate. ; Every command, track and sector byte goes out this way, and the status byte is collected by ; exchanging a dummy byte. Both shift registers work LSB first (LSR sendByteShift / ROR ; recvByteShift); the phase-1 note saying 'MSB first' was wrong. ; In: A = the byte to send. ; Out: A = the byte received from the drive, C=0, X=0, sendByteShift = 0, recvByteShift = the received ; byte. ; Called from: writeOneSector ($0874/$0879/$087E), sendSectorData ($0893), readSectorBody ; ($0900/$0905/$090A) and finishTransfer ($0949); also from the modem driver and the trainer at ; $EA1D/$EA29. ; CIA2 port A ($DD00) is the serial bus: bit 4 = CLK OUT, bit 5 = DATA OUT, bit 6 = CLK IN, bit 7 = ; DATA IN, and bits 0-1 are the VIC bank select, which must survive every write (the game runs in bank ; 2, so they read %01). The C64's outputs are inverted by the bus drivers - writing a 1 pulls the ; line low - while its inputs read the true line level, so a line that somebody is pulling reads back ; as 0. In the drive it is the other way round, which is why the two sides look mirrored. ; ---------------------------------------------------------------------- sendByteToDrive: sta sendByteShift ; 089C the byte to send goes into the output shift register ldx #$08 ; 089F eight bits, one per pass of the loop L_08A1: bit CIA2_PRA ; 08A1 poll the serial port; V = bit 6 = CLK IN bvc L_08A1 ; 08A4 wait until the drive releases CLK - by then its bit is sitting on DATA lda CIA2_PRA ; 08A6 read the port so the VIC bank bits can be written back unchanged and #$DF ; 08A9 clear bit 5: release our DATA OUT so the line shows only what the drive drives sta CIA2_PRA ; 08AB let go of DATA lda CIA2_PRA ; 08AE sample the port with the drive's bit on DATA IN cmp #$80 ; 08B1 C = bit 7 = the drive's data bit (line high = a 1 bit) ror recvByteShift ; 08B3 shift it in from the top; after eight rounds the first bit has reached bit 0, i.e. LSB first ora #$10 ; 08B6 set bit 4 = pull CLK low: 'I have taken your bit' sta CIA2_PRA ; 08B8 assert CLK and #$BF ; 08BB clear bit 6 in the copy: that is what the port reads back now that CLK is low L_08BD: cmp CIA2_PRA ; 08BD compare the live port against that snapshot beq L_08BD ; 08C0 wait until it differs - the drive toggles DATA as its acknowledgement ($0364 in the drive code) lsr sendByteShift ; 08C2 shift the next outgoing bit (LSB first) into the carry lda #$00 ; 08C5 a 0 bit: leave DATA OUT clear so the line stays high bcc L_08CB ; 08C7 branch when the bit to send is 0 lda #$20 ; 08C9 a 1 bit: bit 5 = DATA OUT, which pulls the line low; the drive reads its inputs inverted and sees a 1 L_08CB: ora CIA2_PRA ; 08CB merge with the live port so the bank bits and CLK OUT survive sta CIA2_PRA ; 08CE put the bit on the DATA line and #$EF ; 08D1 clear bit 4 = release CLK: 'the bit is on the line, take it' sta CIA2_PRA ; 08D3 release CLK L_08D6: bit CIA2_PRA ; 08D6 poll the port again; V = CLK IN bvs L_08D6 ; 08D9 wait until the drive pulls CLK low as its acknowledgement ($037F in the drive code) eor #$20 ; 08DB toggle our own DATA OUT bit - the handshake the drive waits for at $0389 sta CIA2_PRA ; 08DD write the toggled value dex ; 08E0 one bit done bne L_08A1 ; 08E1 round again until all eight bits have been exchanged lda recvByteShift ; 08E3 return whatever the drive sent back (the status byte, when that is what we came for) clc ; 08E6 C=0 - readSectorBody depends on this: its ADC #$2F must add exactly $2F rts ; 08E7 back to the caller ; sendByteShift - the outgoing shift register of sendByteToDrive; LSR pushes bit 0 out first, so it ; holds 0 when the byte has gone. sendByteShift: .byte $00 ; 08E8 . outgoing bit buffer ; recvByteShift - the incoming shift register of sendByteToDrive; ROR feeds bits in at the top, so ; after eight of them the first bit received sits in bit 0. The second byte, $08EA, is never ; referenced - a pad byte. The ; $BE the register holds in the disk image is just what the assembler left behind. recvByteShift: .byte $BE,$00 ; 08E9 .. incoming bit buffer; the $BE it holds in the disk image is just whatever the assembler left behind ; ---------------------------------------------------------------------- ; readOneSector - reads one sector into the load destination. It stops CIA1 timer A for the duration ; (the 60 Hz tick the KERNAL left running) so that absolutely nothing can interfere with the ; cycle-counted receive loop, and restarts it afterwards. ; In: Y = track, X = sector, loadDestLo/loadDestHi = destination page. ; Out: 256 bytes in memory, ldTrack/ldSector set, C=1 and A = the error code when the drive reports a ; problem. ; Called from: loadSectors $0808. ; ---------------------------------------------------------------------- readOneSector: sei ; 08EB interrupts off - from here on the transfer is counted in cycles sty ldTrack ; 08EC keep the track for the retry logic in loadSectors stx ldSector ; 08EE and the sector number ldx #$00 ; 08F0 CRA bit 0 = 0... stx CIA1_CRA ; 08F2 ...stop CIA1 timer A while the sector comes in jsr readSectorBody ; 08F5 do the actual request and receive ldx #$01 ; 08F8 CRA = $01: timer A running, continuous mode... stx CIA1_CRA ; 08FA ...start it again rts ; 08FD C and A carry the drive's verdict back to loadSectors ; ---------------------------------------------------------------------- ; readSectorBody - the body of a sector read. It sends command $80 with the track and sector, parks ; the sprites, works out the raster constant the receive loop uses to dodge bad lines, waits for the ; drive to announce that the sector is decoded, and then runs the fast 2-bits-per-port-read receive ; loop. ; In: ldTrack, ldSector, loadDestLo/loadDestHi. ; Out: 256 bytes at the destination, savedSpriteEnable = the sprite enable register as it was, ; ldCia2Bits = the VIC bank bits, the operand at $095F patched, C=1 on a drive error. ; Called from: readOneSector $08F5. ; ---------------------------------------------------------------------- readSectorBody: lda #$80 ; 08FE $80 = the drive's 'read a sector' command (anything that is not $C0 or $60 in commandDispatchLoop $0313) jsr sendByteToDrive ; 0900 send the command byte lda ldTrack ; 0903 the track to read jsr sendByteToDrive ; 0905 send it lda ldSector ; 0908 the sector to read jsr sendByteToDrive ; 090A send it lda VIC_SPR_ENA ; 090D the sprite enable register as the game left it... sta savedSpriteEnable ; 0910 ...saved so the display can be put back together afterwards lda CIA2_PRA ; 0913 read CIA2 port A... and #$03 ; 0916 ...and keep only bits 0-1, the VIC bank select (%01 = bank 2, where the bitmap and screen live) sta ldCia2Bits ; 0918 the receive loop rewrites the port from this on every strobe, so the bank must never move ora #$20 ; 091A add bit 5 = DATA OUT: pull DATA low, which tells the drive 'not ready for a byte yet' sta CIA2_PRA ; 091C this also releases CLK OUT and the ATN and RS-232 lines lda VIC_CTRL1 ; 091F $D011 holds the vertical fine scroll in bits 0-2... and #$07 ; 0922 ...isolate it: a bad line happens whenever (raster AND 7) equals that value adc #$2F ; 0924 $2F = one line before the first display line ($30); C is 0 here because sendByteToDrive ended with CLC sta L_095E+1 ; 0926 patch the SBC operand of the receive loop at $095F, so the loop can test 'is the next line a bad line?' L_0929: bit CIA2_PRA ; 0929 poll the serial port; V = CLK IN bvs L_0929 ; 092C wait until the drive pulls CLK low = 'the sector is read, decrypted and ready to go' ($044D in the drive code) lda #$00 ; 092E no sprites at all... sta VIC_SPR_ENA ; 0930 ...so the VIC cannot steal cycles with sprite DMA while the sector comes in lda loadDestLo ; 0933 copy the patched destination address... sta ldDestPtr ; 0936 ...into the zero-page pointer the receive loop stores through lda loadDestHi ; 0938 high byte of the destination page sta ldDestPtrHi ; 093B (ldDestPtr),y then walks the whole 256-byte sector jsr receiveSectorData ; 093D take the 256 bytes jmp finishTransfer ; 0940 put the sprites back and read the drive's status byte ; ---------------------------------------------------------------------- ; finishTransfer - shared tail of a read and a write: puts the sprite enable register back the way it ; was and collects the drive's status byte, which arrives on the ordinary handshaked path. Status 0 ; means success; anything else is a DOS error code ($02 header not found, $03 no sync, $05 data ; checksum, $08 write protected or wrong disk id). ; In: savedSpriteEnable. ; Out: A = the status byte, C=1 when it is non-zero, sprites restored. ; Called from: readSectorBody $0940 and sendSectorData $0899, both by JMP, so its RTS returns to their ; callers. ; ---------------------------------------------------------------------- finishTransfer: lda savedSpriteEnable ; 0943 the sprite enable register as it was before the transfer... sta VIC_SPR_ENA ; 0946 ...restored, so the cursor and marker sprites come back jsr sendByteToDrive ; 0949 exchange a dummy byte (the sprite mask happens to be in A) to collect the status byte beq L_094F ; 094C status 0 = no error, leave C=0 sec ; 094E C=1 tells the caller the sector failed L_094F: rts ; 094F A still holds the status byte ; ---------------------------------------------------------------------- ; receiveSectorData - the fast half of the protocol and the only cycle-counted code in the game. The ; drive does not handshake per bit here: it writes four port values per byte at fixed intervals ; (sendSectorToHost $044B), each carrying two data bits on CLK and DATA, and the only synchronisation ; is per byte - the host releases DATA to ask for the next one and then samples four times at fixed ; cycle offsets. That is why sprites are off, CIA1 timer A is stopped, interrupts are disabled and ; each byte only starts on a raster line that is not immediately before a bad line. The four samples ; are folded together with LSR/EOR so that the two bits of sample 1 end up in bits 0-1 and those of ; sample 4 in bits 6-7; the VIC bank bits shift out of the bottom on their own, except in the last ; sample, which is why they are EORed out explicitly at $0984. ; In: ldDestPtr/ldDestPtrHi = destination page, ldCia2Bits = the VIC bank bits, the SBC operand at ; $095F. ; Out: 256 bytes stored at (ldDestPtr); the bus left with DATA pulled low. ; Called from: readSectorBody $093D. ; ---------------------------------------------------------------------- receiveSectorData: bit CIA2_PRA ; 0950 poll the serial port; V = CLK IN bvc receiveSectorData ; 0953 wait until the drive releases CLK again - it does that just before every byte ($0466 in the drive code) ldy #$00 ; 0955 index of the first byte of the sector ; ---------------------------------------------------------------------- ; One pass per byte of the sector. The pass first waits for a safe raster line, then releases DATA to ; ask for the byte and samples the port four times, two bits at a time. ; ---------------------------------------------------------------------- receiveByteLoop: sec ; 0957 SBC below must subtract exactly the patched constant ldx ldCia2Bits ; 0958 the bank bits with CLK OUT and DATA OUT clear: writing this releases both lines nop ; 095A one cycle of padding L_095B: lda VIC_RASTER ; 095B where is the raster right now? L_095E: sbc #$32 ; 095E operand patched at $0926 with (yscroll + $2F): subtracts the top of the display window bcc L_0966 ; 0960 above the display window there are no bad lines - go straight ahead and #$07 ; 0962 how far the raster is from the next bad line beq L_095B ; 0964 wait while the next line would be a bad line: the VIC would steal 40 cycles in the middle of the byte L_0966: stx CIA2_PRA ; 0966 release CLK OUT and DATA OUT: 'send me the next byte' - the drive waits for exactly this at $046C txa ; 0969 take a copy of the bank bits... ora #$20 ; 096A ...with bit 5 set, i.e. DATA OUT asserted, ready to say 'got it' after the byte tax ; 096C keep it in X until then nop ; 096D padding so the first sample lands on the drive's first port write nop ; 096E padding bit loaderTimingPad ; 096F BIT on an unused zero-page byte: three cycles of padding, nothing is tested lda CIA2_PRA ; 0971 sample 1: CLK IN and DATA IN carry data bits 0 and 1 lsr a ; 0974 shift them down two places... lsr a ; 0975 ...so the next sample can be merged in above them (the bank bits fall off the bottom) nop ; 0976 padding to hit the drive's second write eor CIA2_PRA ; 0977 sample 2: data bits 2 and 3 lsr a ; 097A make room for the next pair... lsr a ; 097B ...again dropping the bank bits of the older sample nop ; 097C padding nop ; 097D padding nop ; 097E padding to hit the drive's third write eor CIA2_PRA ; 097F sample 3: data bits 4 and 5 lsr a ; 0982 shift the four bits gathered so far down... lsr a ; 0983 ...into their final positions eor ldCia2Bits ; 0984 cancel the bank bits of the last sample in advance - they are not shifted out any more eor CIA2_PRA ; 0986 sample 4: data bits 6 and 7; the byte is now complete stx CIA2_PRA ; 0989 pull DATA low again: 'byte taken, do not send yet' nop ; 098C padding so the store cannot overlap the drive's last write sta (ldDestPtr),y ; 098D put the byte in the sector buffer iny ; 098F next byte bne receiveByteLoop ; 0990 loop until Y wraps: 256 bytes received rts ; 0992 back to readSectorBody, which reads the status byte ; strBlockExecuteLeftover - the DOS command strings 'B-E 2 0 1 17' and '#' that the boot loader ; ($C32C) sends to start the drive code, left in this copy of the loader. The game never opens a ; KERNAL channel (the ROMs are banked out by the time $0800 runs), and nothing references these bytes: ; dead weight carried over when the loader was copied into the main program. strBlockExecuteLeftover: .byte "B-E 2 0 1 17",$00,"#",$00; 0993 'B-E 2 0 1 17' = block-execute track 1 sector 17 in the drive, then a 0 terminator, the filename '#' and another 0 ; loadDestLo - operand byte of the LDA at $0868/$0933: the low byte of the transfer address, patched ; by setLoadDest. loadDestLo: .byte $00 ; 09A2 . low byte of the next transfer address ; loadDestHi - operand byte of the LDA at $086D/$0938: the high byte of the transfer address. ; loadSectors and writeSectors increment it after every sector, so it walks the destination page by ; page. $09A4 is a pad byte nothing touches. loadDestHi: .byte $00,$00 ; 09A3 .. high byte of the next transfer address ; savedSpriteEnable - VIC_SPR_ENA as it was when a sector transfer started; the sprites are blanked so ; that sprite DMA cannot steal cycles, and this puts them back. savedSpriteEnable: .byte $00 ; 09A5 . sprite enable register saved across one sector transfer ; ---------------------------------------------------------------------- ; manualLookupCheck - the manual look-up copy protection, and it is disabled in this build: the JSR ; that called it at $0A9B was assembled as BIT ($2C instead of $20), so it is never entered. It picks ; one of the 32 map seeds in the table at $0401 using the boot-time random byte at $09C3, generates ; and draws that battlefield, prints 'IDENTIFY THIS MAP (SEE MANUAL)' and lets the player type the ; five-letter map name (the printable form of the 24-bit seed) three times. A wrong answer three ; times over wipes memory downwards from $C000 and hangs the machine. The current map seed and map ; name are saved on the stack and put back on success, so a real game is unaffected. ; In: the answer table mapNameSeedTable ($0401), the random byte at $09C3, the player's typing ; through enterMapName ($7717), which packs the five characters into scenarioSeed0-2 ($5A-$5C). ; Out: mapSeed and mapNameText restored, textColour/textPadColour = 0, the info panel cleared; on ; failure it never returns. ; Called from: nobody - the only reference is the disabled JSR at $0A9B. ; ---------------------------------------------------------------------- manualLookupCheck: lda mapSeed ; 09A6 byte 0 of the map seed of the game in progress... pha ; 09A9 ...onto the stack lda mapSeed1 ; 09AA byte 1 of the map seed pha ; 09AD saved lda mapSeed2 ; 09AE byte 2 of the map seed pha ; 09B1 saved ldy #$00 ; 09B2 start at the first character of the map name L_09B4: lda mapNameText,y ; 09B4 the printable map name plus its terminator ($0BA9-$0BAE) pha ; 09B7 push all six bytes; they come back off in reverse at $0A2B iny ; 09B8 next character cpy #$06 ; 09B9 five characters and the terminator byte bcc L_09B4 ; 09BB loop until all six are on the stack lda #$03 ; 09BD three attempts... sta manualCheckTries ; 09BF ...counted down in manualCheckTries lda #$00 ; 09C2 the operand byte $09C3 is the random seed the boot loader took from the SID noise generator ($C215) and verifyProgramChecksum preserves ($0461) and #$1F ; 09C4 32 questions in the manual, so keep 5 bits sta scratch18 ; 09C6 hold the question number asl a ; 09C8 x2... adc scratch18 ; 09C9 ...plus one more = x3, the stride of the 3-byte answer table tay ; 09CB index the table with it lda mapNameSeedTable,y ; 09CC byte 0 of that map's seed (also trainerPlaybook0200:play3Col13) sta mapSeed ; 09CF make it the current map seed iny ; 09D2 next byte of the entry lda mapNameSeedTable,y ; 09D3 byte 1 (also trainerPlaybook0200:play3Col13) sta mapSeed1 ; 09D6 stored iny ; 09D9 last byte of the entry lda mapNameSeedTable,y ; 09DA byte 2 (also trainerPlaybook0200:play3Col13) sta mapSeed2 ; 09DD stored jsr generateBattlefieldMap ; 09E0 build the 40x40 battlefield from that seed (map generator overlay, $6F04) jsr drawOverviewMap ; 09E3 draw it in the overview window so the player can compare it with the manual lda #$02 ; 09E6 colour byte $02 for the prompt cells: with the inverted font the strokes come out red on black sta textColour ; 09E8 colour of the printed characters sta textPadColour ; 09EA colour of the blanks that pad a centred line jsr clearInfoPanel ; 09EC clear the info panel to that colour lda #$47 ; 09EF low byte of the prompt - $0A47, one character before msgIdentifyMap, so the text starts with an extra space ldy #$0A ; 09F1 high byte of the prompt jsr printString ; 09F3 print 'IDENTIFY THIS MAP (SEE MANUAL)' (it contains two $0D line breaks) L_09F6: jsr clearMapNameField ; 09F6 blank the five-character name field ($7836 in the overlay) jsr missileLineFromComcen ; 09F9 let the player type a map name; it is packed 5 bits per letter into scenarioSeed0-2 (also mapGenerator6F00:enterMapName) lda scenarioSeed0 ; 09FC byte 0 of what was typed cmp mapSeed ; 09FE against byte 0 of the seed the map was built from bne L_0A11 ; 0A01 wrong - no need to look further lda scenarioSeed1 ; 0A03 byte 1 typed cmp mapSeed1 ; 0A05 against byte 1 of the seed bne L_0A11 ; 0A08 wrong lda scenarioSeed2 ; 0A0A byte 2 typed cmp mapSeed2 ; 0A0C against byte 2 of the seed beq L_0A29 ; 0A0F all three match: the player owns the manual L_0A11: lda #$03 ; 0A11 sound 3 = the error beep jsr playSound ; 0A13 play it dec manualCheckTries ; 0A16 one attempt gone bne L_09F6 ; 0A19 ask again while attempts are left ldy #$00 ; 0A1B index 0... tya ; 0A1D ...and A = 0: the byte written everywhere ; ---------------------------------------------------------------------- ; Three wrong answers: erase the machine. The loop zeroes one page at a time, walking downwards from ; $C000 by decrementing the high byte of its own STA operand, and stops when that byte reaches 0. ; Long before then it wipes page $0A - its own code - so the CPU runs into zeroed memory and the ; machine has to be switched off. The tables above $C0FF survive, which is of no use to anybody. ; ---------------------------------------------------------------------- wipeMemoryLoop: sta bootEntry,y ; 0A1E zero one byte; the operand high byte at $0A20 is what makes this walk down memory (also textEngineC000:bitPairHighTable) iny ; 0A21 next byte of the page bne wipeMemoryLoop ; 0A22 256 bytes per pass dec wipeMemoryLoop+2 ; 0A24 step the STA operand down to the previous page bne wipeMemoryLoop ; 0A27 keep going until the operand high byte wraps to 0 - by then this code is gone too L_0A29: ldy #$05 ; 0A29 restore the six saved map-name bytes, last pushed first L_0A2B: pla ; 0A2B pull one byte sta mapNameText,y ; 0A2C back into the map name dey ; 0A2F work backwards bpl L_0A2B ; 0A30 until all six are back pla ; 0A32 byte 2 of the saved map seed sta mapSeed2 ; 0A33 restored pla ; 0A36 byte 1 sta mapSeed1 ; 0A37 restored pla ; 0A3A byte 0 sta mapSeed ; 0A3B restored - the game in progress never notices the detour lda #$00 ; 0A3E back to colour 0... sta textColour ; 0A40 ...for printed characters sta textPadColour ; 0A42 ...and for the padding jmp clearInfoPanel ; 0A44 clear the info panel again and return through it ; padA47: unknown, 1 bytes. single $20 byte before msgIdentifyMap msgIdentifyMapLeadSpace: .byte $20 ; 0A47 a plain space: printString is called with $0A47, so this is really the first character of the prompt below ; msgIdentifyMap: text, 36 bytes. copy-protection prompt msgIdentifyMap: .byte " IDENTIFY",$0D," THIS MAP",$0D," (SEE MANUAL",')'|$80; 0A48 'IDENTIFY THIS MAP (SEE MANUAL)' - the $0D bytes are line breaks and the ')' carries bit 7 as the terminator ; ---------------------------------------------------------------------- ; mainStart - the cold start of the game, run once. It silences the CIAs, parks both hardware vectors ; on an RTI, pulls in the runtime overlays (text engine, setup overlay, message strings and the $E000 ; opponent module), loads the saved setup block and runs the little start-up routine at $0300 that ; came with it, installs the raster IRQ, paints the empty battlefield screen and drops into the main ; menu. The manual look-up check that used to run here is disabled in this build. ; In: nothing but the memory image the boot loader left behind. ; Out: everything initialised: overlays resident, IRQ running, currentScreen = 0, playerSide = $FE (no ; side chosen); falls straight into returnToMainMenu. ; Called from: gameEntry $0800. ; ---------------------------------------------------------------------- mainStart: sei ; 0A6C no interrupts until the vectors are safe jsr disableCiaInterrupts ; 0A6D mask and acknowledge both CIAs so no timer or serial interrupt can arrive lda #$98 ; 0A70 low byte of irqRti ($1298)... sta nmiVector ; 0A72 ...into the NMI vector - the ROMs are banked out, so these RAM vectors are the real ones sta irqVector ; 0A75 and into the IRQ vector, until installRasterIrq puts the real handler there lda #$12 ; 0A78 high byte of irqRti sta nmiVectorHi ; 0A7A NMI vector high byte sta irqVectorHi ; 0A7D IRQ vector high byte - any interrupt now just RTIs cli ; 0A80 interrupts on again; nothing can happen except an RTI jsr loadStartupOverlays ; 0A81 pull in $C000 (text engine), the $6F00 setup overlay, the message strings and the $E000 opponent module jsr loadSetupBlock ; 0A84 load the saved setup block to $0200-$03FF (track 18 sectors 15-16) jsr basicVectorHijack ; 0A87 run initGameSystems ($0300), the start-up code that came inside that block: RNG seed, fonts, map row tables, bitmap screen (also tileRules0200:initGameSystems, trainerPlaybook0200:play1Row06) jsr initIrqAndInputState ; 0A8A install the raster IRQ and clear the input state jsr initInterruptVectors ; 0A8D point the NMI vector and the comm module's copy of it at the RTI, and arm the BRK exit lda #$00 ; 0A90 screen 0... sta currentScreen ; 0A92 ...is the battlefield map screen jsr clearScreenAndRedrawGameScreen; 0A95 clear the bitmap and draw the empty game screen jsr clearOverviewWindow ; 0A98 blank the 20x20 overview window in it bit manualLookupCheck ; 0A9B assembled as BIT, not JSR: the manual look-up copy protection is switched off in this build lda #$FE ; 0A9E $FE = no side chosen yet... sta playerSide ; 0AA0 ...the setup screens or the link decide who plays side 0 and who side 1 ; ---------------------------------------------------------------------- ; returnToMainMenu - the panic exit back to the main menu, used whenever the game has to abandon ; whatever it was doing: the end of a game, a lost link, the SHIFT + C= + F7 abort in the raster IRQ, ; and the fall-through from mainStart. It cannot return to its caller, so it throws the stack away ; and rebuilds it. ; In: nothing. ; Out: VIC interrupts and all sprites off, stack pointer = $FD, input state reset, message queue ; empty, diskLoadDepth = 0, currentScreen = 3 (the menu screen); continues into the menu flow at ; $1A86. ; Called from: $126B (the abort hot key in the raster IRQ), $5A3F, game/textEngineC000 $C876, ; game/modemDriverE000 $EE0C, and by falling out of mainStart. ; ---------------------------------------------------------------------- returnToMainMenu: lda #$00 ; 0AA3 0 into both VIC registers below sta VIC_IRQ_MASK ; 0AA5 no more raster interrupts while the screen is rebuilt sta VIC_SPR_ENA ; 0AA8 all eight sprites off - the cursor and marker sprites belong to the game screens cli ; 0AAB allow interrupts again (the handler is still installed) ldx #$FD ; 0AAC throw away whatever return addresses the abandoned code left... txs ; 0AAE ...by resetting the stack pointer to $FD, which is where the game keeps it jsr initBitmapScreen ; 0AAF clear the bitmap, screen and colour RAM to grey and set up the display registers jsr initIrqAndInputState ; 0AB2 silence the sound, re-install the raster IRQ, lock the input out for 158 frames jsr resetMessageQueue ; 0AB5 drop every pending status message lda #$00 ; 0AB8 no disk load is in progress... sta diskLoadDepth ; 0ABA ...so the IRQ may pass keypresses to the game again lda #$03 ; 0ABD screen 3... sta currentScreen ; 0ABF ...is the main menu / console screen jsr showMainMenu ; 0AC2 draw the title, credits and menu frame jmp L_1A86 ; 0AC5 into the menu's link-status flow, which ends up in mainMenuLoop ; ---------------------------------------------------------------------- ; startGameFromSetup - the hand-over from the setup screens to the battle, entered by JMP from the ; setup overlay ($7E3F) once both sides have agreed on a game. It resets the game state, makes sure ; the game disk is in the drive, swaps the $E000 opponent module if the player changed between modem ; and solo trainer, asks for ANSWER or ORIGINATE when a modem game is starting, loads the battlefield ; map and the setup block, negotiates the sides and the map over the link, brings overlay B in and ; finally jumps into the battlefield loop. ; In: setupPlayerSide, filmPlaybackMode, linkSessionState, isSoloTrainer / prevSoloTrainerFlag, the ; resident build id $E030, overlayVariantFlag $87FF. ; Out: the battle running - never returns; jumps to runMapMainLoop ($1FD0). ; Called from: game/mapGenerator6F00 $7E3F (JMP). ; ---------------------------------------------------------------------- startGameFromSetup: lda #$00 ; 0AC8 0 into VIC_IRQ_MASK below sta VIC_IRQ_MASK ; 0ACA no raster interrupts while the game state is torn down and rebuilt lda setupPlayerSide ; 0ACD the side chosen in the setup screens... sta playerSide ; 0AD0 ...becomes the side this machine plays (0 owns units 0-49, 1 owns units 50-99) jsr resetGameVariables ; 0AD3 clear the zero page, the variable block and the message queue jsr initIrqAndInputState ; 0AD6 sound off, raster IRQ back on, input locked out for a moment jsr patchUnitSideBranches ; 0AD9 write the BCC/BCS opcode that decides 'is this unit mine?' into the two map renderers jsr waitForGameDiskInserted ; 0ADC block until the game disk is in the drive (its BAM says OZ) lda filmPlaybackMode ; 0ADF watching a game film? bmi L_0AFF ; 0AE2 then there is no opponent module to arrange lda isSoloTrainer ; 0AE4 the opponent the player picked: bit 7 set = solo trainer, clear = modem sta soloTrainerAtGameStart ; 0AE7 remembered for the whole game (the flag itself is overwritten below) eor commBuildId ; 0AEA compare it with the build id of the module actually resident ($FF trainer, $00 modem) (also trainerAiE000:commBuildId) and #$80 ; 0AED only bit 7 says which build it is beq L_0AF7 ; 0AEF same kind of opponent as last time - the module can stay inc diskLoadDepth ; 0AF1 mark a disk load in progress so the IRQ swallows keypresses jsr loadCommModule ; 0AF4 load the other $E000 module (track 35 for the trainer, track 18 s7 + track 34 for the modem) L_0AF7: lda linkSessionState ; 0AF7 is a link session already running? bmi L_0AFF ; 0AFA yes - do not re-initialise the module underneath it jsr initCommModule ; 0AFC cold-initialise the module and point its keyboard vector at scanKeyboard L_0AFF: lda linkSessionState ; 0AFF link already up? bmi L_0B2C ; 0B02 then the modem has long since been set up lda filmPlaybackMode ; 0B04 watching a film? bmi L_0B2C ; 0B07 then no modem prompt either ldy prevSoloTrainerFlag ; 0B09 what kind of opponent the previous game had lda isSoloTrainer ; 0B0C and what this one has sta prevSoloTrainerFlag ; 0B0F remember it for next time bmi L_0B2C ; 0B12 solo trainer now: nothing to dial tya ; 0B14 the kind of opponent the previous game had, back into A bpl L_0B2C ; 0B15 the previous game was a modem game too: the modem is already set ; ---------------------------------------------------------------------- ; Switching from the solo trainer to a modem game: put the two-line prompt up and wait for A or O. ; Message slots $20 and $21 are the two run-time slots whose text pointers setMessageSlot patches. ; ---------------------------------------------------------------------- ldx #$20 ; 0B17 message slot $20... lda #$00 ; 0B19 ...low byte of msgGetOpponentOnPhoneAnd ($8800 = 'GET OPPONENT ON PHONE AND...') ldy #$88 ; 0B1B ...high byte jsr setMessageSlot ; 0B1D patch slot $20 to point at it ldx #$21 ; 0B20 message slot $21... lda #$1C ; 0B22 ...low byte of msgPressAOrOAndSetModem ($881C = 'PRESS A OR O AND SET MODEM.') ldy #$88 ; 0B24 ...high byte jsr setMessageSlot ; 0B26 patch slot $21 jsr waitForAnswerOriginateKey; 0B29 show them and wait until the player presses A (answer) or O (originate) L_0B2C: lda commBuildId ; 0B2C the build id of the module that is really resident... (also trainerAiE000:commBuildId) sta isSoloTrainer ; 0B2F ...written back over the wanted flag, so $0BA5 always describes what is in memory ($FF trainer / $00 modem) inc diskLoadDepth ; 0B32 disk load starting: swallow keypresses jsr loadUnitTemplatesAndSetup; 0B35 load the unit start-position templates to $F000 and the setup block to $0200 lda #$00 ; 0B38 0 into diskLoadDepth sta diskLoadDepth ; 0B3A the disk is done with for now jsr resumeCommAfterDiskAccess; 0B3D let the comm module run again and clear the loading message jsr startGameSession ; 0B40 agree the sides and the map with the opponent and set up all the battle state inc diskLoadDepth ; 0B43 one more disk load coming lda isSoloTrainer ; 0B46 solo trainer? bpl L_0B5B ; 0B49 no - the modem build's $EC00 tail was loaded with the module itself ldx #$00 ; 0B4B destination low byte $00... ldy #$EC ; 0B4D ...high byte $EC: the trainer tail loads at $EC00 ; ---------------------------------------------------------------------- ; loadCommTailBuild1 - not a routine of its own: the tail of startGameFromSetup, entered with X/Y ; already set at $0B4B. It pulls the solo trainer's $EC00-$EFFF sub-overlay (the code that picks the ; opening play) off track 35 sectors 12-15. The modem build's tail comes from track 34 sectors 12-15 ; through loadCommTailBuild2 ($1070) instead. ; In: X = $00, Y = $EC (the destination page). ; Out: $EC00-$EFFF loaded. ; Called from: falls in from $0B4D. ; ---------------------------------------------------------------------- loadCommTailBuild1: jsr setLoadDest ; 0B4F destination $EC00 ldy #$23 ; 0B52 track 35 = the solo trainer's track ldx #$0C ; 0B54 first sector 12 lda #$04 ; 0B56 four sectors = $EC00-$EFFF jsr loadSectors ; 0B58 read them L_0B5B: lda #$00 ; 0B5B 0 into gamePhase sta gamePhase ; 0B5D gamePhase 0 means the battle itself is running (non-zero = setup, film or menu) jsr loadOverlayBIfNotResident; 0B5F bring the comcen missile/drone screens into $6F00 if the setup overlay is still there lda #$00 ; 0B62 0 into the two bytes below sta diskLoadDepth ; 0B64 no disk load in progress: the IRQ may deliver keys again sta manualCheckTries ; 0B67 the manual-check attempt counter is not needed during a game jsr resetMessageQueue ; 0B6A empty the message queue before the battle messages start lda filmPlaybackMode ; 0B6D watching a film? bmi L_0B77 ; 0B70 then do not announce the battle lda #$1C ; 0B72 message $1C = 'THE BATTLE BEGINS!' jsr queueMessage ; 0B74 queue it L_0B77: jsr buildOwnerNameString ; 0B77 build the ' ' name line shown on the menu panel jmp runMapMainLoop ; 0B7A into the battlefield screen's main loop - this never returns ; inputLockoutTimer: byteTable, 2 bytes. $0B7D lockout timer, $0B7E settings byte inputLockoutTimer: .byte $00 ; 0B7D frames during which the IRQ ignores the keyboard and joystick and keeps the previous state programChecksum: .byte $DB ; 0B7E checksum of $0402-$87FF computed by verifyProgramChecksum and exchanged with the opponent so both sides can detect a different build ; ownerNameString: text, 21 bytes. owner name string built at run time ownerNameString: .byte $A0,$A0 ; 0B7F 21-byte owner/player name built at run time from ownPlayerName and opponentPlayerName, terminated with bit 7 set .byte $A0,$A0,$A0,$A0,$A0,$A0 ; 0B81 more of the 21 blanks the owner-name line starts life as .byte $A0,$A0 ; 0B87 more blanks of the owner-name line .byte $A0 ; 0B89 more blanks of the owner-name line .byte $A0,$A0,$A0,$A0,$A0,$A0,$A0; 0B8A ....... .byte $A0 ; 0B91 the last blanks before the terminator position .byte $A0 ; 0B92 . ownerNameEnd: .byte $A0 ; 0B93 last byte of ownerNameString (the bit-7 terminator position) ; $0B94-$0BB4 - the game settings block: everything that describes the game being played and has to ; match on both machines. gameVersionByte and programChecksum are exchanged in the version handshake, ; mapSeed and mapFlipFlag pick the battlefield, gameTypeOptions the rules, playerSide and ; playsFirstFlag the sides, and the two 6-byte text fields at mapNameText and mapNameBackup hold the ; printable map name. The film header carries a copy of most of it. gameVersionByte: .byte $00 ; 0B94 game/film format id: written to film header offset $2F and verified on load, and sent in the link version handshake mapSeed: .byte $00 ; 0B95 24-bit seed of the current battlefield ($0B95-$0B97) = the 5-letter map name packed 5 bits per letter; sent with command $8B/$A6 mapSeed1: .byte $00 ; 0B96 byte 1 of mapSeed mapSeed2: .byte $00 ; 0B97 byte 2 of mapSeed prevMapSeed: .byte $00 ; 0B98 3-byte copy of the previous game's mapSeed; if it is unchanged the saved map name is restored, otherwise the first-play flag is reset prevMapSeed1: .byte $00 ; 0B99 byte 1 of prevMapSeed prevMapSeed2: .byte $00 ; 0B9A byte 2 of prevMapSeed filmPlaybackMode: .byte $FF ; 0B9B bit 7 = a game film is being played back (commands come from the film, player input is locked out); bits 0-1 = the replay viewpoint 0 US, 1 THEM, 2 BOTH linkSessionState: .byte $00 ; 0B9C link session: 0 = none, 2 = game session agreed, $80 = link up at menu level, $FF = link lost mapFlipFlag: .byte $00 ; 0B9D 0/1: the chosen map is shown rotated 180 degrees (F1 TO FLIP); XORed with playerSide to decide the orientation and sent as command $A6 instead of $8B prevMapFlipFlag: .byte $00 ; 0B9E mapFlipFlag of the previous game; a change forces the first-play flag back to 1 playerSide: .byte $FE ; 0B9F side this machine plays, 0 or 1 ($FE = not chosen yet); EOR 1 gives the opponent, and units 0-49 belong to side 0, 50-99 to side 1 setupPlayerSide: .byte $FE ; 0BA0 side chosen in the setup screens, copied into playerSide when the game starts playsFirstFlag: .byte $00 ; 0BA1 non-zero = this side has 1ST PLAY; it alternates every game and is forced to 1 when the map or the options change savedPlaysFirstFlag: .byte $00 ; 0BA2 copy of playsFirstFlag taken at game start and restored after a film playback gameTypeOptions: .byte $07 ; 0BA3 bits 0-2 game type (0 SCRIMAGE .. 6 DEFENDER, names at $CCC8), bit 3 UNIT MENUS, bit 4 DAMAGE, bit 6 CUSTOM rules, bit 7 custom map prevGameTypeOptions: .byte $FF ; 0BA4 gameTypeOptions of the previous game; a difference forces the first-play flag back to 1 isSoloTrainer: .byte $FF ; 0BA5 bit 7 set = PRACTICE WITH SOLO TRAINER (the track 35 computer opponent is behind the $E000 jump table); clear = a modem game (track 34) prevSoloTrainerFlag: .byte $FF ; 0BA6 previous value of isSoloTrainer; a change from set to clear triggers the GET OPPONENT ON PHONE prompt diskLoadDepth: .byte $01 ; 0BA7 nesting counter bumped around every disk load and overlay swap; while it is non-zero the IRQ throws keypresses away gameEndReason: .byte $01 ; 0BA8 0 while a game runs; 1 = aborted / no game, $08 = time up, $A0/$C0 = the comcen of side 0/1 was knocked out mapNameText: .byte $A0 ; 0BA9 5-character map name ($0BA9-$0BAD) from the 32-letter alphabet at $890C - the printable form of mapSeed .byte $A0 ; 0BAA characters 2-5 of the printable map name .byte $A0,$A0,$A0 ; 0BAB ... mapNameTerminator: .byte $A0 ; 0BAE byte after mapNameText: $8D while it is printed, '!' | $80 when the name is hidden from the opponent mapNameBackup: .byte $A0 ; 0BAF 6-byte copy of mapNameText and its terminator, kept with prevMapSeed and restored when the same map is used again .byte $A0 ; 0BB0 the rest of the saved copy of the map name and its terminator .byte $A0,$A0,$A0,$A0 ; 0BB1 .... ; ---------------------------------------------------------------------- ; initIrqAndInputState - brings the interrupt and input state back to a known point. Called at cold ; start, on every return to the main menu and at the start of a game. It silences the sound, installs ; the raster IRQ, re-arms the one-shot 'SPOTTED RECYCLER' check, blanks the chat line and locks the ; keyboard and joystick out for 158 frames so that the keypress or click that got here is not read ; again by the screen the player is about to land in. ; In: nothing. ; Out: raster IRQ installed, inputLockoutTimer = $9E, chat line blank, gameEndCountdown / ; shiftHeldFlag / comcenStunStatus = 0, requestedScreenKey = $FF, the NOP restored at $3F57. ; Called from: mainStart $0A8A, returnToMainMenu $0AB2 and startGameFromSetup $0AD6. ; ---------------------------------------------------------------------- initIrqAndInputState: jsr stopAllSound ; 0BB5 silence all four sound slots jsr installRasterIrq ; 0BB8 point $FFFE at rasterIrqHandler and enable the raster interrupt lda #$EA ; 0BBB $EA = NOP... sta checkEnemyRecyclerSpotted; 0BBD ...restored over the first byte of checkEnemyRecyclerSpotted, which patches itself to RTS ($60) once it has fired, so the 'SPOTTED RECYCLER!' message can happen again in the next game lda #$9E ; 0BC0 $9E is used twice below: as a frame count and as a string terminator sta inputLockoutTimer ; 0BC2 158 frames (about 2.6 seconds) during which the IRQ ignores the keyboard and the joystick sta D_0549 ; 0BC5 and as the second byte of the chat line, where $9E is a bit-7 terminator that prints nothing - i.e. blank the incoming chat line lda #$00 ; 0BC8 0 into the three flags below sta gameEndCountdown ; 0BCA the end-of-game countdown is not running sta shiftHeldFlag ; 0BCD no SHIFT held as far as the game is concerned sta comcenStunStatus ; 0BD0 the comcen is not stunned lda #$FF ; 0BD3 $FF = no key sta requestedScreenKey ; 0BD5 forget any pending F-key screen request rts ; 0BD7 back to the caller ; ---------------------------------------------------------------------- ; disableCiaInterrupts - masks every interrupt source in both CIAs and acknowledges whatever was ; already pending. Writing $7F to an interrupt control register clears all five mask bits (bit 7 = 0 ; means 'clear the bits I name'), and reading the register clears the latched flags. ; In: nothing. ; Out: CIA1 and CIA2 will not raise IRQ or NMI; their pending flags cleared. ; Called from: mainStart $0A6D and loadCommModule $0F22 (before the trainer build is read in). ; ---------------------------------------------------------------------- disableCiaInterrupts: lda #$7F ; 0BD8 $7F = clear all five interrupt mask bits (bit 7 clear = 'clear the bits set below') sta CIA1_ICR ; 0BDA CIA1 raises the IRQ line - no more timer or keyboard interrupts sta CIA2_ICR ; 0BDD CIA2 raises the NMI line - no more serial or timer NMIs lda CIA1_ICR ; 0BE0 reading the register clears any interrupt already latched in CIA1 lda CIA2_ICR ; 0BE3 and in CIA2 rts ; 0BE6 back to the caller ; ---------------------------------------------------------------------- ; initInterruptVectors - finishes the interrupt set-up: it points the BRK escape of the raster handler ; at the normal IRQ exit and parks both the hardware NMI vector and the comm module's private copy of ; it on the bare RTI at $1298. The comm module later replaces its own copy with the modem's NMI ; routine when the UART is running. ; In: nothing. ; Out: the JMP operand at $10FF/$1100 = irqExit ($1293); $E031/$E032 and $FFFA/$FFFB = irqRti ($1298). ; Called from: mainStart $0A8D. ; ---------------------------------------------------------------------- initInterruptVectors: lda #$93 ; 0BE7 low byte of irqExit ($1293) sta L_10FE+1 ; 0BE9 operand of the JMP at $10FE, which rasterIrqHandler takes when the pushed status byte says BRK - so a stray BRK just restores the registers and RTIs lda #$12 ; 0BEC high byte of irqExit sta L_10FE+2 ; 0BEE second operand byte of that JMP lda #$98 ; 0BF1 low byte of irqRti ($1298) sta nmiChainVector ; 0BF3 the comm module's copy of the NMI vector, which its own handler chains through (also trainerAiE000:nmiChainVector) sta nmiVector ; 0BF6 and the hardware NMI vector itself lda #$12 ; 0BF9 high byte of irqRti sta nmiChainVectorHi ; 0BFB comm module copy, high byte (also trainerAiE000:nmiChainVectorHi) sta nmiVectorHi ; 0BFE hardware NMI vector, high byte - a RESTORE keypress now does nothing rts ; 0C01 back to the caller ; ---------------------------------------------------------------------- ; setNmiVectorRti - points the hardware NMI vector at the bare RTI at $1298. The solo trainer never ; uses an NMI, so loadCommModule jumps here after reading the trainer build in, to undo whatever the ; modem build had left in the vector. ; In: nothing. ; Out: $FFFA/$FFFB = $1298. ; Called from: loadCommModule $0F2E (JMP). ; ---------------------------------------------------------------------- setNmiVectorRti: lda #$98 ; 0C02 low byte of irqRti sta nmiVector ; 0C04 NMI vector low lda #$12 ; 0C07 high byte of irqRti sta nmiVectorHi ; 0C09 NMI vector high - an NMI now returns immediately L_0C0C: rts ; 0C0C shared RTS, also used by jumpToCd00IfPatched below ; ---------------------------------------------------------------------- ; jumpToCd00IfPatched - dead code. It looks at $CD00 and, if it finds a JMP opcode there, turns the ; VIC interrupt off and jumps into it. Nothing calls it, and at run time $CD00 is unitPictureHiTable ; inside the $C000 overlay, i.e. ordinary data, so the test would practically never pass. It reads ; like a development hook for jumping into a monitor or a test routine loaded over the overlay. ; In: $CD00. ; Out: normally just an RTS; if the byte were $4C it would disable the raster interrupt and jump ; there. ; Called from: nothing in any traced code. ; ---------------------------------------------------------------------- jumpToCd00IfPatched: lda unitPictureHiTable ; 0C0D first byte of the $C000 overlay's unit-picture table cmp #$4C ; 0C10 $4C = JMP absolute: has somebody planted a jump there? bne L_0C0C ; 0C12 no - return through the RTS at $0C0C lda #$00 ; 0C14 0 into VIC_IRQ_MASK sta VIC_IRQ_MASK ; 0C16 no raster interrupts, so the planted code owns the machine jmp unitPictureHiTable ; 0C19 and run it ; ---------------------------------------------------------------------- ; resetGameVariables - wipes the game's variables back to a cold state: zero page $15-$7F and $8C-$FF, ; the whole variable block $9008-$92FF, the eight-entry message queue and the last-key bytes. It ; deliberately leaves $00-$14 (the 6510 port and the loader's own scratch) and $80-$8B alone, and ; starts the variable block at $9008 so the VIC shadow registers at $9000-$9007 survive. Clearing the ; RNG state $57-$59 is intentional too: stepRandomSeed re-seeds it from the joystick as soon as it ; sees all three bytes zero. ; In: nothing. ; Out: zero page and $9008-$92FF cleared, messageQueue and lastTypedKey = $FF, newKeyEvent = $FF, ; gamePhase = $FF, filmPtr rewound to $D000, currentScreen = 3, the chat line blanked. ; Called from: startGameFromSetup $0AD3 and the setup block's initGameSystems ($0300). ; ---------------------------------------------------------------------- resetGameVariables: ldy #$15 ; 0C1C start at $0015 - $00-$14 belong to the 6510 port and the loader lda #$00 ; 0C1E the value written everywhere L_0C20: sta a:CPU_DDR,y ; 0C20 absolute,Y so it can reach the whole of page zero iny ; 0C23 next byte bpl L_0C20 ; 0C24 stop at $80, i.e. clear $15-$7F ldy #$8C ; 0C26 second run: $8C upwards ($80-$8B are left alone) L_0C28: sta a:CPU_DDR,y ; 0C28 clear it iny ; 0C2B next byte bne L_0C28 ; 0C2C until Y wraps at the end of page zero lda #$08 ; 0C2E $9008 is the first variable after the VIC shadow registers... sta mapCellPtr ; 0C30 ...low byte of the fill pointer lda #$90 ; 0C32 high byte... sta mapCellPtrHi ; 0C34 ...so the fill starts at $9008 lda #$FF ; 0C36 $92FF is the last variable... sta fillEndPtr ; 0C38 ...low byte of the limit lda #$92 ; 0C3A high byte... sta fillEndPtrHi ; 0C3C ...so the fill ends at $92FF ldy #$00 ; 0C3E the indirect store always uses offset 0 L_0C40: lda #$00 ; 0C40 zero sta (mapCellPtr),y ; 0C42 clear one variable byte jsr advanceFillPointer ; 0C44 step the pointer and set C when the limit is reached bcc L_0C40 ; 0C47 loop until the whole block is clear ldy #$07 ; 0C49 eight message queue slots... lda #$FF ; 0C4B ...filled with $FF = empty L_0C4D: sta messageQueue,y ; 0C4D clear queue slot Y dey ; 0C50 next slot bpl L_0C4D ; 0C51 until all eight are done sta lastTypedKey ; 0C53 $FF = nothing typed sta newKeyEvent ; 0C56 $FF = no key event pending sta gamePhase ; 0C59 $FF = not in the battle phase jsr rewindFilmPtr ; 0C5B rewind the game-film pointer to the start of the buffer under the I/O area ($D000) lda #$03 ; 0C5E screen 3... sta currentScreen ; 0C60 ...the menu/console screen is where the game goes next jmp clearChatStatusLine ; 0C63 blank the chat status line and return through it ; ---------------------------------------------------------------------- ; installRasterIrq - installs rasterIrqHandler as the interrupt routine. There is no KERNAL to go ; through: with $01 = $35 the hardware vector at $FFFE is the real one, and the handler rewrites it ; from the zero-page shadow on every pass, so a corrupted vector repairs itself. The interrupt is ; armed for raster line $FA (250), in the lower border below the 200 display lines, so all the ; per-frame work happens off screen. ; In: nothing. ; Out: $FFFE/$FFFF and zp_AB/zp_AC = $10F1, rasterIrqLine = $FA, irqInProgress = 0, VIC raster ; interrupt acknowledged and enabled. ; Called from: initIrqAndInputState $0BB8. ; ---------------------------------------------------------------------- installRasterIrq: sei ; 0C66 no interrupts while the vector is half written lda #$F1 ; 0C67 low byte of rasterIrqHandler ($10F1) sta irqVectorShadowLo ; 0C69 the shadow the handler writes back to $FFFE on every interrupt sta irqVector ; 0C6B and the live hardware vector lda #$10 ; 0C6E high byte of rasterIrqHandler sta irqVectorShadowHi ; 0C70 shadow high byte sta irqVectorHi ; 0C72 hardware vector high byte lda #$FA ; 0C75 raster line 250, in the lower border sta rasterIrqLine ; 0C77 the handler re-arms VIC_RASTER from this every time sta VIC_RASTER ; 0C79 arm the raster compare now lda #$00 ; 0C7C 0 into irqInProgress sta irqInProgress ; 0C7E the handler's re-entrancy guard starts clear cli ; 0C80 interrupts on lda #$01 ; 0C81 bit 0 = the raster interrupt sta VIC_IRQ_FLAG ; 0C83 acknowledge anything already latched sta VIC_IRQ_MASK ; 0C86 and enable raster interrupts rts ; 0C89 back to the caller ; comcenUnitIndexBySide - the unit index of each side's COMCEN: 49 ($31) for side 0, 99 ($63) for side ; 1, the last unit of each side's 50. Indexed by the side number, so the code can reach either ; command centre's row in the unit arrays (energy, stun, position) without a multiply. Phase 1 called ; this perSideValueTable; ; $7BF2 in the setup overlay copies the entry straight into comcenUnit ($9236). comcenUnitIndexBySide: .byte $31,$63 ; 0C8A 1c side 0's comcen is unit 49, side 1's is unit 99 ; ---------------------------------------------------------------------- ; showModemSetupPrompt - puts a caller-supplied line above 'PRESS SPACE WHEN READY.' and waits for the ; space bar. The setup overlay uses it for the modem dialogue ('PICK UP PHONE THEN PRESS SPACE.' and ; friends). ; In: A/Y = the string to show in message slot $20. ; Out: message slots $20 and $21 patched, both messages shown, then it tail-jumps into waitForSpaceKey ; ($7DB8) in the overlay, so it never returns here. ; Called from: game/mapGenerator6F00 $79E3. ; ---------------------------------------------------------------------- showModemSetupPrompt: ldx #$20 ; 0C8C message slot $20 = the run-time slot for the caller's line jsr setMessageSlot ; 0C8E patch its text pointer to A/Y ldx #$21 ; 0C91 message slot $21 = the second run-time slot lda #$37 ; 0C93 low byte of msgPressSpaceWhenReady ($8837) ldy #$88 ; 0C95 high byte jsr setMessageSlot ; 0C97 patch slot $21 to it jsr resetMessagesAndBeep ; 0C9A show slot $20, beep twice and arm slot $21 as the fallback line jmp waitForSpaceKey ; 0C9D wait for the space bar in the setup overlay; that routine returns to our caller ; ---------------------------------------------------------------------- ; resetMessagesAndBeep - clears the message queue, shows message slot $20 immediately, beeps twice a ; third of a second apart and makes slot $21 the line the status bar falls back to once slot $20 has ; had its time. The double beep is what tells the player across the room that the game wants ; something. ; In: message slots $20 and $21 already patched by the caller. ; Out: queue empty, slot $20 displayed, persistentMessageId = $21. ; Called from: showModemSetupPrompt $0C9A and waitForAnswerOriginateKey $0CBD. ; ---------------------------------------------------------------------- resetMessagesAndBeep: jsr resetMessageQueue ; 0CA0 drop anything already queued lda #$20 ; 0CA3 message id $20 = the first run-time slot jsr queueMessage ; 0CA5 put it on the status line lda #$03 ; 0CA8 sound 3 = the attention beep jsr playSound ; 0CAA play it lda #$14 ; 0CAD 20 frames = a third of a second jsr waitFrames ; 0CAF wait, so the two beeps are heard as two lda #$03 ; 0CB2 sound 3 again jsr playSound ; 0CB4 play it ldx #$21 ; 0CB7 message id $21 = the second run-time slot stx persistentMessageId ; 0CB9 it becomes the line the status bar returns to when the queue drains rts ; 0CBC back to the caller ; ---------------------------------------------------------------------- ; waitForAnswerOriginateKey - the ANSWER/ORIGINATE question at the start of a modem game. It shows ; the two prompt lines, then spins until the player types A or O, and tells the comm module which of ; the two Hayes command strings to use ($E011 indexes the modem's init-string table). The spin also ; stirs the game RNG, so the moment the player answers seeds the map. ; In: message slots $20/$21 already patched by the caller; lastTypedKey fed by the raster IRQ. ; Out: $E011 = 0 (answer) or 1 (originate), inputLockoutTimer = 0, message queue emptied. ; Called from: startGameFromSetup $0B29. ; ---------------------------------------------------------------------- waitForAnswerOriginateKey: jsr resetMessagesAndBeep ; 0CBD show the prompt and beep twice lda #$FF ; 0CC0 $FF = nothing typed sta lastTypedKey ; 0CC2 forget any key that was typed before the prompt went up lda #$00 ; 0CC5 0 into inputLockoutTimer sta inputLockoutTimer ; 0CC7 cancel the 158-frame lockout so the IRQ passes the keystroke through at once L_0CCA: jsr updateStatusMessage ; 0CCA keep the status line alive while waiting jsr nextGameRandom ; 0CCD stir the 24-bit game RNG: the human reaction time is the entropy source ldx #$00 ; 0CD0 X = 0 = ANSWER ldy lastTypedKey ; 0CD2 the last key the IRQ recorded cpy #$C1 ; 0CD5 $C1 = 'A' (PETSCII with bit 7 set) = answer the call beq L_0CDE ; 0CD7 got it inx ; 0CD9 X = 1 = ORIGINATE cpy #$CF ; 0CDA $CF = 'O' = place the call bne L_0CCA ; 0CDC neither - keep waiting L_0CDE: stx isOriginateMode ; 0CDE tell the modem driver which mode it is in; it indexes its init-string table with this (also trainerAiE000:isOriginateMode) jmp resetMessageQueue ; 0CE1 clear the prompt away and return through resetMessageQueue ; strOurs - the four characters 'OURS', copied over the owner-name field when the disk carries no ; name. It has no bit-7 terminator because it is never printed directly: buildOwnerNameString copies ; it into ownPlayerName and the terminator is added to the assembled line afterwards. strOurs: .byte "OURS" ; 0CE4 default owner name for an unpersonalised disk ; ---------------------------------------------------------------------- ; buildOwnerNameString - assembles the 21-character line that names the two players on the menu panel, ; out of the two 10-byte name fields at the top of memory: ownPlayerName ($FFDE), copied out of the ; disk's BAM by checkDiskId, and opponentPlayerName ($FFE8), which the modem driver fills in from the ; other machine. The first name is packed to the right of the first ten columns and the second to the ; left of the last ten, so the two meet either side of the single space in the middle. Trailing ; blanks of the first name and leading blanks of the second are dropped ($FFDE holds shifted spaces, ; $A0, which the AND #$7F turns into ordinary spaces). ; In: ownPlayerName and opponentPlayerName, 10 bytes each. ; Out: ownerNameString ($0B7F-$0B93) filled and terminated with bit 7 set on its last byte; ; ownPlayerName replaced by 'OURS' when it was blank. ; Called from: startGameFromSetup $0B77. ; ---------------------------------------------------------------------- buildOwnerNameString: ldy #$14 ; 0CE8 21 characters, $0B7F-$0B93 lda #$20 ; 0CEA a plain space L_0CEC: sta ownerNameString,y ; 0CEC blank the whole line first dey ; 0CEF next character bpl L_0CEC ; 0CF0 until all 21 are blank lda ownPlayerName ; 0CF2 first character of the owner name out of the disk BAM cmp #$A0 ; 0CF5 $A0 = shifted space = the field was never personalised bne L_0D04 ; 0CF7 the disk has a name - use it ldy #$03 ; 0CF9 four characters of 'OURS' L_0CFB: lda strOurs,y ; 0CFB copy them... sta ownPlayerName,y ; 0CFE ...over the first four bytes of the owner-name field dey ; 0D01 next character bpl L_0CFB ; 0D02 until all four are in ; ---------------------------------------------------------------------- ; First name, right aligned: walk it backwards into columns 9..0 of the line. scratch18 stays 0 until ; the first real character has been placed and is negative afterwards, so blanks at the end of the ; field are skipped while blanks inside the name are kept. ; ---------------------------------------------------------------------- L_0D04: lda #$00 ; 0D04 0 into the 'seen a character yet' flag sta scratch18 ; 0D06 scratch18 = 0 means nothing placed so far ldy #$09 ; 0D08 last character of the 10-byte field ldx #$09 ; 0D0A column 9 = the rightmost of the first half of the line L_0D0C: lda ownPlayerName,y ; 0D0C one character of the owner name and #$7F ; 0D0F clear bit 7: shifted characters ($A0 etc.) become plain ones cmp #$20 ; 0D11 is it a space? bne L_0D19 ; 0D13 no - place it bit scratch18 ; 0D15 have any characters been placed yet? bpl L_0D1F ; 0D17 no - this is a trailing blank, drop it L_0D19: sta ownerNameString,x ; 0D19 place the character in the line dex ; 0D1C one column to the left dec scratch18 ; 0D1D make scratch18 negative: from now on blanks count as part of the name L_0D1F: dey ; 0D1F previous character of the field bpl L_0D0C ; 0D20 until the whole field has been scanned ; ---------------------------------------------------------------------- ; Second name, left aligned: columns 11..20, forwards through the field, with the same blank-squeezing ; rule - so this drops the leading blanks. Column 10 stays a space and separates the two names. ; ---------------------------------------------------------------------- ldx #$0B ; 0D22 column 11, just past the separating space ldy #$00 ; 0D24 first character of the opponent's name sty scratch18 ; 0D26 nothing placed yet L_0D28: lda opponentPlayerName,y ; 0D28 one character of the opponent name and #$7F ; 0D2B clear bit 7 cmp #$20 ; 0D2D is it a space? bne L_0D35 ; 0D2F no - place it bit scratch18 ; 0D31 have any characters been placed yet? bpl L_0D3B ; 0D33 no - this is a leading blank, drop it L_0D35: sta ownerNameString,x ; 0D35 place the character inx ; 0D38 one column to the right dec scratch18 ; 0D39 make scratch18 negative so inner blanks survive L_0D3B: iny ; 0D3B next character of the field cpy #$0A ; 0D3C ten characters in the field bcc L_0D28 ; 0D3E loop until they are all scanned lda ownerNameEnd ; 0D40 the last byte of the line ($0B93) ora #$80 ; 0D43 set bit 7 on it... sta ownerNameEnd ; 0D45 ...which is how every string in this game ends rts ; 0D48 back to the caller ; ---------------------------------------------------------------------- ; setJoystickRepeatDelay - the joystick auto-repeat used by every menu and by the map scrolling. The ; caller sets a direction going and calls this to arm the delay before it happens again: 16 frames for ; the first repeat, 6 frames afterwards, so a nudge moves one step and a held stick runs. The raster ; IRQ clears joyRepeatMask when the stick returns to centre, which forces joyHoldCounter back to 0 and ; so restores the long first delay. A is preserved so callers can keep a value in it across the call. ; In: joyRepeatMask ($91C9), joyHoldCounter ($91CA). ; Out: joyRepeatTimer (zp_6C) = 16 or 6, joyHoldCounter stepped, joyRepeatMask = $7F, A unchanged. ; Called from: $20E7, $2230 and $60AF in this file and $8090/$849E in the setup overlay. ; ---------------------------------------------------------------------- setJoystickRepeatDelay: ldy #$10 ; 0D49 16 frames: the pause before a held direction starts repeating pha ; 0D4B the caller's A must survive inc joyHoldCounter ; 0D4C count this step lda joyHoldCounter ; 0D4F read the counter back and joyRepeatMask ; 0D52 the IRQ clears this mask to 0 while the stick is centred, which zeroes the counter sta joyHoldCounter ; 0D55 so the counter only grows while the stick is held cmp #$01 ; 0D58 was this the first step after centring? bcc L_0D5E ; 0D5A yes - keep the long 16-frame delay ldy #$06 ; 0D5C no - 6 frames between repeats L_0D5E: sty joyRepeatTimer ; 0D5E arm the countdown; the raster IRQ decrements it every frame lda #$7F ; 0D60 $7F lets the counter run again... sta joyRepeatMask ; 0D62 ...until the IRQ sees the stick centred and clears the mask pla ; 0D65 give the caller its A back rts ; 0D66 back to the caller ; keyMatrixCodeTable - the whole C64 keyboard as 64 bytes, indexed by (port A row) * 8 + (port B ; column bit), which is exactly the order scanKeyboard finds a key in. Ordinary keys are their ASCII ; code with bit 7 set (so 'A' is $C1 and '3' is $B3), the four function keys are 0-3 because the game ; uses them as screen numbers (F1 = 0 map, F3 = 1 missile, F5 = 2 drone, F7 = 3 menu), and the control ; keys are ASCII control codes with bit 7 set: DEL $88, RETURN $8D, CRSR DOWN $8B, HOME $8C, CRSR ; RIGHT $8E, RUN/STOP $93, left arrow $9B, pound $83. Two magic entries: $FE marks the three modifier ; cells (both SHIFTs and the Commodore key), which the scanner tests separately and skips here, and ; $FF marks CTRL, which makes the scan report 'no key' - the game has no use for CTRL and treats ; holding it as a way to block the keyboard. keyMatrixCodeTable: .byte $88,$8D,$8E,$03,$00,$01,$02,$8B; 0D67 ........ row 0 (PA0): DEL, RETURN, CRSR RIGHT, F7=3, F1=0, F3=1, F5=2, CRSR DOWN .byte $B3 ; 0D6F row 1 (PA1): 3 W A 4 Z S E, then LEFT SHIFT ($FE = handled separately) .byte $D7,$C1,$B4,$DA,$D3,$C5,$FE; 0D70 ....... .byte $B5 ; 0D77 row 2 (PA2): 5 R D 6 C F T X .byte $D2,$C4,$B6,$C3,$C6,$D4,$D8; 0D78 ....... .byte $B7 ; 0D7F row 3 (PA3): 7 Y G 8 B H U V .byte $D9,$C7,$B8,$C2,$C8,$D5,$D6; 0D80 ....... .byte $B9 ; 0D87 row 4 (PA4): 9 I J 0 M K O N .byte $C9,$CA,$B0,$CD,$CB,$CF,$CE; 0D88 ....... .byte $AB ; 0D8F row 5 (PA5): + P L - . : @ , .byte $D0,$CC,$AD,$AE,$BA,$C0,$AC; 0D90 ....... .byte $83 ; 0D97 row 6 (PA6): pound, *, ;, HOME, RIGHT SHIFT ($FE), =, up arrow, / .byte $AA,$BB,$8C,$FE,$BD,$DE,$AF; 0D98 ....... .byte $B1 ; 0D9F row 7 (PA7): 1, left arrow, CTRL ($FF = report no key), 2, SPACE, C= ($FE), Q, RUN/STOP .byte $9B,$FF,$B2,$A0,$FE,$D1,$93; 0DA0 ....... ; shiftedKeyTable - what a key produces when SHIFT is held, for the sixteen codes $AC-$BB (',' '-' '.' ; '/', the digits '0'-'9', ':' and ';'); pollKeyboardEvent indexes it with keycode - $AC. The digits ; give the usual C64 symbols !"#$%&'() and shifted '0' stays '0', ',' '.' '/' become '<' '>' '?' and ; '-' stays '-'. The last two entries repeat those of '8' and '9', so shifted ':' and ';' come out as ; '(' and ')' instead of the brackets a C64 would give - the game's font has no square brackets. ; Letters are not in the table: SHIFT does not change them, the font has only one case. shiftedKeyTable: .byte $BC,$AD,$BE,$BF,$B0,$A1,$A2,$A3; 0DA7 ........ shifted , - . / 0 1 2 3 -> < - > ? 0 ! " # .byte $A4 ; 0DAF shifted 4 5 6 7 8 9 : ; -> $ % & ' ( ) ( ) .byte $A5,$A6,$A7,$A8,$A9,$A8,$A9; 0DB0 ....... ; ---------------------------------------------------------------------- ; scanKeyboard - the game's own keyboard matrix scanner; there is no KERNAL to ask, since the ROMs are ; banked out. It is not called directly: initCommModule patches its address into the comm module's ; key vector ($E013/$E014), and pollKeyboardEvent reaches it once a frame through the module's $E015 ; entry, which lets the modem driver steal keys for its Commodore-key hot keys first. The scan first ; makes sure no joystick is confusing the matrix (a stick in port 1 pulls column lines low, one in ; port 2 pulls row lines low, and either looks exactly like a key), then tests the two SHIFT keys and ; the Commodore key as modifiers, and finally walks rows 7..0 and columns 7..0 and translates the ; first key it finds through keyMatrixCodeTable. Only one key is ever reported - the game needs no ; rollover. ; In: CIA1 ports; the row masks at D_2F8D ($2F8D) and the column bit masks at spriteBitMasks ($2F85). ; Out: A = key code or $FF for none, Y = $80 when a SHIFT is held (else 0), X = $80 when the Commodore ; key is held (else 0). It self-modifies the operands at $0E2B, $0E3A, $0E41 and $0E43, and ; leaves CIA1_DDRA = $FF, CIA1_DDRB = $00 and the last row mask in CIA1_PRA - the raster IRQ ; relies on that to read joystick 2 immediately afterwards. ; Called from: game/modemDriverE000 and game/trainerAiE000 through the patched vector at $E013 ; ($E012/$E015). ; ---------------------------------------------------------------------- scanKeyboard: ldx #$00 ; 0DB7 0 into both result operands below stx L_0E40+1 ; 0DB9 the LDY operand at $0E41: assume no SHIFT is held stx L_0E42+1 ; 0DBC the LDX operand at $0E43: assume the Commodore key is not held stx CIA1_DDRB ; 0DBF port B all inputs - it reads the eight key columns lda #$FF ; 0DC2 $FF into port A and its direction register sta CIA1_DDRA ; 0DC4 port A all outputs - it selects the key rows (a 0 bit selects a row) sta CIA1_PRA ; 0DC7 no row selected at all cmp CIA1_PRB ; 0DCA with no row selected every column should read high beq L_0DD2 ; 0DCD it does - the matrix is undisturbed L_0DCF: jmp L_0E3E ; 0DCF a column is being pulled low with no row selected: that is a joystick in port 1, so report 'no key' L_0DD2: cmp CIA1_PRA ; 0DD2 port A should also read back the $FF it is driving bne L_0DCF ; 0DD5 it does not: a joystick in port 2 is pulling a row line low, so the scan would lie - report 'no key' stx CIA1_PRA ; 0DD7 X is still 0: select all eight rows at once cmp CIA1_PRB ; 0DDA if any key at all is down, some column now reads low beq L_0DCF ; 0DDD nothing is down - report 'no key' without scanning ; ---------------------------------------------------------------------- ; The three modifier keys are wired into the matrix like any other key, so they have to be tested by ; name before the general scan, which skips their cells ($FE in keyMatrixCodeTable). ; ---------------------------------------------------------------------- ldy #$BF ; 0DDF row mask $BF = PA6 low (the row with RIGHT SHIFT) lda #$10 ; 0DE1 column bit 4 = RIGHT SHIFT jsr testKeyMask ; 0DE3 is it down? beq L_0DF1 ; 0DE6 yes - record a shift ldy #$FD ; 0DE8 row mask $FD = PA1 low (the row with LEFT SHIFT) lda #$80 ; 0DEA column bit 7 = LEFT SHIFT jsr testKeyMask ; 0DEC is that one down? bne L_0DF6 ; 0DEF neither shift is down L_0DF1: lda #$80 ; 0DF1 $80 = shift held sta L_0E40+1 ; 0DF3 patch it into the LDY at $0E40, which returns the shift flag in Y L_0DF6: ldy #$7F ; 0DF6 row mask $7F = PA7 low (the row with the Commodore key) lda #$20 ; 0DF8 column bit 5 = the Commodore key jsr testKeyMask ; 0DFA is it down? bne L_0E04 ; 0DFD no lda #$80 ; 0DFF $80 = Commodore key held sta L_0E42+1 ; 0E01 patch it into the LDX at $0E42, which returns that flag in X ; ---------------------------------------------------------------------- ; The scan proper: eight rows, eight columns each, from the top down, stopping at the first key that ; is down. So when two keys are held the one with the higher row (and within a row the higher column) ; wins. ; ---------------------------------------------------------------------- L_0E04: ldx #$07 ; 0E04 start at row 7 L_0E06: ldy #$07 ; 0E06 start at column 7 lda invertedBitMasks,x ; 0E08 the row select pattern for this row (one bit low) sta CIA1_PRA ; 0E0B select it on port A L_0E0E: lda CIA1_PRB ; 0E0E read the columns... cmp CIA1_PRB ; 0E11 ...and read them again bne L_0E0E ; 0E14 until two reads agree - cheap debounce against key bounce and the raster's noise and spriteBitMasks,y ; 0E16 test the single column bit for this column beq L_0E23 ; 0E19 zero means that key is down L_0E1B: dey ; 0E1B next column bpl L_0E0E ; 0E1C until all eight of this row are done dex ; 0E1E next row bpl L_0E06 ; 0E1F until all eight rows are done bmi L_0E3E ; 0E21 nothing found anywhere - report 'no key' L_0E23: txa ; 0E23 the row number... asl a ; 0E24 ...times two... asl a ; 0E25 ...times four... asl a ; 0E26 ...times eight = the start of that row in the code table sty L_0E2A+1 ; 0E27 patch the column number into the ORA operand at $0E2B L_0E2A: ora #$FF ; 0E2A operand patched just above: row * 8 + column = the index into keyMatrixCodeTable stx L_0E39+1 ; 0E2C save the row number in the LDX operand at $0E3A, because X is about to be reused as the table index tax ; 0E2F index the table lda keyMatrixCodeTable,x ; 0E30 the key code for the key that is down cmp #$FE ; 0E33 $FE and $FF are the two magic entries bcc L_0E40 ; 0E35 a real key code - return it bne L_0E3E ; 0E37 $FF = CTRL: pretend nothing is pressed L_0E39: ldx #$FF ; 0E39 $FE = one of the modifier cells: restore the row number (operand patched at $0E2C)... jmp L_0E1B ; 0E3B ...and carry on scanning from the next column L_0E3E: lda #$FF ; 0E3E $FF = no key L_0E40: ldy #$FF ; 0E40 operand patched at $0DB9/$0DF3: Y = $80 when a shift is held, 0 otherwise L_0E42: ldx #$FF ; 0E42 operand patched at $0DBC/$0E01: X = $80 when the Commodore key is held, 0 otherwise rts ; 0E44 back to pollKeyboardEvent (through the comm module) ; ---------------------------------------------------------------------- ; testKeyMask - tests one named key for scanKeyboard: it selects the row given in Y, reads the columns ; twice until two reads agree, and masks out the single column bit the caller passed in A. The mask ; is not kept in a register but patched into the AND operand at $0E54, so both registers stay free. ; In: Y = the port A row select pattern (one bit low), A = the column bit mask. ; Out: Z = 1 when that key is down; A = the masked port B reading; the operand at $0E54 patched. ; Called from: scanKeyboard $0DE3 (right shift), $0DEC (left shift) and $0DFA (Commodore key). ; ---------------------------------------------------------------------- testKeyMask: sta L_0E53+1 ; 0E45 patch the caller's column mask into the AND at $0E53 sty CIA1_PRA ; 0E48 select the row L_0E4B: lda CIA1_PRB ; 0E4B read the columns... cmp CIA1_PRB ; 0E4E ...and read them again bne L_0E4B ; 0E51 until two reads agree L_0E53: and #$FF ; 0E53 keep only the column of the key we are asking about (operand patched at $0E45) rts ; 0E55 Z = 1 means the key is down ; ---------------------------------------------------------------------- ; pollKeyboardEvent - the keyboard front end, run once per frame from the raster IRQ (and only from ; there). It asks the comm module for a key - which reaches scanKeyboard, after the modem driver has ; had its chance to swallow Commodore-key hot keys - and turns the raw 'this key is down' answer into ; events: a key that is the same as last frame is ignored until the 20-frame repeat timer runs out, so ; a held key repeats about three times a second. The four function keys (codes 0-3) are screen ; requests: they go only into requestedScreenKey (zp_66) and explicitly clear newKeyEvent, because ; nothing may type them into a text field. Every other key (all codes have bit 7 set) becomes a ; newKeyEvent for the line editors, a lastTypedKey for the menus, and is also written into ; requestedScreenKey, which cancels any stale F-key request. When a shift is held the sixteen ; punctuation and digit codes $AC-$BB are replaced from shiftedKeyTable. ; In: the comm module key entry $E015 -> scanKeyboard (A = code, Y = shift, X = Commodore); ; prevKeyCode, keyRepeatTimer. ; Out: rawKeyCode/shiftHeldFlag/commodoreHeldFlag ($92A0/$92E4/$92E5) = the raw scan, newKeyEvent ; ($90EA), prevKeyCode ($90E9), lastTypedKey ($9248), requestedScreenKey (zp_66) and ; keyRepeatTimer (zp_70). ; Called from: rasterIrqHandler $11E7 (only when the input lockout timer has expired). ; ---------------------------------------------------------------------- pollKeyboardEvent: jsr commKeyEntry ; 0E56 read a key through the comm module, which vectors on to scanKeyboard (also trainerAiE000:commReadKeyEntry) sta rawKeyCode ; 0E59 keep the raw code the scanner returned sty shiftHeldFlag ; 0E5C Y = $80 when a shift was held during that scan stx commodoreHeldFlag ; 0E5F X = $80 when the Commodore key was held cmp #$FF ; 0E62 $FF = no key is down beq L_0EA4 ; 0E64 nothing pressed - clear the pending state cmp prevKeyCode ; 0E66 the same key as the previous frame? bne L_0E6F ; 0E69 no - a genuinely new keypress ldy keyRepeatTimer ; 0E6B the repeat countdown the IRQ decrements every frame bne L_0EAB ; 0E6D still running: the key is being held, do not report it again yet L_0E6F: ldy #$14 ; 0E6F 20 frames until a held key repeats sty keyRepeatTimer ; 0E71 arm the repeat timer sta newKeyEvent ; 0E73 publish the key as this frame's event sta prevKeyCode ; 0E76 remember it so the next frame can tell 'held' from 'pressed again' cmp #$80 ; 0E79 codes below $80 are only the four function keys bcc L_0E9B ; 0E7B F1/F3/F5/F7 are handled separately below sta lastTypedKey ; 0E7D everything else is a typed character: the menus read it here sta requestedScreenKey ; 0E80 and into the screen-request byte, where a printable code reads as 'no screen change wanted' and so cancels a stale F-key request bit shiftHeldFlag ; 0E82 was a shift held? bpl L_0E9A ; 0E85 no - the code stands as it is sec ; 0E87 prepare an exact subtraction sbc #$AC ; 0E88 $AC = ',' is the first code that has a shifted form bcc L_0E9A ; 0E8A below the table - letters and control codes are unaffected cmp #$10 ; 0E8C sixteen entries, $AC-$BB bcs L_0E9A ; 0E8E above the table - unaffected tax ; 0E90 the difference is the table index lda shiftedKeyTable,x ; 0E91 the shifted form of that key sta newKeyEvent ; 0E94 replace this frame's event... sta lastTypedKey ; 0E97 ...and the typed character with it L_0E9A: rts ; 0E9A back to the IRQ L_0E9B: tay ; 0E9B hold the function key number (0-3) in Y lda #$FF ; 0E9C $FF = no character event... sta newKeyEvent ; 0E9E ...so a screen key can never be typed into a text field sty requestedScreenKey ; 0EA1 the screen the player asked for: 0 map, 1 missile, 2 drone, 3 menu rts ; 0EA3 back to the IRQ L_0EA4: lda #$FF ; 0EA4 $FF = none sta requestedScreenKey ; 0EA6 no screen change pending sta prevKeyCode ; 0EA8 and no previous key, so the very next press counts as new L_0EAB: lda #$FF ; 0EAB $FF = nothing to report sta newKeyEvent ; 0EAD clear this frame's key event rts ; 0EB0 back to the IRQ ; ---------------------------------------------------------------------- ; checkFirePressed - the game's 'has the player clicked?' primitive; almost every interactive loop ; spins on it with BNE. It reports a click only on the edge: the button must have been seen released ; since the last accepted click (fireButtonArmed) and must be down now. An accepted click arms a ; 10-frame debounce and plays the click sound. It also refuses clicks when the outgoing command ring ; has less than five bytes free, which throttles the player instead of overflowing the link. ; In: joyFireUp (zp_5F, 0 = button down), fireButtonArmed (zp_79), fireDebounceTimer (zp_6D), the ; command ring indices through isCmdRingNearlyFull. ; Out: A = 0 (Z=1) for a fresh click, A = 1 otherwise; fireDebounceTimer = 10 and sound 1 queued on a ; click; fireButtonArmed maintained. ; Called from: about a dozen loops - $0ED5, $1470, $21E7, $5E68, $608E, $6226, $63FB here, ; $7751/$806F/$8127/$84A4/$878D in the overlays and $C865 in the text engine. ; ---------------------------------------------------------------------- checkFirePressed: jsr isCmdRingNearlyFull ; 0EB1 C=1 when fewer than five bytes are free in the outgoing command ring bcs L_0EC0 ; 0EB4 the link cannot take another order - swallow the click lda fireDebounceTimer ; 0EB6 frames left of the debounce after the last accepted click bne L_0EC0 ; 0EB8 still counting down - ignore the button lda joyFireUp ; 0EBA 0 = the button is down right now ora fireButtonArmed ; 0EBC the latch that says the button has been released since the last click bne L_0EC3 ; 0EBE either of them non-zero means there is something to look at L_0EC0: lda #$01 ; 0EC0 A = 1: no click this time rts ; 0EC2 callers loop on BNE, so this keeps them waiting L_0EC3: lda joyFireUp ; 0EC3 is the button actually down? bne L_0ED2 ; 0EC5 no - it has just been released, so re-arm the latch lda #$0A ; 0EC7 10 frames... sta fireDebounceTimer ; 0EC9 ...during which further clicks are ignored (contact bounce and human double taps) lda #$01 ; 0ECB sound 1 = the UI click jsr playSound ; 0ECD play it so the player hears the press lda #$00 ; 0ED0 A = 0: a fresh click, and 0 also disarms the latch below L_0ED2: sta fireButtonArmed ; 0ED2 store 0 (click taken) or the button-up state (latch armed) rts ; 0ED4 A tells the caller which of the two happened ; ---------------------------------------------------------------------- ; checkFirePressedInteractive - checkFirePressed plus a 'may the player act at all?' gate, used by the ; loops that turn a click into an order. It reports a click only when a game film is not being ; watched (filmPlaybackMode bit 7) and the game has not already ended (gameOverFlags bit 6, which is ; set when a game is over or aborted). Phase 1 called this checkFirePressedNotSolo, which is wrong: ; the solo trainer leaves filmPlaybackMode clear and its player clicks normally. ; In: the same joystick state as checkFirePressed, plus filmPlaybackMode ($0B9B) and gameOverFlags ; ($923F). ; Out: A = 0 only for an accepted click during live play; non-zero otherwise. ; Called from: $2076 here and $75FA/$8043 in the $6F00 overlays. ; ---------------------------------------------------------------------- checkFirePressedInteractive: jsr checkFirePressed ; 0ED5 the ordinary edge-detected click bne L_0EE4 ; 0ED8 no click - return non-zero without looking further ; ---------------------------------------------------------------------- ; Shared entry: isPlayerInputAllowed ($0EE5) jumps in here with A = 0 to ask the same question without ; testing the button. ; ---------------------------------------------------------------------- L_0EDA: lda filmPlaybackMode ; 0EDA watching a game film? bmi L_0EE4 ; 0EDD then the player is a spectator: the click does not count (A is negative, i.e. non-zero) lda gameOverFlags ; 0EDF the flags set when the game has ended or been aborted and #$40 ; 0EE2 bit 6 blocks all further player commands; clear = the click is accepted (A = 0) L_0EE4: rts ; 0EE4 Z=1 only for an accepted click in a live game ; ---------------------------------------------------------------------- ; isPlayerInputAllowed - the gate half of checkFirePressedInteractive without the button test: it ; jumps into that routine at $0EDA with A = 0, so it answers 'may the local player still act?' - Z=1 ; when no film is being watched and the game has not ended. Phase 1 named it getSoloOrFlag923F and ; thought it was unreferenced; the REPAIR tab uses it to decide whether the selected system may be ; repaired. ; In: filmPlaybackMode ($0B9B), gameOverFlags ($923F). ; Out: Z=1 (A = 0) when the player may act. ; Called from: $658B, the REPAIR tab menu loop. ; ---------------------------------------------------------------------- isPlayerInputAllowed: lda #$00 ; 0EE5 A = 0 stands in for 'the button was pressed' jmp L_0EDA ; 0EE7 run only the film/game-over test of checkFirePressedInteractive ; ---------------------------------------------------------------------- ; isFireHeld - the level-triggered version of checkFirePressed: no ring-buffer check, no debounce ; reload and no click sound, so it keeps answering 0 for as long as the button is held (once it has ; been seen released). Used by the two loops where an action has to run while the button is down - ; the map screen's group box at $2376 and the console at $407A. ; In: joyFireUp (zp_5F), fireButtonArmed (zp_79), fireDebounceTimer (zp_6D). ; Out: A = 0 while the button is held, A = 1 otherwise; fireButtonArmed re-armed when the button is ; up. ; Called from: $2376 and $407A. ; ---------------------------------------------------------------------- isFireHeld: lda fireDebounceTimer ; 0EEA still inside the debounce of an earlier click? bne L_0EF4 ; 0EEC then say 'not held' lda joyFireUp ; 0EEE 0 = the button is down ora fireButtonArmed ; 0EF0 the released-since-last-time latch bne fireArmOrHeld ; 0EF2 something to look at L_0EF4: lda #$01 ; 0EF4 A = 1: the button is not held (or has never been released) rts ; 0EF6 back to the caller ; ---------------------------------------------------------------------- ; Tail of isFireHeld ($0EEA): reached when the button is up, or is down and has already been seen ; released once (fireButtonArmed set). A=0 on return means 'the player is holding fire'. ; ---------------------------------------------------------------------- fireArmOrHeld: lda joyFireUp ; 0EF7 re-read the latched fire state: 0 = button down, 1 = button up beq L_0EFD ; 0EF9 button still down (and armed) -> return A=0 = held sta fireButtonArmed ; 0EFB button is up: arm the latch so the next press counts, and return A=1 = not held L_0EFD: rts ; 0EFD back to the caller; Z=1 means fire is being held ; ---------------------------------------------------------------------- ; stopAllSound - Silences every SID voice by requesting sound id 0, which requestSound turns into ; $FF/$FF/$FF/$00 in the four sound-request slots $92E7-$92EA so the IRQ dequeues 'sound 0' and the ; player releases all three voices. ; In: none ; Out: $92E7-$92EA reset, all voices silenced ; Called from: initIrqAndInputState ($0BB5) and $1AF7 (end of game) ; ---------------------------------------------------------------------- stopAllSound: lda #$00 ; 0EFE sound id 0 = the 'silence everything' request jmp playSound ; 0F00 tail-call the $C000 sound entry (playSound preserves X/Y and returns to our caller) ; ---------------------------------------------------------------------- ; loadStartupOverlays - One-shot start-up loader: fast-loads track 30 sectors 0-15 into $C000-$CFFF ; (the text/glyph engine, which replaces the boot loader that lived there), then loadOverlayA - which ; chains the message strings and the comm tail - and falls straight into loadCommModule. ; In: isSoloTrainer ($0BA5), read further down the chain ; Out: $C000-$CFFF, $6F00-$87FF variant A, $8800-$89FF, $EC00-$EFFF, $E000-$EBFF, comm module ; initialised ; Called from: mainStart ($0A81), once ; ---------------------------------------------------------------------- loadStartupOverlays: ldx #$00 ; 0F03 destination low byte $00 ... ldy #$C0 ; 0F05 ... and high byte $C0: the transfer lands at $C000 jsr setLoadDest ; 0F07 store both into the loader's self-modifying destination operands ldy #$1E ; 0F0A track $1E = 30, the text/glyph engine overlay lda #$10 ; 0F0C $10 = 16 sectors = 4 KB -> $C000-$CFFF ldx #$00 ; 0F0E starting at sector 0 jsr loadSectors ; 0F10 pull it in over the boot loader that is still sitting at $C000 jsr loadOverlayA ; 0F13 then the $6F00 overlay variant A, the $8800 strings and the $EC00 comm tail ; ---------------------------------------------------------------------- ; loadCommModule - Loads the opponent module behind the $E000 jump table and initialises it. With ; isSoloTrainer ($0BA5) bit 7 set it disables the CIA interrupts and reads track 35 sectors 0-11 (the ; solo trainer), then points the NMI vector at the bare RTI because the trainer never uses NMIs; ; otherwise it falls into loadCommBuild2 for the real modem driver. ; In: isSoloTrainer ($0BA5) bit 7 ; Out: $E000-$EBFF filled, comm module initialised, (build 1) NMI vector = irqRti ; Called from: $0AF4 in startGameFromSetup, and by fall-through from loadStartupOverlays ; ---------------------------------------------------------------------- loadCommModule: ldx #$00 ; 0F16 destination low byte $00 ... ldy #$E0 ; 0F18 ... high byte $E0: the module always loads at $E000 jsr setLoadDest ; 0F1A arm the loader's destination lda isSoloTrainer ; 0F1D which opponent did the player pick? bpl loadCommBuild2 ; 0F20 bit 7 clear = modem game -> load build 2 (the software modem) jsr disableCiaInterrupts ; 0F22 solo trainer: no CIA interrupts at all while the fast loader runs ldy #$23 ; 0F25 track $23 = 35, the solo trainer lda #$0C ; 0F27 $0C = 12 sectors = 3 KB -> $E000-$EBFF ldx #$00 ; 0F29 from sector 0 jsr loadCommFinish ; 0F2B load it and run the module's init entry jmp setNmiVectorRti ; 0F2E the trainer has no UART, so send NMIs straight to an RTI and return ; ---------------------------------------------------------------------- ; loadCommBuild2 - Loads the modem-driver build of the comm module with interrupts off. Its first ; page comes from track 18 sector 7 (on the directory track, so it is not encrypted and holds the ; module's jump table and variable block); track 34 sectors 1-11 supply the rest. Falls into ; loadCommFinish. ; In: load destination already set to $E000 ; Out: $E000-$EBFF = the modem driver ; Called from: loadCommModule ($0F20) when isSoloTrainer bit 7 is clear ; ---------------------------------------------------------------------- loadCommBuild2: sei ; 0F31 no interrupts during the fast-loader handshake ldy #$12 ; 0F32 track $12 = 18, the directory track ldx #$07 ; 0F34 sector 7 holds the driver's first page lda #$01 ; 0F36 just that one sector -> $E000-$E0FF jsr loadSectors ; 0F38 read it (loadSectors leaves the destination advanced to $E100) ldy #$22 ; 0F3B track $22 = 34, the rest of the modem driver ldx #$01 ; 0F3D sectors 1..11 (sector 0 is filler) lda #$0B ; 0F3F $0B = 11 sectors -> $E100-$EBFF ; ---------------------------------------------------------------------- ; loadCommFinish - Shared tail of the two comm-module load paths: does the final loadSectors with the ; registers the caller set up and falls into initCommModule. ; In: A = sector count, Y = track, X = first sector, destination from setLoadDest ; Out: comm module loaded and initialised ; Called from: loadCommModule ($0F2B); reached by fall-through from loadCommBuild2 ; ---------------------------------------------------------------------- loadCommFinish: jsr loadSectors ; 0F41 final transfer of the module ; ---------------------------------------------------------------------- ; initCommModule - Cold-initialises the freshly loaded comm module and grafts the game's own keyboard ; scanner into it: the module's keyboard vector at $E013/$E014 is patched to scanKeyboard ($0DB7), so ; the module's $E012/$E015 entries scan the matrix with the game's routine (the KERNAL is banked out). ; In: comm module resident at $E000 ; Out: $E013/$E014 = $0DB7, interrupts enabled ; Called from: loadCommFinish (fall-through) and directly from $0AFC ; ---------------------------------------------------------------------- initCommModule: ldx #$01 ; 0F44 function code 1 = cold initialise the module jsr commLinkControl ; 0F46 call the module's life-cycle entry $E003 (also trainerAiE000:commSetLinkStateEntry) lda #$B7 ; 0F49 low byte of scanKeyboard ($0DB7) ... sta D_E013 ; 0F4B ... into the operand of the module's keyboard JMP lda #$0D ; 0F4E high byte $0D ... sta D_E014 ; 0F50 ... completing the vector: $E012/$E015 now scan the game's own key matrix cli ; 0F53 the module is live: let the raster IRQ run again rts ; 0F54 done ; ---------------------------------------------------------------------- ; loadSectorsDelayed - Wrapper around loadSectors that first burns 8 raster frames through the $C000 ; frame delay, giving the drive time to settle (and the previous fast-load handshake time to finish) ; before the next request. Every overlay, map and setup load in this file goes through it. ; In: A = sector count, Y = track, X = first sector, destination from setLoadDest ; Out: as loadSectors (memory filled, C=1 on error) ; Called from: $0FD1, $1054, $105D, $106D, $1096, $109F, $10B8, $107D, $10C8 and overlay A ($7E97, ; $7F84) ; ---------------------------------------------------------------------- loadSectorsDelayed: pha ; 0F55 the sector count must survive the delay lda #$08 ; 0F56 8 frames ... jsr waitFrames ; 0F58 ... of raster-timed idling (Y and X are preserved by waitFrames) pla ; 0F5B get the sector count back jmp loadSectors ; 0F5C now do the transfer and return to our caller ; ---------------------------------------------------------------------- ; suspendCommModule - Asks the comm module to pause before the game touches the disk: the fast loader ; needs interrupts off and exact cycle timing, which a UART NMI would ruin. When the link is up it ; writes request code $C0 into the module's suspend-request byte $E039 and spins until the module ; acknowledges by setting bit 7 of $E03A. A is preserved across the call. ; In: $E03B (link active flag) ; Out: $E039 = $C0; blocks until $E03A bit 7 is set ; Called from: suspendCommForDiskAccess ($10CB) ; ---------------------------------------------------------------------- suspendCommModule: pha ; 0F5F the caller's A must survive lda isLinkActive ; 0F60 is the link actually up? (also trainerAiE000:isLinkActive) beq L_0F6F ; 0F63 no link (or the solo trainer) - nothing to suspend lda #$C0 ; 0F65 $C0 = 'disable every CIA2 NMI source' request code sta nmiSuspendRequest ; 0F67 hand it to the module's request byte $E039 (also trainerAiE000:nmiSuspendRequest) L_0F6A: lda nmiSuspendAck ; 0F6A poll the acknowledge byte $E03A ... (also trainerAiE000:nmiSuspendAck) bpl L_0F6A ; 0F6D ... until bit 7 says the module has parked itself L_0F6F: pla ; 0F6F restore the caller's A rts ; 0F70 the disk may now be touched ; ---------------------------------------------------------------------- ; resumeCommModule - Counterpart of suspendCommModule: when the link is up it clears the request byte ; $E039 and spins until the module clears bit 7 of the acknowledge byte $E03A, i.e. until the UART ; NMIs are running again. ; In: $E03B (link active flag) ; Out: $E039 = 0; blocks until $E03A bit 7 is clear ; Called from: resumeCommAfterDiskAccess ($10D1) ; ---------------------------------------------------------------------- resumeCommModule: lda isLinkActive ; 0F71 is the link up? (also trainerAiE000:isLinkActive) beq L_0F80 ; 0F74 no link - nothing to resume lda #$00 ; 0F76 0 = 'no suspend request' sta nmiSuspendRequest ; 0F78 release the module (also trainerAiE000:nmiSuspendRequest) L_0F7B: lda nmiSuspendAck ; 0F7B poll the acknowledge byte ... (also trainerAiE000:nmiSuspendAck) bmi L_0F7B ; 0F7E ... until the module reports it is running again L_0F80: rts ; 0F80 back to normal service ; Three status-line strings used by the disk routines below, each terminated by a character with bit 7 ; set. showDiskMessageAndFail is called with A = low byte, Y = high byte ($0F). msgNotADataDisk: .byte "NOT A DATA DISK",'!'|$80; 0F81 shown when the disk in the drive is not the film/data disk ('EA') ; msgInsertGameDisk: text, 17 bytes. 'INSERT GAME DISK.' - shown when the 'OZ' game disk is missing. msgInsertGameDisk: .byte "INSERT GAME DISK",'.'|$80; 0F91 shown when the disk in the drive is not the game disk ('OZ') ; msgDiskError: text, 11 bytes. 'DISK ERROR.' - shown by diskErrorExit when the BAM read fails. msgDiskError: .byte "DISK ERROR",'.'|$80 ; 0FA2 shown when the BAM read itself failed (drive error or no disk) ; diskIdTable: two 2-character disk ids indexed by X (X=0 -> 'OZ' = the game disk, X=2 -> 'EA' = the ; film/data disk). checkDiskId reads the first character with diskIdTable,X and the second with ; $0FAE,X and compares them against the id field of the BAM at $02A2/$02A3. diskIdTable: .byte "O" ; 0FAD first character of the game-disk id 'OZ' = Ozark Softscape D_0FAE: .byte $5A ; 0FAE 'Z' completes 'OZ'; 'E','A' is the data-disk id used for game films .byte $45,$41 ; 0FAF EA ; ---------------------------------------------------------------------- ; checkDiskId - Verifies which disk is in the drive. Reads the BAM (track 18 sector 0, which is not ; encrypted) to $0200 and compares the disk id at $02A2/$02A3 with the two characters of ; diskIdTable[X]; the two expected characters and the 'silent' flag are stuffed into instruction ; operands rather than variables. For the game disk (X=0), outside film playback, it also copies the ; ten bytes at BAM offset $AB - the owner name the game writes onto each player's disk - into ; ownPlayerName ($FFDE), which buildOwnerNameString turns into the printed owner line and which is ; exchanged with the opponent. NOTE: the read clobbers $0200-$02FF, so callers reload the setup ; block. ; In: A = silent flag (bit 7 set = fail without a message), X = 0 ('OZ') or 2 ('EA') ; Out: C=0 ok / C=1 wrong disk or drive error, diskIdIndex ($92F0), $FFDE-$FFE7, $0200-$02FF clobbered ; Called from: waitForGameDisk ($10E3) and the film save/load code in overlay A ($7E53, $7E5C) ; ---------------------------------------------------------------------- checkDiskId: sta L_0FFE+1 ; 0FB1 self-modifying: the caller's silent flag becomes the operand of the LDA #$FF at $0FFE stx diskIdIndex ; 0FB4 remember which id we are looking for (X is about to be destroyed) ldy diskIdTable,x ; 0FB7 Y = expected first id character ('O' or 'E') lda D_0FAE,x ; 0FBA A = expected second id character ('Z' or 'A'), from diskIdTable+1 tax ; 0FBD park it in X so both can be written into the compare operands sty L_0FDC+1 ; 0FBE self-modifying: patch the CMP #$FF at $0FDC with the expected first character stx L_0FE3+1 ; 0FC1 self-modifying: patch the CMP #$FF at $0FE3 with the expected second character ldx #$00 ; 0FC4 BAM destination low byte $00 ... ldy #$02 ; 0FC6 ... high byte $02: the sector lands at $0200 (over the terrain rule table) jsr setLoadDest ; 0FC8 arm the loader lda #$01 ; 0FCB one sector ldy #$12 ; 0FCD track $12 = 18, the directory track ldx #$00 ; 0FCF sector 0 = the BAM jsr loadSectorsDelayed ; 0FD1 read it with the usual 8-frame settling delay bcc L_0FD9 ; 0FD4 C=0: the BAM is in memory jmp diskErrorExit ; 0FD6 drive error / no disk -> 'DISK ERROR.' and C=1 L_0FD9: lda bamDiskIdChar0 ; 0FD9 first character of the disk id in the BAM (also trainerPlaybook0200:play1Grp12) L_0FDC: cmp #$FF ; 0FDC compare against the expected first character (operand patched at $0FBE) bne L_0FFE ; 0FDE wrong disk lda bamDiskIdChar1 ; 0FE0 second character of the disk id (also trainerPlaybook0200:play1Grp13) L_0FE3: cmp #$FF ; 0FE3 compare against the expected second character (operand patched at $0FC1) bne L_0FFE ; 0FE5 wrong disk lda diskIdIndex ; 0FE7 which id were we checking? bne L_1014 ; 0FEA non-zero = the 'EA' data disk: nothing else to do, report success lda filmPlaybackMode ; 0FEC game disk: is a film being replayed? bmi L_1014 ; 0FEF during playback the film supplies the names, so leave $FFDE alone ldx #$09 ; 0FF1 copy 10 bytes, index 9 down to 0 L_0FF3: lda bamDiskSerial,x ; 0FF3 BAM offset $AB = the owner name EA writes into the unused tail of the BAM (also trainerPlaybook0200:play1Grp21) sta ownPlayerName,x ; 0FF6 -> ownPlayerName ($FFDE-$FFE7), used by buildOwnerNameString and the link handshake dex ; 0FF9 next byte bpl L_0FF3 ; 0FFA loop until index 0 has been copied bmi L_1014 ; 0FFC always taken (X went to $FF): report success L_0FFE: lda #$FF ; 0FFE operand patched at $0FB1 with the caller's silent flag bmi L_1016 ; 1000 bit 7 set = caller wants a silent failure -> just return C=1 lda #$91 ; 1002 low byte of msgInsertGameDisk ($0F91) ... ldy #$0F ; 1004 ... and its high byte ldx L_0FDC+1 ; 1006 which id did we expect (the patched compare operand)? cpx #$45 ; 1009 'E' means we wanted the data disk 'EA' bne L_1011 ; 100B wanted the game disk -> keep 'INSERT GAME DISK.' L_100D: lda #$81 ; 100D low byte of msgNotADataDisk ($0F81) - overlay A jumps straight in here at $7E71 ldy #$0F ; 100F ... and its high byte L_1011: jmp showDiskMessageAndFail ; 1011 show it, beep, pause and return C=1 L_1014: clc ; 1014 C=0 = the expected disk is in the drive rts ; 1015 success return L_1016: sec ; 1016 C=1 = wrong disk rts ; 1017 silent failure return ; ---------------------------------------------------------------------- ; diskErrorExit - Loads the address of msgDiskError ('DISK ERROR.') and falls into ; showDiskMessageAndFail. ; In: none ; Out: message shown, C=1 ; Called from: checkDiskId ($0FD6) and overlay A ($7E9D) ; ---------------------------------------------------------------------- diskErrorExit: lda #$A2 ; 1018 low byte of msgDiskError ($0FA2) ... ldy #$0F ; 101A ... and its high byte ; ---------------------------------------------------------------------- ; showDiskMessageAndFail - Common failure exit of the disk routines: error beep, put the message on ; the centred status line for about two seconds, blank the line again and return C=1. ; In: A = message address low byte, Y = high byte (bit-7 terminated string) ; Out: C=1, status line restored ; Called from: checkDiskId ($1011) and diskErrorExit by fall-through ; ---------------------------------------------------------------------- showDiskMessageAndFail: pha ; 101C the message pointer's low byte has to survive the beep lda #$03 ; 101D sound 3 = the error buzz jsr playSound ; 101F queue it (playSound preserves Y, which still holds the message page) pla ; 1022 get the low byte back jsr showMessageString ; 1023 print the string centred on the 36-column status line lda #$64 ; 1026 $64 = 100 frames, about 2 seconds ... jsr waitFrames ; 1028 ... so the player can read it jsr clearMessageLine ; 102B blank the status line again sec ; 102E C=1 = the operation failed rts ; 102F back to the caller ; ---------------------------------------------------------------------- ; ensureOverlayAResident - Makes sure the $6F00-$87FF overlay variant A (map generator, setup and menu ; code) is in memory before control leaves for handleMainMenuChoice ($7DE3). overlayVariantFlag ; ($87FF) is the identity byte carried by whichever overlay is resident: $FF = variant B, so variant A ; must be read in full; $00 = variant A is already there and only the $8800 message strings and the ; $EC00 comm tail (which sub-overlays borrow) need refreshing. ; In: overlayVariantFlag ($87FF) ; Out: variant A resident plus fresh strings/comm tail; comm module suspended and resumed around the ; disk access ; Called from: $1C72, with diskLoadDepth bumped around the call ; ---------------------------------------------------------------------- ensureOverlayAResident: jsr suspendCommForDiskAccess; 1030 park the comm module and put 'WORKING...' on the status line jsr waitForGameDiskInserted ; 1033 make sure the OZ disk is really in the drive first lda overlayVariantFlag ; 1036 identity byte of the resident $6F00 overlay (also mapGenerator6F00:overlayVariantFlag) bmi L_1041 ; 1039 $FF = variant B is resident -> the whole of variant A has to be read jsr loadStringsAndCommTail ; 103B variant A is already there: just refresh $8800-$89FF and $EC00-$EFFF jmp resumeCommAfterDiskAccess; 103E restart the comm module, clear the message and return L_1041: jsr loadOverlayA ; 1041 read variant A (which chains the strings and the comm tail) jmp resumeCommAfterDiskAccess; 1044 restart the comm module, clear the message and return ; ---------------------------------------------------------------------- ; loadOverlayA - Reads the $6F00-$87FF overlay variant A: track 31 sectors 0-16 to $6F00-$7FFF and ; track 32 sectors 0-7 to $8000-$87FF. Variant A is the random map generator, the setup phase and the ; film menus. Falls into loadStringsAndCommTail. ; In: none ; Out: $6F00-$87FF = variant A (its last byte $87FF is the identity byte $00), plus $8800-$89FF and ; $EC00-$EFFF ; Called from: loadStartupOverlays ($0F13) and ensureOverlayAResident ($1041) ; ---------------------------------------------------------------------- loadOverlayA: ldx #$00 ; 1047 destination low byte $00 ... ldy #$6F ; 1049 ... high byte $6F: the overlay starts at $6F00 jsr setLoadDest ; 104B arm the loader ldy #$1F ; 104E track $1F = 31 ldx #$00 ; 1050 from sector 0 lda #$11 ; 1052 $11 = 17 sectors -> $6F00-$7FFF jsr loadSectorsDelayed ; 1054 read them ldy #$20 ; 1057 track $20 = 32 ldx #$00 ; 1059 from sector 0 lda #$08 ; 105B 8 sectors -> $8000-$87FF, ending with the identity byte at $87FF jsr loadSectorsDelayed ; 105D read them ; ---------------------------------------------------------------------- ; loadStringsAndCommTail - Reads the modem status-message string block (track 29 sectors 9-10) into ; $8800-$89FF, then falls into loadCommTailBuild2. In a solo game the trainer overwrites this block ; with its own per-unit tables, so it has to be reloaded whenever the strings are wanted again. ; In: none ; Out: $8800-$89FF strings, $EC00-$EFFF comm tail ; Called from: ensureOverlayAResident ($103B); reached by fall-through from loadOverlayA ; ---------------------------------------------------------------------- loadStringsAndCommTail: ldx #$00 ; 1060 destination low byte $00 ... ldy #$88 ; 1062 ... high byte $88 jsr setLoadDest ; 1064 arm the loader ldy #$1D ; 1067 track $1D = 29 ldx #$09 ; 1069 from sector 9 lda #$02 ; 106B 2 sectors -> $8800-$89FF jsr loadSectorsDelayed ; 106D read them ; ---------------------------------------------------------------------- ; loadCommTailBuild2 - Reads the last page group of the modem driver (track 34 sectors 12-15) into ; $EC00-$EFFF. Overlay A also calls it to put the comm tail back after the $EC00 sub-overlay from ; track 29 has temporarily occupied that space. ; In: none ; Out: $EC00-$EFFF = the modem driver tail ; Called from: overlay A ($7B33); reached by fall-through from loadStringsAndCommTail ; ---------------------------------------------------------------------- loadCommTailBuild2: ldx #$00 ; 1070 destination low byte $00 ... ldy #$EC ; 1072 ... high byte $EC jsr setLoadDest ; 1074 arm the loader ldy #$22 ; 1077 track $22 = 34 ldx #$0C ; 1079 from sector $0C = 12 lda #$04 ; 107B 4 sectors -> $EC00-$EFFF jmp loadSectorsDelayed ; 107D read them and return straight to our caller ; ---------------------------------------------------------------------- ; loadOverlayBIfNotResident - Loads the $6F00 overlay variant B only when it is not already in memory. ; overlayVariantFlag ($87FF) is $00 while variant A is resident and $FF while B is, so a positive ; value means the swap has to happen. ; In: overlayVariantFlag ($87FF) ; Out: see loadOverlayB, or nothing at all ; Called from: startGameFromSetup ($0B5F) ; ---------------------------------------------------------------------- loadOverlayBIfNotResident: lda overlayVariantFlag ; 1080 identity byte of the resident $6F00 overlay (also mapGenerator6F00:overlayVariantFlag) bpl loadOverlayB ; 1083 $00 = variant A is resident -> swap variant B in rts ; 1085 variant B is already there, nothing to do ; ---------------------------------------------------------------------- ; loadOverlayB - Reads the $6F00-$87FF overlay variant B (the comcen missile and drone screens): track ; 33 sectors 0-16 to $6F00-$7FFF and track 32 sectors 8-15 to $8000-$87FF. Afterwards it copies the ; byte at $206B into $750A: both are the opcode byte of a BCC/BCS that follows a 'is this unit index ; below 50' test, i.e. the side-dependent branch the main program has already patched for this ; player's side, propagated into the freshly loaded overlay. ; In: $206B (the patched branch opcode), overlayVariantFlag ; Out: $6F00-$87FF = variant B, $750A patched, comm module suspended and resumed ; Called from: loadOverlayBIfNotResident ($1083) ; ---------------------------------------------------------------------- loadOverlayB: jsr suspendCommForDiskAccess; 1086 park the comm module and show 'WORKING...' - the fast loader needs the interrupts ldx #$00 ; 1089 destination low byte $00 ... ldy #$6F ; 108B ... high byte $6F jsr setLoadDest ; 108D arm the loader ldy #$21 ; 1090 track $21 = 33 lda #$11 ; 1092 $11 = 17 sectors -> $6F00-$7FFF ldx #$00 ; 1094 from sector 0 jsr loadSectorsDelayed ; 1096 read them ldy #$20 ; 1099 track $20 = 32 ldx #$08 ; 109B from sector 8 (sectors 0-7 of that track belong to variant A) lda #$08 ; 109D 8 sectors -> $8000-$87FF jsr loadSectorsDelayed ; 109F read them lda D_206B ; 10A2 the branch opcode ($90 BCC / $B0 BCS) that patchOwnSideBranches wrote at $206B for this player's side sta radarSideBranchOpcode ; 10A5 patch the matching 'cpy #$32 / branch' in the overlay at $7508-$750A jmp resumeCommAfterDiskAccess; 10A8 restart the comm module, clear the message and return ; ---------------------------------------------------------------------- ; loadUnitTemplatesAndSetup - Reads track 18 sectors 8-14 into $F000-$F6FF, then falls into ; loadSetupBlock. What is on the disk there is not a battlefield: it is four 400-byte unit ; start-position templates (columns, rows, flags and types for 100 units each) which the setup code ; picks from and copies into the live unit arrays; the 40x40 map is generated, not loaded. ; In: none ; Out: $F000-$F6FF = the start-position templates, $0200-$03FF = the setup block ; Called from: startGameFromSetup ($0B35), with diskLoadDepth bumped so the IRQ drops keypresses ; ---------------------------------------------------------------------- loadUnitTemplatesAndSetup: ldx #$00 ; 10AB destination low byte $00 ... ldy #$F0 ; 10AD ... high byte $F0 jsr setLoadDest ; 10AF arm the loader ldy #$12 ; 10B2 track $12 = 18, the directory track ldx #$08 ; 10B4 from sector 8 lda #$07 ; 10B6 7 sectors -> $F000-$F6FF (four 400-byte templates plus 192 unused bytes) jsr loadSectorsDelayed ; 10B8 read them ; ---------------------------------------------------------------------- ; loadSetupBlock - Reads the saved setup block (track 18 sectors 15-16) into $0200-$03FF: the terrain ; tile rule table at $0200 and the persisted game settings plus the little start-up routine at $0300. ; In: none ; Out: $0200-$03FF ; Called from: mainStart ($0A84); reached by fall-through from loadUnitTemplatesAndSetup ; ---------------------------------------------------------------------- loadSetupBlock: ldx #$00 ; 10BB destination low byte $00 ... ldy #$02 ; 10BD ... high byte $02 jsr setLoadDest ; 10BF arm the loader ldy #$12 ; 10C2 track $12 = 18 ldx #$0F ; 10C4 from sector $0F = 15 lda #$02 ; 10C6 2 sectors -> $0200-$03FF jmp loadSectorsDelayed ; 10C8 read them and return straight to our caller ; ---------------------------------------------------------------------- ; suspendCommForDiskAccess - Standard prologue for disk access: pause the comm module, then set ; persistentMessageId to 1 and paint message 1 ('WORKING...') on the status line so the player knows ; the drive is busy. ; In: none ; Out: comm module paused, persistentMessageId ($92A1) = 1 ; Called from: ensureOverlayAResident ($1030), loadOverlayB ($1086), waitForGameDiskInserted ($10DC) ; and overlay A ($7B7D, $7DF7, $7F57, $815E) ; ---------------------------------------------------------------------- suspendCommForDiskAccess: jsr suspendCommModule ; 10CB stop the UART NMIs so the fast loader can keep its timing jmp showWorkingMessage ; 10CE show the 'WORKING...' notice and return to our caller ; ---------------------------------------------------------------------- ; resumeCommAfterDiskAccess - Epilogue for disk access: restart the comm module, forget the persistent ; 'WORKING...' message and blank the status line. ; In: none ; Out: comm module running, persistentMessageId ($92A1) = 0, status line cleared ; Called from: $0B3D, $103E, $1044, $10A8 and overlay A ($7DFE, $7F95, $81A2, $81AF) ; ---------------------------------------------------------------------- resumeCommAfterDiskAccess: jsr resumeCommModule ; 10D1 let the module's NMIs run again lda #$00 ; 10D4 no message is being held on the line any more sta persistentMessageId ; 10D6 clear the persistent message id jmp clearMessageLine ; 10D9 blank the status line and return to our caller ; ---------------------------------------------------------------------- ; waitForGameDiskInserted - Blocks until the OZ game disk is in the drive. Suspends the comm module ; (and leaves it suspended for the caller), then loops: check the disk id verbosely, and on failure ; print 'PRESS BUTTON WHEN READY.' and wait for a fire click before trying again. Because that prompt ; stirs the RNG while it waits, the delay also randomises the map seed. ; In: none ; Out: returns only with C=0 from checkDiskId; comm module left suspended ; Called from: mainStart ($0ADC), ensureOverlayAResident ($1033) and overlay A ($81AC) ; ---------------------------------------------------------------------- waitForGameDiskInserted: jsr suspendCommForDiskAccess; 10DC pause the comm module and show 'WORKING...' ; ---------------------------------------------------------------------- ; Retry loop: ask for the game disk until it is there. ; ---------------------------------------------------------------------- waitForGameDisk: ldx #$00 ; 10DF id index 0 = the game disk 'OZ' lda #$00 ; 10E1 A bit 7 clear = report a mismatch on the status line jsr checkDiskId ; 10E3 read the BAM and compare the id bcc L_10EE ; 10E6 C=0: the right disk is in the drive jsr promptPressButtonWhenReady; 10E8 wrong disk: prompt and wait for the fire button jmp waitForGameDisk ; 10EB then try again L_10EE: rts ; 10EE the game disk is present ; spriteEnableScratch: written by the raster IRQ at $11CB with the sprite enable mask it is about to ; put into VIC_SPR_ENA. Nothing ever reads it back (a dead store); $10F0 is unused padding before the ; interrupt handler. spriteEnableScratch: .byte $00,$00 ; 10EF .. copy of the last sprite enable mask written by the IRQ; never read ; ---------------------------------------------------------------------- ; rasterIrqHandler - The game's only interrupt handler, entered through the RAM vector $FFFE at raster ; line $FA (250), installed by installRasterIrq ($0C66). A BRK is recognised from the B flag in the ; pushed status byte and thrown away through the patchable JMP operand at $10FF/$1100 (a debug hook ; that initInterruptVectors points at irqExit). Phase 1 acknowledges the VIC interrupt, re-arms the ; raster line and the $FFFE/$FFFF vector from their zero-page shadows, guards against re-entry with ; irqInProgress, copies the two VIC control registers from their shadows and services the comm module ; ($E000 with X=0 and $E00C); this half also runs while a disk load is in progress. Phase 2 copies ; the remaining VIC shadows and the eight sprite colour/X/Y shadows (colours >= $10 hold two nibbles ; that alternate every other frame to make a sprite flash; positions can be biased by the fine-scroll ; offsets), bumps the 24-bit frame counter, re-enables interrupts, polls the keyboard and joystick 2, ; maintains the idle counter, the RNG and the six frame countdowns, watches for the SHIFT+C=+F7 abort, ; blinks the map cursor, ticks the frame timers, runs both halves of the chat and drives the sound. ; In: VIC and CIA1 registers, the shadow block $9000-$9024, inputLockoutTimer ($0B7D), diskLoadDepth ; ($0BA7), screenShakeTimer ($92C8), spriteScrollFineX/Y ($92F4/$92F5), comm entries $E000/$E00C ; Out: every VIC register, frameCounter (zp_45-zp_47), joyDirection/joyFireUp and their copies, ; requestedScreenKey, irqInProgress, the timers zp_6C-zp_71, the chat buffers and the sound state ; Called from: the hardware IRQ vector $FFFE (and BRK) ; ---------------------------------------------------------------------- rasterIrqHandler: pha ; 10F1 save the registers the handler is about to use txa ; 10F2 X ... pha ; 10F3 ... onto the stack tya ; 10F4 Y ... pha ; 10F5 ... onto the stack tsx ; 10F6 S now points 6 bytes below the pushed processor status lda D_0104,x ; 10F7 $0104+S = the status byte the CPU pushed when it took the interrupt and #$10 ; 10FA bit 4 = the B flag: set means this was a BRK, not a raster IRQ beq irqNotBrk ; 10FC a real interrupt - carry on L_10FE: jmp irqExit ; 10FE BRK debug hook: the operand at $10FF/$1100 is patched by initInterruptVectors to irqExit ; ---------------------------------------------------------------------- ; Acknowledge the VIC interrupt and re-arm both the raster line and the RAM interrupt vector; the ; vector is rewritten every frame because the film code temporarily flips $01 to see the RAM under the ; I/O area. ; ---------------------------------------------------------------------- irqNotBrk: lda #$01 ; 1101 bit 0 = the raster interrupt latch ... sta VIC_IRQ_FLAG ; 1103 ... written back to clear it lda rasterIrqLine ; 1106 the raster line the game wants ($FA = 250, below the visible playfield) sta VIC_RASTER ; 1108 re-arm VIC_RASTER for the next frame lda irqVectorShadowLo ; 110B shadow of the IRQ vector low byte ... sta irqVector ; 110D ... written back into $FFFE lda irqVectorShadowHi ; 1110 shadow of the high byte ... sta irqVectorHi ; 1112 ... written back into $FFFF lda irqInProgress ; 1115 is a previous invocation still running? (the handler re-enables interrupts halfway through) bne irqBailOut ; 1117 yes - do nothing this frame inc irqInProgress ; 1119 mark the handler as busy lda vicCtrl1Shadow ; 111B shadow of VIC_CTRL1 (screen mode, raster bit 8, Y scroll) sta VIC_CTRL1 ; 111E -> $D011 lda vicCtrl2Shadow ; 1121 shadow of VIC_CTRL2 (multicolour, X scroll) sta VIC_CTRL2 ; 1124 -> $D016 ldx #$00 ; 1127 function 0 = the comm module's per-frame service tick jsr commJumpTable ; 1129 run the UART / trainer bit-and-byte timing (also trainerAiE000:commRequestEntry) jsr pollLinkStatus ; 112C and the suspend handshake plus the carrier-detect sampling (also trainerAiE000:commFrameTickEntry) lda diskLoadDepth ; 112F how deeply nested is the current disk load? bpl irqPhase2 ; 1132 always taken in practice - nothing ever makes diskLoadDepth negative, so the bail-out below is effectively dead irqBailOut: jmp irqExit ; 1134 bail out with only the comm module serviced ; ---------------------------------------------------------------------- ; Phase 2: push the rest of the VIC shadow block ($9002-$9006, $9020, $9023, $9024) into the chip. ; Keeping shadows and copying them once per frame means the game never has to worry about a partly ; updated register being displayed. ; ---------------------------------------------------------------------- irqPhase2: lda vicMemPtrShadow ; 1137 shadow of VIC_MEM_PTR (bitmap at $A000, video matrix at $8C00 within VIC bank 2) sta VIC_MEM_PTR ; 113A -> $D018 lda vicBorderShadow ; 113D border colour shadow sta VIC_BORDER ; 1140 -> $D020 lda vicBg0Shadow ; 1143 background colour 0 shadow sta VIC_BG0 ; 1146 -> $D021 lda vicBg1Shadow ; 1149 multicolour background 1 shadow sta VIC_BG1 ; 114C -> $D022 lda vicBg2Shadow ; 114F multicolour background 2 shadow sta VIC_BG2 ; 1152 -> $D023 lda spriteXMsbShadow ; 1155 sprite X bit-8 mask shadow sta VIC_SPR_HI_X ; 1158 -> $D010 lda spriteXExpandShadow ; 115B sprite X expand mask shadow sta VIC_SPR_EXP_X ; 115E -> $D01D lda spriteYExpandShadow ; 1161 sprite Y expand mask shadow sta VIC_SPR_EXP_Y ; 1164 -> $D017 ; ---------------------------------------------------------------------- ; Sprite position bias: while screenShakeTimer is running the sprites are shifted by the current fine ; scroll offsets so they stay glued to the smooth-scrolled bitmap; otherwise both biases are zero. The ; two offsets are written into the ADC operands at $11B6 and $11BF. ; ---------------------------------------------------------------------- lda screenShakeTimer ; 1167 is the screen being shaken / fine-scrolled? bne irqUseScrollOffsets ; 116A yes - derive the offsets from the scroll position sta L_11B5+1 ; 116C no bias: patch the X adder at $11B5 with 0 sta L_11BE+1 ; 116F and the Y adder at $11BE with 0 beq irqSpriteLoopSetup ; 1172 always taken (A is still 0) irqUseScrollOffsets: lda spriteScrollFineY ; 1174 vertical fine-scroll position and #$07 ; 1177 only the 0-7 pixel offset sec ; 1179 prepare a borrow-free subtraction sbc #$03 ; 117A centre it: -3..+4 pixels sta L_11BE+1 ; 117C patch the Y adder operand at $11BE lda spriteScrollFineX ; 117F horizontal fine-scroll position and #$07 ; 1182 only the 0-7 pixel offset sta L_11B5+1 ; 1184 patch the X adder operand at $11B5 ; ---------------------------------------------------------------------- ; Copy the eight sprite shadows into the VIC. Y counts the sprite index 7..0 and X the register ; offset 2*index, so $D000+X / $D001+X are that sprite's X and Y registers. A colour shadow below ; $10 is a plain colour; $10 and above packs two nibbles which are swapped every other frame to make ; the sprite flash between two colours. ; ---------------------------------------------------------------------- irqSpriteLoopSetup: ldy #$07 ; 1187 start with sprite 7 ldx #$0E ; 1189 $0E = 7*2, the X/Y register offset of sprite 7 lda frameCounter ; 118B free-running frame counter ... ror a ; 118D ... rotated right twice ... ror a ; 118E ... so that bit 7 of the result is bit 0 of the frame counter sta spriteFlashPhase ; 118F the flash phase: negative on every other frame irqSpriteLoop: lda spriteColourShadow,y ; 1191 colour shadow of sprite Y ($9008+Y) cmp #$10 ; 1194 values 0-$0F are ordinary colours bcc irqWriteSpriteColour ; 1196 plain colour - write it straight out bit spriteFlashPhase ; 1198 two packed nibbles: only swap them on the flashing frames bpl irqSpritePosition ; 119A off-phase frame - leave the colour register untouched eor VIC_SPR0_COLOR,y ; 119C compare the shadow with what the register currently holds and #$0F ; 119F look at the low nibble of the difference cmp #$01 ; 11A1 C=1 when the register does not already show the shadow's low nibble lda spriteColourShadow,y ; 11A3 reload the packed shadow byte bcs irqWriteSpriteColour ; 11A6 register differs -> show the low nibble this time lsr a ; 11A8 otherwise shift the high nibble down ... lsr a ; 11A9 ... (4 shifts) ... lsr a ; 11AA ... so the sprite alternates ... lsr a ; 11AB ... between the two packed colours irqWriteSpriteColour: and #$0F ; 11AC only the colour bits sta VIC_SPR0_COLOR,y ; 11AE -> $D027+Y, the sprite's colour register irqSpritePosition: lda spriteXShadow,y ; 11B1 X position shadow of sprite Y ($9010+Y) clc ; 11B4 clear carry for the bias add L_11B5: adc #$FF ; 11B5 add the horizontal bias (operand patched at $116C or $1184) sta VIC_SPR0_X,x ; 11B7 -> $D000+X, the sprite's X register lda spriteYShadow,y ; 11BA Y position shadow of sprite Y ($9018+Y) clc ; 11BD clear carry for the bias add L_11BE: adc #$FF ; 11BE add the vertical bias (operand patched at $116F or $117C) sta VIC_SPR0_Y,x ; 11C0 -> $D001+X, the sprite's Y register dex ; 11C3 step the register offset ... dex ; 11C4 ... down by two dey ; 11C5 next sprite bpl irqSpriteLoop ; 11C6 loop until sprite 0 has been done lda spriteEnableShadow ; 11C8 sprite enable mask shadow sta spriteEnableScratch ; 11CB keep a copy at $10EF (nothing ever reads it back) sta VIC_SPR_ENA ; 11CE -> $D015, so sprites appear and disappear in step with the rest of the frame ; ---------------------------------------------------------------------- ; Bump the free-running 24-bit frame counter zp_45/zp_46/zp_47. ; ---------------------------------------------------------------------- inc frameCounter ; 11D1 low byte of the frame counter bne irqAfterSprites ; 11D3 no carry into the middle byte inc frameCounterMid ; 11D5 middle byte bne irqAfterSprites ; 11D7 no carry into the high byte inc frameCounterHi ; 11D9 high byte: wraps after about 77 hours ; ---------------------------------------------------------------------- ; From here on the handler runs with interrupts enabled - irqInProgress keeps a second raster IRQ from ; re-entering it. inputLockoutTimer counts frames during which the keyboard and joystick are not ; sampled at all, so joyDirection and joyFireUp keep whatever they had. ; ---------------------------------------------------------------------- irqAfterSprites: cli ; 11DB everything time-critical is done; allow interrupts again lda inputLockoutTimer ; 11DC is input still locked out (after a screen change or an abort)? beq irqPollInput ; 11DF no - sample the keyboard and the stick dec inputLockoutTimer ; 11E1 count the lockout down one frame ... jmp irqIdleCounters ; 11E4 ... and skip the whole input poll irqPollInput: jsr pollKeyboardEvent ; 11E7 scan the key matrix through the comm module's keyboard entry lda #$FF ; 11EA default 'joystick idle' pattern in case a key is pending ldy requestedScreenKey ; 11EC did the poll leave a screen-select key pending (0-3 = F1/F3/F5/F7, $FF = none)? bpl irqStoreJoystick ; 11EE yes - do not read the stick this frame ldy newKeyEvent ; 11F0 is a typed key waiting to be consumed? cpy #$FF ; 11F3 $FF = no key bne irqStoreJoystick ; 11F5 a key is pending - do not read the stick this frame lda #$FF ; 11F7 deselect every keyboard row ... sta CIA1_PRA ; 11F9 ... so CIA1 port A shows joystick 2 instead of the matrix L_11FC: lda CIA1_PRA ; 11FC read the port ... nop ; 11FF ... let it settle ... cmp CIA1_PRA ; 1200 ... and read it again bne L_11FC ; 1203 the two reads disagree (contact bounce) - try once more irqStoreJoystick: and #$1F ; 1205 keep bits 0-4: up, down, left, right, fire (all active low) ldy #$01 ; 1207 assume the button is up cmp #$10 ; 1209 bit 4 = the fire button bcs L_120E ; 120B bit set = button not pressed - keep Y=1 dey ; 120D button down -> Y=0 L_120E: sty joyFireUp ; 120E latch the fire state for the whole game sty joyFireUpCopy ; 1210 second copy - nothing ever reads it (dead store) and #$0F ; 1212 keep the four direction bits ($0F = centred) sta joyDirection ; 1214 latch the direction for the whole game sta joyDirectionCopy ; 1216 second copy - nothing ever reads it (dead store) ; ---------------------------------------------------------------------- ; inputIdleFrames counts consecutive frames with the stick centred and the button up, saturating at ; $FF; anything else resets it to 0. The pause/attract logic elsewhere uses it. ; ---------------------------------------------------------------------- irqIdleCounters: lda joyDirection ; 1218 the latched direction nibble cmp #$0F ; 121A $0F = all four switches open = centred bne L_1227 ; 121C the stick is being pushed lda #$00 ; 121E stick centred: forget any held direction ... sta joyRepeatMask ; 1220 ... by clearing the direction repeat mask lda joyFireUp ; 1223 is the fire button also up? bne L_122E ; 1225 yes - nothing at all is being touched L_1227: lda #$00 ; 1227 some input is active ... sta inputIdleFrames ; 1229 ... so restart the idle counter beq L_1238 ; 122C always taken L_122E: lda inputIdleFrames ; 122E frames of no input so far cmp #$FF ; 1231 already saturated? beq L_1238 ; 1233 yes - leave it at $FF inc inputIdleFrames ; 1235 another idle frame L_1238: jsr stepRandomSeed ; 1238 stir the 24-bit game RNG once per frame ldx #$05 ; 123B index 5 down to 0 covers zp_71..zp_6C ; ---------------------------------------------------------------------- ; Decrement the six frame countdowns zp_6C-zp_71 (joystick repeat, fire debounce, menu blink, marker ; blink, key repeat and one spare), each only while it is non-zero. ; ---------------------------------------------------------------------- irqTimerLoop: lda joyRepeatTimer,x ; 123D timer zp_6C+X beq L_1243 ; 123F already expired - leave it alone dec joyRepeatTimer,x ; 1241 one frame closer to expiry L_1243: dex ; 1243 next timer bpl irqTimerLoop ; 1244 loop over all six lda diskLoadDepth ; 1246 is a disk load or overlay swap in progress? beq irqCheckAbortHotkey ; 1249 no - function keys may switch screens lda #$FF ; 124B $FF = no screen requested ... sta requestedScreenKey ; 124D ... so an F-key pressed during a load is thrown away ; ---------------------------------------------------------------------- ; The panic hotkey: SHIFT + C= + F7 abandons whatever is happening and returns to the main menu. ; returnToMainMenu resets the stack pointer, so this JMP never comes back - the interrupt frame is ; simply abandoned (interrupts were already re-enabled at $11DB). ; ---------------------------------------------------------------------- irqCheckAbortHotkey: lda shiftHeldFlag ; 124F was SHIFT held during the key scan? bpl irqPerFrameWork ; 1252 no - not the abort combination lda commodoreHeldFlag ; 1254 was the Commodore key held too? bpl irqPerFrameWork ; 1257 no - not the abort combination lda requestedScreenKey ; 1259 which function key is pending? cmp #$03 ; 125B 3 = F7 bne irqPerFrameWork ; 125D not F7 - carry on normally lda #$01 ; 125F lock the input out ... sta diskLoadDepth ; 1261 ... by pretending a disk load is running lda #$FF ; 1264 $FF ... sta inputLockoutTimer ; 1266 ... = the longest possible input lockout sta requestedScreenKey ; 1269 ... and forget the pending F7 jmp returnToMainMenu ; 126B never returns: returnToMainMenu rebuilds the stack from scratch ; ---------------------------------------------------------------------- ; Per-frame housekeeping that is allowed to run with interrupts on. ; ---------------------------------------------------------------------- irqPerFrameWork: lda currentScreen ; 126E which screen is showing? bne L_127A ; 1271 not the battlefield screen - no map cursor to blink lda gamePhase ; 1273 gamePhase: bit 7 marks the phases in which there is no map cursor to blink bmi L_127A ; 1275 gamePhase negative -> skip the blink jsr blinkCursorColour ; 1277 toggle the map cursor sprite colour L_127A: jsr updateFrameTimers ; 127A count the game's own frame timers down jsr handleChatSend ; 127D outgoing chat: pick up typed characters and queue them to the opponent jsr receiveChatCharacter ; 1280 incoming chat: take one received byte and add it to the message line jsr dequeueSoundRequest ; 1283 take the highest-priority queued sound request into X txa ; 1286 X = $FF when nothing is queued bmi L_128C ; 1287 nothing to start jsr startSound ; 1289 start sound A on its voices L_128C: jsr soundPlayerTick ; 128C advance the SID sound scripts by one frame lda #$00 ; 128F the handler is finished ... sta irqInProgress ; 1291 ... so a later raster IRQ may run it again ; ---------------------------------------------------------------------- ; irqExit - Pulls Y, X and A back off the stack and falls into irqRti. It is both the normal end of ; rasterIrqHandler and the target that initInterruptVectors patches into the BRK-exit JMP at ; $10FF/$1100. ; In: the register frame pushed by rasterIrqHandler ; Out: registers restored, RTI ; Called from: $10FE (BRK) and $1134 (early bail-out); reached by fall-through at the end of the ; handler ; ---------------------------------------------------------------------- irqExit: pla ; 1293 Y ... tay ; 1294 ... restored pla ; 1295 X ... tax ; 1296 ... restored pla ; 1297 and A ; ---------------------------------------------------------------------- ; irqRti - A bare RTI. It is the NMI vector target for the whole game: initInterruptVectors and ; setNmiVectorRti put $1298 into $FFFA/$FFFB and into the comm module's own NMI chain vector ; ($E031/$E032), so a stray RS-232 NMI cannot disturb the game when no modem session is running. ; In: none ; Out: none ; Called from: the NMI vector; reached by fall-through from irqExit ; ---------------------------------------------------------------------- irqRti: rti ; 1298 return from interrupt ; Three leftover bytes between irqRti and updateFrameTimers. Nothing references them and they do not ; disassemble into anything sensible - alignment padding or the tail of an edited-out routine. padIrq1299: .byte $00,$09,$13 ; 1299 ... unreferenced padding ; ---------------------------------------------------------------------- ; updateFrameTimers - Per-frame housekeeping called from the raster IRQ. It bumps the two ; free-running animation ticks, then runs three groups of counters: an 8-frame group (the ; packet-exchange watchdog, the end-of-game countdown and linkWaitTimer), a 4-frame group (the ; status-message hold timer and the link poll delay) and a per-frame group (step delay, shot line, ; view redraw and marker sprite). Every countdown is guarded so it stops at 0. ; In: frameCounter (zp_45), gamePhase (zp_B1) ; Out: $92F8, $92FB, $9223, $92C9, zp_7F, zp_72, $9163, $90FC, $91C8, $9234, $923E ; Called from: rasterIrqHandler ($127A) ; ---------------------------------------------------------------------- updateFrameTimers: inc missileFlightTick ; 129C animation tick used by the missile screen inc radarSweepTick ; 129F animation tick used by the radar sweep lda frameCounter ; 12A2 the frame counter drives all the slower groups and #$07 ; 12A4 every 8th frame ... bne L_12D1 ; 12A6 ... otherwise skip straight to the 4-frame group lda frameCounter ; 12A8 which of the two 8-frame slots is this? and #$0F ; 12AA low nibble 0 = one of the two, i.e. every 16 frames beq bumpLinkWatchdog ; 12AC 16-frame slot: always bump the watchdog lda gamePhase ; 12AE the odd 8-frame slot only counts when gamePhase is not $FF ... cmp #$FF ; 12B0 ... so at gamePhase $FF the watchdog runs at half speed (every 16 frames) beq L_12C1 ; 12B2 skip the watchdog this time ; ---------------------------------------------------------------------- ; linkTimeoutCounter is the packet-exchange watchdog: checkLinkTimeout ($5A14) arms it with bit 7 set, ; a completed exchange clears it at $4E69, and here it counts up while negative and sticks at ; $FF, which is how the game notices that the opponent has stopped answering. ; ---------------------------------------------------------------------- bumpLinkWatchdog: lda linkTimeoutCounter ; 12B4 the watchdog value bpl L_12C1 ; 12B7 positive = not armed, nothing to do clc ; 12B9 clear carry for the increment adc #$01 ; 12BA one more tick towards the timeout bcs L_12C1 ; 12BC already at $FF - saturate instead of wrapping to 0 sta linkTimeoutCounter ; 12BE store the advanced watchdog L_12C1: lda gameEndCountdown ; 12C1 gameEndCountdown: the delay from 'game over' to the summary screen beq L_12CB ; 12C4 0 = not running bmi L_12CB ; 12C6 negative = it has already reached its $80 end stop inc gameEndCountdown ; 12C8 count it up towards $80 L_12CB: lda linkWaitTimer ; 12CB linkWaitTimer: the timeout used while waiting for the comm module beq L_12D1 ; 12CD already expired dec linkWaitTimer ; 12CF one 8-frame tick closer to zero L_12D1: lda frameCounter ; 12D1 the frame counter again ... and #$03 ; 12D3 ... for the every-4-frames group bne L_12E5 ; 12D5 not a 4-frame slot lda messageTimer ; 12D7 messageTimer: how long the status-line message stays up beq L_12DD ; 12D9 already expired dec messageTimer ; 12DB count it down L_12DD: lda linkPollDelay ; 12DD linkPollDelay: pacing of the link status polls beq L_12E5 ; 12E0 already expired dec linkPollDelay ; 12E2 count it down ; ---------------------------------------------------------------------- ; The per-frame group: four independent countdowns, each frozen once it reaches 0. ; ---------------------------------------------------------------------- L_12E5: lda stepDelayTimer ; 12E5 stepDelayTimer: frames until the next unit step of the round beq L_12ED ; 12E8 already expired dec stepDelayTimer ; 12EA count it down L_12ED: lda shotLineTimer ; 12ED shotLineTimer: how long a shot line stays drawn in the viewport beq L_12F5 ; 12F0 already expired dec shotLineTimer ; 12F2 count it down L_12F5: lda viewRedrawTimer ; 12F5 viewRedrawTimer: frames until the 7x5 view is redrawn beq L_1301 ; 12F8 already expired cmp #$80 ; 12FA $80 is the 'frozen / redraw pending' sentinel ... beq L_1301 ; 12FC ... which must not be counted down dec viewRedrawTimer ; 12FE count it down L_1301: lda markerSpriteTimer ; 1301 markerSpriteTimer: how long the repair/target marker sprites stay visible beq L_1309 ; 1304 already expired dec markerSpriteTimer ; 1306 count it down L_1309: rts ; 1309 back to the interrupt handler ; ---------------------------------------------------------------------- ; stepRandomSeed - Called from the IRQ every frame. If the 24-bit game RNG state has collapsed to all ; zeroes (which would lock the LFSR up) it re-seeds the low byte from the current joystick direction, ; so the generator both recovers and is fed a little entropy from the player, then steps the LFSR ; once. ; In: gameRngState0-2 (zp_57-zp_59), joyDirection (zp_5D) ; Out: the RNG state advanced, A = the new random byte ; Called from: rasterIrqHandler ($1238) ; ---------------------------------------------------------------------- stepRandomSeed: lda gameRngState0 ; 130A byte 0 of the RNG state ... ora gameRngState1 ; 130C ... ORed with byte 1 ... ora gameRngState2 ; 130E ... and byte 2 bne L_1316 ; 1310 any bit set - the state is fine lda joyDirection ; 1312 all zero: take the joystick nibble as fresh entropy ... sta gameRngState0 ; 1314 ... and drop it into byte 0 so the LFSR can run again L_1316: jsr nextGameRandom ; 1316 advance the 24-bit LFSR in the high-memory block rts ; 1319 A holds the new random byte ; cursorBlinkTimer: the 30-frame countdown of blinkCursorColour, stored in the code stream just in ; front of the routine that owns it. cursorBlinkTimer: .byte $00 ; 131A . counts 30..0 and then wraps; bit 7 signals 'time to toggle' ; ---------------------------------------------------------------------- ; blinkCursorColour - Blinks the map cursor. A 30-frame countdown toggles bit 0 of the sprite 0 ; colour shadow and copies the result to the sprite 2 shadow, so the two cursor sprites alternate ; between two adjacent colours. The IRQ only calls it while currentScreen is 0 (the battlefield ; screen) and gamePhase is not negative. ; In: cursorBlinkTimer ($131A), spriteColourShadow ($9008) ; Out: $131A, $9008, $900A ; Called from: rasterIrqHandler ($1277) ; ---------------------------------------------------------------------- blinkCursorColour: dec cursorBlinkTimer ; 131B one frame off the blink countdown bpl L_1330 ; 131E still positive - nothing to do this frame lda #$1E ; 1320 $1E = 30 frames until the next toggle ... sta cursorBlinkTimer ; 1322 ... reload the countdown lda spriteColourShadow ; 1325 sprite 0 colour shadow eor #$01 ; 1328 flip the lowest colour bit (e.g. between colours 6 and 7) sta spriteColourShadow ; 132A back into the sprite 0 shadow sta sprite2ColourShadow ; 132D and mirror it into the sprite 2 shadow so both halves of the cursor match L_1330: rts ; 1330 the IRQ picks the shadows up on the next frame ; ---------------------------------------------------------------------- ; handleChatSend - The outgoing half of the modem chat ('message to opponent'), run every frame from ; the IRQ. chatState ($929C) is the state byte: 0 = off, $7F = 'open the typing line', and bit 7 set ; = typing, with bits 0-5 holding the cursor position in the outgoing line buffer $9278. Typing is ; entered by RETURN (or by another routine setting $7F) provided the module's transmit ring is empty; ; after that each frame hands one key to the $C000 line editor and queues the accepted character to ; the opponent. If the transmit ring fills up the line is terminated with RETURN instead. ; In: $E03B link flag, $E02E transmit ring count, filmNameLength ($92E6), chatState ($929C), ; newKeyEvent ($90EA) ; Out: chatState, the line buffer $9278.., newKeyEvent/lastTypedKey cleared, one byte queued through ; $E009, sounds ; Called from: rasterIrqHandler ($127D) ; ---------------------------------------------------------------------- handleChatSend: lda isLinkActive ; 1331 is the link up? (also trainerAiE000:isLinkActive) beq L_133B ; 1334 no link - chat is impossible lda filmNameLength ; 1336 is the SAVE GAME FILM name field using the same line buffer? beq L_1341 ; 1339 no - the chat may use it L_133B: lda #$00 ; 133B shut the chat down ... sta chatState ; 133D ... by clearing the state byte rts ; 1340 nothing more to do this frame L_1341: lda chatState ; 1341 the chat state byte bmi L_1373 ; 1344 bit 7 set = already typing -> go and send the next character cmp #$7F ; 1346 $7F = another routine asked for the typing line to be opened beq L_1352 ; 1348 open it lda newKeyEvent ; 134A otherwise look at the last key cmp #$8D ; 134D $8D = RETURN opens the chat line beq L_1352 ; 134F open it L_1351: rts ; 1351 nothing to do this frame ; ---------------------------------------------------------------------- ; Enter typing mode - but only if the module has nothing left to transmit. ; ---------------------------------------------------------------------- L_1352: lda isLinkActive ; 1352 double-check the link is still up (also trainerAiE000:isLinkActive) beq L_1351 ; 1355 it dropped - abandon lda hostOutCount ; 1357 how many bytes are still queued for transmission? (also trainerAiE000:hostOutCount) bne L_1351 ; 135A the ring is not empty - wait until it drains lda #$C0 ; 135C $C0 = bit 7 (typing) + bit 6, cursor position 0 ... sta chatState ; 135E ... becomes the new chat state lda #$9E ; 1361 $9E = the cursor glyph ... sta typedLineBuffer ; 1363 ... put at the head of the outgoing line so the player sees a caret lda #$FF ; 1366 swallow the key ... sta newKeyEvent ; 1368 ... that opened the line ... sta lastTypedKey ; 136B ... and the typed-key copy as well lda #$02 ; 136E sound 2 = the typing click jmp requestSound ; 1370 requestSound is called directly (playSound is not re-entrant and this is inside the IRQ) ; ---------------------------------------------------------------------- ; Typing mode: bits 0-5 of chatState are the cursor position. One key per frame is fed to the shared ; line editor and the accepted character is queued to the opponent, so the remote sees the message ; being typed letter by letter. ; ---------------------------------------------------------------------- L_1373: and #$3F ; 1373 cursor position within the 36-byte line buffer tay ; 1375 the editor wants it in Y lda hostOutCount ; 1376 how full is the transmit ring? (also trainerAiE000:hostOutCount) cmp #$10 ; 1379 $10 = 16 bytes = full bcc L_1385 ; 137B room left - edit one more character tya ; 137D ring full: force the line to end ... ora #$80 ; 137E ... by setting bit 7 of the cursor position (the editor's 'RETURN' marker) ... tay ; 1380 ... back into Y lda #$8D ; 1381 and send RETURN instead of a character bne L_138C ; 1383 always taken L_1385: lda #$A3 ; 1385 $A3 = max 35 characters with bit 7 = 'scroll the line left when it is full' jsr handleTextEntryKey ; 1387 let the $C000 line editor consume newKeyEvent into the buffer beq L_1351 ; 138A nothing was accepted this frame L_138C: pha ; 138C keep the accepted character tya ; 138D the editor's updated cursor position eor #$80 ; 138E toggle bit 7: a normal character leaves it set (still typing), RETURN clears it (line finished) ora #$40 ; 1390 bit 6 is always set so the state byte is never mistaken for 0 sta chatState ; 1392 store the new chat state pla ; 1395 get the character back jmp putLinkByte ; 1396 queue it to the opponent and return to the IRQ (also trainerAiE000:commSendByteEntry) ; ---------------------------------------------------------------------- ; receiveChatCharacter - The incoming half of the chat, called every frame from the IRQ. It pulls one ; byte out of the comm module's receive ring and ignores anything below $80. RETURN ($8D) marks the ; message complete, DEL ($88) backspaces by overwriting the previous character with a space, and ; everything else is appended to the incoming message line. ; In: the comm receive ring through $E006 (C=1 = empty), incomingChatLength ($9277) ; Out: the $9253 buffer and the $0548 status line, $9277, messageTimer cleared, sound 5 ; Called from: rasterIrqHandler ($1280) ; ---------------------------------------------------------------------- receiveChatCharacter: jsr getLinkByte ; 1399 fetch one received byte (C=1 = nothing waiting) (also trainerAiE000:commGetReceivedByteEntry) bcs L_1411 ; 139C nothing received this frame cmp #$80 ; 139E chat characters always have bit 7 set ... bcc L_1411 ; 13A0 ... so anything below $80 is not chat text cmp #$8D ; 13A2 $8D = RETURN, the end of the opponent's message bne L_13AD ; 13A4 not RETURN - try DEL lda #$80 ; 13A6 $80 marks the message complete: bit 7 set, length 0 ... sta incomingChatLength ; 13A8 ... so the next character starts a fresh message bmi L_1408 ; 13AB always taken - go and beep L_13AD: cmp #$88 ; 13AD $88 = DEL bne appendChatCharacter ; 13AF an ordinary character - append it lda incomingChatLength ; 13B1 how long is the message so far? beq L_1400 ; 13B4 empty - nothing to erase bmi L_1400 ; 13B6 the previous message was already complete - nothing to erase dec incomingChatLength ; 13B8 step back over the last character lda #$20 ; 13BB a space ... jsr appendChatCharacter ; 13BD ... written over it (this re-increments the length) dec incomingChatLength ; 13C0 step back again so the next character lands on that space rts ; 13C3 back to the IRQ ; ---------------------------------------------------------------------- ; appendChatCharacter - Appends character A to the 36-byte incoming message buffer at $9253 and ; rewrites the visible status line from it. A negative incomingChatLength means the previous message ; was finished, so the buffer restarts at 0; when the buffer is full at 35 characters it is scrolled ; one byte left. The line is rewritten backwards from the newest character so that only that ; character carries bit 7 - the game's string terminator convention - and the printed line therefore ; always ends exactly where the message does. ; In: A = character, incomingChatLength ($9277), the $9253 buffer ; Out: $9253-$9276, $9277, the status line $0548-$056C, messageTimer = 0, sound 5 ; Called from: receiveChatCharacter ($13AF as a branch, $13BD as a call) ; ---------------------------------------------------------------------- appendChatCharacter: ldy incomingChatLength ; 13C4 current message length = the index of the next free byte bpl L_13CE ; 13C7 a message is in progress ldy #$00 ; 13C9 previous message finished: start a new one ... sty incomingChatLength ; 13CB ... at offset 0 L_13CE: cpy #$23 ; 13CE $23 = 35 characters, the width of the line bcc L_13E5 ; 13D0 there is still room pha ; 13D2 the buffer is full - save the new character ldx #$00 ; 13D3 scroll the whole buffer one byte to the left ... L_13D5: lda incomingChatScrollSrc,x ; 13D5 ... reading from $9254 (buffer+1) ... sta incomingChatBuffer,x ; 13D8 ... and writing to $9253 inx ; 13DB next byte cpx #$24 ; 13DC $24 = 36 bytes bcc L_13D5 ; 13DE keep scrolling dey ; 13E0 the write position moves back one dec incomingChatLength ; 13E1 and so does the recorded length pla ; 13E4 get the new character back L_13E5: and #$7F ; 13E5 store it without the terminator bit sta incomingChatBuffer,y ; 13E7 into the buffer at the write position inc incomingChatLength ; 13EA the message is one character longer ora #$80 ; 13ED the newest character carries the bit-7 terminator ; ---------------------------------------------------------------------- ; Rewrite the whole visible line backwards, newest character first, so that exactly one byte (the last ; one) has bit 7 set and the string printer stops there. ; ---------------------------------------------------------------------- chatLineWriteLoop: sta D_0549,y ; 13EF write into the status line text at $0549+index dey ; 13F2 walk backwards through the message bmi L_13FB ; 13F3 the whole message has been written lda incomingChatBuffer,y ; 13F5 earlier characters are stored plain (bit 7 clear) jmp chatLineWriteLoop ; 13F8 keep going L_13FB: lda #$1E ; 13FB $1E = the leading marker glyph ... sta chatMessageLine ; 13FD ... at $0548, in front of the message text L_1400: ldy messageQueueHead ; 1400 leftover test: the message queue head ... cpy messageQueueTail ; 1403 ... compared with the tail ... bne L_1408 ; 1406 ... but both outcomes continue at $1408, so this compare has no effect (dead code) L_1408: lda #$00 ; 1408 0 ... sta messageTimer ; 140A ... clears the message hold timer so the chat line is shown at once lda #$05 ; 140C sound 5 = the incoming-character click jsr requestSound ; 140E requestSound directly - this runs inside the IRQ, where playSound is not re-entrant L_1411: rts ; 1411 back to the IRQ ; $1412 is a scratch byte the REPAIR tab uses as a 3..0 loop counter while it erases and redraws the ; four comcen system markers ($184B-$187C). $1413 holds the comcenStunStatus value that was current ; when drawRepairTab last ran ($175F), so the console loop can spot a change of stun state. The ; $7F written into $1412 at $141F is never read: the repair code always reloads it first. repairLoopCounter: .byte $00 ; 1412 . REPAIR tab marker loop counter; the $7F stored at $141F is a leftover dead store repairTabStunCopy: .byte $00 ; 1413 copy of comcenStunStatus taken when the REPAIR tab was last drawn ; ---------------------------------------------------------------------- ; resumeMainMenuScreen - Re-entry into the console screen (screen 3, the F7 screen with the STATS / ; REPAIR / MISC tabs) that does not rebuild the frame: it just re-picks the default tab, clears the ; text window and joins the console loop at its 'tab has changed' handler so the panel is redrawn. ; Used when the comcen-stunned blackout ($0709) hands control back to whichever screen was showing. ; In: none ; Out: menuTabIndex ($90F9) set, the tab panel redrawn; never returns (the console loop takes over) ; Called from: $076D, the screen dispatch at the end of runComcenStunnedBlackout ; ---------------------------------------------------------------------- resumeMainMenuScreen: jsr selectDefaultTab ; 1414 pick the tab the current game state suggests jsr clearMenuTextWindow ; 1417 blank the panel area under the tab bar jmp drawSelectedTab ; 141A draw the newly selected tab and fall into the console loop ; ---------------------------------------------------------------------- ; enterMainMenu - Full entry into the console screen (screen 3): picks the default tab, draws the ; whole screen (tab bar and frames) and enters the console loop with that tab's panel drawn. ; In: comcenStunStatus ($922A) and everything selectDefaultTab reads ; Out: the console screen drawn, menuTabIndex set; never returns ; Called from: $0706, the F7 branch of switchToRequestedScreen ; ---------------------------------------------------------------------- enterMainMenu: lda #$7F ; 141D $7F into the REPAIR tab's scratch byte $1412 ... sta repairLoopCounter ; 141F ... which nothing ever reads: the repair marker loops always reload it first, so this is a leftover dead store ldy #$00 ; 1422 leftover tab pre-selection: default to tab 0 (STATS) ... lda comcenStunStatus ; 1424 ... unless the comcen is stunned ... bpl L_142A ; 1427 ... (bit 7 of comcenStunStatus) ... iny ; 1429 ... in which case tab 1 (REPAIR) L_142A: sty menuTabIndex ; 142A store it - but selectDefaultTab three bytes later overwrites it unconditionally, so all of $1422-$142A is dead jsr selectDefaultTab ; 142D really choose the tab jsr showMainMenu ; 1430 draw the tab bar and both frames (it reads menuTabIndex for the text colour) jsr selectDefaultTab ; 1433 choose the tab again - redundant, nothing showMainMenu does changes the inputs jmp drawSelectedTab ; 1436 draw that tab's panel and fall into the console loop ; ---------------------------------------------------------------------- ; mainMenuLoop - The console screen's main loop (screen 3, tabs STATS / REPAIR / MISC). Every pass it ; runs one frame of the game engine (the battle keeps going while the console is open), blinks the ; selected tab, refreshes the REPAIR tab when the comcen stun state changes and the STATS tab when a ; command exchange has been applied, and honours the function keys. The joystick moves the selection ; left and right between the three tabs with a 20-frame repeat, and the fire button dispatches through ; tabActionTable. The loop never returns; every path jumps back to $1439. ; In: joyDirection (zp_5D), joyRepeatTimer (zp_6C), menuTabIndex ($90F9), comcenStunStatus ($922A), ; exchangeAppliedFlag ($920E) ; Out: menuTabIndex, the console screen; runs forever ; Called from: entered by fall-through/JMP from resumeMainMenuScreen and enterMainMenu ; ---------------------------------------------------------------------- mainMenuLoop: jsr updateGameFrame ; 1439 keep the simulation running while the console is open jsr blinkSelectedTab ; 143C toggle the selected tab between its bright and normal colour lda menuTabIndex ; 143F which tab is showing? cmp #$01 ; 1442 1 = REPAIR bne L_1456 ; 1444 any other tab - skip the repair refresh lda comcenStunStatus ; 1446 current comcen stun state ... eor repairTabStunCopy ; 1449 ... against the copy taken when the tab was drawn ... and #$80 ; 144C ... looking only at bit 7 (stunned or not) beq L_1453 ; 144E unchanged - no full redraw needed jsr drawRepairTab ; 1450 the stun state flipped: redraw the whole REPAIR panel in its new colour L_1453: jsr printEnergyValue ; 1453 refresh the comcen energy percentage (it only prints when the value changed) L_1456: lda exchangeAppliedFlag ; 1456 has a command exchange been applied since the STATS tab was drawn? beq L_1463 ; 1459 no - nothing to refresh lda menuTabIndex ; 145B which tab is showing? bne L_1463 ; 145E only tab 0 (STATS) shows the numbers that just changed jsr drawStatsTab ; 1460 recount the units and reprint the STATS panel (it clears the flag again) (also mapGenerator6F00:editDronesAndMissilesBody) L_1463: jsr switchToRequestedScreen ; 1463 let F1/F3/F5/F7 switch to another screen (never returns if it does) ; ---------------------------------------------------------------------- ; Joystick handling: left and right move between the three tabs, everything else falls through to the ; fire-button test. joyRepeatTimer holds the auto-repeat delay so a held stick steps once every 20 ; frames. ; ---------------------------------------------------------------------- lda joyRepeatTimer ; 1466 is the joystick auto-repeat still counting down? bne L_1470 ; 1468 yes - ignore the stick, only look at the button lda joyDirection ; 146A the latched joystick direction cmp #$0F ; 146C $0F = centred bne menuJoyLeftRight ; 146E the stick is being pushed - see which way L_1470: jsr checkFirePressed ; 1470 edge-detect a fresh fire-button press bne mainMenuLoop ; 1473 no press - go round again ; ---------------------------------------------------------------------- ; Fire on a tab: light that tab and call its handler through tabActionTable. The handler address is ; copied into the operand of the JSR at $148D, which is why the disassembly shows 'jsr $FFFF' there. ; ---------------------------------------------------------------------- ldx menuTabIndex ; 1475 the selected tab jsr setTabHighlightColour ; 1478 show it in its bright colour while its action runs lda menuTabIndex ; 147B tab index ... asl a ; 147E ... times two ... tay ; 147F ... is the offset into the table of 16-bit handler addresses lda tabActionTable,y ; 1480 handler low byte ... sta L_148D+1 ; 1483 ... self-modifying: into the operand of the JSR at $148D iny ; 1486 next table byte lda tabActionTable,y ; 1487 handler high byte ... sta L_148D+2 ; 148A ... into the high half of that operand L_148D: jsr irqVectorHi ; 148D call the tab's fire handler (operand patched just above; tab 0 points at the RTS at $158E) L_1490: jmp mainMenuLoop ; 1490 back to the top of the loop ; ---------------------------------------------------------------------- ; Left/right tab movement. The direction bits are active low, so after masking bits 2 and 3, $04 ; means right is pressed and $08 means left; $0C means neither. ; ---------------------------------------------------------------------- menuJoyLeftRight: and #$0C ; 1493 keep the left (bit 2) and right (bit 3) switches cmp #$0C ; 1495 $0C = neither of them pressed beq mainMenuLoop ; 1497 up or down only - ignore it ldx #$14 ; 1499 $14 = 20 frames ... stx joyRepeatTimer ; 149B ... of auto-repeat delay before the selection may move again pha ; 149D remember the direction ldx menuTabIndex ; 149E the tab we are leaving ... jsr setTabNormalColour ; 14A1 ... goes back to its normal colour pla ; 14A4 get the direction back cmp #$04 ; 14A5 $04 = right pressed (bit 3 low) bne L_14B5 ; 14A7 not right, so it must be left lda menuTabIndex ; 14A9 the selected tab cmp #$02 ; 14AC 2 = MISC, the rightmost tab beq L_1490 ; 14AE already at the right edge - ignore inc menuTabIndex ; 14B0 move one tab to the right bne L_14BD ; 14B3 always taken L_14B5: lda menuTabIndex ; 14B5 the selected tab beq L_1490 ; 14B8 already at STATS, the left edge - ignore dec menuTabIndex ; 14BA move one tab to the left L_14BD: lda #$02 ; 14BD sound 2 = the selection click jsr playSound ; 14BF play it ldx menuTabIndex ; 14C2 the newly selected tab ... jsr setTabHighlightColour ; 14C5 ... gets its bright colour jsr clearMenuTextWindow ; 14C8 blank the panel under the tab bar ready for the new contents ; ---------------------------------------------------------------------- ; Draw the panel of the currently selected tab. Entered directly by both console entry points as well ; as after a tab change. ; ---------------------------------------------------------------------- drawSelectedTab: lda menuTabIndex ; 14CB which tab? bne L_14D9 ; 14CE not tab 0 jsr drawGameSetupSummary ; 14D0 STATS: the game type, rules, play order, owner names and map id jsr drawStatsTab ; 14D3 plus the unit counts, drones, missiles, comspeed and terrain points (in overlay B) (also mapGenerator6F00:editDronesAndMissilesBody) jmp mainMenuLoop ; 14D6 back to the loop L_14D9: cmp #$01 ; 14D9 tab 1? bne L_14E6 ; 14DB no - it must be MISC jsr drawRepairTab ; 14DD REPAIR: the comcen schematic and its four system markers jsr snapshotComcenStatus ; 14E0 remember the comcen status that is now on screen jmp mainMenuLoop ; 14E3 back to the loop L_14E6: cmp #$02 ; 14E6 tab 2? bne L_14ED ; 14E8 menuTabIndex is only ever 0, 1 or 2 (every writer is in this loop), so this branch is never taken jsr printOptionsList ; 14EA MISC: print the option list (ABORT GAME, VOICE PAUSE, EXIT ...) L_14ED: jmp mainMenuLoop ; 14ED back to the loop ; ---------------------------------------------------------------------- ; snapshotComcenStatus - Remembers what the REPAIR tab is currently showing: copies the three comcen ; system status bytes ($9242-$9244) into the snapshot at $92D7-$92D9 and the comcen's energy (the low ; 6 bits of its entry in unitEnergyTable) into $92D6. selectDefaultTab compares the live values ; against this snapshot to decide whether the player still has repairs to look at. ; In: comcenSystemStatus ($9241..), comcenUnit ($9236), unitEnergyTable ($F9C4) ; Out: comcenStatusSnapshot ($92D6-$92D9) ; Called from: mainMenuLoop ($14E0) and the end of repairTabAction ($187E) ; ---------------------------------------------------------------------- snapshotComcenStatus: ldx #$03 ; 14F0 index 3 down to 1: the battle, radar and drone system bytes L_14F2: lda comcenSystemStatus,x ; 14F2 system status byte $9241+X sta comcenStatusSnapshot,x ; 14F5 into the snapshot at $92D6+X dex ; 14F8 next system bne L_14F2 ; 14F9 stop at index 1 - slot 0 is filled from the energy below ldx comcenUnit ; 14FB this side's comcen (unit 49 or 99) lda unitEnergyTable,x ; 14FE its energy byte and #$3F ; 1501 the low 6 bits are the energy 0-50 (the top bits are flags) sta comcenStatusSnapshot ; 1503 snapshot slot 0 = the comcen energy rts ; 1506 done ; ---------------------------------------------------------------------- ; selectDefaultTab - Chooses which console tab should be showing. If the game is over or has not ; started (gameEndReason non-zero) it opens MISC (2), where ABORT/EXIT live. Otherwise it opens ; REPAIR (1) when the comcen is stunned or when anything on the comcen has changed since the last ; snapshotComcenStatus, and MISC (2) when nothing needs attention. STATS (0) is never chosen here. ; In: gameEndReason ($0BA8), comcenStunStatus ($922A), comcenSystemStatus, comcenStatusSnapshot, ; comcenUnit, unitEnergyTable ; Out: menuTabIndex ($90F9) ; Called from: resumeMainMenuScreen ($1414) and enterMainMenu ($142D, $1433) ; ---------------------------------------------------------------------- selectDefaultTab: ldy #$02 ; 1507 assume tab 2 = MISC lda gameEndReason ; 1509 0 while a game is actually running bne L_1531 ; 150C no game in progress - MISC it is ldy #$00 ; 150E a game is running: assume tab 0 = STATS for now lda comcenStunStatus ; 1510 is the comcen stunned? bmi L_152F ; 1513 yes - the player needs the REPAIR tab ldx #$03 ; 1515 compare the three system bytes with the snapshot, index 3 down to 1 L_1517: lda comcenSystemStatus,x ; 1517 live system status $9241+X ... cmp comcenStatusSnapshot,x ; 151A ... against the snapshot $92D6+X bne L_152F ; 151D something changed - show REPAIR dex ; 151F next system bne L_1517 ; 1520 stop at index 1 ldx comcenUnit ; 1522 this side's comcen lda unitEnergyTable,x ; 1525 its energy byte and #$3F ; 1528 keep the 0-50 energy value cmp comcenStatusSnapshot ; 152A has the comcen lost energy since the snapshot? beq L_1531 ; 152D unchanged - nothing to repair, leave Y at 2 = MISC L_152F: ldy #$01 ; 152F something needs attention: tab 1 = REPAIR L_1531: sty menuTabIndex ; 1531 store the chosen tab rts ; 1534 done ; tabActionTable: the three fire-button handlers of the console tabs, one 16-bit little-endian address ; each, indexed by menuTabIndex*2 and copied into the JSR operand at $148D. ; +0 tab 0 STATS -> $158E, the RTS at the end of setMenuWindowBounds, i.e. no action at all ; +2 tab 1 REPAIR -> repairTabAction ($17DA), the 'repair which system' menu ; +4 tab 2 MISC -> miscTabAction ($1A49), the ABORT GAME / VOICE PAUSE / EXIT option list tabActionTable: .addr $158E ; 1535 three handler addresses, one per console tab .addr repairTabAction ; 1537 .addr miscTabAction ; 1539 ; ---------------------------------------------------------------------- ; showMainMenu - Builds the console screen from scratch: resets the glyph plotter's parameter block, ; sets the background colour, draws the STATS/REPAIR/MISC tab bar and the two nested frames, and ends ; by repainting the status message line. ; In: none ; Out: the whole screen 3 drawn; textDrawParams, vicBg0Shadow ($9004), cellBackgroundColour ($9226), ; screenBuiltFlag (zp_41) ; Called from: returnToMainMenu ($0AC2) and enterMainMenu ($1430) ; ---------------------------------------------------------------------- showMainMenu: lda #$00 ; 153B 0 into the four glyph-plotter parameters below sta useDrawColourFlag ; 153D useFixedColourFlag = 0: take the colour from the text state, not a fixed byte sta drawColourByte ; 1540 drawColourByte = 0 sta blockFillFlag ; 1543 blockFillFlag = 0 sta blockFillChar ; 1546 blockFillChar = 0 lda #$0C ; 1549 colour $0C = medium grey ... sta vicBg0Shadow ; 154B ... as the screen background (the IRQ copies the shadow into $D021) lda #$00 ; 154E 0 ... sta cellBackgroundColour ; 1550 ... = the cell background colour used by the tile drawing lda #$01 ; 1553 1 ... sta screenBuiltFlag ; 1555 ... marks 'a screen has been built' (written by every screen builder, read nowhere) lda #$00 ; 1557 0 ... sta blockFillFlag ; 1559 ... into blockFillFlag again - redundant, it was already cleared at $1543 jsr drawMenuTabBar ; 155C print STATS, REPAIR and MISC and colour their three tab boxes jsr drawMenuFrame ; 155F draw the outer and inner frames jmp redrawStatusMessage ; 1562 repaint whatever message belongs on the status line and return to our caller ; ---------------------------------------------------------------------- ; drawMenuFrame - Draws the console's two nested frames from the predefined window table: window $18 ; is the outer 40x20 box starting at row 5, window $19 the inner 38x18 box at row 6. Falls into ; clearMenuTextWindow. ; In: none ; Out: the frames drawn, then the text window set and cleared ; Called from: showMainMenu ($155F) ; ---------------------------------------------------------------------- drawMenuFrame: ldx #$18 ; 1565 window $18 = column 0, row 5, 40x20 cells - the outer frame jsr drawWindowFrame ; 1567 draw it from its 9-tile frame set ldx #$19 ; 156A window $19 = column 1, row 6, 38x18 cells - the inner frame jsr drawWindowFrame ; 156C draw it too ; ---------------------------------------------------------------------- ; clearMenuTextWindow - Sets the console panel's text colour and window bounds and blanks the window. ; In: comcenStunStatus ($922A), menuTabIndex ($90F9) ; Out: textColour (zp_3D), the window bounds zp_39-zp_40, the panel area cleared ; Called from: resumeMainMenuScreen ($1417), mainMenuLoop ($14C8), $1ACA, $1AD6 and overlay A ($7EA0) ; ---------------------------------------------------------------------- clearMenuTextWindow: jsr setMenuTextWindow ; 156F colour and window bounds for the panel jmp bootDriveDataBit ; 1572 blank it and return to our caller (also textEngineC000:clearTextWindow) ; ---------------------------------------------------------------------- ; setMenuTextWindow - Picks the console text colour from the comcen stun state (red on the REPAIR tab ; while the comcen is stunned) and falls into setMenuWindowBounds. ; In: comcenStunStatus ($922A), menuTabIndex ($90F9) ; Out: textColour (zp_3D) plus the window bounds ; Called from: clearMenuTextWindow ($156F), $1866 and $1A80 ; ---------------------------------------------------------------------- setMenuTextWindow: jsr setMenuTextColour ; 1575 $61 normally, $21 on a stunned REPAIR tab ; ---------------------------------------------------------------------- ; setMenuWindowBounds - Sets the text window that the console panels print into: rows 7 to 21, left ; column 2, 36 characters wide, cursor homed at the top left of the window. Its final RTS at $158E ; doubles as the do-nothing handler of the STATS tab in tabActionTable. ; In: none ; Out: textCursorCol=0, textWindowTop=7, textCursorRow=7, textWindowLeft=2, textWindowBottom=$16, ; textWindowWidth=$24 ; Called from: setMenuTextWindow by fall-through, clearMenuWindowPlain ($1593) and $19E7 ; ---------------------------------------------------------------------- setMenuWindowBounds: lda #$00 ; 1578 cursor column 0 ... sta textCursorCol ; 157A ... relative to the window's left edge lda #$07 ; 157C row 7 = the first line under the tab bar and frame ... sta textWindowTop ; 157E ... as the top of the window ... sta textCursorRow ; 1580 ... and as the starting cursor row lda #$02 ; 1582 screen column 2 ... sta textWindowLeft ; 1584 ... is the window's left edge lda #$16 ; 1586 $16 = row 22 is the exclusive bottom limit ... sta textWindowBottom ; 1588 ... so the window covers rows 7-21 lda #$24 ; 158A $24 = 36 characters ... sta textWindowWidth ; 158C ... is the window width and the wrap point rts ; 158E also the STATS tab's 'do nothing' fire handler (tabActionTable entry 0) ; ---------------------------------------------------------------------- ; clearMenuWindowPlain - Blanks the whole console-menu panel with text colour 0 (black text, no ; highlight EOR mask). This is the screen-id-3 arm of clearCurrentScreenWindows ($2DDE), which ; re-clears the content area of whichever screen is active; it is reached only by the JMP at $2E0A, so ; it returns straight to that routine's caller (typically the $C000 overlay's resume-after-pause path ; at $C75E). ; In: nothing in registers; textFillGlyph zp_3E supplies the blank glyph ; Out: zp_3D = 0, the menu window geometry of setMenuWindowBounds (columns 2..37, rows 7..21) and the ; whole panel filled with blanks ; Called from: JMP at $2E0A (clearCurrentScreenWindows, screen id 3) ; ---------------------------------------------------------------------- clearMenuWindowPlain: lda #$00 ; 158F colour byte 0: black text and an all-zero highlight EOR mask sta textColour ; 1591 the text engine writes zp_3D into colour RAM for every cell it touches jsr setMenuWindowBounds ; 1593 menu panel window: left column 2, width $24, rows 7..$16 jmp bootDriveDataBit ; 1596 fill the panel with the blank glyph zp_3E and return to clearCurrentScreenWindows' caller (also textEngineC000:clearTextWindow) ; ---------------------------------------------------------------------- ; drawGameSetupSummary - Draws the fixed header of the console STATS tab (menu tab 0): the rules word ; STANDARD/CUSTOM, the game-type name, an optional '. 1ST PLAY'/'. 2ND PLAY' suffix, then the run-time ; owner-name line and the column of row labels (GRUNTS/RIDERS/.../MAP ID) that drawStatsTab ($8518, ; overlay B) later fills numbers in beside. For a CUSTOM game it adds the ' MENUS xxxx DAMAGE xxx.' ; summary line. ; In: gameTypeOptions $0BA3 (bits 0-2 game type, bit 6 CUSTOM rules), playsFirstFlag $0BA1, ; ownerNameString $0B7F, statsTabLabels $86AC and msgOptionSummary $0636 (both only valid while ; overlay B / the string block is resident) ; Out: text drawn into the bitmap from row 7 down; zp_3B window left ends at 10 (or 8 for CUSTOM), ; zp_3D = $6E, zp_3F/zp_40 left after the last character; self-modifies the operand at $15DD ; Called from: mainMenuLoop at $14D0 when tab 0 (STATS) becomes the selected tab ; Line 1 of the STATS tab: ' [. 1ST PLAY]'. ; ---------------------------------------------------------------------- drawGameSetupSummary: lda #$6F ; 1599 colour byte $6F: light-grey text (colour RAM low nibble), highlight EOR mask 6 sta textColour ; 159B set the text colour for the heading line lda #$00 ; 159D cursor to the left edge of the window sta textCursorCol ; 159F textCursorCol is relative to textWindowLeft lda #$07 ; 15A1 heading starts at screen column 7 sta textWindowLeft ; 15A3 window left edge = 7 lda #$07 ; 15A5 first line of the panel is screen row 7 sta textCursorRow ; 15A7 absolute cursor row jsr updateCursorPointers ; 15A9 recompute the bitmap and colour-RAM pointers for that cell ldx #$01 ; 15AC assume rulesNameTable entry 1 = CUSTOM lda gameTypeOptions ; 15AE game settings byte: bits 0-2 type, bit 3 UNIT MENUS, bit 4 DAMAGE, bit 6 CUSTOM rules pha ; 15B1 keep the settings byte on the stack for the game-type lookup below and #$40 ; 15B2 bit 6 = CUSTOM rules selected bne L_15C2 ; 15B4 branch if CUSTOM - keep table entry 1 and always print the play order jsr isAsymmetricGameType ; 15B6 Z=1 for game type 1 (QB SNEAK) or 6 (DEFENDER), the asymmetric ones beq L_15C0 ; 15B9 branch if this type shows 1ST/2ND PLAY - no padding needed lda #$04 ; 15BB four blanks... jsr bootReceiveSectorData ; 15BD ...printFillChars ($C0EF): centres 'STANDARD ' when no ' 1ST PLAY' follows (also textEngineC000:printFillChars) L_15C0: ldx #$00 ; 15C0 rulesNameTable entry 0 = STANDARD L_15C2: stx L_15DC+1 ; 15C2 self-modifying code: patches the LDA #imm operand at $15DD, so $15DC loads 1 for CUSTOM and 0 for STANDARD lda #$11 ; 15C5 low byte of rulesNameTable ldy #$CC ; 15C7 $CC11 = rulesNameTable, two 8-byte bit-7-terminated entries jsr printTableEntry ; 15C9 print entry X ($CC11 + X*8): 'STANDARD' or 'CUSTOM' lda #$01 ; 15CC one blank... jsr bootReceiveSectorData ; 15CE ...between the rules word and the game type name (also textEngineC000:printFillChars) pla ; 15D1 recover the settings byte and #$07 ; 15D2 bits 0-2 = game type 0..6 tax ; 15D4 use it as the table index lda #$C8 ; 15D5 low byte of scenarioNameTable ldy #$CC ; 15D7 $CCC8 = scenarioNameTable, seven 8-byte names jsr printTableEntry ; 15D9 print SCRIMAGE / QB SNEAK / THE BOMB / FACE-OFF / SLUGGERS / FULL WAR / DEFENDER L_15DC: lda #$FF ; 15DC operand patched at $15C2: 1 = CUSTOM rules, 0 = STANDARD bne L_15E5 ; 15DE CUSTOM rules always show whose play it is jsr isAsymmetricGameType ; 15E0 standard rules: only QB SNEAK and DEFENDER have a play order bne L_1606 ; 15E3 symmetric game type under standard rules - skip the play-order suffix ; ---------------------------------------------------------------------- ; Suffix '. 1ST PLAY' / '. 2ND PLAY'. ; ---------------------------------------------------------------------- L_15E5: lda #$AE ; 15E5 $AE = '.' with bit 7 set; printChar masks bit 7 off jsr printChar ; 15E7 print the full stop after the game type lda #$02 ; 15EA two blanks... jsr bootReceiveSectorData ; 15EC ...before the play-order words (also textEngineC000:printFillChars) lda #$FE ; 15EF low byte of '1ST' ldy #$CB ; 15F1 $CBFE = '1ST' ldx playsFirstFlag ; 15F3 $0BA1 non-zero = this machine has first play this game bne L_15FC ; 15F6 branch if we play first - keep the '1ST' pointer lda #$01 ; 15F8 low byte of '2ND' ldy #$CC ; 15FA $CC01 = '2ND' L_15FC: jsr printString ; 15FC print '1ST' or '2ND' lda #$F9 ; 15FF low byte of ' PLAY' ldy #$CB ; 1601 $CBF9 = ' PLAY' jsr printString ; 1603 print ' PLAY' ; ---------------------------------------------------------------------- ; Owner-name line plus the column of STATS row labels, then the CUSTOM rules summary. ; ---------------------------------------------------------------------- L_1606: jsr printNewline ; 1606 carriage return: column 0 of the next panel row lda #$0A ; 1609 label block starts at screen column 10 sta textWindowLeft ; 160B move the window left edge lda #$03 ; 160D indent three more columns (absolute column 13) sta textCursorCol ; 160F cursor column relative to the window jsr updateCursorPointers ; 1611 recompute the cell pointers for the new window/cursor dec textColour ; 1614 $6F -> $6E: light-blue body text instead of light grey lda #$7F ; 1616 low byte of ownerNameString ldy #$0B ; 1618 $0B7F = the 21-character owner/player line built at run time by buildOwnerNameString jsr printString ; 161A print it; printString leaves zp_A2/zp_A3 just past the terminator lda #$AC ; 161D low byte of statsTabLabels ldy #$86 ; 161F $86AC lives in the resident $6F00 overlay B (comcenScreens6F00) jsr printString ; 1621 one string with embedded CRs draws GRUNTS/RIDERS/BOOMERS/SPIES/DRONES/MISSILES/COMSPEED/TER PTS/RECYCLER/MAP ID lda gameTypeOptions ; 1624 settings byte again and #$40 ; 1627 bit 6 = CUSTOM rules beq L_1639 ; 1629 standard rules - nothing more to print lda #$08 ; 162B custom summary starts at screen column 8 sta textWindowLeft ; 162D move the window left edge once more jsr printNewline ; 162F carriage return onto the summary row lda #$36 ; 1632 low byte of optionSummaryTemplate ldy #$06 ; 1634 $0636 = ' MENUS xxxx DAMAGE xxx.' - overlay B has already patched the xxxx/xxx fields in place jsr printString ; 1636 print the rules summary L_1639: rts ; 1639 done - back to mainMenuLoop ; tabColumnTable - screen column of the top-left cell of each console tab box, indexed by tab number ; (0 STATS, 1 REPAIR, 2 MISC). Each box is 2x2 character cells at row 1. Read by ; setTabNormalColour/setTabHighlightColour. tabColumnTable: .byte $0B,$13,$1B ; 163A ... STATS at column $0B, REPAIR at $13, MISC at $1B ; tabColourTable - normal screen-RAM colour byte of each tab box (upper nibble = multicolour pair 01, ; lower nibble = pair 10). One entry per tab 0..2. tabColourTable: .byte $05,$06,$02 ; 163D ... STATS black/green $05, REPAIR black/blue $06, MISC black/red $02 ; tabHighlightColourTable - bright variant of tabColourTable used while a tab is selected and ; blinking; same three-entry layout. A zero entry would make setTabHighlightColour fall through to the ; normal colour, but none is zero. tabHighlightColourTable: .byte $0D,$0E,$0A ; 1640 ... STATS black/light green $0D, REPAIR black/light blue $0E, MISC black/light red $0A ; ---------------------------------------------------------------------- ; drawMenuTabBar - Draws the three console tabs of screen 3: prints the labels STATS, REPAIR and MISC ; on row 3 starting at column 10, each in its own colour, then paints the 2x2 tab box behind each of ; them in its normal colour, and finally resets the bitmap/colour draw modes to 'replace'. ; In: nothing; uses the character-block at $9804 and the three colour/column tables at $163A-$1642 ; Out: labels and tab boxes drawn; zp_21 used as the 2..0 tab counter (setTabNormalColour clobbers X); ; text window left as the full screen, zp_3D = $C2, zp_3F/zp_40 after 'MISC' ; Called from: showMainMenu at $155C, when the STATS/REPAIR/MISC screen is built ; Print the three tab labels on row 3, two/three blanks apart. ; ---------------------------------------------------------------------- drawMenuTabBar: lda #$CD ; 1643 colour byte $CD: light-green text, highlight EOR mask $C - matches the green STATS tab sta textColour ; 1645 set the label colour jsr setFullScreenWindow ; 1647 print over the whole screen: left 0, top 0, bottom $19, width $28 lda #$0A ; 164A labels start at column 10 sta textCursorCol ; 164C cursor column (window left is 0 here, so this is absolute) lda #$03 ; 164E tab labels sit on screen row 3 sta textCursorRow ; 1650 absolute cursor row jsr updateCursorPointers ; 1652 recompute the cell pointers lda #$70 ; 1655 low byte of 'STATS' ldy #$CB ; 1657 $CB70 = 'STATS' jsr printString ; 1659 print it lda #$02 ; 165C two blanks... jsr bootReceiveSectorData ; 165E ...between STATS and REPAIR (also textEngineC000:printFillChars) lda #$C6 ; 1661 colour byte $C6: blue text, matching the blue REPAIR tab sta textColour ; 1663 switch colour before the next label lda #$75 ; 1665 low byte of 'REPAIR' ldy #$CB ; 1667 $CB75 = 'REPAIR' jsr printString ; 1669 print it lda #$03 ; 166C three blanks... jsr bootReceiveSectorData ; 166E ...between REPAIR and MISC (also textEngineC000:printFillChars) lda #$C2 ; 1671 colour byte $C2: red text, matching the red MISC tab sta textColour ; 1673 switch colour again lda #$6C ; 1675 low byte of 'MISC' ldy #$CB ; 1677 $CB6C = 'MISC' jsr printString ; 1679 print it ; ---------------------------------------------------------------------- ; Paint the three tab boxes, from MISC (2) down to STATS (0). The loop counter has to live in zero ; page because setTabNormalColour reloads X for fillScreenColourRect. ; ---------------------------------------------------------------------- ldx #$02 ; 167C start with tab 2 = MISC stx scratch21 ; 167E keep the tab index in zp_21 across the call L_1680: jsr setTabNormalColour ; 1680 draw tab X's box shape and fill it with its normal colour dec scratch21 ; 1683 next tab down ldx scratch21 ; 1685 reload the index that setTabNormalColour destroyed bpl L_1680 ; 1687 loop while the index is still 0..2 jmp resetDrawModes ; 1689 restore bitmap and colour draw mode 0 (replace) and return ; ---------------------------------------------------------------------- ; setTabHighlightColour - Redraws console tab X in its bright colour: looks the colour up in ; tabHighlightColourTable and joins setTabNormalColour's common tail. A zero table entry would fall ; through to the normal colour, but all three entries are non-zero, so the BNE is always taken. ; In: X = tab index 0..2 ; Out: the tab's 2x2 box redrawn and recoloured; see setTabNormalColour for the details ; Called from: mainMenuLoop at $1478 (fire pressed on the tab) and $14C5 (tab changed), and ; blinkSelectedTab at $16D0 ; ---------------------------------------------------------------------- setTabHighlightColour: lda tabHighlightColourTable,x; 168C bright colour byte of tab X ($0D/$0E/$0A) bne L_1694 ; 168F always taken; a zero entry would mean 'no bright colour, use the normal one' ; ---------------------------------------------------------------------- ; setTabNormalColour - Redraws console tab X: stores the chosen colour and the tab index into the two ; self-modified operands, sets the character-block origin to row 1 / tabColumnTable[X], draws the 2x2 ; tab-box shape from $9804 and then fills those 2x2 screen-RAM colour cells with the tab colour. ; In: X = tab index 0..2; tabColumnTable, tabColourTable/tabHighlightColourTable ; Out: D_2A2D = 1 (row), D_2A2E = tab column, D_9221/D_9222 = 2x2 (set by drawCharBlock), four bitmap ; cells and four screen-RAM colour bytes updated; A/X/Y clobbered; patches the operands at $16AD ; and $16AF ; Called from: mainMenuLoop at $14A1, drawMenuTabBar's loop at $1680, blinkSelectedTab at $16D3, and ; by falling through from setTabHighlightColour ; ---------------------------------------------------------------------- setTabNormalColour: lda tabColourTable,x ; 1691 normal colour byte of tab X ($05/$06/$02) L_1694: sta L_16AE+1 ; 1694 self-modifying code: the colour becomes the LDA #imm operand at $16AF stx L_16AC+1 ; 1697 self-modifying code: the tab index becomes the LDX #imm operand at $16AD - dead, see $16AC lda #$01 ; 169A tab boxes sit on character row 1 sta windowOriginRow ; 169C block/fill origin row for drawCharBlock and fillScreenColourRect lda tabColumnTable,x ; 169F screen column of this tab ($0B, $13 or $1B) sta windowOriginCol ; 16A2 block/fill origin column lda #$04 ; 16A5 low byte of the tab-box shape ldy #$98 ; 16A7 $9804 = character block: width 2, height 2, glyphs $45,$46,$47,$48 jsr drawCharBlock ; 16A9 draw the 2x2 box into the bitmap at (row 1, tab column) L_16AC: ldx #$FF ; 16AC operand written at $1697 = the tab index; dead code - X is reloaded two instructions later L_16AE: lda #$FF ; 16AE operand written at $1694 = the tab colour byte ldx #$02 ; 16B0 fill width: 2 character cells ldy #$02 ; 16B2 fill height: 2 character cells jmp fillScreenColourRect ; 16B4 paint the 2x2 colour cells and return to the caller ; ---------------------------------------------------------------------- ; blinkSelectedTab - Called once per pass of mainMenuLoop. It does nothing until the IRQ frame ; countdown zp_6E reaches 0; then it reloads it with 10 frames, flips blinkPhase and repaints the ; currently selected tab in either its bright or its normal colour, so the open tab blinks about three ; times a second. ; In: zp_6E menuBlinkTimer, menuTabIndex $90F9 (selected tab), blinkPhase $1889 ; Out: zp_6E = 10, blinkPhase toggled, the selected tab recoloured ; Called from: mainMenuLoop at $143C ; ---------------------------------------------------------------------- blinkSelectedTab: lda menuBlinkTimer ; 16B7 IRQ frame countdown shared by the tab blink and the menu-row blink beq L_16BC ; 16B9 branch when the countdown has expired rts ; 16BB still counting down - leave the tab as it is L_16BC: lda #$0A ; 16BC 10 frames per blink phase sta menuBlinkTimer ; 16BE restart the countdown (the raster IRQ decrements it) ldx menuTabIndex ; 16C0 $90F9 = tab currently open: 0 STATS, 1 REPAIR, 2 MISC bpl L_16C6 ; 16C3 branch if a tab is selected rts ; 16C5 negative index = no tab open, nothing to blink L_16C6: lda blinkPhase ; 16C6 current blink phase, 0 or 1 eor #$01 ; 16C9 flip it sta blinkPhase ; 16CB store the new phase (also read by the marker blink) beq L_16D3 ; 16CE phase 0 - draw the tab in its normal colour jmp setTabHighlightColour ; 16D0 phase 1 - draw it bright L_16D3: jmp setTabNormalColour ; 16D3 tail call: normal colour ; ---------------------------------------------------------------------- ; printEnergyLabel - Prints the fixed word 'ENERGY' of the REPAIR tab at window column $1C, row 8 ; (absolute column 30 inside the menu panel whose left edge is 2), in the current menu text colour. ; In: none; setMenuTextColour picks $61 or $21 depending on the comcen stun flag ; Out: zp_3D, zp_3F, zp_40 set; the label drawn ; Called from: drawRepairTab at $17BE ; ---------------------------------------------------------------------- printEnergyLabel: jsr setMenuTextColour ; 16D6 white text, or white on the red panel while the comcen is stunned lda #$08 ; 16D9 row 8 of the screen sta textCursorRow ; 16DB absolute cursor row lda #$1C ; 16DD column $1C relative to the panel's left edge (2) = screen column 30 sta textCursorCol ; 16DF cursor column jsr updateCursorPointers ; 16E1 recompute the cell pointers lda #$B8 ; 16E4 low byte of 'ENERGY' ldy #$CB ; 16E6 $CBB8 = 'ENERGY' jmp printString ; 16E8 print it and return to drawRepairTab ; ---------------------------------------------------------------------- ; setMenuTextColour - Chooses the text colour byte for the console menu screen: normally $61 (white ; text, highlight EOR mask 6 matching the blue panel), but $21 while the REPAIR tab is open and our ; own comcen is stunned, because drawRepairTab then paints the panel red instead of blue. ; In: comcenStunStatus $922A (bit 7 = stunned), menuTabIndex $90F9 ; Out: zp_3D textColour ; Called from: setMenuTextWindow $1575, printEnergyLabel $16D6, printEnergyValue $1702, ; setRepairMessageWindow $19E4 ; ---------------------------------------------------------------------- setMenuTextColour: lda #$61 ; 16EB default: white text on the blue panel, EOR mask 6 for the row highlight ldy comcenStunStatus ; 16ED $922A bit 7 = our comcen is stunned (bits 0-3 hold the stun counter) bpl L_16FB ; 16F0 not stunned - keep $61 ldy menuTabIndex ; 16F2 which tab is open? cpy #$01 ; 16F5 tab 1 = REPAIR bne L_16FB ; 16F7 the other tabs keep the blue-panel colour lda #$21 ; 16F9 REPAIR tab while stunned: white text, EOR mask 2 for the red panel L_16FB: sta textColour ; 16FB store the chosen colour rts ; 16FD return ; lastEnergyShown - the raw comcen energy value (0..50) that printEnergyValue last drew, so the ; reading is only redrawn when it actually changes. drawRepairTab presets it to $FF to force the first ; print. lastEnergyShown: .byte $00 ; 16FE . on disk 0; drawRepairTab writes $FF to force the first print ; ---------------------------------------------------------------------- ; printEnergyValue - Prints the comcen's energy as a percentage at column $22, row 9 of the REPAIR ; tab, right-aligned in a three-character field, but only when the value has changed since the last ; call. It then classifies the same value into comcenSystemStatus[0], the ENERGY entry of the four ; comcen systems: 0 = OK at 100%, $40 = degraded from 21% up, $80 = critical below that. ; In: comcenUnit $9236 (49 or 99), unitEnergyTable $F9C4 (bits 0-5 = 0..50 energy), lastEnergyShown ; $16FE ; Out: lastEnergyShown updated, comcenSystemStatus $9241 updated, up to three characters drawn; the ; caller's text window is saved to $92B6 and restored ; Called from: mainMenuLoop at $1453 (every pass while the REPAIR tab is open), drawRepairTab at ; $17C1, drawSystemMarker at $18A0 ; Print the percentage, padded with leading blanks so it always occupies three cells. ; ---------------------------------------------------------------------- printEnergyValue: jsr saveTextWindow ; 16FF save the caller's 12-byte text-window state to $92B6 jsr setMenuTextColour ; 1702 menu text colour ($61, or $21 while stunned) lda #$09 ; 1705 the reading sits on row 9 sta textCursorRow ; 1707 absolute cursor row lda #$22 ; 1709 column $22 relative to the panel left edge (2) = screen column 36 sta textCursorCol ; 170B cursor column jsr updateCursorPointers ; 170D recompute the cell pointers ldy comcenUnit ; 1710 our own comcen: unit 49 (side 0) or 99 (side 1) lda unitEnergyTable,y ; 1713 energy array of the unit records, $64 apart from $F640 and #$3F ; 1716 bits 0-5 hold 0..50; the upper bits are other unit flags cmp lastEnergyShown ; 1718 compare with the value already on screen beq L_173C ; 171B unchanged - skip the (slow) redraw sta lastEnergyShown ; 171D remember the new raw value asl a ; 1720 x2 turns 0..50 into a 0..100 percentage cmp #$64 ; 1721 100? bcs L_1737 ; 1723 three digits - no padding pha ; 1725 keep the percentage lda #$01 ; 1726 one blank... jsr bootReceiveSectorData ; 1728 ...so a two-digit value is right-aligned (also textEngineC000:printFillChars) pla ; 172B get the percentage back cmp #$0A ; 172C 10? bcs L_1737 ; 172E two digits - one blank was enough pha ; 1730 keep the percentage lda #$01 ; 1731 a second blank... jsr bootReceiveSectorData ; 1733 ...so a one-digit value is right-aligned too (also textEngineC000:printFillChars) pla ; 1736 get the percentage back L_1737: ldy #$00 ; 1737 printDecimal wants a 24-bit value: middle byte 0 (zp_7E is already 0) jsr printDecimal ; 1739 print it without leading zeros ; ---------------------------------------------------------------------- ; Recompute the ENERGY entry of comcenSystemStatus from the same reading. This runs even when the ; printing was skipped, so drawSystemMarker always sees a fresh status. ; ---------------------------------------------------------------------- L_173C: jsr restoreTextWindow ; 173C put the caller's text window back ldy comcenUnit ; 173F comcen unit index again lda unitEnergyTable,y ; 1742 its energy byte and #$3F ; 1745 0..50 asl a ; 1747 0..100 percent ldy #$00 ; 1748 assume status 0 = system OK cmp #$64 ; 174A exactly 100%? beq L_1756 ; 174C yes - the energy system counts as undamaged ldy #$40 ; 174E $40 = degraded (yellow marker) cmp #$15 ; 1750 21%? bcs L_1756 ; 1752 21% or more - degraded ldy #$80 ; 1754 $80 = critical/damaged (red marker) L_1756: sty comcenSystemStatus ; 1756 store into comcenSystemStatus[0], the ENERGY system rts ; 1759 return ; ---------------------------------------------------------------------- ; drawRepairTab - Builds the whole REPAIR tab: clears the panel in blue ($66) or, if our comcen is ; stunned, red ($22); unpacks the comcen schematic picture into the bitmap; paints a 20x10 colour ; block behind it; resets the blink and energy bookkeeping; draws the four system markers (only when ; not stunned); prints the ENERGY label and value; and, when stunned, prints 'COMCEN STUNNED!'. The ; stun flag is copied to $1413 so mainMenuLoop can notice a change and redraw. ; In: comcenStunStatus $922A, comcenUnit $9236, unitEnergyTable $F9C4 ; Out: $1413 = stun flag snapshot, zp_6E = 0, showOkMarkers = 0, blinkPhase = 0 then 1, ; lastEnergyShown = $FF, comcenEnergySnapshot = current energy, menuSelectedItem $91D5 left at 4 ; (the (EXIT) row of the REPAIR WHICH menu), the panel redrawn ; Called from: mainMenuLoop at $1450 (stun state changed) and $14DD (REPAIR tab selected) ; Clear the panel and draw the comcen schematic with its backing colour block. ; ---------------------------------------------------------------------- drawRepairTab: ldy #$66 ; 175A assume panel colour $66: blue text/EOR mask 6 lda comcenStunStatus ; 175C bit 7 = our comcen is stunned sta repairTabStunCopy ; 175F snapshot for mainMenuLoop's 'has the stun state changed?' test at $1449 bpl L_1766 ; 1762 not stunned - keep the blue panel ldy #$22 ; 1764 stunned: colour $22, a red panel L_1766: sty textColour ; 1766 set the panel text colour jsr bootDriveDataBit ; 1768 blank the whole menu window in that colour (also textEngineC000:clearTextWindow) jsr drawComcenSchematic ; 176B $8632 in overlay B: RLE-unpacks the comcen side view into the bitmap at row 10, column 13 (the same address holds unrelated code in overlay A) (also mapGenerator6F00:printOnOffMenuAndApplyBit) lda #$0D ; 176E colour-block origin column 13 sta windowOriginCol ; 1770 fillScreenColourRect origin column lda #$0A ; 1773 colour-block origin row 10 sta windowOriginRow ; 1775 fillScreenColourRect origin row ldx #$14 ; 1778 20 cells wide - the width of the schematic ldy #$0A ; 177A 10 cells tall lda textColour ; 177C take the panel colour... and #$0F ; 177E ...keep its low nibble (6 blue, or 2 red)... ora #$10 ; 1780 ...and put white (1) in the upper nibble: $16 normally, $12 when stunned jsr fillScreenColourRect ; 1782 paint the schematic's background colour cells ; ---------------------------------------------------------------------- ; Reset the blink/energy bookkeeping the marker code depends on. ; ---------------------------------------------------------------------- ldx #$00 ; 1785 zero for the three flags below stx menuBlinkTimer ; 1787 blink countdown expired, so the next blink happens at once stx showOkMarkers ; 1789 only the REPAIR WHICH menu wants markers on healthy systems stx blinkPhase ; 178C blink phase 0 ldx #$FF ; 178F sentinel value stx lastEnergyShown ; 1791 force printEnergyValue to actually draw the reading ldx comcenUnit ; 1794 our comcen's unit index lda unitEnergyTable,x ; 1797 its energy byte and #$3F ; 179A 0..50 sta comcenEnergySnapshot ; 179C remember it; attemptSystemRepair works from this snapshot ; ---------------------------------------------------------------------- ; Draw the four system markers, unless the comcen is stunned (then it has no readable status at all). ; $91D5 doubles as the 3..0 loop counter. ; ---------------------------------------------------------------------- lda comcenStunStatus ; 179F stun flag once more bmi L_17BE ; 17A2 stunned - no markers, jump straight to the ENERGY readout ldy #$03 ; 17A4 start at system 3 (DRONE) sty menuSelectedItem ; 17A6 menuSelectedItem is reused as the loop counter here L_17A9: jsr drawSystemMarker ; 17A9 draw the marker of system Y dec menuSelectedItem ; 17AC next system down ldy menuSelectedItem ; 17AF reload the counter into Y (drawSystemMarker clobbers it) bpl L_17A9 ; 17B2 loop over systems 3,2,1,0 lda #$01 ; 17B4 blink phase 1 = 'marker currently drawn' sta blinkPhase ; 17B6 so the first toggle erases it lda #$04 ; 17B9 menu row 4 of the REPAIR WHICH menu = (EXIT) sta menuSelectedItem ; 17BB leave the selection on (EXIT) ; ---------------------------------------------------------------------- ; ENERGY label and reading, plus the stun banner. ; ---------------------------------------------------------------------- L_17BE: jsr printEnergyLabel ; 17BE print the word ENERGY at column 30, row 8 jsr printEnergyValue ; 17C1 print the percentage at column 36, row 9 and refresh comcenSystemStatus[0] lda comcenStunStatus ; 17C4 stunned? bpl L_17D7 ; 17C7 no - done lda #$05 ; 17C9 banner starts at column 5 of the panel (screen column 7) sta textCursorCol ; 17CB cursor column jsr updateCursorPointers ; 17CD recompute the cell pointers (the row is wherever printEnergyValue left it) lda #$42 ; 17D0 low byte of 'COMCEN STUNNED!' ldy #$87 ; 17D2 $8742 in overlay B (msgComcenStunned) jsr printString ; 17D4 print the banner L_17D7: rts ; 17D7 return ; comcenEnergySnapshot - the comcen's raw energy (0..50) as of the last REPAIR-tab redraw or repair ; attempt. attemptSystemRepair sizes the repair from it and adds the repaired amount back locally, ; because the real unitEnergyTable entry only changes once the $9A command has travelled through the ; lock-step exchange. comcenEnergySnapshot: .byte $00 ; 17D8 . 0..50, refreshed by drawRepairTab and attemptSystemRepair ; repairAmount - energy points granted by the last successful ENERGY repair, computed as (50 - ; comcenEnergySnapshot)/8 + 1 and sent as the parameter of command $9A. repairAmount: .byte $00 ; 17D9 . parameter of the $9A cmdAddEnergy command ; ---------------------------------------------------------------------- ; repairTabAction - Fire-button handler of the REPAIR tab (entry 1 of menuItemHandlerTable at $1535, ; reached through the self-modified JSR at $148D). It does nothing unless the DAMAGE option is on, the ; game is not a film playback and a game is actually running. If the comcen is stunned it runs a ; single stun-recovery attempt: message, sound, 200 frames of real-time battle, then - only while the ; stun counter is below 8 - a 1-in-8 chance of queueing command $9E, which clears the stun on both ; machines. If the comcen is not stunned it halts the comcen if it is still moving or under group ; orders, blanks the four markers and opens the REPAIR WHICH menu. ; In: gameTypeOptions $0BA3 bit 4 (DAMAGE), filmPlaybackMode $0B9B bit 7, gameEndReason $0BA8, ; comcenStunStatus $922A, comcenUnit $9236, unitDisplayFlagsTable $FB54, ; unitColTable/unitRowTable, nextGameRandom ; Out: command $9E or $80 queued into the outgoing ring; sounds $12/$13/$18; the battle advanced by ; runGameFrames; blinkPhase, showOkMarkers, $1412 (3..0 loop counter) and the four markers ; updated; tail-calls snapshotSetupId so the menu's 'anything changed?' test is re-armed ; Called from: mainMenuLoop through menuItemHandlerTable[1] at $148D ; Guards: repairs need the DAMAGE option, a live game and player control. ; ---------------------------------------------------------------------- repairTabAction: lda gameTypeOptions ; 17DA game settings byte and #$10 ; 17DD bit 4 = the DAMAGE option is on beq L_1822 ; 17DF no damage model - the REPAIR tab is decoration only lda filmPlaybackMode ; 17E1 bit 7 = a game film is playing back bmi L_1822 ; 17E4 during playback the player may not issue commands lda gameEndReason ; 17E6 0 while a game is running bne L_1822 ; 17E9 no game in progress - nothing to repair ; ---------------------------------------------------------------------- ; Stunned comcen: one attempt at shaking the stun off. ; ---------------------------------------------------------------------- lda comcenStunStatus ; 17EB bit 7 = comcen stunned bpl L_1823 ; 17EE not stunned - go and open the REPAIR WHICH menu jsr printAttemptingRepair ; 17F0 print 'ATTEMPTING REPAIR...' on the panel's message row lda #$18 ; 17F3 sound $18 = the repair work noise jsr playSound ; 17F5 start it lda #$C8 ; 17F8 200 frames... jsr runGameFrames ; 17FA ...of real-time battle while the crew works jsr clearRepairMessage ; 17FD erase the message row again ldy #$12 ; 1800 assume sound $12 = failure lda comcenStunStatus ; 1802 stun status byte and #$0F ; 1805 bits 0-3 = how badly stunned (counts down elsewhere) cmp #$08 ; 1807 8 or more? bcs L_1819 ; 1809 too badly stunned - the attempt cannot succeed jsr nextGameRandom ; 180B shared RNG so both machines and a replayed film agree and #$07 ; 180E one chance in eight bne L_1819 ; 1810 not this time - keep the failure sound lda #$9E ; 1812 $9E = cmdComcenRepairedOrSetupReady jsr queueCmdBytePending ; 1814 queue the 1-byte command; both machines clear the stun counter ldy #$13 ; 1817 sound $13 = success L_1819: tya ; 1819 move the chosen sound id into A jsr playSound ; 181A play it lda #$14 ; 181D 20 more frames... jsr runGameFrames ; 181F ...so the sound is heard before the menu redraws L_1822: rts ; 1822 return to mainMenuLoop ; ---------------------------------------------------------------------- ; Not stunned: halt the comcen if necessary, blank the markers and open the 'REPAIR WHICH:' menu. A ; comcen that is driving or following a group order has to stop before it can be worked on, which is ; done by ordering it to move to the cell it already occupies - cmdMoveUnit also drops its group ; membership. ; ---------------------------------------------------------------------- L_1823: ldy comcenUnit ; 1823 our comcen's unit index lda unitDisplayFlagsTable,y ; 1826 $FB54: bit 7 = idle/arrived, bit 6 = acting on a group order bpl L_182F ; 1829 bit 7 clear = still moving - it must be halted and #$40 ; 182B bit 6 = following a group order beq L_1840 ; 182D idle and ungrouped - no halt command needed L_182F: sty cmdParam0 ; 182F command argument 0 = the unit to move lda unitColTable,y ; 1831 current map column of the comcen sta cmdParam1 ; 1834 command argument 1 = destination column lda unitRowTable,y ; 1836 current map row sta cmdParam2 ; 1839 command argument 2 = destination row lda #$80 ; 183B $80 = cmdMoveUnit jsr queueCmd4 ; 183D queue the 4-byte command: 'move to where you already are' = halt and leave the group L_1840: ldy #$01 ; 1840 blink phase 1... sty blinkPhase ; 1842 ...so the first toggle in the menu erases the marker dey ; 1845 Y = 0 sty showOkMarkers ; 1846 healthy systems stay invisible while the markers are wiped ldy #$03 ; 1849 start at system 3 sty repairLoopCounter ; 184B $1412 is the 3..0 loop counter here (drawRepairTab used $91D5 instead) L_184E: ldx #$16 ; 184E colour $16 = the schematic background (white/blue) jsr eraseSystemMarker ; 1850 blank system Y's 3x3 marker area dec repairLoopCounter ; 1853 next system down ldy repairLoopCounter ; 1856 reload the counter into Y bpl L_184E ; 1859 loop over systems 3,2,1,0 lda #$01 ; 185B now show markers for healthy systems too... sta showOkMarkers ; 185D ...so the player can pick any of the four in the menu jsr runRepairWhichMenu ; 1860 $652E: draw and run the 'REPAIR WHICH:' menu (ENERGY/BATTLE/RADAR/DRONE/(EXIT)) jsr consumeSpaceKey ; 1863 swallow a pending space key so it is not re-processed jsr setMenuTextWindow ; 1866 put the menu panel's text window and colour back lda #$00 ; 1869 back to 'only damaged systems get a marker' sta showOkMarkers ; 186B clear the flag ldy #$03 ; 186E start at system 3 again sty repairLoopCounter ; 1870 reuse $1412 as the counter L_1873: jsr drawSystemMarker ; 1873 redraw system Y's marker with its new status dec repairLoopCounter ; 1876 next system down ldy repairLoopCounter ; 1879 reload the counter bpl L_1873 ; 187C loop over systems 3,2,1,0 jmp snapshotComcenStatus ; 187E re-arm selectDefaultMenuItem's 'has anything changed?' snapshot and return ; systemMarkerColumnTable - screen column of the marker of each comcen system, indexed by system ; number: 0 ENERGY, 1 BATTLE, 2 RADAR, 3 DRONE. The marker is a plus of five colour cells inside a 3x3 ; block whose top-left corner is this column and systemMarkerRowTable[Y]. systemMarkerColumnTable: .byte $0D,$10,$14,$19 ; 1881 .... ENERGY $0D, BATTLE $10, RADAR $14, DRONE $19 ; systemMarkerRowTable - screen row of each comcen system marker, same index as ; systemMarkerColumnTable. All four sit inside the schematic block drawn at row 10. systemMarkerRowTable: .byte $0E,$0A,$0A,$0C ; 1885 .... ENERGY $0E, BATTLE $0A, RADAR $0A, DRONE $0C ; blinkPhase - the shared 0/1 blink phase used by both the console tab blink (blinkSelectedTab) and ; the comcen system marker blink (toggleSystemMarker). 0 = the item is currently shown in its 'on' ; state. blinkPhase: .byte $00 ; 1889 . 0 or 1 ; showOkMarkers - when non-zero, drawSystemMarker also draws a marker for a system whose status is OK ; (green) instead of erasing its cells. Set only while the REPAIR WHICH menu is open, so all four ; systems can be selected. showOkMarkers: .byte $00 ; 188A . 1 only while the REPAIR WHICH menu is up ; ---------------------------------------------------------------------- ; toggleSystemMarker - One blink step for the marker of the comcen system currently highlighted in the ; REPAIR WHICH menu: flips blinkPhase and then either draws the marker (new phase 0) or erases it with ; the schematic background colour $16 (new phase 1). ; In: menuSelectedItem $91D5 = system 0..3, blinkPhase $1889 ; Out: blinkPhase toggled; falls through to drawSystemMarker or jumps to eraseSystemMarker with X = ; $16 ; Called from: blinkRepairMarker at $19C1, and toggleMenuRowHighlight at $6108 whenever the menu ; selector is running in repair mode (selectedUnit $90F8 == $FF) on a row below 4, so the marker ; blinks in step with the highlighted row ; ---------------------------------------------------------------------- toggleSystemMarker: ldy menuSelectedItem ; 188B the highlighted comcen system, 0 ENERGY .. 3 DRONE ldx #$16 ; 188E $16 = white/blue, the schematic background - used if we are erasing lda blinkPhase ; 1890 current blink phase eor #$01 ; 1893 flip it sta blinkPhase ; 1895 store the new phase beq drawSystemMarker ; 1898 new phase 0 - draw the marker (falls straight into drawSystemMarker) jmp eraseSystemMarker ; 189A new phase 1 - wipe the 3x3 marker block ; ---------------------------------------------------------------------- ; drawSystemMarker - Draws the status marker of comcen system Y on the REPAIR tab schematic. The ; marker is a plus made of five 1x1 colour-cell fills around (systemMarkerRowTable[Y], ; systemMarkerColumnTable[Y]): white/green $15 when the system is OK, white/red $12 when its status ; byte has bit 7 set (damaged) and white/yellow $17 otherwise ($40 = degraded). A healthy system is ; wiped instead of drawn unless showOkMarkers is set. Y has to be stashed in a self-modified operand ; because the energy refresh in the first line destroys it. ; In: Y = system index 0..3, comcenSystemStatus $9241, showOkMarkers $188A ; Out: five (or nine, when erasing) screen-RAM colour cells at $8C00 updated; D_2A2A holds the fill ; colour, D_2A2D/D_2A2E the running row/column; the operand at $18A4 patched; the ENERGY reading ; refreshed as a side effect ; Called from: drawRepairTab's loop at $17A9, repairTabAction's redraw loop at $1873, and by falling ; through from toggleSystemMarker ; Pick the marker colour from the system's status byte. ; ---------------------------------------------------------------------- drawSystemMarker: sty L_18A3+1 ; 189D self-modifying code: save Y into the LDY #imm operand at $18A4 - printEnergyValue clobbers Y jsr printEnergyValue ; 18A0 refresh the percentage; this also recomputes comcenSystemStatus[0] for the ENERGY system L_18A3: ldy #$FF ; 18A3 operand written at $189D = the system index ldx #$15 ; 18A5 assume $15 = white/green (system OK) lda comcenSystemStatus,y ; 18A7 $9241+Y: 0 OK, $40 degraded, $80 damaged beq L_18B5 ; 18AA status 0 - keep green ldx #$12 ; 18AC $12 = white/red (damaged) lda comcenSystemStatus,y ; 18AE look at the status again bmi L_18B5 ; 18B1 bit 7 set = damaged - keep red ldx #$17 ; 18B3 $17 = white/yellow (degraded, i.e. $40) ; ---------------------------------------------------------------------- ; A healthy system normally shows nothing at all; only the REPAIR WHICH menu wants green markers so ; that every system can be picked. ; ---------------------------------------------------------------------- L_18B5: cpx #$15 ; 18B5 is the colour the 'OK' green? bne L_18C3 ; 18B7 no - draw the marker lda showOkMarkers ; 18B9 are healthy systems supposed to show a marker? bne L_18C3 ; 18BC yes (REPAIR WHICH menu open) - draw it ldx #$16 ; 18BE $16 = schematic background jmp eraseSystemMarker ; 18C0 wipe the whole 3x3 block instead and return to the caller ; ---------------------------------------------------------------------- ; Paint the plus: (r,c+1), (r+2,c+1), (r+1,c), (r+1,c+1), (r+1,c+2). D_2A2D/D_2A2E are stepped between ; the five 1x1 fills; fillScreenColourRect reloads D_2A2A from A, so the colour is fetched again ; before each fill. ; ---------------------------------------------------------------------- L_18C3: stx drawColourByte ; 18C3 keep the chosen colour byte in the fill-colour slot lda systemMarkerColumnTable,y; 18C6 screen column of this system's marker sta windowOriginCol ; 18C9 fill origin column lda systemMarkerRowTable,y ; 18CC screen row of this system's marker sta windowOriginRow ; 18CF fill origin row inc windowOriginCol ; 18D2 step right one cell: top arm of the plus lda drawColourByte ; 18D5 marker colour ldx #$01 ; 18D8 1 cell wide ldy #$01 ; 18DA 1 cell tall jsr fillScreenColourRect ; 18DC paint (row, col+1) inc windowOriginRow ; 18DF down one row... inc windowOriginRow ; 18E2 ...and one more: bottom arm lda drawColourByte ; 18E5 marker colour ldx #$01 ; 18E8 1 cell wide ldy #$01 ; 18EA 1 cell tall jsr fillScreenColourRect ; 18EC paint (row+2, col+1) dec windowOriginRow ; 18EF back up to the middle row dec windowOriginCol ; 18F2 back left to the original column lda drawColourByte ; 18F5 marker colour ldx #$01 ; 18F8 1 cell wide ldy #$01 ; 18FA 1 cell tall jsr fillScreenColourRect ; 18FC paint (row+1, col) - left arm inc windowOriginCol ; 18FF step right lda drawColourByte ; 1902 marker colour ldx #$01 ; 1905 1 cell wide ldy #$01 ; 1907 1 cell tall jsr fillScreenColourRect ; 1909 paint (row+1, col+1) - centre inc windowOriginCol ; 190C step right again lda drawColourByte ; 190F marker colour ldx #$01 ; 1912 1 cell wide ldy #$01 ; 1914 1 cell tall jmp fillScreenColourRect ; 1916 paint (row+1, col+2) - right arm, and return to the caller ; ---------------------------------------------------------------------- ; eraseSystemMarker - Fills the whole 3x3 colour-cell block of system Y's marker with the colour byte ; in X. Every caller passes $16, the white/blue background the schematic is drawn on, so this blanks ; the marker. ; In: Y = system index 0..3, X = colour byte (always $16) ; Out: D_2A2D/D_2A2E set to the marker origin, nine screen-RAM colour bytes overwritten ; Called from: repairTabAction's wipe loop at $1850, toggleSystemMarker at $189A (blink off), ; drawSystemMarker at $18C0 (healthy system with showOkMarkers clear) ; ---------------------------------------------------------------------- eraseSystemMarker: lda systemMarkerColumnTable,y; 1919 screen column of this system's marker sta windowOriginCol ; 191C fill origin column lda systemMarkerRowTable,y ; 191F screen row of this system's marker sta windowOriginRow ; 1922 fill origin row txa ; 1925 the colour byte arrives in X, fillScreenColourRect wants it in A ldx #$03 ; 1926 3 cells wide ldy #$03 ; 1928 3 cells tall - covers the whole plus jmp fillScreenColourRect ; 192A paint the block and return to the caller ; ---------------------------------------------------------------------- ; sendEnergyRepairCommand - Queues the 3-byte command $9A (cmdAddEnergy) with the comcen's unit index ; and the repaired amount, so that both machines add the same energy to that unit. This is the only ; part of a comcen repair that travels over the link - the status of systems 1-3 is purely local ; cosmetics. ; In: comcenUnit $9236, repairAmount $17D9 ; Out: zp_74 = unit, zp_75 = amount, the command written into the outgoing ring and pendingOwnCmdCount ; $929F bumped ; Called from: attemptSystemRepair at $196C ; ---------------------------------------------------------------------- sendEnergyRepairCommand: lda comcenUnit ; 192D our comcen (unit 49 or 99) sta cmdParam1 ; 1930 command argument 1 = unit index lda repairAmount ; 1932 energy points won by this repair sta cmdParam2 ; 1935 command argument 2 = amount lda #$9A ; 1937 $9A = cmdAddEnergy jmp queueCmd3 ; 1939 queue the 3-byte command and return to the caller ; ---------------------------------------------------------------------- ; attemptSystemRepair - Resolves one repair attempt on the comcen system highlighted in the REPAIR ; WHICH menu. It shows the message, plays the work sound and rolls the shared RNG: the attempt ; succeeds with probability 1/2 for the ENERGY system and 1/4 for the other three, and only if the ; system is not already OK. For ENERGY it computes (50 - snapshot)/8 + 1 points, sends them with ; command $9A and updates the snapshot (capped at 50); for the others it just clears the status byte. ; Success plays sound $13, failure $12, each surrounded by about a second of marker blinking during ; which the real-time battle keeps running. ; In: menuSelectedItem $91D5 = system 0..3, comcenSystemStatus $9241, comcenEnergySnapshot $17D8, ; comcenUnit $9236, unitEnergyTable $F9C4, nextGameRandom ; Out: repairAmount, comcenEnergySnapshot, comcenSystemStatus[Y], one $9A command, sounds $12/$13/$18, ; zp_6E = 0, blinkPhase = 0 and the message row cleared ; Called from: the REPAIR WHICH menu loop at $6590 (skipped in solo/film mode) ; Announce the attempt and roll for success. ; ---------------------------------------------------------------------- attemptSystemRepair: jsr printAttemptingRepair ; 193C print 'ATTEMPTING REPAIR...' lda #$18 ; 193F sound $18 = repair work noise jsr playSound ; 1941 start it ldy menuSelectedItem ; 1944 the system the player picked: 0 ENERGY, 1 BATTLE, 2 RADAR, 3 DRONE jsr nextGameRandom ; 1947 shared RNG - both machines and a replayed film draw the same number cpy #$00 ; 194A the ENERGY system? bne L_1950 ; 194C systems 1-3 keep both random bits and #$01 ; 194E ENERGY: one bit only, so a 1-in-2 chance L_1950: and #$03 ; 1950 1-in-4 for systems 1-3 (already narrowed to 1-in-2 above) bne L_1990 ; 1952 non-zero - the attempt failed lda comcenSystemStatus,y ; 1954 status of the chosen system beq L_1990 ; 1957 already OK - treat it as a failure, there is nothing to repair ; ---------------------------------------------------------------------- ; ENERGY: work out how much energy the crew restores and tell the opponent. ; ---------------------------------------------------------------------- cpy #$00 ; 1959 the ENERGY system? bne L_197F ; 195B systems 1-3 just get their status cleared lda #$32 ; 195D 50 = a full comcen energy bar sec ; 195F prepare the subtraction sbc comcenEnergySnapshot ; 1960 how much energy is missing lsr a ; 1963 divide by 8... lsr a ; 1964 ... lsr a ; 1965 ...so a badly hurt comcen recovers faster clc ; 1966 prepare the add adc #$01 ; 1967 always at least one point sta repairAmount ; 1969 remember what we granted jsr sendEnergyRepairCommand ; 196C queue command $9A so both machines credit the energy clc ; 196F prepare the add lda comcenEnergySnapshot ; 1970 energy before the repair adc repairAmount ; 1973 plus the repaired amount cmp #$33 ; 1976 51 or more? bcc L_197C ; 1978 still below the maximum lda #$32 ; 197A clamp at 50 L_197C: sta comcenEnergySnapshot ; 197C update the local snapshot (the real table only changes when $9A executes) ; ---------------------------------------------------------------------- ; Success: blink, clear the status, chime. Failure joins at $1990. ; ---------------------------------------------------------------------- L_197F: jsr blinkRepairMarker ; 197F about a second of marker blinking while the battle runs on ldy menuSelectedItem ; 1982 the chosen system again beq L_198C ; 1985 ENERGY - its status is recomputed by printEnergyValue, not stored here lda #$00 ; 1987 0 = system OK sta comcenSystemStatus,y ; 1989 mark BATTLE/RADAR/DRONE repaired L_198C: lda #$13 ; 198C sound $13 = repair succeeded bne L_1995 ; 198E always taken L_1990: jsr blinkRepairMarker ; 1990 failure: the same blink pause so both outcomes take the same time lda #$12 ; 1993 sound $12 = repair failed L_1995: jsr playSound ; 1995 play the outcome sound jsr blinkRepairMarker ; 1998 another second of blinking while it sounds ; ---------------------------------------------------------------------- ; Re-read the real energy (the $9A command may have been applied in the meantime) and clean up the ; blink state and the message row. ; ---------------------------------------------------------------------- ldy comcenUnit ; 199B our comcen lda unitEnergyTable,y ; 199E its energy byte and #$3F ; 19A1 0..50 sta comcenEnergySnapshot ; 19A3 refresh the snapshot from the live table lda #$00 ; 19A6 zero... sta menuBlinkTimer ; 19A8 ...so the menu-row blink fires again on the next pass sta blinkPhase ; 19AA ...and the marker is left in the 'drawn' phase jmp clearRepairMessage ; 19AD erase 'ATTEMPTING REPAIR...' and return to the menu loop ; ---------------------------------------------------------------------- ; blinkRepairMarker - Blinks the marker of the selected comcen system for about one second while a ; repair attempt resolves. It arms the IRQ frame countdown zp_6F with 60 and toggles the marker every ; 20 game frames; runGameFrames is used rather than a plain delay so that the real-time battle (and ; the lock-step command exchange) keeps running. It bails out at once if the game has ended. ; In: menuSelectedItem $91D5, gameEndCountdown $92C9 ; Out: zp_6F counted down by the raster IRQ, blinkPhase toggled several times, the marker left in ; whichever phase it ended on, the battle advanced ; Called from: attemptSystemRepair at $197F, $1990 and $1998 ; ---------------------------------------------------------------------- blinkRepairMarker: lda #$3C ; 19B0 60 frames, about one second sta markerBlinkTimer ; 19B2 the raster IRQ counts zp_6F down L_19B4: lda #$14 ; 19B4 20 frames... jsr runGameFrames ; 19B6 ...of real-time battle between toggles lda gameEndCountdown ; 19B9 non-zero once the game is over bne L_19C8 ; 19BC the game ended while we blinked - stop immediately ldy menuSelectedItem ; 19BE the highlighted comcen system jsr toggleSystemMarker ; 19C1 flip the marker on or off lda markerBlinkTimer ; 19C4 has the one-second countdown expired? bne L_19B4 ; 19C6 no - keep blinking L_19C8: rts ; 19C8 return ; ---------------------------------------------------------------------- ; printAttemptingRepair - Positions the REPAIR panel's own message row (window column 10, screen ; column 12, screen row 20) and prints 'ATTEMPTING REPAIR...' there, then restores the text window the ; caller had. ; In: none ; Out: 20 characters drawn on the panel's message row; the text window restored from the single save ; slot $92B6 ; Called from: repairTabAction at $17F0 and attemptSystemRepair at $193C ; ---------------------------------------------------------------------- printAttemptingRepair: jsr setRepairMessageWindow ; 19C9 save the caller's window and move to column 10, row 20 lda #$E5 ; 19CC low byte of the message ldy #$CB ; 19CE $CBE5 = 'ATTEMPTING REPAIR...' jsr printString ; 19D0 print it jmp restoreTextWindow ; 19D3 put the caller's text window back and return ; ---------------------------------------------------------------------- ; clearRepairMessage - Erases the REPAIR panel's message row by printing 20 copies of the blank glyph ; zp_3E over it, then restores the caller's text window. (The routine it calls is the $C000 overlay's ; printFillChars at $C0EF; the listing's bootReceiveSectorData label is a stale leftover from the boot ; loader that used to live at $C000.) ; In: zp_3E fill glyph ; Out: 20 cells of the panel's message row blanked; the text window restored from $92B6 ; Called from: repairTabAction at $17FD and attemptSystemRepair at $19AD ; ---------------------------------------------------------------------- clearRepairMessage: jsr setRepairMessageWindow ; 19D6 same window and cursor position as printAttemptingRepair lda #$14 ; 19D9 20 characters - the length of 'ATTEMPTING REPAIR...' jsr bootReceiveSectorData ; 19DB printFillChars ($C0EF): print the blank glyph that many times (also textEngineC000:printFillChars) jmp restoreTextWindow ; 19DE put the caller's text window back and return ; ---------------------------------------------------------------------- ; setRepairMessageWindow - Shared prologue of printAttemptingRepair and clearRepairMessage: saves the ; caller's 12-byte text state, picks the menu text colour, applies the menu panel geometry (left ; column 2, width $24, rows 7..$16) and places the cursor at window column 10 = screen column 12 of ; screen row 20. ; In: comcenStunStatus and menuTabIndex through setMenuTextColour ; Out: $92B6-$92C1 = saved window, zp_39-zp_40 = menu window, zp_3D = $61 or $21, cursor pointers ; recomputed ; Called from: printAttemptingRepair $19C9, clearRepairMessage $19D6 ; ---------------------------------------------------------------------- setRepairMessageWindow: jsr saveTextWindow ; 19E1 copy zp_39-zp_44 to $92B6 (there is only one save slot, so this cannot nest) jsr setMenuTextColour ; 19E4 $61, or $21 while the stunned-comcen panel is red jsr setMenuWindowBounds ; 19E7 menu panel window: left column 2, width $24, rows 7..$16 lda #$0A ; 19EA column 10 of the window (screen column 12) sta textCursorCol ; 19EC cursor column lda #$14 ; 19EE screen row 20 - the panel's message row jmp setCursorRow ; 19F0 set the row, recompute the pointers and return to the caller ; ---------------------------------------------------------------------- ; printOptionsList - Prints the body of the MISC tab. While a game is running it lists the in-game ; options ABORT GAME, VOICE PAUSE (only for a modem game that is not a film playback) and ' (EXIT)'. ; Otherwise it prints the five main-menu lines COMPETE WITH MODEM OPPONENT / PRACTICE WITH SOLO ; TRAINER / WATCH GAME FILM / SAVE GAME FILM / LOAD GAME FILM, plus DISCONNECT/SET VOICE when the comm ; module reports the link is up. ; In: gameEndReason $0BA8, gameEndCountdown $92C9, filmPlaybackMode $0B9B, isSoloTrainer $0BA5, ; link-active flag $E03B ; Out: the item text drawn from row 9 down; zp_39 = 9, zp_3B = 6, zp_3C = $1D, zp_3D = $61, zp_3F = 0, ; zp_40 left after the last line ; Called from: mainMenuLoop at $14EA, miscTabAction at $1A53, $1A83 and $1AD0 ; Set up the option-list window and print the ' OPTIONS' heading. ; ---------------------------------------------------------------------- printOptionsList: lda #$06 ; 19F3 items start at screen column 6 sta textWindowLeft ; 19F5 window left edge lda #$09 ; 19F7 first row of the list is screen row 9 sta textWindowTop ; 19F9 window top sta textCursorRow ; 19FB and the cursor row lda #$1D ; 19FD 29 columns wide - also the width of the menu highlight bar sta textWindowWidth ; 19FF window width lda #$00 ; 1A01 column 0 of the window sta textCursorCol ; 1A03 cursor column jsr updateCursorPointers ; 1A05 recompute the cell pointers lda #$61 ; 1A08 white text, EOR mask 6 for the highlight bar sta textColour ; 1A0A set the colour lda #$13 ; 1A0C low byte of the heading ldy #$CB ; 1A0E $CB13 = ' OPTIONS' followed by a CR jsr printString ; 1A10 print it ; ---------------------------------------------------------------------- ; Decide which list to print. ; ---------------------------------------------------------------------- lda gameEndReason ; 1A13 0 while a game is running ora gameEndCountdown ; 1A16 non-zero while the end-of-game sequence runs (endGame sets it to $78 and the IRQ counts it up to $80) bne L_1A36 ; 1A19 no live game - print the main menu instead ; ---------------------------------------------------------------------- ; In-game options. ; ---------------------------------------------------------------------- lda #$A6 ; 1A1B low byte of 'ABORT GAME' ldy #$CC ; 1A1D $CCA6 = 'ABORT GAME' + CR jsr printString ; 1A1F print it; the pointer is left at $CCB1 = 'VOICE PAUSE' lda filmPlaybackMode ; 1A22 bit 7 = film playback bmi L_1A2F ; 1A25 you cannot ring your opponent during a replay lda isSoloTrainer ; 1A27 bit 7 = solo trainer game bmi L_1A2F ; 1A2A the trainer has no phone - skip VOICE PAUSE jsr printStringAtPointer ; 1A2C print the next string in the block: 'VOICE PAUSE' + CR L_1A2F: lda #$B2 ; 1A2F low byte of ' (EXIT)' ldy #$CA ; 1A31 $CAB2 = ' (EXIT)', the tail of the formation-menu string block jmp printString ; 1A33 print the last item and return ; ---------------------------------------------------------------------- ; Main menu (no game running). ; ---------------------------------------------------------------------- L_1A36: lda #$1F ; 1A36 low byte of mainMenuItemText ldy #$CC ; 1A38 $CC1F = four CR-separated lines: COMPETE WITH MODEM OPPONENT / PRACTICE WITH SOLO TRAINER / WATCH GAME FILM / SAVE GAME FILM jsr printString ; 1A3A print all four; the pointer is left at $CC75 jsr printStringAtPointer ; 1A3D print the next string: 'LOAD GAME FILM ' lda isLinkActive ; 1A40 comm module's link-active flag (also trainerAiE000:isLinkActive) beq L_1A48 ; 1A43 no link - the menu has five items jsr printStringAtPointer ; 1A45 link up: print 'DISCONNECT/SET VOICE ' as item 5 L_1A48: rts ; 1A48 return ; ---------------------------------------------------------------------- ; miscTabAction - Fire-button handler of the MISC tab (entry 2 of menuItemHandlerTable at $1535) and, ; through the JMP at $0AC5, the top of the whole main-menu loop at cold start. With a game running it ; runs the in-game options menu; picking ABORT GAME tells a connected opponent with command $8C reason ; 0, waits up to $26 ticks for the exchange to drain, hangs up and restores the status message. It ; then falls into an endless loop that clears the panel, undoes any film-playback settings, reprints ; the option list and runs the main menu. ; In: gameEndCountdown $92C9, gameEndReason $0BA8, filmPlaybackMode $0B9B, isSoloTrainer $0BA5, link ; flags $E03B/$E030, linkSessionState $0B9C, persistentMessageId $92A1 ; Out: menu state $91D4/$91D5/$91D6, command $8C queued, linkPollDelay $9163 = 0, zp_7F timeout, the ; operand at $1AC0 patched with the saved message id; may end the session through ; endGameToMainMenu. Never returns while no game is running - runMainMenu leaves through ; handleMainMenuChoice instead ; Called from: mainMenuLoop through menuItemHandlerTable[2] at $148D, and JMP $0AC5 (cold start into ; the main menu) ; Run the in-game options menu, if there is a game to have options about. ; ---------------------------------------------------------------------- miscTabAction: lda gameEndCountdown ; 1A49 non-zero during the countdown after a game has just ended bne L_1AC7 ; 1A4C the game is over - go straight to the session teardown lda gameEndReason ; 1A4E 0 while a game is running bne L_1ACA ; 1A51 no game at all - go straight to the main-menu loop jsr printOptionsList ; 1A53 print ABORT GAME / [VOICE PAUSE] / (EXIT) ldy #$02 ; 1A56 assume two items lda filmPlaybackMode ; 1A58 bit 7 = film playback bmi L_1A63 ; 1A5B no VOICE PAUSE during a replay lda isSoloTrainer ; 1A5D bit 7 = solo trainer bmi L_1A63 ; 1A60 no VOICE PAUSE against the trainer iny ; 1A62 modem game: three items L_1A63: sty menuItemCount ; 1A63 tell the menu selector how many rows there are dey ; 1A66 index of the last item sty menuSelectedItem ; 1A67 preselect (EXIT) dec textWindowLeft ; 1A6A shift the window left edge to 5 so the highlight bar starts one column left of the text lda textWindowLeft ; 1A6C the new left edge... sta textCursorCol ; 1A6E ...also used as the cursor column; toggleMenuRowHighlight forces column 0 anyway, so this looks vestigial lda #$0B ; 1A70 the first menu row is screen row 11 sta menuFirstRow ; 1A72 the selector adds the selection index to this jsr setCursorRow ; 1A75 put the cursor on that row jsr runInGameOptionsMenu ; 1A78 run the selector; it handles VOICE PAUSE itself and returns Y = chosen item ldy menuSelectedItem ; 1A7B which item was picked? beq L_1A86 ; 1A7E item 0 = ABORT GAME jsr setMenuTextWindow ; 1A80 (EXIT): restore the menu window and colour... jmp printOptionsList ; 1A83 ...reprint the list and return to mainMenuLoop ; ---------------------------------------------------------------------- ; ABORT GAME (also the cold-start entry from $0AC5). Tell the opponent, drain the link and hang up, ; then fall through into the session teardown. ; ---------------------------------------------------------------------- L_1A86: lda persistentMessageId ; 1A86 the message currently pinned on the status line sta L_1ABF+1 ; 1A89 self-modifying code: it becomes the LDA #imm operand at $1AC0, so it can be restored at $1ABF lda isLinkActive ; 1A8C is the comm link up? (also trainerAiE000:isLinkActive) beq L_1ABF ; 1A8F no link - nothing to tell anybody lda linkSessionState ; 1A91 0 none, 2 game session agreed, $80 link up at menu level, $FF link lost bmi L_1ABF ; 1A94 $80/$FF: no game session to abort beq L_1ABC ; 1A96 0: no session at all - just hang up lda #$00 ; 1A98 reason 0 = 'I am leaving the game' sta cmdParam0 ; 1A9A command argument 0 sta linkPollDelay ; 1A9C clear the exchange throttle so the packet goes out at full speed lda commBuildId ; 1A9F $E030 = comm build id: $00 modem driver, $FF solo trainer (also trainerAiE000:commBuildId) bmi L_1ABC ; 1AA2 the trainer discards everything we send - skip the handshake jsr showWorkingMessage ; 1AA4 put 'WORKING...' on the status line lda #$8C ; 1AA7 $8C = cmdSessionControl jsr queueCmd2 ; 1AA9 queue the 2-byte command (code + reason) lda #$26 ; 1AAC $26 ticks... sta linkWaitTimer ; 1AAE ...as the timeout (zp_7F is decremented every 8th frame by the IRQ) L_1AB0: jsr serviceCommandExchange ; 1AB0 keep the lock-step exchange running so the packet is actually sent lda exchangeFlags ; 1AB3 $E01D = the comm module's exchange flag byte (also trainerAiE000:commInterfaceBlock) beq L_1ABF ; 1AB6 all flags clear - nothing is in flight any more lda linkWaitTimer ; 1AB8 has the timeout expired? bne L_1AB0 ; 1ABA no - keep servicing the link L_1ABC: jsr hangUpModem ; 1ABC drop the line (prompting the player to pick up the handset first) L_1ABF: lda #$FF ; 1ABF operand patched at $1A89 = the message id that was on the status line sta persistentMessageId ; 1AC1 pin it again jsr showStatusMessage ; 1AC4 and redisplay it ; ---------------------------------------------------------------------- ; Session teardown and the main-menu loop. runMainMenu normally never comes back: it jumps into ; handleMainMenuChoice in the $6F00 overlay. ; ---------------------------------------------------------------------- L_1AC7: jsr endGameToMainMenu ; 1AC7 reset all the per-game state (also entered directly when the game just ended) L_1ACA: jsr clearMenuTextWindow ; 1ACA clear the menu panel jsr restoreSettingsAfterFilm; 1ACD if we were watching a film, put the real game settings back jsr printOptionsList ; 1AD0 print the five- or six-item main menu jsr runMainMenu ; 1AD3 run it; only DISCONNECT/SET VOICE returns here jsr clearMenuTextWindow ; 1AD6 clear the panel again jmp miscTabAction ; 1AD9 and start over ; ---------------------------------------------------------------------- ; endGameToMainMenu - Tears a finished game session down and prepares the main menu. It clears the ; end-of-game countdown and the pause state, empties the outgoing command ring, silences the sound ; unless a win/lose jingle is still playing, marks 'no game in progress', writes the game film out ; when the battle really ended, chooses a sensible default main-menu item, and leaves the link either ; flushed and up ($0B9C = $80) or down (0). ; In: sndVoiceSoundId $67C8, comm build id $E030, link-active flag $E03B, gamePhase zp_B1, ; filmPlaybackMode $0B9B, linkSessionState $0B9C, gameEndReason $0BA8 ; Out: $92C9 = 0, zp_BE = 0, $908D/$908E = 0, defaultMainMenuItem $92FD, gameEndReason $0BA8 >= 1, ; film header written, $0B9C = 0 or $80 ; Called from: miscTabAction at $1AC7 and by JMP from the $C000 overlay at $C879 when $92C9 goes ; negative ; Clear the per-game state. ; ---------------------------------------------------------------------- endGameToMainMenu: lda #$00 ; 1ADC zero for the five stores below sta gameEndCountdown ; 1ADE cancel the end-of-game countdown sta pauseState ; 1AE1 clear own/opponent pause and voice-pause bits sta outgoingCmdWriteIndex ; 1AE3 empty the outgoing command ring (write index) sta outgoingCmdReadIndex ; 1AE6 and its read index sta defaultMainMenuItem ; 1AE9 default main-menu item 0 = COMPETE WITH MODEM OPPONENT lda sndVoiceSoundId ; 1AEC sound id currently playing on the 'voice' channel cmp #$0D ; 1AEF $0D = the 'we won' fanfare beq L_1AFA ; 1AF1 let it finish cmp #$0E ; 1AF3 $0E = the 'we lost' theme beq L_1AFA ; 1AF5 let that finish too jsr stopAllSound ; 1AF7 otherwise silence all three SID voices ; ---------------------------------------------------------------------- ; Pick the default main-menu item and finalise the film. ; ---------------------------------------------------------------------- L_1AFA: ldy commBuildId ; 1AFA $E030: $FF = the solo trainer module is behind the $E000 jump table (also trainerAiE000:commBuildId) bpl L_1B07 ; 1AFD modem build - the link flag means something lda #$00 ; 1AFF a trainer game never has a real link... sta isLinkActive ; 1B01 ...so force the link-active flag down (also trainerAiE000:isLinkActive) inc defaultMainMenuItem ; 1B04 default item 1 = PRACTICE WITH SOLO TRAINER L_1B07: lda gameEndReason ; 1B07 0 while a game was running bne L_1B0F ; 1B0A already non-zero - leave the real end reason alone inc gameEndReason ; 1B0C 0 -> 1 = 'no game in progress' L_1B0F: lda gamePhase ; 1B0F zp_B1 is 0 only during the actual battle bne L_1B20 ; 1B11 we were in setup, film or menu phase - no film to close lda filmPlaybackMode ; 1B13 bit 7 = we were watching a film, not recording one bmi L_1B20 ; 1B16 nothing to write in that case jsr finalizeFilm ; 1B18 write the end marker, length, clock, scores and checksum into the film header lda #$02 ; 1B1B default item 2 = WATCH GAME FILM sta defaultMainMenuItem ; 1B1D so the player can review what just happened ; ---------------------------------------------------------------------- ; Leave the link in a defined state. ; ---------------------------------------------------------------------- L_1B20: lda isLinkActive ; 1B20 is the comm link up? (also trainerAiE000:isLinkActive) beq L_1B2F ; 1B23 no - session state 0 lda linkSessionState ; 1B25 current session state bmi L_1B2D ; 1B28 $80/$FF - already at menu level, nothing in flight jsr completeExchange ; 1B2A flush the packet exchange that is still in progress L_1B2D: lda #$80 ; 1B2D $80 = link still up, but at menu level L_1B2F: sta linkSessionState ; 1B2F store the new session state rts ; 1B32 return ; ---------------------------------------------------------------------- ; hangUpModem - Convenience entry: hang up and leave linkSessionState at 0 (no session). ; In: none ; Out: see hangUpModemSetState ; Called from: miscTabAction $1ABC, disconnectFromOpponent $1C28, and by JMP from $56DB, $5722, $5771 ; ---------------------------------------------------------------------- hangUpModem: lda #$00 ; 1B33 the new linkSessionState is 0 = no session ; ---------------------------------------------------------------------- ; hangUpModemSetState - Drops the modem link and stores A as the new linkSessionState. On the modem ; build it first tells the player 'PICK UP PHONE THEN PRESS SPACE.' (message $17) and waits for the ; space bar, so the voice call survives the modem going on-hook; then it waits up to $26 ticks for bit ; 7 of the debounced carrier state $E03C to clear before calling the comm module's link-control entry ; with X = 3 (hang up, then shut the UART down). ; In: A = new value for linkSessionState $0B9C; link-active flag $E03B, comm build id $E030, carrier ; state $E03C ; Out: $0B9C = A, the comm module's link variables $E039-$E047 cleared, zp_7F used as the timeout; the ; operand at $1B5A is patched with A ; Called from: hangUpModem (fall-through) and hangUpAndPause at $1C0F. NOTE: the survey text had ; messages $17 and $18 swapped between this routine and openCommLink; the pointer tables at ; $0500/$0524 give id $17 = $8897 'PICK UP PHONE THEN PRESS SPACE.' and id $18 = $88B6 'PRESS ; SPACE, WAIT, HANGUP PHONE.' ; ---------------------------------------------------------------------- hangUpModemSetState: sta L_1B59+1 ; 1B35 self-modifying code: A becomes the LDA #imm operand at $1B5A, restored at $1B59 lda isLinkActive ; 1B38 is the link up at all? (also trainerAiE000:isLinkActive) beq L_1B5B ; 1B3B no - just record the new session state lda commBuildId ; 1B3D $E030: $FF = solo trainer module, $00 = real modem driver (also trainerAiE000:commBuildId) bmi L_1B54 ; 1B40 the trainer has no phone line - skip the prompt lda #$17 ; 1B42 message $17 = 'PICK UP PHONE THEN PRESS SPACE.' jsr showMessageWaitForSpace ; 1B44 show it and block until the player presses space lda #$26 ; 1B47 $26 ticks... sta linkWaitTimer ; 1B49 ...as a timeout (zp_7F is decremented every 8th frame by the IRQ) L_1B4B: lda linkStatus ; 1B4B debounced carrier state; bit 7 mirrors the 'sample has settled' bit the driver puts in $E03D (also trainerAiE000:linkStatus) bpl L_1B54 ; 1B4E bit 7 clear = the line state is no longer settled, i.e. the modem has reacted - stop waiting (probably) lda linkWaitTimer ; 1B50 timeout expired? bne L_1B4B ; 1B52 no - keep watching the carrier L_1B54: ldx #$03 ; 1B54 X = 3: hang up, wait for the UART to idle, then shut it down and clear the link variables jsr commLinkControl ; 1B56 call the $E003 entry of whichever comm module is loaded (also trainerAiE000:commSetLinkStateEntry) L_1B59: lda #$FF ; 1B59 operand patched at $1B35 = the requested session state L_1B5B: sta linkSessionState ; 1B5B store it rts ; 1B5E return ; ---------------------------------------------------------------------- ; openCommLink - Brings the link up if it is not already. On the modem build it first shows message ; $18 'PRESS SPACE, WAIT, HANGUP PHONE.' and waits for the space bar - the player presses space so the ; modem goes off-hook, waits, and then puts the handset down - and then calls the comm module's ; link-control entry with X = 0, which sets the link flags $E03B/$E03C/$E03D. ; In: link-active flag $E03B, isSoloTrainer $0BA5 ; Out: the comm module's link flags set ; Called from: openLinkAndWaitForConnection $1B74 and the $6F00 overlay A at $7962. NOTE: the survey ; text had this message swapped with the one used by hangUpModemSetState ; ---------------------------------------------------------------------- openCommLink: lda isLinkActive ; 1B5F is the link already up? (also trainerAiE000:isLinkActive) bne L_1B73 ; 1B62 yes - nothing to do lda isSoloTrainer ; 1B64 bit 7 = solo trainer game bmi L_1B6E ; 1B67 the trainer needs no prompt lda #$18 ; 1B69 message $18 = 'PRESS SPACE, WAIT, HANGUP PHONE.' jsr showMessageWaitForSpace ; 1B6B show it and block until the player presses space L_1B6E: ldx #$00 ; 1B6E X = 0: open the link jsr commLinkControl ; 1B70 call the $E003 entry of whichever comm module is loaded (also trainerAiE000:commSetLinkStateEntry) L_1B73: rts ; 1B73 return ; ---------------------------------------------------------------------- ; openLinkAndWaitForConnection - openCommLink followed by waitForConnection (fall-through). ; In: none ; Out: see the two callees ; Called from: voicePauseReconnect at $1BFA ; ---------------------------------------------------------------------- openLinkAndWaitForConnection: jsr openCommLink ; 1B74 bring the line up (with the player prompt on a modem build) ; ---------------------------------------------------------------------- ; waitForConnection - On the modem build, pins the status message 'WAITING FOR CONNECTION...', spins ; in waitForCarrier until the modem reports carrier, then clears the voice-pause bit of pauseState and ; blanks the message line. On the solo trainer build it returns immediately. ; In: isSoloTrainer $0BA5 ; Out: persistentMessageId $92A1, zp_18, zp_BE bit 7 cleared, message line cleared ; Called from: openLinkAndWaitForConnection (fall-through) and the $6F00 overlay A at $7965 ; ---------------------------------------------------------------------- waitForConnection: lda isSoloTrainer ; 1B77 bit 7 = solo trainer game bmi L_1B99 ; 1B7A no line to wait for - return at once lda #$10 ; 1B7C message $10 = 'WAITING FOR CONNECTION...' sta persistentMessageId ; 1B7E pin it so it is not replaced by queued messages jsr showStatusMessage ; 1B81 show it immediately lda #$FF ; 1B84 preset the (never tested) wait counter sta scratch18 ; 1B86 zp_18 - see the note in waitForCarrier jsr waitForCarrier ; 1B88 spin until the modem reports carrier lda pauseState ; 1B8B pause/disconnect flags and #$7F ; 1B8D clear bit 7 = the voice pause is over sta pauseState ; 1B8F store them back lda #$00 ; 1B91 no pinned message any more sta persistentMessageId ; 1B93 release the status line jsr clearMessageLine ; 1B96 blank the status line and return L_1B99: rts ; 1B99 return ; ---------------------------------------------------------------------- ; waitForCarrier - Polling loop that runs until the comm module reports carrier (bit 6 of the ; debounced carrier state $E03C). Each pass it services the status/message system, runs the link ; watchdog when the game is in the $FF phase, waits two frames and steps a countdown in zp_18 while ; the raw carrier sample shows no carrier. Once carrier appears it silences the sound and returns. ; In: gamePhase zp_B1, carrier state $E03C, raw carrier sample $E03D, zp_18 ; Out: sound silenced; zp_18 counted down but never tested by anybody, so the countdown is vestigial ; (and zp_18 is a shared scratch that the printing inside updateStatusMessage may clobber anyway) ; Called from: waitForConnection at $1B88 and by its own loop ; ---------------------------------------------------------------------- waitForCarrier: jsr updateStatusMessage ; 1B9A keep the status line and chat display alive while we wait lda gamePhase ; 1B9D zp_B1 = $FF marks the pre-game / side-selection phase cmp #$FF ; 1B9F the only phase in which a stalled link has to be noticed here bne L_1BA6 ; 1BA1 any other phase - no watchdog jsr checkLinkTimeout ; 1BA3 'PHONE TROUBLE.' / 'TRY AGAIN.' watchdog on the packet exchange L_1BA6: lda #$02 ; 1BA6 two frames... jsr waitFrames ; 1BA8 ...of raster-timed delay per poll bit linkStatusSample ; 1BAB raw carrier sample from the last IRQ (also trainerAiE000:linkStatusSample) bvs L_1BB6 ; 1BAE bit 6 set = the sample sees carrier, so do not count down dec scratch18 ; 1BB0 step the (unused) no-carrier countdown bne L_1BB6 ; 1BB2 not at zero yet inc scratch18 ; 1BB4 hold it at 1 instead of wrapping round to $FF L_1BB6: bit linkStatus ; 1BB6 debounced carrier state (also trainerAiE000:linkStatus) bvc waitForCarrier ; 1BB9 bit 6 clear = still no carrier - keep waiting lda #$00 ; 1BBB sound 0 = silence all voices jmp playSound ; 1BBD silence the dial tone/noise and return to waitForConnection ; orphanRts1BC0 - a stray $60 (RTS) between waitForCarrier and showMessageWaitForSpace. Nothing ; branches or jumps to it and waitForCarrier always leaves through the JMP at $1BBD, so it is ; unreachable - probably the tail of an earlier version of the wait loop. orphanRts1BC0: .byte $60 ; 1BC0 ` unreachable RTS ; ---------------------------------------------------------------------- ; showMessageWaitForSpace - Pins status message A on the message line and blocks until the player ; presses the space bar. It discards any buffered incoming chat text so the prompt is not overwritten, ; leaves chat typing mode, and while waiting keeps the message system running and the shared RNG ; stirring. On exit it empties the message queue, locks the keyboard out for 30 frames and clears the ; key latch so the space press is not seen twice. ; In: A = message id (used with $17 'PICK UP PHONE THEN PRESS SPACE.' and $18 'PRESS SPACE, WAIT, ; HANGUP PHONE.') ; Out: persistentMessageId $92A1, incomingChatLength $9277 = 0, chatState $929C = 0, inputLockoutTimer ; $0B7D = $1E, lastTypedKey $9248 = $FF, message queue emptied ; Called from: hangUpModemSetState $1B44 and openCommLink $1B6B ; ---------------------------------------------------------------------- showMessageWaitForSpace: sta persistentMessageId ; 1BC1 pin the message so nothing queued replaces it ldy #$00 ; 1BC4 zero sty incomingChatLength ; 1BC6 throw away buffered incoming chat text so it cannot overwrite the prompt jsr showStatusMessage ; 1BC9 A still holds the message id: show it with its sound now lda #$FF ; 1BCC no key seen yet sta lastTypedKey ; 1BCE clear the key latch lda #$00 ; 1BD1 zero for the three stores below sta chatState ; 1BD3 leave chat typing mode sta inputLockoutTimer ; 1BD6 accept keys immediately (no input lockout) sta incomingChatLength ; 1BD9 clear the chat length again - showStatusMessage may have refilled it at $C52E L_1BDC: jsr updateStatusMessage ; 1BDC service the status line and chat display jsr nextGameRandom ; 1BDF keep stepping the shared RNG so the wait does not bias it ldy lastTypedKey ; 1BE2 last key code seen by the IRQ keyboard scan cpy #$A0 ; 1BE5 $A0 = space (the game sets bit 7 on its key codes) bne L_1BDC ; 1BE7 anything else - keep waiting jsr resetMessageQueue ; 1BE9 empty the message queue and refresh the status line lda #$1E ; 1BEC 30 frames... sta inputLockoutTimer ; 1BEE ...during which the IRQ ignores the keyboard lda #$FF ; 1BF1 no key sta lastTypedKey ; 1BF3 clear the latch so the space press is not processed again rts ; 1BF6 return ; ---------------------------------------------------------------------- ; voicePauseReconnect - The VOICE PAUSE round trip: hang up so both players can talk on the phone, ; wait five seconds, then bring the line back up and wait for carrier. Finally it clears bit 7 of ; menuSelectedItem, which is the flag runInGameOptionsMenu spins on at $65D5 while the pause lasts. ; In: linkSessionState $0B9C ; Out: zp_7B (see below), menuSelectedItem $91D5 bit 7 cleared, the link re-established ; Called from: the VOICE PAUSE item of runInGameOptionsMenu at $65C1, the link watchdog at $5A42 and ; the $C000 overlay at $C768 (when the opponent requested the pause) ; ---------------------------------------------------------------------- voicePauseReconnect: jsr hangUpAndPause ; 1BF7 hang up and wait 250 frames so the players can talk jsr openLinkAndWaitForConnection; 1BFA put the call back into data mode and wait for carrier sta shownMessageId ; 1BFD stores whatever A the return path left; in practice bit 7 is always set ($FF on the trainer path, a screen/colour RAM page high byte otherwise), which marks the status line as free - almost certainly unintended but harmless lda menuSelectedItem ; 1BFF the menu selection doubles as the pause flag and #$7F ; 1C02 clear bit 7 sta menuSelectedItem ; 1C04 this releases the wait loop in runInGameOptionsMenu at $65D5 rts ; 1C07 return ; ---------------------------------------------------------------------- ; hangUpAndPause - Hangs up for a voice pause and then waits about five seconds. The new session state ; is the current one, except that a negative state ($80 link up at menu level, $FF link lost) is ; replaced by 0. ; In: linkSessionState $0B9C ; Out: $0B9C updated by hangUpModemSetState; 250 frames of raster-timed delay ; Called from: voicePauseReconnect at $1BF7 ; ---------------------------------------------------------------------- hangUpAndPause: lda linkSessionState ; 1C08 current session state bpl L_1C0F ; 1C0B 0 or 2 - keep it as the state to restore lda #$00 ; 1C0D $80/$FF becomes 0 = no session L_1C0F: jsr hangUpModemSetState ; 1C0F prompt, drop carrier and store the state lda #$FA ; 1C12 250 frames, about five seconds... jmp waitFrames ; 1C14 ...to give the modems time to release the line, then return ; ---------------------------------------------------------------------- ; disconnectFromOpponent - Ends a link session cleanly: tells the opponent with command $8C reason 5 ; (disconnect), pushes the packet out immediately and hangs up. Does nothing when no session is active ; (linkSessionState bit 7 clear). ; In: linkSessionState $0B9C ; Out: zp_73 = 5, the $8C command queued and transmitted, the link closed and $0B9C = 0 ; Called from: runMainMenu at $1C60 (DISCONNECT/SET VOICE) and $1C6C (before starting a solo game), ; the link watchdog at $5A2F, and by JMP from $56B8 ; ---------------------------------------------------------------------- disconnectFromOpponent: lda linkSessionState ; 1C17 session state; bit 7 set = link up bpl L_1C2B ; 1C1A no session - nothing to disconnect lda #$05 ; 1C1C reason 5 = disconnect (cmdSessionControl sets the opponent's pauseState to $80) sta cmdParam0 ; 1C1E command argument 0 lda #$8C ; 1C20 $8C = cmdSessionControl jsr queueCmd2 ; 1C22 queue the 2-byte command jsr sendOutgoingPacket ; 1C25 build and hand the packet to the comm module right away jsr hangUpModem ; 1C28 then drop the line, leaving linkSessionState = 0 L_1C2B: rts ; 1C2B return ; ---------------------------------------------------------------------- ; runMainMenu - Runs the main menu whose text printOptionsList has just drawn: five items, or six when ; a link session is up and DISCONNECT/SET VOICE is offered. It preselects defaultMainMenuItem, marks ; the menu phase (selectedUnit and gamePhase = $F0, so the selector services the command exchange ; instead of the battle) and calls the generic selector with C = 0 (no slow-down before the last row). ; Item 5 just disconnects and returns to miscTabAction; every other item reloads the $6F00 overlay and ; jumps into handleMainMenuChoice, so the routine does not return. ; In: linkSessionState $0B9C, defaultMainMenuItem $92FD, the text window left by printOptionsList ; Out: menuItemCount $91D4, menuSelectedItem $91D5, menuFirstRow $91D6 = $0B, selectedUnit $90F8 = ; $F0, gamePhase zp_B1 = $F0, zp_3D/zp_42 = $60, diskLoadDepth $0BA7 bumped around the overlay ; reload ; Called from: miscTabAction at $1AD3 ; Set the menu up: item count, preselection, geometry and the menu-phase flags. ; ---------------------------------------------------------------------- runMainMenu: ldy #$05 ; 1C2C five items: COMPETE / PRACTICE / WATCH FILM / SAVE FILM / LOAD FILM lda linkSessionState ; 1C2E session state; bit 7 set = a link session exists bpl L_1C34 ; 1C31 no session - DISCONNECT/SET VOICE was not printed iny ; 1C33 six items, the last one being DISCONNECT/SET VOICE L_1C34: sty menuItemCount ; 1C34 tell the selector how many rows there are lda defaultMainMenuItem ; 1C37 the item endGameToMainMenu or selectDefaultMenuItem thought most useful sta menuSelectedItem ; 1C3A preselect it dec textWindowLeft ; 1C3D shift the window left edge to 5 so the highlight bar starts left of the text lda textWindowLeft ; 1C3F the new left edge... sta textCursorCol ; 1C41 ...also stored as the cursor column; toggleMenuRowHighlight forces column 0, so this looks vestigial lda #$0B ; 1C43 the first menu row is screen row 11 sta menuFirstRow ; 1C45 the selector adds the selection index to this jsr setCursorRow ; 1C48 put the cursor on that row lda #$F0 ; 1C4B $F0 = 'no unit is being watched'... sta selectedUnit ; 1C4D ...so the selector skips the unit-death and group-highlight logic sta gamePhase ; 1C50 zp_B1 = $F0: menu phase, bits 6/7 set, so the selector runs serviceCommandExchange instead of the battle tick lda #$60 ; 1C52 colour $60: black text, EOR mask 6 for the highlight bar sta textColour ; 1C54 text colour sta textPadColour ; 1C56 and the colour used to pad centred strings clc ; 1C58 C = 0: no extra delay before the joystick can reach the last row jsr runMenuLoopWithFlag ; 1C59 run the selector; it returns with Y = the chosen item ; ---------------------------------------------------------------------- ; Act on the choice. ; ---------------------------------------------------------------------- cpy #$05 ; 1C5C item 5 = DISCONNECT/SET VOICE bne L_1C63 ; 1C5E any other item starts or loads something jmp disconnectFromOpponent ; 1C60 just drop the link and return to miscTabAction, which reprints the menu ; ---------------------------------------------------------------------- ; Tail of runMainMenu, reached for menu items 0-4 (0 COMPETE WITH MODEM OPPONENT, 1 PRACTICE WITH SOLO ; TRAINER, 2 WATCH GAME FILM, 3 SAVE GAME FILM, 4 LOAD GAME FILM; item 5 DISCONNECT/SET VOICE was ; dealt with at $1C60). The chosen item is still in Y and is acted on by overlay A. ; ---------------------------------------------------------------------- L_1C63: lda #$0F ; 1C63 15 frames jsr waitFrames ; 1C65 let the player see the item he picked before the drive starts up cpy #$01 ; 1C68 menu item 1 = PRACTICE WITH SOLO TRAINER? bne L_1C6F ; 1C6A any other choice leaves an open modem link alone jsr disconnectFromOpponent ; 1C6C a solo game has nobody on the line: send $8C reason 5 and hang up first L_1C6F: inc diskLoadDepth ; 1C6F $0BA7 non-zero makes the raster IRQ swallow keys while the drive is busy jsr ensureOverlayAResident ; 1C72 reload the $6F00 overlay variant named by $87FF plus the message strings and comm tail dec diskLoadDepth ; 1C75 load finished - the keyboard is live again jmp handleMainMenuChoice ; 1C78 enter overlay A at $7DE3, which acts on menu item Y; it never comes back here ; ---------------------------------------------------------------------- ; isAsymmetricGameType - Tests the game type held in gameTypeOptions bits 0-2 and returns Z=1 for type ; 1 (QB SNEAK) or type 6 (DEFENDER), the two scenarios in which the two sides have different jobs and ; the setup summary therefore prints '1ST PLAY' / '2ND PLAY'. ; In: gameTypeOptions ($0BA3) bits 0-2 ; Out: A = game type 0-6, Z=1 when the type is asymmetric ; Called from: drawGameSetupSummary ($15B6 and $15E0) ; ---------------------------------------------------------------------- isAsymmetricGameType: lda gameTypeOptions ; 1C7B game options: bits 0-2 type, bit 3 unit menus, bit 4 damage, bits 6-7 custom rules/map and #$07 ; 1C7E keep only the game type 0-6 (SCRIMAGE, QB SNEAK, THE BOMB, FACE-OFF, SLUGGERS, FULL WAR, DEFENDER) cmp #$01 ; 1C80 type 1 = QB SNEAK: one side sneaks, the other defends beq L_1C86 ; 1C82 asymmetric - return with Z=1 cmp #$06 ; 1C84 type 6 = DEFENDER, the other asymmetric scenario; this compare sets the returned Z L_1C86: rts ; 1C86 Z=1 the two sides have different jobs, Z=0 both play the same game ; ---------------------------------------------------------------------- ; restoreSettingsAfterFilm - Undoes what WATCH GAME FILM overwrote. When the film-playback flag is ; set it leaves film mode and, unless the backup slot is marked empty ($FF), copies the pre-film map ; name, map seed, play order, map orientation and game options back over the live ones, so the main ; menu again describes the player's own game instead of the film's. ; In: filmPlaybackMode ($0B9B) bit 7; prevGameTypeOptions ($0BA4) = $FF when nothing was saved; the ; backups $0BAF-$0BB4, $0B98-$0B9A, $0BA2 and $0B9E ; Out: filmPlaybackMode = 0; mapNameText $0BA9-$0BAE, mapSeed $0B95-$0B97, playsFirstFlag $0BA1, ; mapFlipFlag $0B9D and gameTypeOptions $0BA3 restored ; Called from: the main-menu flow at $1ACD, just before the options list is reprinted ; ---------------------------------------------------------------------- restoreSettingsAfterFilm: lda filmPlaybackMode ; 1C87 $0B9B bit 7 = a game film is being watched bpl L_1CC0 ; 1C8A not a film: the live settings are already the real ones lda #$00 ; 1C8C leave film mode sta filmPlaybackMode ; 1C8E $0B9B = 0 also lets outgoing commands be queued again lda prevGameTypeOptions ; 1C91 $0BA4 = the game options saved before the film was started cmp #$FF ; 1C94 $FF marks 'nothing was ever saved here' beq L_1CC0 ; 1C96 no backup: keep whatever the film left behind ldy #$05 ; 1C98 6 bytes: the 5-character map name plus its bit-7 terminator L_1C9A: lda mapNameBackup,y ; 1C9A $0BAF+y = the saved map id text sta mapNameText,y ; 1C9D $0BA9+y = the map id the STATS tab and the setup summary print dey ; 1CA0 next byte down bpl L_1C9A ; 1CA1 loop over all 6 bytes ldy #$02 ; 1CA3 3 bytes of the 24-bit map seed L_1CA5: lda prevMapSeed,y ; 1CA5 $0B98+y = saved seed byte sta mapSeed,y ; 1CA8 $0B95+y = the seed the generator turns into the battlefield dey ; 1CAB next seed byte down bpl L_1CA5 ; 1CAC loop over all three lda savedPlaysFirstFlag ; 1CAE $0BA2 = who had the first play before the film sta playsFirstFlag ; 1CB1 $0BA1 drives the '1ST PLAY' / '2ND PLAY' line lda prevMapFlipFlag ; 1CB4 $0B9E = saved map orientation (0 normal, 1 rotated 180 degrees) sta mapFlipFlag ; 1CB7 $0B9D, read by the map generator lda prevGameTypeOptions ; 1CBA and finally the scenario and rule bits sta gameTypeOptions ; 1CBD $0BA3 back to the player's own game type L_1CC0: rts ; 1CC0 done - the main menu can be reprinted ; ---------------------------------------------------------------------- ; setViewVerticalLimit - Sets viewRowLimit, the exclusive map row limit the centre cell of the ; battlefield view has to stay under: 40 during the battle, 20 in the setup/film/menu phases where a ; player may only look at his own half of the map. In the restricted case the row passed in Y is ; biased by -20 as well, so the caller can test it against 0..19; scrollViewBy adds the 20 back before ; it stores the new view origin. ; In: Y = a map row (a row delta from scrollViewBy, the view origin from keepGroupBoxInsideMap), ; gamePhase (zp_B1) ; Out: viewRowLimit (zp_F8) = $28 or $14, Y reduced by $14 in the restricted phase, A clobbered ; Called from: scrollViewBy ($1CD7) and keepGroupBoxInsideMap ($1F56) ; ---------------------------------------------------------------------- setViewVerticalLimit: lda #$28 ; 1CC1 40 = the full height of the battlefield map sta viewRowLimit ; 1CC3 zp_F8 bounds the centre (cursor) row of the view lda gamePhase ; 1CC5 zp_B1 is 0 only while the battle proper runs beq L_1CD2 ; 1CC7 battle: all 40 rows may be looked at lda #$14 ; 1CC9 20 rows sta viewRowLimit ; 1CCB setup/film/menu phase: a player may only see his own half of the map tya ; 1CCD take the row value the caller handed in sec ; 1CCE prepare an exact subtraction sbc #$14 ; 1CCF shift it down by 20 so map rows 20-39 test as 0-19 tay ; 1CD1 hand the biased row back in Y L_1CD2: rts ; 1CD2 zp_F8 is set either way ; ---------------------------------------------------------------------- ; scrollViewBy - Moves the 7x5-cell tactical view by one map cell: X = column delta, Y = row delta, ; each -1, 0 or +1. The cursor is always the centre cell (viewOriginCol+3, viewOriginRow+2), so a ; step is refused when that cell - or, while a group formation box is being dragged, any edge of the ; box - would leave the 40-column map or the row window set by setViewVerticalLimit. A refused axis ; has its delta zeroed; the deltas that survive stay in scrollDeltaCol/scrollDeltaRow, the mini-map ; cursor sprite is moved, and control falls through into scrollViewBitmap, which shifts the pixels. ; In: X = dx, Y = dy (-1/0/+1); viewOriginCol/Row (zp_A5/zp_A6); gamePhase; groupBoxActive ($920A) ; bit 7 with the box edge offsets $922E-$9231 ; Out: viewOriginCol/Row updated; scrollDeltaCol/scrollDeltaRow (zp_F0/zp_F1) = the deltas actually ; applied; viewRowLimit set; activeSpriteIndex = 0 and sprite 0 repositioned; jumps into ; scrollViewBitmap and returns from there. Self-modifies the LDA # operands at $1CF6+1 and ; $1D2E+1 ; Called from: scrollViewToUnit ($1D8F) and scrollViewportByDirection ($2179) ; ---------------------------------------------------------------------- scrollViewBy: stx scrollDeltaCol ; 1CD3 remember the requested column step (-1/0/+1) sty scrollDeltaRow ; 1CD5 and the requested row step jsr setViewVerticalLimit ; 1CD7 zp_F8 = 40 or 20; in the restricted phases Y comes back biased by -20 tya ; 1CDA row delta clc ; 1CDB plain add adc viewOriginRow ; 1CDC candidate new top row of the view tay ; 1CDE keep the candidate origin in Y clc ; 1CDF plain add adc #$02 ; 1CE0 the cursor cell sits 2 rows below the top edge of the 5-cell-high view cmp viewRowLimit ; 1CE2 is the new cursor row still inside the allowed rows? bcs L_1D0E ; 1CE4 off the map (or out of your own half): refuse the vertical step bit groupBoxActive ; 1CE6 $920A bit 7 = a group formation box is riding on the cursor bpl L_1D00 ; 1CE9 no box: the centre cell alone decides sta L_1CF6+1 ; 1CEB self-modifying: park the candidate cursor row in the LDA # operand at $1CF6+1 clc ; 1CEE plain add adc groupBoxTopOffset ; 1CEF $9231 = signed offset from the cursor to the top edge of the box cmp viewRowLimit ; 1CF2 unsigned compare, so a wrapped (negative) row fails too bcs L_1D0E ; 1CF4 the box would hang off the top of the map: refuse L_1CF6: lda #$FF ; 1CF6 operand written at $1CEB: the candidate cursor row clc ; 1CF8 plain add adc groupBoxBottomOffset ; 1CF9 $9230 = signed offset to the bottom edge of the box cmp viewRowLimit ; 1CFC bottom edge still on the map? bcs L_1D0E ; 1CFE the box would hang off the bottom: refuse L_1D00: lda gamePhase ; 1D00 which phase are we in? beq L_1D09 ; 1D02 battle: the candidate row needs no correction tya ; 1D04 candidate origin row, still biased clc ; 1D05 plain add adc #$14 ; 1D06 undo the -20 bias setViewVerticalLimit applied tay ; 1D08 back into Y for the store below L_1D09: sty viewOriginRow ; 1D09 accept the vertical scroll jmp L_1D12 ; 1D0B now try the horizontal step L_1D0E: lda #$00 ; 1D0E no vertical movement sta scrollDeltaRow ; 1D10 vertical step clamped: tell scrollViewBitmap not to shift any rows L_1D12: txa ; 1D12 column delta clc ; 1D13 plain add adc viewOriginCol ; 1D14 candidate new left column of the view tax ; 1D16 keep the candidate origin in X clc ; 1D17 plain add adc #$03 ; 1D18 the cursor cell sits 3 columns right of the left edge of the 7-cell-wide view cmp #$28 ; 1D1A 40 map columns bcs L_1D3D ; 1D1C off the map: refuse the horizontal step bit groupBoxActive ; 1D1E group formation box active? bpl L_1D38 ; 1D21 no box: accept sta L_1D2E+1 ; 1D23 self-modifying: park the candidate cursor column in the LDA # operand at $1D2E+1 clc ; 1D26 plain add adc groupBoxLeftOffset ; 1D27 $922F = signed offset to the left edge of the box cmp #$28 ; 1D2A left edge still on the map? bcs L_1D3D ; 1D2C box would stick out to the left: refuse L_1D2E: lda #$FF ; 1D2E operand written at $1D23: the candidate cursor column clc ; 1D30 plain add adc groupBoxRightOffset ; 1D31 $922E = signed offset to the right edge of the box cmp #$28 ; 1D34 right edge still on the map? bcs L_1D3D ; 1D36 box would stick out to the right: refuse L_1D38: stx viewOriginCol ; 1D38 accept the horizontal scroll jmp L_1D41 ; 1D3A both axes decided L_1D3D: lda #$00 ; 1D3D no horizontal movement sta scrollDeltaCol ; 1D3F horizontal step clamped: no column shift L_1D41: lda #$00 ; 1D41 sprite 0 sta activeSpriteIndex ; 1D43 $91CC = the sprite positionCursorSprite works on: the mini-map cursor box jsr positionCursorSprite ; 1D46 move it onto the new centre cell (the mini-map draws each map cell 4x4 pixels) jmp scrollViewBitmap ; 1D49 shift bitmap, screen and colour RAM and redraw the cells that came into sight ; scrollTargetUnit / scrollStepDelay - the two parameters of scrollViewToUnit. They live in memory ; because that routine gives the registers up to updateGameFrame between steps. scrollTargetUnit: .byte $00 ; 1D4C . scrollTargetUnit: unit index the view is being scrolled onto ; scrollStepDelay - frames between two scroll steps; 0 makes scrollViewToUnit jump to the unit in one ; go without animating. scrollStepDelay: .byte $00 ; 1D4D . scrollStepDelay: frames between steps; 0 = jump there without animating ; ---------------------------------------------------------------------- ; scrollViewToUnit - Scrolls the battlefield view until unit X sits under the centre cursor, one map ; cell per step along a Bresenham line that starts at the cell the caller left in lineCurRow/ ; lineCurCol (normally the current cursor cell). With A = 0 the whole walk runs at once; with A != 0 ; each step waits A frames on timer zp_6E while updateGameFrame keeps the game, the sound and the link ; running, and the walk is abandoned as soon as the player touches the joystick. The target is ; re-read every step, so the view follows a unit that is still moving. ; In: A = frames per step (0 = no animation), X = unit index, lineCurRow/lineCurCol (zp_92/zp_93) = ; the cell the walk starts from ; Out: view scrolled (viewOriginCol/Row); scrollTargetUnit/scrollStepDelay; line stepper state ; zp_8C-zp_97; zp_6E used as the step timer ; Called from: recenterViewportOnUnit ($25B3, A=1, the selected unit) and $485C (A=0, the unit shown ; in the info panel) ; ---------------------------------------------------------------------- scrollViewToUnit: sta scrollStepDelay ; 1D4E frames between steps (0 = immediate) stx scrollTargetUnit ; 1D51 unit to centre the view on jsr setLineTargetToUnit ; 1D54 line end point (zp_95/zp_94) = the unit's map cell jsr initLineStepper ; 1D57 set up the Bresenham walk from the cursor cell to the unit L_1D5A: lda scrollStepDelay ; 1D5A animated or immediate? beq L_1D7D ; 1D5D no animation wanted: take the next step straight away lda lineCurRow ; 1D5F updateGameFrame steps the units through the same line stepper, pha ; 1D61 so save the walker's row lda lineCurCol ; 1D62 and its column pha ; 1D64 on the stack across the call jsr updateGameFrame ; 1D65 one frame of engine, message, sound and link work pla ; 1D68 restore our own walk position sta lineCurCol ; 1D69 zp_93 = current column pla ; 1D6B and the row sta lineCurRow ; 1D6C zp_92 lda joyDirection ; 1D6E zp_5D = joystick nibble latched by the raster IRQ cmp #$0F ; 1D70 $0F = stick centred bne L_1D96 ; 1D72 the player took over: stop the automatic scroll lda menuBlinkTimer ; 1D74 zp_6E, an IRQ frame countdown reused here as the step timer bne L_1D5A ; 1D76 this step is not due yet: spin another frame lda scrollStepDelay ; 1D78 step delay sta menuBlinkTimer ; 1D7B reload the timer for the next step L_1D7D: ldx scrollTargetUnit ; 1D7D the unit we are chasing jsr setLineTargetToUnit ; 1D80 re-read its position: it may have moved a cell meanwhile jsr initLineStepper ; 1D83 re-aim the line from the cell we are on now jsr stepLine ; 1D86 walk one cell; the applied deltas land in $90F4/$90F5 ldx lastStepColDelta ; 1D89 $90F5 = column step just taken (-1/0/+1) ldy lastStepRowDelta ; 1D8C $90F4 = row step just taken (-1/0/+1) jsr scrollViewBy ; 1D8F scroll the view by that one cell lda lineStepsLeft ; 1D92 zp_91 = steps still to walk bne L_1D5A ; 1D94 keep going until the unit is under the cursor L_1D96: rts ; 1D96 view centred (or the player interrupted) ; ---------------------------------------------------------------------- ; setLineTargetToUnit - Points the line stepper's end point at unit X. The unit records are parallel ; 100-byte arrays $64 apart from $F640, so the unit's column and row are unitColTable[X] and ; unitRowTable[X]. ; In: X = unit index 0-99 ; Out: lineEndCol (zp_95), lineEndRow (zp_94) ; Called from: scrollViewToUnit ($1D54 and $1D80) ; ---------------------------------------------------------------------- setLineTargetToUnit: lda unitColTable,x ; 1D97 $F640+unit = the unit's map column sta lineEndCol ; 1D9A zp_95 = target column of the line stepper lda unitRowTable,x ; 1D9C $F6A4+unit = the unit's map row (the next 100-byte array, $64 further on) sta lineEndRow ; 1D9F zp_94 = target row rts ; 1DA1 the caller now calls initLineStepper ; Parameter tables for scrollViewBitmap: 22 tables of three bytes, all indexed by a scroll direction ; biased to 0/1/2 = -1/0/+1. The 9 tables read with X describe the horizontal shift (which byte or ; character range of a row is copied, and in which direction), the 13 read with Y describe the ; vertical shift (which character row the copy starts on, where it goes and the row stride). The view ; is 7x5 map cells of 2x2 characters at screen column 25, character row 1: bitmap $A208, screen RAM ; $8C41, colour RAM $D841 (screen RAM + $4C00). One character row of it is 14 characters = 112 ; consecutive bitmap bytes, and one map cell is 2 characters = 16 bitmap bytes. scrollXStartIndex - ; first Y index of the bitmap byte copy. scrollXStartIndex: .byte $5F,$00,$00 ; 1DA2 _.. $5F,$00,$00 = first Y index of the bitmap byte copy (count down from $5F, or up from 0) ; scrollXEndIndex: byteTable, 3 bytes. end Y index of the bitmap byte copy ($FF,$70,$60) scrollXEndIndex: .byte $FF,$70,$60 ; 1DA5 .p` $FF,$70,$60 = index the copy stops at: $5F..0 down, 0..$6F up, 0..$5F up ; scrollXStepOpcode: byteTable, 3 bytes. DEY ($88) or INY ($C8) opcode patched into both copy loops scrollXStepOpcode: .byte $88,$C8,$C8 ; 1DA8 ... $88 DEY (copy right, backwards) or $C8 INY (copy left / straight, forwards) ; scrollXBitmapSrcOffset: byteTable, 3 bytes. byte offset added to the bitmap source address (0,0,$10) scrollXBitmapSrcOffset: .byte $00,$00,$10 ; 1DAB ... $00,$00,$10 = +16 bytes (2 characters = 1 map cell) is added to the source when the view moves right ; scrollXBitmapDstOffset: byteTable, 3 bytes. byte offset added to the bitmap destination address ; ($10,0,0) scrollXBitmapDstOffset: .byte $10,$00,$00 ; 1DAE ... $10,$00,$00 = the same 16-byte shift on the destination when the view moves left ; scrollYBitmapSrcLo: byteTable, 3 bytes. per vertical direction: bitmap source address lo ; ($AAC8,$A208,$A488) scrollYBitmapSrcLo: .byte $C8,$08,$88 ; 1DB1 ... $C8,$08,$88 = low byte of $AAC8/$A208/$A488, the first bitmap row copied (char rows 8/1/3) ; scrollYBitmapSrcHi: byteTable, 3 bytes. bitmap source address hi scrollYBitmapSrcHi: .byte $AA,$A2,$A4 ; 1DB4 ... $AA,$A2,$A4 = high bytes of those three bitmap addresses ; scrollYBitmapDstLo: byteTable, 3 bytes. bitmap destination address lo ($AD48,$A208,$A208) scrollYBitmapDstLo: .byte $48,$08,$08 ; 1DB7 H.. $48,$08,$08 = low byte of $AD48/$A208/$A208, where that row is copied to (char rows 10/1/1) ; scrollYBitmapDstHi: byteTable, 3 bytes. bitmap destination address hi scrollYBitmapDstHi: .byte $AD,$A2,$A2 ; 1DBA ... $AD,$A2,$A2 = high bytes of the destinations ; scrollYBitmapStrideLo: byteTable, 3 bytes. bitmap row stride lo ($FEC0 = -320 when scrolling up, ; $0140 = +320 otherwise) scrollYBitmapStrideLo: .byte $C0,$40,$40 ; 1DBD .@@ $C0,$40,$40 = low byte of the row stride -320/+320/+320 (one character row of the bitmap) ; scrollYBitmapStrideHi: byteTable, 3 bytes. bitmap row stride hi scrollYBitmapStrideHi: .byte $FE,$01,$01 ; 1DC0 ... $FE,$01,$01 = high byte of the same stride ; scrollYRowCount: byteTable, 3 bytes. character rows to copy (8,10,8) scrollYRowCount: .byte $08,$0A,$08 ; 1DC3 ... 8,10,8 character rows to copy: all 10 when only columns move, 8 when 2 rows are freed ; scrollXScreenStartIndex: byteTable, 3 bytes. start Y index of the screen/colour copy ($0B,0,0) scrollXScreenStartIndex: .byte $0B,$00,$00 ; 1DC6 ... $0B,$00,$00 = first Y index of the 14-character screen/colour copy ; scrollXScreenEndIndex: byteTable, 3 bytes. end Y index of the screen/colour copy ($FF,$0E,$0C) scrollXScreenEndIndex: .byte $FF,$0E,$0C ; 1DC9 ... $FF,$0E,$0C = index it stops at: $0B..0 down, 0..$0D up, 0..$0B up ; scrollXScreenSrcOffset: byteTable, 3 bytes. column offset of the screen source (0,0,2) scrollXScreenSrcOffset: .byte $00,$00,$02 ; 1DCC ... $00,$00,$02 = 2 characters added to the screen source when the view moves right ; scrollXScreenDstOffset: byteTable, 3 bytes. column offset of the screen destination (2,0,0) scrollXScreenDstOffset: .byte $02,$00,$00 ; 1DCF ... $02,$00,$00 = 2 characters added to the screen destination when the view moves left ; scrollYScreenSrcLo: byteTable, 3 bytes. screen RAM source address lo ($8D59 row 8, $8C41 row 1, ; $8C91 row 3; column 25) scrollYScreenSrcLo: .byte $59,$41,$91 ; 1DD2 YA. $59,$41,$91 = low byte of $8D59/$8C41/$8C91: screen RAM column 25 of char rows 8/1/3 ; scrollYScreenSrcHi: byteTable, 3 bytes. screen RAM source address hi (colour RAM = +$4C00) scrollYScreenSrcHi: .byte $8D,$8C,$8C ; 1DD5 ... $8D,$8C,$8C = high bytes; the colour RAM copy uses the same addresses + $4C00 ; scrollYScreenDstLo: byteTable, 3 bytes. screen RAM destination address lo ($8DA9, $8C41, $8C41) scrollYScreenDstLo: .byte $A9,$41,$41 ; 1DD8 .AA $A9,$41,$41 = low byte of $8DA9/$8C41/$8C41: column 25 of char rows 10/1/1 ; scrollYScreenDstHi: byteTable, 3 bytes. screen RAM destination address hi scrollYScreenDstHi: .byte $8D,$8C,$8C ; 1DDB ... $8D,$8C,$8C = high bytes of the screen destinations ; scrollYScreenStrideLo: byteTable, 3 bytes. screen row stride lo ($FFD8 = -40 up, $0028 = +40) scrollYScreenStrideLo: .byte $D8,$28,$28 ; 1DDE .(( $D8,$28,$28 = low byte of the screen row stride -40/+40/+40 ; scrollYScreenStrideHi: byteTable, 3 bytes. screen row stride hi scrollYScreenStrideHi: .byte $FF,$00,$00 ; 1DE1 ... $FF,$00,$00 = high byte of the screen row stride ; ---------------------------------------------------------------------- ; scrollViewBitmap - Shifts the whole 7x5-cell tactical view one map cell (16 pixels = 2 characters) ; in the direction left in scrollDeltaCol/scrollDeltaRow and then redraws the cells that scrolled into ; sight. Both deltas are first biased to 0/1/2 = -1/0/+1 and used to index the 22 tables at ; $1DA2; the values are poked into two self-modified copy loops - one moving 96 or 112 bitmap bytes ; per character row, one moving 12 or 14 bytes of screen RAM and of colour RAM - which are run once ; per character row (8 or 10 of them) with the row stride added between passes. A one-cell shift is ; 16 bitmap bytes or 2 characters, so a 12-character window is copied 2 characters over and the ; exposed column (5 cells) and/or row (7 cells) is drawn from the map. ; In: scrollDeltaCol/scrollDeltaRow (zp_F0/zp_F1) = -1/0/+1; viewOriginCol/Row, already updated by ; scrollViewBy ; Out: bitmap $A208.., screen RAM $8C41.. and colour RAM $D841.. scrolled and the new edge drawn; ; zp_F0/zp_F1 left biased to 0/1/2; zp_F2-zp_F7 and $54-$56 used as scratch. Self-modifies ; $1E81+1, $1E83+1/+2, $1E86+1/+2, $1E89, $1E8A+1, $1EB0+1, $1EB2+1/+2, $1EB5+1/+2, $1EB8+1/+2, ; $1EBB+1/+2, $1EBE and $1EBF+1 ; Called from: scrollViewBy ($1D49, by JMP - this is that routine's tail) ; ---------------------------------------------------------------------- scrollViewBitmap: inc scrollDeltaCol ; 1DE4 bias the column step -1/0/+1 into a table index 0/1/2 ldx scrollDeltaCol ; 1DE6 X selects the horizontal-shift parameters inc scrollDeltaRow ; 1DE8 same for the row step ldy scrollDeltaRow ; 1DEA Y selects the vertical-shift parameters lda scrollXStartIndex,x ; 1DEC start index of the bitmap copy sta L_1E81+1 ; 1DEF patch the LDY # that opens the bitmap loop lda scrollXEndIndex,x ; 1DF2 end index of the bitmap copy sta L_1E8A+1 ; 1DF5 patch the CPY # that closes it lda scrollYBitmapSrcLo,y ; 1DF8 first bitmap row to read from clc ; 1DFB plain add adc scrollXBitmapSrcOffset,x; 1DFC +16 bytes = one map cell to the right on the source when the view scrolls right sta L_1E83+1 ; 1DFF patch the low byte of the LDA abs,y source operand lda scrollYBitmapSrcHi,y ; 1E02 high byte of the source row address adc #$00 ; 1E05 take the carry out of the low byte sta L_1E83+2 ; 1E07 patch the high byte of the source operand lda scrollYBitmapDstLo,y ; 1E0A bitmap row to write to clc ; 1E0D plain add adc scrollXBitmapDstOffset,x; 1E0E +16 bytes on the destination instead when the view scrolls left sta L_1E86+1 ; 1E11 patch the low byte of the STA abs,y destination operand lda scrollYBitmapDstHi,y ; 1E14 high byte of the destination row address adc #$00 ; 1E17 carry out of the low byte sta L_1E86+2 ; 1E19 patch the high byte of the destination operand lda scrollXStepOpcode,x ; 1E1C $88 DEY or $C8 INY sta scrollBitmapStepOpcode ; 1E1F patch the step instruction of the bitmap loop lda scrollYRowCount,y ; 1E22 8 or 10 character rows sta scrollRowCount ; 1E25 zp_F6 counts the rows still to move lda scrollYBitmapStrideLo,y ; 1E27 +320 or -320 = one character row of the bitmap sta bitmapRowStride ; 1E2A zp_F2 low byte of the bitmap row stride lda scrollYBitmapStrideHi,y ; 1E2C sign/high byte of the stride sta bitmapRowStrideHi ; 1E2F zp_F3 lda scrollXScreenStartIndex,x; 1E31 start index of the screen/colour copy sta L_1EB0+1 ; 1E34 patch the LDY # that opens the screen loop lda scrollXScreenEndIndex,x ; 1E37 end index of the screen copy sta L_1EBF+1 ; 1E3A patch its CPY # lda scrollYScreenSrcLo,y ; 1E3D first screen RAM row to read from clc ; 1E40 plain add adc scrollXScreenSrcOffset,x; 1E41 +2 characters on the source when the view scrolls right sta L_1EB2+1 ; 1E44 patch the screen RAM source operand (low) sta L_1EB8+1 ; 1E47 the colour RAM source has the same low byte lda scrollYScreenSrcHi,y ; 1E4A high byte of the screen row address ($8C/$8D) adc #$00 ; 1E4D carry out of the low byte sta L_1EB2+2 ; 1E4F patch the screen RAM source operand (high) - screen RAM is at $8C00 adc #$4C ; 1E52 +$4C00 turns a screen RAM address into the matching colour RAM address ($D800) sta L_1EB8+2 ; 1E54 patch the colour RAM source operand (high) lda scrollYScreenDstLo,y ; 1E57 screen RAM row to write to clc ; 1E5A plain add adc scrollXScreenDstOffset,x; 1E5B +2 characters on the destination instead when the view scrolls left sta L_1EB5+1 ; 1E5E patch the screen RAM destination operand (low) sta L_1EBB+1 ; 1E61 colour RAM destination shares the low byte lda scrollYScreenDstHi,y ; 1E64 high byte of the destination row adc #$00 ; 1E67 carry out of the low byte sta L_1EB5+2 ; 1E69 patch the screen RAM destination operand (high) adc #$4C ; 1E6C same $4C00 bias into colour RAM sta L_1EBB+2 ; 1E6E patch the colour RAM destination operand (high) lda scrollXStepOpcode,x ; 1E71 $88 DEY or $C8 INY again sta scrollScreenStepOpcode ; 1E74 patch the step instruction of the screen loop lda scrollYScreenStrideLo,y ; 1E77 +40 or -40 = one screen row sta screenRowStride ; 1E7A zp_F4 low byte of the screen row stride lda scrollYScreenStrideHi,y ; 1E7C sign/high byte sta screenRowStrideHi ; 1E7F zp_F5 ; ---------------------------------------------------------------------- ; One character row of the view: move 96 bytes (a 12-character window shifted by one map cell) or all ; 112 bytes of the row in the bitmap, then the matching 12 or 14 bytes of screen RAM and of colour ; RAM. Every operand that the listing shows as irqVectorHi ($FFFF) is a placeholder that was ; rewritten above, and the INY at $1E89/$1EBE may have been replaced by a DEY, so that a shifted copy ; walks away from the overlap instead of into it. ; ---------------------------------------------------------------------- L_1E81: ldy #$FF ; 1E81 start index, patched at $1DEF ($5F downwards, or 0 upwards) L_1E83: lda irqVectorHi,y ; 1E83 source byte of this bitmap row (operand patched at $1DFF/$1E07) L_1E86: sta irqVectorHi,y ; 1E86 destination byte (operand patched at $1E11/$1E19) scrollBitmapStepOpcode: iny ; 1E89 INY or DEY, patched at $1E1F: copy forwards or backwards so a shift cannot eat itself L_1E8A: cpy #$FF ; 1E8A end index, patched at $1DF5 bne L_1E83 ; 1E8C next byte of this character row (96 or 112 bytes) lda L_1E86+1 ; 1E8E step the destination pointer one character row on clc ; 1E91 plain add adc bitmapRowStride ; 1E92 +320 or -320 sta L_1E86+1 ; 1E94 write the new low byte back into the STA operand lda L_1E86+2 ; 1E97 high byte of the destination adc bitmapRowStrideHi ; 1E9A with the sign of the stride sta L_1E86+2 ; 1E9C write it back lda L_1E83+1 ; 1E9F the same for the source pointer clc ; 1EA2 plain add adc bitmapRowStride ; 1EA3 +320 or -320 sta L_1E83+1 ; 1EA5 write the new low byte into the LDA operand lda L_1E83+2 ; 1EA8 high byte of the source adc bitmapRowStrideHi ; 1EAB with the sign of the stride sta L_1E83+2 ; 1EAD write it back L_1EB0: ldy #$FF ; 1EB0 start index of the character copy, patched at $1E34 L_1EB2: lda irqVectorHi,y ; 1EB2 screen RAM source (the tile's colour nibbles), operand patched at $1E44/$1E4F L_1EB5: sta irqVectorHi,y ; 1EB5 screen RAM destination, patched at $1E5E/$1E69 L_1EB8: lda irqVectorHi,y ; 1EB8 the same character in colour RAM at $D800, patched at $1E47/$1E54 L_1EBB: sta irqVectorHi,y ; 1EBB colour RAM destination, patched at $1E61/$1E6E scrollScreenStepOpcode: iny ; 1EBE INY or DEY, patched at $1E74 L_1EBF: cpy #$FF ; 1EBF end index, patched at $1E3A bne L_1EB2 ; 1EC1 next character of this row (12 or 14 of them) lda L_1EB5+1 ; 1EC3 step the screen destination one row on clc ; 1EC6 plain add adc screenRowStride ; 1EC7 +40 or -40 sta L_1EB5+1 ; 1EC9 write it back into the screen STA operand sta L_1EBB+1 ; 1ECC and into the colour RAM STA operand, which shares the low byte lda L_1EB5+2 ; 1ECF high byte of the screen destination adc screenRowStrideHi ; 1ED2 with the sign of the stride sta L_1EB5+2 ; 1ED4 write it back clc ; 1ED7 prepare the colour RAM bias adc #$4C ; 1ED8 screen page + $4C = colour RAM page sta L_1EBB+2 ; 1EDA write the colour RAM destination high byte lda L_1EB2+1 ; 1EDD step the screen source one row on clc ; 1EE0 plain add adc screenRowStride ; 1EE1 +40 or -40 sta L_1EB2+1 ; 1EE3 write it back into the screen LDA operand sta L_1EB8+1 ; 1EE6 and into the colour RAM LDA operand lda L_1EB2+2 ; 1EE9 high byte of the screen source adc screenRowStrideHi ; 1EEC with the sign of the stride sta L_1EB2+2 ; 1EEE write it back clc ; 1EF1 prepare the colour RAM bias adc #$4C ; 1EF2 +$4C pages = $D800 sta L_1EB8+2 ; 1EF4 write the colour RAM source high byte dec scrollRowCount ; 1EF7 one character row of the view is done bne L_1E81 ; 1EF9 8 or 10 rows in all ; ---------------------------------------------------------------------- ; The cells that scrolled into sight still hold the old picture: redraw the newly exposed column (5 ; cells) and/or the newly exposed row (7 cells) straight from the map. $54/$55/$56 are the cell byte ; / column / row triple that drawViewportCell expects. ; ---------------------------------------------------------------------- lda viewOriginCol ; 1EFB left edge of the view ldx scrollDeltaCol ; 1EFD 0/1/2 = the view moved left / not at all / right cpx #$01 ; 1EFF 1 = no horizontal movement beq L_1F24 ; 1F01 nothing new appeared at the sides bcc L_1F08 ; 1F03 moved left: the freshly exposed column is the left edge itself clc ; 1F05 plain add adc #$06 ; 1F06 moved right: it is the 7th (rightmost) column of the view L_1F08: sta savedRegX ; 1F08 $55 = column of the cell to draw lda viewOriginRow ; 1F0A top row of the view sta savedRegY ; 1F0C $56 = row of the cell to draw lda #$05 ; 1F0E the view is 5 cells tall sta redrawCellCount ; 1F10 zp_F7 counts the cells still to draw L_1F12: ldx savedRegX ; 1F12 X = map column ldy savedRegY ; 1F14 Y = map row jsr readMapCell ; 1F16 read the map byte: terrain code, or $80 | unit index when a unit stands there sta savedRegA ; 1F19 $54 = the cell byte drawViewportCell expects jsr drawViewportCell ; 1F1B render the 16x16 pixel tile into the bitmap inc savedRegY ; 1F1E next row down the exposed column dec redrawCellCount ; 1F20 one cell done bne L_1F12 ; 1F22 5 cells of the column L_1F24: lda viewOriginRow ; 1F24 top row of the view ldy scrollDeltaRow ; 1F26 0/1/2 = the view moved up / not at all / down cpy #$01 ; 1F28 1 = no vertical movement beq L_1F4D ; 1F2A nothing new appeared above or below: done bcc L_1F31 ; 1F2C moved up: the freshly exposed row is the top edge itself clc ; 1F2E plain add adc #$04 ; 1F2F moved down: it is the 5th (bottom) row of the view L_1F31: sta savedRegY ; 1F31 $56 = row of the cells to draw lda viewOriginCol ; 1F33 start at the left edge sta savedRegX ; 1F35 $55 = column of the cell to draw lda #$07 ; 1F37 the view is 7 cells wide sta redrawCellCount ; 1F39 zp_F7 counts them down L_1F3B: ldx savedRegX ; 1F3B X = map column ldy savedRegY ; 1F3D Y = map row jsr readMapCell ; 1F3F read the map byte sta savedRegA ; 1F42 $54 = cell byte jsr drawViewportCell ; 1F44 draw the 16x16 tile inc savedRegX ; 1F47 next column along the exposed row dec redrawCellCount ; 1F49 one cell done bne L_1F3B ; 1F4B 7 cells of the row L_1F4D: rts ; 1F4D the view now matches the map again ; ---------------------------------------------------------------------- ; keepGroupBoxInsideMap - Nudges the view origin one cell at a time until the whole group formation ; box (the cursor cell plus the signed edge offsets $922E-$9231 built by buildGroupFormationSprite) ; fits inside the map: 40 columns wide and, vertically, inside the row window set by ; setViewVerticalLimit. Every nudge restarts the four edge tests; if at least one nudge was needed ; the view is repainted. ; In: viewOriginCol/Row (zp_A5/zp_A6); groupBoxLeftOffset/RightOffset/TopOffset/BottomOffset ; ($922F/$922E/$9231/$9230); gamePhase ; Out: viewOriginCol/Row corrected, viewRowLimit set, zp_18/zp_19 scratch; tail-jumps to drawViewport ; when anything moved ; Called from: buildGroupFormationSprite ($319D), right after the box offsets are computed ; ---------------------------------------------------------------------- keepGroupBoxInsideMap: lda #$01 ; 1F4E 1 so the first DEC below leaves 0 sta scratch19 ; 1F50 zp_19 = 0 means 'the view has not been nudged yet' L_1F52: dec scratch19 ; 1F52 count this pass; any pass after the first leaves zp_19 non-zero ldy viewOriginRow ; 1F54 current top row of the view jsr setViewVerticalLimit ; 1F56 zp_F8 = 40 or 20; Y comes back biased by -20 in the setup phase tya ; 1F59 top row clc ; 1F5A plain add adc #$02 ; 1F5B cursor row = top edge + 2 sta scratch18 ; 1F5D zp_18 keeps the cursor row for the second test clc ; 1F5F plain add adc groupBoxTopOffset ; 1F60 $9231 = signed offset to the top edge of the group box cmp viewRowLimit ; 1F63 still inside the allowed rows? bcc L_1F6C ; 1F65 yes: test the bottom edge inc viewOriginRow ; 1F67 box sticks out above the map: slide the view one row down jmp L_1F52 ; 1F69 and start the four tests again L_1F6C: lda scratch18 ; 1F6C cursor row again clc ; 1F6E plain add adc groupBoxBottomOffset ; 1F6F $9230 = signed offset to the bottom edge of the box cmp viewRowLimit ; 1F72 still inside the allowed rows? bcc L_1F7B ; 1F74 yes: go on to the columns dec viewOriginRow ; 1F76 box sticks out below: slide the view one row up jmp L_1F52 ; 1F78 and re-test L_1F7B: lda viewOriginCol ; 1F7B current left column of the view clc ; 1F7D plain add adc #$03 ; 1F7E cursor column = left edge + 3 sta scratch18 ; 1F80 zp_18 keeps the cursor column clc ; 1F82 plain add adc groupBoxLeftOffset ; 1F83 $922F = signed offset to the left edge of the box cmp #$28 ; 1F86 40 map columns bcc L_1F8F ; 1F88 inside: test the right edge inc viewOriginCol ; 1F8A box sticks out to the left: slide the view one column right jmp L_1F52 ; 1F8C and re-test L_1F8F: lda scratch18 ; 1F8F cursor column again clc ; 1F91 plain add adc groupBoxRightOffset ; 1F92 $922E = signed offset to the right edge of the box cmp #$28 ; 1F95 40 map columns bcc L_1F9E ; 1F97 the whole box fits: done dec viewOriginCol ; 1F99 box sticks out to the right: slide the view one column left jmp L_1F52 ; 1F9B and re-test L_1F9E: lda scratch19 ; 1F9E 0 only if the box already fitted on the very first pass beq L_1FA5 ; 1FA0 nothing moved: leave the screen alone jmp drawViewport ; 1FA2 the view moved: repaint all 35 cells and return from there L_1FA5: rts ; 1FA5 view origin now keeps the whole group box on the map ; joystickDirectionTable - joystick nibble to direction code. The raster IRQ latches bits 0-4 of CIA1 ; port A (joystick 2) into zp_5D, active low, so $0F means centred: bit 0 up, bit 1 down, bit 2 left, ; bit 3 right. The table maps that nibble to a direction code 0-7 (0 N, 1 S, 2 E, 3 W, 4 NW, 5 NE, 6 ; SE, 7 SW) or to $FF for 'no usable direction'. Logically it is 16 bytes; the last four ; ($FF,$01,$00,$FF for nibbles $0C-$0F) were split off by the disassembler and are the first four ; bytes of the block labelled diagonalDirectionTable. joystickDirectionTable: .byte $FF,$FF,$FF,$FF,$FF,$06,$05,$02; 1FA6 ........ nibbles $00-$07: only $05 (down+right) -> 6 SE, $06 (up+right) -> 5 NE and $07 (right) -> 2 E are real .byte $FF,$07,$04,$03 ; 1FAE .... ; $1FB2-$1FB5 are still joystickDirectionTable (nibble $0C impossible, $0D = down -> 1 S, $0E = up -> ; 0 N, $0F = centred -> $FF). diagonalDirectionTable proper is $1FB6-$1FB9: for direction codes 4-7 it ; packs (index of the column delta << 4) | index of the row delta into directionDeltaTable - NW = ; (-1,-1), NE = (+1,-1), SE = (+1,+1), SW = (-1,+1). scrollViewportByDirection ($2111), the comcen ; screens ($736F, $76CF) and the drone AI ($EF58) all index it with the direction code, so only ; offsets 4-7 are ever read. The eight bytes at $1FBA (01 00 03 02 06 07 04 05) are an ; opposite-direction table - N<->S, E<->W, NW<->SE, NE<->SW - that no unit references; together with ; the $00 pad at $1FC2 they look like leftover data. diagonalDirectionTable: .byte $FF,$01,$00,$FF,$30,$20,$21,$31; 1FB2 ....0 !1 $1FB2-$1FB5 finish joystickDirectionTable; $1FB6-$1FB9 = diagonals 4-7; $1FBA-$1FC1 unused .byte $01,$00,$03,$02,$06,$07,$04,$05; 1FBA ........ .byte $00 ; 1FC2 . ; ---------------------------------------------------------------------- ; enterMapScreen - Entry point of the battlefield screen (game mode 0). Restores the viewport origin ; the player left it at and repaints the whole screen - the three frames, the strategic mini-map, the ; tactical view and the status bar - then falls straight into runMapMainLoop. ; In: savedViewOriginCol/Row ($92C3/$92C4) ; Out: viewOriginCol/Row set, screen redrawn; does not return - falls into runMapMainLoop ; Called from: switchToRequestedScreen ($06F5, by JMP, when F1 selects screen 0) ; ---------------------------------------------------------------------- enterMapScreen: lda savedViewOriginCol ; 1FC3 $92C3, written every pass of the map main loop sta viewOriginCol ; 1FC6 zp_A5 = map column of the left edge of the 7x5 view lda savedViewOriginRow ; 1FC8 $92C4 sta viewOriginRow ; 1FCB zp_A6 = map row of the top edge of the view jsr redrawGameScreen ; 1FCD rebuild frames, mini-map, tactical view and status bar, then fall into the main loop ; ---------------------------------------------------------------------- ; runMapMainLoop - The interactive loop of the battlefield screen and of the unit-placement phase. It ; resets the cursor state, draws the view, and then loops: one game frame, then hover handling ; (resting 15 frames on one of your own units pops up the info panel) and click handling (a click ; selects the unit, or destroys it during placement while DESTROY mode is armed), then joystick ; scrolling with auto-repeat. During the battle (gamePhase = 0) an F1/F3/F5/F7 request is honoured by ; switchToRequestedScreen, which rebuilds the stack and jumps to the new screen instead of returning; ; during unit placement the overlay-A '*' hook is polled every pass and the routine returns when F1 is ; pressed or the phase ends. Units of the other side are filtered out by two 'cmp #$32' tests whose ; following branch opcode ($206B, $2084) is written at run time from the local player's side. ; In: gamePhase (zp_B1), joyDirection (zp_5D), inputIdleFrames ($90EE), requestedScreenKey (zp_66), ; gameEndCountdown ($92C9), revealAllUnits ($92FC), filmPlaybackMode ($0B9B), comcenUnit ($9236), ; comcenCloakedFlag ($9245), setupToggleFlag ($92F3), the unit arrays and the map ; Out: savedViewOriginCol/Row kept up to date; selectedUnit ($90F8); info panel, cursor sprites and ; view redrawn; command $91 queued in DESTROY mode; returns only from the placement phase ; Called from: enterMapScreen (fall-through), startGameFromSetup ($0B7A), the screen dispatcher ; ($075E) and the setup phase of overlay A ($7B4E) ; ---------------------------------------------------------------------- runMapMainLoop: lda #$FF ; 1FD0 no key sta lastTypedKey ; 1FD2 $9248: forget anything typed before the battlefield came up sta infoPanelUnit ; 1FD5 $91D7 = no unit is being shown in the info panel lda #$00 ; 1FD8 clear sta pathMarkerState ; 1FDA $24E1 = the mini-map path marker has no passes left to run sta cursorTargetMode ; 1FDD zp_BB = 0: the cursor is a plain move cursor, not a target picker sta screenBuiltFlag ; 1FDF zp_41, a spare slot of the saved text state; nothing ever reads it (dead store) lda savedViewOriginCol ; 1FE1 return to the part of the map the player was looking at sta viewOriginCol ; 1FE4 zp_A5 lda savedViewOriginRow ; 1FE6 $92C4 sta viewOriginRow ; 1FE9 zp_A6 jsr showCursorBoxSprite ; 1FEB sprite 2 = the expanded box that frames the centre cell of the view lda gamePhase ; 1FEE 0 = the battle proper, non-zero = unit placement beq L_1FFA ; 1FF0 battle: no standing message lda #$0A ; 1FF2 message 10 = 'SETUP UNITS. PRESS F 1 WHEN DONE.' sta persistentMessageId ; 1FF4 $92A1 = the message the status line falls back to jsr queueMessage ; 1FF7 and show it right now as well L_1FFA: lda #$00 ; 1FFA clear sta uiLevel ; 1FFC $9209 = 0: just browsing, no unit or group selected sta activeSpriteIndex ; 1FFF $91CC = sprite 0 for positionCursorSprite sta textColour ; 2002 zp_3D = 0 so the panel is cleared to black jsr clearInfoPanel ; 2004 blank the info panel window (rows 12-20, columns 25-38) lda #$FF ; 2007 no highlight sta highlightedUnit ; 2009 $9204 = no unit is drawn inverted in the view sta highlightedGroupKey ; 200C $9205 = no group is drawn inverted either jsr drawViewport ; 200F repaint the 7x5 tactical view from the map jsr positionCursorSprite ; 2012 put the mini-map cursor sprite on the centre cell jsr setCursorShapeSquare ; 2015 sprite 0 = the hollow square cursor ; ---------------------------------------------------------------------- ; Main loop. One pass is one game frame plus one look at the joystick. ; ---------------------------------------------------------------------- L_2018: lda viewOriginCol ; 2018 remember where the player is looking... sta savedViewOriginCol ; 201A $92C3, so the console and drone screens can come back to this spot lda viewOriginRow ; 201D same for the row sta savedViewOriginRow ; 201F $92C4 lda gamePhase ; 2022 which phase? beq L_2033 ; 2024 0 = the battle proper jsr callSetupOverlayHook ; 2026 unit placement: let overlay A's '*' handler ($8658) toggle DESTROY mode lda requestedScreenKey ; 2029 zp_66: 0-3 = F1/F3/F5/F7 pressed, $FF = none beq L_2032 ; 202B F1 means 'finished placing units' jsr checkSetupUiContinue ; 202D may the placement UI keep running? bne L_2036 ; 2030 yes: carry on L_2032: rts ; 2032 back to the setup code in overlay A L_2033: jsr switchToRequestedScreen ; 2033 battle: F1/F3/F5/F7 switch screens, and that call never returns L_2036: jsr updateGameFrame ; 2036 one game frame: engine step, messages, chat, sound and the link lda joyDirection ; 2039 joystick nibble latched by the IRQ cmp #$0F ; 203B $0F = centred beq L_2042 ; 203D resting on a cell: look at what is under the cursor jmp L_20D7 ; 203F stick deflected: straight to the scroll handler ; ---------------------------------------------------------------------- ; Hovering: after 15 idle frames on one of your own units the info panel is opened. Everything below ; runs only while the stick is centred. ; ---------------------------------------------------------------------- L_2042: lda inputIdleFrames ; 2042 $90EE = frames since the last stick or button activity cmp #$0F ; 2045 15 frames, about a quarter of a second bcc L_2076 ; 2047 not hovering long enough yet jsr readCellUnderCursor ; 2049 map byte under the cursor cell (viewOriginCol+3, viewOriginRow+2) bpl L_2076 ; 204C bit 7 clear = plain terrain, no unit to describe ldy gameEndCountdown ; 204E $92C9 counts down while the end-of-game sequence runs bne L_2076 ; 2051 do not open panels while the game is ending and #$7F ; 2053 low 7 bits of an occupied cell = the unit index bit revealAllUnits ; 2055 $92FC bit 7 = beginner display, every enemy unit visible bmi L_206D ; 2058 then any unit may be inspected bit filmPlaybackMode ; 205A $0B9B bit 7 = watching a game film bmi L_206D ; 205D a film viewer may inspect both sides cmp comcenUnit ; 205F $9236 = your own COMCEN's unit index (49 or 99) beq L_206D ; 2062 your comcen can always be inspected ldy comcenCloakedFlag ; 2064 $9245 bit 7 = your own comcen is cloaked bmi L_2076 ; 2067 a cloaked comcen is off the air: no unit reports cmp #$32 ; 2069 50 = the boundary between the two sides' halves of the unit arrays D_206B: bcs L_2076 ; 206B opcode patched by patchOwnSideBranches ($7BE6) to BCC or BCS: skip the opponent's units L_206D: sta selectedUnit ; 206D $90F8 = the unit the panel will describe jsr showUnitInfoPanel ; 2070 print its stats and hold them while the cursor rests on the unit jsr consumeSpaceKey ; 2073 if SPACE closed the panel, swallow the key so nothing else acts on it ; ---------------------------------------------------------------------- ; Fire button: select one of your own units, or - during unit placement with DESTROY mode armed - send ; the self-destruct command for it. ; ---------------------------------------------------------------------- L_2076: jsr checkFirePressedInteractive; 2076 A = 0 only for a fresh click that counts (not in a film, not after game over) bne L_20D7 ; 2079 no click: fall through to scrolling jsr readCellUnderCursor ; 207B map byte under the cursor bpl L_20D7 ; 207E clicked on empty terrain and #$7F ; 2080 unit index under the cursor cmp #$32 ; 2082 50 = the side boundary again D_2084: bcs L_20D7 ; 2084 opcode patched by patchOwnSideBranches ($7BE9): ignore clicks on the opponent's units sta selectedUnit ; 2086 $90F8 = the clicked unit tay ; 2089 Y = unit index for the array lookups lda unitStunTable,y ; 208A $F960: bits 0-2 = stun counter, bits 5-7 = dug-in state and #$07 ; 208D keep the stun counter bne L_20D7 ; 208F a stunned unit takes no orders cpy comcenUnit ; 2091 is this the comcen itself? beq L_209B ; 2094 the comcen is always reachable bit comcenCloakedFlag ; 2096 $9245 bit 7 = own comcen cloaked bmi L_20D7 ; 2099 with the comcen cloaked you cannot give orders L_209B: lda gamePhase ; 209B battle or unit placement? beq L_20CC ; 209D battle: open the unit selection / options flow lda setupToggleFlag ; 209F $92F3 bit 7 = DESTROY mode, armed with '*' during placement bpl L_20CC ; 20A2 normal placement click: pick the unit up instead lda #$00 ; 20A4 clear sta setupToggleFlag ; 20A6 DESTROY is one-shot: disarm it again lda unitDisplayFlagsTable,y ; 20A9 $FB54: bit 6 = the unit is in a group, bits 2-4 = which group and #$40 ; 20AC test the 'in a group' bit beq L_20B4 ; 20AE a loner: destroy just this unit tya ; 20B0 unit index ora #$80 ; 20B1 bit 7 of the argument means 'the whole group this unit belongs to' tay ; 20B3 back into Y L_20B4: sty cmdParam0 ; 20B4 zp_73 = the command's argument byte lda #$91 ; 20B6 command $91 = self destruct jsr queueCmd2 ; 20B8 queue the 2-byte command for the engine and for the opponent lda #$0A ; 20BB back to message 10, 'SETUP UNITS. PRESS F 1 WHEN DONE.' sta persistentMessageId ; 20BD $92A1 = fallback message sta shownMessageId ; 20C0 zp_7B = the message on the status line right now lda #$3C ; 20C2 60 frames = one second sta joyRepeatTimer ; 20C4 zp_6C: no scrolling for a second sta inputLockoutTimer ; 20C6 $0B7D: the IRQ ignores stick and keys too, so one click destroys exactly one unit jmp L_20D7 ; 20C9 join the scroll handler L_20CC: jsr handleOwnUnitClick ; 20CC battle: unit or group selection, info panel, destination or options menu lda #$FF ; 20CF no key sta lastTypedKey ; 20D1 drop whatever key the menus left behind jmp L_1FFA ; 20D4 rebuild the browsing state: uiLevel 0, panel cleared, view repainted ; ---------------------------------------------------------------------- ; Joystick scrolling with auto-repeat. Reached from every path above, so a click and a scroll can ; never happen in the same pass. ; ---------------------------------------------------------------------- L_20D7: lda joyRepeatTimer ; 20D7 zp_6C, the auto-repeat delay counted down by the IRQ bne L_20ED ; 20D9 still inside the delay: do not scroll yet ldy joyDirection ; 20DB the latched joystick nibble lda #$00 ; 20DD clear sta inputLockoutTimer ; 20DF $0B7D = 0: stick and keyboard are live again lda joystickDirectionTable,y; 20E2 nibble -> direction code 0-7, or $FF when centred/impossible bmi L_20ED ; 20E5 no usable direction jsr setJoystickRepeatDelay ; 20E7 16 frames before the first repeat, 6 between repeats jsr scrollViewportByDirection; 20EA move the view one cell in that direction L_20ED: jmp L_2018 ; 20ED next pass of the main loop ; directionDeltaTable - the signed one-cell steps shared by every direction decoder in the game (the ; comcen missile screen and the drone AI read the same four bytes): index 0 = row delta for north, 1 = ; row delta for south, 2 = column delta for east, 3 = column delta for west. directionDeltaTable: .byte $FF,$01,$01,$FF ; 20F0 .... $FF,$01,$01,$FF = -1 north, +1 south, +1 east, -1 west ; ---------------------------------------------------------------------- ; scrollViewportByDirection - Turns a direction code 0-7 into a column/row step and scrolls the view ; one cell with scrollViewBy. Codes 0-3 move on one axis only; codes 4-7 read the packed byte in ; diagonalDirectionTable for both axes and stretch the auto-repeat delay by 1.5 so a diagonal does not ; cover ground faster than a straight move. In the range-limited target mode (cursorTargetMode ; positive, i.e. aiming a BOOMER) the step is only taken if the new cursor cell is still inside the ; selected unit's firing range. ; In: A = direction code 0-7; joyRepeatTimer (zp_6C); cursorTargetMode (zp_BB); selectedUnit ; ($90F8); viewOriginCol/Row; unitTerrainUnderTable ($FAF0) ; Out: view scrolled; joyRepeatTimer stretched for diagonals; fireDebounceTimer (zp_6D) = 10; ; fireButtonArmed (zp_79) = 0; zp_22/zp_24/zp_27/zp_28 scratch. Self-modifies the LDX/LDY ; operands at $2175+1 and $2177+1 ; Called from: runMapMainLoop ($20EA), runTargetCursorLoop ($2233) and the console tab loop ($6435) ; ---------------------------------------------------------------------- scrollViewportByDirection: ldy #$00 ; 20F4 default row step 0 ldx #$00 ; 20F6 default column step 0 cmp #$04 ; 20F8 codes 4-7 are the diagonals bcs L_2110 ; 20FA handle them separately cmp #$02 ; 20FC codes 2-3 are east and west bcs L_2108 ; 20FE those move on the column axis tay ; 2100 codes 0-1 (north/south) index the delta table directly lda directionDeltaTable,y ; 2101 -1 for north, +1 for south tay ; 2104 Y = row step jmp L_212E ; 2105 column step stays 0 L_2108: tax ; 2108 codes 2-3 select entries 2-3 lda directionDeltaTable,x ; 2109 +1 for east, -1 for west tax ; 210C X = column step jmp L_212E ; 210D row step stays 0 L_2110: tay ; 2110 direction code 4-7 lda diagonalDirectionTable,y; 2111 $30 NW, $20 NE, $21 SE, $31 SW = (column delta index << 4) | row delta index pha ; 2114 keep the packed byte and #$0F ; 2115 low nibble = index of the row delta tay ; 2117 into Y lda directionDeltaTable,y ; 2118 -1 or +1 tay ; 211B Y = row step pla ; 211C packed byte back lsr a ; 211D shift the high nibble down... lsr a ; 211E ... lsr a ; 211F ... lsr a ; 2120 = index of the column delta tax ; 2121 into X lda directionDeltaTable,x ; 2122 -1 or +1 tax ; 2125 X = column step lda joyRepeatTimer ; 2126 current auto-repeat delay lsr a ; 2128 half of it clc ; 2129 plain add adc joyRepeatTimer ; 212A delay * 1.5 sta joyRepeatTimer ; 212C a diagonal step covers more ground, so it repeats more slowly ; ---------------------------------------------------------------------- ; In BOOMER target mode the cursor may not leave the unit's firing range, so the step is tried out on ; paper first: squared distance from the unit to the cell we would move to, against r*(r+1) for the ; unit's range. ; ---------------------------------------------------------------------- L_212E: lda cursorTargetMode ; 212E zp_BB: 0 = move order, >0 = range-limited target, <0 = free cell pick beq L_2179 ; 2130 move order: scroll freely bmi L_2179 ; 2132 free cell pick: scroll freely stx L_2175+1 ; 2134 self-modifying: stash the column step in the LDX # operand at $2175+1 sty L_2177+1 ; 2137 and the row step in the LDY # operand at $2177+1 txa ; 213A column step clc ; 213B plain add adc viewOriginCol ; 213C left edge of the view clc ; 213E plain add adc #$03 ; 213F +3 = the cursor cell after the move sta cellColA ; 2141 zp_22 = column of point A for distanceSquared tya ; 2143 row step clc ; 2144 plain add adc viewOriginRow ; 2145 top edge of the view clc ; 2147 plain add adc #$02 ; 2148 +2 = the cursor row after the move sta cellRowA ; 214A zp_24 = row of point A ldy selectedUnit ; 214C the unit that is doing the shooting lda unitColTable,y ; 214F $F640+unit sta cellColB ; 2152 zp_27 = column of point B (the unit) lda unitRowTable,y ; 2154 $F6A4+unit sta cellRowB ; 2157 zp_28 = row of point B jsr distanceSquared ; 2159 A = dx*dx + dy*dy, each delta capped at 15 and the sum at $FF sta cellColA ; 215C reuse zp_22 to hold that squared distance ldx selectedUnit ; 215E the firing unit again ldy unitTerrainUnderTable,x ; 2161 $FAF0 = the terrain code hidden underneath the unit ldx D_077E,y ; 2164 $077E indexed by the raw cell value = terrain class 0-8 lda terrainSightBonusTable,x; 2167 $35BB = 0,0,0,1,1,2,2,0 extra cells of reach for standing high clc ; 216A plain add adc #$06 ; 216B a BOOMER shoots 6 cells, plus the terrain bonus tax ; 216D range 6-8 as a table index lda rangeDistanceSqTable,x ; 216E $35B0 = r*(r+1), the squared distance a target has to stay under cmp cellColA ; 2171 compare it with the distance to the cell we would move to bcc L_217C ; 2173 out of range: leave the cursor where it is L_2175: ldx #$FF ; 2175 column step, operand patched at $2134 L_2177: ldy #$FF ; 2177 row step, operand patched at $2137 L_2179: jsr scrollViewBy ; 2179 scroll the view, and the cursor with it, by one cell L_217C: lda #$0A ; 217C 10 frames sta fireDebounceTimer ; 217E zp_6D: ignore the fire button briefly after a scroll lda #$00 ; 2180 clear sta fireButtonArmed ; 2182 zp_79: the button must be seen released again before the next click counts rts ; 2184 the caller loops back and reads the stick again ; ---------------------------------------------------------------------- ; runTargetCursorLoop - Modal loop that lets the player drive the cursor to a cell and then acts on ; it. cursorTargetMode picks the flavour: 0 = choose a destination for the selected unit, or for its ; whole group when uiLevel is 2 (the formation ghost is built into sprite 0); positive = choose a ; target for a BOOMER (cross cursor, movement limited to its range); negative = only report the cell ; the player stops on in pickedCellCol/pickedCellRow. Each pass runs one game frame, previews the ; route after 90 idle frames, scrolls on joystick, and on a fire click issues the order. The normal ; exits scroll the view back onto the unit and restore the plain battlefield cursor. ; In: cursorTargetMode (zp_BB), selectedUnit ($90F8), uiLevel ($9209), gamePhase bit 6, ; stepDelayTimer ($90FC), inputIdleFrames ($90EE), joystick and fire button ; Out: an order queued by orderSelectedUnitToCursor / sendGroupMoveToCursor / orderAttackUnitAtCursor, ; or pickedCellCol/Row ($91D8/$91D9) for a free pick; cursorTargetMode = 0, groupBoxActive = 0, ; sprites 0 and 1 blanked and un-expanded, sprite 2 back to the box cursor ; Called from: the unit destination flow ($4BE1, $5F64, $650A) and overlay A's setup screen ($83FF) ; ---------------------------------------------------------------------- runTargetCursorLoop: lda #$FF ; 2185 no unit sta infoPanelUnit ; 2187 $91D7: nothing is being described while the target cursor is up lda #$00 ; 218A sprite 0 sta activeSpriteIndex ; 218C $91CC = the sprite that carries the cursor shape lda cursorTargetMode ; 218F zp_BB selects the flavour of this loop beq L_219B ; 2191 0 = choosing a destination bmi L_21A8 ; 2193 negative = free cell pick: plain block cursor jsr setCursorShapeCross ; 2195 positive = attack target: thick cross into sprite 0 jmp L_21AE ; 2198 sprite 2 is left as the caller set it L_219B: lda uiLevel ; 219B $9209: 0 browsing, 1 single unit, 2 whole group cmp #$02 ; 219E 2 or more = a group is selected bcc L_21A8 ; 21A0 single unit: plain block cursor jsr buildGroupFormationSprite; 21A2 group move: sprite 0 shows every member as a dot, and the box offsets are built jmp L_21AB ; 21A5 on to the crosshair L_21A8: jsr setCursorShapeBlock ; 21A8 small solid block into sprite $91CC L_21AB: jsr showCrosshairSprite ; 21AB sprite 2 = the diamond crosshair over the centre cell ; ---------------------------------------------------------------------- ; Cursor loop: one pass per frame until the player clicks, the unit dies or the UI is aborted. ; ---------------------------------------------------------------------- L_21AE: bit gamePhase ; 21AE zp_B1 bit 6 marks the phases in which the engine must not run bvc L_21B8 ; 21B0 normal game: run a whole frame jsr serviceCommandExchange ; 21B2 setup/film phase: only keep the lock-step link serviced jmp L_21C0 ; 21B5 skip the frame update L_21B8: lda stepDelayTimer ; 21B8 $90FC counts down to the next engine step bne L_21C0 ; 21BB one frame update per engine step is enough jsr updateGameFrame ; 21BD engine, messages, sound, link L_21C0: lda inputIdleFrames ; 21C0 $90EE = frames since the last input cmp #$5A ; 21C3 90 frames = one and a half seconds bcc L_21CE ; 21C5 player is still busy lda cursorTargetMode ; 21C7 only the plain move cursor previews a route bne L_21CE ; 21C9 target or free-pick cursor: no preview jsr previewPathToCursor ; 21CB crawl the marker sprite from the unit to the cursor cell L_21CE: ldy selectedUnit ; 21CE the unit being ordered lda unitTypeTable,y ; 21D1 $F76C: bit 7 set = the unit has been destroyed bpl L_21D9 ; 21D4 still alive: carry on jmp L_2239 ; 21D6 it died while we were aiming: clean up and re-centre L_21D9: jsr checkUiContinue ; 21D9 SPACE, a function key or the end of the game abort every modal UI bne L_21E1 ; 21DC A = 1: keep going jmp L_223C ; 21DE aborted: clean up without scrolling back L_21E1: lda joyDirection ; 21E1 joystick nibble cmp #$0F ; 21E3 $0F = centred bne L_2220 ; 21E5 deflected: scroll the cursor jsr checkFirePressed ; 21E7 fresh press of the fire button? bne L_2220 ; 21EA no click: fall through to the scroll handler lda cursorTargetMode ; 21EC which kind of pick is this? beq L_220B ; 21EE 0 = a move order bmi L_21F8 ; 21F0 negative = the caller only wants the cell jsr orderAttackUnitAtCursor ; 21F2 positive = attack: queue command $87 against the unit under the cursor jmp L_2239 ; 21F5 order sent: clean up L_21F8: lda viewOriginCol ; 21F8 left edge of the view clc ; 21FA plain add adc #$03 ; 21FB +3 = the cursor column sta pickedCellCol ; 21FD $91D8 = the cell the caller asked for lda viewOriginRow ; 2200 top edge of the view clc ; 2202 plain add adc #$02 ; 2203 +2 = the cursor row sta pickedCellRow ; 2205 $91D9 jmp L_2263 ; 2208 return at once and leave the sprites to the caller L_220B: lda uiLevel ; 220B $9209: single unit or whole group? cmp #$02 ; 220E 2 = group bcs L_2218 ; 2210 group move jsr orderSelectedUnitToCursor; 2212 single unit: queue command $80 unit,column,row jmp L_2239 ; 2215 order sent: clean up L_2218: jsr sendGroupMoveToCursor ; 2218 queue command $81, move the whole group to the cursor cell bcs L_2236 ; 221B sendGroupMoveToCursor always returns C=0, so this 'stay in the loop' branch is dead code jmp L_2239 ; 221D order sent: clean up L_2220: lda joyRepeatTimer ; 2220 zp_6C auto-repeat delay bne L_2236 ; 2222 still inside the delay ldy joyDirection ; 2224 joystick nibble lda #$00 ; 2226 clear sta inputLockoutTimer ; 2228 $0B7D = 0: input is live lda joystickDirectionTable,y; 222B nibble -> direction code 0-7, $FF when centred bmi L_2236 ; 222E no usable direction jsr setJoystickRepeatDelay ; 2230 16 frames for the first step, 6 for the repeats jsr scrollViewportByDirection; 2233 move the cursor one cell (range-checked in target mode) L_2236: jmp L_21AE ; 2236 next pass of the cursor loop ; ---------------------------------------------------------------------- ; Leaving the target cursor: put the sprites and the cursor mode back the way the battlefield loop ; expects them. $2239 re-centres the view on the unit first; $223C is entered directly when the UI ; was aborted. ; ---------------------------------------------------------------------- L_2239: jsr recenterViewportOnUnit ; 2239 scroll the view back until the unit is under the cursor again L_223C: lda #$00 ; 223C clear sta groupBoxActive ; 223E $920A: the formation box is no longer being dragged sta cursorTargetMode ; 2241 zp_BB back to the plain move cursor lda #$01 ; 2243 sprite 1 jsr clearSpriteShape ; 2245 blank its 64-byte shape (the path marker) lda #$00 ; 2248 sprite 0 jsr clearSpriteShape ; 224A blank its shape (cursor or formation ghost) lda spriteXExpandShadow ; 224D $9023 = shadow of VIC_SPR_EXP_X and #$FC ; 2250 clear bits 0-1: sprites 0 and 1 back to normal width sta spriteXExpandShadow ; 2252 the IRQ copies the shadow to the VIC lda spriteYExpandShadow ; 2255 $9024 = shadow of VIC_SPR_EXP_Y and #$FC ; 2258 sprites 0 and 1 back to normal height sta spriteYExpandShadow ; 225A write the shadow back jsr resetPathMarkerToUnit ; 225D park the path marker back on the unit, ready for five more passes jsr showCursorBoxSprite ; 2260 sprite 2 back to the normal centre-cell box L_2263: rts ; 2263 back to the menu or setup code that asked for a cell ; ---------------------------------------------------------------------- ; previewPathToCursor - Idle-time animation: while the player leaves the controls alone with a move ; cursor up, the marker sprite crawls along the straight line from the selected unit to the cursor ; cell so the intended route can be seen on the strategic mini-map. The target is re-read every pass ; (the view may still scroll) and the animation stops as soon as checkUserInterrupt reports stick, ; button, key or link activity. ; In: selectedUnit ($90F8), viewOriginCol/Row, unit arrays $F640/$F6A4/$F76C ; Out: lineEndCol/lineEndRow (zp_95/zp_94), marker state $24E1-$24E3, sprite 1 animated and finally ; hidden ; Called from: runTargetCursorLoop ($21CB) after 90 idle frames ; ---------------------------------------------------------------------- previewPathToCursor: ldy selectedUnit ; 2264 the unit being ordered lda viewOriginCol ; 2267 left edge of the view clc ; 2269 plain add adc #$03 ; 226A +3 = cursor column cmp unitColTable,y ; 226C $F640+unit bne L_227B ; 226F different column: there is a route to show lda viewOriginRow ; 2271 top edge of the view clc ; 2273 plain add adc #$02 ; 2274 +2 = cursor row cmp unitRowTable,y ; 2276 $F6A4+unit beq L_229F ; 2279 cursor sits on the unit itself: nothing to preview L_227B: jsr resetPathMarker ; 227B put the marker on the unit and give it five passes L_227E: ldy selectedUnit ; 227E the unit being ordered lda unitTypeTable,y ; 2281 $F76C bit 7 = destroyed bmi L_229F ; 2284 it died: stop the animation jsr updateGameFrame ; 2286 keep the game and the link running while we animate lda #$03 ; 2289 3 clc ; 228B plain add adc viewOriginCol ; 228C left edge of the view sta lineEndCol ; 228E zp_95 = cursor column, re-read every pass in case the view scrolled lda #$02 ; 2290 2 clc ; 2292 plain add adc viewOriginRow ; 2293 top edge of the view sta lineEndRow ; 2295 zp_94 = cursor row jsr animatePathMarkerStep ; 2297 advance the marker one cell every 4 frames jsr checkUserInterrupt ; 229A C=1 as soon as the player or the link wants attention bcc L_227E ; 229D still idle: keep animating L_229F: jmp hideSprite1 ; 229F turn the marker sprite off and return ; ---------------------------------------------------------------------- ; showUnitInfoPanel - Fills the info panel under the tactical view (text window rows 12-20, columns ; 25-38) with the description of unit selectedUnit: the type name and unit number, the GROUP / ENERGY ; / WEAPON / ACTION labels, the values printed by printUnitStatusLines and the three sprites that make ; up the unit picture. It then holds that display while the cursor rests on the unit, reprinting the ; values whenever the unit is being stepped or shot at or a lock-step exchange has been applied, and ; animating the destination marker. Moving off the unit, an interrupt or the end of the placement ; phase clears the panel and the picture sprites again. ; In: selectedUnit ($90F8), uiLevel ($9209), the unit arrays, stepDelayTimer ($90FC), currentUnit ; ($90FA), fireTargetUnit ($91F0), exchangeAppliedFlag ($920E), highlightedGroupKey ($9205), ; cursorTargetMode, gamePhase ; Out: panel drawn and cleared again; infoPanelUnit ($91D7) = the unit while it is shown and $FF ; afterwards; textCentreWidth = 0; sprites 5-7 disabled; text window reset to full screen. ; Self-modifies the LDX # operand at $22EF+1 ; Called from: runMapMainLoop ($2070, hovering), handleOwnUnitClick ($5E4C) and the console unit list ; ($63F5) ; ---------------------------------------------------------------------- showUnitInfoPanel: lda #$00 ; 22A2 clear sta textCentreWidth ; 22A4 $90EF = 0: print flush left, no centring sta textFillGlyph ; 22A7 zp_3E: blanks are glyph 0 sta rawGlyphMode ; 22A9 zp_43: characters go through the normal ASCII translation ldy uiLevel ; 22AB $9209: 0 browsing, 1 unit selected, 2 group selected lda uiLevelTextColourTable,y; 22AE $5E14 = 9 / 5 / 4: the panel text colour for that level sta textColour ; 22B1 zp_3D = colour byte written for every character cell jsr clearInfoPanel ; 22B3 open and blank the info panel window (rows 12-20, columns 25-38) lda #$02 ; 22B6 column 2 sta textCursorCol ; 22B8 zp_3F: start two characters in from the left edge of the panel jsr updateCursorPointers ; 22BA recompute the bitmap and colour pointers for that cell ldx selectedUnit ; 22BD the unit on display lda unitTypeTable,x ; 22C0 $F76C: type in the low bits, bit 7 = dead and #$07 ; 22C3 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN tax ; 22C5 X = table index stx L_22EF+1 ; 22C6 self-modifying: the LDX # at $22EF+1 that passes the type to the picture routine lda #$64 ; 22C9 low byte of $C964 ldy #$C9 ; 22CB high byte: the five 8-byte unit type names in the $C000 overlay jsr printTableEntry ; 22CD print entry X of that table lda #$A0 ; 22D0 space jsr printChar ; 22D2 separate the name from the number lda selectedUnit ; 22D5 the unit index 0-99 ldy #$00 ; 22D8 middle byte of the 24-bit value = 0 (zp_7E holds the high byte) jsr printDecimal ; 22DA print it in decimal without leading zeros jsr printNewline ; 22DD next panel row lda #$E9 ; 22E0 low byte of $CAE9 ldy #$CA ; 22E2 high byte: the GROUP / ENERGY / WEAPON / ACTION label block jsr printString ; 22E4 print the four labels jsr printUnitStatusLines ; 22E7 fill in the value column beside them lda unitPictureHiTable ; 22EA $CD00 = first high byte of the unit picture address table bpl L_22F4 ; 22ED not a high address: the $C000 overlay holds no pictures, skip them L_22EF: ldx #$FF ; 22EF unit type, operand patched at $22C6 jsr showUnitPictureSprites ; 22F1 unpack the picture and show it as sprites 5, 6 and 7 L_22F4: jsr hideSprite1 ; 22F4 the path marker must not sit over the panel ldx selectedUnit ; 22F7 the unit on display lda unitDestColTable,x ; 22FA $F7D0: bit 7 set = the unit has no destination bmi L_2330 ; 22FD nowhere to go: skip the marker set-up jsr resetPathMarker ; 22FF park the marker on the unit, ready to walk toward the destination ; ---------------------------------------------------------------------- ; Panel loop: hold the display while the cursor stays on this unit, reprinting the values only when ; something can actually have changed. ; ---------------------------------------------------------------------- L_2302: lda stepDelayTimer ; 2302 $90FC: is an engine step due? bne L_2333 ; 2305 no: only animate the marker and test for input lda selectedUnit ; 2307 the unit on display sta infoPanelUnit ; 230A $91D7 tells the rest of the game which unit the panel is showing jsr updateGameFrame ; 230D one full game frame jsr readCellUnderCursor ; 2310 map byte under the cursor bpl L_234D ; 2313 cursor moved onto empty terrain: close the panel and #$7F ; 2315 unit index under the cursor cmp selectedUnit ; 2317 still the same unit? bne L_234D ; 231A a different unit: close the panel cmp currentUnit ; 231C $90FA = the unit the engine is stepping right now beq L_2330 ; 231F it is moving: reprint its status cmp fireTargetUnit ; 2321 $91F0 = the unit currently being shot at beq L_2330 ; 2324 it is under fire: reprint its status lda exchangeAppliedFlag ; 2326 $920E bit 7 = a lock-step exchange was applied (one turn tick) bpl L_2333 ; 2329 nothing has changed since the last print lda #$00 ; 232B clear sta exchangeAppliedFlag ; 232D consume the tick L_2330: jsr printUnitStatusLines ; 2330 reprint group, energy, weapon and action L_2333: lda cursorTargetMode ; 2333 zp_BB < 0 = free cell pick bmi L_233C ; 2335 then always show where the unit is heading lda highlightedGroupKey ; 2337 $9205 = $FF when no group is highlighted bpl L_233F ; 233A a group is highlighted: no individual destination marker L_233C: jsr animatePathMarkerToDestination; 233C crawl the marker from the unit toward its destination L_233F: jsr checkUserInterrupt ; 233F C=1 on stick, button, key or link activity bcs L_234D ; 2342 player wants something: close the panel lda gamePhase ; 2344 battle or unit placement? beq L_2302 ; 2346 battle: keep the panel up jsr callSetupOverlayHook ; 2348 placement: let overlay A see the '*' key bcc L_2302 ; 234B C=0, nothing happened: keep the panel up ; ---------------------------------------------------------------------- ; Leaving the panel: wipe it, drop the marker and the three unit-picture sprites. ; ---------------------------------------------------------------------- L_234D: lda #$FF ; 234D no unit sta infoPanelUnit ; 234F $91D7 = the panel is empty again lda #$00 ; 2352 colour 0 sta textColour ; 2354 zp_3D: clear the panel to black jsr clearInfoPanel ; 2356 wipe the panel window jsr hideSprite1 ; 2359 path marker sprite off lda spriteEnableShadow ; 235C $9022 = shadow of VIC_SPR_ENA and #$1F ; 235F clear bits 5-7: the three unit-picture sprites go away sta spriteEnableShadow ; 2361 the IRQ writes the shadow to the VIC jmp setFullScreenWindow ; 2364 text window back to the whole screen, and return through it ; ---------------------------------------------------------------------- ; checkUserInterrupt - The 'may this passive display keep running?' test used by the idle loops of the ; info panel and the path preview. It returns C=0 only while the text engine agrees the UI may ; continue (checkUiContinue $C451), no function key is waiting to switch screens, the joystick is ; centred and the fire button is not held. Anything else returns C=1 and freezes stick and keyboard ; for 30 frames so the same push is not consumed twice. ; In: requestedScreenKey zp_66 ($FF = no function key), joyDirection zp_5D ($0F = centred), joyFireUp ; zp_5F / fireButtonArmed zp_79 through isFireHeld, plus everything checkUiContinue looks at ; (filmPlaybackMode, gameEndCountdown, pauseState, gamePhase, lastTypedKey) ; Out: C=0 keep going / C=1 stop; on C=1 inputLockoutTimer $0B7D = $1E (30 frames) ; Called from: the path-preview loop at $229A and the info-panel idle loop at $233F ; ---------------------------------------------------------------------- checkUserInterrupt: jsr checkUiContinue ; 2367 ask the text engine whether a modal display may still run (A=1 = yes) beq L_237D ; 236A branch if it says stop: game ending, paused, SPACE typed or a screen change pending lda requestedScreenKey ; 236C function key latched by the IRQ: 0-3 = F1/F3/F5/F7, $FF = none pending bpl L_237D ; 236E branch if a function key is waiting to switch screens lda joyDirection ; 2370 joystick 2 direction nibble latched by the IRQ (active low) cmp #$0F ; 2372 $0F = no direction pulled low = stick centred bne L_237D ; 2374 branch if the player has pushed the stick jsr isFireHeld ; 2376 level test of the fire button: A=0 while it is held down beq L_237D ; 2379 branch if the button is down clc ; 237B nothing interrupted: C=0 tells the caller to carry on rts ; 237C back to the idle loop ; ---------------------------------------------------------------------- ; Something wants attention: stop the display and swallow the input for half a second so the menu the ; caller returns to does not immediately act on the same stick push or key. ; ---------------------------------------------------------------------- L_237D: lda #$1E ; 237D 30 frames = about half a second sta inputLockoutTimer ; 237F the IRQ ignores stick and keyboard while this counts down sec ; 2382 C=1 = the caller must tear its display down rts ; 2383 return to the caller's loop, which exits on C=1 ; ---------------------------------------------------------------------- ; printUnitStatusLines - Fills in the value column of the unit info panel for selectedUnit ($90F8): ; the group name on row 13, the energy percentage on row 17, the weapon state on row 18, the action on ; row 19 and one flag word (BLITZ / CLOAKED / DUG IN) on row 20, then adds up to two '*' zone markers ; in front of the unit name on row 12. The labels GROUP:/ENERGY:/WEAPON:/ACTION: themselves are ; printed once by showUnitInfoPanel from the string at $CAE9. ; In: selectedUnit $90F8; the unit arrays unitDisplayFlagsTable $FB54 (bit 0 fired, bit 1 pinned, ; bits 2-4 group id, bit 6 grouped), unitEnergyTable $F9C4, unitTypeTable $F76C, unitStunTable ; $F960, unitPathColTable $F898 (waypoint column + cloak bits), unitPathRowTable $F8FC (waypoint ; row + blitz bits), unitDestColTable $F7D0, unitDestRowTable $F834, unitMovePointsTable $FA28; ; currentUnit $90FA, gameClock $91CB, markerBlinkTimer zp_6F ; Out: text drawn into the bitmap; textWindowLeft zp_3B / textWindowWidth zp_3C / textCursorCol zp_3F ; / textCursorRow zp_40 left pointing into the panel, markerBlinkTimer = 20 when FIRED starts, ; scratch18/scratch19 clobbered ; Called from: showUnitInfoPanel, once when the panel is opened ($22E7) and again on every refresh ; while the unit is in combat ($2330) ; Set up the narrow value column of the info panel. The panel window itself is columns 25-38; the ; values are printed in columns 31/32 so they line up behind the labels. ; ---------------------------------------------------------------------- printUnitStatusLines: lda #$20 ; 2384 column 32 = the value column of the panel sta textWindowLeft ; 2386 text engine window left edge lda #$09 ; 2388 9 characters wide, i.e. out to the right frame sta textWindowWidth ; 238A text engine window width / wrap point dec textWindowLeft ; 238C the group name starts one column further left (column 31) lda #$00 ; 238E first character of the window sta textCursorCol ; 2390 cursor column, relative to textWindowLeft lda #$0D ; 2392 screen row 13 = the GROUP: line jsr setCursorRow ; 2394 move the text cursor there (recomputes the bitmap pointers) ; ---------------------------------------------------------------------- ; GROUP: line - the group name, or ' - ' when the unit is not in a group. ; ---------------------------------------------------------------------- ldx #$00 ; 2397 string index 0 = the ' - ' filler at $C9A4 ldy selectedUnit ; 2399 the unit the info panel is showing lda unitDisplayFlagsTable,y ; 239C its group/status flags ($FB54) and #$40 ; 239F bit 6 = the unit belongs to a group bne L_23A9 ; 23A1 branch if it is a group member lda #$A4 ; 23A3 low byte of the ' - ' string table entry at $C9A4 ldy #$C9 ; 23A5 high byte; entry 0 of that 8-byte-per-entry table is ' - ' bne L_23B5 ; 23A7 always taken L_23A9: lda unitDisplayFlagsTable,y ; 23A9 re-read the group flags and #$1C ; 23AC bits 2-4 hold the group id lsr a ; 23AE shift the id down... lsr a ; 23AF ...to 0-6 = ALPHA, BAKER, CHARLIE, DELTA, EASY, FOXTROT, GROVER tax ; 23B0 index into the name table lda #$2C ; 23B1 low byte of groupNameTable $C92C (7 names of 8 bytes) ldy #$C9 ; 23B3 high byte L_23B5: jsr printTableEntry ; 23B5 print entry X of the 8-byte-per-entry string table at A/Y ; ---------------------------------------------------------------------- ; ENERGY: line - the energy byte is 0-50 and is shown as a percentage (x2). ; ---------------------------------------------------------------------- inc textWindowLeft ; 23B8 value column back to 32 for all the remaining lines inc textCursorRow ; 23BA the GROUP: label is followed by three blank rows... inc textCursorRow ; 23BC ...row 15... inc textCursorRow ; 23BE ...row 16 jsr printNewline ; 23C0 newline lands on row 17 = the ENERGY: line, cursor at column 0 ldx selectedUnit ; 23C3 the selected unit again lda unitEnergyTable,x ; 23C6 energy byte ($F9C4) and #$3F ; 23C9 bits 0-5 = energy 0-50; bits 6-7 are the 'spotted by the enemy' flags asl a ; 23CB x2 gives 0-100 percent ldy #$00 ; 23CC high byte of the number to print = 0 jsr printDecimal ; 23CE print it as decimal digits lda #$A5 ; 23D1 '%' jsr printChar ; 23D3 print it lda #$A0 ; 23D6 trailing space, wipes the third digit of a previous 100% jsr printChar ; 23D8 print it ; ---------------------------------------------------------------------- ; WEAPON: line - READY / RELOAD / FIRED, or ' - ' for a spy, a comcen or a cloaked unit, or STUN'D ; while the unit is stunned. Entries are 8 bytes apart in the table at $C98C: 0 READY, 1 RELOAD, 2 ; FIRED, 3 ' - ', 4 REPAIR, 5 MOVE, 6 PINNED, 7 STUN'D. ; ---------------------------------------------------------------------- jsr printNewline ; 23DB newline to row 18 = the WEAPON: line ldx selectedUnit ; 23DE selected unit lda unitPathColTable,x ; 23E1 waypoint column + cloak bits ($F898; bit 7 = cloaked) sta scratch18 ; 23E4 keep the cloak byte for the test below lda unitTypeTable,x ; 23E6 type byte ($F76C) sta scratch19 ; 23E9 keep it as well lda unitStunTable,x ; 23EB stun / dig-in byte ($F960) and #$0F ; 23EE low nybble = clicks of stun left beq L_23F6 ; 23F0 branch if the unit is not stunned ldx #$07 ; 23F2 table entry 7 = STUN'D bne L_2428 ; 23F4 always taken L_23F6: ldx #$03 ; 23F6 table entry 3 = ' - ' (no weapon state to show) lda scratch19 ; 23F8 unit type byte and #$07 ; 23FA bits 0-2 = 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN cmp #$03 ; 23FC SPY and COMCEN are types 3 and 4... bcs L_2428 ; 23FE ...and carry no weapon, so leave ' - ' lda markerBlinkTimer ; 2400 the 20-frame hold started when FIRED was printed (shared blink timer zp_6F) bne L_2441 ; 2402 while it runs skip the field entirely so FIRED stays on screen lda scratch18 ; 2404 the cloak byte saved above bmi L_2428 ; 2406 a cloaked unit cannot fire: show ' - ' ldx #$00 ; 2408 table entry 0 = READY ldy selectedUnit ; 240A selected unit lda unitDisplayFlagsTable,y ; 240D status flags and #$01 ; 2410 bit 0 = the unit has fired and is reloading beq L_2428 ; 2412 not fired -> READY inx ; 2414 table entry 1 = RELOAD cpy currentUnit ; 2415 is this the unit the simulation is stepping right now? bne L_2428 ; 2418 no -> just RELOAD tya ; 241A unit index eor gameClock ; 241B mix it with the game clock... and #$01 ; 241E ...and keep bit 0, so only every other click flashes bne L_2428 ; 2420 off click -> RELOAD lda #$14 ; 2422 20 frames sta markerBlinkTimer ; 2424 hold the field so FIRED stays readable ldx #$02 ; 2426 table entry 2 = FIRED L_2428: lda #$8C ; 2428 low byte of the weapon-word table $C98C ldy #$C9 ; 242A high byte jsr printTableEntry ; 242C print entry X ; ---------------------------------------------------------------------- ; A boomer with a locked target gets a '*' after the weapon word. ; ---------------------------------------------------------------------- ldx selectedUnit ; 242F selected unit jsr getBoomerTarget ; 2432 boomers return their target byte, other types a small positive value tay ; 2435 into Y for the test lda #$A0 ; 2436 space = nothing to mark cpy #$80 ; 2438 bit 7 of the target byte = a target is locked bcc L_243E ; 243A no target -> print the space lda #$AA ; 243C '*' = this boomer has a target locked L_243E: jsr printChar ; 243E print the marker ; ---------------------------------------------------------------------- ; ACTION: line - ' - ' / REPAIR / MOVE / PINNED / STUN'D / DIGGIN, entries 0-5 of the 8-byte-per-entry ; table at $C9A4. ; ---------------------------------------------------------------------- L_2441: jsr printNewline ; 2441 newline to row 19 = the ACTION: line ldx #$02 ; 2444 table entry 2 = MOVE ldy selectedUnit ; 2446 selected unit lda unitDestColTable,y ; 2449 ordered destination column ($F7D0); bit 7 = no destination bmi L_2464 ; 244C branch if the unit has nowhere to go lda unitDisplayFlagsTable,y ; 244E status flags and #$02 ; 2451 bit 1 = the unit was hit and is pinned down beq L_245C ; 2453 not pinned -> just check the movement points lda unitMovePointsTable,y ; 2455 movement points ($FA28) cmp #$0A ; 2458 fewer than 10 points left? bcc L_2461 ; 245A yes -> PINNED rather than MOVE L_245C: lda unitMovePointsTable,y ; 245C movement points again bpl L_246C ; 245F still positive -> MOVE L_2461: inx ; 2461 table entry 3 = PINNED (out of movement) bne L_246C ; 2462 always taken L_2464: ldx #$00 ; 2464 table entry 0 = ' - ' (nothing to do) lda unitDestRowTable,y ; 2466 destination row ($F834); bit 7 = the unit is regaining energy bpl L_246C ; 2469 not repairing -> leave ' - ' inx ; 246B table entry 1 = REPAIR L_246C: lda unitStunTable,y ; 246C stun / dig-in byte and #$0F ; 246F low nybble = stun clicks left beq L_2477 ; 2471 not stunned ldx #$04 ; 2473 table entry 4 = STUN'D bne L_2482 ; 2475 always taken L_2477: lda unitStunTable,y ; 2477 stun / dig-in byte again bmi L_2482 ; 247A bit 7 = already dug in, that is a flag word not an action and #$E0 ; 247C bits 5-7 also count the dig-in progress beq L_2482 ; 247E not digging in ldx #$05 ; 2480 table entry 5 = DIGGIN L_2482: lda #$A4 ; 2482 low byte of the action-word table $C9A4 ldy #$C9 ; 2484 high byte jsr printTableEntry ; 2486 print entry X ; ---------------------------------------------------------------------- ; Flag line - at most one of BLITZ, CLOAKED or DUG IN, printed further left (column 28) because the ; words are longer than the value column. ; ---------------------------------------------------------------------- lda #$1C ; 2489 column 28 for the wider flag word sta textWindowLeft ; 248B text window left edge jsr printNewline ; 248D newline to row 20 ldx selectedUnit ; 2490 selected unit lda unitPathRowTable,x ; 2493 waypoint row + blitz bits ($F8FC) bpl L_249E ; 2496 bit 7 clear = blitz is off lda #$82 ; 2498 low byte of 'BLITZ' at $CB82 ldy #$CB ; 249A high byte bne L_24B2 ; 249C always taken L_249E: lda unitPathColTable,x ; 249E waypoint column + cloak bits ($F898) bpl L_24A9 ; 24A1 bit 7 clear = not cloaked lda #$7B ; 24A3 low byte of 'CLOAKED' at $CB7B ldy #$CB ; 24A5 high byte bne L_24B2 ; 24A7 always taken L_24A9: lda unitStunTable,x ; 24A9 stun / dig-in byte bpl L_24B5 ; 24AC bit 7 clear = not dug in: print no flag word at all lda #$87 ; 24AE low byte of 'DUG IN' at $CB87 ldy #$CB ; 24B0 high byte L_24B2: jsr printString ; 24B2 print the bit-7 terminated string at A/Y L_24B5: jsr bootWaitClkAck ; 24B5 wipe whatever the previously shown unit left below this line (also textEngineC000:clearToWindowBottom) ; ---------------------------------------------------------------------- ; Zone markers - unitTypeTable bits 3-4 record which half of the enemy ground this unit has already ; scored (bit 3 = the near zone, columns 10-19; bit 4 = the far end zone, columns 0-9). One '*' is ; printed in front of the unit name for either, a second one for the far zone. ; ---------------------------------------------------------------------- lda #$20 ; 24B8 value column back to 32 sta textWindowLeft ; 24BA text window left edge ldx selectedUnit ; 24BC selected unit lda unitTypeTable,x ; 24BF type byte and #$18 ; 24C2 bits 3-4 = zones this unit has already scored beq L_24E0 ; 24C4 no marker to print pha ; 24C6 keep the two bits over the window change jsr setInfoPanelWindow ; 24C7 back to the full panel window (columns 25-38); leaves A = $0C ldy #$00 ; 24CA column 0 of the window = screen column 25 sty textCursorCol ; 24CC cursor column jsr setCursorRow ; 24CE A is still the $0C left by setInfoPanelWindow, so this lands on row 12 = the unit name line lda #$AA ; 24D1 '*' jsr printChar ; 24D3 print it in front of the name pla ; 24D6 get the zone bits back and #$10 ; 24D7 bit 4 = the far, double-value end zone beq L_24E0 ; 24D9 near zone only -> one marker lda #$AA ; 24DB a second '*' jsr printChar ; 24DD print it L_24E0: rts ; 24E0 done: the caller redraws the sprites ; pathMarkerState - three RAM bytes living inside the code, the whole state of the animated path ; marker (sprite 1) that walks from the selected unit to its ordered destination on the mini-map: ; passes still to run, and the map cell the marker currently occupies. pathMarkerState: .byte $05 ; 24E1 . passes still to walk before the marker jumps back to the unit (reloaded with 5) pathMarkerCol: .byte $00 ; 24E2 map column of the marker pathMarkerRow: .byte $00 ; 24E3 map row of the marker ; ---------------------------------------------------------------------- ; animatePathMarkerToDestination - Sets the line-stepper target to the ordered destination of ; selectedUnit and falls into animatePathMarkerStep, so the mini-map marker crawls from the unit ; towards where it has been sent. A unit with no destination just gets sprite 1 switched off. ; In: selectedUnit $90F8, unitDestColTable $F7D0 (bit 7 set = no destination), unitDestRowTable $F834 ; Out: lineEndCol zp_95 / lineEndRow zp_94 = the destination cell, then everything ; animatePathMarkerStep does; sprite 1 hidden when there is no destination ; Called from: the info-panel idle loop ($233C) and the unit menu loops at $6065 and $6214 ; ---------------------------------------------------------------------- animatePathMarkerToDestination: ldx selectedUnit ; 24E4 the unit the panel is showing lda unitDestColTable,x ; 24E7 its ordered destination column; bit 7 = no destination bmi L_253C ; 24EA nothing ordered -> just hide the marker sta lineEndCol ; 24EC line stepper target column lda unitDestRowTable,x ; 24EE destination row sta lineEndRow ; 24F1 line stepper target row ; ---------------------------------------------------------------------- ; animatePathMarkerStep - Advances the mini-map path marker by one cell every 4 frames, but only after ; the player has been idle for 90 frames. Each step walks one cell of the Bresenham line from ; pathMarkerCol/Row towards lineEndCol/Row and moves sprite 1 there; at the end of the line the pass ; counter is decremented and the sprite is blinked, and when the counter runs past zero ; resetPathMarker puts the marker back on the unit. ; In: inputIdleFrames $90EE, joyRepeatTimer zp_6C (used here as the step delay), pathMarkerState ; $24E1, pathMarkerCol/Row $24E2/$24E3, target lineEndCol zp_95 / lineEndRow zp_94 ; Out: pathMarkerCol/Row advanced, sprite 1 position shadows $9011/$9019 and enable bit 1 of $9022, ; joyRepeatTimer = 4, line stepper state zp_8C-zp_97 ; Called from: the path-preview loop at $2297, and by fall-through from animatePathMarkerToDestination ; ---------------------------------------------------------------------- animatePathMarkerStep: lda inputIdleFrames ; 24F3 frames since the player last moved the stick or pressed a key cmp #$5A ; 24F6 90 frames = 1.5 seconds of doing nothing bcs L_24FD ; 24F8 idle long enough: run the animation jmp L_253C ; 24FA player is active -> no marker on the map L_24FD: lda joyRepeatTimer ; 24FD shared frame countdown, used here as the delay between marker steps beq L_2504 ; 24FF expired: time for the next cell jmp L_253F ; 2501 still counting: leave the marker where it stands L_2504: lda #$04 ; 2504 4 frames per cell sta joyRepeatTimer ; 2506 reload the step delay lda pathMarkerCol ; 2508 marker column sta lineCurCol ; 250B line stepper current column = where the marker stands lda pathMarkerRow ; 250D marker row sta lineCurRow ; 2510 line stepper current row jsr initLineStepper ; 2512 set up the walk to the target cell; A = number of steps beq L_252B ; 2515 zero steps (marker already on the target) -> count this as a finished pass jsr stepLine ; 2517 advance one cell along the line ldy lineCurRow ; 251A new row sty pathMarkerRow ; 251C remember it ldx lineCurCol ; 251F new column stx pathMarkerCol ; 2521 remember it jsr positionMarkerSprite ; 2524 put sprite 1 on that mini-map cell lda lineStepsLeft ; 2527 steps still to walk bne L_2537 ; 2529 more to come -> keep the marker visible ; ---------------------------------------------------------------------- ; End of one pass over the line: blink the marker by toggling sprite 1, and after six passes (the ; counter starts at 5 and is reset by resetPathMarker) start over at the unit. ; ---------------------------------------------------------------------- L_252B: dec pathMarkerState ; 252B one pass of the line finished bmi resetPathMarker ; 252E counter ran past zero -> reload the marker onto the unit lda spriteEnableShadow ; 2530 sprite enable shadow and #$02 ; 2533 bit 1 = sprite 1, the marker bne L_253C ; 2535 currently visible -> hide it, which makes the line end blink L_2537: lda #$02 ; 2537 sprite 1 bit jmp enableSprites ; 2539 show the marker (and return) L_253C: jmp hideSprite1 ; 253C no marker wanted: switch sprite 1 off L_253F: rts ; 253F nothing to do this frame ; ---------------------------------------------------------------------- ; resetPathMarker - Puts the path marker back at the start: sprite 1 off, marker cell and pass counter ; back to the selected unit, and sprite 1's shape reloaded. Normally that is the small grey block; ; when the mini-map is showing the zoomed group box ($920A bit 7) the marker instead copies sprite 0's ; cursor box so it matches the enlarged cells. ; In: selectedUnit $90F8, groupBoxActive $920A bit 7, sprite 0 shape at $8A00 ; Out: sprite 1 shape at $8A40 and colour shadow $9009, activeSpriteIndex $91CC left 0, ; pathMarkerState/Col/Row reloaded, sprite 1 enabled in the zoomed case ; Called from: the info-panel entry and idle loops ($227B, $22FF), the unit menu at $5F18, and from ; animatePathMarkerStep when the pass counter runs out ($252E) ; ---------------------------------------------------------------------- resetPathMarker: jsr hideSprite1 ; 2540 marker off while its shape is swapped jsr resetPathMarkerToUnit ; 2543 pass counter back to 5 and the marker back onto the unit lda groupBoxActive ; 2546 bit 7 set = the mini-map is zoomed onto the group box bmi L_2559 ; 2549 zoomed: use a copy of the cursor box instead lda #$01 ; 254B sprite 1 sta activeSpriteIndex ; 254D tell setCursorShapeBlock which sprite to load jsr setCursorShapeBlock ; 2550 load the small solid block shape ($9E92) and colour it grey lda #$00 ; 2553 sprite 0 sta activeSpriteIndex ; 2555 back to the default sprite for the cursor routines rts ; 2558 done ; ---------------------------------------------------------------------- ; Zoomed mini-map: the marker has to be as large as the cursor box, so sprite 1 simply gets a copy of ; sprite 0's current shape. ; ---------------------------------------------------------------------- L_2559: ldy #$3F ; 2559 64 shape bytes, last one first L_255B: lda sprite0Shape,y ; 255B byte of the sprite 0 cursor box ($8A00) sta sprite1Shape,y ; 255E into the sprite 1 shape ($8A40) dey ; 2561 next byte down bpl L_255B ; 2562 until all 64 are copied lda #$02 ; 2564 sprite 1 bit jsr enableSprites ; 2566 make the marker visible lda #$0C ; 2569 grey sta sprite1ColourShadow ; 256B sprite 1 colour shadow, copied to $D028 by the IRQ rts ; 256E done ; ---------------------------------------------------------------------- ; resetPathMarkerToUnit - Reloads the marker's pass counter with 5 and moves the marker cell onto the ; selected unit's map position, then falls into positionMarkerSprite to place sprite 1 there. ; In: selectedUnit $90F8, unitColTable $F640, unitRowTable $F6A4 ; Out: pathMarkerState $24E1 = 5, pathMarkerCol/Row $24E2/$24E3, sprite 1 position shadows; X = ; column, Y = row ; Called from: the path-preview entry at $225D and resetPathMarker ($2543) ; ---------------------------------------------------------------------- resetPathMarkerToUnit: lda #$05 ; 256F five more passes over the line sta pathMarkerState ; 2571 reload the pass counter ldy selectedUnit ; 2574 the selected unit lda unitColTable,y ; 2577 its map column sta pathMarkerCol ; 257A marker column tax ; 257D X = column for positionMarkerSprite lda unitRowTable,y ; 257E its map row sta pathMarkerRow ; 2581 marker row tay ; 2584 Y = row for positionMarkerSprite ; ---------------------------------------------------------------------- ; positionMarkerSprite - Converts a map cell to mini-map sprite coordinates (column*4+$1F, row*4+$39, ; or the group-box mapping when the mini-map is zoomed) and stores them in sprite 1's position ; shadows, which the raster IRQ copies to the VIC. ; In: X = map column, Y = map row ; Out: sprite1XShadow $9011, sprite1YShadow $9019 ; Called from: animatePathMarkerStep ($2524) and by fall-through from resetPathMarkerToUnit ; ---------------------------------------------------------------------- positionMarkerSprite: jsr mapCellToSpriteXY ; 2585 cell -> sprite coordinates on the 40x40 mini-map (4 pixels per cell) stx sprite1XShadow ; 2588 sprite 1 X shadow sty sprite1YShadow ; 258B sprite 1 Y shadow rts ; 258E done ; ---------------------------------------------------------------------- ; recenterViewportOnUnit - Makes sure the selected unit sits under the battlefield cursor. The cursor ; is always the centre cell of the 7x5 view (viewOriginCol+3, viewOriginRow+2); if the unit is ; somewhere else the view is scrolled to it one cell per frame by scrollViewToUnit. ; In: selectedUnit $90F8, viewOriginCol zp_A5 / viewOriginRow zp_A6, unitColTable $F640 / ; unitRowTable $F6A4 ; Out: lineCurCol zp_93 / lineCurRow zp_92 = the cursor cell (the scroll starts there); the view ; scrolled and redrawn ; Called from: the info-panel entry at $2239 and the unit selection code at $4BF6 and $4C4C ; ---------------------------------------------------------------------- recenterViewportOnUnit: ldx selectedUnit ; 258F the unit that must end up under the cursor lda #$03 ; 2592 the cursor sits 3 cells right of the view origin clc ; 2594 plain add adc viewOriginCol ; 2595 map column of the left edge of the view sta lineCurCol ; 2597 cursor column, also the line stepper start lda #$02 ; 2599 and 2 cells below the top edge clc ; 259B plain add adc viewOriginRow ; 259C map row of the top edge of the view sta lineCurRow ; 259E cursor row lda lineCurCol ; 25A0 cursor column cmp unitColTable,x ; 25A2 same column as the unit? bne L_25AE ; 25A5 no -> scroll lda lineCurRow ; 25A7 cursor row cmp unitRowTable,x ; 25A9 same row as the unit? beq L_25B6 ; 25AC the unit is already under the cursor: nothing to do L_25AE: lda #$01 ; 25AE 1 frame per scroll step, i.e. animated ldx selectedUnit ; 25B0 the unit to scroll to jsr scrollViewToUnit ; 25B3 walk the view over to it cell by cell L_25B6: rts ; 25B6 done ; ---------------------------------------------------------------------- ; consumeSpaceKey - Eats a pending SPACE. The IRQ leaves the last printable key in lastTypedKey ; ($9248) and several loops close on SPACE, so a screen that was itself closed with SPACE clears the ; key before returning, otherwise the next loop would close immediately as well. ; In: lastTypedKey $9248 ; Out: lastTypedKey = $FF (no key) when it held $A0 (space) ; Called from: $1863 and $2073 ; ---------------------------------------------------------------------- consumeSpaceKey: lda lastTypedKey ; 25B7 last printable key latched by the IRQ cmp #$A0 ; 25BA $A0 = space (PETSCII with bit 7 set) bne L_25C3 ; 25BC some other key: leave it for the caller lda #$FF ; 25BE 'no key pending' sta lastTypedKey ; 25C0 swallow the space so the next loop does not act on it L_25C3: rts ; 25C3 done ; ---------------------------------------------------------------------- ; callSetupOverlayHook - Trampoline into overlay variant A at $8658 (handleStarKeyToggle), the ; per-frame '*' key handler of the non-battle phases: it toggles setupToggleFlag $92F3 bit 7 (DESTROY ; mode during unit placement, CUSTOM rules on the setup screen) and reports C=1 when it consumed the ; key. It may only be called while gamePhase zp_B1 is non-zero, because in overlay variant B address ; $8658 is in the middle of an instruction. ; In: gamePhase zp_B1 must be non-zero (overlay A resident); the hook itself reads chatState $929C ; and lastTypedKey $9248 ; Out: C = 1 when '*' was pressed and the toggle changed, C = 0 otherwise ; Called from: the map main loop at $2026 and the info-panel loop at $2348 ; ---------------------------------------------------------------------- callSetupOverlayHook: jmp handleStarKeyToggle ; 25C4 into overlay A: '*' toggles DESTROY / CUSTOM, C=1 when it acted ; ---------------------------------------------------------------------- ; getViewportOriginPointer - Builds a map pointer for the top-left cell of the 7x5 battlefield view. ; The view origin may be off the map (it is clamped only for the cursor cell), so a negative row is ; handled by taking row 0 and stepping the pointer back 40 bytes per row, and a negative column by the ; sign-extended add in getViewportRowPointer. ; In: viewOriginCol zp_A5, viewOriginRow zp_A6, the map row address tables $BFB0/$BFD8 ; Out: mapCellPtr zp_48/zp_49 -> the origin cell (may point in front of $F000 when the view hangs off ; the map), A = that cell's byte only when the origin is really on the map, Y = 0 ; Called from: drawViewport ($26D1) and the $6F00 overlays ($71D4, $71F9, $740D, $7430, $7BCC) ; ---------------------------------------------------------------------- getViewportOriginPointer: ldy viewOriginRow ; 25C7 map row of the top edge of the view bpl getViewportRowPointer ; 25C9 row is on the map: ordinary row lookup ldy #$00 ; 25CB the view hangs off the top of the map: start from row 0 jsr getViewportRowPointer ; 25CD pointer to (viewOriginCol, row 0) ldy viewOriginRow ; 25D0 the negative row count, e.g. $FE = two rows above the map ; ---------------------------------------------------------------------- ; Step the pointer back one map row (40 cells) per row above the map, counting Y up to 0. ; ---------------------------------------------------------------------- L_25D2: lda mapCellPtr ; 25D2 pointer low byte sec ; 25D4 no borrow sbc #$28 ; 25D5 one map row = 40 cells sta mapCellPtr ; 25D7 store it back lda mapCellPtrHi ; 25D9 pointer high byte sbc #$00 ; 25DB propagate the borrow sta mapCellPtrHi ; 25DD store it back iny ; 25DF one row less to go bne L_25D2 ; 25E0 until the negative row count reaches 0 rts ; 25E2 done ; ---------------------------------------------------------------------- ; getViewportRowPointer - Map pointer for the cell (viewOriginCol, Y). A negative view column is ; handled by reading column 0 and then adding the signed offset to the pointer, so the caller gets a ; pointer that is correct even when the view sticks out over the left edge of the map. ; In: Y = map row 0-39, viewOriginCol zp_A5 ; Out: mapCellPtr zp_48/zp_49, A = the cell byte when the column is on the map, Y = 0 ; Called from: getViewportOriginPointer ($25CD and the branch at $25C9) ; ---------------------------------------------------------------------- getViewportRowPointer: ldx viewOriginCol ; 25E3 map column of the left edge of the view bpl readMapCell ; 25E5 column is on the map: plain lookup, and its result is our result ldx #$00 ; 25E7 the view hangs off the left edge: read column 0 instead jsr readMapCell ; 25E9 pointer to (column 0, row Y) lda mapCellPtr ; 25EC pointer low byte clc ; 25EE plain add adc viewOriginCol ; 25EF add the negative column offset... sta mapCellPtr ; 25F1 store it back lda mapCellPtrHi ; 25F3 pointer high byte adc #$FF ; 25F5 ...sign extended: -1 in the high byte sta mapCellPtrHi ; 25F7 store it back rts ; 25F9 done ; ---------------------------------------------------------------------- ; readMapCell - The map accessor of the whole game: forms mapCellPtr = rowAddress[Y] + X from the ; 40-entry row address tables at $BFB0/$BFD8 (built by the start-up code at $0339 as $F000 + 40*row) ; and returns the byte of cell (column X, row Y) of the 40x40 battlefield. ; In: X = map column 0-39, Y = map row 0-39; row address tables $BFB0 (lo) / $BFD8 (hi) ; Out: A = the map byte (bit 7 set: bits 0-6 are the index of the unit standing there and the covered ; terrain is in $FAF0+unit; otherwise a terrain code, $40 = open ground), mapCellPtr zp_48/zp_49 ; -> that cell, Y = 0 ; Called from: about 30 places - the main program ($1F16, $1F3F, $25E9, $36FB, $37D7, $3C2C, $3E06, ; $3E38, $3ECD, $3F3C, $42E7, $4571, $476A, $5400, $2619), both $6F00 overlays and the solo ; trainer ($E97B, $E9B2) ; ---------------------------------------------------------------------- readMapCell: txa ; 25FA map column clc ; 25FB no carry into the add adc mapRowAddrLoTable,y ; 25FC + low byte of the address of map row Y ($F000 + 40*row) sta mapCellPtr ; 25FF map pointer low byte lda #$00 ; 2601 only the carry goes into the high byte adc mapRowAddrHiTable,y ; 2603 + high byte of the row address sta mapCellPtrHi ; 2606 map pointer high byte ldy #$00 ; 2608 index 0 for the indirect read lda (mapCellPtr),y ; 260A the cell byte: bit 7 = a unit stands here, else a terrain code rts ; 260C done ; ---------------------------------------------------------------------- ; readCellUnderCursor - Reads the map cell under the battlefield cursor. The cursor never moves: it is ; always the centre cell of the 7x5 view, i.e. (viewOriginCol+3, viewOriginRow+2); the joystick ; scrolls the view instead. ; In: viewOriginCol zp_A5, viewOriginRow zp_A6 ; Out: A = the map byte, mapCellPtr zp_48/zp_49 -> that cell, X = cursor column, Y = 0 ; Called from: the map main loop ($2049, $207B), the info-panel loop ($2310) and the console at $63D9 ; ---------------------------------------------------------------------- readCellUnderCursor: lda viewOriginCol ; 260D map column of the left edge of the view clc ; 260F plain add adc #$03 ; 2610 the cursor is the 4th of the 7 visible columns tax ; 2612 X = cursor column lda viewOriginRow ; 2613 map row of the top edge of the view clc ; 2615 plain add adc #$02 ; 2616 the cursor is the 3rd of the 5 visible rows tay ; 2618 Y = cursor row jmp readMapCell ; 2619 read that cell and return its byte ; ---------------------------------------------------------------------- ; clearInfoPanel - Selects the unit info panel as the text window and blanks it with the text engine's ; window clear, using the current textColour zp_3D and fill glyph zp_3E. ; In: textColour zp_3D, textFillGlyph zp_3E ; Out: the text window set to rows 12-20 / columns 25-38 and cleared; text cursor zp_3F/zp_40 homed ; Called from: 20 places - the manual check ($09EC/$0A44), the map loop ($2004, $22B3, $2356), ; redrawGameScreen ($267C), the screen switcher ($2DEE), the unit and console menus ($4BBF, $4C3D, ; $5E49, $5E9F, $6151, $6201, $6245, $6374, $6441, $64EE, $65FF) and overlay A ; ---------------------------------------------------------------------- clearInfoPanel: jsr setInfoPanelWindow ; 261C rows 12-20, columns 25-38 jmp bootDriveDataBit ; 261F fill the window with textFillGlyph and return (also textEngineC000:clearTextWindow) ; ---------------------------------------------------------------------- ; setInfoPanelWindow - Defines the text window that covers the unit info panel: columns 25-38, rows 12 ; to 20 (row 21 is the exclusive limit). It resets the window to full screen first so that leftovers ; of a narrower window do not survive. It exits with A = $0C, which printUnitStatusLines relies on to ; reach row 12. ; In: - ; Out: textWindowTop zp_39 = $0C, textWindowBottom zp_3A = $15, textWindowLeft zp_3B = $19, ; textWindowWidth zp_3C = $0E; A = $0C ; Called from: clearInfoPanel ($261C) and printUnitStatusLines ($24C7) ; ---------------------------------------------------------------------- setInfoPanelWindow: jsr setFullScreenWindow ; 2622 start from a full-screen window lda #$19 ; 2625 screen column 25 sta textWindowLeft ; 2627 window left edge lda #$0E ; 2629 14 columns -> columns 25-38 sta textWindowWidth ; 262B window width / wrap point lda #$15 ; 262D row 21 = the exclusive bottom limit sta textWindowBottom ; 262F window bottom lda #$0C ; 2631 top row 12 sta textWindowTop ; 2633 window top; A stays $0C for the caller rts ; 2635 done ; ---------------------------------------------------------------------- ; clearScreenAndRedrawGameScreen - Rebuilds the battlefield display from scratch: sets the VIC up for ; the multicolour bitmap and wipes it grey, then falls into redrawGameScreen. ; In: - ; Out: the whole screen, see initBitmapScreen and redrawGameScreen ; Called from: start-up ($0A95) and overlay A ($7D59, $8232, $8559) ; ---------------------------------------------------------------------- clearScreenAndRedrawGameScreen: jsr initBitmapScreen ; 2636 VIC into multicolour bitmap mode, bitmap and colour RAM cleared to grey ; ---------------------------------------------------------------------- ; redrawGameScreen - Draws the whole battlefield screen: the three panel frames (mini-map, tactical ; view, info panel), the bottom status bar, the 40x40 overview map, the 7x5 tactical view and finally ; the current status message. gamePhase zp_B1 gates the map: bit 7 (menu phases) leaves the whole map ; area empty, bit 6 (unit setup) draws the overview but not the tactical view. ; In: gamePhase zp_B1, currentScreen $90FB (via drawMainWindowFrame), the map at $F000 and the unit ; arrays, shownMessageId zp_7B ; Out: vicCtrl2Shadow $9001 = $D8 (multicolour), vicBg0Shadow $9004 = 7, cellBackgroundColour $9226 = ; 0, the glyph plotter parameters $2A29-$2A2C = 0, textColour zp_3D, the whole screen redrawn ; Called from: $1FCD (the battlefield screen entry), and by fall-through from ; clearScreenAndRedrawGameScreen ; ---------------------------------------------------------------------- redrawGameScreen: jsr patchUnitSideBranches ; 2639 re-patch the own/enemy branch in renderMapTile for the current side and replay viewpoint lda #$D8 ; 263C $D016: multicolour on (bit 4), 40 columns (bit 3), no fine scroll sta vicCtrl2Shadow ; 263E the raster IRQ copies this shadow to $D016 lda #$0C ; 2641 grey sta textColour ; 2643 text colour used for the frames jsr setFullScreenWindow ; 2645 text window = the whole screen jsr bootDriveDataBit ; 2648 blank the entire bitmap through the text engine (also textEngineC000:clearTextWindow) lda #$00 ; 264B reset the glyph plotter parameter block... sta useDrawColourFlag ; 264D ...no forced colour... sta drawColourByte ; 2650 ...colour byte 0... sta blockFillFlag ; 2653 ...no block fill... sta blockFillChar ; 2656 ...and fill character 0 lda #$07 ; 2659 yellow sta vicBg0Shadow ; 265B $D021 background = the colour of the %00 pixels of the map tiles lda #$00 ; 265E black sta cellBackgroundColour ; 2660 background colour used behind drawn character cells lda #$00 ; 2663 redundant: the block-fill flag was already cleared at $2653 sta blockFillFlag ; 2665 (leftover store, no effect) jsr drawMiniMapFrame ; 2668 22x22 frame at column 0 around the strategic map jsr drawViewportFrame ; 266B 16x12 frame at column 24 around the tactical view; also clears its interior jsr drawInfoPanelFrame ; 266E 16x11 frame at column 24 row 11 around the unit info panel jsr drawMainWindowFrame ; 2671 the 38x3 status bar across the bottom lda gamePhase ; 2674 0 during the battle, non-zero during setup and in the menus bpl L_2682 ; 2676 bit 7 clear -> the battlefield may be drawn lda #$00 ; 2678 colour 0 sta textColour ; 267A text colour for the clear jsr clearInfoPanel ; 267C menu phases: leave the info panel blank jmp L_268C ; 267F and skip both map drawings L_2682: jsr drawOverviewMap ; 2682 the whole 40x40 map at 4x4 pixels per cell bit gamePhase ; 2685 test gamePhase again bvs L_268C ; 2687 bit 6 set (unit placement) -> no tactical view jsr drawViewport ; 2689 the 7x5 cell tactical view L_268C: jmp redrawStatusMessage ; 268C put the current status message back on the bottom bar ; ---------------------------------------------------------------------- ; drawMiniMapFrame - Draws window 0: the 22x22 character frame at column 0, row 0 that surrounds the ; 20x20 strategic mini-map. ; In: - ; Out: textColour zp_3D = $0C, the frame on screen, the window geometry left in ; $2A2D/$2A2E/$9221/$9222 by drawWindowFrame ; Called from: redrawGameScreen ($2668) ; ---------------------------------------------------------------------- drawMiniMapFrame: lda #$0C ; 268F grey sta textColour ; 2691 colour of the frame characters ldx #$00 ; 2693 window 0 = column 0, row 0, 22x22 jmp drawWindowFrame ; 2695 draw it and return ; ---------------------------------------------------------------------- ; drawViewportFrame - Draws window 1: the 16x12 character frame at column 24, row 0 around the ; tactical view, then falls into clearViewportTextArea to blank the interior. ; In: - ; Out: textColour zp_3D, the frame drawn and the 14x10 interior cleared ; Called from: redrawGameScreen ($266B) ; ---------------------------------------------------------------------- drawViewportFrame: lda #$0C ; 2698 grey sta textColour ; 269A colour of the frame characters ldx #$01 ; 269C window 1 = column 24, row 0, 16x12 jsr drawWindowFrame ; 269E draw the frame ; ---------------------------------------------------------------------- ; clearViewportTextArea - Declares the interior of the tactical view (rows 1-10, columns 25-38, i.e. ; the 7x5 map cells at 2x2 characters each) as the text window and clears it in colour 0. ; In: - ; Out: textWindowTop zp_39 = 1, textWindowBottom zp_3A = $0B, textWindowLeft zp_3B = $19, ; textWindowWidth zp_3C = $0E, textColour zp_3D = 0; the area blanked ; Called from: the screen-switch clear at $2DF4, overlay A ($8425), and by fall-through from ; drawViewportFrame ; ---------------------------------------------------------------------- clearViewportTextArea: lda #$01 ; 26A1 first character row of the view sta textWindowTop ; 26A3 window top lda #$19 ; 26A5 screen column 25 sta textWindowLeft ; 26A7 window left edge lda #$0E ; 26A9 14 columns = 7 map cells sta textWindowWidth ; 26AB window width lda #$0B ; 26AD row 11 = exclusive limit, so rows 1-10 = 5 map rows sta textWindowBottom ; 26AF window bottom lda #$00 ; 26B1 colour 0 sta textColour ; 26B3 text colour for the clear jmp bootDriveDataBit ; 26B5 blank the area and return (also textEngineC000:clearTextWindow) ; ---------------------------------------------------------------------- ; drawInfoPanelFrame - Draws window 2: the 16x11 character frame at column 24, row 11 around the unit ; info panel. ; In: - ; Out: textColour zp_3D = $0C, the frame on screen ; Called from: redrawGameScreen ($266E) ; ---------------------------------------------------------------------- drawInfoPanelFrame: lda #$0C ; 26B8 grey sta textColour ; 26BA colour of the frame characters ldx #$02 ; 26BC window 2 = column 24, row 11, 16x11 jmp drawWindowFrame ; 26BE draw it and return ; ---------------------------------------------------------------------- ; drawViewport - Redraws the tactical view: 5 rows of 7 map cells, each cell a 16x16 pixel multicolour ; tile occupying 2x2 character cells, into character rows 1-10 and columns 25-38. It walks the map ; from the view origin, has renderMapTile build each tile in the buffer at $04C8-$04E7 and then copies ; the 32 bytes into the bitmap - 16 bytes to the upper character pair and 16 to the pair one character ; row (320 bytes) lower, through a store whose address is self-modified. The view origin and the text ; plotter's scratch bytes are saved and restored, because the loop walks the origin to tell ; renderMapTile which map cell it is drawing. ; In: viewOriginCol zp_A5 / viewOriginRow zp_A6, the map at $F000 and the unit arrays, gamePhase ; zp_B1 bit 6 (suppresses units) ; Out: bitmap, screen RAM and colour RAM of the 14x10 character area; scanResultCount zp_2F and ; magnifySrcCol zp_31 used as row/column counters, mapCellPtr zp_48/zp_49, bitmapPtr ; zp_B4-screenPtrHi zp_B7, $2A2D/$2A2E, drawCellSaveCol/Row $90F6/$90F7; self-modifies the store ; at tileCopyStore ($2703) ; Called from: everywhere the battlefield changes - the map main loop ($1FA2, $200F), redrawGameScreen ; ($2689), the tick loop ($4023, $413A, $4152), the unit menus ($4BDE, $5E38, $5E7F, $605F, $61F8, ; $63A4, $636B, $64BB, $64CE), overlay A ($83F5), the screen switcher ($075B) and the text engine ; ($C797) ; ---------------------------------------------------------------------- drawViewport: lda scratch18 ; 26C1 the text plotter's row byte... pha ; 26C3 ...is borrowed as the character row counter, so save it lda scratch19 ; 26C4 the text plotter's column byte... pha ; 26C6 ...is borrowed as the character column counter lda viewOriginCol ; 26C7 map column of the view origin sta drawCellSaveCol ; 26C9 saved: the loop walks the origin cell by cell lda viewOriginRow ; 26CC map row of the view origin sta drawCellSaveRow ; 26CE saved as well jsr getViewportOriginPointer; 26D1 mapCellPtr -> the map cell at the top-left corner of the view lda #$01 ; 26D4 character row 1 = the first row inside the frame sta windowOriginRow ; 26D6 window origin row used by the colour writes sta scratch18 ; 26D9 row for calcCellPointers lda #$00 ; 26DB start at the top sta scanResultCount ; 26DD viewport row counter 0-4 ; ---------------------------------------------------------------------- ; Outer loop: one row of 7 map cells = two character rows. ; ---------------------------------------------------------------------- L_26DF: lda #$19 ; 26DF character column 25 = the first column inside the frame sta windowOriginCol ; 26E1 window origin column sta scratch19 ; 26E4 column for calcCellPointers lda #$00 ; 26E6 start at the left sta magnifySrcCol ; 26E8 viewport column counter 0-6 ; ---------------------------------------------------------------------- ; Inner loop: one map cell = one 16x16 tile = 2x2 characters. ; ---------------------------------------------------------------------- L_26EA: jsr calcCellPointers ; 26EA bitmapPtr zp_B4/B5 and screenPtr zp_B6/B7 for character (row,column) ldy magnifySrcCol ; 26ED offset of this cell inside the map row lda (mapCellPtr),y ; 26EF the map byte of the cell jsr renderMapTile ; 26F1 build its 32-byte tile in $04C8 and write the cell colours lda bitmapPtr ; 26F4 bitmap address of the upper-left character sta tileCopyStore+1 ; 26F6 patch the destination of the copy store... lda bitmapPtrHi ; 26F9 ... sta tileCopyStore+2 ; 26FB ...so it writes into this cell ldy #$00 ; 26FE start at the first tile byte ; ---------------------------------------------------------------------- ; Copy the tile buffer into the bitmap: bytes $00-$0F are the two upper characters (which are adjacent ; in a C64 bitmap), bytes $10-$1F the two characters one character row lower. ; ---------------------------------------------------------------------- L_2700: lda play4Row12,y ; 2700 byte of the tile buffer tileCopyStore: sta irqVectorHi,y ; 2703 into the bitmap; the absolute address is patched at $26F6/$26FB and $2714/$271B (the unpatched operand in the file happens to read $FFFF) iny ; 2706 next byte cpy #$20 ; 2707 32 bytes = four characters bcs L_2720 ; 2709 tile finished cpy #$10 ; 270B the first 16 bytes are the upper character pair bne L_2700 ; 270D not there yet clc ; 270F no carry into the add lda bitmapPtr ; 2710 bitmap address of the cell again adc #$30 ; 2712 +$130, and Y has already reached $10, so the store lands at +$140 = 320 = one character row down sta tileCopyStore+1 ; 2714 re-patch the store... lda bitmapPtrHi ; 2717 ... adc #$01 ; 2719 ... sta tileCopyStore+2 ; 271B ...to the lower character pair bne L_2700 ; 271E always taken (the bitmap lives at $A000) ; ---------------------------------------------------------------------- ; Cell done: step one map cell to the right = two character columns. ; ---------------------------------------------------------------------- L_2720: inc scratch19 ; 2720 one map cell is two characters wide... inc scratch19 ; 2722 ...so advance the character column by 2 inc viewOriginCol ; 2724 renderMapTile reads the view origin as the coordinates of the cell it draws inc magnifySrcCol ; 2726 next cell of this row ldy magnifySrcCol ; 2728 the column counter cpy #$07 ; 272A 7 cells across the view bcc L_26EA ; 272C keep going along the row inc scratch18 ; 272E row done: one map cell is two characters high... inc scratch18 ; 2730 ...so advance the character row by 2 inc viewOriginRow ; 2732 and the map row the next line draws lda drawCellSaveCol ; 2734 the saved view origin column sta viewOriginCol ; 2737 back to the left edge of the view inc scanResultCount ; 2739 one more viewport row done ldy scanResultCount ; 273B the row counter cpy #$05 ; 273D 5 cell rows in the view bcs L_2751 ; 273F all rows drawn lda mapCellPtr ; 2741 map pointer low byte clc ; 2743 plain add adc #$28 ; 2744 one map row = 40 cells sta mapCellPtr ; 2746 store it back lda mapCellPtrHi ; 2748 map pointer high byte adc #$00 ; 274A propagate the carry sta mapCellPtrHi ; 274C store it back jmp L_26DF ; 274E draw the next row of cells ; ---------------------------------------------------------------------- ; Restore everything the loop borrowed: the view origin and the text plotter's scratch bytes. ; ---------------------------------------------------------------------- L_2751: lda drawCellSaveCol ; 2751 saved origin column sta viewOriginCol ; 2754 restore it (the loop walked it across the view) lda drawCellSaveRow ; 2756 saved origin row sta viewOriginRow ; 2759 restore it pla ; 275B saved text column sta scratch19 ; 275C give the text plotter its scratch byte back pla ; 275E saved text row sta scratch18 ; 275F and the other one rts ; 2761 done ; nybbleToMulticolourTable - expands the four pixels of one nybble of a terrain shape into one ; multicolour bitmap byte: every set bit becomes the pixel pair %10, i.e. bit 0 -> $02, bit 1 -> ; $08, bit 2 -> $20, bit 3 -> $80. %10 pixels take their colour from the low nybble of the ; screen RAM byte, which renderMapTile fills with the terrain colour; %00 pixels show the ; $D021 background (yellow) and the %11 pixels of the unit glyphs come from colour RAM. nybbleToMulticolourTable: .byte $00,$02,$08,$0A,$20,$22,$28,$2A; 2762 .... "(* nybbles $0-$7: bit 0 -> $02, bit 1 -> $08, bit 2 -> $20 .byte $80 ; 276A nybbles $8-$F: bit 3 -> $80; $AA = all four pixels set .byte $82,$88,$8A,$A0,$A2,$A8,$AA; 276B ....... ; ---------------------------------------------------------------------- ; renderMapTile - Builds the 16x16 pixel multicolour tile of one map cell in the 32-byte buffer at ; $04C8-$04E7 and writes the cell's four screen-RAM colour bytes. The cell byte in A is either a ; terrain code (the 8x8 one-bit shape at $9C6E + (code-$40)*8, doubled in both directions through ; nybbleToMulticolourTable) or, with bit 7 set, a unit index, in which case the terrain hidden ; underneath is drawn first and the 32-byte unit glyph is ORed on top and the unit colour written to ; colour RAM. Cells outside the 40x40 map use the four border shapes $68-$6B. The caller copies the ; buffer into the bitmap. ; In: A = the map cell byte; viewOriginCol zp_A5 / viewOriginRow zp_A6 = that cell's map coordinates; ; screenPtr zp_B6/zp_B7 -> its screen RAM cell; gamePhase zp_B1 bit 6 suppresses units; ; ownUnitColour zp_77 / enemyUnitColour zp_78; filmPlaybackMode $0B9B, revealAllUnits $92FC, ; highlightedUnit $9204, highlightedGroupKey $9205, uiLevel $9209; the branch opcodes at ; ownSideBranchOpcode ($2840) and unitVisibleBranchOpcode ($2846) decide which half of the unit ; array is 'ours' ; Out: the 32-byte tile buffer $04C8-$04E7; the terrain colour in the four screen RAM bytes at ; (zp_B6)+0/+1/+40/+41 and, when a unit is drawn, the unit colour in the matching colour RAM bytes ; (zp_B7 is advanced by $4C, so screenPtr points into $D800 on exit); tileColour zp_32, glyphPtr ; zp_B2/B3; self-modifies mapCellByteImm+1 ($2808), terrainShapeRowImm+1 ($27D4), ; terrainShapeRowCounter+1 ($27F2), unitGlyphEorMask+1 ($28B8) and cloakSkipFlagImm+1 ($28C0) ; Called from: drawViewport ($26F1) and the single-cell redraw at $5D45 ; Off-map check: the view may hang over the edge of the battlefield. Both coordinates are compared ; unsigned against 40, which also catches the negative (>= $80) values. ; ---------------------------------------------------------------------- renderMapTile: sta mapCellByteImm+1 ; 2772 keep the cell byte in the LDA # at mapCellByteImm for the unit pass below tay ; 2775 Y = cell byte lda viewOriginCol ; 2776 map column of this cell cmp #$28 ; 2778 40 = past the right edge (negative columns are >= 40 unsigned too) bcs L_2782 ; 277A off the map lda viewOriginRow ; 277C map row of this cell cmp #$28 ; 277E 40 = past the bottom edge bcc L_2790 ; 2780 the cell is on the battlefield L_2782: lda viewOriginRow ; 2782 off-map: pick one of four border patterns from the coordinates... clc ; 2784 plain add adc viewOriginCol ; 2785 ...so the border is dithered rather than uniform and #$03 ; 2787 four patterns clc ; 2789 no carry adc #$68 ; 278A shapes $68-$6B tile everything outside the battlefield tay ; 278C Y = border shape code sta mapCellByteImm+1 ; 278D and make the unit pass see plain terrain, not a unit index ; ---------------------------------------------------------------------- ; Terrain selection: when a unit stands on the cell the map byte holds its index, and the terrain it ; covers is kept in unitTerrainUnder ($FAF0 + unit). unitTerrainUnderAlias is that table biased by ; -$80 so it can be indexed with the raw $80|unit byte. ; ---------------------------------------------------------------------- L_2790: tya ; 2790 the cell byte bpl L_2797 ; 2791 bit 7 clear = plain terrain, use it directly lda unitTerrainUnderAlias,y ; 2793 a unit stands here: fetch the terrain it is hiding tay ; 2796 Y = terrain code L_2797: cmp #$40 ; 2797 $40 = open ground, which has no shape at all bne L_27A8 ; 2799 real terrain: draw its shape lda #$00 ; 279B blank tile ldy #$1F ; 279D 32 buffer bytes L_279F: sta play4Row12,y ; 279F clear one dey ; 27A2 next bpl L_279F ; 27A3 until the buffer is empty jmp mapCellByteImm ; 27A5 straight on to the unit pass; open ground keeps the colours already on screen ; ---------------------------------------------------------------------- ; Terrain colour and shape: terrainClassTable ($077E indexed by the raw cell value) maps the cell code ; to a terrain class 0-8, terrainColourTable gives the colour of that class (7 plain, 3 river, 5 ; forest, 8 contours/hills/summit, 0 for the off-map classes). ; ---------------------------------------------------------------------- L_27A8: ldx D_077E,y ; 27A8 terrain class 0-8 of this cell code lda terrainColourTable,x ; 27AB colour nibble of that class sta tileColour ; 27AE remember it for the screen RAM writes tya ; 27B0 the cell code sec ; 27B1 no borrow sbc #$40 ; 27B2 shape index = code - $40 for the ordinary terrain codes... bcs L_27B8 ; 27B4 ... adc #$20 ; 27B6 ...and code - $20 for the contour codes below $40 L_27B8: jsr setTerrainTilePtr ; 27B8 glyphPtr zp_B2/B3 -> the 8-byte shape at $9C6E + index*8 ; ---------------------------------------------------------------------- ; Expansion loop: eight shape rows, each one byte of 8 one-bit pixels. The low nybble (right half) ; becomes the right-hand character, the high nybble the left-hand one, and every row is written twice ; so the shape is doubled vertically as well as horizontally. Buffer layout: ; $04C8 upper-left character, $04D0 upper-right, $04D8 lower-left, $04E0 lower-right. ; ---------------------------------------------------------------------- ldx #$00 ; 27BB buffer offset, two bytes per shape row ldy #$00 ; 27BD shape row 0 sty terrainShapeRowCounter+1; 27BF the loop counter lives in the LDY # at terrainShapeRowCounter L_27C2: lda (glyphPtr),y ; 27C2 one row of the 8x8 terrain shape sta terrainShapeRowImm+1 ; 27C4 keep it in the LDA # at terrainShapeRowImm for the high nybble and #$0F ; 27C7 the right four pixels tay ; 27C9 index into the expansion table lda nybbleToMulticolourTable,y; 27CA each set bit becomes a %10 pixel pair sta play4Row20,x ; 27CD right-hand character, first scanline sta play4Row21,x ; 27D0 and again: the shape is doubled vertically terrainShapeRowImm: lda #$FF ; 27D3 the shape row saved at $27C4 lsr a ; 27D5 shift the left four pixels... lsr a ; 27D6 ...down... lsr a ; 27D7 ...into... lsr a ; 27D8 ...bits 0-3 tay ; 27D9 index into the expansion table lda nybbleToMulticolourTable,y; 27DA expand them the same way sta play4Row12,x ; 27DD left-hand character, first scanline sta play4Row13,x ; 27E0 doubled inx ; 27E3 two scanlines... inx ; 27E4 ...per shape row cpx #$08 ; 27E5 after four rows the upper character pair is full bne L_27EE ; 27E7 still in the upper half txa ; 27E9 buffer offset clc ; 27EA no carry adc #$08 ; 27EB skip the 8 bytes of the upper-right character to reach $04D8 tax ; 27ED continue in the lower character pair L_27EE: inc terrainShapeRowCounter+1; 27EE next shape row terrainShapeRowCounter: ldy #$FF ; 27F1 the row counter (operand patched at $27BF/$27EE) cpy #$08 ; 27F3 eight rows in a shape bcc L_27C2 ; 27F5 keep expanding ; ---------------------------------------------------------------------- ; Terrain colour into screen RAM: in multicolour mode the low nybble of the screen RAM byte is the ; colour of the %10 pixels the terrain shape is made of, the high nybble that of the %01 pixels (0 = ; black here). All four characters of the tile get the same byte. ; ---------------------------------------------------------------------- lda tileColour ; 27F7 the terrain colour ldy #$00 ; 27F9 first character of the tile sta (screenPtr),y ; 27FB upper-left iny ; 27FD next column sta (screenPtr),y ; 27FE upper-right ldy #$28 ; 2800 one screen row = 40 bytes sta (screenPtr),y ; 2802 lower-left iny ; 2804 next column sta (screenPtr),y ; 2805 lower-right ; ---------------------------------------------------------------------- ; Unit pass. mapCellByteImm+1 holds the map cell byte stored at $2772 (or the border code at ; $278D); bit 7 set means the low 7 bits are the index of the unit standing here. ; ---------------------------------------------------------------------- mapCellByteImm: lda #$FF ; 2807 the map cell byte patched in at $2772/$278D bpl L_2859 ; 2809 no unit on this cell: the tile is finished bit gamePhase ; 280B gamePhase bit 6 = draw terrain only (unit placement phase) bvs L_2859 ; 280D skip the unit and #$7F ; 280F unit index 0-99 ldy #$00 ; 2811 default values... sty unitGlyphEorMask+1 ; 2813 ...glyph EOR mask 0 = draw the glyph as it is... sty cloakSkipFlagImm+1 ; 2816 ...and scanline-skip flag 0 = draw every line ldy ownUnitColour ; 2819 6 = blue, the local player's colour sty tileColour ; 281B assume the unit is ours tay ; 281D Y = unit index lda unitTypeTable,y ; 281E type byte of the unit bpl L_2831 ; 2821 bit 7 clear = the unit is alive and #$07 ; 2823 dead: bits 0-2 still give the type for the glyph tax ; 2825 glyph selector lda #$01 ; 2826 white sta tileColour ; 2828 wrecks are drawn white... lda #$FF ; 282A ... sta unitGlyphEorMask+1 ; 282C ...and inverted bne L_2878 ; 282F always taken: a destroyed unit is visible to both players L_2831: and #$07 ; 2831 alive: type 0-4 tax ; 2833 glyph selector lda unitPathColTable,y ; 2834 waypoint column + cloak bits ($F898) bpl L_283E ; 2837 bit 7 clear = not cloaked lda #$FF ; 2839 cloaked... sta cloakSkipFlagImm+1 ; 283B ...so only every other scanline of the glyph is drawn (a see-through ghost) L_283E: cpy #$32 ; 283E 50 = the first unit of side 1; units 0-49 are side 0 ownSideBranchOpcode: bcc unitVisibleBranchOpcode ; 2840 opcode patched from the local side by $7BD2 in the overlay ($90 BCC when we are side 0, $B0 BCS when side 1): branch when this is one of our units lda enemyUnitColour ; 2842 2 = red sta tileColour ; 2844 enemy units are drawn red unitVisibleBranchOpcode: bcc L_285C ; 2846 same test, opcode patched by patchUnitSideBranches (inverted for the THEM replay viewpoint): our own units are always visible lda filmPlaybackMode ; 2848 film playback state cmp #$82 ; 284B $82 = playing back with viewpoint 2 = BOTH, which sees everything bcs L_285C ; 284D omniscient: draw the enemy unit lda revealAllUnits ; 284F $92FC bit 7 = reveal every unit (set when the game is over) bmi L_285C ; 2852 draw it lda unitEnergyTable,y ; 2854 energy byte of the enemy unit bmi L_285C ; 2857 bit 7 = this unit has been spotted by us L_2859: jmp L_28E0 ; 2859 unspotted enemy: leave the terrain tile as it is ; ---------------------------------------------------------------------- ; The unit is going to be drawn. First the two overlays that change the tile rather than the glyph: ; the dug-in bars and the white hit flash. ; ---------------------------------------------------------------------- L_285C: lda unitStunTable,y ; 285C stun / dig-in byte bpl L_286F ; 285F bit 7 clear = not dug in lda #$FF ; 2861 a solid row of %11 pixels sta play4Row12 ; 2863 top scanline of the upper-left character... sta play4Row35 ; 2866 ...bottom scanline of the lower-left one... sta play4Row20 ; 2869 ...top scanline of the upper-right... sta play4Row43 ; 286C ...and bottom of the lower-right: a bar above and below the dug-in unit L_286F: lda unitHitFlashTable,y ; 286F $FA8C bit 7 = the unit has just been hit bpl L_2878 ; 2872 no flash lda #$01 ; 2874 white sta tileColour ; 2876 flash the unit white this frame ; ---------------------------------------------------------------------- ; Highlight: the unit the player has selected, or any member of the selected group while the console ; is at group level (uiLevel 2), is drawn in reverse video. ; ---------------------------------------------------------------------- L_2878: cpy highlightedUnit ; 2878 the unit the player currently has selected beq L_2895 ; 287B this is it -> invert it lda uiLevel ; 287D which selection level the console is in cmp #$02 ; 2880 2 = a whole group is selected bne L_289A ; 2882 single-unit level: no group highlight lda unitDisplayFlagsTable,y ; 2884 group/status flags and #$40 ; 2887 bit 6 = the unit is in a group beq L_289A ; 2889 not grouped lda unitDisplayFlagsTable,y ; 288B flags again and #$3C ; 288E bits 2-5 = group id plus the side bit cmp highlightedGroupKey ; 2890 the selected group's key bne L_289A ; 2893 different group: no highlight L_2895: lda #$FF ; 2895 invert every glyph byte... sta unitGlyphEorMask+1 ; 2897 ...by patching the EOR mask at unitGlyphEorMask ; ---------------------------------------------------------------------- ; Fetch the unit glyph: 20 shapes of 32 bytes, indexed by unit type * 4 + facing through the word ; table unitGlyphPointerTable ($6C35). unitGlyphBaseTable gives $2C, $30, $34, $38, $3C for GRUNT, ; RIDER, BOOMER, SPY, COMCEN; the same numbers are the overview-map symbols, so ; $2C is subtracted again here. ; ---------------------------------------------------------------------- L_289A: lda unitFlagsTable,y ; 289A flags byte of the unit and #$03 ; 289D bits 0-1 = facing 0-3 clc ; 289F no carry adc unitGlyphBaseTable,x ; 28A0 + the base glyph number of the unit type sec ; 28A3 no borrow sbc #$2C ; 28A4 back to 0-19 = type*4 + facing asl a ; 28A6 two bytes per pointer tay ; 28A7 table index lda unitGlyphPointerTable,y ; 28A8 low byte of the glyph address sta glyphPtr ; 28AB glyph pointer iny ; 28AD next table byte lda unitGlyphPointerTable,y ; 28AE high byte of the glyph address sta glyphPtrHi ; 28B1 glyph pointer high ldy #$1F ; 28B3 32 glyph bytes, last first L_28B5: lda (glyphPtr),y ; 28B5 one byte of the unit glyph unitGlyphEorMask: eor #$FF ; 28B7 invert mask: $00 (as-is) or $FF (reverse video), patched at $2813/$282C/$2897 ora play4Row12,y ; 28B9 OR the unit on top of the terrain tile... sta play4Row12,y ; 28BC ...in the tile buffer cloakSkipFlagImm: lda #$FF ; 28BF cloak flag: $00 or $FF, patched at $2816/$283B bpl L_28C4 ; 28C1 not cloaked: one step per byte dey ; 28C3 cloaked: skip a scanline so the unit shows as a half-drawn ghost L_28C4: dey ; 28C4 next glyph byte bpl L_28B5 ; 28C5 until all 32 are merged ; ---------------------------------------------------------------------- ; Unit colour into colour RAM: the %11 pixels of a unit glyph take their colour from $D800, which is ; $4C00 above the screen RAM at $8C00. ; ---------------------------------------------------------------------- lda screenPtrHi ; 28C7 screen RAM pointer high byte ($8Cxx) clc ; 28C9 no carry adc #$4C ; 28CA +$4C00 turns it into a colour RAM pointer ($D8xx) sta screenPtrHi ; 28CC keep the colour RAM pointer ldy #$00 ; 28CE redundant: Y is loaded again at $28D2 lda tileColour ; 28D0 blue for our units, red for the enemy, white when hit or destroyed ldy #$00 ; 28D2 first character of the tile sta (screenPtr),y ; 28D4 upper-left iny ; 28D6 next column sta (screenPtr),y ; 28D7 upper-right ldy #$28 ; 28D9 one screen row = 40 bytes sta (screenPtr),y ; 28DB lower-left iny ; 28DD next column sta (screenPtr),y ; 28DE lower-right L_28E0: rts ; 28E0 tile finished ; ---------------------------------------------------------------------- ; drawMainWindowFrame - Draws the frame of the bottom bar: window 3 on the battlefield screen, window ; 4 on every other screen. Both are the same 38x3 box at column 1, row 22; they differ only in the ; 9-tile frame set used, so the status bar looks different in the console and menu screens. ; In: currentScreen $90FB (0 = battlefield) ; Out: textColour zp_3D = $0C, the frame drawn, window geometry left in $2A2D/$2A2E/$9221/$9222 ; Called from: redrawGameScreen ($2671) and both $6F00 overlays ($712E, $7A65) ; ---------------------------------------------------------------------- drawMainWindowFrame: lda #$0C ; 28E1 grey sta textColour ; 28E3 colour of the frame characters ldx #$03 ; 28E5 window 3 = the battlefield status bar (column 1, row 22, 38x3) ldy currentScreen ; 28E7 which screen is up beq L_28EE ; 28EA 0 = battlefield: use window 3 ldx #$04 ; 28EC window 4 = the same box with the other frame style L_28EE: jmp drawWindowFrame ; 28EE draw it and return ; ---------------------------------------------------------------------- ; computeGameResult - Turns the state at the end of a game into the two final point totals and the ; result message. The score pair $92A2/$92A4 and $92A3/$92A5 is indexed by the side that was scored ; AGAINST, so the larger entry belongs to the losing side. On points: equal totals give 250 each and ; message $13 'IT WAS A DRAW.', otherwise the winner gets 300 + min(difference/2, 150) and the loser ; 200, with message $11 'WE LOST ON POINTS!' or $12 'WE WON ON POINTS!'. On a knockout (gameEndReason ; bit 7): both comcens down is again a draw, otherwise the winner gets 500, the loser 150 and message ; $14 'WE WERE KNOCKED OUT!' or $15 'WE WON BY A KNOCKOUT!'. The chosen message is queued at the end ; and the status line is released. Y is preserved through a self-modified LDY # at $29B0. ; In: Y (returned unchanged), gameEndReason $0BA8 ($08 = clock ran out, $A0/$C0 = the comcen of side ; 0/1 was knocked out), playerSide $0B9F, the running scores ; sideScoreLo/side1ScoreLo/sideScoreHi/side1ScoreHi $92A2-$92A5 ; Out: $92A2-$92A5 rewritten with the final points, gameOverFlags $923F = $C0, persistentMessageId ; $92A1 = $9E and chatMessageLine $0548 = $9E (status line free), chatState $929C = 0, messages ; $16 and the result message queued; scratch18/scratch19 clobbered; self-modifies $29B0+1 ; Called from: endGame ($4032) ; ---------------------------------------------------------------------- computeGameResult: sty L_29B0+1 ; 28F1 save Y in the LDY # at $29B0 so the caller gets it back jsr resetMessageQueue ; 28F4 throw away anything still queued for the status line lda #$C0 ; 28F7 bits 6-7 = the game is over and player input is locked out sta gameOverFlags ; 28F9 game-over flags lda gameEndReason ; 28FC why the game ended bmi L_2976 ; 28FF bit 7 set = a comcen was knocked out lda #$16 ; 2901 message $16 'GAME OVER.' jsr queueMessage ; 2903 queue it ; ---------------------------------------------------------------------- ; Decide the result on points. The 16-bit totals are indexed by the side that was scored against, so ; subtracting them tells which side lost and by how much. ; ---------------------------------------------------------------------- lda sideScoreLo ; 2906 points scored against side 0, low byte sec ; 2909 no borrow sbc side1ScoreLo ; 290A minus the points scored against side 1 sta scratch18 ; 290D difference low byte lda sideScoreHi ; 290F high bytes... sbc side1ScoreHi ; 2912 ...with the borrow sta scratch19 ; 2915 difference high byte ora scratch18 ; 2917 is the difference zero? bne L_292F ; 2919 no: somebody won on points ; ---------------------------------------------------------------------- ; A tie: both sides get 250 points. setOtherPlayerPoints flips X each time, so the two calls write ; slot 1 and then slot 0. ; ---------------------------------------------------------------------- L_291B: ldx #$00 ; 291B start from side 0 ldy #$FA ; 291D 250 points jsr setOtherPlayerPoints ; 291F writes the total of the other side (1) and leaves X = 1 ldy #$FA ; 2922 250 points again jsr setOtherPlayerPoints ; 2924 writes the total of side 0 and leaves X = 0 lda #$13 ; 2927 message $13 'IT WAS A DRAW.' sta persistentMessageId ; 2929 remember it as the result message jmp L_299D ; 292C queue it and finish ; ---------------------------------------------------------------------- ; Somebody won: make the difference positive and remember which side lost (X). The winner's points go ; into the loser's score slot, because that slot counts what was scored against the loser. ; ---------------------------------------------------------------------- L_292F: ldx #$00 ; 292F assume side 0 was outscored bcs L_2946 ; 2931 no borrow: more was scored against side 0, so side 0 lost lda scratch18 ; 2933 otherwise negate the difference... eor #$FF ; 2935 ... clc ; 2937 ... adc #$01 ; 2938 ... sta scratch18 ; 293A ...low byte lda scratch19 ; 293C ... eor #$FF ; 293E ... adc #$00 ; 2940 ... sta scratch19 ; 2942 ...high byte ldx #$01 ; 2944 side 1 is the loser L_2946: lsr scratch19 ; 2946 half the point difference... ror scratch18 ; 2948 ...16 bits lda scratch19 ; 294A high byte of half the difference bne L_2954 ; 294C over 255: clamp lda scratch18 ; 294E low byte cmp #$96 ; 2950 150 = the cap on the winning margin bcc L_2956 ; 2952 below the cap: use it L_2954: lda #$96 ; 2954 clamp to 150 L_2956: clc ; 2956 no carry adc #$2C ; 2957 $012C = 300 base points for the winner sta sideScoreLo,x ; 2959 into the loser's slot, which holds the winner's total lda #$00 ; 295C high byte... adc #$01 ; 295E ...= 1 plus any carry from the 300 + margin add sta sideScoreHi,x ; 2960 winner's total, high byte ldy #$11 ; 2963 message $11 'WE LOST ON POINTS!' cpx playerSide ; 2965 is the losing side ours? beq L_296B ; 2968 yes: keep message $11 iny ; 296A no: message $12 'WE WON ON POINTS!' L_296B: sty persistentMessageId ; 296B remember the result message ldy #$C8 ; 296E 200 consolation points jsr setOtherPlayerPoints ; 2970 into the winner's slot, i.e. the loser's total jmp L_299D ; 2973 queue the message, free the status line and restore Y ; ---------------------------------------------------------------------- ; Knockout ending of computeGameResult (reached from $28FF when gameEndReason $0BA8 bit 7 is set). ; Bits 5-6 of gameEndReason were built at $3BBC-$3BC9 from the destroyed comcen's side bit and ORed ; into the old value, so $20 = side 0's comcen died, $40 = side 1's, $60 = both died in one exchange. ; X ends up holding the LOSING side. Both score slots are indexed by the OPPONENT of the side they ; belong to: hits are added at $3B88 with X = the side of the unit that was hit, and ; drawStatusScoreLine prints slot (playerSide EOR 1) as 'US'. Storing into slot X therefore credits ; the winner, and setOtherPlayerPoints fills in the loser's slot. ; ---------------------------------------------------------------------- L_2976: ldx #$00 ; 2976 assume side 0 is the loser (X = index of the knocked-out side) and #$60 ; 2978 keep only the two knockout bits of gameEndReason (bit 5 = side 0 lost its comcen, bit 6 = side 1) cmp #$60 ; 297A both comcens destroyed in the same exchange? beq L_291B ; 297C yes - reuse the draw path at $291B: 250 points each and message $13 'IT WAS A DRAW.' cmp #$40 ; 297E otherwise test the side-1 bit ($40) on its own bcc L_2983 ; 2980 only bit 5 set - side 0 was knocked out, leave X = 0 inx ; 2982 bit 6 set - side 1 was knocked out, so the loser is side 1 ; ---------------------------------------------------------------------- ; Award the knockout scores: 500 to the winner, 150 to the loser. ; ---------------------------------------------------------------------- L_2983: lda #$F4 ; 2983 low byte of the winner's final total, $01F4 = 500 points sta sideScoreLo,x ; 2985 slot X is the loser's opponent, i.e. the winner lda #$01 ; 2988 high byte of 500 sta sideScoreHi,x ; 298A store the winner's 16-bit total ldy #$14 ; 298D message $14 = 'WE WERE KNOCKED OUT!' (string $87BA) cpx playerSide ; 298F was the knocked-out side the side this machine plays? beq L_2995 ; 2992 yes - keep the 'we were knocked out' text iny ; 2994 no - message $15 = 'WE WON BY A KNOCKOUT!' (string $87CE) L_2995: sty persistentMessageId ; 2995 remember the result message id so the status line can reshow it ldy #$96 ; 2998 the knocked-out side keeps only $96 = 150 points jsr setOtherPlayerPoints ; 299A write 150 into the other slot (X EOR 1), which belongs to the loser ; ---------------------------------------------------------------------- ; Common tail of every ending (draw, points and knockout): show the result once, then park the status ; line and leave chat entry. ; ---------------------------------------------------------------------- L_299D: lda persistentMessageId ; 299D the result message id chosen above ($11-$15) jsr queueMessage ; 29A0 push it onto the status-line message queue ($C566) lda #$9E ; 29A3 $9E = the 'nothing pending' marker used by the message system sta persistentMessageId ; 29A5 stop the status line from repeating the result message sta chatMessageLine ; 29A8 and blank the chat text line slot at $0548 the same way lda #$00 ; 29AB leave chat/typing mode sta chatState ; 29AD clear the chat state machine at $929C L_29B0: ldy #$FF ; 29B0 operand patched at $28F1 with the caller's Y, so Y is returned unchanged rts ; 29B2 back to endGame ($4032) ; ---------------------------------------------------------------------- ; setOtherPlayerPoints - Writes Y as the 16-bit point total of the side OTHER than X and returns with ; X already flipped. The two totals live in the parallel slots sideScoreLo/sideScoreHi ($92A2/$92A3 ; and ; $92A4/$92A5) which are indexed by the opponent of the side that owns them, so computeGameResult uses ; this to fill in the second player after storing the first with sideScoreLo,x. ; In: X = side index 0/1, Y = the point value (0-255) ; Out: sideScoreLo[X EOR 1] = Y, sideScoreHi[X EOR 1] = 0, X = X EOR 1, A = 0 ; Called from: computeGameResult ($291F, $2924 draw path; $2970 points path; $299A knockout path) ; ---------------------------------------------------------------------- setOtherPlayerPoints: txa ; 29B3 take the side index that was just written eor #$01 ; 29B4 flip it: 0 <-> 1 tax ; 29B6 index the other side's score slot tya ; 29B7 the value the caller passed in Y sta sideScoreLo,x ; 29B8 low byte of that side's 16-bit total lda #$00 ; 29BB result totals are always below 256 ... sta sideScoreHi,x ; 29BD ... so clear the high byte rts ; 29C0 X is left flipped, which is what the caller relies on ; ---------------------------------------------------------------------- ; shakeScreenScroll - One frame of the screen-shake effect used when a comcen is hit. Takes one byte ; from the game PRNG and offers bits 0-2 as a new $D011 fine Y scroll and bits 3-5 as a new $D016 fine ; X scroll; each is used only if it is non-zero and smaller than the remaining shake amount ; screenShakeTimer, otherwise the resting values ($0B for $D011, $08 for $D016) are put back. The ; chosen low nibbles are merged into the shadow registers vicCtrl1Shadow/vicCtrl2Shadow (the raster ; IRQ copies those to the VIC) and kept in spriteScrollFineY/spriteScrollFineX so the IRQ can shift ; the sprites by the same amount at $1174-$1186. Because the low nibble is replaced wholesale, an ; accepted shake value also clears bit 3 (RSEL/CSEL), so a shaking frame drops to 24 rows / 38 columns ; - part of the jolt. ; In: screenShakeTimer ($92C8), vicCtrl1Shadow ($9000), vicCtrl2Shadow ($9001), PRNG state zp_57-59 ; Out: vicCtrl1Shadow, vicCtrl2Shadow, spriteScrollFineY ($92F5), spriteScrollFineX ($92F4); ; self-modified operands at $29D6, $29E6, $29F3, $2A03 ; Called from: the battle main loop at $41C7, once per frame while screenShakeTimer counts down ; ---------------------------------------------------------------------- shakeScreenScroll: jsr nextGameRandom ; 29C1 one byte from the 24-bit game PRNG at $FD5D (both machines step it in lock step) sta L_29D5+1 ; 29C4 patch the LDA #imm at $29D5 with the raw random byte (vertical candidate) lsr a ; 29C7 shift the byte down three places ... lsr a ; 29C8 ... so that bits 3-5 become bits 0-2 ... lsr a ; 29C9 ... giving an independent horizontal candidate sta L_29F2+1 ; 29CA patch the LDA #imm at $29F2 with it lda vicCtrl1Shadow ; 29CD current $D011 shadow and #$F0 ; 29D0 keep bits 4-7 (DEN, BMM, ECM, RST8) and drop the RSEL/Y-scroll nibble sta L_29E5+1 ; 29D2 patch the ORA #imm at $29E5 with the preserved high nibble ; ---------------------------------------------------------------------- ; Vertical jitter: pick a new $D011 low nibble. ; ---------------------------------------------------------------------- L_29D5: lda #$FF ; 29D5 operand patched at $29C4 = the random byte and #$07 ; 29D7 reduce it to a 0-7 fine Y scroll beq L_29E0 ; 29D9 0 would not move anything - use the resting value instead cmp screenShakeTimer ; 29DB only accept a jitter smaller than the shake energy still left bcc L_29E2 ; 29DE accepted: this frame is displaced by A raster lines L_29E0: lda #$0B ; 29E0 resting $D011 low nibble: RSEL 25 rows (bit 3) + Y-scroll 3 L_29E2: sta spriteScrollFineY ; 29E2 remember it so the IRQ can offset the sprites by the same amount L_29E5: ora #$FF ; 29E5 operand patched at $29D2 = the preserved bits 4-7 of $D011 sta vicCtrl1Shadow ; 29E7 the raster IRQ writes this shadow to $D011 ; ---------------------------------------------------------------------- ; Horizontal jitter: the same again for the $D016 low nibble. ; ---------------------------------------------------------------------- lda vicCtrl2Shadow ; 29EA current $D016 shadow and #$F0 ; 29ED keep bits 4-7 (MCM and the unused high bits) and drop the CSEL/X-scroll nibble sta L_2A02+1 ; 29EF patch the ORA #imm at $2A02 with it L_29F2: lda #$FF ; 29F2 operand patched at $29CA = the random byte shifted down 3 and #$07 ; 29F4 reduce it to a 0-7 fine X scroll beq L_29FD ; 29F6 0 would not move anything cmp screenShakeTimer ; 29F8 again, only a jitter below the remaining shake energy bcc L_29FF ; 29FB accepted: this frame is displaced by A pixels sideways L_29FD: lda #$08 ; 29FD resting $D016 low nibble: CSEL 40 columns (bit 3) + X-scroll 0 L_29FF: sta spriteScrollFineX ; 29FF remember it for the sprite offset at $117F L_2A02: ora #$FF ; 2A02 operand patched at $29EF = the preserved bits 4-7 of $D016 sta vicCtrl2Shadow ; 2A04 the raster IRQ writes this shadow to $D016 rts ; 2A07 the caller decrements screenShakeTimer afterwards ; ---------------------------------------------------------------------- ; patchUnitSideBranches - Re-points the two 'is this unit mine?' branches at the viewpoint currently ; being drawn. Overlay A stores $90 (BCC) or $B0 (BCS) at $2840 from playerSide (see $7BC6), which ; makes the 'cpy #$32' unit-index test at $283E select the local side. This routine copies that ; opcode byte into the following visibility test at $2846 (renderMapTile) and into its twin at $5BFE ; (the overview map renderer), inverting BCC <-> BCS with EOR $20 when filmPlaybackMode bit 0 is set, ; i.e. when a game film is being replayed from the opponent's point of view (viewpoint 1 = THEM). ; Viewpoint 2 = BOTH has bit 0 clear and is handled separately by the 'cmp #$82' tests at $284B/$5C03, ; which reveal everything. ; In: filmPlaybackMode ($0B9B) bit 0, the opcode byte at $2840 ; Out: the opcode bytes at $2846 and $5BFE ; Called from: start-up ($0AD9), the battlefield screen rebuild ($2639) and the replay viewpoint key ; ($4134) ; ---------------------------------------------------------------------- patchUnitSideBranches: lda filmPlaybackMode ; 2A08 film playback flags: bit 7 = replaying, bits 0-1 = viewpoint 0 US / 1 THEM / 2 BOTH lsr a ; 2A0B shift viewpoint bit 0 into the carry lda ownSideBranchOpcode ; 2A0C the side-select branch opcode overlay A wrote: $90 = BCC (side 0), $B0 = BCS (side 1) bcc L_2A13 ; 2A0F viewpoint bit 0 clear (US or BOTH) - use the opcode unchanged eor #$20 ; 2A11 viewpoint THEM - flip BCC <-> BCS so the enemy's units count as 'ours' L_2A13: sta unitVisibleBranchOpcode ; 2A13 patch the opcode of the branch at $2846 inside renderMapTile sta D_5BFE ; 2A16 and the identical branch at $5BFE in the overview-map renderer rts ; 2A19 the two renderers now test 'is this unit on the viewed side?' the right way round ; ---------------------------------------------------------------------- ; printOptionWord - Prints one of the three 4-byte DAMAGE option words held at $0652 in the message ; block - 'FULL' ($0652), 'HALF' ($0656), 'NONE' ($065A), each bit-7 terminated - through the text ; engine's string printer $C133. The overlays call it with the per-side damage setting recyclerMode ; ($92A6,side) to fill the XXX field of the rules summary line at $0636. ; In: A = word index 0 FULL / 1 HALF / 2 NONE ; Out: the word is printed at the current text cursor; A/Y = the string address handed to printString ; Called from: the setup screens of both $6F00 overlays ($843B, $85D8, $85E9) ; ---------------------------------------------------------------------- printOptionWord: asl a ; 2A1A index * 2 ... asl a ; 2A1B ... * 4: the words are 4 bytes apart clc ; 2A1C prepare the 16-bit add adc #$52 ; 2A1D low byte of the 'FULL' entry, $0652 pha ; 2A1F keep the low byte while the high byte is built lda #$00 ; 2A20 start the high byte at 0 ... adc #$06 ; 2A22 ... plus $06 and any carry, giving $06xx tay ; 2A24 printString wants the high byte in Y pla ; 2A25 and the low byte in A jmp printString ; 2A26 tail-call the text engine's string printer ; Parameter block shared by every glyph plotter in this file and by both $6F00 overlays (all seven ; bytes just happen to be initialised to $01). Every screen builder sets them before calling ; drawWindowFrame, drawCharBlock, fillScreenColourRect or drawCharAt: ; +0 useDrawColourFlag non-zero = ignore the per-glyph colour table and colour with drawColourByte ; +1 drawColourByte the screen-RAM colour byte (high nibble foreground, low nibble background) ; +2 blockFillFlag non-zero = drawCharBlock ignores its character data and fills with ; blockFillChar ; +3 blockFillChar the character used for that fill ; +4 windowOriginRow top screen row (0-24) of the window the plotters draw into ; +5 windowOriginCol left screen column (0-39) of that window ; +6 (spare) never read by any traced code useDrawColourFlag: .byte $01 ; 2A29 . useDrawColourFlag: read by plotGlyph at $2AD1; 0 = take the colour from fontColorTable drawColourByte: .byte $01 ; 2A2A drawColourByte: written by fillScreenColourRect and used by plotGlyph and $2A85 blockFillFlag: .byte $01 ; 2A2B blockFillFlag: tested by drawCharBlock at $2B54 blockFillChar: .byte $01 ; 2A2C blockFillChar: the glyph drawCharBlock repeats when the flag is set windowOriginRow: .byte $01 ; 2A2D windowOriginRow: top row used by fillScreenColourRect, drawCharBlock, drawFrameBox, magnifyWindow2x windowOriginCol: .byte $01 ; 2A2E windowOriginCol: left column for the same routines; the 7th byte is a spare nobody reads .byte $01 ; 2A2F . ; The two opcode tables that drive the four draw modes. Mode 0 replace, 1 OR, 2 EOR, 3 keep existing ; (the CMP just throws the new value away, leaving the byte that was read first). bitmapDrawOpcodes: .byte $B1,$11,$51,$D1 ; 2A30 ..Q. (zp),Y opcodes for the bitmap combine at $2AF3/$C22B: $B1 LDA, $11 ORA, $51 EOR, $D1 CMP ; colourDrawOpcodes: byteTable, 4 bytes. opcodes LDA/ORA/EOR/CMP abs ($AD,$0D,$4D,$CD) for the colour ; draw modes 0-3 colourDrawOpcodes: .byte $AD,$0D,$4D,$CD ; 2A34 ..M. absolute opcodes for the colour combine at $2B00/$2A85: $AD LDA, $0D ORA, $4D EOR, $CD CMP ; ---------------------------------------------------------------------- ; setBitmapDrawMode - Chooses how a glyph's eight bitmap bytes are combined with what is already in ; the ; $A000 bitmap. It overwrites the opcode of the second LDA of the read-modify-write pair inside ; plotGlyph ; ($2AF3) and of the identical pair in the text engine's own glyph plotter ($C22B), so mode 0 ; replaces, 1 ORs the glyph in, 2 EORs it (used for highlighting) and 3 leaves the bitmap untouched. ; A is preserved through the self-modified operand at $2A45. ; In: Y = draw mode 0-3, A = value to preserve ; Out: the opcode bytes at $2AF3 and $C22B; A unchanged ; Called from: resetDrawModes ($2A5B) and the setup/menu screens of both overlays ($701E, $7AA1) ; ---------------------------------------------------------------------- setBitmapDrawMode: sta L_2A44+1 ; 2A38 stash A in the operand of the LDA #imm at $2A44 so it survives lda bitmapDrawOpcodes,y ; 2A3B pick the (zp),Y opcode for this mode sta glyphBitmapOpcode ; 2A3E patch the bitmap combine instruction inside plotGlyph sta D_C22B ; 2A41 and the same instruction in the $C000 text engine's plotter L_2A44: lda #$FF ; 2A44 operand patched at $2A38 - restores the caller's A rts ; 2A46 back to the caller with A intact ; ---------------------------------------------------------------------- ; setColourDrawMode - The colour-plane twin of setBitmapDrawMode: chooses how drawColourByte is ; combined with the colour byte already in screen RAM by patching the absolute LDA at $2B00 (inside ; plotGlyph) and at $2A85 (inside fillScreenColourRect). Mode 0 replaces, 1 ORs, 2 EORs ; (xorColourBlock2x2 uses this to flash a tile), 3 keeps the existing colour. A is preserved through ; the operand at $2A54. ; In: Y = draw mode 0-3, A = value to preserve ; Out: the opcode bytes at $2B00 and $2A85; A unchanged ; Called from: resetDrawModes ($2A5E), xorColourBlock2x2 ($2BD0) and overlay screen code ($7B5B) ; ---------------------------------------------------------------------- setColourDrawMode: sta L_2A53+1 ; 2A47 stash A in the operand of the LDA #imm at $2A53 lda colourDrawOpcodes,y ; 2A4A pick the absolute opcode for this mode sta glyphColourOpcode ; 2A4D patch the colour combine inside plotGlyph sta colourFillOpcode ; 2A50 and the one inside fillScreenColourRect L_2A53: lda #$FF ; 2A53 operand patched at $2A47 - restores the caller's A rts ; 2A55 back to the caller with A intact ; ---------------------------------------------------------------------- ; resetDrawModes - Puts both planes back to mode 0 (plain replace) after a highlight or overlay ; effect. Y is preserved through the self-modified operand at $2A62. ; In: Y = value to preserve ; Out: the opcodes at $2AF3, $C22B, $2B00 and $2A85 are all LDA again; Y unchanged ; Called from: $1689 and xorColourBlock2x2 ($2BDA) here, and from the overlays ($7033, $70F2, $7AB6, ; $7B40, $7B86, $810E) ; ---------------------------------------------------------------------- resetDrawModes: sty L_2A61+1 ; 2A56 stash Y in the operand of the LDY #imm at $2A61 ldy #$00 ; 2A59 mode 0 = replace jsr setBitmapDrawMode ; 2A5B patch the two bitmap combine instructions jsr setColourDrawMode ; 2A5E patch the two colour combine instructions L_2A61: ldy #$FF ; 2A61 operand patched at $2A56 - restores the caller's Y rts ; 2A63 back to the caller with Y intact ; ---------------------------------------------------------------------- ; fillScreenColourRect - Paints an X columns by Y rows rectangle of the multicolour bitmap's colour ; cells (screen RAM at $8C00, 40 bytes per row) starting at the window origin ; windowOriginRow/windowOriginCol with the colour byte A, combined according to the current colour ; draw mode. Only the colour plane is touched - the bitmap itself keeps whatever it held, which is ; how panels are recoloured without redrawing. ; In: A = screen-RAM colour byte, X = width in cells (1-40), Y = height in rows, ; windowOriginRow/windowOriginCol, the mode opcode at $2A85 ; Out: the rectangle recoloured; drawColourByte = A; zp_B6/B7, zp_1B (rows left) and zp_1C (width-1) ; clobbered ; Called from: everywhere - $1782, $18DC, $18EC, $18FC, $1909, $2BD7 and jumps at $16B4, $1916, $192A ; here, plus a dozen sites in each $6F00 overlay ; ---------------------------------------------------------------------- fillScreenColourRect: sta drawColourByte ; 2A64 remember the colour byte where the patched instruction at $2A85 can read it sty scratch1B ; 2A67 zp_1B counts the rows still to fill ldy windowOriginRow ; 2A69 the window's top screen row lda screenRowHiTable,y ; 2A6C high byte of $8C00 + row*40 from the screen row table sta screenPtrHi ; 2A6F high byte of the first cell of the top row lda screenRowLoTable,y ; 2A71 low byte of $8C00 + row*40 clc ; 2A74 clear the carry before adding the column offset adc windowOriginCol ; 2A75 step right to the window's left column sta screenPtr ; 2A78 zp_B6/B7 now points at the first cell of the top row bcc L_2A7E ; 2A7A no carry out of the column add inc screenPtrHi ; 2A7C carry - bump the high byte L_2A7E: dex ; 2A7E width-1 = the highest column offset ... stx scratch1C ; 2A7F ... kept in zp_1C as the per-row start index ; ---------------------------------------------------------------------- ; Outer loop: one screen row per pass. Inner loop walks the row right to left. ; ---------------------------------------------------------------------- L_2A81: ldy scratch1C ; 2A81 start each row at its rightmost cell L_2A83: lda (screenPtr),y ; 2A83 read the cell's current colour (needed by the OR/EOR/keep modes) colourFillOpcode: lda drawColourByte ; 2A85 opcode patched by setColourDrawMode: LDA / ORA / EOR / CMP drawColourByte sta (screenPtr),y ; 2A88 write the combined colour back dey ; 2A8A next cell to the left bpl L_2A83 ; 2A8B until offset 0 has been done dec scratch1B ; 2A8D one row finished beq L_2A9E ; 2A8F all rows done clc ; 2A91 clear the carry before the row step lda screenPtr ; 2A92 advance the pointer ... adc #$28 ; 2A94 ... by one screen row (40 bytes) sta screenPtr ; 2A96 pointer now addresses the same column one row down bcc L_2A81 ; 2A98 no page crossed inc screenPtrHi ; 2A9A carry into the high byte bne L_2A81 ; 2A9C always taken - back for the next row L_2A9E: rts ; 2A9E the rectangle is filled ; ---------------------------------------------------------------------- ; calcCellPointers - Loads the character cell coordinates from scratch18 (row) and scratch19 (column) ; and falls into calcCellPointersXY. This is the entry every routine in this file uses. ; In: scratch18 = screen row 0-24, scratch19 = screen column 0-39 ; Out: as calcCellPointersXY; Y = row, X = column ; Called from: $26EA, drawCharAt ($2AC4), drawCharBlock ($2B4E), setMagnifyCellPointers ($2F44 and the ; tail jump at $2F67) and $5B15 ; ---------------------------------------------------------------------- calcCellPointers: ldy scratch18 ; 2A9F Y = character row ldx scratch19 ; 2AA1 X = character column ; ---------------------------------------------------------------------- ; calcCellPointersXY - Turns a character cell (Y = row, X = column) into the two pointers every ; plotter needs: bitmapPtr = $A000 + row*320 + column*8 (the eight bytes of that cell in the ; multicolour bitmap) and screenPtr = $8C00 + row*40 + column (its colour byte). Both are looked up ; in the six address tables at $9300 rather than multiplied: bitmap row lo/hi at $9300/$9319 (25 ; entries), column*8 lo/hi at ; $9332/$935A (40 entries) and screen row lo/hi at $9382/$939B (25 entries). ; In: Y = screen row 0-24, X = screen column 0-39 ; Out: bitmapPtr/bitmapPtrHi (zp_B4/B5), screenPtr/screenPtrHi (zp_B6/B7); X and Y unchanged ; Called from: $5CB6, $5D3E here, the text engine's cell setter ($C0A7) and the overlays ($864B) ; ---------------------------------------------------------------------- calcCellPointersXY: lda bitmapRowLoTable,y ; 2AA3 low byte of $A000 + row*320 clc ; 2AA6 clear the carry for the 16-bit add adc bitmapColLoTable,x ; 2AA7 plus column*8 (low) sta bitmapPtr ; 2AAA low byte of the cell's bitmap address lda bitmapRowHiTable,y ; 2AAC high byte of $A000 + row*320 adc bitmapColHiTable,x ; 2AAF plus column*8 (high: 0 for columns 0-31, 1 for 32-39) sta bitmapPtrHi ; 2AB2 zp_B4/B5 = address of the cell's 8 bitmap bytes txa ; 2AB4 the column is also the byte offset within a screen row clc ; 2AB5 clear the carry for the screen-RAM add adc screenRowLoTable,y ; 2AB6 plus the low byte of $8C00 + row*40 sta screenPtr ; 2AB9 low byte of the cell's screen-RAM address lda screenRowHiTable,y ; 2ABB high byte of $8C00 + row*40 ($8C-$8F) adc #$00 ; 2ABE add only the carry - the screen fits in four pages sta screenPtrHi ; 2AC0 zp_B6/B7 = address of the cell's colour byte rts ; 2AC2 both pointers are ready for plotGlyph ; ---------------------------------------------------------------------- ; drawCharAt - Draws character A at screen row scratch18, column scratch19: computes the cell pointers ; and falls straight into plotGlyph. ; In: A = character code 0-113, scratch18 = row, scratch19 = column ; Out: see plotGlyph ; Called from: drawWindowInnerCorners, drawTallGlyph, drawFrameBox, drawFrameRow/Column here ($2B95, ; $2B9E, $2BA7, $2BB3, $2BBD, $2BC8, $2CC5, $2CD0, $2CDB, $2CE7, $2D23, $2D35) and from the ; overlays ; ($6F6F, $6F76, $6F7D) ; ---------------------------------------------------------------------- drawCharAt: pha ; 2AC3 the cell pointer helper clobbers A jsr calcCellPointers ; 2AC4 work out where row/column lands in the bitmap and in screen RAM pla ; 2AC7 get the character code back ; ---------------------------------------------------------------------- ; plotGlyph - Draws one 8x8 glyph of the game's own font (114 glyphs at $9426, 8 bytes each) into the ; bitmap cell at bitmapPtr, combining the bytes according to the bitmap draw mode, then sets the ; cell's colour at screenPtr. The colour is the glyph's own default from fontColorTable ($93B4) when ; useDrawColourFlag is 0 and that entry is non-zero; otherwise it is drawColourByte combined according ; to the colour draw mode. When the display is in multicolour mode ($D016 bit 4 set in ; vicCtrl2Shadow) the routine also writes textColour into the matching colour RAM cell, reached by ; adding $4C to the high byte of screenPtr ($8C00 + $4C00 = $D800). ; In: A = character code 0-113; bitmapPtr/screenPtr from calcCellPointers; useDrawColourFlag, ; drawColourByte, textColour (zp_3D), vicCtrl2Shadow; the mode opcodes at $2AF3 and $2B00 ; Out: 8 bitmap bytes, 1 screen-RAM colour byte and optionally 1 colour RAM byte written; glyphPtr ; (zp_B2/B3) = the glyph's font address; screenPtrHi is left pointing into colour RAM in ; multicolour mode; A/X/Y clobbered ; Called from: drawCharBlock ($2B5F) and by falling through from drawCharAt ; ---------------------------------------------------------------------- plotGlyph: tay ; 2AC8 keep the character code in Y for the font-colour lookup lda #$00 ; 2AC9 clear the glyph pointer before building it sta glyphPtr ; 2ACB dead store - glyphPtr is overwritten at $2AE7 before it is used sta glyphPtrHi ; 2ACD clear the high byte so the three ROLs below can shift into it lda #$00 ; 2ACF default colour value 0 = 'this glyph has no colour of its own' ldx useDrawColourFlag ; 2AD1 does the caller insist on drawColourByte for every glyph? bne L_2AD9 ; 2AD4 yes - keep the 0 so the fill colour is used lda fontColorTable,y ; 2AD6 no - take this glyph's default colour byte from the 114-entry table at $93B4 L_2AD9: tax ; 2AD9 park it in X across the glyph copy tya ; 2ADA character code back into A asl a ; 2ADB code * 2 ... rol glyphPtrHi ; 2ADC carry the overflow into the high byte asl a ; 2ADE ... * 4 ... rol glyphPtrHi ; 2ADF carry again asl a ; 2AE1 ... * 8: each glyph is 8 bytes rol glyphPtrHi ; 2AE2 and again - glyphPtrHi now holds bits 8-10 of code*8 clc ; 2AE4 clear the carry for the base-address add adc #$26 ; 2AE5 add the low byte of the font base $9426 sta glyphPtr ; 2AE7 low byte of $9426 + code*8 lda glyphPtrHi ; 2AE9 bits 8-10 of code*8 adc #$94 ; 2AEB and its high byte sta glyphPtrHi ; 2AED zp_B2/B3 now points at the glyph's 8 rows ; ---------------------------------------------------------------------- ; Copy the glyph into the bitmap cell, bottom row first. ; ---------------------------------------------------------------------- ldy #$07 ; 2AEF 8 pixel rows, counted 7 down to 0 L_2AF1: lda (bitmapPtr),y ; 2AF1 read the byte already in the bitmap (only the OR/EOR/keep modes need it) glyphBitmapOpcode: lda (glyphPtr),y ; 2AF3 opcode patched by setBitmapDrawMode: LDA / ORA / EOR / CMP (glyphPtr),y sta (bitmapPtr),y ; 2AF5 store the combined pixel row dey ; 2AF7 next pixel row up bpl L_2AF1 ; 2AF8 until row 0 has been written ; ---------------------------------------------------------------------- ; Now the colour plane: one screen-RAM byte, plus a colour RAM byte in multicolour mode. ; ---------------------------------------------------------------------- iny ; 2AFA Y = 0, the single colour byte of this cell txa ; 2AFB the glyph's default colour saved in X bne L_2B03 ; 2AFC non-zero - write it straight in, ignoring the draw mode lda (screenPtr),y ; 2AFE otherwise read the colour that is already there glyphColourOpcode: lda drawColourByte ; 2B00 opcode patched by setColourDrawMode: LDA / ORA / EOR / CMP drawColourByte L_2B03: sta (screenPtr),y ; 2B03 store the cell colour into screen RAM at $8C00+ lda vicCtrl2Shadow ; 2B05 shadow of $D016 and #$10 ; 2B08 bit 4 = MCM; the game runs hires ($C8) for text and multicolour ($D8) for the map beq L_2B17 ; 2B0A hires - one colour byte is enough clc ; 2B0C clear the carry for the pointer bump lda screenPtrHi ; 2B0D high byte of the screen-RAM cell address adc #$4C ; 2B0F $8C00 + $4C00 = $D800: the same cell in colour RAM sta screenPtrHi ; 2B11 zp_B6/B7 now addresses the same cell in colour RAM lda textColour ; 2B13 the text engine's current colour supplies the third multicolour pair sta (screenPtr),y ; 2B15 third colour of the multicolour pixel pair L_2B17: rts ; 2B17 one cell drawn ; ---------------------------------------------------------------------- ; drawCharBlock - Draws a rectangular block of characters at windowOriginRow/windowOriginCol. A/Y ; point at the block: byte 0 = width in cells, byte 1 = height, then width*height character codes ; stored row by row. When blockFillFlag is non-zero the character data is ignored and every cell is ; drawn with blockFillChar instead (the 2-byte header is still read), which is how the screen builders ; erase a panel. The header values are also left in frameWidth/frameHeight for drawWindowInnerCorners. ; In: A/Y = lo/hi address of the block; windowOriginRow, windowOriginCol, blockFillFlag, ; blockFillChar ; Out: frameWidth ($9221) = width, frameHeight ($9222) = height; blockPtr (zp_B8/B9) = the character ; data; ; scratch18/19 = last cell, scratch1B = row, scratch1C = column, scratch1D = data index ; Called from: the main menu builder ($16A9) and the panel builders of both overlays ($6F13, $6F24, ; $6F53, ; $70A3, $70D6, $7B1B and the jump at $726E) ; ---------------------------------------------------------------------- drawCharBlock: sta blockPtr ; 2B18 keep the block address in zp_B8/B9 sty blockPtrHi ; 2B1A high byte of the block address ldy #$00 ; 2B1C read the 2-byte header through (blockPtr),y lda (blockPtr),y ; 2B1E header byte 0 = width in character cells sta frameWidth ; 2B20 also published for drawWindowInnerCorners inc blockPtr ; 2B23 step past the width byte bne L_2B29 ; 2B25 no page crossing inc blockPtrHi ; 2B27 carry into the high byte L_2B29: lda (blockPtr),y ; 2B29 header byte 1 = height in character rows sta frameHeight ; 2B2B also published for drawWindowInnerCorners inc blockPtr ; 2B2E step past the height byte - zp_B8/B9 now addresses the character data bne L_2B34 ; 2B30 no page crossing inc blockPtrHi ; 2B32 carry into the high byte L_2B34: lda #$00 ; 2B34 start at the top row of the block sta scratch1B ; 2B36 row counter = 0 sta scratch1D ; 2B38 index into the character data = 0 ; ---------------------------------------------------------------------- ; Row loop: scratch1B counts rows, scratch1C counts columns, scratch1D walks the data linearly. ; ---------------------------------------------------------------------- L_2B3A: lda #$00 ; 2B3A start at the left column of this row sta scratch1C ; 2B3C column counter back to 0 for this row lda windowOriginRow ; 2B3E the window's top row ... clc ; 2B41 clear the carry for the row add adc scratch1B ; 2B42 ... plus the row we are on sta scratch18 ; 2B44 target screen row for drawCharAt ; ---------------------------------------------------------------------- ; Column loop: one character per cell. ; ---------------------------------------------------------------------- L_2B46: lda windowOriginCol ; 2B46 the window's left column ... clc ; 2B49 clear the carry for the column add adc scratch1C ; 2B4A ... plus the column we are on sta scratch19 ; 2B4C target screen column jsr calcCellPointers ; 2B4E point zp_B4-zp_B7 at that cell lda blockFillChar ; 2B51 assume the fill character ldx blockFillFlag ; 2B54 is this a plain fill rather than a character block? bne L_2B5F ; 2B57 yes - draw blockFillChar and leave the data untouched ldy scratch1D ; 2B59 no - index the next byte of the block data lda (blockPtr),y ; 2B5B fetch the character for this cell inc scratch1D ; 2B5D and advance the data index L_2B5F: jsr plotGlyph ; 2B5F draw it into the cell the pointers already address inc scratch1C ; 2B62 next column lda scratch1C ; 2B64 column just drawn cmp frameWidth ; 2B66 have we reached the block's width? bcc L_2B46 ; 2B69 keep going until the whole row is drawn inc scratch1B ; 2B6B next row lda scratch1B ; 2B6D row just finished cmp frameHeight ; 2B6F have we reached the block's height? bcc L_2B3A ; 2B72 keep going until every row is drawn rts ; 2B74 the whole block is on screen ; ---------------------------------------------------------------------- ; drawWindowInnerCorners - Draws the small hollow-ring glyph $38 (font bytes at $95E6) in the four ; cells just inside the corners of the current window: rows windowOriginRow+1 and ; windowOriginRow+frameHeight-1, columns windowOriginCol+1 and windowOriginCol+frameWidth-1. It is ; always called straight after drawWindowFrame or drawCharBlock, so frameWidth/frameHeight are the ; extents those routines left behind. Only the two $6F00 overlays use it, to stud the corners of their ; menu and setup panels. ; In: windowOriginRow, windowOriginCol, frameWidth ($9221), frameHeight ($9222) ; Out: four characters drawn; scratch18/19 (cell), scratch1A (right column), scratch1B (bottom row) ; Called from: overlay panel builders only ($6FFD, $7092, $7A80, $7B0A, $7B4C) ; ---------------------------------------------------------------------- drawWindowInnerCorners: lda windowOriginRow ; 2B75 window top row sta scratch18 ; 2B78 scratch18 = the row of the top studs (bumped below) clc ; 2B7A clear the carry for the height add adc frameHeight ; 2B7B plus the height ... sta scratch1B ; 2B7E ... = the bottom row of the frame lda windowOriginCol ; 2B80 window left column sta scratch19 ; 2B83 scratch19 = the column of the left studs (bumped below) clc ; 2B85 clear the carry for the width add adc frameWidth ; 2B86 plus the width ... sta scratch1A ; 2B89 ... = the right column of the frame inc scratch18 ; 2B8B move one row down, inside the frame inc scratch19 ; 2B8D and one column right dec scratch1A ; 2B8F one column left of the right edge dec scratch1B ; 2B91 one row up from the bottom edge lda #$38 ; 2B93 glyph $38 = the small hollow ring at $95E6 jsr drawCharAt ; 2B95 top-left stud lda scratch1A ; 2B98 the column just inside the right edge sta scratch19 ; 2B9A move to the right-hand column lda #$38 ; 2B9C the same ring glyph again jsr drawCharAt ; 2B9E top-right stud lda scratch1B ; 2BA1 the row just inside the bottom edge sta scratch18 ; 2BA3 move down to the bottom row lda #$38 ; 2BA5 the same ring glyph again jsr drawCharAt ; 2BA7 bottom-right stud lda windowOriginCol ; 2BAA back to the window's left column ... sta scratch19 ; 2BAD back to the window's left column inc scratch19 ; 2BAF ... plus one, inside the frame lda #$38 ; 2BB1 the fourth ring glyph jsr drawCharAt ; 2BB3 bottom-left stud rts ; 2BB6 all four studs drawn ; ---------------------------------------------------------------------- ; drawTallGlyph - Draws one of the ten two-cell-tall symbols whose character-code pairs live at $9810 ; (20 bytes: top code at $9810+A*2, bottom code at $9810+A*2+1). The top half goes at ; scratch18/scratch19 and the bottom half one row lower; scratch18 is restored on the way out. Used ; by the overlays for the double-height icons on the comcen and setup screens. ; In: A = symbol index 0-9, scratch18 = top row, scratch19 = column ; Out: two characters drawn; scratch18/scratch19 unchanged ; Called from: overlay screens only ($6FD9, $6FE1, $6FE9, $7055, $7AD8, $7AE0) ; ---------------------------------------------------------------------- drawTallGlyph: asl a ; 2BB7 two character codes per symbol pha ; 2BB8 keep the doubled index across the first draw tax ; 2BB9 index the top half of the pair lda titleColorRow00Cell16,x ; 2BBA top-half character code (also graphicsData9300:tallDigitGlyphPairTable) jsr drawCharAt ; 2BBD draw the top cell inc scratch18 ; 2BC0 the bottom half sits one row lower pla ; 2BC2 get the doubled index back tax ; 2BC3 index it again for the bottom half inx ; 2BC4 the odd entry of the pair lda titleColorRow00Cell16,x ; 2BC5 bottom-half character code (also graphicsData9300:tallDigitGlyphPairTable) jsr drawCharAt ; 2BC8 draw the bottom cell dec scratch18 ; 2BCB put the caller's row back rts ; 2BCD both halves of the symbol are drawn ; ---------------------------------------------------------------------- ; xorColourBlock2x2 - Flashes one battlefield tile by EORing the colour byte A into the 2x2 screen-RAM ; colour cells at windowOriginRow/windowOriginCol (a map cell is 16x16 pixels = 2x2 character cells in ; the magnified view), then puts both draw modes back to plain replace. Calling it twice with the ; same value restores the original colours. ; In: A = colour byte to toggle, windowOriginRow/windowOriginCol ; Out: four screen-RAM colour bytes toggled; drawColourByte = A; both draw modes reset to 0 ; Called from: the overlays only ($75DA, $807F, $814D) ; ---------------------------------------------------------------------- xorColourBlock2x2: ldy #$02 ; 2BCE colour draw mode 2 = EOR jsr setColourDrawMode ; 2BD0 patch the two colour combine instructions ldx #$02 ; 2BD3 2 cells wide ... ldy #$02 ; 2BD5 ... and 2 cells tall = one map tile jsr fillScreenColourRect ; 2BD7 toggle the four colour bytes jmp resetDrawModes ; 2BDA restore replace mode and return through it ; The 27 predefined screen windows, held as four parallel 27-byte tables plus a table of frame ; tile-set pointers, all indexed by the window number passed to drawWindowFrame in X. The ones used ; by this file are 0 = the 22x22 overview panel at (0,0), 1 = the 16x12 battlefield view frame at ; column 24, 2 = the 16x11 console panel below it, 3 and 4 = the 38x3 message frame at row 22 (3 in ; battle, 4 elsewhere), ; $18/$19 = the full-width main-menu frames. Windows 5-$16 belong to the $6F00 overlays' setup, stats ; and ; comcen screens. windowLeftTable: .byte $00,$18,$18,$01,$01,$01,$02,$1B; 2BDD ........ left screen column of windows 0-7: overview 0, view 24, console 24, message 1, message 1, ... .byte $1F ; 2BE5 windows 8-15 .byte $1D,$1C,$1D,$1E,$01,$01,$0B; 2BE6 ....... .byte $0C ; 2BED windows $10-$17 .byte $1D,$20,$1E,$0D,$12,$0D,$0D; 2BEE . ..... .byte $00 ; 2BF5 windows $18-$1A: the two main-menu frames at columns 0 and 1, and a 30-wide box at column 5 .byte $01,$05 ; 2BF6 .. ; windowTopTable: byteTable, 27 bytes. Top screen row of windows 0-26 windowTopTable: .byte $00,$00,$0B,$16,$16,$01,$02,$0F; 2BF8 ........ top screen row of windows 0-7: overview 0, view 0, console 11, message 22, message 22, ... .byte $10 ; 2C00 windows 8-15 .byte $08,$00,$01,$02,$03,$0C,$04; 2C01 ....... .byte $05 ; 2C08 windows $10-$17 .byte $03,$04,$0A,$06,$0A,$06,$04; 2C09 ....... .byte $05 ; 2C10 windows $18-$1A .byte $06,$00 ; 2C11 .. ; windowWidthTable: byteTable, 27 bytes. Width in cells (including the frame) of windows 0-26; e.g. ; window 0 = 22x22 around the 20x20 overview map, window 1 = 16x12 around the 7x5 cell (14x10 char) ; battlefield view windowWidthTable: .byte $16,$10,$10,$26,$26,$19,$17,$0C; 2C13 ...&&... width in cells (frame included) of windows 0-7: 22, 16, 16, 38, 38, ... .byte $04 ; 2C1B windows 8-15 .byte $08,$0B,$09,$07,$09,$09,$12; 2C1C ....... .byte $10 ; 2C23 windows $10-$17 .byte $0B,$05,$0A,$0E,$04,$0E,$0E; 2C24 ....... .byte $28 ; 2C2B windows $18-$1A: 40, 38 and 30 cells wide .byte $26,$1E ; 2C2C &. ; windowHeightTable: byteTable, 27 bytes. Height in cells of windows 0-26 windowHeightTable: .byte $16,$0C,$0B,$03,$03,$15,$13,$07; 2C2E ........ height in cells (frame included) of windows 0-7: 22, 12, 11, 3, 3, ... .byte $04 ; 2C36 windows 8-15 .byte $07,$08,$06,$04,$09,$09,$10; 2C37 ....... .byte $0E ; 2C3E windows $10-$17 .byte $07,$04,$0B,$0C,$04,$0C,$0C; 2C3F ....... .byte $14 ; 2C46 windows $18-$1A: 20, 18 and 7 rows tall .byte $12,$07 ; 2C47 .. ; One little-endian pointer per window to its 9-byte frame tile set in $9824-$987D. A set is laid out ; top-left, top edge, top-right, left edge, centre (never used), right edge, bottom-left, bottom edge, ; bottom-right. $9824, $982D, $9836 and $983F are four different line styles, $9848 is all glyph $03 ; (a solid block) and $9851 is all glyph $00 (blank, i.e. the frame erases itself), while $985A, ; $9863, ; $986C and $9875 are the four heavy panel styles used by windows 0-3. windowFramePtrTable: .addr windowFrameTileSetDither; 2C49 window 0 - overview panel .byte $63 ; 2C4B window 1 - battlefield view .byte $98 ; 2C4C . .byte $6C ; 2C4D window 2 - console panel .byte $98 ; 2C4E . .byte $75 ; 2C4F window 3 - message frame during the battle .byte $98 ; 2C50 . .byte $24 ; 2C51 window 4 - message frame outside the battle .byte $98 ; 2C52 . .byte $36 ; 2C53 window 5 .byte $98 ; 2C54 . .byte $3F ; 2C55 window 6 .byte $98 ; 2C56 . .byte $2D ; 2C57 window 7 .byte $98 ; 2C58 . .byte $24 ; 2C59 window 8 .byte $98 ; 2C5A . .byte $2D ; 2C5B window 9 .byte $98 ; 2C5C . .byte $2D ; 2C5D window $0A .byte $98 ; 2C5E . .byte $24 ; 2C5F window $0B .byte $98 ; 2C60 . .byte $48 ; 2C61 window $0C - solid block frame .byte $98 ; 2C62 . .byte $36 ; 2C63 window $0D .byte $98 ; 2C64 . .byte $36 ; 2C65 window $0E .byte $98 ; 2C66 . .byte $36 ; 2C67 window $0F .byte $98 ; 2C68 . .byte $3F ; 2C69 window $10 .byte $98 ; 2C6A . .byte $2D ; 2C6B window $11 .byte $98 ; 2C6C . .byte $24 ; 2C6D window $12 .byte $98 ; 2C6E . .byte $2D ; 2C6F window $13 .byte $98 ; 2C70 . .byte $51 ; 2C71 window $14 - blank tiles, so drawing this window erases the frame .byte $98 ; 2C72 . .byte $51 ; 2C73 window $15 - blank tiles as well .byte $98 ; 2C74 . .byte $48 ; 2C75 window $16 - solid block frame .byte $98 ; 2C76 . .byte $48 ; 2C77 window $17 - solid block frame .byte $98 ; 2C78 . .byte $36 ; 2C79 window $18 - main menu outer frame .byte $98 ; 2C7A . .byte $3F ; 2C7B window $19 - main menu inner frame .byte $98 ; 2C7C . .byte $2D ; 2C7D window $1A .byte $98 ; 2C7E . ; ---------------------------------------------------------------------- ; drawWindowFrame - Draws one of the 27 predefined windows selected by X. Copies its left column, top ; row, width and height from the four parallel tables into windowOriginCol/windowOriginRow/frameWidth/ ; frameHeight, fetches its 9-byte frame tile set from windowFramePtrTable and falls into drawFrameBox. ; In: X = window number 0-26 ; Out: windowOriginRow/Col and frameWidth/frameHeight set (the latter two are then decremented by ; drawFrameBox, so they end up as the right/bottom extents); the frame is drawn; A/X/Y clobbered ; Called from: $1567, $156C, $269E and the jumps at $2695, $26BE, $28EE here, and from sixteen sites ; in ; each $6F00 overlay ; ---------------------------------------------------------------------- drawWindowFrame: lda windowLeftTable,x ; 2C7F left screen column of this window sta windowOriginCol ; 2C82 left column of the window the plotters will use lda windowTopTable,x ; 2C85 top screen row sta windowOriginRow ; 2C88 top row of that window lda windowWidthTable,x ; 2C8B width in cells sta frameWidth ; 2C8E width in cells, before drawFrameBox decrements it lda windowHeightTable,x ; 2C91 height in cells sta frameHeight ; 2C94 height in cells, before drawFrameBox decrements it txa ; 2C97 the pointer table has two bytes per window ... asl a ; 2C98 ... so double the index tax ; 2C99 index the pointer table lda windowFramePtrTable,x ; 2C9A low byte of this window's tile set inx ; 2C9D step to the high byte of the pointer ldy windowFramePtrTable,x ; 2C9E high byte - drawFrameBox expects A/Y ; ---------------------------------------------------------------------- ; drawFrameBox - Draws a rectangular frame from a 9-byte tile set at windowOriginRow/windowOriginCol, ; frameWidth by frameHeight cells. The tile set is indexed 0 top-left, 1 top edge, 2 top-right, 3 ; left edge, 4 centre (never read), 5 right edge, 6 bottom-left, 7 bottom edge, 8 bottom-right. The ; four corners are drawn first, then the bottom, top, left and right runs through ; drawFrameRow/drawFrameColumn, each of which stops before the corner it would overwrite. Width and ; height are decremented on entry so they become the right/bottom offsets, and are left that way for ; drawWindowInnerCorners. ; In: A/Y = lo/hi address of the 9-byte tile set; windowOriginRow, windowOriginCol, frameWidth, ; frameHeight; the current colour state (useDrawColourFlag / drawColourByte / fontColorTable) ; Out: the frame is drawn into the bitmap and screen RAM; frameWidth and frameHeight are each 1 ; smaller; ; scratch1A = right column, scratch1B = bottom row, blockPtr (zp_B8/B9) = the tile set ; Called from: drawWindowFrame by fall-through, and directly by overlay B ($724A) with its own tile ; set ; ---------------------------------------------------------------------- drawFrameBox: sta blockPtr ; 2CA1 keep the tile set address in zp_B8/B9 sty blockPtrHi ; 2CA3 zp_B8/B9 = the 9-byte tile set dec frameWidth ; 2CA5 width in cells becomes the offset of the rightmost column dec frameHeight ; 2CA8 height in cells becomes the offset of the bottom row lda windowOriginRow ; 2CAB window top row ... sta scratch18 ; 2CAE ... is the row of the top corners clc ; 2CB0 clear the carry for the height add adc frameHeight ; 2CB1 top row + height-1 ... sta scratch1B ; 2CB4 ... = the bottom row, kept in scratch1B as the column loops' stop value lda windowOriginCol ; 2CB6 window left column ... sta scratch19 ; 2CB9 ... is the column of the left corners clc ; 2CBB clear the carry for the width add adc frameWidth ; 2CBC left column + width-1 ... sta scratch1A ; 2CBF ... = the right column, kept in scratch1A as the row loops' stop value ; ---------------------------------------------------------------------- ; The four corners first, so the edge runs can stop one cell short of each. ; ---------------------------------------------------------------------- ldy #$00 ; 2CC1 tile 0 = top-left corner lda (blockPtr),y ; 2CC3 tile 0 of the set jsr drawCharAt ; 2CC5 draw it at (top, left) lda scratch1A ; 2CC8 move to the right column sta scratch19 ; 2CCA move to the right-hand column ldy #$02 ; 2CCC tile 2 = top-right corner lda (blockPtr),y ; 2CCE tile 2 of the set jsr drawCharAt ; 2CD0 draw it at (top, right) lda scratch1B ; 2CD3 move down to the bottom row sta scratch18 ; 2CD5 move down to the bottom row ldy #$08 ; 2CD7 tile 8 = bottom-right corner lda (blockPtr),y ; 2CD9 tile 8 of the set jsr drawCharAt ; 2CDB draw it at (bottom, right) lda windowOriginCol ; 2CDE back to the left column sta scratch19 ; 2CE1 back to the left-hand column ldy #$06 ; 2CE3 tile 6 = bottom-left corner lda (blockPtr),y ; 2CE5 tile 6 of the set jsr drawCharAt ; 2CE7 draw it at (bottom, left) ; ---------------------------------------------------------------------- ; Now the four edges. Row and column are still at the bottom-left corner. ; ---------------------------------------------------------------------- inc scratch19 ; 2CEA start one column right of the bottom-left corner ldy #$07 ; 2CEC tile 7 = bottom edge jsr drawFrameRow ; 2CEE run it along the bottom row up to (not including) the right corner ldy windowOriginRow ; 2CF1 back to the top row sty scratch18 ; 2CF4 row of the top edge ldy windowOriginCol ; 2CF6 and one column right of the left corner iny ; 2CF9 one column right of the top-left corner sty scratch19 ; 2CFA first column of the top edge run ldy #$01 ; 2CFC tile 1 = top edge jsr drawFrameRow ; 2CFE run it along the top row ldy windowOriginRow ; 2D01 one row below the top-left corner iny ; 2D04 one row below the top-left corner sty scratch18 ; 2D05 first row of the left edge run ldy windowOriginCol ; 2D07 in the window's left column sty scratch19 ; 2D0A the window's left column ldy #$03 ; 2D0C tile 3 = left edge jsr drawFrameColumn ; 2D0E run it down the left side up to (not including) the bottom corner ldy windowOriginRow ; 2D11 one row below the top-right corner iny ; 2D14 one row below the top-right corner sty scratch18 ; 2D15 first row of the right edge run lda scratch1A ; 2D17 in the right-hand column sta scratch19 ; 2D19 the window's right column ldy #$05 ; 2D1B tile 5 = right edge - and fall into drawFrameColumn to draw it ; ---------------------------------------------------------------------- ; drawFrameColumn - Repeats the tile at offset Y of the current tile set down column scratch19, ; starting at row scratch18 and stopping when the row reaches scratch1B (exclusive), so the corner ; already drawn there is not overwritten. Used for the left and right frame edges. ; In: Y = tile offset in the set, blockPtr = the tile set, scratch18 = first row, scratch19 = column, ; scratch1B = stop row ; Out: scratch18 = scratch1B, scratch1C = the tile's character code, the cells drawn ; Called from: drawFrameBox ($2D0E) and by fall-through from $2D1B ; ---------------------------------------------------------------------- drawFrameColumn: lda (blockPtr),y ; 2D1D fetch the edge tile's character code once sta scratch1C ; 2D1F keep it in zp_1C - drawCharAt clobbers A ; ---------------------------------------------------------------------- ; One cell per row until the bottom corner is reached. ; ---------------------------------------------------------------------- L_2D21: lda scratch1C ; 2D21 the edge tile jsr drawCharAt ; 2D23 draw it at the current row/column inc scratch18 ; 2D26 step one row down ldy scratch18 ; 2D28 the row we have just moved to cpy scratch1B ; 2D2A reached the row holding the corner? bne L_2D21 ; 2D2C not yet - keep going rts ; 2D2E the column is finished one cell above the corner ; ---------------------------------------------------------------------- ; drawFrameRow - Repeats the tile at offset Y of the current tile set along row scratch18, starting at ; column scratch19 and stopping when the column reaches scratch1A (exclusive). Used for the top and ; bottom frame edges. ; In: Y = tile offset in the set, blockPtr = the tile set, scratch18 = row, scratch19 = first column, ; scratch1A = stop column ; Out: scratch19 = scratch1A, scratch1C = the tile's character code, the cells drawn ; Called from: drawFrameBox ($2CEE bottom edge, $2CFE top edge) ; ---------------------------------------------------------------------- drawFrameRow: lda (blockPtr),y ; 2D2F fetch the edge tile's character code once sta scratch1C ; 2D31 keep it in zp_1C - drawCharAt clobbers A ; ---------------------------------------------------------------------- ; One cell per column until the corner column is reached. ; ---------------------------------------------------------------------- L_2D33: lda scratch1C ; 2D33 the edge tile jsr drawCharAt ; 2D35 draw it at the current row/column inc scratch19 ; 2D38 step one column right ldy scratch19 ; 2D3A the column we have just moved to cpy scratch1A ; 2D3C reached the column holding the corner? bne L_2D33 ; 2D3E not yet - keep going rts ; 2D40 the run is finished one cell before the corner ; ---------------------------------------------------------------------- ; initBitmapScreen - Builds the game's display from scratch: a full-screen text window, grey border ; and background, colour RAM filled with grey, all 1024 screen-RAM bytes set to $CC (grey foreground ; on grey background so an empty bitmap looks uniform), the whole 8000-byte bitmap $A000-$BF3F ; cleared, and then the three VIC control registers and their shadows set for a hires bitmap in VIC ; bank 2 - $D016 = $C8 (40 columns, hires), $D018 = $38 (video matrix $8C00, bitmap $A000) and $D011 = ; $3B (bitmap mode, 25 rows, Y-scroll 3). Finally initSpritePointers rewrites the eight sprite ; pointers the screen fill just overwrote. ; In: none ; Out: VIC_BORDER/BG0/CTRL1/CTRL2/MEM_PTR and the shadows $9000-$9004; colour RAM, screen RAM and ; bitmap ; cleared; screenBuiltFlag = 1, decimalValueHigh = 0, spriteEnableShadow = 0, sprite pointers ; $8FF8-$8FFF = $28-$2F; mapCellPtr (zp_48/49) clobbered ; Called from: mainStart ($0AAF), drawBattlefieldScreen ($2636), the screen switcher ($06E8) and the ; start-up block ($0318 by JMP) ; ---------------------------------------------------------------------- initBitmapScreen: jsr setFullScreenWindow ; 2D41 text window = the whole 40x25 screen lda #$01 ; 2D44 mark the screen as (re)built sta screenBuiltFlag ; 2D46 every screen builder sets this flag; nothing ever reads it lda #$0C ; 2D48 colour $0C = medium grey sta vicBorderShadow ; 2D4A shadow the IRQ writes back to $D020 sta VIC_BORDER ; 2D4D grey border sta vicBg0Shadow ; 2D50 shadow of $D021 sta VIC_BG0 ; 2D53 grey background ; ---------------------------------------------------------------------- ; Fill all four pages of colour RAM ($D800-$DBFF) with grey. ; ---------------------------------------------------------------------- ldy #$00 ; 2D56 256 offsets per page lda #$0C ; 2D58 grey again (the STAs below clobber A) L_2D5A: sta COLOR_RAM,y ; 2D5A $D800-$D8FF sta D_D900,y ; 2D5D $D900-$D9FF sta D_DA00,y ; 2D60 $DA00-$DAFF sta D_DB00,y ; 2D63 $DB00-$DBFF (only $D800-$DBE7 is real colour RAM) iny ; 2D66 next offset in the page bne L_2D5A ; 2D67 256 bytes per page ; ---------------------------------------------------------------------- ; Fill the video matrix $8C00-$8FFF with $CC: in bitmap mode the high nibble is the '1' colour and the ; low nibble the '0' colour, so grey on grey hides the empty bitmap. ; ---------------------------------------------------------------------- ldy #$00 ; 2D69 256 offsets per page lda #$CC ; 2D6B $CC = grey foreground, grey background L_2D6D: sta D_8C00,y ; 2D6D $8C00-$8CFF sta D_8D00,y ; 2D70 $8D00-$8DFF sta D_8E00,y ; 2D73 $8E00-$8EFF sta D_8F00,y ; 2D76 $8F00-$8FFF (this also wipes the sprite pointers at $8FF8, restored below) iny ; 2D79 next offset in the page bne L_2D6D ; 2D7A all four pages of the video matrix ; ---------------------------------------------------------------------- ; Clear the 8000-byte bitmap $A000-$BF3F (25 rows x 320 bytes). ; ---------------------------------------------------------------------- lda #$00 ; 2D7C point zp_48/49 at the start of the bitmap ... sta mapCellPtr ; 2D7E start of the bitmap ... lda #$A0 ; 2D80 ... sta mapCellPtrHi ; 2D82 ... $A000 lda #$00 ; 2D84 0 for the store below and for the bitmap fill sta decimalValueHigh ; 2D86 clear the third byte of printDecimal's 24-bit accumulator (zp_7E); the printer clears it again ldy #$FF ; 2D88 fill each page from offset $FF downwards lda #$00 ; 2D8A the bitmap is cleared to background pixels L_2D8C: sta (mapCellPtr),y ; 2D8C clear one bitmap byte dey ; 2D8E work down through the page cpy #$FF ; 2D8F wrapped past 0 - the page is done bne L_2D8C ; 2D91 not wrapped yet - keep clearing inc mapCellPtrHi ; 2D93 next page ldx mapCellPtrHi ; 2D95 check how far up the bitmap we have got cpx #$BF ; 2D97 $BF00 is the last, partial page bcc L_2D8C ; 2D99 still below it - clear a whole page bne L_2DA1 ; 2D9B past $BF00 - the bitmap is finished ldy #$3F ; 2D9D $BF00-$BF3F: the final 64 bytes of the 8000-byte bitmap bne L_2D8C ; 2D9F always taken ; ---------------------------------------------------------------------- ; Switch the VIC into the game's display mode. ; ---------------------------------------------------------------------- L_2DA1: lda #$C8 ; 2DA1 $D016 = $C8: hires (bit 4 clear), 40 columns (bit 3), X-scroll 0 sta vicCtrl2Shadow ; 2DA3 shadow the raster IRQ writes back to $D016 sta VIC_CTRL2 ; 2DA6 and the register itself lda #$38 ; 2DA9 $D018 = $38: video matrix at bank+$0C00 = $8C00, bitmap at bank+$2000 = $A000 sta vicMemPtrShadow ; 2DAB shadow of $D018 sta VIC_MEM_PTR ; 2DAE and the register itself lda #$3B ; 2DB1 $D011 = $3B: display on, bitmap mode, 25 rows, Y-scroll 3 sta vicCtrl1Shadow ; 2DB3 shadow of $D011 sta VIC_CTRL1 ; 2DB6 and the register itself - the bitmap screen is now live jsr initSpritePointers ; 2DB9 put the eight sprite pointers back after the screen fill rts ; 2DBC the display is ready ; ---------------------------------------------------------------------- ; setTerrainTilePtr - Points glyphPtr at the 8-byte terrain tile definition number A in the table at ; $9C6E and turns off the per-glyph colour override so the caller's drawColourByte is used. The ; caller at ; $27B2 forms A by subtracting $40 from the map byte, and $27C2 expands the 8 bytes into the ; 16x16-pixel ; cell of the magnified battlefield view. ; In: A = terrain tile index (map cell byte - $40) ; Out: glyphPtr/glyphPtrHi (zp_B2/B3) = $9C6E + A*8, useDrawColourFlag = 0, Y = 0 ; Called from: renderMapTile ($27B8) and the radar tile renderer of overlay B ($73A4) ; ---------------------------------------------------------------------- setTerrainTilePtr: ldy #$00 ; 2DBD Y = 0 is used as the zero source for the three stores below sty glyphPtr ; 2DBF clear the tile pointer before building it sty glyphPtrHi ; 2DC1 clear the high byte so the three ROLs can shift into it sty useDrawColourFlag ; 2DC3 0 = let the caller's drawColourByte colour the cell asl a ; 2DC6 index * 2 ... rol glyphPtrHi ; 2DC7 carry the overflow into the high byte asl a ; 2DC9 ... * 4 ... rol glyphPtrHi ; 2DCA carry again asl a ; 2DCC ... * 8: eight bytes per terrain tile rol glyphPtrHi ; 2DCD and again - glyphPtrHi holds bits 8-10 of index*8 clc ; 2DCF clear the carry for the base-address add adc #$6E ; 2DD0 add the low byte of the tile table base $9C6E sta glyphPtr ; 2DD2 low byte of $9C6E + index*8 lda glyphPtrHi ; 2DD4 bits 8-10 of index*8 adc #$9C ; 2DD6 and its high byte sta glyphPtrHi ; 2DD8 zp_B2/B3 now points at the 8-byte terrain tile rts ; 2DDA the caller can now read the eight tile bytes through (glyphPtr),y ; ---------------------------------------------------------------------- ; clearMessageLineAndScreen - Wipes the status/message line first, then falls into ; clearCurrentScreenWindows to clear the rest of whatever screen is showing. ; In: currentScreen ($90FB) ; Out: see clearCurrentScreenWindows ; Called from: the F1/F3/F5/F7 screen switcher at $070C ; ---------------------------------------------------------------------- clearMessageLineAndScreen: jsr clearMessageLine ; 2DDB blank the bottom message line ($C39F) ; ---------------------------------------------------------------------- ; clearCurrentScreenWindows - Re-clears the content areas of whichever screen is active, dispatching ; on currentScreen: 0 = battlefield (sprites off, text colour 0, the console info panel unless the ; game is paused, then the overview window and the viewport text area), 1 = the comcen radar screen ; (overlay B's ; $7A6B), 2 = the drone screen (only sprite 1 enabled, then overlay B's $821F), 3 = the menu screen. ; All ; four exits are tail calls. ; In: currentScreen ($90FB), pauseState (zp_BE) ; Out: spriteEnableShadow = 0 (or 2 on the drone screen), textColour = 0 on the battlefield; the ; per-screen clear routine is jumped to ; Called from: clearMessageLineAndScreen by fall-through, and from the text engine at $C75E (after the ; text window has been saved, e.g. when a pause ends) ; ---------------------------------------------------------------------- clearCurrentScreenWindows: lda #$00 ; 2DDE 0 is used both as the sprite mask and as the text colour below sta spriteEnableShadow ; 2DE0 hide every sprite while the screen is rebuilt ldy currentScreen ; 2DE3 which screen is showing? 0 battlefield, 1 comcen radar, 2 drone, 3 menu bne L_2DF7 ; 2DE6 not the battlefield - dispatch below sta textColour ; 2DE8 battlefield: draw the panels in colour 0 lda pauseState ; 2DEA pause / disconnect flags bne L_2DF1 ; 2DEC while paused the console keeps the pause text - do not wipe it jsr clearInfoPanel ; 2DEE clear the console info panel (rows 12-21, columns 25-38) L_2DF1: jsr clearOverviewWindow ; 2DF1 clear the 20x20 overview map window jmp clearViewportTextArea ; 2DF4 clear the text area of the battlefield viewport and return through it L_2DF7: dey ; 2DF7 screen 1? bne L_2DFD ; 2DF8 no jmp redrawRadarMapWindow ; 2DFA comcen radar screen - overlay B redraws the radar map window L_2DFD: dey ; 2DFD screen 2? bne L_2E0A ; 2DFE no - it must be screen 3, the menu lda #$02 ; 2E00 drone screen: only sprite 1 (the drone marker) stays visible sta spriteEnableShadow ; 2E02 %00000010 - sprite 1 only lda #$00 ; 2E05 argument 0 = clear the whole drone view window jmp fillDroneViewWindow ; 2E07 overlay B's drone window filler (variant A holds unrelated code at this address) (also mapGenerator6F00:runGameTypeMenu) L_2E0A: jmp clearMenuWindowPlain ; 2E0A menu screen: plain clear of the menu window ; Two work flags of magnifyWindow2x, set when the source rectangle has an odd width or height so that ; its outermost row/column is drawn at half size and the zoom stays centred. magnifyWidthOdd: .byte $00 ; 2E0D . magnifyWidthOdd: $FF when frameWidth is odd magnifyHeightOdd: .byte $00 ; 2E0E magnifyHeightOdd: $FF when frameHeight is odd ; ---------------------------------------------------------------------- ; magnifyWindow2x - Redraws the character rectangle at windowOriginRow/windowOriginCol, frameWidth by ; frameHeight cells, at twice the size in place and centred on itself: every source cell (its 8 bitmap ; bytes plus its screen-RAM colour byte) is re-plotted as a 2x2 cell block through the text engine's ; double-size glyph renderer plotGlyphDoubleSize ($C24B), at (windowOriginRow - ceil(h/2) + 2*row, ; windowOriginCol - ceil(w/2) + 2*col). The rectangle is walked as four quadrants, each from the ; outer edge inwards, so no source cell is destroyed before it has been read. An odd width or height ; makes the routine step half a cell beyond each edge and suppress the outer half of those cells ; through the quadrant mask magnifyEdgeMask (zp_BA), which keeps the zoom centred. It first waits for ; raster line $80 to reduce tearing. ; In: windowOriginRow, windowOriginCol, frameWidth ($9221), frameHeight ($9222) ; Out: the area redrawn twice the size; frameWidth/frameHeight decremented when even; scratch20 = ; ceil(w/2), scratch21 = ceil(h/2), textCursorCol/textCursorRow (zp_3F/zp_40) = destination ; origin, ; magnifySrcRow/Col (zp_30/31) = counters, textColour = the current cell's colour, ; magnifyEdgeMask ; left at 0, zp_B2-zp_B9 clobbered ; Called from: overlay B's zoom-in animation at $756B, four times over a 7-wide by 6-tall area at row ; 9, ; column 16 ; ---------------------------------------------------------------------- magnifyWindow2x: lda #$00 ; 2E0F clear both odd-size flags before measuring the rectangle sta magnifyWidthOdd ; 2E11 assume an even width ... sta magnifyHeightOdd ; 2E14 ... and an even height lda frameWidth ; 2E17 source rectangle width in cells lsr a ; 2E1A half of it, carry = the odd bit sta scratch20 ; 2E1B zp_20 = how many columns each half-pass covers bcc L_2E28 ; 2E1D even width - nothing special to do inc scratch20 ; 2E1F odd: round the half width up lda #$FF ; 2E21 $FF = odd sta magnifyWidthOdd ; 2E23 remember that the left/right edge cells are drawn half width bmi L_2E2B ; 2E26 always taken ($FF is negative) L_2E28: dec frameWidth ; 2E28 even width: turn the count into the index of the last column L_2E2B: lda windowOriginCol ; 2E2B source left column ... sec ; 2E2E prepare the subtraction sbc scratch20 ; 2E2F ... minus half the width = destination left column sta textCursorCol ; 2E31 zp_3F holds the destination origin column (the text cursor is free here) lda frameHeight ; 2E33 source rectangle height in cells lsr a ; 2E36 half of it sta scratch21 ; 2E37 zp_21 = how many rows each half-pass covers bcc L_2E44 ; 2E39 even height inc scratch21 ; 2E3B odd: round the half height up lda #$FF ; 2E3D $FF = odd sta magnifyHeightOdd ; 2E3F remember that the top/bottom edge cells are drawn half height bmi L_2E47 ; 2E42 always taken L_2E44: dec frameHeight ; 2E44 even height: turn the count into the index of the last row L_2E47: lda windowOriginRow ; 2E47 source top row ... sec ; 2E4A prepare the subtraction sbc scratch21 ; 2E4B ... minus half the height = destination top row sta textCursorRow ; 2E4D zp_40 holds the destination origin row ; ---------------------------------------------------------------------- ; Wait for the raster to reach line $80 so the redraw starts at a predictable place on screen. ; ---------------------------------------------------------------------- L_2E4F: lda VIC_RASTER ; 2E4F current raster line cmp #$80 ; 2E52 line 128, roughly the middle of the display bne L_2E4F ; 2E54 spin until it comes round ; ---------------------------------------------------------------------- ; Quadrant pass 1 and 2: the top half of the rectangle, row 0 downwards. For each row the left half ; is drawn left-to-right and then the right half right-to-left, so a cell is always read before the ; enlarged copy of its neighbour can cover it. ; ---------------------------------------------------------------------- lda #$00 ; 2E56 top-half row counter sta magnifySrcRow ; 2E58 start at the topmost source row lda magnifyHeightOdd ; 2E5A odd height? beq L_2E63 ; 2E5D no - no vertical clipping on this row lda #$01 ; 2E5F bit 0 = suppress the top cells of the 2x2 block ... sta magnifyEdgeMask ; 2E61 ... so the top row is drawn only half tall L_2E63: lda #$00 ; 2E63 left-half column counter sta magnifySrcCol ; 2E65 start at the leftmost source column lda magnifyWidthOdd ; 2E67 odd width? beq L_2E72 ; 2E6A no - no horizontal clipping on this column lda magnifyEdgeMask ; 2E6C add to the mask that is already there ora #$04 ; 2E6E bit 2 = suppress the left cells of the 2x2 block sta magnifyEdgeMask ; 2E70 so the leftmost column is drawn half wide L_2E72: jsr setMagnifyCellPointers ; 2E72 set up source and destination pointers for this cell ldy #$00 ; 2E75 the colour byte is the single byte at the source screen cell lda (blockPtr),y ; 2E77 the cell's colour byte, read from the source screen RAM copy in zp_B8/B9 sta textColour ; 2E79 plotGlyphDoubleSize colours all four cells with zp_3D jsr plotGlyphDoubleSize ; 2E7B expand the cell into a 2x2 block at the destination lda magnifyEdgeMask ; 2E7E read the mask back and #$03 ; 2E80 clear the left/right suppression bits, keep the top/bottom ones for the rest of the row sta magnifyEdgeMask ; 2E82 only the top/bottom bits survive into the next cell inc magnifySrcCol ; 2E84 next column to the right lda magnifySrcCol ; 2E86 the column just drawn cmp scratch20 ; 2E88 compare against the half width bcc L_2E72 ; 2E8A keep going until the middle of the row lda frameWidth ; 2E8C now the right half: start at the outermost column ... sta magnifySrcCol ; 2E8F ... which for an odd width is half a cell past the rectangle lda magnifyWidthOdd ; 2E91 odd width? beq L_2E9C ; 2E94 no lda magnifyEdgeMask ; 2E96 add to the mask that is already there ora #$08 ; 2E98 bit 3 = suppress the right cells, so this column is drawn half wide sta magnifyEdgeMask ; 2E9A so the rightmost column is drawn half wide L_2E9C: jsr setMagnifyCellPointers ; 2E9C set up the pointers for this cell ldy #$00 ; 2E9F the colour byte is the single byte at the source screen cell lda (blockPtr),y ; 2EA1 its colour byte sta textColour ; 2EA3 colour for the 2x2 block jsr plotGlyphDoubleSize ; 2EA5 expand it lda magnifyEdgeMask ; 2EA8 read the mask back and #$03 ; 2EAA drop the left/right suppression again sta magnifyEdgeMask ; 2EAC only the top/bottom bits survive into the next cell dec magnifySrcCol ; 2EAE next column to the left lda magnifySrcCol ; 2EB0 the column just drawn cmp scratch20 ; 2EB2 compare against the half width bcs L_2E9C ; 2EB4 keep going while we are still right of the middle lda magnifyEdgeMask ; 2EB6 read the mask back and #$0C ; 2EB8 row finished: drop the top/bottom suppression, keep nothing else sta magnifyEdgeMask ; 2EBA only the left/right bits survive into the next row inc magnifySrcRow ; 2EBC next row down lda magnifySrcRow ; 2EBE the row just finished cmp scratch21 ; 2EC0 compare against the half height bcc L_2E63 ; 2EC2 keep going until the middle row ; ---------------------------------------------------------------------- ; Quadrant pass 3 and 4: the bottom half, walked upwards from the outermost row. ; ---------------------------------------------------------------------- lda frameHeight ; 2EC4 start at the outermost row ... sta magnifySrcRow ; 2EC7 ... which for an odd height is half a cell below the rectangle lda magnifyHeightOdd ; 2EC9 odd height? beq L_2ED2 ; 2ECC no lda #$02 ; 2ECE bit 1 = suppress the bottom cells ... sta magnifyEdgeMask ; 2ED0 ... so this row is drawn only half tall L_2ED2: lda #$00 ; 2ED2 left-half column counter sta magnifySrcCol ; 2ED4 start at the leftmost source column again lda magnifyWidthOdd ; 2ED6 odd width? beq L_2EE1 ; 2ED9 no lda magnifyEdgeMask ; 2EDB add to the mask that is already there ora #$04 ; 2EDD bit 2 = suppress the left cells sta magnifyEdgeMask ; 2EDF so the leftmost column is drawn half wide L_2EE1: jsr setMagnifyCellPointers ; 2EE1 set up the pointers for this cell ldy #$00 ; 2EE4 read the source cell's colour byte lda (blockPtr),y ; 2EE6 its colour byte sta textColour ; 2EE8 colour for the 2x2 block jsr plotGlyphDoubleSize ; 2EEA expand it lda magnifyEdgeMask ; 2EED read the mask back and #$03 ; 2EEF drop the left/right suppression sta magnifyEdgeMask ; 2EF1 only the top/bottom bits survive into the next cell inc magnifySrcCol ; 2EF3 next column to the right lda magnifySrcCol ; 2EF5 the column just drawn cmp scratch20 ; 2EF7 compare against the half width bcc L_2EE1 ; 2EF9 until the middle of the row lda frameWidth ; 2EFB right half of this row: outermost column ... sta magnifySrcCol ; 2EFE start the right half at the outermost column lda magnifyWidthOdd ; 2F00 odd width? beq L_2F0B ; 2F03 no lda magnifyEdgeMask ; 2F05 add to the mask that is already there ora #$08 ; 2F07 bit 3 = suppress the right cells sta magnifyEdgeMask ; 2F09 so the rightmost column is drawn half wide L_2F0B: jsr setMagnifyCellPointers ; 2F0B set up the pointers for this cell ldy #$00 ; 2F0E read the source cell's colour byte lda (blockPtr),y ; 2F10 its colour byte sta textColour ; 2F12 colour for the 2x2 block jsr plotGlyphDoubleSize ; 2F14 expand it lda magnifyEdgeMask ; 2F17 read the mask back and #$03 ; 2F19 drop the left/right suppression sta magnifyEdgeMask ; 2F1B only the top/bottom bits survive into the next cell dec magnifySrcCol ; 2F1D next column to the left lda magnifySrcCol ; 2F1F the column just drawn cmp scratch20 ; 2F21 compare against the half width bcs L_2F0B ; 2F23 while still right of the middle lda magnifyEdgeMask ; 2F25 read the mask back and #$0C ; 2F27 row finished - this also leaves magnifyEdgeMask at 0 on the final pass sta magnifyEdgeMask ; 2F29 only the left/right bits survive into the next row dec magnifySrcRow ; 2F2B next row up lda magnifySrcRow ; 2F2D the row just finished cmp scratch21 ; 2F2F compare against the half height bcs L_2ED2 ; 2F31 keep going while still below the middle row rts ; 2F33 the whole rectangle has been redrawn at double size ; ---------------------------------------------------------------------- ; setMagnifyCellPointers - Helper of magnifyWindow2x. Computes the pointers of the source cell ; (windowOriginRow + magnifySrcRow, windowOriginCol + magnifySrcCol), copies them into glyphPtr (so ; the bitmap cell is used as the 'glyph' to enlarge) and blockPtr (so its screen-RAM colour byte can ; be read), then computes the destination cell (zp_40 + 2*row, zp_3F + 2*column) into ; bitmapPtr/screenPtr. ; In: magnifySrcRow/magnifySrcCol (zp_30/31), windowOriginRow/Col, textCursorRow/Col (zp_40/zp_3F) ; Out: glyphPtr (zp_B2/B3) = source bitmap cell, blockPtr (zp_B8/B9) = source screen cell, ; bitmapPtr/screenPtr (zp_B4-B7) = destination cell, scratch18/19 = destination row/column ; Called from: magnifyWindow2x ($2E72, $2E9C, $2EE1, $2F0B) ; ---------------------------------------------------------------------- setMagnifyCellPointers: clc ; 2F34 clear the carry for the row add lda magnifySrcRow ; 2F35 row offset within the rectangle ... adc windowOriginRow ; 2F37 ... plus the rectangle's top row sta scratch18 ; 2F3A absolute source row clc ; 2F3C clear the carry for the column add lda magnifySrcCol ; 2F3D column offset within the rectangle ... adc windowOriginCol ; 2F3F ... plus the rectangle's left column sta scratch19 ; 2F42 absolute source column jsr calcCellPointers ; 2F44 zp_B4-zp_B7 now point at the source cell ; ---------------------------------------------------------------------- ; Copy both source pointers aside before they are recomputed for the destination. LDA zp,Y does not ; exist on the 6502, so these are absolute accesses into page zero. ; ---------------------------------------------------------------------- ldy #$01 ; 2F47 two bytes each, high byte first L_2F49: lda a:bitmapPtr,y ; 2F49 bitmapPtr -> ... sta a:glyphPtr,y ; 2F4C ... glyphPtr: plotGlyphDoubleSize reads the source cell as if it were a font glyph lda a:screenPtr,y ; 2F4F screenPtr -> ... sta a:blockPtr,y ; 2F52 ... blockPtr: the caller reads the source cell's colour byte through it dey ; 2F55 high byte then low byte bpl L_2F49 ; 2F56 two bytes of each pointer copied ; ---------------------------------------------------------------------- ; Now the destination cell: the zoom doubles every offset. ; ---------------------------------------------------------------------- lda magnifySrcRow ; 2F58 row offset within the rectangle asl a ; 2F5A row offset * 2 clc ; 2F5B clear the carry for the origin add adc textCursorRow ; 2F5C plus the destination top row sta scratch18 ; 2F5E destination row lda magnifySrcCol ; 2F60 column offset within the rectangle asl a ; 2F62 column offset * 2 (no CLC needed - the offset is always below $80, so ASL clears the carry) adc textCursorCol ; 2F63 plus the destination left column sta scratch19 ; 2F65 destination column jmp calcCellPointers ; 2F67 leave zp_B4-zp_B7 pointing at the destination cell and return through it ; ---------------------------------------------------------------------- ; initSpritePointers - Rewrites the eight sprite pointers in the last bytes of the video matrix, ; $8FF8-$8FFF, with $28..$2F. In VIC bank 2 a pointer value n means shape data at $8000 + n*64, so ; sprite 0 takes its shape from $8A00 and sprite 7 from $8BC0 - the 512-byte block clearAllSpriteData ; wipes. Also clears the sprite enable shadow. Needed after initBitmapScreen fills $8C00-$8FFF. ; In: none ; Out: spriteEnableShadow = 0, $8FF8-$8FFF = $28-$2F; mapCellPtr (zp_48/49) = $8FF8; A/X/Y clobbered ; Called from: initBitmapScreen ($2DB9) only ; ---------------------------------------------------------------------- initSpritePointers: lda #$00 ; 2F6A no sprite is on screen while the display is rebuilt sta spriteEnableShadow ; 2F6C no sprites visible yet lda #$F8 ; 2F6F low byte of $8FF8 ... sta mapCellPtr ; 2F71 ... lda #$8F ; 2F73 ... high byte sta mapCellPtrHi ; 2F75 zp_48/49 = $8FF8, the first sprite pointer ldy #$07 ; 2F77 eight pointers, counted down ldx #$2F ; 2F79 $2F = sprite 7's shape at $8000 + $2F*64 = $8BC0 L_2F7B: txa ; 2F7B the pointer value for this sprite sta (mapCellPtr),y ; 2F7C $8FF8+Y = the pointer of sprite Y dex ; 2F7E next shape block down dey ; 2F7F next sprite down bpl L_2F7B ; 2F80 until sprite 0 gets $28 = $8A00 adc #$01 ; 2F82 leftover: A becomes $29/$2A and is never used by the only caller rts ; 2F84 the eight sprite pointers are back in place ; The two bit-mask tables. spriteBitMasks is the set mask for bit n and is used both for sprite ; enable bits ($3065) and as the keyboard column mask ($0E16); invertedBitMasks is its complement, ; used to clear a sprite bit ($337D) and as the keyboard row mask ($0E08). spriteBitMasks: .byte $01,$02,$04,$08,$10,$20,$40,$80; 2F85 ..... @. set masks $01,$02,$04 ... $80 for bit / sprite / keyboard column n invertedBitMasks: .byte $FE ; 2F8D clear masks $FE,$FD,$FB ... $7F - the same table complemented (was D_2F8D) .byte $FD,$FB,$F7,$EF,$DF,$BF,$7F; 2F8E ....... ; Sprite shape addresses split into two tables so that $8A00 + n*64 can be formed with a single 16-bit ; add (see clearSpriteShape). spriteOffsetLoTable: .byte $00,$40,$80,$C0,$00,$40,$80,$C0; 2F95 .@...@.. low byte of n*64 for sprites 0-7 ; spriteOffsetHiTable: byteTable, 8 bytes. High byte of n*64 for sprite 0-7 (0,0,0,0,1,1,1,1) spriteOffsetHiTable: .byte $00,$00,$00,$00,$01,$01,$01,$01; 2F9D ........ high byte of n*64: 0 for sprites 0-3, 1 for sprites 4-7 ; ---------------------------------------------------------------------- ; clearAllSpriteData - Zeroes $8A00-$8BFF, the 64-byte shape blocks of all eight sprites, in one ; 256-pass loop that writes two pages at a time. ; In: none ; Out: $8A00-$8BFF = 0; A = 0, X = 0 ; Called from: the start-up block at $0386 (by JMP, but the routine ends in RTS) ; ---------------------------------------------------------------------- clearAllSpriteData: ldx #$00 ; 2FA5 one pass over 256 offsets txa ; 2FA7 the fill value is 0 L_2FA8: sta sprite0Shape,x ; 2FA8 sprites 0-3 ($8A00-$8AFF) sta sprite4Shape,x ; 2FAB sprites 4-7 ($8B00-$8BFF) inx ; 2FAE next offset bne L_2FA8 ; 2FAF 256 offsets, both pages at once rts ; 2FB1 all eight sprite shapes are blank ; ---------------------------------------------------------------------- ; clearSpriteShape - Zeroes the 64-byte shape of sprite A at $8A00 + A*64. The address is built from ; the two half tables and patched into the operand of the STA abs,Y at $2FC8, which in the raw image ; still holds its unpatched $FFFF operand (hence the 'sta irqVectorHi,y' the disassembler shows). ; In: A = sprite number 0-7 ; Out: that sprite's 64 shape bytes are 0; the operand at $2FC9/$2FCA is patched; A = 0, Y = $FF ; Called from: $2245, $224A, setCursorShapeSquare ($2FDB), setCursorShapeBlock ($2FFD), $3385, $346A, ; $3CFF and the jump at $3D53, plus the overlay screens ($7184, $76A1, $7781, $8241) ; ---------------------------------------------------------------------- clearSpriteShape: tay ; 2FB2 index the two half tables with the sprite number clc ; 2FB3 clear the carry for the 16-bit add lda #$00 ; 2FB4 the low half of the base address $8A00 is zero adc spriteOffsetLoTable,y ; 2FB6 low byte of sprite*64 sta L_2FC8+1 ; 2FB9 patch the low operand byte of the STA at $2FC8 lda #$8A ; 2FBC high byte of the shape area base $8A00 ... adc spriteOffsetHiTable,y ; 2FBE ... plus the high byte of sprite*64 sta L_2FC8+2 ; 2FC1 patch the high operand byte ldy #$3F ; 2FC4 64 bytes per sprite shape lda #$00 ; 2FC6 the fill value L_2FC8: sta irqVectorHi,y ; 2FC8 operand patched at $2FB9/$2FC1 - really STA $8A00+sprite*64,Y dey ; 2FCB work down through the 64 bytes bpl L_2FC8 ; 2FCC all 64 bytes cleared rts ; 2FCE the sprite is blank and ready to be filled ; ---------------------------------------------------------------------- ; setCursorShapeSquare - Gives sprite 0, the map cursor, the 18-byte (6 rows of 24 pixels) hollow ; square at $9E6E, sets its colour shadow to 0 and enables it. The sprite is Y-expanded elsewhere so ; the six rows cover a whole 12-pixel map cell. ; In: none ; Out: sprite 0's shape at $8A00, spriteColourShadow ($9008) = 0, sprite 0 enabled ; Called from: $2015, $4C53, $5E40, $5E87, $61F5, $6393 - whenever a unit is picked up or ordered ; ---------------------------------------------------------------------- setCursorShapeSquare: lda #$6E ; 2FCF low byte of the hollow-square shape at $9E6E ldy #$9E ; 2FD1 high byte ; ---------------------------------------------------------------------- ; installCursorShape - Shared tail of setCursorShapeSquare and setCursorShapeCross: copies the 18-byte ; shape whose address is in A/Y into sprite 0, after blanking the rest of the sprite, and enables it ; in black. The source address is planted in the operand of the LDA abs,Y at $2FE0. ; In: A/Y = lo/hi address of an 18-byte (6 row) sprite shape ; Out: sprite 0's shape rewritten, spriteColourShadow = 0, spriteEnableShadow bit 0 set; the operand ; at ; $2FE1/$2FE2 patched ; Called from: setCursorShapeSquare by fall-through and setCursorShapeCross ($2FF7 by JMP) ; ---------------------------------------------------------------------- installCursorShape: sta L_2FE0+1 ; 2FD3 patch the low operand byte of the LDA at $2FE0 sty L_2FE0+2 ; 2FD6 and its high operand byte lda #$00 ; 2FD9 sprite 0 = the map cursor jsr clearSpriteShape ; 2FDB blank all 64 bytes so the previous shape cannot show through ldy #$11 ; 2FDE 18 bytes = 6 rows of 3 bytes L_2FE0: lda irqVectorHi,y ; 2FE0 operand patched at $2FD3/$2FD6 - really LDA $9E6E,Y or $9E80,Y sta sprite0Shape,y ; 2FE3 sprite 0's shape data at $8A00 dey ; 2FE6 work down through the 18 bytes bpl L_2FE0 ; 2FE7 18 bytes copied lda #$00 ; 2FE9 colour 0 = black; shadow values below $10 are steady, $10 and above flash between two nibbles sta spriteColourShadow ; 2FEB sprite 0's colour shadow, applied to $D027 by the raster IRQ lda #$01 ; 2FEE sprite 0's enable bit jmp enableSprites ; 2FF0 OR it into the enable shadow and return through it ; ---------------------------------------------------------------------- ; setCursorShapeCross - The same as setCursorShapeSquare but installs the thick plus/cross shape at ; $9E80 instead. Used for the ranged attack cursor, when cursorTargetMode (zp_BB) is positive. ; In: none ; Out: sprite 0 = the cross shape, colour 0, enabled ; Called from: the cursor selector at $2195 ; ---------------------------------------------------------------------- setCursorShapeCross: lda #$80 ; 2FF3 low byte of the cross shape at $9E80 ldy #$9E ; 2FF5 high byte jmp installCursorShape ; 2FF7 share the copy/enable tail ; ---------------------------------------------------------------------- ; setCursorShapeBlock - Loads the small 4x4 solid block at $9E92 into the sprite named by ; activeSpriteIndex ($91CC) and sets that sprite's colour shadow to $0C (grey) - except for sprite 0, ; which keeps whatever colour the cursor code gave it. Note the shape address is formed with six ; ASLs, so it only reaches sprites 0-3; both callers pass 0 or 1. ; In: activeSpriteIndex ($91CC) = sprite number (0 or 1 in practice) ; Out: that sprite's 64-byte shape holds the block; spriteColourShadow[n] = $0C for n other than 0 ; Called from: the cursor selector at $21A8 (plain battlefield cursor) and $2550 (sprite 1 when no ; group ; box is up) ; ---------------------------------------------------------------------- setCursorShapeBlock: lda activeSpriteIndex ; 2FFA which sprite is being dressed jsr clearSpriteShape ; 2FFD blank its 64 shape bytes first lda activeSpriteIndex ; 3000 the sprite number again asl a ; 3003 sprite number * 2 ... asl a ; 3004 ... * 4 ... asl a ; 3005 ... * 8 ... asl a ; 3006 ... * 16 ... asl a ; 3007 ... * 32 ... asl a ; 3008 ... * 64 = its offset inside the shape block (wraps above sprite 3) tax ; 3009 X = the byte offset of this sprite's shape inside $8A00 ldy #$00 ; 300A start at the first byte of the shape L_300C: lda titleScreenRow16Col18,y ; 300C the 18-byte solid block shape at $9E92 (also graphicsData9300:cursorBlockShape) sta sprite0Shape,x ; 300F into $8A00 + sprite*64 inx ; 3012 next destination byte iny ; 3013 next source byte cpy #$12 ; 3014 18 bytes = 6 rows of 3 bcc L_300C ; 3016 18 bytes = 6 rows of 3 lda #$0C ; 3018 colour $0C = medium grey ldx activeSpriteIndex ; 301A which sprite was dressed? beq L_3022 ; 301D sprite 0 keeps the cursor colour set by installCursorShape sta spriteColourShadow,x ; 301F any other sprite is drawn grey L_3022: rts ; 3022 done - the cursor sprite is dressed ; ---------------------------------------------------------------------- ; mapCellToSpriteXY - Converts a battlefield cell (X = column 0-39, Y = row 0-39) into the sprite ; coordinates that put a marker on that cell of the overview (mini) map in the left panel. While a ; group formation box is being dragged (groupBoxActive $920A bit 7 set) the cell is first shifted by ; the box's top-left offsets, so the formation ghost sprite is anchored at the corner of the box ; instead of at the cursor cell, and the result is nudged one pixel right and down to line up with the ; 4x4 pixel blocks the overview map is drawn with. ; In: X = map column, Y = map row, groupBoxActive $920A, groupBoxLeftOffset $922F, groupBoxTopOffset ; $9231 ; Out: X = sprite X coordinate, Y = sprite Y coordinate; A clobbered ; Called from: positionMarkerSprite ($2585), positionCursorSprite ($3055), the unit-follow code ; ($483F) ; ---------------------------------------------------------------------- mapCellToSpriteXY: lda groupBoxActive ; 3023 $920A = $FF while a group formation box is being dragged around the map bpl mapCellToSpriteXYRaw ; 3026 branch if no group box is active: convert the cell as it stands txa ; 3028 group box: start from the requested map column clc ; 3029 clear carry for the offset addition adc groupBoxLeftOffset ; 302A $922F = column offset of the box's left edge from the cursor cell (may be negative) tax ; 302D X = column of the top-left cell of the box tya ; 302E same for the row clc ; 302F clear carry for the row offset addition adc groupBoxTopOffset ; 3030 $9231 = row offset of the box's top edge from the cursor cell tay ; 3033 Y = row of the top-left cell of the box jsr mapCellToSpriteXYRaw ; 3034 convert that corner cell to overview-map sprite coordinates inx ; 3037 +1 pixel in X: the 4x4 cell blocks are drawn one pixel into their cell iny ; 3038 +1 pixel in Y, likewise rts ; 3039 return with the shifted sprite coordinates ; ---------------------------------------------------------------------- ; mapCellToSpriteXYRaw - The plain cell-to-sprite conversion for the overview map: spriteX = column*4 ; + $1F, spriteY = row*4 + $39. The whole 40x40 battlefield is drawn in the left panel at 4x4 pixels ; per cell starting at screen pixel (7,7); $1F and $39 are that origin plus the VIC sprite coordinate ; offsets ($18 horizontally, $32 vertically). ; In: X = map column 0-39, Y = map row 0-39 ; Out: X = sprite X coordinate, Y = sprite Y coordinate; A clobbered ; Called from: mapCellToSpriteXY ($3026/$3034), showExplosionMarker ($33E5), the shot-line setup ; ($3A70, $3A7B) ; ---------------------------------------------------------------------- mapCellToSpriteXYRaw: txa ; 303A map column 0-39 asl a ; 303B column*2 asl a ; 303C column*4 - the overview map uses 4 pixels per cell clc ; 303D clear carry before adding the map origin adc #$1F ; 303E sprite X of overview column 0 = 24 (sprite origin) + 7 (left edge of the map) tax ; 3040 return the sprite X in X tya ; 3041 map row 0-39 asl a ; 3042 row*2 asl a ; 3043 row*4 clc ; 3044 clear carry before adding the map origin adc #$39 ; 3045 sprite Y of overview row 0 = 50 (sprite origin) + 7 (top edge of the map) tay ; 3047 return the sprite Y in Y rts ; 3048 done ; ---------------------------------------------------------------------- ; positionCursorSprite - Puts the sprite named by activeSpriteIndex ($91CC) over the cursor cell of ; the overview map. The cursor cell is always the middle of the 7x5 tactical view, i.e. (viewOriginCol ; + 3, viewOriginRow + 2). Nothing happens when activeSpriteIndex is negative (no cursor sprite). ; In: viewOriginCol zp_A5, viewOriginRow zp_A6, activeSpriteIndex $91CC, group-box state ; Out: spriteXShadow/spriteYShadow of that sprite, spriteEnableShadow bit set; A/X/Y clobbered ; Called from: the viewport scroller ($1D46), battlefield entry ($2012), buildGroupFormationSprite ; ($31A0, by JMP) and the map generator overlay ($83F2) ; ---------------------------------------------------------------------- positionCursorSprite: lda viewOriginCol ; 3049 map column of the left edge of the 7x5 tactical view clc ; 304B clear carry before stepping to the cursor column adc #$03 ; 304C +3 = the middle column of the seven, where the cursor sits tax ; 304E X = cursor column lda viewOriginRow ; 304F map row of the top edge of the view clc ; 3051 clear carry before stepping to the cursor row adc #$02 ; 3052 +2 = the middle row of the five tay ; 3054 Y = cursor row jsr mapCellToSpriteXY ; 3055 cell -> overview sprite coordinates (shifted when a group box is active) txa ; 3058 keep the sprite X byte ldx activeSpriteIndex ; 3059 $91CC = which sprite is currently acting as the cursor bmi L_306B ; 305C negative = no cursor sprite wanted, leave everything alone sta spriteXShadow,x ; 305E $9010+n: shadow of the VIC sprite X register, copied out by the raster IRQ tya ; 3061 sprite Y sta spriteYShadow,x ; 3062 $9018+n: shadow of the VIC sprite Y register lda spriteBitMasks,x ; 3065 $2F85[n] = 1 << n, the enable bit of this sprite jsr enableSprites ; 3068 turn the sprite on (unless sprites are suppressed) L_306B: rts ; 306B done ; ---------------------------------------------------------------------- ; drawLineInPathSprite - Draws a straight line into the shape of sprite 3 ($8AC0), which is the game's ; path/shot-line sprite. It uses the resident high-memory Bresenham stepper ($FBB8 init, $FBFA step), ; but here its zp_92-zp_95 coordinates are pixel positions inside the 24x21 sprite, not map cells. A ; negative argument makes the walker skip two pixels after every plotted one, giving a dotted line ; (used for a BOOMER's arcing shot). ; In: A = 0 solid / $80 dotted; lineCurRow/Col zp_92/zp_93 = start pixel, lineEndRow/Col zp_94/zp_95 ; = end pixel ; Out: pixels set in $8AC0-$8AFF; the line-walker state is consumed, zp_92/zp_93 left at the end point ; Called from: the shot-line builder ($3B11) and the drone heading indicator in overlay B ($8266) ; ---------------------------------------------------------------------- drawLineInPathSprite: sta L_307C+1 ; 306C patch the LDA # at $307C with the dot flag: 0 = solid, $80 = dotted jsr initLineStepper ; 306F $FBB8: set up the Bresenham walk from (zp_93,zp_92) to (zp_95,zp_94) ; ---------------------------------------------------------------------- ; Walk the line one pixel at a time, plotting into sprite 3. ; ---------------------------------------------------------------------- L_3072: jsr plotPathSpritePixel ; 3072 set the pixel the walker is standing on lda lineStepsLeft ; 3075 steps still to be taken beq L_3089 ; 3077 branch when the end point has been reached jsr stepLine ; 3079 $FBFA: advance the walker one pixel L_307C: lda #$FF ; 307C dot flag patched at $306C bpl L_3072 ; 307E solid line: plot every pixel jsr stepLine ; 3080 dotted line: skip this pixel... jsr stepLine ; 3083 ...and the next one, so only every third pixel is drawn jmp L_3072 ; 3086 plot the pixel we landed on L_3089: rts ; 3089 line finished ; ---------------------------------------------------------------------- ; plotPathCross - Marks the end of a path/shot line by plotting the four orthogonal neighbours of the ; walker's current pixel into sprite 3: left, up, down and (by falling through into ; plotPathSpritePixel) right. Together with the line's own last pixel this makes a small cross over ; the target. ; In: lineCurRow zp_92, lineCurCol zp_93 = the pixel to mark ; Out: 4 more pixels set in $8AC0; zp_93 left one greater than on entry ; Called from: the shot-line builder ($3B14), immediately after drawLineInPathSprite ; ---------------------------------------------------------------------- plotPathCross: dec lineCurCol ; 308A one pixel to the left of the end point jsr plotPathSpritePixel ; 308C plot it inc lineCurCol ; 308F back to the end column dec lineCurRow ; 3091 one pixel above the end point jsr plotPathSpritePixel ; 3093 plot it inc lineCurRow ; 3096 back to the end row inc lineCurRow ; 3098 one pixel below the end point jsr plotPathSpritePixel ; 309A plot it dec lineCurRow ; 309D back to the end row inc lineCurCol ; 309F one pixel to the right - fall through and plot that one too ; ---------------------------------------------------------------------- ; plotPathSpritePixel - Sets one pixel in the shape of sprite 3 at $8AC0. A sprite row is 3 bytes, so ; the byte index is row*3 + column/8 and the bit is taken from the $80>>n mask table at $C008. The ; column/8 part is patched into the ADC # at $30B3 rather than added with a register. ; In: lineCurRow zp_92 = pixel row 0-20, lineCurCol zp_93 = pixel column 0-23 ; Out: one bit set in $8AC0-$8AFF; A/X/Y clobbered, the operand at $30B4 patched ; Called from: drawLineInPathSprite ($3072), plotPathCross ($308C/$3093/$309A and by fall-through), ; and directly by overlay B ($7852, $785E) ; ---------------------------------------------------------------------- plotPathSpritePixel: lda lineCurCol ; 30A1 pixel column 0-23 inside the sprite lsr a ; 30A3 column/2 lsr a ; 30A4 column/4 lsr a ; 30A5 column/8 = which of the three bytes of a sprite row holds the pixel sta L_30B3+1 ; 30A6 patch the ADC # at $30B3 with that byte offset lda lineCurCol ; 30A9 the column again and #$07 ; 30AB bit number 0-7 inside the byte tax ; 30AD index into the bit mask table lda lineCurRow ; 30AE pixel row 0-20 asl a ; 30B0 row*2 adc lineCurRow ; 30B1 row*3 = offset of the start of that sprite row (3 bytes per row) L_30B3: adc #$FF ; 30B3 + byte within the row, patched at $30A6 tay ; 30B5 Y = byte offset inside the 63-byte shape lda sprite3Shape,y ; 30B6 $8AC0 = shape of sprite 3, the path/shot line sprite ora bootReadSectorLoop,x ; 30B9 $C008[bit] = $80 >> bit (also textEngineC000:bitMaskTable_C008) sta sprite3Shape,y ; 30BC set the pixel rts ; 30BF done ; ---------------------------------------------------------------------- ; buildGroupFormationSprite - Builds the 'formation ghost' shown while a whole group is being ordered ; to a new place: every living member of the selected group is drawn as one 2x2-pixel dot into sprite ; 0, laid out exactly as the group's ordered destinations are laid out on the map. Because sprite 0 is ; X- and Y-expanded, one dot covers the 4x4 pixels the overview map gives a cell. The bounding box of ; the destinations is turned into offsets relative to the dragged unit, so the ghost follows the ; cursor, groupBoxActive is set, the view is scrolled until the box fits on the map and the ghost is ; positioned. ; In: selectedGroupKey $9206, selectedUnit $90F8, unit arrays $F76C/$FB54/$F7D0/$F834 ; Out: sprite 0 shape, spriteBuildBuffer $9025, groupBox offsets $922E-$9231, groupBoxActive $920A = ; $FF, ; spritePlotMode $91F3 = $40, X/Y expand shadows bits 0-1 set, view origin possibly moved ; Called from: runTargetCursorLoop ($21A2), when the group being ordered has 2 or more members ; ---------------------------------------------------------------------- buildGroupFormationSprite: lda selectedGroupKey ; 30C0 $9206 = group id bits of the group being ordered ldy #$80 ; 30C3 bit 7 = measure the members' ordered destinations, not their current cells jsr computeGroupBounds ; 30C5 $4B5E -> zp_18/zp_19 = min column/row, zp_1B/zp_1C = max column/row ldy #$3F ; 30C8 clear the 64-byte shape build buffer lda #$00 ; 30CA with zeroes L_30CC: sta spriteBuildBuffer,y ; 30CC $9025 = scratch sprite shape used while the ghost is assembled dey ; 30CF next byte down bpl L_30CC ; 30D0 loop over all 64 bytes lda #$40 ; 30D2 bit 6 = 'set the pixel' sta spritePlotMode ; 30D4 $91F3 tells plotSpritePixel which operation to use lda scratch1B ; 30D7 max column of the group's destinations sta groupBoxRightOffset ; 30D9 $922E = right edge of the group box lda scratch18 ; 30DC min column sta groupBoxLeftOffset ; 30DE $922F = left edge of the group box lda scratch1C ; 30E1 max row sta groupBoxBottomOffset ; 30E3 $9230 = bottom edge of the group box lda scratch19 ; 30E6 min row sta groupBoxTopOffset ; 30E8 $9231 = top edge of the group box lda selectedGroupKey ; 30EB group id bits again ora #$40 ; 30EE bit 6 = 'this unit is in a group' sta scratch1B ; 30F0 the value unitDisplayFlags bits 2-6 must have for a member of this group ldx #$63 ; 30F2 start at unit 99, the last unit record stx scratch1A ; 30F4 keep the loop counter in memory (X is needed for the pixel plotter) ; ---------------------------------------------------------------------- ; Walk units 99..0 and plot a 2x2 dot for every living member of the group at its ordered destination, ; relative to the top-left corner of the group box. ; ---------------------------------------------------------------------- L_30F6: lda unitTypeTable,x ; 30F6 $F76C[unit] = type byte, bit 7 set = destroyed bmi L_3142 ; 30F9 skip dead units lda unitDisplayFlagsTable,x ; 30FB $FB54[unit] = local flags copy and #$7C ; 30FE keep the group id (bits 2-4) and the in-a-group bit (6) cmp scratch1B ; 3100 does it belong to the group being ordered? bne L_3142 ; 3102 no - try the next unit lda unitDestRowTable,x ; 3104 $F834[unit] = ordered destination row, bit 7 = already standing there and #$7F ; 3107 drop that flag, keep the row sec ; 3109 set carry for the subtraction sbc groupBoxTopOffset ; 310A row inside the group box asl a ; 310D x2: two sprite pixels per map cell (the sprite is Y-expanded) tay ; 310E Y = pixel row of this member's dot sty scratch19 ; 310F stash it, plotSpritePixel needs Y for each of the four pixels lda unitDestColTable,x ; 3111 $F7D0[unit] = ordered destination column and #$7F ; 3114 drop the 'already there' flag sec ; 3116 set carry for the subtraction sbc groupBoxLeftOffset ; 3117 column inside the group box asl a ; 311A x2: two sprite pixels per map cell (the sprite is X-expanded) tax ; 311B X = pixel column of this member's dot stx scratch18 ; 311C stash it too lda #$FF ; 311E bit 7 set = plot into spriteBuildBuffer $9025 instead of a real sprite jsr setSpritePixel ; 3120 top-left pixel of the 2x2 dot ldx scratch18 ; 3123 pixel column again inx ; 3125 one pixel to the right ldy scratch19 ; 3126 same pixel row lda #$FF ; 3128 again into the build buffer jsr setSpritePixel ; 312A top-right pixel ldx scratch18 ; 312D pixel column ldy scratch19 ; 312F pixel row iny ; 3131 one pixel down lda #$FF ; 3132 build buffer jsr setSpritePixel ; 3134 bottom-left pixel ldx scratch18 ; 3137 pixel column inx ; 3139 one right ldy scratch19 ; 313A pixel row iny ; 313C one down lda #$FF ; 313D build buffer jsr setSpritePixel ; 313F bottom-right pixel - the dot is complete L_3142: dec scratch1A ; 3142 step to the next unit ldx scratch1A ; 3144 reload the loop counter bpl L_30F6 ; 3146 keep going until unit 0 has been handled ldy #$3F ; 3148 64 bytes to copy ; ---------------------------------------------------------------------- ; Publish the finished shape and set up the sprite geometry. ; ---------------------------------------------------------------------- L_314A: lda spriteBuildBuffer,y ; 314A the assembled ghost sta sprite0Shape,y ; 314D $8A00 = shape of sprite 0 dey ; 3150 next byte down bpl L_314A ; 3151 loop lda spriteXExpandShadow ; 3153 $9023 = shadow of VIC_SPR_EXP_X ora #$03 ; 3156 double the width of sprites 0 and 1 sta spriteXExpandShadow ; 3158 store the shadow back lda spriteYExpandShadow ; 315B $9024 = shadow of VIC_SPR_EXP_Y ora #$03 ; 315E double the height too, so one 2x2 dot covers a whole 4x4 overview cell sta spriteYExpandShadow ; 3160 store the shadow back ; ---------------------------------------------------------------------- ; Turn the absolute box corners into offsets from the dragged unit's destination so that ; mapCellToSpriteXY can keep the box under the cursor while the player scrolls. ; ---------------------------------------------------------------------- ldy selectedUnit ; 3163 $90F8 = the unit the player is actually dragging lda unitDestColTable,y ; 3166 its ordered destination column and #$7F ; 3169 without the 'already there' flag sta scratch18 ; 316B keep it lda unitDestRowTable,y ; 316D its ordered destination row and #$7F ; 3170 without the flag sta scratch19 ; 3172 keep it lda groupBoxLeftOffset ; 3174 left edge of the box sec ; 3177 set carry for the subtraction sbc scratch18 ; 3178 minus the dragged unit's column sta groupBoxLeftOffset ; 317A $922F is now an offset relative to the cursor lda groupBoxRightOffset ; 317D right edge sec ; 3180 set carry for the subtraction sbc scratch18 ; 3181 minus the dragged unit's column sta groupBoxRightOffset ; 3183 $922E likewise lda groupBoxTopOffset ; 3186 top edge sec ; 3189 set carry for the subtraction sbc scratch19 ; 318A minus the dragged unit's row sta groupBoxTopOffset ; 318C $9231 likewise lda groupBoxBottomOffset ; 318F bottom edge sec ; 3192 set carry for the subtraction sbc scratch19 ; 3193 minus the dragged unit's row sta groupBoxBottomOffset ; 3195 $9230 likewise lda #$FF ; 3198 flag value sta groupBoxActive ; 319A $920A: from now on mapCellToSpriteXY applies the box offsets jsr keepGroupBoxInsideMap ; 319D $1F4E: scroll the view until the whole box fits inside the 40x40 map jmp positionCursorSprite ; 31A0 place the ghost over the cursor cell and switch it on, then return to the caller ; ---------------------------------------------------------------------- ; enableSprites - ORs the sprite mask in A into spriteEnableShadow ($9022), the shadow the raster IRQ ; copies into VIC_SPR_ENA. The store is skipped while comcenStunStatus ($922A) bit 6 marks the comcen ; knock-out blackout round, so no sprite can reappear during it; A still returns the combined mask. ; In: A = sprite bit mask (1 << sprite number) ; Out: spriteEnableShadow updated unless suppressed; A = old shadow OR the mask ; Called from: everywhere a cursor, marker or ghost is switched on - $2539, $2566, $2FF0, $3068, ; $333D, $3380, $33AE, $3402, $344A, $3B19, $3D04, $484A, the $C000 overlay and both $6F00 ; overlays ; ---------------------------------------------------------------------- enableSprites: ora spriteEnableShadow ; 31A3 $9022 = shadow of VIC_SPR_ENA, one bit per sprite bit comcenStunStatus ; 31A6 $922A = comcen stun/knock-out status bvs L_31AE ; 31A9 bit 6 set = knock-out blackout round: keep every sprite off sta spriteEnableShadow ; 31AB the raster IRQ writes this to $D015 next frame L_31AE: rts ; 31AE A holds the combined mask either way ; ---------------------------------------------------------------------- ; plotSpritePixel - Sets, clears or toggles one pixel of a sprite shape. The operation is chosen by ; spritePlotMode ($91F3): bit 7 = toggle (EOR), bit 6 = set (ORA), neither = clear (AND with the ; inverted mask). It works by patching the opcode and the mask of the read-modify-write instruction at ; $3232. A negative sprite number selects the 64-byte build buffer at $9025 instead of the real sprite ; data at $8A00 + n*64. ; In: A = sprite number 0-7 (bit 7 set = use $9025), X = pixel column 0-23, Y = pixel row 0-20, ; spritePlotMode $91F3 ; Out: one pixel modified; spritePlotPtr zp_98/zp_99 = byte pointer, spritePlotWork clobbered; ; self-modifies $31EE (sprite number), $3232 (opcode) and $3233 (mask) ; Called from: plotCircleOctantPoints ($32CE) and, through setSpritePixel, buildGroupFormationSprite ; ---------------------------------------------------------------------- plotSpritePixel: bit spritePlotMode ; 31AF $91F3 selects the operation bmi L_31C0 ; 31B2 bit 7 set = toggle the pixel bvs setSpritePixel ; 31B4 bit 6 set = set the pixel sta L_31ED+1 ; 31B6 clear mode: stash the sprite number in the LDX # operand at $31EE lda #$29 ; 31B9 opcode of AND #imm sta spritePlotOperation ; 31BB patch the read-modify-write instruction at $3232 bne L_31D2 ; 31BE always taken ($29 is never zero) L_31C0: sta L_31ED+1 ; 31C0 toggle mode: stash the sprite number lda #$49 ; 31C3 opcode of EOR #imm sta spritePlotOperation ; 31C5 patch the operation bne L_31D2 ; 31C8 always taken ; ---------------------------------------------------------------------- ; setSpritePixel - Entry point of plotSpritePixel that forces the ORA (set) operation. Note that the ; mask is still inverted further down unless spritePlotMode has bit 6 or 7 set, so callers set ; spritePlotMode = $40 first; buildGroupFormationSprite does exactly that. ; In: A = sprite number (bit 7 = build buffer), X = pixel column, Y = pixel row ; Out: the pixel is set ; Called from: buildGroupFormationSprite ($3120, $312A, $3134, $313F) and by branch from $31B4 ; ---------------------------------------------------------------------- setSpritePixel: sta L_31ED+1 ; 31CA stash the sprite number in the LDX # operand at $31EE lda #$09 ; 31CD opcode of ORA #imm sta spritePlotOperation ; 31CF patch the operation at $3232 ; ---------------------------------------------------------------------- ; Shared body: build a pointer to the byte holding the pixel and the bit mask to apply. ; ---------------------------------------------------------------------- L_31D2: stx spritePlotWork ; 31D2 keep the pixel column lda L_31ED+1 ; 31D5 the sprite number stashed above bpl L_31E5 ; 31D8 0-7 = a real sprite lda #$25 ; 31DA negative: use the scratch shape buffer at $9025... sta spritePlotPtr ; 31DC low byte of the pointer lda #$90 ; 31DE high byte $90 sta spritePlotPtrHi ; 31E0 pointer = $9025 jmp L_31FE ; 31E2 ...which needs no per-sprite offset L_31E5: lda #$00 ; 31E5 real sprite: base of the sprite data block sta spritePlotPtr ; 31E7 low byte lda #$8A ; 31E9 high byte $8A sta spritePlotPtrHi ; 31EB pointer = $8A00 L_31ED: ldx #$FF ; 31ED sprite number, patched at $31B6/$31C0/$31CA lda spritePlotPtr ; 31EF add the sprite's 64-byte offset clc ; 31F1 clear carry for the 16-bit addition of the sprite offset adc spriteOffsetLoTable,x ; 31F2 $2F95[n] = low byte of n*64 sta spritePlotPtr ; 31F5 store it back lda spritePlotPtrHi ; 31F7 high byte of the pointer adc spriteOffsetHiTable,x ; 31F9 $2F9D[n] = high byte of n*64 sta spritePlotPtrHi ; 31FC pointer now addresses the shape of sprite n L_31FE: lda spritePlotWork ; 31FE the pixel column tax ; 3201 keep a copy in X and #$07 ; 3202 bit number inside the byte sta spritePlotWork ; 3204 save it for the mask lookup txa ; 3207 column again lsr a ; 3208 column/2 lsr a ; 3209 column/4 lsr a ; 320A column/8 = byte 0, 1 or 2 of a sprite row clc ; 320B clear carry before folding the byte offset into the pointer adc spritePlotPtr ; 320C fold it into the pointer sta spritePlotPtr ; 320E low byte bcc L_3214 ; 3210 no carry out inc spritePlotPtrHi ; 3212 carry into the high byte L_3214: ldx spritePlotWork ; 3214 bit number lda bootReadSectorLoop,x ; 3217 $C008[bit] = $80 >> bit (also textEngineC000:bitMaskTable_C008) bit spritePlotMode ; 321A which operation was chosen? bmi L_3223 ; 321D toggle: EOR with the plain mask bvs L_3223 ; 321F set: ORA with the plain mask eor #$FF ; 3221 clear: AND with the inverted mask L_3223: sta spritePlotOperation+1 ; 3223 patch the immediate operand of the operation at $3232 tya ; 3226 pixel row 0-20 sta spritePlotWork ; 3227 keep it asl a ; 322A row*2 clc ; 322B clear carry for row*2 + row adc spritePlotWork ; 322C row*3 = byte index of that sprite row (3 bytes per row) tay ; 322F Y = index into the shape lda (spritePlotPtr),y ; 3230 read the shape byte spritePlotOperation: ora #$FF ; 3232 patched: ORA/AND/EOR with the patched mask sta (spritePlotPtr),y ; 3234 write the modified byte back rts ; 3236 done ; Two work bytes living inside the code. spritePlotWork: .byte $00 ; 3237 . scratch used by plotSpritePixel: pixel column, then bit number, then pixel row circlePointIndex: .byte $00 ; 3238 which of the eight mirrored circle points plotCircleOctantPoints is plotting (7 down to 0) ; ---------------------------------------------------------------------- ; drawCircleInSprite - Draws a circle of radius X (1-10) centred on pixel (12,10) - the middle of a ; 24x21 sprite - into sprite A, using 8-way octant symmetry. The dx/dy point lists of each radius live ; in $9F69.. and are reached through the pointer tables at $9F37/$9F41 (dx) and $9F4B/$9F55 (dy); the ; index of the last point of the octant comes from $9F5F. spritePlotMode decides whether the pixels ; are set, cleared or toggled. Radius 1 degenerates into a filled 3x3 dot. ; In: A = sprite number (bit 7 = build buffer), X = radius 1-10, spritePlotMode $91F3 ; Out: the circle is plotted; activeSpriteIndex $91CC = A, circleStepsLeft $91F2 = $FF, ; circleDxPtr/circleDyPtr zp_AD-zp_B0 point at the two lists ; Called from: drawRingsInSprite4 ($3471), the unit blob markers ($3D13, $3D43) and overlay B's ; range rings ($7190-$71DA) ; ---------------------------------------------------------------------- drawCircleInSprite: sta activeSpriteIndex ; 3239 $91CC: plotCircleOctantPoints will plot into this sprite dex ; 323C the tables are indexed by radius-1 lda titleScreenRow20Col23,x ; 323D $9F37[r-1] = low byte of the dx list (also graphicsData9300:circleDxPtrLoTable) sta circleDxPtr ; 3240 zp_AD lda titleScreenRow20Col33,x ; 3242 $9F41[r-1] = high byte (always $9F) (also graphicsData9300:circleDxPtrHiTable) sta circleDxPtrHi ; 3245 zp_AE lda titleScreenRow21Col03,x ; 3247 $9F4B[r-1] = low byte of the dy list (also graphicsData9300:circleDyPtrLoTable) sta circleDyPtr ; 324A zp_AF lda titleScreenRow21Col13,x ; 324C $9F55[r-1] = high byte (also graphicsData9300:circleDyPtrHiTable) sta circleDyPtrHi ; 324F zp_B0 ldy titleScreenRow21Col23,x ; 3251 $9F5F[r-1] = index of the last point of the octant (also graphicsData9300:circleStepCountTable) sty circleStepsLeft ; 3254 $91F2 = octant point counter ; ---------------------------------------------------------------------- ; Walk the octant point list from the last entry down to entry 0. ; ---------------------------------------------------------------------- L_3257: jsr computeCircleOctantPoints; 3257 mirror point Y into the 8 symmetric coordinates jsr plotCircleOctantPoints ; 325A plot those 8 pixels dec circleStepsLeft ; 325D previous point of the octant ldy circleStepsLeft ; 3260 reload it as the index bpl L_3257 ; 3263 loop until point 0 has been plotted rts ; 3265 circle complete ; ---------------------------------------------------------------------- ; computeCircleOctantPoints - Expands one octant point (dx,dy) of a circle into the coordinates of all ; eight mirrored points, writing them into circlePointX0-7 ($32DA) and circlePointY0-7 ($32E2). The ; centre is pixel (12,10), the middle of a 24x21 sprite. ; In: Y = index into the octant lists, circleDxPtr zp_AD/zp_AE, circleDyPtr zp_AF/zp_B0 ; Out: $32DA-$32E9 filled with 8 X and 8 Y coordinates ; Called from: drawCircleInSprite ($3257) ; ---------------------------------------------------------------------- computeCircleOctantPoints: clc ; 3266 prepare the additions lda #$0C ; 3267 12 = centre column of a 24-pixel-wide sprite adc (circleDxPtr),y ; 3269 +dx of this octant point sta circlePointX0 ; 326B X of mirrored point 0 sta circlePointX7 ; 326E X of mirrored point 7 sec ; 3271 prepare the subtractions lda #$0C ; 3272 centre column again sbc (circleDxPtr),y ; 3274 12-dx sta circlePointX3 ; 3276 X of mirrored point 3 sta circlePointX4 ; 3279 X of mirrored point 4 clc ; 327C clear carry for the next addition lda #$0C ; 327D centre column adc (circleDyPtr),y ; 327F 12+dy - the octant swap that mirrors about the diagonal sta circlePointX1 ; 3281 X of mirrored point 1 sta circlePointX6 ; 3284 X of mirrored point 6 sec ; 3287 set carry for the next subtraction lda #$0C ; 3288 centre column sbc (circleDyPtr),y ; 328A 12-dy sta circlePointX2 ; 328C X of mirrored point 2 sta circlePointX5 ; 328F X of mirrored point 5 clc ; 3292 clear carry for the next addition lda #$0A ; 3293 10 = centre row of a 21-pixel-high sprite adc (circleDyPtr),y ; 3295 10+dy sta circlePointY0 ; 3297 Y of mirrored point 0 sta circlePointY3 ; 329A Y of mirrored point 3 sec ; 329D set carry for the next subtraction lda #$0A ; 329E centre row sbc (circleDyPtr),y ; 32A0 10-dy sta circlePointY4 ; 32A2 Y of mirrored point 4 sta circlePointY7 ; 32A5 Y of mirrored point 7 clc ; 32A8 clear carry for the next addition lda #$0A ; 32A9 centre row adc (circleDxPtr),y ; 32AB 10+dx sta circlePointY1 ; 32AD Y of mirrored point 1 sta circlePointY2 ; 32B0 Y of mirrored point 2 sec ; 32B3 set carry for the next subtraction lda #$0A ; 32B4 centre row sbc (circleDxPtr),y ; 32B6 10-dx sta circlePointY5 ; 32B8 Y of mirrored point 5 sta circlePointY6 ; 32BB Y of mirrored point 6 rts ; 32BE all eight points ready ; ---------------------------------------------------------------------- ; plotCircleOctantPoints - Plots the eight mirrored points prepared by computeCircleOctantPoints into ; the sprite named by activeSpriteIndex, counting circlePointIndex ($3238) down from 7 to 0. ; In: circlePointX0-7 $32DA, circlePointY0-7 $32E2, activeSpriteIndex $91CC, spritePlotMode $91F3 ; Out: 8 pixels plotted; circlePointIndex = $FF ; Called from: drawCircleInSprite ($325A) ; ---------------------------------------------------------------------- plotCircleOctantPoints: ldx #$07 ; 32BF eight symmetric points per octant step stx circlePointIndex ; 32C1 keep the point index in memory, X is needed by the plotter ; ---------------------------------------------------------------------- ; Plot the eight mirrored points of the current octant step. ; ---------------------------------------------------------------------- L_32C4: ldy circlePointY0,x ; 32C4 $32E2[n] = Y coordinate of point n lda circlePointX0,x ; 32C7 $32DA[n] = X coordinate of point n tax ; 32CA X = pixel column lda activeSpriteIndex ; 32CB the sprite chosen by drawCircleInSprite jsr plotSpritePixel ; 32CE set/clear/toggle the pixel dec circlePointIndex ; 32D1 next point ldx circlePointIndex ; 32D4 reload the index bpl L_32C4 ; 32D7 loop until point 0 has been plotted rts ; 32D9 done ; Sixteen RAM bytes inside the code: the eight X coordinates ($32DA-$32E1) and the eight Y coordinates ; ($32E2-$32E9) of the mirrored points of one circle step. Written by computeCircleOctantPoints and ; read, indexed 0-7, by plotCircleOctantPoints. circlePointX0: .byte $00 ; 32DA . X coordinate of mirrored circle point 0 circlePointX1: .byte $00 ; 32DB X coordinate of mirrored circle point 1 circlePointX2: .byte $00 ; 32DC X coordinate of mirrored circle point 2 circlePointX3: .byte $00 ; 32DD X coordinate of mirrored circle point 3 circlePointX4: .byte $00 ; 32DE X coordinate of mirrored circle point 4 circlePointX5: .byte $00 ; 32DF X coordinate of mirrored circle point 5 circlePointX6: .byte $00 ; 32E0 X coordinate of mirrored circle point 6 circlePointX7: .byte $00 ; 32E1 X coordinate of mirrored circle point 7 circlePointY0: .byte $00 ; 32E2 Y coordinate of mirrored circle point 0 circlePointY1: .byte $00 ; 32E3 Y coordinate of mirrored circle point 1 circlePointY2: .byte $00 ; 32E4 Y coordinate of mirrored circle point 2 circlePointY3: .byte $00 ; 32E5 Y coordinate of mirrored circle point 3 circlePointY4: .byte $00 ; 32E6 Y coordinate of mirrored circle point 4 circlePointY5: .byte $00 ; 32E7 Y coordinate of mirrored circle point 5 circlePointY6: .byte $00 ; 32E8 Y coordinate of mirrored circle point 6 circlePointY7: .byte $00 ; 32E9 Y coordinate of mirrored circle point 7 ; ---------------------------------------------------------------------- ; hideSprite1 - Clears bit 1 of the sprite enable shadow and writes the result straight to ; VIC_SPR_ENA, so the unit marker sprite disappears in the current frame instead of at the next raster ; interrupt. ; In: spriteEnableShadow $9022 ; Out: $9022 and $D015 with bit 1 cleared; A clobbered ; Called from: the battlefield cursor and selection code ($229F, $22F4, $2359, $253C, $2540, $3D4E) ; ---------------------------------------------------------------------- hideSprite1: lda spriteEnableShadow ; 32EA $9022 = shadow of VIC_SPR_ENA and #$FD ; 32ED clear bit 1 = sprite 1, the unit marker on the overview map sta spriteEnableShadow ; 32EF keep the shadow in step sta VIC_SPR_ENA ; 32F2 write $D015 at once so the marker vanishes immediately rts ; 32F5 done ; ---------------------------------------------------------------------- ; hideSprite0Dead - Dead code: a byte-for-byte copy of hideSprite1 that clears bit 0 (sprite 0) ; instead of bit 1. No traced code refers to $32F6, and callers that want sprite 0 off clear the ; shadow themselves, so this is a leftover of an earlier build. ; In: none (never called) ; Out: would clear bit 0 of $9022 and $D015 ; ---------------------------------------------------------------------- hideSprite0Dead: lda spriteEnableShadow ; 32F6 dead code: sprite enable shadow and #$FE ; 32F9 clear bit 0 = sprite 0 sta spriteEnableShadow ; 32FB store the shadow sta VIC_SPR_ENA ; 32FE write it to $D015 rts ; 3301 return ; ---------------------------------------------------------------------- ; showCrosshairSprite - Loads the 63-byte diamond crosshair (the second shape of the pair at $9EA4, ; bytes $9EE3-$9F21) into sprite 2 and falls into the shared body at $3308, which paints it black, ; doubles it in both directions, parks it over the centre cell of the 7x5 tactical view and enables ; it. ; In: none ; Out: sprite 2 shape/colour/size/position set, sprite 2 enabled ; Called from: runTargetCursorLoop ($21AB) when the player is choosing an attack target ; ---------------------------------------------------------------------- showCrosshairSprite: ldx #$7D ; 3302 $7D = last byte of shape 1 ($9EE3-$9F21), the diamond crosshair bne L_3308 ; 3304 always taken ; ---------------------------------------------------------------------- ; showCursorBoxSprite - Same as showCrosshairSprite but copies the first shape of the pair ($9EA4- ; $9EE2), the hollow box that frames the cursor cell of the tactical view. ; In: none ; Out: sprite 2 holds the box, black, expanded, at sprite X 256 / Y $4D, enabled ; Called from: battlefield entry ($1FEB, $2260) and the order-cancel path ($4C56, by JMP) ; ---------------------------------------------------------------------- showCursorBoxSprite: ldx #$3E ; 3306 $3E = last byte of shape 0 ($9EA4-$9EE2), the hollow cursor box ; ---------------------------------------------------------------------- ; Shared body: copy 63 bytes backwards into sprite 2, then make it a black, double-sized frame sitting ; over the centre cell of the 7x5 tactical view. ; ---------------------------------------------------------------------- L_3308: ldy #$3E ; 3308 63 bytes, copied from the end backwards L_330A: lda titleScreenRow16Col36,x ; 330A $9EA4 holds the two 63-byte shapes back to back (also graphicsData9300:cursorBoxSprite) sta sprite2Shape,y ; 330D $8A80 = shape of sprite 2 dex ; 3310 previous source byte dey ; 3311 previous destination byte bpl L_330A ; 3312 loop over all 63 bytes lda #$00 ; 3314 colour 0 sta sprite2ColourShadow ; 3316 $900A = sprite 2 colour shadow: black lda spriteXExpandShadow ; 3319 $9023 = X expansion shadow ora #$04 ; 331C double the width of sprite 2 sta spriteXExpandShadow ; 331E store it back lda spriteYExpandShadow ; 3321 $9024 = Y expansion shadow ora #$04 ; 3324 double the height too, so the frame covers a whole 16x16 view cell sta spriteYExpandShadow ; 3326 store it back lda #$00 ; 3329 low byte of the sprite X coordinate sta sprite2XShadow ; 332B $9012 = sprite 2 X shadow lda spriteXMsbShadow ; 332E $9020 = shadow of the sprite X MSB register ora #$04 ; 3331 set bit 2: sprite 2 X coordinate becomes 256 sta spriteXMsbShadow ; 3333 store it back lda #$4D ; 3336 sprite Y 77 sta sprite2YShadow ; 3338 $901A: the centre cell of the 7x5 tactical view lda #$04 ; 333B sprite 2 enable bit jmp enableSprites ; 333D switch it on and return to the caller ; ---------------------------------------------------------------------- ; loadDirectionShapeSprite - Copies sprite shape number A (0-7, one per facing direction) from the ; decompressed shape buffer at $0200 + A*64 into sprite 0 ($8A00) when activeSpriteIndex is 0, or into ; sprite 5 ($8B40) otherwise, and enables that sprite. The buffer is filled by the RLE decompressor at ; $67CB from the compressed shape sets at $FD70/$FE24. ; In: A = shape index 0-7, activeSpriteIndex $91CC = target sprite (0 or 5) ; Out: 63 bytes of sprite 0 or 5 rewritten and the sprite enabled; spritePlotPtr zp_98/zp_99 ; clobbered; ; self-modifies the destination address of the STA at $3373 ; Called from: the unit direction display ($5322) and overlay B ($802A, $810B, $829E) ; ---------------------------------------------------------------------- loadDirectionShapeSprite: sta spritePlotPtrHi ; 3340 shape number in the high byte = shape*256 lda #$00 ; 3342 low byte starts at 0 lsr spritePlotPtrHi ; 3344 16-bit shift right... ror a ; 3346 ...through the low byte lsr spritePlotPtrHi ; 3347 and again ror a ; 3349 shape*64 sta spritePlotPtr ; 334A low byte of the source pointer lda #$00 ; 334C no extra low offset ldy #$02 ; 334E $0200 = base of the decompressed shape buffer clc ; 3350 clear carry for the 16-bit pointer addition adc spritePlotPtr ; 3351 add the shape offset sta spritePlotPtr ; 3353 source pointer low byte tya ; 3355 base page $02 adc spritePlotPtrHi ; 3356 plus the high byte of shape*64 sta spritePlotPtrHi ; 3358 source pointer = $0200 + shape*64 ldx #$00 ; 335A default destination low byte ldy #$8A ; 335C default destination high byte: $8A00 = sprite 0 lda activeSpriteIndex ; 335E $91CC = which sprite to load beq L_3367 ; 3361 sprite 0 keeps the default ldx #$40 ; 3363 otherwise $8B40... ldy #$8B ; 3365 ...= the shape of sprite 5 L_3367: stx L_3373+1 ; 3367 patch the low byte of the STA absolute,x at $3373 sty L_3373+2 ; 336A patch its high byte ldy #$3E ; 336D 63 bytes to copy ldx #$3E ; 336F same index for source and destination L_3371: lda (spritePlotPtr),y ; 3371 read from the shape buffer L_3373: sta irqVectorHi,x ; 3373 patched to $8A00,x or $8B40,x; the $FFFF resting value is never used dex ; 3376 previous destination byte dey ; 3377 previous source byte bpl L_3371 ; 3378 loop over all 63 bytes ldx activeSpriteIndex ; 337A which sprite was loaded lda spriteBitMasks,x ; 337D $2F85[n] = 1 << n jmp enableSprites ; 3380 switch it on and return to the caller ; ---------------------------------------------------------------------- ; showComcenCrossMarker - Puts the small cross marker at $9F22 into rows 7-13 of sprite 2, colours it ; light red and parks it on the comcen's cell of overlay B's 21x17 radar grid (grid column 10, row 8 - ; the grid is always drawn centred on the comcen). ; In: none ; Out: sprite 2 shape/colour/position set and enabled, gridCursorCol zp_67 = 10, gridCursorRow zp_68 = ; 8 ; Called from: overlay B ($75B6) while the RADAR/MISSILE screen is being built ; ---------------------------------------------------------------------- showComcenCrossMarker: lda #$02 ; 3383 sprite 2 jsr clearSpriteShape ; 3385 blank its 64 shape bytes first ldy #$14 ; 3388 21 bytes = 7 sprite rows of 3 bytes L_338A: lda titleScreenRow20Col02,y ; 338A $9F22 = the small cross/plus shape (also graphicsData9300:comcenCrossMarkerShape) sta sprite2ShapeRow7,y ; 338D $8A95 = sprite 2 shape + $15, i.e. row 7 of the sprite dey ; 3390 previous byte bpl L_338A ; 3391 loop over the 21 bytes lda #$0A ; 3393 colour $0A = light red sta sprite2ColourShadow ; 3395 $900A = sprite 2 colour shadow ldx #$0A ; 3398 grid column 10 - where the own comcen always sits stx gridCursorCol ; 339A zp_67 = radar grid cursor column ldy #$08 ; 339C grid row 8 sty gridCursorRow ; 339E zp_68 = radar grid cursor row jsr isRadarGridCellInRange ; 33A0 overlay B range test; its carry result is never examined here, so the call is a leftover jsr gridCellToSpriteCoords ; 33A3 grid cell -> sprite coordinates stx sprite2XShadow ; 33A6 $9012 = sprite 2 X shadow sty sprite2YShadow ; 33A9 $901A = sprite 2 Y shadow lda #$04 ; 33AC sprite 2 enable bit jmp enableSprites ; 33AE switch it on and return to the caller ; ---------------------------------------------------------------------- ; gridCellToSpriteCoords - Converts a cell of overlay B's 21x17 radar grid (one map cell per 8x8 ; character cell) into sprite coordinates: spriteX = column*8 + $27, spriteY = row*8 + $43. The grid ; starts at character position (3,3); the two constants fold in the VIC sprite origin (24,50) and the ; offset of the marker shapes inside their sprite. ; In: X = grid column 0-20, Y = grid row 0-16 ; Out: X = sprite X, Y = sprite Y; A clobbered ; Called from: showComcenCrossMarker ($33A3), getUnitSpriteCentreOnGrid ($3410) and overlay B ; ($7709, $77BC) ; ---------------------------------------------------------------------- gridCellToSpriteCoords: txa ; 33B1 grid column asl a ; 33B2 column*2 asl a ; 33B3 column*4 asl a ; 33B4 column*8 - the radar grid uses one 8x8 character cell per map cell clc ; 33B5 clear carry before adding the grid origin adc #$27 ; 33B6 sprite X of grid column 0 tax ; 33B8 return it in X tya ; 33B9 grid row asl a ; 33BA row*2 asl a ; 33BB row*4 asl a ; 33BC row*8 clc ; 33BD clear carry before adding the grid origin adc #$43 ; 33BE sprite Y of grid row 0 tay ; 33C0 return it in Y rts ; 33C1 done ; Three 2-entry tables indexed by explosion type: 0 = comcen missile strike, 1 = drone detonation. ; They give the sprite offsets that centre the ring blob on the target cell and the number of ; concentric rings that make the blob. explosionMarkerXOffset: .byte $0A,$08 ; 33C2 .. X offsets: 10 pixels for a missile strike, 8 for a drone blast explosionMarkerYOffset: .byte $08 ; 33C4 matching Y offsets: 8 and 9 pixels .byte $09 ; 33C5 . explosionMarkerRingCount: .byte $02 ; 33C6 ring counts: 2 rings for a missile strike, 4 for the bigger drone blast .byte $04 ; 33C7 . ; ---------------------------------------------------------------------- ; showExplosionMarker - Shows the explosion marker for a weapon effect at map cell (X,Y). On the ; battlefield screen it draws a blob of concentric rings into sprite 4 and puts it straight on the ; 4x4-per-cell overview map; a missile strike that arrives while a drone is in the air instead just ; records the cell in the two patched operands of updateExplosionMarker, which keeps the marker in ; step with the scrolling drone camera window. Either way markerSpriteTimer is set to 90 frames, and ; the per-frame update in updateGameFrame switches sprite 4 off when it expires. The survey called ; this routine dead; it is not - it is called by two command handlers. ; In: A = effect type (0 = comcen missile strike, 1 = drone detonation), X = map column, Y = map row; ; droneControlFlags $920D, currentScreen $90FB ; Out: sprite 4 shape/position/colour, markerSpriteTimer $923E = 90; self-modifies $33EA (the effect ; index) and, on the drone path, $3425/$3437 (the marker cell) ; Called from: cmdDroneDetonate ($5375, A=1) and cmdMissileStrike ($541C, A=0) ; ---------------------------------------------------------------------- showExplosionMarker: sta L_33E9+1 ; 33C8 patch the LDX # at $33E9 with the effect index (0 missile, 1 drone) cmp #$00 ; 33CB is this the comcen missile strike? bne L_33E0 ; 33CD no - a drone blast always goes on the overview map lda droneControlFlags ; 33CF $920D = drone control flags bpl L_33E0 ; 33D2 bit 7 clear = no drone in the air, so the drone camera is not on screen jsr startExplosionMarkerTimer; 33D4 start the 90-frame marker countdown stx loadExplosionMarkerCol+1; 33D7 remember the blast column in the LDA # of updateExplosionMarker sty loadExplosionMarkerRow+1; 33DA and the blast row jmp updateExplosionMarker ; 33DD position sprite 4 inside the drone camera window and return ; ---------------------------------------------------------------------- ; Battlefield path: build the ring blob and drop it straight on the overview map. ; ---------------------------------------------------------------------- L_33E0: lda currentScreen ; 33E0 $90FB = which screen is showing; 0 = the battlefield bne L_340A ; 33E3 not on the battlefield - there is nothing to draw on jsr mapCellToSpriteXYRaw ; 33E5 blast cell -> overview map sprite coordinates txa ; 33E8 sprite X of the cell L_33E9: ldx #$FF ; 33E9 effect index, patched at $33C8 sec ; 33EB set carry for the centring subtraction sbc explosionMarkerXOffset,x; 33EC shift left so the ring blob is centred on the cell sta sprite4XShadow ; 33EF $9014 = sprite 4 X shadow tya ; 33F2 sprite Y of the cell sec ; 33F3 set carry for the centring subtraction sbc explosionMarkerYOffset,x; 33F4 shift up by the matching Y offset sta sprite4YShadow ; 33F7 $901C = sprite 4 Y shadow lda explosionMarkerRingCount,x; 33FA how many concentric rings this effect uses jsr drawRingsInSprite4 ; 33FD draw them into sprite 4 lda #$10 ; 3400 sprite 4 enable bit jsr enableSprites ; 3402 switch the marker on ; ---------------------------------------------------------------------- ; startExplosionMarkerTimer - Two-instruction tail shared by both paths of showExplosionMarker: arms ; the explosion marker for 90 frames (about 1.5 seconds). ; In: none ; Out: markerSpriteTimer $923E = $5A ; Called from: showExplosionMarker ($33D4) and by fall-through from $3402 ; ---------------------------------------------------------------------- startExplosionMarkerTimer: lda #$5A ; 3405 $5A = 90 frames sta markerSpriteTimer ; 3407 $923E; updateGameFrame counts it down and hides sprite 4 at zero L_340A: rts ; 340A return (also the exit taken when the battlefield screen is not showing) ; ---------------------------------------------------------------------- ; getUnitSpriteCentreOnGrid - Maps a battlefield cell onto overlay B's radar grid and returns the ; sprite coordinates of the centre of that grid cell, or C=1 when the cell lies outside the 21x17 grid ; drawn around the comcen. ; In: X = map column, Y = map row ; Out: C=0 with X/Y = sprite coordinates of the cell centre, or C=1 and X/Y meaningless ; Called from: the unit tracking code ($5307) and overlay B ($7A03) ; ---------------------------------------------------------------------- getUnitSpriteCentreOnGrid: jsr mapCellToRadarGrid ; 340B overlay B $7A13: map cell -> radar grid cell relative to the comcen, C=1 when off-grid bcs L_341E ; 340E outside the 21x17 grid: return with C=1 jsr gridCellToSpriteCoords ; 3410 grid cell -> sprite coordinates of its top-left tya ; 3413 sprite Y clc ; 3414 clear carry before adding the half-cell offset adc #$04 ; 3415 +4 = half a character cell, i.e. the centre of the cell tay ; 3417 back into Y txa ; 3418 sprite X clc ; 3419 clear carry before adding the half-cell offset adc #$04 ; 341A +4 for the same reason tax ; 341C back into X clc ; 341D C=0 = the cell is on the grid L_341E: rts ; 341E return ; ---------------------------------------------------------------------- ; updateExplosionMarker - Keeps the explosion marker (sprite 4) on the right cell of the drone camera ; window, which is 14 columns by 12 rows of 8x8 pixel cells starting at the view origin. The marker ; cell lives in the two immediate operands at $3425 and $3437, patched by showExplosionMarker. When ; the countdown has expired or the cell has scrolled out of the window, sprite 4 is switched off ; instead. The survey believed nothing ever patched the operands; showExplosionMarker does. ; In: markerSpriteTimer $923E, viewOriginCol zp_A5, viewOriginRow zp_A6, the operands at $3425/$3437 ; Out: sprite 4 position shadows $9014/$901C and its enable bit; A clobbered ; Called from: showExplosionMarker ($33DD, by JMP) and overlay B's drone camera step ($827E) ; ---------------------------------------------------------------------- updateExplosionMarker: lda markerSpriteTimer ; 341F $923E, counted down once per frame by updateGameFrame beq L_344D ; 3422 zero = no explosion pending: make sure sprite 4 is off loadExplosionMarkerCol: lda #$00 ; 3424 blast column, patched at $33D7 sec ; 3426 set carry for the subtraction of the view origin sbc viewOriginCol ; 3427 column relative to the left edge of the drone camera window cmp #$0E ; 3429 the window is 14 cells wide bcs L_344D ; 342B outside it (this also catches negative differences): hide the marker asl a ; 342D x2 asl a ; 342E x4 asl a ; 342F x8 - the drone camera draws one map cell per 8x8 character cell clc ; 3430 clear carry before adding the window origin adc #$78 ; 3431 sprite X that centres the ring blob on window column 0 sta sprite4XShadow ; 3433 $9014 = sprite 4 X shadow loadExplosionMarkerRow: lda #$FF ; 3436 blast row, patched at $33DA sec ; 3438 set carry for the subtraction of the view origin sbc viewOriginRow ; 3439 row relative to the top of the window cmp #$0C ; 343B the window is 12 cells high bcs L_344D ; 343D outside it: hide the marker asl a ; 343F x2 asl a ; 3440 x4 asl a ; 3441 x8 clc ; 3442 clear carry before adding the window origin adc #$5C ; 3443 sprite Y that centres the blob on window row 0 sta sprite4YShadow ; 3445 $901C = sprite 4 Y shadow lda #$10 ; 3448 sprite 4 enable bit jmp enableSprites ; 344A switch it on and return to the caller L_344D: lda spriteEnableShadow ; 344D $9022 = sprite enable shadow and #$EF ; 3450 clear bit 4 = sprite 4, the explosion marker sta spriteEnableShadow ; 3452 store it back (the raster IRQ writes $D015) rts ; 3455 done ; ---------------------------------------------------------------------- ; drawComcenRingSprite - Builds the comcen marker of the drone screen: two concentric rings (radius 2 ; then radius 1, which is a filled 3x3 dot) in sprite 4, coloured with the flashing pair $12 so it ; alternates between white and red. ; In: none ; Out: sprite 4 shape rebuilt, sprite4ColourShadow $900C = $12, sprite 4 left disabled here, ; activeSpriteIndex $91CC = 4, spritePlotMode $91F3 = $40 ; Called from: overlay B ($80E1), right after the view has been centred on the comcen ; ---------------------------------------------------------------------- drawComcenRingSprite: lda #$02 ; 3456 two concentric rings for the comcen marker ; ---------------------------------------------------------------------- ; drawRingsInSprite4 - Shared body of drawComcenRingSprite: clears sprite 4 and draws A concentric ; rings into it, radius A down to radius 1, then gives it the flashing colour pair $12. The ring count ; is held in the LDX # operand at $346E and counted down there. ; In: A = number of rings / radius of the outermost one ; Out: sprite 4 shape and colour; sprite 4 disabled while it is rebuilt; self-modifies $346E ; Called from: showExplosionMarker ($33FD, A = 2 or 4) and by fall-through from drawComcenRingSprite ; ---------------------------------------------------------------------- drawRingsInSprite4: sta L_346D+1 ; 3458 patch the LDX # at $346D with the outermost radius / ring count lda spriteEnableShadow ; 345B $9022 = sprite enable shadow and #$EF ; 345E clear bit 4: hide sprite 4 while its shape is rebuilt sta spriteEnableShadow ; 3460 store it back lda #$40 ; 3463 bit 6 = 'set the pixel' sta spritePlotMode ; 3465 $91F3 = plot mode for drawCircleInSprite lda #$04 ; 3468 sprite 4 jsr clearSpriteShape ; 346A blank its 64 shape bytes L_346D: ldx #$FF ; 346D current radius, patched at $3458 and decremented below lda #$04 ; 346F draw into sprite 4 jsr drawCircleInSprite ; 3471 one circle of that radius, centred on sprite pixel (12,10) dec L_346D+1 ; 3474 next smaller radius bne L_346D ; 3477 loop down to radius 1, which fills the centre with a 3x3 dot lda #$12 ; 3479 $12 = flashing colour pair: alternates colour 2 (red) and colour 1 (white) sta sprite4ColourShadow ; 347B $900C = sprite 4 colour shadow rts ; 347E done ; ---------------------------------------------------------------------- ; orderSelectedUnitToCursor - Issues a MOVE order for the selected unit to the cell under the cursor: ; takes the unit out of its group, queues command $80 unit,col,row into the outgoing ring (so the ; engine, the opponent and the game film all see the same bytes) and refreshes the 'already at the ; destination' flag. ; In: selectedUnit $90F8, viewOriginCol zp_A5, viewOriginRow zp_A6 ; Out: cmdParam0-2 zp_73-zp_75 = unit/column/row, $FB54[unit] bit 6 cleared and bit 7 refreshed, ; 4 bytes queued for the command interpreter, C=0 ; Called from: the fire-button handler of the battlefield cursor loop ($2212) ; ---------------------------------------------------------------------- orderSelectedUnitToCursor: lda viewOriginCol ; 347F left edge column of the tactical view clc ; 3481 clear carry before stepping to the cursor column adc #$03 ; 3482 +3 = the cursor column sta cmdParam1 ; 3484 zp_74 = command argument 1 (destination column) lda viewOriginRow ; 3486 top edge row of the tactical view clc ; 3488 clear carry before stepping to the cursor row adc #$02 ; 3489 +2 = the cursor row sta cmdParam2 ; 348B zp_75 = command argument 2 (destination row) ldx selectedUnit ; 348D $90F8 = the unit being ordered lda unitDisplayFlagsTable,x ; 3490 $FB54[unit] = local display/group flags and #$BF ; 3493 clear bit 6: a unit given its own order leaves its group sta unitDisplayFlagsTable,x ; 3495 store the flags back stx cmdParam0 ; 3498 zp_73 = command argument 0 (unit index) lda #$80 ; 349A command byte $80 = MOVE unit,col,row jsr queueCmd4 ; 349C $560A: queue the 4 bytes for the engine, the link and the film jsr updateUnitAtDestinationFlag; 349F set or clear the 'standing on its destination' bit clc ; 34A2 C=0 tells the cursor loop the order was accepted rts ; 34A3 done ; ---------------------------------------------------------------------- ; updateUnitAtDestinationFlag - Sets bit 7 of unitDisplayFlags ($FB54) for the unit in cmdParam0 when ; its current cell is the same as the ordered destination in cmdParam1/cmdParam2, and clears it ; otherwise. The bit is what the console and the map drawing use to tell a moving unit from an idle ; one. ; In: cmdParam0 zp_73 = unit, cmdParam1 zp_74 = destination column, cmdParam2 zp_75 = destination row ; Out: $FB54[unit] bit 7; self-modifies the ORA operand at $34C1 ; Called from: orderSelectedUnitToCursor ($349F) and the command executors at $4A4D and $4C10 ; ---------------------------------------------------------------------- updateUnitAtDestinationFlag: ldy cmdParam0 ; 34A4 zp_73 = unit index lda #$00 ; 34A6 assume the unit still has somewhere to go ldx unitColTable,y ; 34A8 $F640[unit] = current map column cpx cmdParam1 ; 34AB same as the ordered column? bne L_34B8 ; 34AD no - it is still travelling ldx unitRowTable,y ; 34AF $F6A4[unit] = current map row cpx cmdParam2 ; 34B2 same as the ordered row? bne L_34B8 ; 34B4 no - still travelling lda #$80 ; 34B6 bit 7 = the unit is already standing on its destination L_34B8: sta L_34C0+1 ; 34B8 patch the ORA # at $34C0 with $80 or $00 lda unitDisplayFlagsTable,y ; 34BB $FB54[unit] and #$7F ; 34BE clear the old 'at destination' bit L_34C0: ora #$FF ; 34C0 patched: put the freshly computed bit back sta unitDisplayFlagsTable,y ; 34C2 store the flags rts ; 34C5 done ; ---------------------------------------------------------------------- ; applyMoveOrder - Executor of command $80 MOVE (unit, column, row). It takes the unit out of its ; group, republishes the group bits into the local flags copy, and falls into setUnitDestination to ; store the clamped destination. The survey listed this as an unreferenced older version; it is in ; fact live - cmdMoveUnit jumps here from $5017 - and there is no other move executor in the build. ; In: cmdParam0 zp_73 = unit, cmdParam1/cmdParam2 zp_74/zp_75 = destination (already mirrored for a ; remote command) ; Out: $F708[unit] bit 6 cleared, $FB54[unit] group bits refreshed, then everything setUnitDestination ; does ; Called from: cmdMoveUnit ($5017, by JMP) ; ---------------------------------------------------------------------- applyMoveOrder: ldx cmdParam0 ; 34C6 zp_73 = the unit being ordered lda unitFlagsTable,x ; 34C8 $F708[unit] = authoritative unit flags (facing, side, group) and #$BF ; 34CB clear bit 6: an individual move order takes the unit out of its group sta unitFlagsTable,x ; 34CD store the flags back jsr syncLocalGroupFlags ; 34D0 $4C6B: copy the group bits into the $FB54 display flags ldy cmdParam0 ; 34D3 Y = unit index for the destination code below ; ---------------------------------------------------------------------- ; setUnitDestination - Stores a destination for unit Y. A comcen whose speed option is class 4 (fixed) ; has its destination forced back to its own cell; a destination equal to the current cell is stored ; with bit 7 set, which marks the unit idle and is mirrored into unitDisplayFlags bit 7; anything else ; is clamped to the 40x40 map. The walking waypoint is reset to the unit's current cell (except for ; the solo trainer's own units, which keep theirs), and the order cancels any dig-in in progress. ; In: Y = unit index, cmdParam1/cmdParam2 zp_74/zp_75 = destination column/row, gamePhase zp_B1, ; comcenSpeedClass $92B4 / opponentComcenSpeedClass $92B5, comcenUnit $9236, ; soloTrainerAtGameStart $92FE ; Out: $F7D0/$F834 destination, $FB54 bit 7, $F898/$F8FC waypoint, $F960 bits 5-7 cleared, ; $F76C bit 6 cleared; self-modifies the ORA operand at $3521 ; Called from: the group formation move ($4A31, $4A9B), the destination command ($4D21), the group ; order tail ($4C68, by JMP) and by fall-through from applyMoveOrder ; ---------------------------------------------------------------------- setUnitDestination: lda gamePhase ; 34D5 zp_B1 = 0 only while the battle is actually running bne L_34FA ; 34D7 in the setup, film and menu phases the comcen speed rule does not apply cpy #$31 ; 34D9 unit 49 = player 0's comcen beq L_34E1 ; 34DB it is a comcen cpy #$63 ; 34DD unit 99 = player 1's comcen bne L_34FA ; 34DF not a comcen: nothing special L_34E1: lda comcenSpeedClass ; 34E1 $92B4 = the local player's COMCEN SPEED option cpy comcenUnit ; 34E4 $9236 = index of the local player's comcen beq L_34EC ; 34E7 it is our own comcen, use our own option lda opponentComcenSpeedClass; 34E9 $92B5 = the opponent's comcen speed class L_34EC: cmp #$04 ; 34EC class 4 = the comcen may not move at all bne L_34FA ; 34EE any other class: the ordered destination stands lda unitColTable,y ; 34F0 immobile comcen: destination column... sta cmdParam1 ; 34F3 ...forced to where it already stands lda unitRowTable,y ; 34F5 and the same for the row sta cmdParam2 ; 34F8 zp_75 ; ---------------------------------------------------------------------- ; Store the destination: equal to the current cell means 'idle', anything else is clamped to the map, ; and the idle bit is mirrored into the display flags. ; ---------------------------------------------------------------------- L_34FA: lda cmdParam1 ; 34FA destination column cmp unitColTable,y ; 34FC $F640[unit] = current column bne L_350E ; 34FF different - the unit really has to move lda cmdParam2 ; 3501 destination row cmp unitRowTable,y ; 3503 $F6A4[unit] = current row bne L_350E ; 3506 different - the unit really has to move lda cmdParam1 ; 3508 same cell: take the column again ora #$80 ; 350A bit 7 = 'destination reached, nothing to do' bmi L_3513 ; 350C always taken (bit 7 was just set) L_350E: lda cmdParam1 ; 350E destination column jsr clampMapCoord ; 3510 keep it inside the 40x40 battlefield L_3513: sta unitDestColTable,y ; 3513 $F7D0[unit] = destination column (bit 7 = idle) and #$80 ; 3516 isolate the idle bit sta L_3520+1 ; 3518 patch the ORA # at $3520 with it lda unitDisplayFlagsTable,y ; 351B $FB54[unit] = display flags and #$7F ; 351E clear the old idle bit L_3520: ora #$FF ; 3520 patched: mirror the new idle bit into bit 7 sta unitDisplayFlagsTable,y ; 3522 store the display flags lda cmdParam2 ; 3525 destination row jsr clampMapCoord ; 3527 clamp it to the map as well sta unitDestRowTable,y ; 352A $F834[unit] = destination row bit soloTrainerAtGameStart ; 352D $92FE bit 7 = this game is against the solo trainer bpl L_3536 ; 3530 modem or film game: always reset the waypoint cpy #$32 ; 3532 units 50-99 are the trainer's own units bcs L_354C ; 3534 the trainer maintains their waypoints itself ; ---------------------------------------------------------------------- ; Reset the walking waypoint to the cell the unit is standing on, keeping the two flag bits in the top ; of those bytes. ; ---------------------------------------------------------------------- L_3536: lda unitPathColTable,y ; 3536 $F898[unit] = waypoint column, bits 6-7 are flags and #$C0 ; 3539 keep only those two flag bits ora unitColTable,y ; 353B waypoint column = the cell the unit is standing on sta unitPathColTable,y ; 353E store it back lda unitPathRowTable,y ; 3541 $F8FC[unit] = waypoint row, bits 6-7 are flags and #$C0 ; 3544 keep the flag bits ora unitRowTable,y ; 3546 waypoint row = current row sta unitPathRowTable,y ; 3549 store it back ; ---------------------------------------------------------------------- ; An order to move cancels any dig-in the unit had started or completed. ; ---------------------------------------------------------------------- L_354C: lda unitStunTable,y ; 354C $F960[unit]: low nibble stun counter, bits 5-7 dig progress and #$1F ; 354F clear bits 5-7 - the unit stops digging in sta unitStunTable,y ; 3551 store it back lda unitTypeTable,y ; 3554 $F76C[unit] = type byte and #$BF ; 3557 clear bit 6 = no longer dug in / fortified sta unitTypeTable,y ; 3559 store it back rts ; 355C the order is stored ; ---------------------------------------------------------------------- ; clampMapCoord - Clamps a signed map coordinate to the battlefield: negative becomes 0, anything from ; 40 upwards becomes 39. ; In: A = coordinate ; Out: A = 0..39 ; Called from: setUnitDestination ($3510 for the column, $3527 for the row) ; ---------------------------------------------------------------------- clampMapCoord: bpl L_3561 ; 355D already zero or positive lda #$00 ; 355F negative coordinates clamp to column/row 0 L_3561: cmp #$28 ; 3561 40 = width and height of the battlefield bcc L_3567 ; 3563 inside the map lda #$27 ; 3565 39 = last valid column/row L_3567: rts ; 3567 done ; ---------------------------------------------------------------------- ; markAllUnitsVisible - Sets bits 6 and 7 (spotted this cycle, currently visible) of unitEnergy for ; all 100 units, i.e. reveals the whole battlefield. Used at the end of a game and whenever no game is ; in progress. ; In: none ; Out: $F9C4[0..99] bits 6-7 set; Y = $FF, A clobbered ; Called from: the game reset at $4018 and ageUnitVisibility ($3579, by branch) ; ---------------------------------------------------------------------- markAllUnitsVisible: ldy #$63 ; 3568 unit 99 down to unit 0 L_356A: lda unitEnergyTable,y ; 356A $F9C4[unit]: bits 0-5 energy, bit 6 spotted, bit 7 visible ora #$C0 ; 356D mark it both spotted and visible sta unitEnergyTable,y ; 356F store it back dey ; 3572 previous unit bpl L_356A ; 3573 loop over all 100 units rts ; 3575 done ; ---------------------------------------------------------------------- ; ageUnitVisibility - Run once per unit-processing cycle, before the spotting passes: every unit that ; was spotted during the last cycle (energy bit 6) stays visible (bit 7) and everything else fades ; out, and bit 6 is cleared ready for the coming scan. It also resets the unit-scan side filter to ; 'accept every unit'. When the game is over it simply reveals everything instead. ; In: gameEndReason $0BA8, unitEnergyTable $F9C4 ; Out: $F9C4 visibility bits rewritten, the collectUnitsInRadius filter at $37FF/$3800 set to ; compare against 0 with BCC; self-modifies $359C ; Called from: finishRound ($4660) ; ---------------------------------------------------------------------- ageUnitVisibility: lda gameEndReason ; 3576 $0BA8 = nonzero once the game has ended bne markAllUnitsVisible ; 3579 game over: show every unit instead of ageing the flags lda #$00 ; 357B compare value 0... sta scanSideFilterCompare+1 ; 357D ...into the CMP # of the unit scan filter at $37FE lda #$90 ; 3580 opcode of BCC sta scanSideFilterBranch ; 3582 so the filter at $3800 accepts every unit index ldy #$00 ; 3585 unit 0 upwards ; ---------------------------------------------------------------------- ; Shift each unit's visibility: bit 6 (spotted last cycle) becomes bit 7 (visible now). ; ---------------------------------------------------------------------- L_3587: ldx #$80 ; 3587 assume the unit stays visible lda unitEnergyTable,y ; 3589 $F9C4[unit] and #$40 ; 358C bit 6 = it was spotted during the last scan cycle bne L_3592 ; 358E it was - keep it visible ldx #$00 ; 3590 it was not - it fades out of sight L_3592: lda unitEnergyTable,y ; 3592 read the byte again and #$3F ; 3595 keep the energy value, drop both visibility bits sta L_359B+1 ; 3597 patch the ORA # at $359B with it txa ; 359A $80 (visible) or $00 (hidden) L_359B: ora #$FF ; 359B patched energy value sta unitEnergyTable,y ; 359D bit 7 = visible, bit 6 cleared ready for this cycle's scan iny ; 35A0 next unit cpy #$64 ; 35A1 100 units bcc L_3587 ; 35A3 loop rts ; 35A5 done ; sightRangeByType - base range in cells, indexed by unit type for the spotting pass (entries 0-4) and ; by type+5 for the attack pass (entries 5-9). The terrain bonus below is added on top and the sum is ; capped at 10. sightRangeByType: .byte $03,$03,$03,$05,$03,$03,$03,$06; 35A6 ........ spotting: GRUNT 3, RIDER 3, BOOMER 3, SPY 5, COMCEN 3; attack: GRUNT 3, RIDER 3, BOOMER 6 .byte $05 ; 35AE attack range of SPY 5 and COMCEN 3 (entries 8 and 9) .byte $03 ; 35AF . ; rangeDistanceSqTable - r*(r+1) for r = 0..10. scanVisibleEnemies compares the squared distance of a ; candidate against this, which rounds the square search box into a circle. rangeDistanceSqTable: .byte $00,$02,$06,$0C,$14,$1E,$2A,$38; 35B0 ......*8 r*(r+1) for r = 0..7: 0, 2, 6, 12, 20, 30, 42, 56 .byte $48 ; 35B8 r*(r+1) for r = 8, 9, 10: 72, 90, 110 .byte $5A,$6E ; 35B9 Zn ; terrainSightBonusTable - extra range given by the terrain class under the observer (0-7); doubled on ; the spotting pass, counted once on the attack pass. terrainSightBonusTable: .byte $00,$00,$00,$01,$01,$02,$02,$00; 35BB ........ classes 0-7: 0, 0, 0, 1, 1, 2, 2, 0 extra cells of range ; ---------------------------------------------------------------------- ; scanVisibleEnemies - Builds the list of enemy units that unit Y can see (X bit 7 clear, the spotting ; pass) or can shoot at (X bit 7 set, the attack-target pass). The effective range is ; sightRangeByType[type, +5 when attacking] plus the terrain bonus of the cell the unit stands on ; (doubled while spotting), capped at 10 cells. collectUnitsInRadius fills $0400 with the enemy units ; in the square and $0432 with their squared distances; this routine then walks that list and strikes ; off ($0400[n] = $FF) every candidate that is out of range, dead, not yet visible (attack pass), ; protected by the spy/hidden rules (spotting pass) or hidden behind a ridge. $0464[n] receives the ; direction 0-3 from the scanner to the candidate. Candidates that are not in the unit's preferred ; direction (zp_6B) are pushed one step further away, which biases a unit towards the target it is ; already facing. The first sighting of an enemy spy or comcen queues status message $0F / $0D. Only ; the head of the routine ($35C3-$366E) belongs to this chunk; it continues to $3745. ; In: X = pass (bit 7: 0 = spotting, 1 = attack targets), Y = the scanning unit; unit arrays, the ; map, ; the terrain class table at $077E ; Out: $0400 candidate list (entries struck off with $FF), $0432 squared distances, $0464 directions, ; scanResultCount zp_2F = index of the last entry ($FF = none), fireTargetDistance $91F1 = range, ; scanUnitTerrain/$9227, scanUnitType/$9228, scanUnitTerrainHeight/$9229; self-modifies $35F9 ; (range table index) and $362B (distance limit) ; Called from: the boomer target search ($3C8A) and the two scan passes of processUnitStep ($43AA ; spotting, $43DC attack targets) ; ---------------------------------------------------------------------- scanVisibleEnemies: stx scratch20 ; 35C3 zp_20 = pass flag: bit 7 clear = who can I see, bit 7 set = who can I shoot at sty scratch21 ; 35C5 zp_21 = the unit doing the scanning lda unitTypeTable,y ; 35C7 $F76C[unit] = type byte and #$07 ; 35CA unit type 0-4 (GRUNT, RIDER, BOOMER, SPY, COMCEN) sta scanUnitType ; 35CC $9228 = type of the scanning unit, used by the spy/hidden rules below bit scratch20 ; 35CF which pass is this? bpl L_35D6 ; 35D1 spotting pass uses sightRangeByType entries 0-4 clc ; 35D3 clear carry before shifting to the attack half of the table adc #$05 ; 35D4 attack pass uses entries 5-9 L_35D6: sta L_35F8+1 ; 35D6 patch the LDX # at $35F8 with that table index jsr setScanFilterOppositeSide; 35D9 $378B: set the collectUnitsInRadius filter to 'units of the other side only' lda unitTerrainUnderTable,y ; 35DC $FAF0[unit] = the terrain tile hidden under the unit sta scanUnitTerrain ; 35DF $9227 = that tile, kept for the callers tax ; 35E2 index it lda D_077E,x ; 35E3 $077E[tile] = terrain class 0-7 tax ; 35E6 index the class tables with it lda terrainHeightTable,x ; 35E7 $38B3[class] = height, used by the line-of-sight walk sta scanUnitTerrainHeight ; 35EA $9229 = height the observer is standing at lda terrainSightBonusTable,x; 35ED extra range for standing on high ground beq L_35F7 ; 35F0 flat ground: no bonus bit scratch20 ; 35F2 which pass? bmi L_35F7 ; 35F4 attack pass: the bonus counts once asl a ; 35F6 spotting pass: high ground doubles the terrain bonus L_35F7: clc ; 35F7 prepare the addition L_35F8: ldx #$FF ; 35F8 range table index, patched at $35D6 adc sightRangeByType,x ; 35FA base range for this unit type and pass cmp #$0A ; 35FD 10 cells is the hard limit bcc L_3603 ; 35FF below it lda #$0A ; 3601 clamp to 10 L_3603: sta fireTargetDistance ; 3603 $91F1 = effective range of this scan tax ; 3606 index the squared-distance table with it lda rangeDistanceSqTable,x ; 3607 r*(r+1) = the squared distance a target must stay below sta L_362A+1 ; 360A patch the CMP # at $362A with that limit ldx unitColTable,y ; 360D $F640[unit] = the scanner's column lda unitRowTable,y ; 3610 $F6A4[unit] = its row tay ; 3613 Y = centre row lda fireTargetDistance ; 3614 half width of the square to search = the range jsr collectUnitsInRadius ; 3617 $37AA: gather the enemy units in that square into $0400 / $0432 stx scanResultCount ; 361A zp_2F = index of the last candidate ($FF if none) stx scratch18 ; 361C zp_18 = index of the candidate being examined bpl L_3623 ; 361E at least one candidate jmp scanCandidatesDone ; 3620 nothing in range: return with zp_2F = $FF ; ---------------------------------------------------------------------- ; Candidate loop: zp_18 walks the list from the last entry down to entry 0. A candidate that fails any ; test is struck off at $3735 by writing $FF into $0400[n]. ; ---------------------------------------------------------------------- L_3623: lda checksumXorConstant,x ; 3623 $0400[n] = index of the candidate unit (also trainerPlaybook0200:play3Col12) tay ; 3626 Y = candidate unit lda play3Row12,x ; 3627 $0432[n] = squared distance from the scanner L_362A: cmp #$FF ; 362A patched at $360A with r*(r+1) for the effective range bcc L_3631 ; 362C inside the circle L_362E: jmp rejectScanCandidate ; 362E outside it: strike this candidate off the list L_3631: ldx scratch21 ; 3631 the scanning unit jsr initLineBetweenUnits ; 3633 $375D: set up the line walker from scanner to candidate; A = direction 0-3 ldx scratch18 ; 3636 candidate index again sta play4Grp12,x ; 3638 $0464[n] = direction of this candidate from the scanner cmp targetDirection ; 363B zp_6B = the direction the unit prefers (where its current target is) beq L_3657 ; 363D same direction: no penalty lda scanResultCount ; 363F how many candidates were found cmp #$01 ; 3641 index 0 means a single candidate bcc L_3657 ; 3643 only one candidate: no penalty, take it whatever the direction lda play3Row12,x ; 3645 squared distance again cmp #$03 ; 3648 squared distance below 3 = an adjacent or diagonal cell bcc L_3657 ; 364A point blank: no penalty clc ; 364C otherwise... adc #$01 ; 364D ...count it as one step further away than it is sta play3Row12,x ; 364F store the penalised distance back cmp L_362A+1 ; 3652 did the penalty push it out of range? bcs L_362E ; 3655 yes: strike it off L_3657: lda unitTypeTable,y ; 3657 $F76C[candidate] = type byte bmi L_362E ; 365A bit 7 = destroyed: strike it off and #$07 ; 365C candidate type 0-4 bit scratch20 ; 365E which pass? bpl L_3671 ; 3660 spotting pass: apply the spy and hidden-unit rules lda unitEnergyTable,y ; 3662 attack pass: $F9C4[candidate] bit 7 = the target is currently visible bpl L_362E ; 3665 you cannot shoot at something you have not spotted lda L_35F8+1 ; 3667 the range table index of this scan cmp #$07 ; 366A index 7 = BOOMER attacking; 5 and 6 are GRUNT and RIDER bcc beginLineOfSightWalk ; 366C grunts and riders must have a clear line of sight jmp nextScanCandidate ; 366E boomers, spies and comcens fire without one: accept this candidate ; ---------------------------------------------------------------------- ; Spotting pass only (scan mode zp_20 bit 7 clear): the two kinds of target that need more than plain ; range. A = candidate unit type 0-4, X = its index in the candidate lists, Y = its unit index, zp_18 ; = the same list index. ; ---------------------------------------------------------------------- L_3671: cmp #$03 ; 3671 is the candidate a SPY (type 3)? bne L_3685 ; 3673 no - fall through to the cloak rule lda scanUnitType ; 3675 type of the unit that is doing the looking cmp #$03 ; 3678 only a SPY can see another SPY bne L_362E ; 367A anything else never notices it -> drop the candidate lda play3Row12,x ; 367C squared distance from the $0432 list cmp #$14 ; 367F a spy is visible only inside dist^2 20 bcs L_362E ; 3681 farther away -> drop the candidate bcc beginLineOfSightWalk ; 3683 close enough, but it still needs a clear line of sight (always taken, C=0 here) ; ---------------------------------------------------------------------- ; A cloaked unit ($F898 bit 7, set by the CLOAKING ON command) can only be made out from close up, and ; a SPY has twice the range for it. ; ---------------------------------------------------------------------- L_3685: lda unitPathColTable,y ; 3685 cloak flag of the candidate ($F898 bit 7) bpl beginLineOfSightWalk ; 3688 not cloaked -> ordinary line-of-sight test lda play3Row12,x ; 368A squared distance to the cloaked unit cmp #$0C ; 368D nobody sees a cloaked unit beyond dist^2 12 bcs L_362E ; 368F too far -> drop the candidate ldy scanUnitType ; 3691 type of the spotter (this overwrites the candidate index in Y; $36AC reloads it) cpy #$03 ; 3694 is the spotter itself a SPY? beq beginLineOfSightWalk ; 3696 a spy detects cloaked units out to the full dist^2 12 cmp #$06 ; 3698 everyone else only inside dist^2 6 (A still holds the distance) bcs L_362E ; 369A too far for a non-spy -> drop the candidate ; ---------------------------------------------------------------------- ; Line-of-sight walk. The line from the spotter to the candidate was set up by initLineBetweenUnits ; at $3633; zp_1E carries the terrain height of the previous cell and zp_1F the ridge state, both used ; by checkLineOfSightStep. ; ---------------------------------------------------------------------- beginLineOfSightWalk: lda #$80 ; 369C $80 = the ground has not risen yet along this line sta scratch1F ; 369E reset the ridge tracker lda scanUnitTerrainHeight ; 36A0 terrain height under the spotter itself sta scratch1E ; 36A3 the height walk starts at the spotter's own level losWalkNextCell: jsr stepLine ; 36A5 advance the line by one map cell bne L_36F7 ; 36A8 steps left -> this is a cell between the two units ; ---------------------------------------------------------------------- ; The line has reached the candidate's own cell: forest cover and one last height step. ; ---------------------------------------------------------------------- ldx scratch18 ; 36AA index of the candidate in the scan lists ldy checksumXorConstant,x ; 36AC its unit index (the $0400 candidate list lies over the boot checksum table) (also trainerPlaybook0200:play3Col12) ldx unitTerrainUnderTable,y ; 36AF map byte hidden underneath the candidate ($FAF0) lda D_077E,x ; 36B2 -> terrain class 0-8 (the table is addressed with the raw cell byte) cmp #$02 ; 36B5 class 2 = forest bne L_36C7 ; 36B7 open ground -> only the height test is left lda unitEnergyTable,y ; 36B9 energy byte; bit 7 = the unit is already visible bmi L_36C7 ; 36BC a unit that has been seen once stays visible even in the trees ldx scratch18 ; 36BE back to the candidate's list index lda play3Row12,x ; 36C0 its squared distance cmp #$06 ; 36C3 inside dist^2 6 the trees do not hide it bcs rejectScanCandidate ; 36C5 farther away and in forest -> not visible L_36C7: ldx unitTerrainUnderTable,y ; 36C7 terrain byte under the candidate again (X was reused above) lda D_077E,x ; 36CA -> terrain class tax ; 36CD checkLineOfSightStep wants the class in X jsr checkLineOfSightStep ; 36CE last height step: does a ridge stand between us and the target? bcs rejectScanCandidate ; 36D1 blocked -> not visible cpy #$32 ; 36D3 unit 50 = first unit of side 1: is the candidate one of the local player's units? D_36D5: bcc nextScanCandidate ; 36D5 branch opcode patched per side by overlay A ($7BD8: $90 BCC for side 0, $B0 BCS for side 1) - our own unit, accept it without a message lda unitEnergyTable,y ; 36D7 an enemy: bit 7 = it was already visible bmi nextScanCandidate ; 36DA not a first sighting -> no message lda unitTypeTable,y ; 36DC candidate's type byte and #$07 ; 36DF strip the dead/dug-in bits cmp #$03 ; 36E1 only SPY (3) and COMCEN (4) are announced bcc nextScanCandidate ; 36E3 grunt/rider/boomer -> accept silently bne L_36EB ; 36E5 not a spy -> test for the comcen lda #$0F ; 36E7 message $0F 'SPOTTED ENEMY SPY!' bne L_36F1 ; 36E9 (always taken) L_36EB: cmp #$04 ; 36EB type 4 = COMCEN? bne nextScanCandidate ; 36ED neither -> accept silently lda #$0D ; 36EF message $0D 'SPOTTED ENEMY COMCEN!' L_36F1: jsr queueMessageUnlessFilm ; 36F1 queue it (dropped during solo/film playback) jmp nextScanCandidate ; 36F4 candidate accepted, on to the next one ; ---------------------------------------------------------------------- ; An intermediate cell of the line. A unit standing in it can stop a shot but never blocks plain ; spotting; forest and a ridge stop both. ; ---------------------------------------------------------------------- L_36F7: ldx lineCurCol ; 36F7 column the line walker has reached ldy lineCurRow ; 36F9 and its row jsr readMapCell ; 36FB read that map cell (N set = a unit stands there) bpl L_3724 ; 36FE bare terrain -> straight to the height test tay ; 3700 unit cell: Y = $80|index, which the base-$80 alias tables undo lda scratch20 ; 3701 scan mode bpl L_3721 ; 3703 spotting pass: units in the way never block the view lda unitTypeTableAlias,y ; 3705 attack pass: type of the unit standing in the line of fire ($F76C-$80) and #$07 ; 3708 strip the dead/dug-in bits cmp #$03 ; 370A type 3 = SPY beq L_3721 ; 370C a spy is too small to stop a shot lda unitFlagsTableAlias,y ; 370E flags of the blocking unit ($F708-$80) ldx scratch21 ; 3711 the unit that wants to fire eor unitFlagsTable,x ; 3713 compare the two flag bytes and #$20 ; 3716 bit 5 = side bne rejectScanCandidate ; 3718 an enemy unit in the way stops the shot lda scanUnitTerrainHeight ; 371A one of ours in the way: are we firing from high ground? bne L_3721 ; 371D yes -> shoot over his head beq rejectScanCandidate ; 371F on the flat a friendly unit blocks the shot (always taken) L_3721: lda unitTerrainUnderAlias,y ; 3721 terrain byte hidden under that unit ($FAF0-$80) L_3724: tax ; 3724 map byte -> table index lda D_077E,x ; 3725 -> terrain class of this cell tax ; 3728 checkLineOfSightStep wants the class in X cpx #$02 ; 3729 class 2 = forest beq rejectScanCandidate ; 372B woods in the way cut the line completely jsr checkLineOfSightStep ; 372D feed this cell's height into the ridge test bcs rejectScanCandidate ; 3730 a ridge cuts the line -> candidate not visible jmp losWalkNextCell ; 3732 keep walking toward the target ; ---------------------------------------------------------------------- ; Candidate rejected: $FF in its slot of the $0400 list tells every reader to ignore the entry. ; $373C is the tail of the per-candidate loop whose head is at $3623. ; ---------------------------------------------------------------------- rejectScanCandidate: ldx scratch18 ; 3735 index of the candidate lda #$FF ; 3737 $FF = 'no unit' sta checksumXorConstant,x ; 3739 strike it off the candidate list (also trainerPlaybook0200:play3Col12) nextScanCandidate: dec scratch18 ; 373C the list is walked backwards ldx scratch18 ; 373E reload the index bmi scanCandidatesDone ; 3740 index went negative -> every candidate has been tested jmp L_3623 ; 3742 test the next candidate scanCandidatesDone: rts ; 3745 back to the caller (the per-unit update loop or selectBoomerTarget) ; ---------------------------------------------------------------------- ; checkLineOfSightStep - Feeds the terrain class of one more cell into the running line-of-sight test. ; It compares the height of that cell with the height of the previous one: a rise is remembered in ; zp_1F, and a fall after a rise means a ridge stands between the two units and hides the target. ; Equal heights, or a fall while the line has never risen (simply looking downhill), leave the line ; clear. ; In: X = terrain class 0-7 of the next cell, zp_1E = height of the previous cell, zp_1F = $80 until ; the line has gone uphill ; Out: C = 1 blocked / C = 0 still clear, zp_1E = height of this cell, zp_1F = size of the rise; A and ; X clobbered ; Called from: scanVisibleEnemies ($36CE for the target's own cell, $372D for each cell in between) ; ---------------------------------------------------------------------- checkLineOfSightStep: lda terrainHeightTable,x ; 3746 height 0-4 of this terrain class tax ; 3749 keep it as the new 'previous height' sec ; 374A prepare the subtraction sbc scratch1E ; 374B difference to the height of the previous cell beq L_3755 ; 374D same level -> nothing changes, the line stays clear stx scratch1E ; 374F remember this cell's height for the next step bcc L_3757 ; 3751 borrow: the ground drops here sta scratch1F ; 3753 the ground rises: record the rise, which clears the $80 'no rise yet' marker L_3755: clc ; 3755 C = 0: the line of sight is still clear rts ; 3756 back to the walk L_3757: lda scratch1F ; 3757 the ground is dropping - had it risen before? bmi L_3755 ; 3759 still $80: we are only looking downhill, and that hides nothing sec ; 375B a drop after a rise = a ridge in between rts ; 375C C = 1: the target is hidden ; ---------------------------------------------------------------------- ; initLineBetweenUnits - Points the line walker at unit Y's cell and falls into initLineFromUnit, ; which sets the start to unit X's cell. Together they are the standard 'walk from unit X to unit Y' ; set-up. ; In: X = unit the line starts at, Y = unit the line ends at ; Out: zp_94/zp_95 = target row/column plus everything initLineFromUnit produces (A = direction code ; 0-3, line walker state zp_8C-zp_97) ; Called from: scanVisibleEnemies ($3633) ; ---------------------------------------------------------------------- initLineBetweenUnits: lda unitColTable,y ; 375D column of the far unit sta lineEndCol ; 3760 line walker target column lda unitRowTable,y ; 3762 row of the far unit sta lineEndRow ; 3765 line walker target row (falls into initLineFromUnit) ; ---------------------------------------------------------------------- ; initLineFromUnit - Sets the line walker start to unit X's cell, runs initLineStepper ($FBB8) and ; reports the principal direction of the line: 0 = north or 1 = south for a row-major line, 2 = east ; or 3 = west for a column-major one. That value is the unit facing code kept in bits 0-1 of $F708 ; (stored at $4464 and used by the combat facing bonus). ; In: X = unit the line starts at, zp_94/zp_95 = target row/column ; Out: A = direction code 0-3, line walker state zp_8C-zp_97 set up (X = 1 for a row-major line) ; Called from: initLineBetweenUnits (falls through), $428E where a unit is turned toward its ; destination ; ---------------------------------------------------------------------- initLineFromUnit: lda unitColTable,x ; 3767 column of the unit the line starts at sta lineCurCol ; 376A line walker start column lda unitRowTable,x ; 376C its row sta lineCurRow ; 376F line walker start row jsr initLineStepper ; 3771 set up the Bresenham walk; it returns X = 1 when the row is the major axis cpx #$01 ; 3774 was the line mostly vertical? bne L_3782 ; 3776 no, it is mostly horizontal -> report east or west lda #$00 ; 3778 facing 0 = north (up) bit lineStepRow ; 377A row step is $01 (down) or $FF (up) bmi L_378A ; 377C stepping up -> north lda #$01 ; 377E facing 1 = south (down) bne L_378A ; 3780 (always taken) L_3782: lda #$02 ; 3782 facing 2 = east (right) bit lineStepCol ; 3784 column step is $01 (right) or $FF (left) bpl L_378A ; 3786 stepping right -> east lda #$03 ; 3788 facing 3 = west (left) L_378A: rts ; 378A A = direction/facing code 0-3 ; ---------------------------------------------------------------------- ; setScanFilterOppositeSide - Configures the unit filter of collectUnitsInRadius so that only units of ; the side opposite to unit Y are collected, by writing the compare value and the branch opcode of the ; test at $37FE/$3800. A side 0 scanner (unit < 50) keeps the units >= 50 with a BCC, a side 1 ; scanner keeps the units < 50 with a BCS. ; In: Y = the reference unit whose enemies are wanted ; Out: scanSideFilterCompare+1 = $32 and scanSideFilterBranch = $90 (BCC) or $B0 (BCS); A clobbered ; Called from: scanVisibleEnemies ($35D9), checkEnemyRecyclerSpotted ($3F64) ; ---------------------------------------------------------------------- setScanFilterOppositeSide: lda #$32 ; 378B unit 50 = the first unit of side 1 sta scanSideFilterCompare+1 ; 378D patch the CMP operand of the scan filter lda #$90 ; 3790 $90 = BCC, i.e. skip everything below unit 50 cpy #$32 ; 3792 is the reference unit itself on side 0? bcc L_3798 ; 3794 yes -> keep the units of side 1 eor #$20 ; 3796 side 1 scanner: $90 EOR $20 = $B0 = BCS, keep the units of side 0 L_3798: sta scanSideFilterBranch ; 3798 patch the branch opcode of the scan filter rts ; 379B the filter is now set for the next collectUnitsInRadius ; Leftover of an older build: as code these two bytes are 'bcc $3798', the tail of an earlier version ; of the side test. Overlay A's per-side patcher still stores its branch opcode here ($7BDB), where ; it now has no effect at all. leftoverScanSideBranch: .byte $90,$FA ; 379C .. $90 $FA = the dead 'bcc $3798' of an earlier layout ; ---------------------------------------------------------------------- ; collectAllUnitsInRadiusDead - Unreferenced alternate entry of collectUnitsInRadius that turns the ; side filter into 'accept every unit' (compare against unit 0 with a BCC) before falling into the ; scan. Nothing calls it - ageUnitVisibility ($357D) writes the same two bytes inline - so this is ; leftover code from an earlier build. ; In: A = radius, X = centre column, Y = centre row ; Out: dead code; it would leave scanSideFilterCompare+1 = 0 and scanSideFilterBranch = $90 ; Called from: nothing (dead code, rendered as .byte in the listing) ; ---------------------------------------------------------------------- collectAllUnitsInRadiusDead: pha ; 379E keep the radius across the patching lda #$00 ; 379F compare the unit index against 0 ... sta scanSideFilterCompare+1 ; 37A1 -> the compare operand of the scan filter lda #$90 ; 37A4 ... with a BCC, so no unit is ever skipped sta scanSideFilterBranch ; 37A6 -> the branch opcode of the scan filter pla ; 37A9 radius back into A and fall into the scan ; ---------------------------------------------------------------------- ; collectUnitsInRadius - Scans the square of half-width A around map cell (X,Y), clipped to the 40x40 ; battlefield, and lists every cell holding a unit that passes the side filter patched by ; setScanFilterOppositeSide: the unit index goes into $0400[n] and its squared distance from the ; centre into $0432[n]. When more than one unit was found, side 1 reverses the lists so that both ; machines break distance ties identically. ; In: A = radius in cells, X = centre column, Y = centre row, playerSide $0B9F, the map at $F000 ; Out: candidate lists $0400/$0432, X = zp_29 = index of the last entry ($FF = nothing found); ; zp_22-zp_29 and zp_48/zp_49 used as scratch; operand scanReverseLimit+1 patched ; Called from: scanVisibleEnemies ($3617), checkEnemyRecyclerSpotted ($3F72) ; ---------------------------------------------------------------------- collectUnitsInRadius: sta cellColA ; 37AA keep the radius (zp_22 becomes the running column further down) asl a ; 37AC 2 * radius sta scanColCount ; 37AD width of the scan square (the +1 comes at $37DA) sta scanRowCount ; 37AF and its height stx cellColB ; 37B1 centre column = point B of distanceSquared sty cellRowB ; 37B3 centre row txa ; 37B5 centre column back into A sec ; 37B6 prepare the subtraction sbc cellColA ; 37B7 leftmost column = centre - radius bcs L_37C3 ; 37B9 still on the map -> use it txa ; 37BB clipped at the left edge: the square still ends at centre + radius ... clc ; 37BC prepare the addition adc cellColA ; 37BD centre + radius sta scanColCount ; 37BF ... so that is all the width there is left lda #$00 ; 37C1 and the scan starts at column 0 L_37C3: sta scratch23 ; 37C3 first column of every scanned row tax ; 37C5 -> X for readMapCell tya ; 37C6 centre row back into A sec ; 37C7 prepare the subtraction sbc cellColA ; 37C8 topmost row = centre - radius (zp_22 still holds the radius) bcs L_37D4 ; 37CA still on the map -> use it tya ; 37CC clipped at the top edge ... clc ; 37CD prepare the addition adc cellColA ; 37CE centre + radius sta scanRowCount ; 37D0 ... so there are fewer rows to scan lda #$00 ; 37D2 and the scan starts at row 0 L_37D4: sta cellRowA ; 37D4 row the scan starts on tay ; 37D6 -> Y for readMapCell jsr readMapCell ; 37D7 zp_48/49 = address of the top left cell of the square, Y = 0 inc scanColCount ; 37DA width = 2*radius+1 cells inc scanRowCount ; 37DC height = 2*radius+1 rows lda #$FF ; 37DE $FF ... sta scanListIndex ; 37E0 ... = the candidate list is still empty ; ---------------------------------------------------------------------- ; Row loop: one map row of the square per pass; zp_48/49 walks down the map 40 bytes at a time. ; ---------------------------------------------------------------------- L_37E2: lda cellRowA ; 37E2 row about to be scanned bmi L_3818 ; 37E4 negative row -> nothing to do here cmp #$28 ; 37E6 the battlefield is 40 rows deep bcs L_3818 ; 37E8 past the bottom edge -> skip the row ldy #$00 ; 37EA Y = offset from the start of the map row lda scratch23 ; 37EC first column of the square sta cellColA ; 37EE restart at the left edge of the square ; ---------------------------------------------------------------------- ; Column loop over one row of the square. ; ---------------------------------------------------------------------- L_37F0: lda cellColA ; 37F0 column about to be tested bmi L_3811 ; 37F2 off the left edge of the map cmp #$28 ; 37F4 40 columns bcs L_3811 ; 37F6 off the right edge lda (mapCellPtr),y ; 37F8 the map byte of this cell bpl L_3811 ; 37FA bit 7 clear = plain terrain, no unit here and #$7F ; 37FC bit 7 is the 'unit here' marker; the rest is the unit index 0-99 scanSideFilterCompare: cmp #$FF ; 37FE operand patched by setScanFilterOppositeSide ($32) or by the dead entry above (0) scanSideFilterBranch: bcc L_3811 ; 3800 opcode patched BCC/BCS: drop the units of the unwanted side inc scanListIndex ; 3802 one more candidate ldx scanListIndex ; 3804 its slot number sta checksumXorConstant,x ; 3806 unit index -> the $0400 candidate list (also trainerPlaybook0200:play3Col12) jsr distanceSquared ; 3809 dist^2 between this cell (zp_22/zp_24) and the centre (zp_27/zp_28) ldx scanListIndex ; 380C same slot sta play3Row12,x ; 380E -> the parallel $0432 distance list L_3811: inc cellColA ; 3811 next column iny ; 3813 next byte of the map row cpy scanColCount ; 3814 whole width of the square done? bcc L_37F0 ; 3816 no -> next cell ; ---------------------------------------------------------------------- ; End of a row: step the map pointer down to the next one. ; ---------------------------------------------------------------------- L_3818: lda mapCellPtr ; 3818 map row pointer, low byte clc ; 381A prepare the addition adc #$28 ; 381B 40 bytes per map row sta mapCellPtr ; 381D store it back lda mapCellPtrHi ; 381F high byte adc #$00 ; 3821 propagate the carry sta mapCellPtrHi ; 3823 zp_48/49 now points at the next row of the square inc cellRowA ; 3825 next row number dec scanRowCount ; 3827 one row less to do bne L_37E2 ; 3829 until the whole square has been scanned ; ---------------------------------------------------------------------- ; Side 1 reverses both lists. The two machines see the battlefield mirrored and therefore scan the ; square in opposite directions; reversing makes them pick the same unit whenever two candidates tie ; on distance. ; ---------------------------------------------------------------------- ldx scanListIndex ; 382B index of the last entry bmi L_385D ; 382D nothing found at all beq L_385D ; 382F only one entry - nothing to reverse lda playerSide ; 3831 0 = the local player is side 0, 1 = side 1 beq L_385D ; 3834 side 0 keeps the natural scan order ldy #$00 ; 3836 Y walks up from the front while X walks down from the back L_3838: lda checksumXorConstant,y ; 3838 swap the two unit slots ... (also trainerPlaybook0200:play3Col12) pha ; 383B keep the front entry lda checksumXorConstant,x ; 383C back entry (also trainerPlaybook0200:play3Col12) sta checksumXorConstant,y ; 383F -> to the front (also trainerPlaybook0200:play3Col12) pla ; 3842 front entry back off the stack sta checksumXorConstant,x ; 3843 -> to the back (also trainerPlaybook0200:play3Col12) lda play3Row12,y ; 3846 ... and the two matching distances pha ; 3849 keep the front distance lda play3Row12,x ; 384A back distance sta play3Row12,y ; 384D -> to the front pla ; 3850 front distance back off the stack sta play3Row12,x ; 3851 -> to the back iny ; 3854 next pair from the front dex ; 3855 next pair from the back stx scanReverseLimit+1 ; 3856 patch the loop limit with the index that is walking down scanReverseLimit: cpy #$FF ; 3859 operand = X bcc L_3838 ; 385B keep swapping until the two indexes meet L_385D: ldx scanListIndex ; 385D X = index of the last entry, $FF when nothing was found rts ; 385F back to the caller ; squaresTable - n*n for n = 0..15. distanceSquared uses it forwards; cellDistance in the opponent ; module ($E832) and the map generator ($7831) use it backwards as a square root. squaresTable: .byte $00,$01,$04,$09,$10,$19,$24,$31; 3860 ......$1 0,1,4,9,16,25,36,49 = n*n for n = 0-7 .byte $40 ; 3868 64,81,100,121,144,169,196,225 = n*n for n = 8-15 .byte $51,$64,$79,$90,$A9,$C4,$E1; 3869 Qdy.... ; ---------------------------------------------------------------------- ; distanceSquared - Squared distance between two map cells: |dCol|^2 + |dRow|^2 taken from ; squaresTable. Each delta is clamped to 15 cells and the sum saturates at $FF, which makes it a ; usable range metric everywhere on the 40x40 map without a square root. ; In: zp_22/zp_24 = column/row of the cell, zp_27/zp_28 = column/row of the reference point ; Out: A = squared distance ($FF when it overflows); X clobbered ; Called from: collectUnitsInRadius ($3809), the target cursor range check ($2159), cellDistance in ; the opponent module ($E82A) ; ---------------------------------------------------------------------- distanceSquared: lda cellColA ; 3870 column of the cell sec ; 3872 prepare the subtraction sbc cellColB ; 3873 minus the reference column bpl L_387C ; 3875 already positive eor #$FF ; 3877 negate to get |dCol| ... clc ; 3879 prepare the addition adc #$01 ; 387A ... two's complement L_387C: tax ; 387C -> table index cpx #$10 ; 387D squaresTable only covers 0-15 bcc L_3883 ; 387F in range ldx #$0F ; 3881 clamp the distance at 15 cells L_3883: lda squaresTable,x ; 3883 dCol^2 pha ; 3886 hold on to it while the row delta is worked out lda cellRowA ; 3887 row of the cell sec ; 3889 prepare the subtraction sbc cellRowB ; 388A minus the reference row bpl L_3893 ; 388C already positive eor #$FF ; 388E negate to get |dRow| ... clc ; 3890 prepare the addition adc #$01 ; 3891 ... two's complement L_3893: tax ; 3893 -> table index cpx #$10 ; 3894 again only 0-15 are tabulated bcc L_389A ; 3896 in range ldx #$0F ; 3898 clamp at 15 cells L_389A: pla ; 389A dCol^2 back off the stack clc ; 389B prepare the addition adc squaresTable,x ; 389C + dRow^2 bcc L_38A3 ; 389F no overflow lda #$FF ; 38A1 saturate at $FF instead of wrapping round L_38A3: rts ; 38A3 A = dCol^2 + dRow^2 ; baseDamageTable - base damage of one shot: 3 rows of 5 bytes, row = attacker type (0 GRUNT, 1 RIDER, ; 2 BOOMER; spies and comcens never fire) and column = target type (0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, ; 4 COMCEN). Read as baseDamageTable[attackerRow*5 + targetType] at $38F4. baseDamageTable: .byte $04,$05,$19,$08,$0C,$03,$04,$14; 38A4 ........ GRUNT vs grunt 4, rider 5, boomer 25, spy 8, comcen 12; RIDER vs grunt 3, rider 4, boomer 20 .byte $06 ; 38AC RIDER vs spy 6, comcen 8; BOOMER vs grunt 12, rider 16, boomer 8, spy 8, comcen 20 .byte $08,$0C,$10,$08,$08,$14 ; 38AD ...... ; terrainHeightTable - height (cover level) of each terrain class 0-7: class 0 blank, 1 river and 2 ; forest are flat, 3 lower contour = 1, 4 hill = 2, 5 upper contour = 3, 6 summit = 4, 7 recycler = 0. ; Used by the line-of-sight ridge test and by the combat height bonus. terrainHeightTable: .byte $00,$00,$00,$01,$02,$03,$04,$00; 38B3 ........ heights of terrain classes 0-7: 0,0,0,1,2,3,4,0 ; combatScratch - the seven work bytes of resolveAttack, held inside the code: target unit, attacker ; unit, the running damage total, and the four map coordinates of the shot. The coordinates have to ; be saved because overlay B's drawShotLineInViewport destroys zp_92-zp_95. combatScratch: .byte $00 ; 38BB . the unit being shot at combatAttackerUnit: .byte $00 ; 38BC the unit doing the shooting combatDamage: .byte $00 ; 38BD running damage total; half of it is what actually gets applied combatAttackerRow: .byte $00 ; 38BE attacker's map row combatAttackerCol: .byte $00 ; 38BF attacker's map column combatTargetRow: .byte $00 ; 38C0 target's map row combatTargetCol: .byte $00 ; 38C1 target's map column ; ---------------------------------------------------------------------- ; resolveAttack - Resolves one shot of unit X at unit Y. Only GRUNT/RIDER/BOOMER (types 0-2) carry a ; weapon. The damage starts at baseDamageTable[attacker][target] and is then modified: a blitzing ; attacker that has built up a charge ($F708 bit 7) throws up to 15 energy (5 for a BOOMER) into the ; shot and pays 3/4 of that itself; +2 when the attacker is dug in and -25% when the target is; plus ; the height difference of the two cells; +10 when the target stands in a river (class 1) and +2 on a ; contour (class 3 or 5); and for non-BOOMERs +20 when the target faces the same way (shot in the ; back) or +10 for a flank shot. A zero or negative total becomes 2, and half of the total is applied ; through applyHitToUnit. If the battlefield screen is up and the comcen display is alive the shot is ; drawn both in the zoom window and on the overview map, then the shot or kill sound plays. ; In: X = attacker unit, Y = target unit; the unit record arrays, terrain classes $077E, ; currentScreen $90FB, comcenStunStatus $922A ; Out: target damaged via applyHitToUnit, the attacker may lose energy, attackerType $923D, ; $38BB-$38C1 scratch, shot graphics (sprite 3 and the zoom window) and a sound; the operands ; loadShotSoundId+1, loadChargeEnergySpent+1 and subtractDugInDiscount+1 are patched ; Called from: the per-unit update loop ($44A0) ; ---------------------------------------------------------------------- resolveAttack: stx combatAttackerUnit ; 38C2 remember the attacker sty combatScratch ; 38C5 and the target lda #$00 ; 38C8 no offset yet ... sta scratch1A ; 38CA row offset into baseDamageTable (5 entries per attacker type) lda unitTypeTable,x ; 38CC attacker's type byte and #$07 ; 38CF 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN sta attackerType ; 38D1 $923D, also read by both shot-drawing routines cmp #$03 ; 38D4 types 3 and 4 have no weapon bcc L_38DB ; 38D6 only grunts, riders and boomers shoot jmp L_3A59 ; 38D8 a spy or comcen never fires - nothing happens at all ; ---------------------------------------------------------------------- ; Pick the row of baseDamageTable that belongs to the attacker's type. ; ---------------------------------------------------------------------- L_38DB: cmp #$02 ; 38DB BOOMER? bne L_38E3 ; 38DD no lda #$0A ; 38DF boomer row = table entries 10-14 sta scratch1A ; 38E1 -> row offset 10 L_38E3: cmp #$01 ; 38E3 RIDER? bne L_38EB ; 38E5 no, so it is a GRUNT and the offset stays 0 lda #$05 ; 38E7 rider row = table entries 5-9 sta scratch1A ; 38E9 -> row offset 5 L_38EB: lda unitTypeTable,y ; 38EB target's type byte and #$07 ; 38EE type 0-4 selects the column clc ; 38F0 prepare the addition adc scratch1A ; 38F1 + the attacker's row offset tax ; 38F3 -> table index lda baseDamageTable,x ; 38F4 base damage for this attacker/target pairing sta combatDamage ; 38F7 start the running damage total lda #$0A ; 38FA sound 10 = ordinary shot ldx attackerType ; 38FC attacker type cpx #$02 ; 38FF BOOMER? bne L_3905 ; 3901 no -> keep sound 10 lda #$0B ; 3903 sound 11 = boomer shell L_3905: sta loadShotSoundId+1 ; 3905 patch the LDA # of the sound played when the shot is over ; ---------------------------------------------------------------------- ; Blitz charge: a unit with blitz switched on ($F8FC bit 7) collects a one-shot charge flag in ; $F708 bit 7 while it moves ($3EE2). Firing spends the charge, adding energy to the damage and ; taking 3/4 of that energy off the attacker itself. ; ---------------------------------------------------------------------- ldx combatAttackerUnit ; 3908 the attacker lda unitPathRowTable,x ; 390B $F8FC bit 7 = blitz is switched on bpl L_3950 ; 390E not blitzing -> no charge bonus lda unitFlagsTable,x ; 3910 $F708 bit 7 = a charge was built up by moving bpl L_3950 ; 3913 nothing charged up and #$7F ; 3915 clear the charge bit ... sta unitFlagsTable,x ; 3917 ... it is used up by this shot lda #$0F ; 391A up to 15 energy may be thrown into the charge ldy scratch1A ; 391C which damage row is in use? cpy #$0A ; 391E row offset 10 = BOOMER bcc L_3924 ; 3920 grunt or rider lda #$05 ; 3922 a boomer only spends 5 L_3924: sta scratch18 ; 3924 the amount we would like to spend lda unitEnergyTable,x ; 3926 attacker's energy byte and #$3F ; 3929 bits 0-5 = energy 0-50, bits 6-7 are the visibility flags cmp scratch18 ; 392B enough energy for the whole charge? bcc L_3931 ; 392D no - it cannot spend more than it has lda scratch18 ; 392F yes - spend the full amount L_3931: sta loadChargeEnergySpent+1 ; 3931 patch the amount spent into the LDA # below clc ; 3934 prepare the addition adc combatDamage ; 3935 the running damage total sta combatDamage ; 3938 the whole amount is added to the damage ldy combatAttackerUnit ; 393B the attacker itself pays for it loadChargeEnergySpent: lda #$FF ; 393E operand = the energy just spent lsr a ; 3940 /2 sta scratch18 ; 3941 keep half of it lsr a ; 3943 /4 clc ; 3944 prepare the addition adc scratch18 ; 3945 1/2 + 1/4 = 3/4 of the amount spent jsr reduceUnitEnergyBy ; 3947 take that off the attacker's own energy ldy combatScratch ; 394A Y back to the target ldx combatAttackerUnit ; 394D X back to the attacker ; ---------------------------------------------------------------------- ; Dug-in modifiers: $F76C bit 6 marks a unit that has finished digging in. ; ---------------------------------------------------------------------- L_3950: lda unitTypeTable,x ; 3950 attacker's type byte and #$40 ; 3953 bit 6 = dug in / fortified beq L_395D ; 3955 not dug in inc combatDamage ; 3957 +1 ... inc combatDamage ; 395A ... +1 = +2 damage when firing from a prepared position L_395D: lda unitTypeTable,y ; 395D target's type byte and #$40 ; 3960 bit 6 = the target is dug in beq L_3975 ; 3962 not dug in lda combatDamage ; 3964 running damage total lsr a ; 3967 /2 lsr a ; 3968 /4 sta subtractDugInDiscount+1 ; 3969 patch the subtraction below with damage/4 lda combatDamage ; 396C the total again sec ; 396F prepare the subtraction subtractDugInDiscount: sbc #$FF ; 3970 operand = damage/4: a dug-in target takes 25% less sta combatDamage ; 3972 the reduced total ; ---------------------------------------------------------------------- ; Terrain modifiers: the height difference between the two cells, plus a bonus for a target caught in ; the open (a river) or on a slope. ; ---------------------------------------------------------------------- L_3975: ldx unitTerrainUnderTable,y ; 3975 map byte hidden under the target lda D_077E,x ; 3978 -> terrain class 0-8 sta scratch1C ; 397B keep the target's terrain class tax ; 397D -> height table index lda terrainHeightTable,x ; 397E height of the target's cell sta scratch1E ; 3981 keep it for the difference below ldx combatAttackerUnit ; 3983 the attacker ldy unitTerrainUnderTable,x ; 3986 map byte hidden under the attacker lda D_077E,y ; 3989 -> terrain class sta scratch1D ; 398C stored but never read again - a dead store tay ; 398E -> height table index lda terrainHeightTable,y ; 398F height of the attacker's cell ldy combatScratch ; 3992 Y back to the target unit sec ; 3995 prepare the subtraction sbc scratch1E ; 3996 attacker height - target height clc ; 3998 prepare the addition adc combatDamage ; 3999 + the running total sta combatDamage ; 399C firing downhill adds, firing uphill subtracts lda scratch1C ; 399F terrain class the target is standing on cmp #$01 ; 39A1 class 1 = river bne L_39B1 ; 39A3 not a river lda combatDamage ; 39A5 running damage total clc ; 39A8 prepare the addition adc #$0A ; 39A9 +10: a target in the water has no cover at all sta combatDamage ; 39AB store the total again jmp L_39BF ; 39AE the river bonus and the slope bonus are exclusive L_39B1: cmp #$03 ; 39B1 class 3 = lower contour beq L_39B9 ; 39B3 yes cmp #$05 ; 39B5 class 5 = upper contour bne L_39BF ; 39B7 any other class gives no bonus L_39B9: inc combatDamage ; 39B9 +1 ... inc combatDamage ; 39BC ... +1 = +2 for a target caught on an open slope ; ---------------------------------------------------------------------- ; Facing bonus. Facing codes are 0 north, 1 south, 2 east, 3 west, so EOR 1 gives the opposite ; direction: the same facing means the target has its back to us, the opposite facing means it is ; looking straight at us, and the other two are a flank shot. Boomers lob shells and get no bonus at ; all. ; ---------------------------------------------------------------------- L_39BF: lda attackerType ; 39BF attacker type cmp #$02 ; 39C2 BOOMER? beq L_39F0 ; 39C4 artillery gets no facing bonus lda unitFlagsTable,x ; 39C6 attacker's facing, bits 0-1 of $F708 and #$03 ; 39C9 facing bits only sta scratch1A ; 39CB keep it lda unitFlagsTable,y ; 39CD target's facing and #$03 ; 39D0 facing bits only cmp scratch1A ; 39D2 both looking the same way? bne L_39E1 ; 39D4 no lda combatDamage ; 39D6 running damage total clc ; 39D9 prepare the addition adc #$14 ; 39DA +20: the target has its back to the shooter sta combatDamage ; 39DC store the total again bne L_39F0 ; 39DF (always taken, the total cannot be 0 here) L_39E1: eor #$01 ; 39E1 the facing opposite to the target's cmp scratch1A ; 39E3 is the target looking straight at the shooter? beq L_39F0 ; 39E5 head on -> no bonus lda combatDamage ; 39E7 the remaining two facings are a flank shot clc ; 39EA prepare the addition adc #$0A ; 39EB +10 sta combatDamage ; 39ED store the total again ; ---------------------------------------------------------------------- ; Apply the shot. ; ---------------------------------------------------------------------- L_39F0: lda combatDamage ; 39F0 final total bmi L_39F7 ; 39F3 the modifiers can drive it below zero bne L_39F9 ; 39F5 or exactly to zero L_39F7: lda #$02 ; 39F7 every hit does at least 2 points (before halving) L_39F9: ldy combatScratch ; 39F9 the target unit lsr a ; 39FC only half of the accumulated total is really applied jsr applyHitToUnit ; 39FD damage the target, score it and maybe end the game ; ---------------------------------------------------------------------- ; Draw the shot, but only while the player is actually looking at the battlefield and the comcen ; display is alive. Overlay B draws it as fat pixels in the 7x5 zoom window and drawShotLineSprite ; draws the same shot on the 4-pixel-per-cell overview map. ; ---------------------------------------------------------------------- lda currentScreen ; 3A00 0 = the battlefield screen (F1) is the one on show bne L_3A4A ; 3A03 radar, drone or menu screen -> no shot graphics bit comcenStunStatus ; 3A05 $922A = $FF (so bit 6 is set) while the comcen is knocked out bvs L_3A4A ; 3A08 blacked-out display -> draw nothing ldy combatScratch ; 3A0A the target lda unitColTable,y ; 3A0D its column sta combatTargetCol ; 3A10 saved, because the overlay routine destroys zp_92-zp_95 sta lineEndCol ; 3A13 line walker target column lda unitRowTable,y ; 3A15 its row sta combatTargetRow ; 3A18 saved copy sta lineEndRow ; 3A1B line walker target row ldx combatAttackerUnit ; 3A1D the attacker lda unitColTable,x ; 3A20 its column sta combatAttackerCol ; 3A23 saved copy sta lineCurCol ; 3A26 line walker start column lda unitRowTable,x ; 3A28 its row sta combatAttackerRow ; 3A2B saved copy sta lineCurRow ; 3A2E line walker start row jsr drawShotLineInViewport ; 3A30 overlay B: white fat-pixel shot line in the 7x5 zoom window (also mapGenerator6F00:runMapTypeMenu) lda combatTargetRow ; 3A33 put the four endpoints back for the sprite version sta lineEndRow ; 3A36 target row lda combatTargetCol ; 3A38 saved target column sta lineEndCol ; 3A3B target column lda combatAttackerRow ; 3A3D saved attacker row sta lineCurRow ; 3A40 attacker row lda combatAttackerCol ; 3A42 saved attacker column sta lineCurCol ; 3A45 attacker column jsr drawShotLineSprite ; 3A47 and draw the same shot on the small overview map ; ---------------------------------------------------------------------- ; Sound: a kill overrides the shot sound patched in at $3905. ; ---------------------------------------------------------------------- L_3A4A: ldy combatScratch ; 3A4A the target loadShotSoundId: lda #$FF ; 3A4D operand = sound 10 (shot) or 11 (boomer shell), patched at $3905 ldx unitTypeTable,y ; 3A4F bit 7 of the type byte = the target was destroyed bpl L_3A56 ; 3A52 still alive -> ordinary shot sound lda #$09 ; 3A54 sound 9 = kill L_3A56: jsr playSound ; 3A56 queue it L_3A59: rts ; 3A59 shared exit, also used by the 'spy/comcen cannot fire' case ; ---------------------------------------------------------------------- ; drawShotLineSprite - Draws the shot on the 4-pixel-per-cell overview map using hardware sprite 3. ; Both cells are converted to sprite pixel coordinates; because a sprite only covers 24x21 pixels the ; start point is walked toward the target until the rest of the line fits that box. Sprite 3 is then ; placed on the top left corner of the box (+2 pixels so the line sits in the middle of the 4x4 cell) ; in white, the segment is plotted into the sprite shape (dotted for a BOOMER) with a small diamond at ; the impact end, and shotLineTimer gives the IRQ 8 frames before it hides the sprite again. ; In: zp_92/zp_93 = attacker row/column, zp_94/zp_95 = target row/column (map cells), attackerType ; $923D ; Out: sprite 3 shape at $8AC0, position $9013/$901B, colour $900B = 1 (white), bit 3 of the sprite ; enable shadow $9022, shotLineTimer $91C8 = 8; zp_48 and the whole line walker state clobbered ; Called from: resolveAttack ($3A47) ; Hide sprite 3 and wipe its shape before rebuilding it. ; ---------------------------------------------------------------------- drawShotLineSprite: lda spriteEnableShadow ; 3A5A shadow of VIC_SPR_ENA $D015 and #$F7 ; 3A5D clear bit 3: sprite 3 off while its shape is rebuilt sta spriteEnableShadow ; 3A5F write the shadow back ldy #$3F ; 3A62 64 bytes of sprite data lda #$00 ; 3A64 blank byte L_3A66: sta sprite3Shape,y ; 3A66 clear sprite 3's shape at $8AC0 dey ; 3A69 next byte bpl L_3A66 ; 3A6A until all 64 are cleared ; ---------------------------------------------------------------------- ; Convert both map cells into sprite pixel coordinates (column*4+$1F, row*4+$39). ; ---------------------------------------------------------------------- ldx lineEndCol ; 3A6C target column ldy lineEndRow ; 3A6E target row jsr mapCellToSpriteXYRaw ; 3A70 -> sprite pixel position on the overview map stx lineEndCol ; 3A73 from here on the line walker works in pixels, not cells sty lineEndRow ; 3A75 in sprite pixels as well ldx lineCurCol ; 3A77 attacker column ldy lineCurRow ; 3A79 attacker row jsr mapCellToSpriteXYRaw ; 3A7B -> sprite pixel position stx lineCurCol ; 3A7E line walker start, in pixels sty lineCurRow ; 3A80 in sprite pixels as well jsr initLineStepper ; 3A82 set up the Bresenham walk over the pixel line lda #$00 ; 3A85 step counter ... sta mapCellPtr ; 3A87 counts the pixels dropped off the far end - nothing ever reads it (leftover) ; ---------------------------------------------------------------------- ; A sprite is only 24x21 pixels, so walk the start point toward the target until what is left of the ; line fits inside one sprite; the tail nearest the shooter simply is not drawn. ; ---------------------------------------------------------------------- L_3A89: lda lineEndRow ; 3A89 how tall is the remaining segment? sec ; 3A8B prepare the subtraction sbc lineCurRow ; 3A8C end row - current row bcs L_3A94 ; 3A8E positive already eor #$FF ; 3A90 |dy| ... adc #$01 ; 3A92 ... two's complement L_3A94: cmp #$14 ; 3A94 a sprite is 21 pixel rows tall bcs L_3AA7 ; 3A96 still too tall -> drop one more step lda lineEndCol ; 3A98 how wide is it? sec ; 3A9A prepare the subtraction sbc lineCurCol ; 3A9B end column - current column bcs L_3AA3 ; 3A9D positive already eor #$FF ; 3A9F |dx| ... adc #$01 ; 3AA1 ... two's complement L_3AA3: cmp #$17 ; 3AA3 a sprite is 24 pixel columns wide bcc L_3AB1 ; 3AA5 the rest of the line fits -> draw it L_3AA7: jsr stepLine ; 3AA7 walk one pixel closer to the target beq L_3AB1 ; 3AAA the whole line has been consumed inc mapCellPtr ; 3AAC count the dropped step (dead store) jmp L_3A89 ; 3AAE test the shortened line again ; ---------------------------------------------------------------------- ; Put sprite 3 on the top left corner of the remaining segment. ; ---------------------------------------------------------------------- L_3AB1: ldx lineCurCol ; 3AB1 assume the current point is the left edge lda lineStepCol ; 3AB3 which way does the walk run? bpl L_3AB9 ; 3AB5 to the right -> the current point really is leftmost ldx lineEndCol ; 3AB7 to the left -> the target end is leftmost L_3AB9: ldy lineCurRow ; 3AB9 same for the top edge lda lineStepRow ; 3ABB which way vertically? bpl L_3AC1 ; 3ABD downwards -> the current point is topmost ldy lineEndRow ; 3ABF upwards -> the target end is topmost L_3AC1: cpx lineEndCol ; 3AC1 a perfectly vertical line? bne L_3AC6 ; 3AC3 no dex ; 3AC5 shift the sprite one pixel left so the line is not on its edge column L_3AC6: cpy lineEndRow ; 3AC6 a perfectly horizontal line? bne L_3ACB ; 3AC8 no dey ; 3ACA shift it one pixel up L_3ACB: stx sprite3XShadow ; 3ACB $9013 = shadow of sprite 3's X position sty sprite3YShadow ; 3ACE $901B = shadow of sprite 3's Y position ; ---------------------------------------------------------------------- ; Make both endpoints relative to the sprite origin, then centre the sprite in the map cell. ; ---------------------------------------------------------------------- lda lineCurCol ; 3AD1 line start column sec ; 3AD3 prepare the subtraction sbc sprite3XShadow ; 3AD4 minus the sprite origin sta lineCurCol ; 3AD7 start column inside the sprite (0-23) lda lineEndCol ; 3AD9 line end column sec ; 3ADB prepare the subtraction sbc sprite3XShadow ; 3ADC minus the sprite origin sta lineEndCol ; 3ADF end column inside the sprite lda lineCurRow ; 3AE1 line start row sec ; 3AE3 prepare the subtraction sbc sprite3YShadow ; 3AE4 minus the sprite origin sta lineCurRow ; 3AE7 start row inside the sprite (0-20) lda lineEndRow ; 3AE9 line end row sec ; 3AEB prepare the subtraction sbc sprite3YShadow ; 3AEC minus the sprite origin sta lineEndRow ; 3AEF end row inside the sprite lda sprite3XShadow ; 3AF1 sprite X position clc ; 3AF4 prepare the addition adc #$02 ; 3AF5 +2 pixels: a map cell is 4x4 pixels, so this centres the line inside it sta sprite3XShadow ; 3AF7 store the sprite X position lda sprite3YShadow ; 3AFA sprite Y position clc ; 3AFD prepare the addition adc #$02 ; 3AFE same +2 vertically sta sprite3YShadow ; 3B00 store the sprite Y position ; ---------------------------------------------------------------------- ; Plot the segment into the sprite shape and show it for 8 frames. ; ---------------------------------------------------------------------- lda #$01 ; 3B03 colour 1 = white sta sprite3ColourShadow ; 3B05 $900B = shadow of sprite 3's colour register $D02A lda attackerType ; 3B08 attacker type cmp #$02 ; 3B0B BOOMER? bne L_3B11 ; 3B0D no -> solid line lda #$80 ; 3B0F bit 7 = draw the line dotted (a lobbed shell) L_3B11: jsr drawLineInPathSprite ; 3B11 plot the segment into sprite 3's shape jsr plotPathCross ; 3B14 small diamond at the impact end lda #$08 ; 3B17 sprite 3 jsr enableSprites ; 3B19 switch it on lda #$08 ; 3B1C 8 frames sta shotLineTimer ; 3B1E the IRQ hides the sprite again when this runs out rts ; 3B21 back to resolveAttack ; ---------------------------------------------------------------------- ; applyHitToUnit - Takes A points of damage off unit Y and deals with everything that follows. For ; one of the local player's own units it warns when a spy is shot at for the first time (message $0E) ; and shakes the screen with sound 8 when the player's own comcen is hit. Either comcen halves the ; damage, caps it at the energy it has left, has one of its systems knocked out at random when it is ; ours, and scores 2 points per energy point for the attacking side. Then the energy is reduced and ; the hit recorded; a unit that runs out of energy is marked destroyed, while a comcen instead ends ; the game through endGame. ; In: A = damage points, Y = target unit ; Out: hitDamage $9240 = the damage actually applied; the target's energy $F9C4, movement points ; $FA28, hit effect $FA8C, display flags $FB54 and type byte $F76C; scores $92A2-$92A5 and $9208; ; screenShakeTimer $92C8; gameEndReason $0BA8; sounds, messages and a redraw of the cell ; Called from: resolveAttack ($39FD), cmdMissileStrike ($5413), the drone blast in overlay B ($7312); ; the alternate entry applyHitToUnitNoAlert ($3B52) from destroyUnit ($5465) ; Reactions that only happen for the local player's own units. The branch opcode at $3B2C is patched ; per side by overlay A ($7BEC), so 'below unit 50' means 'mine' for side 0 and 'the opponent's' for ; side 1. ; ---------------------------------------------------------------------- applyHitToUnit: sta hitDamage ; 3B22 $9240 = the damage points to apply lda unitTypeTable,y ; 3B25 target's type byte and #$07 ; 3B28 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN cpy #$32 ; 3B2A unit 50 = the first unit of side 1 D_3B2C: bcs L_3B5A ; 3B2C opcode patched per side ($7BEC): the opponent's units skip the alerts below cmp #$03 ; 3B2E one of ours - is it a SPY? bne L_3B42 ; 3B30 no lda unitEnergyTable,y ; 3B32 its energy byte and #$3F ; 3B35 bits 0-5 = energy 0-50 cmp #$32 ; 3B37 still untouched at 50? bne L_3B40 ; 3B39 already damaged -> warn only on the very first hit lda #$0E ; 3B3B message $0E 'SPY UNDER ATTACK!' jsr queueMessageUnlessFilm ; 3B3D queue it (dropped during solo/film playback) L_3B40: lda #$03 ; 3B40 restore the type value the message call clobbered L_3B42: cmp #$04 ; 3B42 is this our own COMCEN being hit? bne L_3B5A ; 3B44 no -> straight on lda #$08 ; 3B46 8 frames sta screenShakeTimer ; 3B48 $92C8: shake the screen while the comcen is being hit jsr playSound ; 3B4B sound 8 = own comcen hit lda #$04 ; 3B4E restore the type value bne L_3B5A ; 3B50 (always taken) ; ---------------------------------------------------------------------- ; applyHitToUnitNoAlert - Alternate entry of applyHitToUnit that skips the block above, i.e. the spy ; warning, the screen shake and the comcen hit sound that only fire for the local player's own units. ; Everything else - the comcen half-damage and scoring, the energy loss and the destruction handling - ; is identical. ; In: A = damage points, Y = target unit ; Out: as applyHitToUnit ; Called from: destroyUnit ($5465) with A = $34, i.e. more than any unit's 50 energy, so the SELF ; DESTRUCT command always kills ; ---------------------------------------------------------------------- applyHitToUnitNoAlert: sta hitDamage ; 3B52 alternate entry used by SELF DESTRUCT ($5465): damage without the own-unit alerts lda unitTypeTable,y ; 3B55 target's type byte and #$07 ; 3B58 type 0-4 ; ---------------------------------------------------------------------- ; A hit on either comcen: half damage, capped at what is left, and the only thing in the game that ; scores points. The score table is indexed by the damaged side - slot X holds the points scored ; against side X, which is the other side's running total (see $2965). ; ---------------------------------------------------------------------- L_3B5A: cmp #$04 ; 3B5A type 4 = COMCEN bne L_3B93 ; 3B5C an ordinary unit -> straight to the damage cpy comcenUnit ; 3B5E $9236 = the local player's own comcen (unit 49 or 99) bne L_3B69 ; 3B61 the opponent's comcen -> no system damage rolled here jsr rollComcenSystemDamage ; 3B63 knock out one of our comcen's systems at random ldy comcenUnit ; 3B66 the call clobbers Y L_3B69: lsr hitDamage ; 3B69 a comcen only ever takes half damage lda unitEnergyTable,y ; 3B6C its energy byte and #$3F ; 3B6F energy it has left cmp hitDamage ; 3B71 more damage than energy? bcs L_3B79 ; 3B74 no sta hitDamage ; 3B76 never score more than the comcen still has L_3B79: ldx #$00 ; 3B79 X = the side that was hit cpy #$32 ; 3B7B unit 50 = first unit of side 1 bcc L_3B80 ; 3B7D side 0 inx ; 3B7F units 50-99 belong to side 1 L_3B80: inc lastShownClock ; 3B80 make the clock/score line redraw itself ($C519 compares this with the game clock) lda hitDamage ; 3B83 the damage actually applied asl a ; 3B86 2 points per energy point taken off a comcen clc ; 3B87 prepare the addition adc sideScoreLo,x ; 3B88 slot X = points scored against side X = the attacking side's total sta sideScoreLo,x ; 3B8B 16-bit score, low byte bcc L_3B93 ; 3B8E no carry inc sideScoreHi,x ; 3B90 carry into the high byte of the 16-bit score ; ---------------------------------------------------------------------- ; Take the energy off and record the hit. ; ---------------------------------------------------------------------- L_3B93: lda hitDamage ; 3B93 (reloading A is redundant - reduceUnitEnergy reads $9240 itself) jsr reduceUnitEnergy ; 3B96 energy = energy - damage, with a floor of 0 lda hitDamage ; 3B99 the damage again jsr recordUnitHit ; 3B9C flash the unit, pin it down and flag a status refresh lda unitEnergyTable,y ; 3B9F its energy byte and #$3F ; 3BA2 any energy left? bne L_3BDB ; 3BA4 still alive -> just redraw it ; ---------------------------------------------------------------------- ; The unit ran out of energy. An ordinary unit is marked destroyed and gets the death flash; a comcen ; never carries the dead bit - losing it ends the game instead. ; ---------------------------------------------------------------------- lda unitTypeTable,y ; 3BA6 type byte ora #$80 ; 3BA9 bit 7 = destroyed sta unitTypeTable,y ; 3BAB the unit is dead and #$07 ; 3BAE type 0-4 cmp #$04 ; 3BB0 was it a COMCEN? bne L_3BD6 ; 3BB2 an ordinary unit -> death flash lda unitTypeTable,y ; 3BB4 type byte again and #$7F ; 3BB7 a comcen never carries the dead bit ... sta unitTypeTable,y ; 3BB9 ... it stays on the map lda unitFlagsTable,y ; 3BBC flags of the destroyed comcen and #$20 ; 3BBF bit 5 = its side clc ; 3BC1 prepare the addition adc #$20 ; 3BC2 $20 = side 0 knocked out, $40 = side 1 knocked out ora #$80 ; 3BC4 bit 7 = the game ended by a knockout ora gameEndReason ; 3BC6 keep any reason that was already set sta gameEndReason ; 3BC9 $0BA8 = the game end reason ldx #$01 ; 3BCC assume the opponent's comcen fell cpy #$32 ; 3BCE unit 50 = first unit of side 1 D_3BD0: bcs L_3BD3 ; 3BD0 opcode patched per side ($7BEF) dex ; 3BD2 X = 0 when it was our own comcen that fell L_3BD3: jmp endGame ; 3BD3 halt everything, reveal the units and show the result L_3BD6: lda #$85 ; 3BD6 $80 = a hit effect is pending, 5 = the 'destroyed' flash sta unitHitFlashTable,y ; 3BD8 $FA8C = hit effect of the unit L_3BDB: jsr drawUnitAtPosition ; 3BDB redraw the unit's cell on both map panels rts ; 3BDE back to the caller ; ---------------------------------------------------------------------- ; reduceUnitEnergyBy - Stores A as the damage in hitDamage $9240 and falls into reduceUnitEnergy. ; In: A = energy points to take off, Y = unit ; Out: see reduceUnitEnergy ; Called from: resolveAttack ($3947, the blitz charge the attacker pays for) and $3EF8 (1 point per ; blitz step) ; ---------------------------------------------------------------------- reduceUnitEnergyBy: sta hitDamage ; 3BDF $9240 = the energy points to take off ; ---------------------------------------------------------------------- ; reduceUnitEnergy - Subtracts hitDamage $9240 from the energy of unit Y, keeping the two visibility ; flags in bits 6-7 and stopping at 0. ; In: Y = unit, hitDamage $9240 ; Out: $F9C4[Y] updated; the ORA operand orVisibilityBits+1 is patched with the saved flags ; Called from: applyHitToUnit ($3B96); reduceUnitEnergyBy falls into it ; ---------------------------------------------------------------------- reduceUnitEnergy: lda unitEnergyTable,y ; 3BE2 energy byte: bits 0-5 = 0-50, bits 6-7 = the spotted/visible flags pha ; 3BE5 keep the whole byte for a moment and #$C0 ; 3BE6 isolate the two visibility flags sta orVisibilityBits+1 ; 3BE8 patch them into the ORA below pla ; 3BEB the byte back off the stack and #$3F ; 3BEC energy value only sec ; 3BEE prepare the subtraction sbc hitDamage ; 3BEF minus the damage bcs orVisibilityBits ; 3BF2 no borrow -> the result is still positive lda #$00 ; 3BF4 energy never goes below 0 orVisibilityBits: ora #$FF ; 3BF6 operand = the two visibility bits saved at $3BE6 sta unitEnergyTable,y ; 3BF8 energy and visibility flags back into the array rts ; 3BFB back to the caller ; ---------------------------------------------------------------------- ; recordUnitHit - Records the visible effect of a hit on unit Y: the damage, capped at 20, is stored ; as $80|damage in the hit-effect byte $FA8C that drives the flash and the PINNED status word, the ; same number of movement points is taken off the signed counter $FA28 (clamped at -127), and bit 1 of ; the display flags asks for the console entry to be refreshed. ; In: A = damage points, Y = unit ; Out: $FA8C[Y] = $80|damage, $FA28[Y] reduced, $FB54[Y] bit 1 set; the SBC operand ; subtractPinnedDamage+1 is patched ; Called from: applyHitToUnit ($3B9C) ; ---------------------------------------------------------------------- recordUnitHit: cmp #$14 ; 3BFC more than 20 points? bcc L_3C02 ; 3BFE no lda #$14 ; 3C00 the effect saturates at 20 points L_3C02: sta subtractPinnedDamage+1 ; 3C02 patch the subtraction below ora #$80 ; 3C05 bit 7 = a hit effect is pending on this unit sta unitHitFlashTable,y ; 3C07 $FA8C drives the hit flash and the PINNED status word lda unitMovePointsTable,y ; 3C0A $FA28 = signed movement points sec ; 3C0D prepare the subtraction subtractPinnedDamage: sbc #$FF ; 3C0E operand = the damage: being shot at pins the unit down bvc L_3C14 ; 3C10 no signed overflow lda #$81 ; 3C12 clamp at -127 L_3C14: sta unitMovePointsTable,y ; 3C14 movement points back into the array lda unitDisplayFlagsTable,y ; 3C17 display flags ora #$02 ; 3C1A bit 1 = the unit's console entry needs refreshing sta unitDisplayFlagsTable,y ; 3C1C display flags back into the array rts ; 3C1F back to applyHitToUnit ; ---------------------------------------------------------------------- ; orderAttackUnitAtCursor - Fire-button action of the attack cursor. It reads the map cell under the ; battlefield cursor (always the centre of the 7x5 view, viewOriginCol+3 / viewOriginRow+2); if an ; enemy unit stands there it queues command $87 SET TARGET carrying the selected unit and that target, ; otherwise it just beeps. ; In: zp_A5/zp_A6 = view origin, selectedUnit $90F8, the map, unit flags $F708 ; Out: cmdParam1/cmdParam2 (zp_74/zp_75) and a 3-byte command $87 in the outgoing ring, or sound 3 ; Called from: the target cursor loop ($21F2, with cursorTargetMode zp_BB > 0) ; ---------------------------------------------------------------------- orderAttackUnitAtCursor: lda viewOriginCol ; 3C20 left edge of the 7x5 battlefield view clc ; 3C22 prepare the addition adc #$03 ; 3C23 the cursor sits 3 cells in ... tax ; 3C25 -> X = cursor column lda viewOriginRow ; 3C26 top edge of the view clc ; 3C28 prepare the addition adc #$02 ; 3C29 ... and 2 cells down: the centre cell tay ; 3C2B -> Y = cursor row jsr readMapCell ; 3C2C read the cell under the cursor bpl L_3C4B ; 3C2F bare terrain -> nothing to shoot at tay ; 3C31 Y = $80|index, which the base-$80 alias tables undo ldx selectedUnit ; 3C32 the unit the order is for lda unitFlagsTableAlias,y ; 3C35 flags of the unit under the cursor ($F708-$80) eor unitFlagsTable,x ; 3C38 compare them with the selected unit's flags and #$20 ; 3C3B bit 5 = side beq L_3C4B ; 3C3D same side -> one of ours, not a target stx cmdParam1 ; 3C3F argument 1 = the unit that will fire tya ; 3C41 the target's cell byte and #$7F ; 3C42 strip the map's 'unit here' marker bit sta cmdParam2 ; 3C44 argument 2 = the target unit lda #$87 ; 3C46 command $87 SET TARGET jmp queueCmd3 ; 3C48 queue the code plus its two arguments for both machines L_3C4B: lda #$03 ; 3C4B sound 3 = error beep jmp playSound ; 3C4D nothing was ordered ; ---------------------------------------------------------------------- ; setBoomerTargetCmd - Execution half of command $87 SET TARGET. The boomer's ordinal among all ; boomers is the index into boomerTargetTable, where the target unit is stored with bit 7 set to mark ; it as a lock the player asked for. The listing still shows these 13 bytes as data; they are entered ; by the JMP $3C50 at $5182 inside the command dispatcher. ; In: cmdParam0 zp_73 = boomer unit index, cmdParam2 zp_75 = target unit index ; Out: boomerTargetTable $91DC[ordinal] = $80|target; Y = the ordinal, X = the boomer index ; Called from: cmdSetTarget ($5182), the handler of command $87 on both machines ; ---------------------------------------------------------------------- setBoomerTargetCmd: ldx cmdParam0 ; 3C50 boomer unit index jsr countBoomersBelow ; 3C52 Y = its ordinal among all boomers = its slot in $91DC lda cmdParam2 ; 3C55 target unit index ora #$80 ; 3C57 bit 7 = the player locked this target on purpose sta boomerTargetTable,y ; 3C59 one entry per boomer rts ; 3C5C back to the command dispatcher ; ---------------------------------------------------------------------- ; countBoomersBelow - Counts the BOOMERs (type 2) with an index below X, which is the ordinal of ; boomer X among all boomers and therefore its slot in boomerTargetTable. The limit is patched into ; the CPX at $3C6F. ; In: X = unit index (exclusive upper limit) ; Out: Y = the number of boomers below X, X = the input value, C = 1; the operand countBoomersLimit+1 ; is patched ; Called from: setBoomerTargetCmd ($3C52), getBoomerTarget ($3C7E) ; ---------------------------------------------------------------------- countBoomersBelow: stx countBoomersLimit+1 ; 3C5D patch the loop limit with the unit we are looking for ldy #$00 ; 3C60 boomers counted so far ldx #$00 ; 3C62 start at unit 0 L_3C64: lda unitTypeTable,x ; 3C64 type byte of this unit and #$07 ; 3C67 strip the dead/dug-in bits cmp #$02 ; 3C69 type 2 = BOOMER bne L_3C6E ; 3C6B not a boomer iny ; 3C6D count it L_3C6E: inx ; 3C6E next unit countBoomersLimit: cpx #$FF ; 3C6F operand = the unit index passed in bcc L_3C64 ; 3C71 count every boomer below it rts ; 3C73 Y = the boomer's slot in boomerTargetTable ; ---------------------------------------------------------------------- ; getBoomerTarget - Reads the target lock of unit X. For a BOOMER it returns C = 1, Y = the boomer's ; slot and A = its boomerTargetTable entry (bit 7 = locked by the player, bits 0-6 = the target unit); ; any other unit type returns C = 0 and a non-zero, positive A. Callers test C and N: the unit ; options menu offers CLEAR TARGET or SET TARGET, and the update loop drops the lock when the boomer ; can no longer fire. ; In: X = unit index ; Out: for a boomer C = 1, A = $91DC[ordinal], Y = the ordinal; otherwise C = 0 and A = type EOR 2 ; Called from: $2432, selectBoomerTarget ($3C8F), the per-unit update loop ($444D), cmdClearTarget ; ($5162), the unit options menu ($5EFF) ; ---------------------------------------------------------------------- getBoomerTarget: clc ; 3C74 C = 0 will mean 'not a boomer' lda unitTypeTable,x ; 3C75 type byte and #$07 ; 3C78 strip the dead/dug-in bits eor #$02 ; 3C7A zero only for type 2 = BOOMER bne L_3C84 ; 3C7C any other type: return C=0 and A = type EOR 2 jsr countBoomersBelow ; 3C7E Y = the boomer's slot in $91DC (this also returns C=1) lda boomerTargetTable,y ; 3C81 bit 7 = locked, bits 0-6 = the target unit L_3C84: rts ; 3C84 back to the caller ; ---------------------------------------------------------------------- ; selectBoomerTarget - Picks what boomer Y shoots at this round. It first runs the weapon-range scan ; (scanVisibleEnemies with X = $80) to build the candidate lists, then walks them: if the target the ; player locked on is still listed it is kept and nothing else is looked at; otherwise an enemy COMCEN ; wins outright and, failing that, the nearest candidate is taken - and the now stale lock in ; boomerTargetTable is cleared. ; In: Y = boomer unit index; boomerTargetTable $91DC; the candidate lists $0400/$0432/$0464 and zp_2F ; produced by the scan ; Out: fireTargetUnit $91F0 = the chosen target ($FF = none), fireTargetDistance $91F1 = its squared ; distance, targetDirection zp_6B = the direction 0-3 to turn to; $91DC[ordinal] cleared when the ; lock was lost; zp_18/zp_19/zp_1A scratch; operand loadBoomerUnit+1 patched ; Called from: the per-unit update loop ($4425) for units of type 2 ; ---------------------------------------------------------------------- selectBoomerTarget: sty loadBoomerUnit+1 ; 3C85 remember the boomer: the scan below needs X and Y for itself ldx #$80 ; 3C88 bit 7 = weapon-range scan, not a spotting scan jsr scanVisibleEnemies ; 3C8A build the candidate lists $0400/$0432/$0464 loadBoomerUnit: ldx #$FF ; 3C8D operand = the boomer unit index jsr getBoomerTarget ; 3C8F A = its lock byte, Y = its slot in $91DC sty scratch1A ; 3C92 keep the slot for the 'lost the target' case below eor #$80 ; 3C94 strip the lock bit -> a plain unit index ($80 when nothing is locked) sta scratch19 ; 3C96 the unit this boomer is currently locked on lda #$FF ; 3C98 $FF ... sta fireTargetDistance ; 3C9A ... no best distance yet sta fireTargetUnit ; 3C9D $91F0 = no target chosen yet ldx scanResultCount ; 3CA0 index of the last candidate stx scratch18 ; 3CA2 the loop walks the list backwards bmi L_3CD9 ; 3CA4 nothing within weapon range at all ; ---------------------------------------------------------------------- ; Walk the candidate list backwards looking for the best target. ; ---------------------------------------------------------------------- L_3CA6: ldx scratch18 ; 3CA6 current candidate ldy checksumXorConstant,x ; 3CA8 its unit index, or $FF where scanVisibleEnemies rejected the entry (also trainerPlaybook0200:play3Col12) bmi L_3CD5 ; 3CAB rejected entry -> skip it cpy scratch19 ; 3CAD is this the target the player locked on? beq L_3CE2 ; 3CAF yes -> keep firing at it and stop looking cpy #$31 ; 3CB1 unit 49 = side 0's comcen beq L_3CB9 ; 3CB3 yes cpy #$63 ; 3CB5 unit 99 = side 1's comcen bne L_3CBF ; 3CB7 an ordinary unit -> compare distances L_3CB9: lda #$00 ; 3CB9 a comcen outranks everything ... sta scratch18 ; 3CBB ... and the DEC below ends the search right after this entry beq L_3CCA ; 3CBD (always taken; note that the comcen's distance is never stored) L_3CBF: lda play3Row12,x ; 3CBF squared distance of this candidate cmp fireTargetDistance ; 3CC2 against the best so far bcs L_3CD5 ; 3CC5 farther away -> keep the old one sta fireTargetDistance ; 3CC7 a new nearest candidate L_3CCA: lda checksumXorConstant,x ; 3CCA its unit index (also trainerPlaybook0200:play3Col12) sta fireTargetUnit ; 3CCD $91F0 = the unit that will be fired at lda play4Grp12,x ; 3CD0 direction 0-3 toward it sta targetDirection ; 3CD3 zp_6B, copied into the unit's facing bits at $4464 L_3CD5: dec scratch18 ; 3CD5 next candidate (the list is walked backwards) bpl L_3CA6 ; 3CD7 until the whole list has been looked at ; ---------------------------------------------------------------------- ; Reached whenever the locked target was not in the list any more (and also when there never was one): ; the lock is dropped, so the unit options menu offers SET TARGET again. ; ---------------------------------------------------------------------- L_3CD9: ldy scratch1A ; 3CD9 the boomer's slot in $91DC lda #$00 ; 3CDB 0 = no target locked sta boomerTargetTable,y ; 3CDD clear the stale lock beq L_3CF0 ; 3CE0 (always taken) ; ---------------------------------------------------------------------- ; The locked target is still within range: keep it and ignore everything else in the list. ; ---------------------------------------------------------------------- L_3CE2: sty fireTargetUnit ; 3CE2 Y still holds the locked unit lda play3Row12,x ; 3CE5 its squared distance sta fireTargetDistance ; 3CE8 $91F1 = distance to the locked target lda play4Grp12,x ; 3CEB direction 0-3 toward it sta targetDirection ; 3CEE zp_6B = direction to turn to L_3CF0: rts ; 3CF0 back to the per-unit update loop ; ringAnimStep - one byte of state for animateRingBurst below: the radius index of the circle frame ; currently being drawn into or erased from sprite 1 (1..A-1 going out, A-1..1 coming back). ringAnimStep: .byte $00 ; 3CF1 . radius index of the ring frame being drawn/erased (0 when the effect is idle) ; ---------------------------------------------------------------------- ; animateRingBurst - Expanding-then-contracting ring drawn into hardware sprite 1. It clears the ; sprite shape, switches the sprite on, draws circle outlines of radius 1,2,..,A-1 into it ; (spritePlotMode $40 = OR the pixels in), plays the explosion sound at full size and then erases the ; same frames in reverse order (spritePlotMode 0 = AND them out) before hiding the sprite again. ; In: A = number of radius steps, patched into the CPX operand at L_3D24+1 (drawCircleInSprite only ; has point lists for radii 1-10); sprite 1 must already be positioned through the shadow ; registers $9011/$9019. ; Out: sprite 1 shape built and cleared again, spriteEnableShadow bit 1 set and then cleared, ; ringAnimStep = 0, spritePlotMode left at 0, sprite1ColourShadow = $12, unusedByte7D = 0 (dead ; store), sound $17 played. Every wait runs through runGameFrames, so the battle keeps ticking ; during the animation. ; Called from: the $6F00 overlay only - $767B with A=3 (the ring that marks a unit being placed / the ; kick-off) and $7A0E with A=$0B (the comcen knock-out explosion). ; ---------------------------------------------------------------------- animateRingBurst: sta L_3D24+1 ; 3CF2 patch the loop limit: A becomes the operand of the CPX at $3D24, i.e. the largest radius + 1 lda #$12 ; 3CF5 $12 = packed flash colour pair (high nibble 1 = white, low nibble 2 = red) sta sprite1ColourShadow ; 3CF7 sprite 1 colour shadow; the IRQ alternates the two nibbles so the ring flashes white/red jsr leftoverSetC397 ; 3CFA wait until the raster is below the visible screen so building the shape does not tear (the $C000 overlay resident here is waitRasterBelowScreen; the label is the boot loader's leftover at the same address) (also textEngineC000:waitRasterBelowScreen) lda #$01 ; 3CFD sprite number 1 (its shape lives at $8A00 + 1*64 = $8A40) jsr clearSpriteShape ; 3CFF wipe the 64 shape bytes before the first frame is plotted lda #$02 ; 3D02 bit mask %00000010 = sprite 1 jsr enableSprites ; 3D04 OR it into spriteEnableShadow ($9022) so the IRQ turns sprite 1 on lda #$40 ; 3D07 plot mode $40 = set (OR) the pixels of the circle sta spritePlotMode ; 3D09 drawCircleInSprite reads spritePlotMode for every plotted point ldx #$01 ; 3D0C start with radius 1 (a filled 3x3 dot) stx ringAnimStep ; 3D0E keep the radius index in ringAnimStep as well as in X ; ---------------------------------------------------------------------- ; Growing phase: draw radius 1, 2, ... up to the caller's limit minus 1. ; ---------------------------------------------------------------------- ringExpandLoop: lda #$01 ; 3D11 sprite number 1 (drawCircleInSprite takes the sprite in A, the radius in X) jsr drawCircleInSprite ; 3D13 plot the circle of radius X into sprite 1 lda ringAnimStep ; 3D16 run 1 game frame on odd radii and none on even ones ... and #$01 ; 3D19 ... so the ring grows about one step every other frame jsr runGameFrames ; 3D1B let the engine advance while the ring expands inc ringAnimStep ; 3D1E next radius ldx ringAnimStep ; 3D21 reload it into X for the compare and for the next drawCircleInSprite L_3D24: cpx #$FF ; 3D24 operand patched at $3CF2 with the caller's step count bcc ringExpandLoop ; 3D26 keep growing while the radius is below the requested maximum lda #$00 ; 3D28 dead store: nothing anywhere reads zp $7D sta unusedByte7D ; 3D2A (a leftover of an earlier version of the effect) lda #$17 ; 3D2C sound $17 = the explosion noise, played at full ring size jsr playSound ; 3D2E queue it on a SID voice lda #$05 ; 3D31 hold the largest ring for 5 frames jsr runGameFrames ; 3D33 the engine keeps running while we wait lda #$00 ; 3D36 plot mode 0 = clear (AND out) the pixels again sta spritePlotMode ; 3D38 so the same circle calls now erase what they drew dec ringAnimStep ; 3D3B step back to the last radius that was actually drawn ldx ringAnimStep ; 3D3E X = that radius ; ---------------------------------------------------------------------- ; Shrinking phase: erase the frames in reverse order, largest first, with no frame delay. ; ---------------------------------------------------------------------- ringShrinkLoop: lda #$01 ; 3D41 sprite number 1 jsr drawCircleInSprite ; 3D43 erase the circle of radius X dec ringAnimStep ; 3D46 next smaller radius ldx ringAnimStep ; 3D49 X = radius for the next pass bne ringShrinkLoop ; 3D4C stop once radius 1 has been erased jsr hideSprite1 ; 3D4E clear bit 1 of the sprite enable shadow and of VIC_SPR_ENA at once lda #$01 ; 3D51 sprite number 1 jmp clearSpriteShape ; 3D53 tail call: blank its 64 shape bytes and return to the caller ; comcenSystemIndex - loop counter of rollComcenSystemDamage: the comcen system being rolled for, ; counting 3, 2, 1 (1 = BATTLE, 2 = RADAR, 3 = DRONE; index 0 is the derived energy status and is ; never damaged here). comcenSystemIndex: .byte $00 ; 3D56 . the comcen system index 3..1 while the damage roll runs ; ---------------------------------------------------------------------- ; rollComcenSystemDamage - Rolls for collateral damage to the local comcen's three repairable systems ; after it has been hit. It runs only in a live game with the DAMAGE rule switched on; for each ; system that is still healthy it sets the status to $80 (damaged) when a random 6-bit number is ; smaller than the damage of the hit, so a big hit is much more likely to knock a system out. ; In: hitDamage $9240 = the damage the hit just did; filmPlaybackMode $0B9B bit 7 (film / solo ; replay), gameTypeOptions $0BA3 bit 4 (the DAMAGE option), gameEndReason $0BA8 (0 = a game is ; running). ; Out: comcenSystemStatus[1..3] ($9242-$9244) may become $80 = damaged; comcenSystemIndex left 0; A/Y ; clobbered. The system states are never sent over the link - each machine damages and repairs ; its own comcen locally. ; Called from: applyHitToUnit at $3B63, only when the unit that was hit is the local comcen. ; ---------------------------------------------------------------------- rollComcenSystemDamage: lda filmPlaybackMode ; 3D57 bit 7 set = watching a game film / solo replay ... bmi L_3D89 ; 3D5A ... in which case no new damage may be rolled lda gameTypeOptions ; 3D5C the rule options of this game and #$10 ; 3D5F bit 4 = the DAMAGE option ('comcen systems can be damaged') beq L_3D89 ; 3D61 the systems are immune when the option is off lda gameEndReason ; 3D63 non-zero = the battle has ended (or has not started) bne L_3D89 ; 3D66 no damage rolls outside a running game lda #$03 ; 3D68 start at system 3 (DRONE) sta comcenSystemIndex ; 3D6A comcenSystemIndex is the loop counter ; ---------------------------------------------------------------------- ; Roll once for each of the three repairable systems 3, 2, 1 (DRONE, RADAR, BATTLE). ; ---------------------------------------------------------------------- comcenSystemRollLoop: ldy comcenSystemIndex ; 3D6D Y = system index lda comcenSystemStatus,y ; 3D70 $9241+Y: 0 = healthy, $40 = degraded, $80 = damaged bmi L_3D84 ; 3D73 already damaged, nothing left to break jsr nextGameRandom ; 3D75 next pseudo random byte and #$3F ; 3D78 keep 6 bits: 0..63 cmp hitDamage ; 3D7A compare it against the damage of the hit (energy runs 0-50) bcs L_3D84 ; 3D7D roll too high: this system survives lda #$80 ; 3D7F $80 = damaged sta comcenSystemStatus,y ; 3D81 mark the system as knocked out L_3D84: dec comcenSystemIndex ; 3D84 next lower system bne comcenSystemRollLoop ; 3D87 stop after system 1; index 0 is the energy display status, not a system L_3D89: rts ; 3D89 return ; ---------------------------------------------------------------------- ; initAllUnits - Resets all 100 unit records from their start positions and puts every living unit ; back on the battlefield map, then clears the 16 per-group state bytes. Run once when a battle ; starts, after the start-position template has been copied into the unit arrays. ; In: unitColTable/unitRowTable hold the start coordinates, unitTypeTable the types and ; unitFlagsTable the group bits taken from the template. ; Out: every per-unit array reset by initUnitRecord/placeUnitOnMap, map cells at the unit positions ; set to $80|index, groupStatusTable $91F4-$9203 zeroed; scratch18 = $FF. ; Called from: overlay A (the setup phase) at $7AEF. ; ---------------------------------------------------------------------- initAllUnits: ldy #$63 ; 3D8A start at unit 99, the last unit of side 1 sty scratch18 ; 3D8C initUnitRecord takes the unit index in scratch18 ; ---------------------------------------------------------------------- ; Reset units 99 down to 0. ; ---------------------------------------------------------------------- initUnitLoop: jsr initUnitRecord ; 3D8E reset one unit and place it on the map dec scratch18 ; 3D91 next lower unit index bpl initUnitLoop ; 3D93 loop until scratch18 goes negative (unit 0 done) ldy #$0F ; 3D95 16 group slots (group key = unitFlagsTable bits 2-5, i.e. 7 groups x 2 sides) ; ---------------------------------------------------------------------- ; Clear the 16 groupStatusTable entries (slowest speed class in bits 0-2, worst terrain class in bits ; 3-5). ; ---------------------------------------------------------------------- clearGroupStatusLoop: lda #$00 ; 3D97 no speed or terrain class recorded yet sta groupStatusTable,y ; 3D99 $91F4+Y dey ; 3D9C next group slot bpl clearGroupStatusLoop ; 3D9D until all 16 are cleared rts ; 3D9F return ; ---------------------------------------------------------------------- ; initUnitRecord - Resets the order state of one unit: its path position and its destination are ; snapped to the cell it stands on, the destination is marked 'idle', the zone-scored bits and the ; stun counter are cleared, the group membership is kept and the facing and side bits are rebuilt. ; Falls straight through into placeUnitOnMap, which finishes the reset and puts the unit on the map. ; In: scratch18 = unit index (loaded into X here). ; Out: unitPathCol/Row = unitCol/Row (cloak and blitz bits cleared), unitDestCol = col|$80 ; (stationary), unitDestRow = row, unitTypeTable bits 3-4 (zone scored) cleared, unitStunTable = ; 0, unitFlagsTable = group bits + facing + side bit; then everything placeUnitOnMap does. ; Called from: initAllUnits ($3D8E) for every unit at game start and recycleDeadUnits ($3E94) for a ; robot respawning at the recycler. ; ---------------------------------------------------------------------- initUnitRecord: ldx scratch18 ; 3DA0 X = unit index (the caller passes it in scratch18) lda unitColTable,x ; 3DA2 the unit's current map column sta unitPathColTable,x ; 3DA5 path position = map position; bits 6-7 (cloak) are cleared as a side effect ora #$80 ; 3DA8 bit 7 of unitDestCol = 'at its destination / stationary' sta unitDestColTable,x ; 3DAA destination column = where it stands, marked idle lda unitRowTable,x ; 3DAD its current map row sta unitPathRowTable,x ; 3DB0 path row = map row; bits 6-7 (blitz) cleared with it sta unitDestRowTable,x ; 3DB3 destination row = current row (bit 7 = 'repairing' cleared too) lda unitTypeTable,x ; 3DB6 type byte: bits 0-2 type, bits 3-4 zone scored, bit 6 dug in, bit 7 dead and #$E7 ; 3DB9 $E7 keeps everything except bits 3 and 4 = the two 'already scored this zone' flags sta unitTypeTable,x ; 3DBB so a reset unit can score its terrain zones again lda #$00 ; 3DBE clear the stun / dig-in byte ... sta unitStunTable,x ; 3DC0 ... no stun counter and no dig-in progress lda unitFlagsTable,x ; 3DC3 flags: facing 0-1, group id 2-4, side 5, group member 6, blitz charge 7 and #$5C ; 3DC6 $5C keeps bits 2-4 (group id) and bit 6 (is a group member) sta unitFlagsTable,x ; 3DC8 facing, side and the one-shot charge bit are dropped jsr initialFacingForUnit ; 3DCB A = 0 (face up) for our units, 1 (face down) for the opponent's ora unitFlagsTable,x ; 3DCE merge the facing into bits 0-1 cpx #$32 ; 3DD1 units 0-49 are side 0, 50-99 side 1 (a fixed test here, not one of the patched ones) bcc L_3DD7 ; 3DD3 side 0 leaves bit 5 clear ora #$20 ; 3DD5 bit 5 = side 1 L_3DD7: sta unitFlagsTable,x ; 3DD7 store the rebuilt flag byte and fall into placeUnitOnMap ; ---------------------------------------------------------------------- ; placeUnitOnMap - Second half of the unit reset: restores the dug-in bit from the type byte, gives ; the unit a fresh movement allowance and full energy, copies the stationary and group bits into the ; display flags, remembers the terrain of the cell it stands on and writes $80|index into that map ; cell. A unit whose type byte has bit 7 set (destroyed) becomes an empty slot instead ($FF in ; unitTypeTable and unitTerrainUnderTable) and is not put on the map. ; In: X = scratch18 = unit index; unitColTable/unitRowTable = the cell to occupy. ; Out: unitStunTable bit 7 = dug in, unitHitFlashTable = 0, unitMovePointsTable = 10, unitEnergyTable ; = 50 (100%), unitDisplayFlagsTable = stationary bit + group bits, unitTerrainUnderTable = the ; terrain hidden by the unit, map cell at (col,row) = $80|index; mapCellPtr points at that cell, Y ; = 0. ; Called from: initUnitRecord (fall-through) and overlay A at $7D83, which re-places every unit once ; the setup phase has moved them around. ; ---------------------------------------------------------------------- placeUnitOnMap: lda unitTypeTable,x ; 3DDA the type byte again and #$40 ; 3DDD bit 6 = the unit has finished digging in asl a ; 3DDF shift it into bit 7 ... sta unitStunTable,x ; 3DE0 ... which is the 'dug in' bit of the stun / dig-in byte lda #$00 ; 3DE3 clear the hit-flash / pinned countdown ... sta unitHitFlashTable,x ; 3DE5 ... so the unit is drawn normally lda #$0A ; 3DE8 10 movement points to start with sta unitMovePointsTable,x ; 3DEA the signed movement-point balance ($FA28) lda #$32 ; 3DED 50 energy points = 100% (the console shows it as a percentage) sta unitEnergyTable,x ; 3DEF energy in bits 0-5; the visibility bits 6-7 stay clear (not spotted yet) lda unitDestColTable,x ; 3DF2 the destination byte initUnitRecord has just written and #$80 ; 3DF5 keep only bit 7 = stationary sta unitDisplayFlagsTable,x ; 3DF7 the display flags start as 'stationary, nothing fired, not pinned' jsr syncLocalGroupFlags ; 3DFA copy the authoritative group bits (unitFlagsTable & $7C) into the display copy ldy scratch18 ; 3DFD Y = unit index ldx unitColTable,y ; 3DFF X = its map column lda unitRowTable,y ; 3E02 A = its map row ... tay ; 3E05 ... which readMapCell expects in Y jsr readMapCell ; 3E06 A = the byte in that cell, mapCellPtr = its address, Y = 0 ldx scratch18 ; 3E09 X = unit index again sta unitTerrainUnderTable,x ; 3E0B remember the terrain the unit is about to cover lda unitTypeTable,x ; 3E0E the type byte bpl L_3E1D ; 3E11 bit 7 clear = the unit is alive, put it on the map lda #$FF ; 3E13 $FF marks an unused unit slot ... sta unitTypeTable,x ; 3E15 ... in the type array ... sta unitTerrainUnderTable,x ; 3E18 ... and in the terrain-under array (= 'not on the map') bmi L_3E22 ; 3E1B always taken ($FF is negative): a destroyed unit is not drawn L_3E1D: txa ; 3E1D A = unit index ora #$80 ; 3E1E an occupied map cell holds $80|index sta (mapCellPtr),y ; 3E20 claim the cell (Y is still 0 from readMapCell) L_3E22: rts ; 3E22 return ; ---------------------------------------------------------------------- ; recycleDeadUnits - Once per round each side that owns a recycler gets one destroyed robot back. For ; a side whose recycler cell is not occupied it scans that side's 50 units downwards from the comcen ; for destroyed ones (type bit 7 set, but not the $FF empty-slot marker). With recycler rate FULL ; (mode 0) the first destroyed unit found is respawned at once; with HALF (mode 1) two destroyed units ; of the SAME type are needed - the first is thrown away (slot set to $FF) and the second respawns. ; The new robot takes the recycler's coordinates, keeps its type, loses all group membership and is ; run through initUnitRecord. ; In: gameEndReason $0BA8 (must be 0), recyclerCol/recyclerRow $92A8/$92AA (bit 7 set = that side has ; no recycler, rate NONE), recyclerMode $92A6 (0 = FULL, 1 = HALF), perSideValueTable $0C8A = each ; side's comcen index ($31/$63), the unit arrays. ; Out: at most one unit per side respawned: its unitCol/unitRow, unitTypeTable, unitFlagsTable and ; everything initUnitRecord touches; for the local side pendingRedrawUnit = the new unit and ; message 5 'UNIT RECYCLED.' is queued. scratch18/19/1A/1B/1C/1D used as scratch. ; Called from: finishRound at $465A, i.e. once per completed round of 100 unit steps. ; ---------------------------------------------------------------------- recycleDeadUnits: lda gameEndReason ; 3E23 0 while a battle is in progress beq L_3E29 ; 3E26 carry on only while the game is running rts ; 3E28 no recycling once the game is over L_3E29: lda #$01 ; 3E29 start with side 1 ... sta scratch1A ; 3E2B ... scratch1A is the side index, counted down to 0 ; ---------------------------------------------------------------------- ; Outer loop over the two sides, 1 then 0. ; ---------------------------------------------------------------------- recyclerSideLoop: ldy scratch1A ; 3E2D Y = side index ldx recyclerCol,y ; 3E2F X = that side's recycler column; bit 7 set = no recycler (rate NONE) bmi L_3E3B ; 3E32 no recycler for this side - N is still set, so the test at $3E3B sends us on lda recyclerRow,y ; 3E34 recycler row ... tay ; 3E37 ... readMapCell wants the row in Y and the column in X jsr readMapCell ; 3E38 A = what is standing on the recycler cell L_3E3B: bmi L_3EB5 ; 3E3B N set = no recycler, or a unit is parked on the recycler: skip this side ldy scratch1A ; 3E3D Y = side index lda recyclerMode,y ; 3E3F recycler rate: 0 = FULL (one for one), 1 = HALF (two of a type for one) sta L_3E5B+1 ; 3E42 patch it into the LDY immediate at $3E5B so the rate is tested inside the loop ldx comcenUnitIndexBySide,y ; 3E45 $0C8A = comcen unit index per side ($31 = 49, $63 = 99) stx scratch1C ; 3E48 scan downwards from the comcen ldy #$32 ; 3E4A 50 units per side sty scratch1D ; 3E4C scratch1D = units left to examine sty scratch19 ; 3E4E scratch19 = type of the previous destroyed unit; 50 can never match a real type (0-4) ; ---------------------------------------------------------------------- ; Scan this side's 50 units from the comcen downwards, looking for destroyed robots. ; ---------------------------------------------------------------------- recyclerScanLoop: ldx scratch1C ; 3E50 X = unit being examined lda unitTypeTable,x ; 3E52 type byte bpl L_3EAF ; 3E55 bit 7 clear = the unit is alive, keep looking cmp #$FF ; 3E57 $FF = empty slot (already recycled away) beq L_3EAF ; 3E59 not a destroyed unit either L_3E5B: ldy #$FF ; 3E5B operand patched at $3E42 with the recycler rate; the LDY sets Z from it bne L_3E65 ; 3E5D rate HALF: a matching pair is needed and #$07 ; 3E5F rate FULL: keep the plain type in bits 0-2 ... sta scratch19 ; 3E61 ... as the type of the unit to respawn bpl respawnUnitAtRecycler ; 3E63 always taken (a type is 0-4): respawn this one immediately L_3E65: and #$07 ; 3E65 rate HALF: plain type of this destroyed unit cmp scratch19 ; 3E67 does it match the type of the destroyed unit found before it? bne L_3EAB ; 3E69 no - remember this one and keep scanning ldx scratch1B ; 3E6B X = the first unit of the pair jsr removeUnitFromMap ; 3E6D take it off the map, restoring the terrain under it lda #$FF ; 3E70 $FF = empty slot ... sta unitTypeTable,x ; 3E72 ... the first of the pair is consumed and gone for good ; ---------------------------------------------------------------------- ; Respawn unit scratch1C of type scratch19 on the recycler cell. ; ---------------------------------------------------------------------- respawnUnitAtRecycler: ldx scratch1C ; 3E75 X = the unit that will come back jsr removeUnitFromMap ; 3E77 make sure it is not on the map anywhere ldy scratch1A ; 3E7A Y = side index lda recyclerCol,y ; 3E7C recycler column ... sta unitColTable,x ; 3E7F ... becomes the new unit's column lda recyclerRow,y ; 3E82 recycler row ... sta unitRowTable,x ; 3E85 ... becomes the new unit's row lda scratch19 ; 3E88 the plain type (bit 7 cleared, so the unit is alive again) sta unitTypeTable,x ; 3E8A store it back into the type array lda #$00 ; 3E8D no facing, no group, side bit rebuilt by initUnitRecord ... sta unitFlagsTable,x ; 3E8F ... a recycled robot always comes back ungrouped stx scratch18 ; 3E92 initUnitRecord takes the unit index in scratch18 jsr initUnitRecord ; 3E94 reset its orders and put it on the map at the recycler lda scratch1A ; 3E97 which side got the replacement? cmp playerSide ; 3E99 only tell the player about his own robots bne L_3EB5 ; 3E9C the opponent's recycling is silent here (his machine reports it) lda scratch18 ; 3E9E the new unit ... sta pendingRedrawUnit ; 3EA0 ... is queued for a redraw by the next updateGameFrame lda #$05 ; 3EA3 message 5 = 'UNIT RECYCLED.' jsr queueMessageUnlessFilm ; 3EA5 queue it (dropped while a film is playing) jmp L_3EB5 ; 3EA8 one respawn per side per round: go to the next side L_3EAB: sta scratch19 ; 3EAB remember this destroyed unit's type ... stx scratch1B ; 3EAD ... and its index, as the first half of a possible pair L_3EAF: dec scratch1C ; 3EAF next lower unit index dec scratch1D ; 3EB1 one of the 50 units done bne recyclerScanLoop ; 3EB3 keep scanning until this side's 50 units are exhausted L_3EB5: dec scratch1A ; 3EB5 next side bmi L_3EBC ; 3EB7 both sides done jmp recyclerSideLoop ; 3EB9 round two, for side 0 L_3EBC: rts ; 3EBC return ; ---------------------------------------------------------------------- ; removeUnitFromMap - Lifts unit X off the battlefield map: the terrain byte the unit was hiding is ; written back into its cell, the unit is marked as 'not on the map' (unitTerrainUnderTable = $FF) and ; its hit-flash/pinned countdown is cleared. Does nothing when the unit is not on the map already. ; In: X = unit index; unitTerrainUnderTable[X] = the saved terrain ($FF = not on the map); ; unitColTable/unitRowTable = its cell. ; Out: map cell restored to terrain, unitTerrainUnderTable[X] = $FF, unitHitFlashTable[X] = 0, ; mapCellPtr points at the cell, Y = 0. ; Called from: recycleDeadUnits ($3E6D and $3E77) and overlay A at $7D9A (clearing the board before ; the units are re-placed). ; ---------------------------------------------------------------------- removeUnitFromMap: lda unitTerrainUnderTable,x ; 3EBD the terrain byte hidden under the unit bmi L_3EE1 ; 3EC0 $FF = the unit is not on the map, nothing to restore ; ---------------------------------------------------------------------- ; removeUnitLeavingCell - Same as removeUnitFromMap but the caller chooses what stays behind in the ; unit's cell (A) instead of the saved terrain. Used when a destroyed unit should leave a ; wreck/marker glyph rather than plain terrain. ; In: A = byte to write into the unit's map cell, X = unit index. ; Out: map cell = A, unitTerrainUnderTable[X] = $FF, unitHitFlashTable[X] = 0, Y = 0; the LDX operand ; at L_3ED7+1 is patched with the unit index. ; Called from: updateHitFlashTimers at $4894, when the flash of a destroyed unit expires. ; ---------------------------------------------------------------------- removeUnitLeavingCell: stx L_3ED7+1 ; 3EC2 park the unit index in the LDX operand at $3ED7 (X is needed for the column) pha ; 3EC5 save the byte to leave in the cell ldy unitRowTable,x ; 3EC6 Y = the unit's map row lda unitColTable,x ; 3EC9 A = its map column ... tax ; 3ECC ... readMapCell wants the column in X jsr readMapCell ; 3ECD sets mapCellPtr to the unit's cell (and returns Y = 0) pla ; 3ED0 get the byte to leave behind back ldy #$00 ; 3ED1 index 0 of the cell pointer sta (mapCellPtr),y ; 3ED3 write it into the map lda #$FF ; 3ED5 $FF = this unit is no longer on the map L_3ED7: ldx #$FF ; 3ED7 operand patched at $3EC2 with the unit index sta unitTerrainUnderTable,x ; 3ED9 mark the unit as off the map lda #$00 ; 3EDC clear the hit-flash / pinned countdown ... sta unitHitFlashTable,x ; 3EDE ... so nothing tries to redraw the vacated cell later L_3EE1: rts ; 3EE1 return ; ---------------------------------------------------------------------- ; applyBlitzStepCost - Charges a blitzing unit for the step it has just taken: it loses one energy ; point and gains 25 movement points (SET BLITZ = 'move extra fast'), and its next shot gets the ; charge bonus (unitFlagsTable bit 7). A unit that has arrived or that has run out of energy pays ; nothing. When the blitzing unit is the local comcen in a live battle it also starts the comcen ; engine sound. ; In: Y = unit index; the caller has already checked that the blitz bit (unitPathRowTable bit 7) is ; set; gamePhase zp_B1, filmPlaybackMode $0B9B, comcenUnit $9236, D_67CA = the sound currently on ; SID voice 2. ; Out: unitFlagsTable[Y] bit 7 set, one energy point taken through reduceUnitEnergyBy (hitDamage = 1), ; unitMovePointsTable[Y] += 25 saturating at $7F; possibly sound $16. ; Called from: processUnitStep at $433C after a successful move step, and by the blitz command ; handlers at $5130 / $5094. ; ---------------------------------------------------------------------- applyBlitzStepCost: lda unitDestColTable,y ; 3EE2 destination byte, bit 7 = the unit is already where it wanted to go bmi L_3F26 ; 3EE5 a stationary unit cannot blitz lda unitEnergyTable,y ; 3EE7 energy byte and #$3F ; 3EEA bits 0-5 = energy 0-50, bits 6-7 are visibility beq L_3F26 ; 3EEC no energy left, the blitz fizzles lda unitFlagsTable,y ; 3EEE unit flags ora #$80 ; 3EF1 bit 7 = one-shot charge bonus applied to the next attack sta unitFlagsTable,y ; 3EF3 store it back lda #$01 ; 3EF6 one energy point is the price of a blitz step jsr reduceUnitEnergyBy ; 3EF8 hitDamage = 1, then subtract it from unit Y's energy lda gamePhase ; 3EFB 0 only during the real-time battle ora filmPlaybackMode ; 3EFD and only when this is not a film / solo replay bne L_3F19 ; 3F00 no engine sound outside a live battle cpy comcenUnit ; 3F02 only the local player's own comcen makes engine noise bne L_3F19 ; 3F05 any other unit is silent lda sndVoice2SoundId ; 3F07 the sound id currently playing on SID voice 2 beq L_3F14 ; 3F0A voice free cmp #$14 ; 3F0C $14 = comcen moving (normal speed) beq L_3F14 ; 3F0E the engine is already running, restart it as a blitz cmp #$16 ; 3F10 $16 = comcen blitzing bne L_3F19 ; 3F12 some other effect owns the voice - do not interrupt it L_3F14: lda #$16 ; 3F14 sound $16 = comcen engine at blitz speed jsr playSound ; 3F16 request it L_3F19: lda unitMovePointsTable,y ; 3F19 current movement-point balance (signed) clc ; 3F1C clear the carry for the addition ... adc #$19 ; 3F1D +25 points buys roughly one extra step bvc L_3F23 ; 3F1F no signed overflow, keep the sum lda #$7F ; 3F21 clamp at +127 so the balance stays positive L_3F23: sta unitMovePointsTable,y ; 3F23 store the new balance L_3F26: rts ; 3F26 return ; ---------------------------------------------------------------------- ; drawRecyclerOnMap - Puts the recycler glyph ($67) on the battlefield map at side Y's recycler ; position and hands the caller everything drawMapCell needs to show it. If a unit happens to be ; standing on the recycler cell the glyph is stored in that unit's 'terrain under the unit' entry ; instead, so it appears as soon as the unit leaves, and the unit's own map byte is returned for the ; redraw. Returns immediately when that side has no recycler. ; In: Y = side index (0 or 1); recyclerCol $92A8 / recyclerRow $92AA (bit 7 of the column set = no ; recycler). ; Out: X = column, Y = row, A = the byte to draw ($67, or $80|unit when a unit is on the cell); the ; map cell or unitTerrainUnderTable updated; the three immediate operands at $3F51/$3F53/$3F55 are ; patched with the return values. ; Called from: checkEnemyRecyclerSpotted ($3F86), the recycler-placement command handler ($550D), ; overlay A ($7B92, $7D6A) and the modem driver ($EE47, which jumps here to draw our own recycler ; at game start). ; ---------------------------------------------------------------------- drawRecyclerOnMap: lda #$67 ; 3F27 $67 = the recycler glyph in the terrain character set sta L_3F54+1 ; 3F29 default return value: patch it into the LDA at $3F54 ldx recyclerCol,y ; 3F2C X = recycler column; bit 7 set = this side has no recycler bpl L_3F32 ; 3F2F there is one, go on rts ; 3F31 rate NONE: nothing to draw L_3F32: stx L_3F50+1 ; 3F32 return the column through the LDX operand at $3F50 lda recyclerRow,y ; 3F35 recycler row ... tay ; 3F38 ... into Y for readMapCell sty L_3F52+1 ; 3F39 return the row through the LDY operand at $3F52 jsr readMapCell ; 3F3C A = current contents of the recycler cell, mapCellPtr = its address bpl L_3F4C ; 3F3F bit 7 clear = the cell is free terrain sta L_3F54+1 ; 3F41 a unit is standing there: return its map byte so the caller redraws the unit tay ; 3F44 Y = $80|unit index lda #$67 ; 3F45 the recycler glyph ... sta unitTerrainUnderAlias,y ; 3F47 ... goes into that unit's saved-terrain slot ($FA70+$80+unit = $FAF0+unit) so it reappears when the unit moves off bne L_3F50 ; 3F4A always taken ($67 is not zero) L_3F4C: lda #$67 ; 3F4C free cell: write the recycler glyph ... sta (mapCellPtr),y ; 3F4E ... straight into the map (Y = 0 from readMapCell) L_3F50: ldx #$FF ; 3F50 operand patched at $3F32: X = recycler column L_3F52: ldy #$FF ; 3F52 operand patched at $3F39: Y = recycler row L_3F54: lda #$FF ; 3F54 operand patched at $3F29/$3F41: A = the cell value to draw rts ; 3F56 return with X/Y/A set up for drawMapCell ; ---------------------------------------------------------------------- ; checkEnemyRecyclerSpotted - Reveals the opponent's recycler the first time one of our own uncloaked ; robots gets within 3 cells of it: it draws the recycler glyph on the map, queues message 8 'SPOTTED ; RECYCLER!' and then patches its own first byte to RTS so the test never runs again. ; In: playerSide $0B9F, perSideValueTable $0C8A (comcen index per side), the opponent's ; recyclerCol/recyclerRow; the area-scan lists at $0400/$0432. ; Out: the enemy recycler cell drawn, message 8 queued, the byte at $3F57 changed from NOP to RTS. ; The byte is set back to NOP by initIrqAndInputState ($0BBD) at game start, and the modem driver ; writes RTS there directly ($EE3E) when the opponent told us he has no recycler. ; Called from: finishRound at $4657, once per round. ; ---------------------------------------------------------------------- checkEnemyRecyclerSpotted: nop ; 3F57 self-patched: NOP while the search is live, $60 (RTS) once the recycler has been spotted lda playerSide ; 3F58 the local player's side (0 or 1) eor #$01 ; 3F5B the other side tax ; 3F5D X = opponent side index sta L_3F84+1 ; 3F5E patch it into the LDY operand at $3F84 for the later drawRecyclerOnMap ldy comcenUnitIndexBySide,x ; 3F61 Y = the opponent's comcen unit index ($31 or $63) jsr setScanFilterOppositeSide; 3F64 set the area-scan filter to 'units of the side opposite to Y', i.e. our own units ldy recyclerRow,x ; 3F67 Y = the opponent's recycler row lda recyclerCol,x ; 3F6A A = its column; bit 7 set = he has no recycler bmi L_3F96 ; 3F6D nothing to spot tax ; 3F6F X = column (collectUnitsInRadius wants column in X, row in Y) lda #$03 ; 3F70 search radius 3 cells around the recycler jsr collectUnitsInRadius ; 3F72 collect our units in that square into $0400 (index) / $0432 (distance) bmi L_3F96 ; 3F75 X = $FF: no unit of ours is near the recycler ; ---------------------------------------------------------------------- ; Walk the candidate list backwards looking for one robot that is not cloaked - a cloaked robot cannot ; report what it sees. ; ---------------------------------------------------------------------- recyclerSpotterLoop: ldy checksumXorConstant,x ; 3F77 candidate unit index from the scan list at $0400+X (the label is a leftover from the copy-protection table that shares this address) (also trainerPlaybook0200:play3Col12) lda unitPathColTable,y ; 3F7A path column byte; bit 7 set = the unit is cloaked bpl L_3F84 ; 3F7D uncloaked: the recycler has been spotted dex ; 3F7F next candidate bpl recyclerSpotterLoop ; 3F80 until the list is exhausted bmi L_3F96 ; 3F82 all nearby units are cloaked - try again next round L_3F84: ldy #$FF ; 3F84 operand patched at $3F5E: Y = the opponent's side index jsr drawRecyclerOnMap ; 3F86 put the recycler glyph into the map and return the cell in X/Y/A jsr drawMapCell ; 3F89 redraw that cell in the overview map and in the tactical view lda #$08 ; 3F8C message 8 = 'SPOTTED RECYCLER!' jsr queueMessageUnlessFilm ; 3F8E queue it (silent while a film is playing) lda #$60 ; 3F91 $60 = RTS opcode sta checkEnemyRecyclerSpotted; 3F93 disable this routine permanently: the recycler stays visible from now on L_3F96: rts ; 3F96 return ; ---------------------------------------------------------------------- ; initialFacingForUnit - Returns the facing a unit starts with: 0 (up, towards the enemy) for the ; local player's units and 1 (down) for the opponent's, since each player sees his own side at the ; bottom of the map. ; In: X = unit index; the branch opcode at D_3F9E is patched by overlay A ($7BD5) to BCC when the ; player is side 0 and BCS when he is side 1, so 'mine' means index < 50 or index >= 50 ; respectively. ; Out: A = 0 or 1 (facing code, bits 0-1 of unitFlagsTable). ; Called from: initUnitRecord ($3DCB) only. ; ---------------------------------------------------------------------- initialFacingForUnit: lda playerSide ; 3F97 dead instruction: the value is thrown away by the LDA #$00 that follows (leftover of an earlier version that read the side here) lda #$00 ; 3F9A assume facing 0 = up cpx #$32 ; 3F9C unit index against 50, the boundary between the two sides initialFacingSideBranch: bcc L_3FA2 ; 3F9E opcode byte patched per player side by overlay A ($7BD5): BCC for side 0, BCS for side 1 eor #$01 ; 3FA0 the opponent's units face the other way (facing 1 = down) L_3FA2: rts ; 3FA2 return the facing code in A ; ---------------------------------------------------------------------- ; mirrorMapCoordinate - Returns 39 - A, mirroring a map column or row across the 40x40 battlefield. ; Both machines run the same simulation but each player looks at the board from his own end, so every ; coordinate that crosses the link is passed through here. ; In: A = coordinate 0..39. ; Out: A = 39 - A; C reflects the internal negate, not a result. ; Called from: moveUnitToCell ($4785, zone scoring), the command handlers ($4FEA/$4FF1), overlay A ; ($7013), the modem driver ($ECC9) and the solo trainer (six places). ; ---------------------------------------------------------------------- mirrorMapCoordinate: sec ; 3FA3 prepare the subtraction sbc #$27 ; 3FA4 A - 39 eor #$FF ; 3FA6 one's complement ... clc ; 3FA8 clear the carry the SEC/SBC above left set ... adc #$01 ; 3FA9 ... plus one = two's complement, so A = 39 - A rts ; 3FAB return the mirrored coordinate ; ---------------------------------------------------------------------- ; haltStunnedUnit - Cancels the orders of a unit that is stunned (a drone hit stuns everything around ; it): its waypoint and destination are snapped back to the cell it stands on, it is marked stationary ; and it is dropped out of its group so the group does not wait for it. With the UNIT MENUS rule off ; only the comcen can be stunned this way, and in a solo game the trainer's own units (index >= 50) ; are left alone. ; In: Y = unit index; gameTypeOptions $0BA3 bit 3 = the UNIT MENUS option; comcenUnit $9236; ; soloTrainerAtGameStart $92FE bit 7. ; Out: unitPathCol/Row = unitCol/Row, unitDestCol = col|$80 (stationary), unitDestRow = row, ; unitFlagsTable bit 6 (group member) cleared, unitDisplayFlagsTable bit 6 cleared and bit 7 ; (stationary) set. ; Called from: processUnitStep at $4272 while the unit's stun counter is running, and from the ; drone-screen overlay at $72DF. ; ---------------------------------------------------------------------- haltStunnedUnit: lda gameTypeOptions ; 3FAC the rule options of this game and #$08 ; 3FAF bit 3 = UNIT MENUS (cloak / blitz / dig in available) bne L_3FB8 ; 3FB1 with the advanced commands on, every stunned unit is halted cpy comcenUnit ; 3FB3 with them off only the local comcen reacts to a stun ... bne L_3FE7 ; 3FB6 ... every other unit keeps its orders L_3FB8: lda soloTrainerAtGameStart ; 3FB8 bit 7 = this game was started against the solo trainer bpl L_3FC1 ; 3FBB modem game: halt either side's units cpy #$32 ; 3FBD units 50-99 belong to the trainer ... bcs L_3FE7 ; 3FBF ... and it moves them itself, so leave them alone L_3FC1: lda unitColTable,y ; 3FC1 the cell the unit stands on sta unitPathColTable,y ; 3FC4 waypoint/path column = current column (cloak bits cleared with it) ora #$80 ; 3FC7 bit 7 = at destination / stationary sta unitDestColTable,y ; 3FC9 destination column = where it stands lda unitRowTable,y ; 3FCC current row sta unitPathRowTable,y ; 3FCF waypoint/path row = current row (blitz bits cleared) sta unitDestRowTable,y ; 3FD2 destination row too - the unit has nowhere to go lda unitFlagsTable,y ; 3FD5 unit flags and #$BF ; 3FD8 $BF clears bit 6 = 'member of a group' sta unitFlagsTable,y ; 3FDA a stunned unit drops out of its group lda unitDisplayFlagsTable,y ; 3FDD the display copy of the flags and #$BF ; 3FE0 clear its group-member bit as well ... ora #$80 ; 3FE2 ... and set bit 7 = stationary sta unitDisplayFlagsTable,y ; 3FE4 store the display flags back L_3FE7: rts ; 3FE7 return ; ---------------------------------------------------------------------- ; haltAllUnitsAndReveal - Game-over clean-up: repairs the three comcen system status bytes, stops all ; 100 units (stationary, nothing fired, nothing pinned, waypoint snapped back onto the unit) and ; reveals the whole board by marking every unit as spotted. Redraws the overview and the tactical ; view when the battlefield screen is up. ; In: currentScreen $90FB (0 = battlefield). ; Out: comcenSystemStatus[1..3] = 0, unitDisplayFlagsTable bits 0-1 cleared and bit 7 set for all ; units, unitPathCol/Row = unitCol/Row keeping the cloak/blitz bits, every unit's visibility bits ; 6-7 set by markAllUnitsVisible, screen redrawn. ; Called from: endGame ($402F) only. ; ---------------------------------------------------------------------- haltAllUnitsAndReveal: lda #$00 ; 3FE8 0 = system healthy ... sta comcenBattleStatus ; 3FEA ... comcen system 1 (BATTLE) sta comcenRadarStatus ; 3FED ... comcen system 2 (RADAR) sta comcenDroneStatus ; 3FF0 ... comcen system 3 (DRONE); the damage is irrelevant now ldy #$63 ; 3FF3 walk units 99 down to 0 ; ---------------------------------------------------------------------- ; Stop every unit and snap its waypoint back onto its real position. ; ---------------------------------------------------------------------- haltUnitLoop: lda unitDisplayFlagsTable,y ; 3FF5 display flags of the unit and #$FC ; 3FF8 $FC clears bit 0 (fired/reloading) and bit 1 (pinned) ora #$80 ; 3FFA bit 7 = stationary sta unitDisplayFlagsTable,y ; 3FFC store them back lda unitPathColTable,y ; 3FFF path column and #$C0 ; 4002 keep bits 6-7 = the cloak flags ora unitColTable,y ; 4004 merge the unit's real column in sta unitPathColTable,y ; 4007 the waypoint is now the unit's own cell lda unitPathRowTable,y ; 400A path row and #$C0 ; 400D keep bits 6-7 = the blitz flags ora unitRowTable,y ; 400F merge the unit's real row in sta unitPathRowTable,y ; 4012 store the waypoint row dey ; 4015 next unit bpl haltUnitLoop ; 4016 until unit 0 has been done jsr markAllUnitsVisible ; 4018 set the visibility bits 6-7 of every unit's energy byte: the board is revealed lda currentScreen ; 401B which screen is showing? bne L_4026 ; 401E only the battlefield screen (0) needs redrawing jsr drawOverviewMap ; 4020 redraw the 40x40 overview map on the left jsr drawViewport ; 4023 redraw the 7x5 tactical view on the right L_4026: rts ; 4026 return ; ---------------------------------------------------------------------- ; endGame - Ends the battle once the caller has stored the reason in gameEndReason $0BA8 ($08 = the ; clock ran out, $A0/$C0 = one side's comcen was knocked out). It drops any drone in flight, halts ; and reveals all units, works out and shows the result, and starts the countdown that takes the ; player back to the menu. ; In: gameEndReason $0BA8; the two 16-bit score totals $92A2-$92A5 (read by computeGameResult). ; Out: droneControlFlags $920D = 0, droneArrivalFlags $92F9 = 0, all units halted and revealed, result ; message queued, gameEndCountdown $92C9 = $78 (the IRQ bumps it every 8th frame; at $80 ; exitIfGameEnded leaves for the menu, i.e. about 64 frames later). ; Called from: applyHitToUnit at $3BD3 (comcen knocked out, via JMP) and finishRound at $460B (clock ; ran out). ; ---------------------------------------------------------------------- endGame: lda #$00 ; 4027 clear the drone state ... sta droneControlFlags ; 4029 ... no drone is under control any more sta droneArrivalFlags ; 402C ... and no drone is on its way in jsr haltAllUnitsAndReveal ; 402F stop every unit and reveal the whole board jsr computeGameResult ; 4032 compare the scores, queue the WON/LOST/DRAW message and set gameOverFlags lda #$78 ; 4035 $78 = start value of the game-end countdown sta gameEndCountdown ; 4037 the IRQ counts it up every 8 frames; when it reaches $80 the game returns to the menu rts ; 403A return ; ---------------------------------------------------------------------- ; markComcenRevealed - Launching a radar sweep or a drone gives the launching comcen away: it sets ; that side's comcen-reveal timer to 6 rounds and forces the visibility bits of that comcen so the ; other player can see it on the map. (The disassembler prints this routine as .byte because it is ; only reached through the command dispatch table.) ; In: X = 0 for our own launch, non-zero for the opponent's; playerSide $0B9F; perSideValueTable ; $0C8A = comcen index per side ($31/$63). ; Out: comcenRevealTimer[side] $92B2 = 6, unitEnergyTable[that comcen] bits 6-7 set (spotted + ; visible); for the opponent's launch the comcen is also queued for redraw in pendingRedrawUnit. ; A and Y clobbered. ; Called from: the radar command handler ($53BC) and the drone command handler ($53F1). ; ---------------------------------------------------------------------- markComcenRevealed: lda playerSide ; 403B the local player's side cpx #$00 ; 403E X = 0 means we launched beq L_4044 ; 4040 our own side then eor #$01 ; 4042 otherwise the opponent's side L_4044: tay ; 4044 Y = the side whose comcen is exposed lda #$06 ; 4045 6 rounds of exposure sta comcenRevealTimer,y ; 4047 $92B2+side, ticked down once per round by finishRound lda comcenUnitIndexBySide,y ; 404A comcen unit index of that side ($31 = 49 or $63 = 99) tay ; 404D Y = the comcen unit lda unitEnergyTable,y ; 404E its energy byte ora #$C0 ; 4051 bits 6-7 = seen this cycle + visible sta unitEnergyTable,y ; 4053 the comcen is now drawn on the enemy's map cpx #$00 ; 4056 was this the opponent's launch? beq L_405D ; 4058 our own comcen is already drawn, nothing to redraw sty pendingRedrawUnit ; 405A queue the opponent's comcen for a redraw next frame L_405D: rts ; 405D return ; terrainMoveCostTable - movement points charged for one step, indexed by the terrain class 0-7 that ; tryStepTowardTarget derived from the cell the unit is standing on (class table at $077E), or by the ; group's worst terrain class for a grouped unit. Spies always use class 0. A diagonal step costs 8 ; points more. 20, 65, 30, 40, 20, 40, 20, 20 - class 1 (hills/water) is by far the most expensive. ; Read by tryStepTowardTarget ($45DA) and by rebuildGroupTables ($493A). Note the $077E class table ; can return 8, which reads one byte past the end into speedClassRateTable[0] = 12. terrainMoveCostTable: .byte $14,$41,$1E,$28,$14,$28,$14,$14; 405E .A.(.(.. cost per step for terrain classes 0-7: 20,65,30,40,20,40,20,20 ; speedClassRateTable - movement points handed out at the end of each round, indexed by the unit type ; for types 0-3 (GRUNT 12, RIDER 25, BOOMER 12, SPY 25); for a comcen (type 4) the index is that ; side's COMCEN SPEED setting ($92B4/$92B5) instead, and for a grouped unit it is the group's slowest ; class (groupStatusTable bits 0-2). Entry 4 = 0 means an immobile comcen. Read by finishRound ($46A4) ; and rebuildGroupTables ($4958). speedClassRateTable: .byte $0C,$19,$0C,$19,$00 ; 4066 ..... points per round by speed class 0-4: 12,25,12,25,0 ; zoneScoredBitTable - the unitTypeTable bit that records 'this unit has already scored the terrain ; points of that zone': [1] = $08 for the near zone (mirrored column 10-19), [2] = $10 for the far ; zone (column 0-9). Entry 0 ($28) is never indexed - the zone number computed in moveUnitToCell is ; only ever 1 or 2. Read by moveUnitToCell ($479A and $47DE). zoneScoredBitTable: .byte $28,$08,$10 ; 406B (.. index 0 unused, 1 = near zone bit $08, 2 = far zone bit $10 ; unitMovedFlag - a single byte, not a table: set to 1 by moveUnitToCell when it really moved the ; current unit, cleared at the top of every unit step. processUnitStep reads it at $436F to decide ; whether the cell the unit came from has to be redrawn. unitMovedFlag: .byte $00 ; 406E . 1 = the current unit changed map cells during this step ; ---------------------------------------------------------------------- ; waitFramesOrInput - Interruptible pause that keeps the battle running: it advances the game one ; frame at a time for up to A frames and returns early on a fresh fire-button press, on the joystick ; leaving its centre position, or when the game-end countdown starts. ; In: A = number of frames to wait; Y = the value to hand back in Y (parked in the LDY operand at ; L_4091+1). ; Out: Y = the entry value, A/X clobbered, the game advanced by however many frames actually ran. The ; remaining frame count lives in the LDA operand at L_4085+1. ; Called from: the menu/console loops at $63C5, $6457 and $64C5. ; ---------------------------------------------------------------------- waitFramesOrInput: sty L_4091+1 ; 406F remember the caller's Y in the LDY operand at $4091 so it survives the wait ; ---------------------------------------------------------------------- ; One frame per pass; the remaining count is kept in the LDA operand at $4085. ; ---------------------------------------------------------------------- waitFrameLoop: sta L_4085+1 ; 4072 store the frames still to wait into the LDA operand at $4085 lda #$01 ; 4075 advance the game by exactly one frame ... jsr runGameFrames ; 4077 ... so the real-time battle continues during the pause jsr isFireHeld ; 407A returns A = 0 while the fire button is held (after having been released once) beq L_4091 ; 407D button down: stop waiting lda joyDirection ; 407F joystick direction nibble, active low cmp #$0F ; 4081 $0F = centred bne L_4091 ; 4083 joystick pushed: stop waiting L_4085: lda #$FF ; 4085 operand patched at $4072: frames still to wait sec ; 4087 prepare the subtraction ... sbc #$01 ; 4088 one frame gone beq L_4091 ; 408A the whole wait has elapsed ldy gameEndCountdown ; 408C non-zero once the game has ended and the menu countdown runs beq waitFrameLoop ; 408F keep waiting while the game is still live L_4091: ldy #$FF ; 4091 operand patched at $406F: restore the caller's Y rts ; 4093 return ; ---------------------------------------------------------------------- ; runGameFrames - Advances the game by A frames: each pass waits for one raster frame and then runs ; one updateGameFrame. Stops early once the game-end countdown has started; A = 0 returns at once. ; This is the routine every wait loop in the game and in both overlays uses so that the real-time ; battle keeps ticking while the user interface is busy. ; In: A = frame count; Y is preserved through the operand at L_40AF+1. ; Out: Y = the entry value, A/X clobbered, the game state advanced. ; Called from: 7 places in the main program ($17FA, $181F, $19B6, $3D1B, $3D33, $4077, $648A) and 9 in ; the $6F00 overlays. ; ---------------------------------------------------------------------- runGameFrames: sty L_40AF+1 ; 4094 park the caller's Y in the LDY operand at $40AF cmp #$00 ; 4097 zero frames requested? beq L_40AF ; 4099 then there is nothing to do ; ---------------------------------------------------------------------- ; One raster frame plus one engine update per pass. ; ---------------------------------------------------------------------- runOneFrameLoop: pha ; 409B save the remaining count on the stack lda #$01 ; 409C wait for exactly one frame ... jsr waitFrames ; 409E ... by polling the raster register jsr updateGameFrame ; 40A1 run the per-frame game update pla ; 40A4 get the count back sec ; 40A5 prepare the subtraction ... sbc #$01 ; 40A6 one frame done beq L_40AF ; 40A8 all requested frames have run ldy gameEndCountdown ; 40AA non-zero once the game has ended beq runOneFrameLoop ; 40AD otherwise keep running frames L_40AF: ldy #$FF ; 40AF operand patched at $4094: restore the caller's Y rts ; 40B1 return ; ---------------------------------------------------------------------- ; updateGameFrame - The heartbeat of the game, called once per frame from every main and ; user-interface loop. In order it: (1) leaves for the menu if the game-end countdown has expired; ; (2) handles the film replay speed keys 1-9, or takes a short cut straight to the engine while the ; comcen knock-out blackout is running; (3) services the status line and the CLR/HOME key, which ; cycles a film's viewpoint through SHOW US / SHOW THEM / SHOW BOTH; (4) handles the RUN/STOP pause ; and switches off the shot-line and marker sprites when their timers expire; (5) performs the pending ; single-unit and whole-group redraws; (6) runs the engine - every stepIntervalFrames frames one unit ; of the round order is stepped, every tenth unit the queued and received commands are exchanged and ; applied, and after all 100 units the round is finished. ; In: filmPlaybackMode $0B9B, comcenStunStatus $922A bit 6, lastTypedKey $9248, currentScreen $90FB, ; gamePhase zp_B1, stepDelayTimer $90FC, stepIntervalFrames $92E3, unitOrderIndex $9162, ; unitProcessOrder $90FD, the timers $9234/$91C8/$923E/$92C8 and the redraw requests $923A/$923B. ; Out: one unit step performed (processUnitStep) or the round finished (finishRound); map, sprites, ; status line and message queue updated; stepDelayTimer reloaded. May not return at all: ; exitIfGameEnded can jump to the menu, and handlePauseKey discards the return address while the ; game is paused. ; Called from: 12 places in the main program (including runGameFrames $40A1), the $6F00 overlays and ; the comcen blackout loop at $0737. ; ---------------------------------------------------------------------- updateGameFrame: jsr exitIfGameEnded ; 40B2 jumps to the menu (and never returns) once gameEndCountdown has reached $80 lda filmPlaybackMode ; 40B5 bit 7 set = a game film is being replayed bmi L_40C5 ; 40B8 film: the digit keys set the replay speed bit comcenStunStatus ; 40BA bit 6 = the comcen knock-out blackout at $0709 is running bvc L_40EB ; 40BD normal frame: do the whole update jsr handlePauseKey ; 40BF blackout: only the RUN/STOP pause handler runs ... jmp L_41BD ; 40C2 ... then go straight to the engine, leaving the screen frozen ; ---------------------------------------------------------------------- ; Film replay: keys '1'-'9' set the step interval (1 = slowest, 9 = fastest) and show 'SPEED n'. ; ---------------------------------------------------------------------- L_40C5: lda lastTypedKey ; 40C5 the last key the IRQ picked up (PETSCII with bit 7 set) sec ; 40C8 prepare the subtraction ... sbc #$B1 ; 40C9 $B1 = '1'|$80, the first speed key bcc L_40EB ; 40CB below '1': not a speed key cmp #$09 ; 40CD nine speed keys, '1' to '9' bcs L_40EB ; 40CF above '9': not a speed key sta scratch18 ; 40D1 A = digit - 1, i.e. 0..8 lda #$08 ; 40D3 8 = the slowest interval ... sec ; 40D5 prepare the subtraction ... sbc scratch18 ; 40D6 interval = 8 - (digit-1) = 9 - digit ... sta stepIntervalFrames ; 40D8 ... so '1' waits 8 frames per unit step and '9' waits none lda lastTypedKey ; 40DB the key code again sta msgSpeedDigit ; 40DE poke it into the 'SPEED *' template at $8730; its bit 7 doubles as the string terminator lda #$07 ; 40E1 message 7 = 'SPEED n' jsr queueMessage ; 40E3 queue it (always, even in a film) lda #$FF ; 40E6 $FF = no key ... sta lastTypedKey ; 40E8 ... so the digit is consumed and not seen again L_40EB: jsr updateStatusMessage ; 40EB service the status line / message queue once per frame lda gamePhase ; 40EE 0 only during the real-time battle bne L_413D ; 40F0 no viewpoint switching outside the battle lda currentScreen ; 40F2 screen id, 0 = the battlefield bne L_413D ; 40F5 the CLR/HOME key only works on the battlefield screen lda lastTypedKey ; 40F7 the last key pressed cmp #$8C ; 40FA $8C = CLR/HOME in the game's own key table bne L_413D ; 40FC some other key: nothing to do here lda #$FF ; 40FE $FF = no key ... sta lastTypedKey ; 4100 ... consume CLR/HOME lda #$3C ; 4103 60 frames ... sta inputLockoutTimer ; 4105 ... of input lockout so the key does not repeat lda filmPlaybackMode ; 4108 bit 7 set = we are watching a film bpl L_413D ; 410B in a live game CLR/HOME only eats the keypress ; ---------------------------------------------------------------------- ; Cycle the film viewpoint $80 (SHOW US) - $81 (SHOW THEM) - $82 (SHOW BOTH) and back. ; ---------------------------------------------------------------------- clc ; 410D clear the carry for the increment ... adc #$01 ; 410E next viewpoint cmp #$83 ; 4110 past SHOW BOTH? bcc L_4116 ; 4112 no, keep it lda #$80 ; 4114 wrap round to $80 = SHOW US L_4116: sta filmPlaybackMode ; 4116 store the new playback mode (bit 7 stays set) and #$03 ; 4119 bits 0-1 = the viewpoint number 0-2 asl a ; 411B double ... asl a ; 411C times 4: the words in filmViewWordTable are 4 bytes each tay ; 411D Y = offset of 'US ' / 'THEM' / 'BOTH' ldx #$00 ; 411E X = destination index ; ---------------------------------------------------------------------- ; Copy the 4-byte viewpoint word into the 'SHOW ****' message template at $8726. ; ---------------------------------------------------------------------- copyFilmViewWordLoop: lda filmViewWordTable,y ; 4120 one character of the viewpoint word ($86FB in the resident overlay) sta msgShowViewWord,x ; 4123 into the four asterisks of message 6 inx ; 4126 next destination byte iny ; 4127 next source byte cpx #$04 ; 4128 all four characters copied? bcc copyFilmViewWordLoop ; 412A no, keep copying lda #$06 ; 412C message 6 = 'SHOW US/THEM/BOTH' jsr queueMessage ; 412E queue it jsr updateStatusMessage ; 4131 and print it immediately jsr patchUnitSideBranches ; 4134 re-patch the 'is this unit mine' branches for the new viewpoint jsr drawOverviewMap ; 4137 redraw the overview map from the new point of view jsr drawViewport ; 413A and the tactical view L_413D: jsr handlePauseKey ; 413D RUN/STOP pause handling (may drop the return address and skip the rest of the frame) lda currentScreen ; 4140 screen id bne L_4167 ; 4143 the view/shot-line timers only matter on the battlefield screen lda viewRedrawTimer ; 4145 countdown before the tactical view is redrawn beq L_415A ; 4148 not armed and #$7F ; 414A the IRQ stops counting at $80 ... bne L_415A ; 414C ... so only the exact value $80 means 'delay expired' ldx viewOriginCol ; 414E X = left edge of the view ldy viewOriginRow ; 4150 Y = top edge of the view jsr drawViewport ; 4152 redraw the 7x5 tactical view lda #$00 ; 4155 disarm the timer ... sta viewRedrawTimer ; 4157 ... until something asks for another delayed redraw L_415A: lda shotLineTimer ; 415A countdown of the shot-line sprite bne L_4167 ; 415D still visible lda spriteEnableShadow ; 415F sprite enable shadow and #$F7 ; 4162 $F7 clears bit 3 = sprite 3, the shot / path line sta spriteEnableShadow ; 4164 the line disappears when its timer runs out L_4167: lda currentScreen ; 4167 screen id and #$01 ; 416A only even screen ids (battlefield and the missile screen) use the marker sprite bne L_417B ; 416C odd screen: leave the sprites alone lda markerSpriteTimer ; 416E countdown of the marker sprite bne L_417B ; 4171 still running lda spriteEnableShadow ; 4173 sprite enable shadow and #$EF ; 4176 $EF clears bit 4 = sprite 4, the repair/target marker sta spriteEnableShadow ; 4178 switch the marker off when its timer expires ; ---------------------------------------------------------------------- ; Deferred redraws: one single unit and one whole group can be queued from anywhere. ; ---------------------------------------------------------------------- L_417B: ldy pendingRedrawUnit ; 417B unit queued for a redraw ($FF = none) bmi L_418D ; 417E nothing queued lda unitTerrainUnderTable,y ; 4180 terrain under that unit; $FF = it is not on the map bmi L_4188 ; 4183 a unit that is off the map cannot be drawn jsr drawUnitAtPosition ; 4185 draw the unit in the overview and in the tactical view L_4188: lda #$FF ; 4188 $FF = nothing queued ... sta pendingRedrawUnit ; 418A ... the request has been serviced L_418D: lda pendingRedrawGroup ; 418D group key queued for a redraw ($FF = none) bmi L_41BD ; 4190 nothing queued: go on to the engine ora #$40 ; 4192 bit 6 = 'is a group member', so the value matches unitFlagsTable & $7C sta pendingRedrawGroup ; 4194 store the completed group key back lda #$63 ; 4197 walk units 99 ... sta scratch18 ; 4199 ... down to 0 ; ---------------------------------------------------------------------- ; Redraw every living unit that belongs to the queued group. ; ---------------------------------------------------------------------- groupRedrawLoop: ldy scratch18 ; 419B Y = unit index lda unitTypeTable,y ; 419D type byte bmi L_41B4 ; 41A0 bit 7 set = dead or unused slot lda unitFlagsTable,y ; 41A2 unit flags and #$7C ; 41A5 $7C = group id bits 2-4, side bit 5, member bit 6 cmp pendingRedrawGroup ; 41A7 does this unit belong to the queued group? bne L_41B4 ; 41AA no lda unitTerrainUnderTable,y ; 41AC terrain under the unit bmi L_41B4 ; 41AF $FF = the unit is not on the map jsr drawUnitAtPosition ; 41B1 redraw it L_41B4: dec scratch18 ; 41B4 next unit bpl groupRedrawLoop ; 41B6 until unit 0 has been checked lda #$FF ; 41B8 $FF = nothing queued ... sta pendingRedrawGroup ; 41BA ... the group redraw is done ; ---------------------------------------------------------------------- ; The engine proper: one unit of the round order is stepped every stepIntervalFrames frames. ; ---------------------------------------------------------------------- L_41BD: lda stepDelayTimer ; 41BD frames still to wait before the next unit step (counted down by the IRQ) bne L_420F ; 41C0 not time yet - nothing more to do this frame lda screenShakeTimer ; 41C2 screen-shake counter, set when the comcen is hit beq L_41CD ; 41C5 no shake running jsr shakeScreenScroll ; 41C7 jitter the fine scroll registers by a random amount dec screenShakeTimer ; 41CA one shake frame used up L_41CD: lda stepIntervalFrames ; 41CD frames between unit steps (film speed keys change this) ldx #$0A ; 41D0 10 units between command exchanges during the battle ldy gamePhase ; 41D2 0 = the battle is running beq L_41DA ; 41D4 battle: keep both values ldx #$32 ; 41D6 setup phase: exchange commands only every 50 units ... lda #$00 ; 41D8 ... and run the unit steps as fast as possible L_41DA: sta stepDelayTimer ; 41DA reload the inter-step delay lda unitOrderIndex ; 41DD position in the round order, 0-99 beq L_41FB ; 41E0 index 0 needs no exchange (finishRound has just run one) ldy #$00 ; 41E2 high byte of the dividend = 0 jsr divideSigned16by8 ; 41E4 A = index / X, Y = index mod X (X is 10, or 50 in the setup phase) cpy #$00 ; 41E7 remainder zero? bne L_41FB ; 41E9 not on a time-slice boundary - step the unit without an exchange sta commandSliceIndex ; 41EB the time slice number (0-9) that identifies this exchange in the film jsr processCommandExchange ; 41EE swap packets with the opponent and execute both sides' commands lda linkTimeoutCounter ; 41F1 non-zero = the link watchdog is still waiting for the opponent's packet bne L_420A ; 41F4 the opponent is late: postpone the step and try again next frame lda #$80 ; 41F6 bit 7 = the commands of this slice have been applied ... sta exchangeAppliedFlag ; 41F8 ... which the setup and console screens poll L_41FB: jsr updateHitFlashTimers ; 41FB age the per-unit hit-flash timers and redraw the cells whose flash ended ldy unitOrderIndex ; 41FE position in the round order ldx unitProcessOrder,y ; 4201 X = the unit to process, taken from the shuffled round order cpy #$64 ; 4204 100 units make one round bcc processUnitStep ; 4206 still units left: step this one (falls into processUnitStep, not a JSR) bcs L_4210 ; 4208 all 100 done: finish the round L_420A: lda #$00 ; 420A opponent not ready: clear the delay ... sta stepDelayTimer ; 420C ... so the exchange is retried on the very next frame L_420F: rts ; 420F return L_4210: jmp finishRound ; 4210 tick the game clock, hand out movement points and rebuild the round order ; ---------------------------------------------------------------------- ; processUnitStep - Processes exactly one unit of the round order; this is an internal entry of ; updateGameFrame reached by the branch at $4206, not by a JSR, and it ends by falling into the shared ; tail at L_4537 which bumps unitOrderIndex. Broadly it: shuffles two entries of the base order with ; the lock-step RNG so both machines agree; skips dead units and everything once the game is over; ; counts a stunned unit's stun down and halts it; turns a moving unit toward its destination; realises ; a unit whose path position has run ahead of its map position by walking back to the first free cell; ; takes one movement step with tryStepTowardTarget, paying terrain and blitz costs; redraws the cell ; it vacated; keeps the tactical view on a directed unit; and then (past this chunk, from $43A4) spots ; enemies, picks a target, fires, digs in or repairs. ; In: X = unit index taken from unitProcessOrder[unitOrderIndex]; the unit record arrays $F640-$FBB7; ; gamePhase zp_B1; gameEndReason $0BA8; gameOverFlags $923F. ; Out: the unit's position, path position, movement points, flags and energy updated; map cells and ; the screen redrawn; currentUnit $90FA, scanUnitType $9228, scanUnitTerrain $9227, ; currentUnitCloakBits $922C, currentUnitCol/Row $91DA/$91DB set for the helper routines; ; exchangeActivityFlag $92C2 set when the unit really moved. ; Called from: updateGameFrame at $4206 (BCC), once per unit step. ; ---------------------------------------------------------------------- processUnitStep: stx currentUnit ; 4213 currentUnit is the unit index every helper in this routine reads lda gameOverFlags ; 4216 $C0 once the game is over (set by computeGameResult) bpl L_421E ; 4219 game still running: process the unit L_421B: jmp endUnitStep ; 421B everything frozen: just advance the round order L_421E: lda gamePhase ; 421E 0 = the real-time battle beq L_4229 ; 4220 battle: shuffle the round order lda #$50 ; 4222 setup phase: 80 movement points ... sta unitMovePointsTable,x ; 4224 ... so units reach their placement instantly bne L_422C ; 4227 always taken ($50 is not zero): skip the shuffle in the setup phase L_4229: jsr shuffleUnitOrder ; 4229 swap two random entries of the base order, using the lock-step RNG so both machines shuffle identically L_422C: ldx currentUnit ; 422C X = the unit being processed lda unitTypeTable,x ; 422F type byte bmi L_421B ; 4232 bit 7 set = destroyed or an empty slot: nothing to do and #$07 ; 4234 bits 0-2 = the type 0 GRUNT, 1 RIDER, 2 BOOMER, 3 SPY, 4 COMCEN sta scanUnitType ; 4236 the sight/attack scans and the scoring code read it from here lda #$00 ; 4239 clear the 'this unit changed cells' flag ... sta unitMovedFlag ; 423B ... moveUnitToCell sets it again if the unit really moves lda unitPathColTable,x ; 423E path column byte and #$C0 ; 4241 bits 6-7 = the cloak flags sta currentUnitCloakBits ; 4243 kept aside so they survive the path-column rewrite at $4350 lda unitTerrainUnderTable,x ; 4246 the terrain byte hidden under the unit sta scanUnitTerrain ; 4249 used for the sight bonus, the move cost and to redraw the vacated cell lda gameEndReason ; 424C non-zero once the battle has ended bne L_4275 ; 424F game over: skip movement and combat, go to the tail lda unitStunTable,x ; 4251 stun / dig-in byte and #$0F ; 4254 low nibble = rounds of stun left beq L_4278 ; 4256 not stunned: normal processing ; ---------------------------------------------------------------------- ; Stunned unit: count one round of stun off, keep the dig-in nibble, and cancel its orders. ; ---------------------------------------------------------------------- tay ; 4258 keep the stun count in Y lda unitStunTable,x ; 4259 read the byte again ... and #$F0 ; 425C ... and isolate the dig-in progress in bits 4-7 sta L_4265+1 ; 425E patch it into the ORA operand at $4265 so it survives the decrement tya ; 4261 A = stun count sec ; 4262 prepare the subtraction ... sbc #$01 ; 4263 one round of stun served L_4265: ora #$FF ; 4265 operand patched at $425E: put the dig-in nibble back sta unitStunTable,x ; 4267 store the new stun/dig-in byte lda #$00 ; 426A a stunned unit gets no movement points ... sta unitMovePointsTable,x ; 426C ... so it cannot move even if it had orders ldy currentUnit ; 426F Y = unit index jsr haltStunnedUnit ; 4272 cancel its orders and drop it out of its group L_4275: jmp unitStepSkipFiring ; 4275 skip movement and combat for this unit ; ---------------------------------------------------------------------- ; Moving unit: point it at its destination so the facing bits and the scans agree. ; ---------------------------------------------------------------------- L_4278: jsr updateIdleFlag ; 4278 recompute bit 7 of unitDestCol: set when the destination equals the unit's cell bmi L_42A0 ; 427B already at its destination: no facing update needed ldx currentUnit ; 427D X = unit index lda unitDestColTable,x ; 4280 destination column ... and #$3F ; 4283 ... without the stationary bit sta lineEndCol ; 4285 target column of the line walker lda unitDestRowTable,x ; 4287 destination row ... and #$3F ; 428A ... without the repairing bit sta lineEndRow ; 428C target row of the line walker jsr initLineFromUnit ; 428E set the line walker up from the unit's cell; A = principal direction 0-3 sta scratch18 ; 4291 keep the direction ldx currentUnit ; 4293 X = unit index lda unitFlagsTable,x ; 4296 unit flags and #$FC ; 4299 $FC clears the facing bits 0-1 ora scratch18 ; 429B insert the new facing sta unitFlagsTable,x ; 429D the unit now faces the way it is walking L_42A0: jsr updateComcenMoveSound ; 42A0 start or stop the local comcen's engine sound to match its movement ldx currentUnit ; 42A3 X = unit index lda unitColTable,x ; 42A6 the column the unit is standing on ... sta lineEndCol ; 42A9 ... as the line walker's target ... sta currentUnitCol ; 42AB ... and remembered so the vacated cell can be redrawn at $4377 lda unitRowTable,x ; 42AE the row it is standing on ... sta lineEndRow ; 42B1 ... as the line walker's target row ... sta currentUnitRow ; 42B3 ... and remembered for the redraw lda unitDestRowTable,x ; 42B6 destination row ... and #$3F ; 42B9 ... without the repairing bit cmp lineEndRow ; 42BB is the unit already on its destination row? bne L_42CB ; 42BD no: it still has somewhere to go lda unitDestColTable,x ; 42BF destination column ... and #$3F ; 42C2 ... without the stationary bit cmp lineEndCol ; 42C4 and on its destination column? bne L_42CB ; 42C6 no: it still has somewhere to go jmp L_436F ; 42C8 the unit has arrived: no movement this step ; ---------------------------------------------------------------------- ; A unit may step onto an occupied cell: then only its path position (F898/F8FC) moves while its map ; position stays behind. Here the map position is dragged forward again, one cell at a time, to the ; first free cell on the way to the path position. ; ---------------------------------------------------------------------- L_42CB: lda unitPathRowTable,x ; 42CB path row ... and #$3F ; 42CE ... without the blitz bits ... sta lineCurRow ; 42D0 ... as the line walker's current row lda unitPathColTable,x ; 42D2 path column ... and #$3F ; 42D5 ... without the cloak bits ... sta lineCurCol ; 42D7 ... as the line walker's current column cmp lineEndCol ; 42D9 path column against the unit's real column bne walkBackToFreeCellLoop ; 42DB different: the unit is out of step with its path lda lineCurRow ; 42DD compare the rows too cmp lineEndRow ; 42DF compare the path row with the unit's real row beq L_42FB ; 42E1 path position equals map position: nothing to catch up ; ---------------------------------------------------------------------- ; Walk from the path position back toward the map position until a free cell turns up. ; ---------------------------------------------------------------------- walkBackToFreeCellLoop: ldx lineCurCol ; 42E3 X = column of the cell being tested ldy lineCurRow ; 42E5 Y = its row jsr readMapCell ; 42E7 A = the cell contents, mapCellPtr = its address bpl L_42F6 ; 42EA bit 7 clear: this cell is free, the unit can materialise here jsr initLineStepper ; 42EC re-aim the walker from this cell at the unit's real cell jsr stepLine ; 42EF step one cell closer bne walkBackToFreeCellLoop ; 42F2 still cells left to try beq L_42FB ; 42F4 arrived at the unit's own cell without finding a gap L_42F6: sta scratch19 ; 42F6 keep the terrain byte of the free cell for moveUnitToCell jsr moveUnitToCell ; 42F8 move the unit's map position into that cell ; ---------------------------------------------------------------------- ; Now take one real step from the path position toward the destination. ; ---------------------------------------------------------------------- L_42FB: ldy currentUnit ; 42FB Y = unit index lda unitDestColTable,y ; 42FE destination column (the stationary bit is clear here, we tested it above) ... sta lineEndCol ; 4301 ... is the walker's target column lda unitDestRowTable,y ; 4303 destination row ... sta lineEndRow ; 4306 ... is the walker's target row lda unitPathColTable,y ; 4308 path column ... and #$3F ; 430B ... without the cloak bits ... sta lineCurCol ; 430D ... is where the step starts lda unitPathRowTable,y ; 430F path row ... and #$3F ; 4312 ... without the blitz bits ... sta lineCurRow ; 4314 ... is where the step starts jsr tryStepTowardTarget ; 4316 try one step: C=1 and A = points left if it is allowed, C=0 if not bcs L_4329 ; 4319 step accepted txa ; 431B X = $FF when the cell was blocked or off the map, = the unit index when the points ran out bpl L_4326 ; 431C not enough movement points: just wait for the next round ldx currentUnit ; 431E blocked: X = unit index again lda #$0F ; 4321 park the balance at 15 points ... sta unitMovePointsTable,x ; 4323 ... so the unit retries soon instead of banking points L_4326: jmp L_436F ; 4326 no movement this step L_4329: ldy currentUnit ; 4329 Y = unit index sta unitMovePointsTable,y ; 432C store the balance left after paying for the step lda gamePhase ; 432F 0 = the real-time battle bne L_433F ; 4331 no blitz costs during the setup phase lda unitPathRowTable,y ; 4333 path row bit 7 = blitz active bpl L_433F ; 4336 not blitzing lda scratch19 ; 4338 cell byte returned by tryStepTowardTarget; 0 = no step was actually taken beq L_433F ; 433A nothing moved, nothing to pay for jsr applyBlitzStepCost ; 433C pay one energy point for +25 movement points and arm the charge bonus L_433F: lda scratch19 ; 433F the cell the unit stepped into bmi L_4348 ; 4341 bit 7 set = another unit is standing there: only the path position advances beq L_436F ; 4343 0 = no step taken at all jsr moveUnitToCell ; 4345 free cell: really move the unit and redraw both cells L_4348: ldx currentUnit ; 4348 X = unit index lda lineCurCol ; 434B new path column ... ora currentUnitCloakBits ; 434D ... with the cloak bits put back ... sta unitPathColTable,x ; 4350 ... stored as the unit's new path position lda unitPathRowTable,x ; 4353 old path row and #$C0 ; 4356 keep bits 6-7 = the blitz flags ora lineCurRow ; 4358 merge the new row in sta unitPathRowTable,x ; 435A store the new path row lda #$01 ; 435D 1 = something happened this turn ... sta exchangeActivityFlag ; 435F ... which the setup phase uses to decide whether the clock ticks lda lineStepsLeft ; 4362 steps the line walker still has to go bne L_436F ; 4364 the unit is still on its way lda scratch19 ; 4366 it has reached its destination cell - is that cell occupied? bpl L_436F ; 4368 no, the unit is really there lda #$64 ; 436A 100 movement points ... sta unitMovePointsTable,x ; 436C ... so a unit blocked on its own destination keeps pushing every round ; ---------------------------------------------------------------------- ; Common tail: repaint the cell the unit left, keep the view on it, refresh its flags. ; ---------------------------------------------------------------------- L_436F: lda unitMovedFlag ; 436F did moveUnitToCell actually move the unit? beq L_4380 ; 4372 no: the old cell still shows the unit ldy currentUnitRow ; 4374 Y = the row it started the step on ldx currentUnitCol ; 4377 X = the column it started on lda scanUnitTerrain ; 437A A = the terrain that was hidden under it jsr drawMapCell ; 437D repaint that cell in the overview map and in the tactical view L_4380: jsr followDirectedUnit ; 4380 if this unit is the one being directed, scroll the view onto it jsr updateIdleFlag ; 4383 recompute the stationary bit; returns A = unitDestCol byte, Y = currentUnit and #$80 ; 4386 keep bit 7 = stationary sta L_4390+1 ; 4388 patch it into the ORA operand at $4390 lda unitDisplayFlagsTable,y ; 438B the display copy of the flags and #$7F ; 438E clear its bit 7 L_4390: ora #$FF ; 4390 operand patched at $4388: the fresh stationary bit sta unitDisplayFlagsTable,y ; 4392 so the console shows MOVE or READY correctly lda unitFlagsTable,y ; 4395 unit flags and #$03 ; 4398 bits 0-1 = the direction the unit faces sta targetDirection ; 439A the sight and attack scans prefer targets in this direction lda unitPathColTable,y ; 439C path column bit 7 = the unit is cloaked bpl spotEnemiesInSight ; 439F not cloaked: go on to spot and fire (next chunk, from $43A4) L_43A1: jmp unitStepSkipFiring ; 43A1 a cloaked unit neither spots nor fires: jump to the tail ; ---------------------------------------------------------------------- ; Spotting pass (part of processUnitStep, which starts at $4213). Only a unit that is not cloaked and ; only a running battle gets here: it looks around with scanVisibleEnemies in sight mode and makes ; every enemy it can see visible. The three parallel 50-entry scan lists live in the shared ; $0400 scratch page: $0400 = unit index ($FF = candidate rejected), $0432 = squared distance, ; $0464 = direction 0-3. (The $0400 label reads 'checksumXorConstant' because the boot checksum ; constant lives at the same address; at run time this page is pure scratch.) ; ---------------------------------------------------------------------- spotEnemiesInSight: lda gamePhase ; 43A4 zp_B1 = 0 only while the battle is actually running bne L_43A1 ; 43A6 setup / film / menu phase: nobody spots or fires, go to the no-fire path ldx #$00 ; 43A8 X = 0 selects the sight-range pass (see, do not shoot) jsr scanVisibleEnemies ; 43AA Y is still currentUnit: fill $0400/$0432/$0464 with the enemies in sight ldx scanResultCount ; 43AD zp_2F = index of the last list entry ($FF = nothing in sight) stx scratch18 ; 43AF walk the list backwards from that index bmi chooseFireTarget ; 43B1 branch if nothing was spotted at all ; ---------------------------------------------------------------------- ; For every candidate the scan kept: mark it seen this visibility cycle and, if it was not visible ; before, make it visible and draw it on the map. ; ---------------------------------------------------------------------- L_43B3: ldy checksumXorConstant,x ; 43B3 $0400+n = unit index of scan result n (also trainerPlaybook0200:play3Col12) bmi L_43CA ; 43B6 $FF = the scan rejected this candidate (out of range / no line of sight) lda unitEnergyTable,y ; 43B8 energy byte: bits 0-5 energy 0-50, bit 6 = seen this cycle, bit 7 = visible ora #$40 ; 43BB bit 6: ageUnitVisibility will shift this into bit 7 at the end of the round sta unitEnergyTable,y ; 43BD store the refreshed visibility bmi L_43CA ; 43C0 bit 7 already set: the enemy was visible before, no redraw needed ora #$80 ; 43C2 newly spotted: make it visible right now sta unitEnergyTable,y ; 43C4 store it jsr drawUnitAtPosition ; 43C7 draw unit Y on the overview map and in the 7x5 view L_43CA: dec scratch18 ; 43CA step down to the previous list entry ldx scratch18 ; 43CC reload the index for the LDY above bpl L_43B3 ; 43CE loop until entry 0 has been handled ; ---------------------------------------------------------------------- ; Target selection. Boomers keep a target of their own (selectBoomerTarget); everybody else runs a ; second scan in weapon-range mode and picks the nearest candidate, except that an enemy comcen is ; always preferred over anything else. ; ---------------------------------------------------------------------- chooseFireTarget: ldy currentUnit ; 43D0 scanVisibleEnemies wants the scanning unit in Y again lda scanUnitType ; 43D3 $9228 = type of the unit being stepped (0 GRUNT 1 RIDER 2 BOOMER 3 SPY 4 COMCEN) cmp #$02 ; 43D6 BOOMER? beq L_4425 ; 43D8 boomers lock onto a target and keep it ldx #$80 ; 43DA X bit 7 set = weapon-range pass (only enemies that can be shot at) jsr scanVisibleEnemies ; 43DC rebuild the scan lists for firing ldx scanResultCount ; 43DF index of the last candidate stx scratch18 ; 43E1 loop counter bmi unitStepSkipFiring ; 43E3 nothing in weapon range: no shot this step lda #$FF ; 43E5 $FF = 'none' for all three running best values sta fireTargetDistance ; 43E7 $91F1 = squared distance of the best target so far sta fireTargetUnit ; 43EA $91F0 = the chosen target unit sta scratch1B ; 43ED $1B = direction 0-3 toward the chosen target ; ---------------------------------------------------------------------- ; Loop over the candidates, keeping the nearest one; an enemy comcen (unit 49 or 99) wins immediately ; and cuts the loop short. ; ---------------------------------------------------------------------- L_43EF: ldx scratch18 ; 43EF current candidate index lda checksumXorConstant,x ; 43F1 $0400+n = the candidate's unit index (also trainerPlaybook0200:play3Col12) bmi L_441A ; 43F4 $FF = rejected entry, skip it cmp #$31 ; 43F6 unit 49 = side 0 comcen beq L_43FE ; 43F8 a comcen is taken whatever the distance cmp #$63 ; 43FA unit 99 = side 1 comcen bne L_4404 ; 43FC an ordinary unit has to win on distance L_43FE: lda #$00 ; 43FE comcen found sta scratch18 ; 4400 force the loop to end after this entry (DEC makes it $FF) beq L_440F ; 4402 always taken (A = 0): take this candidate L_4404: lda play3Row12,x ; 4404 $0432+n = squared distance of this candidate cmp fireTargetDistance ; 4407 nearer than the best so far? bcs L_441A ; 440A no: keep the previous best sta fireTargetDistance ; 440C new best distance L_440F: lda checksumXorConstant,x ; 440F take this candidate... (also trainerPlaybook0200:play3Col12) sta fireTargetUnit ; 4412 ...as the unit to shoot at lda play4Grp12,x ; 4415 $0464+n = direction 0-3 from the firing unit to that enemy sta scratch1B ; 4418 remember it as the facing to turn to L_441A: dec scratch18 ; 441A next candidate down bpl L_43EF ; 441C until entry 0 (or the forced end after a comcen) lda scratch1B ; 441E direction of the winner sta targetDirection ; 4420 zp_6B: the unit will be turned this way below jmp L_4428 ; 4422 join the common 'may it fire?' test L_4425: jsr selectBoomerTarget ; 4425 boomer: keep or re-choose its lock, sets $91F0/$91F1/zp_6B ; ---------------------------------------------------------------------- ; Decide whether the unit is allowed to shoot at the target that was just found. ; ---------------------------------------------------------------------- L_4428: ldx fireTargetUnit ; 4428 the chosen target bmi unitStepSkipFiring ; 442B $FF = nothing to shoot at lda scanUnitType ; 442D type of the firing unit cmp #$03 ; 4430 types 3 (SPY) and 4 (COMCEN) carry no weapon bcs L_4457 ; 4432 they never fire: drop the target ldy currentUnit ; 4434 the firing unit lda unitPathColTable,y ; 4437 bit 7 of $F898 = the unit is cloaked bmi L_4457 ; 443A unreachable: a cloaked unit already branched to L_443E at $43A1 bpl faceAndFireAtTarget ; 443C always taken: keep the target and fire below ; ---------------------------------------------------------------------- ; unitStepSkipFiring - joined by every unit that cannot shoot this step (setup phase, cloaked, no ; target in range). The unit keeps the facing it had, and a boomer that can no longer reach its ; locked target loses the lock. ; ---------------------------------------------------------------------- unitStepSkipFiring: ldx currentUnit ; 443E the unit being stepped lda unitFlagsTable,x ; 4441 authoritative flag byte and #$03 ; 4444 bits 0-1 = facing sta targetDirection ; 4446 zp_6B: keep looking the way it looked lda unitPathColTable,x ; 4448 bit 7 of $F898 = cloaked bmi L_4457 ; 444B a cloaked unit keeps its boomer lock for later jsr getBoomerTarget ; 444D C=1, A = boomerTargetTable entry and Y = boomer ordinal for a boomer; A = type EOR 2 (positive) otherwise bpl L_4457 ; 4450 not a boomer, or no target locked (bit 7 clear) lda #$00 ; 4452 clear the lock sta boomerTargetTable,y ; 4454 $91DC+ordinal: this boomer has lost sight of its target L_4457: lda #$FF ; 4457 no shot is taken this step sta fireTargetUnit ; 4459 $91F0 = none ; ---------------------------------------------------------------------- ; Turn the unit toward its target and fire. Each unit only fires on every other round: the test is ; (gameClock EOR unit index) AND 1, so half of the units shoot on even rounds and half on odd ones, ; which spreads the work and the shot animations over two rounds. ; ---------------------------------------------------------------------- faceAndFireAtTarget: ldy currentUnit ; 445C the unit being stepped lda unitFlagsTable,y ; 445F authoritative flag byte and #$FC ; 4462 clear the facing bits 0-1 ora targetDirection ; 4464 insert the direction of the target (or the old facing) sta unitFlagsTable,y ; 4466 store the new facing lda gameClock ; 4469 $91CB = rounds left, counts down, so its bit 0 alternates every round eor currentUnit ; 446C stagger the units against each other and #$01 ; 446F is this the unit's own firing round? bne L_447B ; 4471 no: leave the fired bit alone lda unitDisplayFlagsTable,y ; 4473 local display copy of the unit flags and #$FE ; 4476 clear bit 0 = 'has fired' so the console shows READY again sta unitDisplayFlagsTable,y ; 4478 store it L_447B: lda fireTargetUnit ; 447B is there a target? bmi runStationaryUnitUpkeep ; 447E no shot: go on to the standing-still upkeep lda gameClock ; 4480 the same parity test again eor currentUnit ; 4483 gameClock EOR unit index and #$01 ; 4486 fire only on this unit's round bne runStationaryUnitUpkeep ; 4488 not this round lda unitEnergyTable,y ; 448A energy/visibility byte of the firing unit ora #$C0 ; 448D firing gives the unit away: seen (bit 6) and visible (bit 7) sta unitEnergyTable,y ; 448F store it ldx currentUnit ; 4492 the firing unit lda unitDisplayFlagsTable,x ; 4495 local display flags ora #$01 ; 4498 bit 0 = fired (console status FIRED / RELOAD) sta unitDisplayFlagsTable,x ; 449A store it ldy fireTargetUnit ; 449D Y = the target unit jsr resolveAttack ; 44A0 X = attacker, Y = target: damage, sound and the shot line ; ---------------------------------------------------------------------- ; Upkeep of a unit that is standing on its destination (bit 7 of unitDestCol set). It stops blitzing, ; and if it is neither stunned nor firing it either advances its dig-in or repairs one energy point ; per round. ; ---------------------------------------------------------------------- runStationaryUnitUpkeep: ldy currentUnit ; 44A3 the unit being stepped lda unitDestColTable,y ; 44A6 bit 7 set = the unit has arrived / has no move order bpl finishUnitStep ; 44A9 still travelling: nothing to do here lda unitRowTable,y ; 44AB map row 0-39 sta unitPathRowTable,y ; 44AE re-sync the path row and clear its bit 7 = blitz (a standing unit stops charging) lda unitStunTable,y ; 44B1 stun/dig-in byte and #$0F ; 44B4 low nibble = rounds of stun left bne finishUnitStep ; 44B6 a stunned unit neither digs in nor repairs lda unitDestRowTable,y ; 44B8 destination row and #$7F ; 44BB clear bit 7 = 'repairing', it is recomputed every round sta unitDestRowTable,y ; 44BD store it back sta unitPathRowTable,y ; 44C0 destination equals position here, so this also refreshes the path row lda unitDisplayFlagsTable,y ; 44C3 local display flags and #$01 ; 44C6 bit 0 = the unit fired this round bne finishUnitStep ; 44C8 shooting cancels digging in and repairing lda unitStunTable,y ; 44CA stun/dig-in byte bmi L_44E3 ; 44CD bit 7 = the unit is already dug in and #$E0 ; 44CF bits 5-7 = dig-in progress (command $9F starts it with $20) beq L_44EB ; 44D1 progress 0 = not digging in: try to repair instead lda #$01 ; 44D3 something is still happening this round... sta exchangeActivityFlag ; 44D5 ...so the setup-phase clock keeps running ($92C2) lda unitStunTable,y ; 44D8 dig-in byte again clc ; 44DB clear carry for the add adc #$20 ; 44DC one more step of digging in; three steps carry into bit 7 sta unitStunTable,y ; 44DE store the progress bpl finishUnitStep ; 44E1 not finished yet L_44E3: lda unitTypeTable,y ; 44E3 type byte ora #$40 ; 44E6 bit 6 = dug in / fortified (resolveAttack takes 25% off the damage) sta unitTypeTable,y ; 44E8 store it L_44EB: cpy #$31 ; 44EB unit 49 = side 0 comcen beq finishUnitStep ; 44ED a comcen never repairs itself here (only the console REPAIR panel repairs it) cpy #$63 ; 44EF unit 99 = side 1 comcen beq finishUnitStep ; 44F1 same for the other comcen lda unitEnergyTable,y ; 44F3 energy/visibility byte sta scratch18 ; 44F6 keep the whole byte and #$C0 ; 44F8 bits 6-7 = seen / visible sta scratch19 ; 44FA save the visibility bits lda scratch18 ; 44FC the byte again and #$3F ; 44FE bits 0-5 = energy 0-50 (displayed as 0-100%) cmp #$32 ; 4500 already at the maximum of 50? bcs finishUnitStep ; 4502 yes: nothing to repair adc #$01 ; 4504 +1 energy point this round (C is clear after the BCS) ora scratch19 ; 4506 put the visibility bits back sta unitEnergyTable,y ; 4508 store the new energy lda unitDestRowTable,y ; 450B destination row ora #$80 ; 450E bit 7 = repairing, the console status word becomes REPAIR sta unitDestRowTable,y ; 4510 store it ; ---------------------------------------------------------------------- ; End of one unit's step: give the solo trainer a look at the unit that was just moved, clear its 'hit ; last round' marker and redraw it. ; ---------------------------------------------------------------------- finishUnitStep: lda gamePhase ; 4513 battle only bne L_4529 ; 4515 setup / film / menu: no AI hook lda filmPlaybackMode ; 4517 $0B9B bit 7 = a game film is being replayed bmi L_4529 ; 451A the film supplies the opponent's orders, the trainer must not run lda isSoloTrainer ; 451C $0BA5 bit 7 = PRACTICE WITH SOLO TRAINER bpl L_4529 ; 451F modem game: the opponent thinks on his own machine lda gameEndReason ; 4521 $0BA8 <> 0 = no game running bne L_4529 ; 4524 nothing to think about jsr trainerUnitStepHook ; 4526 $E0E8 in the trainer overlay: per-unit bookkeeping of the computer opponent L_4529: ldy currentUnit ; 4529 the unit being stepped lda unitDisplayFlagsTable,y ; 452C local display flags and #$FD ; 452F clear bit 1 = 'was hit during the last round' (console status PINNED) sta unitDisplayFlagsTable,y ; 4531 store it jsr drawUnitAtPosition ; 4534 repaint the unit where it now stands ; ---------------------------------------------------------------------- ; endUnitStep - common exit of processUnitStep (dead and frozen units jump straight here from ; $421B). Advances the round order and refreshes the own comcen's stun status, which is also what ; triggers the COMCEN STUNNED cut-scene. ; ---------------------------------------------------------------------- endUnitStep: inc unitOrderIndex ; 4537 $9162: the next engine step takes the following entry of unitProcessOrder lda comcenStunStatus ; 453A $922A: 0 = comcen fine, $80|n = stunned for n rounds, $FF = knocked out cmp #$FF ; 453D $FF is written by applyDroneBlastToCell ($730D) when the own comcen is knocked out bne L_4549 ; 453F not knocked out ldy filmPlaybackMode ; 4541 $0B9B bit 7 = film playback bmi L_4549 ; 4544 the cut-scene is skipped while a film is replaying jmp runComcenStunnedBlackout; 4546 $0709: one blacked-out round, then the message COMCEN STUNNED! L_4549: ldy comcenUnit ; 4549 $9236 = 49 or 99, the local player's comcen lda unitStunTable,y ; 454C its stun/dig-in byte and #$0F ; 454F low nibble = rounds of stun left beq L_4555 ; 4551 not stunned: status byte 0 ora #$80 ; 4553 bit 7 = the comcen is stunned (read by the console and the status line) L_4555: sta comcenStunStatus ; 4555 publish the comcen status rts ; 4558 one unit of the round has been processed ; ---------------------------------------------------------------------- ; tryStepTowardTarget - walks the line stepper one cell from the current cell ($93,$92) toward the ; target ($95,$94) and decides whether currentUnit may enter that cell and can pay for the step. A ; line with no steps returns C=1 and $19 = 0 (the unit is already there). Cells outside the 40x40 map ; are refused (C=0, X=$FF); an occupied cell may only be entered by a spy, by a cloaked unit when the ; occupant is cloaked too, when the occupant is on the same side, or when the occupant is a spy. The ; cost is terrainMoveCostTable[class], where the class is the group's worst terrain class for a ; grouped unit, the class of the ground under the unit otherwise and always 0 for a spy, plus 8 more ; for a diagonal step. ; In: $92/$93 = current row/column, $94/$95 = target row/column, $90FA currentUnit, unit arrays ; $F708/$F76C/$F898/$FA28/$FAF0, $91F4 groupStatusTable, terrain class table $077E ; Out: C=1 accepted, A = movement points left after the step, $19 = the destination cell byte, ; $92/$93 advanced onto it, zp_48/$49 point at it, X = currentUnit; C=0 refused, X=$FF when ; the cell is off the map or blocked and X = currentUnit when the points are not there yet; ; zp_8C-$97 and zp_A9 hold the line state, $18/$19 clobbered ; Called from: processUnitStep $4316 (the real step) and finishRound $470D (look-ahead while the ; round order is built) ; ---------------------------------------------------------------------- tryStepTowardTarget: jsr initLineStepper ; 4559 set up the Bresenham walk from ($93,$92) to ($95,$94); A = number of steps bne L_4562 ; 455C there is at least one cell to walk sta scratch19 ; 455E $19 = 0: there is no cell to enter (A is 0 here) sec ; 4560 C=1 = 'nothing in the way' - the unit is already on its destination rts ; 4561 done ; ---------------------------------------------------------------------- ; Take one step along the line and check the cell it lands on: inside the map, and either empty or ; shareable with the unit that stands there. ; ---------------------------------------------------------------------- L_4562: jsr stepLine ; 4562 advance ($93,$92) by one cell; zp_A9 = 1 if the step was diagonal ldx lineCurCol ; 4565 candidate column cpx #$28 ; 4567 the battlefield is 40 columns wide bcs refuseStep ; 4569 >= 40 (or wrapped negative): off the map ldy lineCurRow ; 456B candidate row cpy #$28 ; 456D 40 rows bcs refuseStep ; 456F off the map jsr readMapCell ; 4571 A = the map byte of that cell, zp_48/$49 = its address ldx currentUnit ; 4574 the unit that wants to move sta scratch19 ; 4577 $19 = the destination cell byte (bit 7 set = a unit stands there) lda unitTypeTable,x ; 4579 type byte of the mover and #$07 ; 457C unit type 0-4 sec ; 457E prepare the subtraction sbc #$03 ; 457F gives exactly 0 for a SPY (type 3) sta scratch18 ; 4581 $18 = 0 marks the mover as a spy ldy scratch19 ; 4583 Y = the cell byte $80|unit, used to index the -$80 biased unit tables bpl chargeMovementCost ; 4585 bit 7 clear = plain terrain, only the movement cost is left to check lda scratch18 ; 4587 is the mover a spy? beq chargeMovementCost ; 4589 a spy may walk into any occupied cell lda unitPathColTable,x ; 458B bit 7 of $F898 = the mover is cloaked bpl L_4595 ; 458E not cloaked: only the side decides lda unitPathColTableAlias,y ; 4590 $F818 = $F898-$80, so the raw map byte indexes the occupant's cloak flag bmi chargeMovementCost ; 4593 two cloaked units may share a cell L_4595: lda unitFlagsTableAlias,y ; 4595 $F688 = $F708-$80: flag byte of the occupant eor unitFlagsTable,x ; 4598 compare it with the mover's flags... and #$20 ; 459B ...in bit 5, which is the side bit beq chargeMovementCost ; 459D same side: units of one army walk through each other lda unitTypeTableAlias,y ; 459F $F6EC = $F76C-$80: type byte of the occupant and #$07 ; 45A2 unit type 0-4 cmp #$03 ; 45A4 an enemy SPY does not block a cell beq chargeMovementCost ; 45A6 step allowed refuseStep: clc ; 45A8 C=0 = step refused ldx #$FF ; 45A9 X = $FF tells the caller 'off the map or blocked' bcc L_45E5 ; 45AB always taken (C was just cleared) ; ---------------------------------------------------------------------- ; Charge the step against the unit's movement-point balance. ; ---------------------------------------------------------------------- chargeMovementCost: lda unitFlagsTable,x ; 45AD flag byte of the mover and #$40 ; 45B0 bit 6 = the unit belongs to a group beq L_45C7 ; 45B2 not in a group: use its own ground lda unitFlagsTable,x ; 45B4 flag byte again and #$3C ; 45B7 group key in bits 2-5 (group number 0-6 plus the side bit) lsr a ; 45B9 shift the key down... lsr a ; 45BA ...to a group index 0-15 tay ; 45BB index into the group table lda groupStatusTable,y ; 45BC $91F4+group = the group's movement byte and #$38 ; 45BF bits 3-5 = the worst terrain class any member stands on lsr a ; 45C1 right-align it... lsr a ; 45C2 ...three places... lsr a ; 45C3 ...to a class 0-8; a group crosses country as slowly as its worst-placed member jmp L_45D2 ; 45C4 charge that class L_45C7: ldy unitTerrainUnderTable,x ; 45C7 terrain code hidden under the unit lda D_077E,y ; 45CA $077E table: terrain code -> movement class 0-8 ldy scratch18 ; 45CD is the mover a spy? bne L_45D2 ; 45CF no: use the terrain class tya ; 45D1 a spy always moves at class 0 (Y = 0 here) L_45D2: tay ; 45D2 Y = movement class clc ; 45D3 C=0 is the answer if the balance turns out to be negative lda unitMovePointsTable,x ; 45D4 accumulated movement points of the unit bmi L_45E5 ; 45D7 negative balance (set by recordUnitHit): the unit has to wait sec ; 45D9 prepare the subtraction sbc terrainMoveCostTable,y ; 45DA $405E: 20/65/30/40/20/40/20/20 points per class bcc L_45E5 ; 45DD not enough points saved up yet: C=0 with X = the unit index ldy isDiagonalStep ; 45DF zp_A9 = 1 when stepLine moved in both axes beq L_45E5 ; 45E1 orthogonal step: accepted, C=1, A = points left sbc #$08 ; 45E3 a diagonal step costs 8 points more L_45E5: rts ; 45E5 back to the caller ; ---------------------------------------------------------------------- ; finishRound - end of a round, reached by JMP from updateGameFrame ($4210) once all 100 units have ; been stepped. It ticks the game clock (in the setup phase only when something actually happened), ; ends the game when the clock reaches 0 and warns at the last quarter, keeps a knocked-out comcen ; visible for a few rounds, then - during the battle only - runs the trainer's round hook, the ; recycler, the group tables and the visibility ageing, hands every unit its movement points and ; finally builds the next round's processing order. ; In: $0BA8 gameEndReason, $91CB gameClock, zp_B1 gamePhase, $92C2 exchangeActivityFlag, $92EF ; lastQuarterRound, $92B2 comcenRevealTimer, $0B9B/$0BA5 opponent flags, unit arrays, $9164 ; unitBaseOrder ; Out: $91CB decremented, $92FA/message $1D at the last quarter, $F9C4 visibility, $FA28 movement ; points, $91F4 group table, $90FD unitProcessOrder rebuilt, $9162 = 0, $90FC = 0, the $0400 ; scratch page used as round-order marks and $0464 as a stack; zp_1A/zp_1B are the stack ; pointer and the base-order cursor ; Called from: updateGameFrame (jmp $4210); the map generator jumps into the tail at L_4657 ($7BC3) ; after shuffling the initial order ; ---------------------------------------------------------------------- finishRound: lda gameEndReason ; 45E6 $0BA8 <> 0 = no game is running bne L_462B ; 45E9 then the clock must not move lda gameClock ; 45EB $91CB = rounds left, counting down beq L_462B ; 45EE already expired ldy gamePhase ; 45F0 0 = the battle is running beq L_45FE ; 45F2 in the battle every round costs a tick ldy exchangeActivityFlag ; 45F4 $92C2 = something moved, dug in or was ordered this round beq L_462B ; 45F7 setup phase: an idle round does not cost time ldy #$00 ; 45F9 consume the flag... sty exchangeActivityFlag ; 45FB ...so the next setup round has to earn its tick again L_45FE: sec ; 45FE prepare the subtraction sbc #$01 ; 45FF one round gone sta gameClock ; 4601 store the clock bne L_4611 ; 4604 time left lda #$08 ; 4606 end reason 8 = the clock ran out sta gameEndReason ; 4608 record it for endGame and the score screen jsr endGame ; 460B halt the units, reveal everything and show the result jmp L_462B ; 460E carry on with the comcen reveal timers L_4611: cmp lastQuarterRound ; 4611 $92EF = the round at which the last quarter begins bne L_462B ; 4614 not that round lda gamePhase ; 4616 battle or setup? beq L_4621 ; 4618 battle: warn the player lda #$FF ; 461A setup phase reuses the same counter as a free-running timer... sta gameClock ; 461C ...so wrap it to 255 instead of warning bne L_462B ; 461F always taken (A = $FF) L_4621: lda #$1D ; 4621 message $1D = 'LAST QTR WARNING!' jsr queueMessage ; 4623 queue it for the status line lda #$80 ; 4626 bit 7 = last quarter sta lastQuarterFlag ; 4628 $92FA: the status line prints the quarter from here on ; ---------------------------------------------------------------------- ; Per-side comcen reveal timer: endGame sets $92B2+side to 6 when that comcen is knocked out, and for ; those rounds the comcen is forced visible so both players can see it. ; ---------------------------------------------------------------------- L_462B: ldx #$01 ; 462B side 1 first, then side 0 L_462D: lda comcenRevealTimer,x ; 462D $92B2+side, counted down here beq L_4640 ; 4630 not revealed dec comcenRevealTimer,x ; 4632 one round less ldy comcenUnitIndexBySide,x ; 4635 $0C8A: 49 for side 0, 99 for side 1 = that side's comcen unit lda unitEnergyTable,y ; 4638 energy/visibility byte of the comcen ora #$C0 ; 463B keep both visibility bits set sta unitEnergyTable,y ; 463D store it L_4640: dex ; 4640 next side bpl L_462D ; 4641 loop over sides 1 and 0 lda gamePhase ; 4643 0 = the battle is running beq L_464A ; 4645 do the full end-of-round work jmp resetRoundCounters ; 4647 setup phase: only reset the step counters ; ---------------------------------------------------------------------- ; Once-per-round engine work, battle only. L_4657 is also entered by JMP from the map generator ; ($7BC3, shuffleInitialUnitOrder) to build the very first round order, which is why the loop below ; tests gamePhase again before it hands out movement points. ; ---------------------------------------------------------------------- L_464A: lda filmPlaybackMode ; 464A $0B9B bit 7 = a film is being replayed bmi L_4657 ; 464D the film already contains the opponent's orders lda isSoloTrainer ; 464F $0BA5 bit 7 = solo trainer bpl L_4657 ; 4652 modem game: the opponent thinks on his own machine jsr runTrainerRoundHook ; 4654 $E079: let the computer opponent plan this round L_4657: jsr checkEnemyRecyclerSpotted; 4657 may reveal the opponent's recycler if one of our units is close enough jsr recycleDeadUnits ; 465A rebuild dead units at the recyclers jsr rebuildGroupTables ; 465D recompute each group's speed and terrain class jsr ageUnitVisibility ; 4660 shift 'seen this cycle' into 'visible' and clear the seen bits ldy #$63 ; 4663 walk the units from 99 down to 0 ; ---------------------------------------------------------------------- ; handOutMovePoints - give every unit this round's movement points and clear its round-order mark in ; the $0400 scratch page. A unit that is dead or standing still is pegged at 15 points; a moving unit ; adds speedClassRateTable[class] to its balance, saturating at +127. The class is the unit type, or ; the group's slowest class for a group member, or the per-player COMCEN SPEED setting for a comcen. ; ---------------------------------------------------------------------- handOutMovePoints: lda gamePhase ; 4665 0 = battle (the map generator can enter here during setup) bne L_46B2 ; 4667 setup: no points, only clear the mark lda unitTypeTable,y ; 4669 type byte bmi L_4673 ; 466C bit 7 = destroyed ldx unitDestColTable,y ; 466E destination column, bit 7 = at destination / no order bpl L_467A ; 4671 the unit still has somewhere to go: it earns points L_4673: lda #$0F ; 4673 standing or dead: a flat 15 points sta unitMovePointsTable,y ; 4675 store the balance bne L_46B2 ; 4678 always taken (A = $0F) L_467A: and #$07 ; 467A A is still the type byte: bits 0-2 = the unit type, which is also the speed class tax ; 467C X = speed class lda unitFlagsTable,y ; 467D flag byte and #$40 ; 4680 bit 6 = member of a group bne L_4696 ; 4682 a group moves at one common speed cpx #$04 ; 4684 type 4 = COMCEN bne L_46A4 ; 4686 ordinary unit: its type is its class ldx comcenSpeedClass ; 4688 $92B4 = the local player's COMCEN SPEED option cpy comcenUnit ; 468B $9236 = the local player's comcen beq L_46A4 ; 468E yes: use the own setting ldx opponentComcenSpeedClass; 4690 $92B5 = the opponent's COMCEN SPEED option jmp L_46A4 ; 4693 look the rate up L_4696: lda unitFlagsTable,y ; 4696 flag byte and #$3C ; 4699 group key in bits 2-5 lsr a ; 469B shift it down... lsr a ; 469C ...to a group index 0-15 tax ; 469D index the group table lda groupStatusTable,x ; 469E $91F4+group and #$07 ; 46A1 bits 0-2 = the class of the group's slowest member tax ; 46A3 X = that class L_46A4: lda speedClassRateTable,x ; 46A4 $4066: 12/25/12/25/0 points per round for classes 0-4 clc ; 46A7 clear carry for the addition adc unitMovePointsTable,y ; 46A8 add to the balance the unit has saved up bvc L_46AF ; 46AB no overflow past +127 lda #$7F ; 46AD saturate: the balance is a signed byte L_46AF: sta unitMovePointsTable,y ; 46AF store the new balance L_46B2: lda #$00 ; 46B2 mark 0 = this unit has not been placed in the round order yet sta checksumXorConstant,y ; 46B4 $0400+unit (the same page as the scan lists, which are finished with) (also trainerPlaybook0200:play3Col12) dey ; 46B7 next unit down bpl handOutMovePoints ; 46B8 until unit 0 ; ---------------------------------------------------------------------- ; Build unitProcessOrder for the coming round out of the shuffled unitBaseOrder. A unit whose next ; step would land on a unit that has not been ordered yet is pushed on a stack at $0464 and the unit ; in front of it is ordered first, so a queue of units all move in the same round instead of advancing ; one cell per round. Marks in $0400+unit: 0 = not ordered, 1 = waiting on the stack, 5 = already ; ordered. zp_1A = stack pointer, zp_1B = cursor into unitBaseOrder. ; ---------------------------------------------------------------------- lda #$00 ; 46BA start the new order at entry 0 sta unitOrderIndex ; 46BC $9162 is reused here as the write index into unitProcessOrder lda #$FF ; 46BF empty markers sta scratch1A ; 46C1 $1A = stack pointer of the waiting stack ($FF = empty) sta scratch1B ; 46C3 $1B = cursor into unitBaseOrder (the first INC gives entry 0) buildRoundOrder: ldx scratch1A ; 46C5 anything waiting on the stack? bmi L_46DA ; 46C7 no: take the next unit of the base order dec scratch1A ; 46C9 pop it ldy play4Grp12,x ; 46CB $0464+sp = the unit that was blocked (same buffer as the scan direction list) lda checksumXorConstant,y ; 46CE its mark (also trainerPlaybook0200:play3Col12) sec ; 46D1 prepare the subtraction sbc #$01 ; 46D2 mark 1 (waiting) becomes 0 (unordered) so it can be placed now sta checksumXorConstant,y ; 46D4 store the mark (also trainerPlaybook0200:play3Col12) jmp L_46E6 ; 46D7 and try to place it L_46DA: inc scratch1B ; 46DA next entry of the shuffled base order ldy scratch1B ; 46DC read the cursor cpy #$64 ; 46DE all 100 entries used? bcs resetRoundCounters ; 46E0 the order is complete lda unitBaseOrder,y ; 46E2 $9164+n = a unit index tay ; 46E5 Y = the unit to place L_46E6: lda checksumXorConstant,y ; 46E6 its mark (also trainerPlaybook0200:play3Col12) bne buildRoundOrder ; 46E9 already ordered or already waiting: take the next one placeUnitInRoundOrder: sty currentUnit ; 46EB the unit we are trying to place lda unitTypeTable,y ; 46EE type byte bmi appendToRoundOrder ; 46F1 a dead unit blocks nothing: order it straight away lda unitPathColTable,y ; 46F3 path column and #$3F ; 46F6 drop the cloak bits 6-7 sta lineCurCol ; 46F8 line stepper start column lda unitPathRowTable,y ; 46FA path row and #$3F ; 46FD drop the blitz bit sta lineCurRow ; 46FF line stepper start row lda unitDestColTable,y ; 4701 destination column bmi appendToRoundOrder ; 4704 bit 7 = standing still: order it straight away sta lineEndCol ; 4706 line stepper target column lda unitDestRowTable,y ; 4708 destination row sta lineEndRow ; 470B line stepper target row jsr tryStepTowardTarget ; 470D look one cell ahead; nothing is moved, only tested bcc appendToRoundOrder ; 4710 blocked or too few movement points: order it now lda scratch19 ; 4712 the cell it would step onto bpl appendToRoundOrder ; 4714 free terrain: nothing to wait for and #$7F ; 4716 a unit stands there: its index sta scratch19 ; 4718 keep it tax ; 471A index the marks with it lda checksumXorConstant,x ; 471B has the blocking unit been ordered or stacked already? (also trainerPlaybook0200:play3Col12) bne appendToRoundOrder ; 471E yes: then this unit can be ordered now lda currentUnit ; 4720 push this unit... inc scratch1A ; 4723 ...onto the waiting stack ldx scratch1A ; 4725 new stack pointer sta play4Grp12,x ; 4727 $0464+sp = the waiting unit tax ; 472A index the marks with it inc checksumXorConstant,x ; 472B mark 1 = waiting for the unit in front of it (also trainerPlaybook0200:play3Col12) ldy scratch19 ; 472E now place the blocking unit first jmp placeUnitInRoundOrder ; 4730 and repeat the same test for it appendToRoundOrder: ldy unitOrderIndex ; 4733 write position in the round order lda currentUnit ; 4736 the unit that has just been settled sta unitProcessOrder,y ; 4739 $90FD+n = the unit that will be stepped n-th next round inc unitOrderIndex ; 473C one more unit ordered tay ; 473F index the marks with the unit lda #$05 ; 4740 mark 5 = ordered sta checksumXorConstant,y ; 4742 store the mark (also trainerPlaybook0200:play3Col12) jmp buildRoundOrder ; 4745 back to the stack / the base order resetRoundCounters: lda #$00 ; 4748 restart the counters sta stepDelayTimer ; 474A $90FC = 0: the very next frame starts the new round sta unitOrderIndex ; 474D $9162 = 0: begin at the first unit of the new order rts ; 4750 the round is finished ; ---------------------------------------------------------------------- ; moveUnitToCell - puts currentUnit into the cell ($93,$92) that tryStepTowardTarget just accepted: ; writes $80|unit into that cell (zp_48 still points at it), restores the terrain that was hidden ; under the unit into the cell it leaves, updates unitCol/unitRow and saves the terrain of the new ; cell. It then scores territory for the step (see the block comment at $4781). ; In: $90FA currentUnit, $93/$92 = new column/row, $19 = the byte that was in the new cell, ; zp_48/$49 -> that cell, $9228 scanUnitType, $92AC terrainPointsBySide, patched branch at $4783 ; Out: both map cells rewritten, $F640/$F6A4/$FAF0/$F76C of the unit updated, $92A2/$92A4 scores, ; $9208 bumped so the status line redraws, $406E unitMovedFlag and $92C2 set; zp_48 low byte is ; reused as the point accumulator and left clobbered ; Called from: processUnitStep $42F8 (catching up with the path position) and $4345 (the normal step) ; ---------------------------------------------------------------------- moveUnitToCell: inc unitMovedFlag ; 4751 $406E: tells processUnitStep to repaint the cell that was left lda #$01 ; 4754 something happened this round... sta exchangeActivityFlag ; 4756 ...so the setup-phase clock may tick ($92C2) ldy #$00 ; 4759 offset 0 for the (zp_48),Y stores ldx currentUnit ; 475B the moving unit txa ; 475E its index ora #$80 ; 475F a map cell holding a unit is $80 | unit index sta (mapCellPtr),y ; 4761 zp_48 still points at the destination cell from readMapCell ldy unitRowTable,x ; 4763 row of the cell being left lda unitColTable,x ; 4766 column of the cell being left tax ; 4769 readMapCell wants the column in X jsr readMapCell ; 476A point zp_48 at the old cell (A = its content, unused) ldy #$00 ; 476D offset 0 again ldx currentUnit ; 476F the moving unit lda unitTerrainUnderTable,x ; 4772 the terrain that was hidden under it sta (mapCellPtr),y ; 4775 put that terrain back into the old cell lda lineCurCol ; 4777 column the line walker stepped onto sta unitColTable,x ; 4779 the unit now stands there lda lineCurRow ; 477C row the line walker stepped onto sta unitRowTable,x ; 477E store the new row ; ---------------------------------------------------------------------- ; Territory scoring. A holds the unit's new row. For the opponent's units the row is mirrored ; (39-row) so that both sides are measured from their own baseline: the branch opcode at $4783 is ; patched by patchUnitSideBranches ($7BC6 in the map generator) with $90 = BCC when this machine plays ; side 0 and $B0 = BCS when it plays side 1, so 'own units' always skip the mirror. Row < 20 means ; the unit has crossed into the enemy half (zone 1), row < 10 means it has reached the enemy's rear ; (zone 2). Each zone pays a unit only once - the receipts are bits 3 and 4 of its type byte. The ; value is the owner's TERRAIN PTS setting, times 16 for a comcen and times 2 for the far zone. ; ---------------------------------------------------------------------- cpx #$32 ; 4781 unit index < 50 = side 0, >= 50 = side 1 zoneRowSideBranch: bcc L_4788 ; 4783 opcode byte patched per side ($90 BCC for side 0, $B0 BCS for side 1): own units skip the mirror jsr mirrorMapCoordinate ; 4785 39 - row: measure the opponent from his own end of the field L_4788: cmp #$0A ; 4788 within 10 rows of the enemy baseline? bcs L_4790 ; 478A no lda #$02 ; 478C zone 2 = the enemy's rear bne L_4796 ; 478E always taken L_4790: cmp #$14 ; 4790 still in the enemy half (rows 10-19)? bcs L_47E7 ; 4792 no, the unit is in its own half: nothing to score lda #$01 ; 4794 zone 1 = the enemy half L_4796: sta L_47BF+1 ; 4796 keep the zone number in the LDA # operand at $47C0 tay ; 4799 index the receipt table with it lda zoneScoredBitTable,y ; 479A $406B: zone 1 -> bit 3 ($08), zone 2 -> bit 4 ($10) and unitTypeTable,x ; 479D has this unit already been paid for this zone? bne L_47E7 ; 47A0 yes: a zone pays only once per unit ldy #$00 ; 47A2 index 0 cpx #$32 ; 47A4 which side does the unit belong to? bcs L_47A9 ; 47A6 side 1 unit: index 0 iny ; 47A8 side 0 unit: index 1 - both arrays are indexed by the opposing side L_47A9: lda terrainPointsBySide,y ; 47A9 $92AC+i = that player's TERRAIN PTS setting (0 disables territory points) beq L_47E7 ; 47AC territory scores nothing in this game sta mapCellPtr ; 47AE zp_48 doubles as the point accumulator; the cell pointer has done its work lda scanUnitType ; 47B0 type of the unit that moved cmp #$04 ; 47B3 COMCEN? bne L_47BF ; 47B5 ordinary unit asl mapCellPtr ; 47B7 a comcen in enemy territory is worth 16 times as much... asl mapCellPtr ; 47B9 ...x2... asl mapCellPtr ; 47BB ...x4... asl mapCellPtr ; 47BD ...x8, x16 L_47BF: lda #$FF ; 47BF operand patched at $4796 = the zone number 1 or 2 pha ; 47C1 keep the zone for the receipt below cmp #$02 ; 47C2 the enemy's rear? bne L_47C8 ; 47C4 no asl mapCellPtr ; 47C6 the far zone is worth double L_47C8: inc lastShownClock ; 47C8 $9208 <> gameClock forces the status line to redraw the score lda mapCellPtr ; 47CB the points earned clc ; 47CD clear carry for the 16-bit add adc sideScoreLo,y ; 47CE $92A2+i: the score array is indexed by the losing side, so this is the mover's own score sta sideScoreLo,y ; 47D1 store the low byte lda sideScoreHi,y ; 47D4 $92A4+i = high byte adc #$00 ; 47D7 add the carry sta sideScoreHi,y ; 47D9 store it pla ; 47DC the zone number again tay ; 47DD index the receipt table lda zoneScoredBitTable,y ; 47DE bit 3 or bit 4 ora unitTypeTable,x ; 47E1 mark the unit as paid for this zone sta unitTypeTable,x ; 47E4 store the type byte L_47E7: lda scratch19 ; 47E7 the byte that was in the new cell (its terrain) sta unitTerrainUnderTable,x ; 47E9 hide it under the unit so it can be restored when the unit leaves rts ; 47EC the move is complete ; ---------------------------------------------------------------------- ; shuffleUnitOrder - exchanges two randomly chosen entries of unitBaseOrder ($9164, the 100 unit ; indices the round order is built from). The indices come from randomUnitIndex, which draws on the ; shared scenario RNG, so both machines shuffle in exactly the same way. ; In: $9164 unitBaseOrder, scenario RNG state $5A-$5C ; Out: two entries of unitBaseOrder exchanged, RNG advanced twice; A/X/Y clobbered ; Called from: processUnitStep $4229 (once per unit stepped during the battle) and the map ; generator $7BBC (51 swaps at game start) ; ---------------------------------------------------------------------- shuffleUnitOrder: nop ; 47ED leftover byte at the entry point - probably an instruction that was patched out jsr randomUnitIndex ; 47EE first slot 0-99 tax ; 47F1 keep it in X jsr randomUnitIndex ; 47F2 second slot 0-99 tay ; 47F5 keep it in Y lda unitBaseOrder,y ; 47F6 entry Y... pha ; 47F9 ...onto the stack lda unitBaseOrder,x ; 47FA entry X... sta unitBaseOrder,y ; 47FD ...into slot Y pla ; 4800 the saved entry... sta unitBaseOrder,x ; 4801 ...into slot X rts ; 4804 the two entries have been exchanged ; ---------------------------------------------------------------------- ; randomUnitIndex - returns a unit index 0-99 drawn uniformly from the scenario random generator: a ; 7-bit sample is rejected and redrawn while it is 100 or more, so the distribution stays flat. ; In: scenario RNG state $5A-$5C ; Out: A = 0..99, C=0; RNG advanced (at least once) ; Called from: shuffleUnitOrder $47EE and $47F2 ; ---------------------------------------------------------------------- randomUnitIndex: jsr nextScenarioRandom ; 4805 $FD4A: the 24-bit generator both machines keep in step and #$7F ; 4808 0-127 cmp #$64 ; 480A 100 units bcs randomUnitIndex ; 480C reject 100-127 and draw again rts ; 480E A = a unit index ; ---------------------------------------------------------------------- ; followDirectedUnit - keeps the battlefield view locked on the unit the console is following. When ; the battlefield screen is up and infoPanelUnit ($91D7) is the unit currently being stepped, it ; computes the cell under the view cursor (viewOriginCol+3, viewOriginRow+2) and, if the unit is not ; on it, moves sprite 0 (the tracking cursor) to the unit on the overview map, switches the sprite on ; and scrolls the 7x5 view onto the unit without animation. ; In: $90FB currentScreen, $91D7 infoPanelUnit, $90FA currentUnit, zp_A5/zp_A6 view origin, ; $F640/$F6A4 unit position ; Out: $91DA/$91DB = the cursor cell, $9010/$9018 sprite 0 position, $9022 sprite 0 enabled, ; zp_92/zp_93 and zp_A5/zp_A6 moved by scrollViewToUnit ; Called from: processUnitStep $4380, after the unit has taken its step ; ---------------------------------------------------------------------- followDirectedUnit: lda currentScreen ; 480F $90FB: 0 = the battlefield screen bne L_485F ; 4812 the console screens do not follow units ldy infoPanelUnit ; 4814 $91D7 = the unit whose panel/menu is open ($FF = none) bmi L_485F ; 4817 nothing is being followed cpy currentUnit ; 4819 is it the unit that is being stepped right now? bne L_485F ; 481C no: leave the view alone lda #$03 ; 481E the view is 7 cells wide... clc ; 4820 clear carry for the add adc viewOriginCol ; 4821 ...so its centre column is the left edge + 3 sta currentUnitCol ; 4823 $91DA = column of the cell under the cursor lda #$02 ; 4826 the view is 5 cells high... clc ; 4828 clear carry adc viewOriginRow ; 4829 ...so its centre row is the top edge + 2 sta currentUnitRow ; 482B $91DB = row of the cell under the cursor ldx unitColTable,y ; 482E column of the followed unit lda unitRowTable,y ; 4831 row of the followed unit tay ; 4834 readable by the compare below cpx currentUnitCol ; 4835 is the unit already under the cursor? bne L_483F ; 4838 no cpy currentUnitRow ; 483A check the row too beq L_485F ; 483D the view is already centred on it: nothing to do L_483F: jsr mapCellToSpriteXY ; 483F X/Y = sprite coordinates of that cell on the overview map stx spriteXShadow ; 4842 $9010 = sprite 0 X (the tracking cursor) sty spriteYShadow ; 4845 $9018 = sprite 0 Y lda #$01 ; 4848 mask for sprite 0 jsr enableSprites ; 484A switch it on (unless sprites are suppressed) lda currentUnitCol ; 484D the scroll starts at the current cursor cell sta lineCurCol ; 4850 line stepper start column lda currentUnitRow ; 4852 cursor row sta lineCurRow ; 4855 line stepper start row lda #$00 ; 4857 0 ticks per step = jump there without animating ldx infoPanelUnit ; 4859 the unit to centre on jsr scrollViewToUnit ; 485C $1D4E: walk the view over to it L_485F: rts ; 485F done ; ---------------------------------------------------------------------- ; updateHitFlashTimers - ages the hit markers of all 100 units. unitHitFlashTable ($FA8C) is ; $80|damage while a unit is flashing (the map drawing code at $286F paints such a unit white); the ; low nibble counts engine steps down and the high nibble is left alone. When the count expires the ; marker is cleared and the cell is repainted: a living unit is drawn again, and a destroyed one has ; the terrain that was under it written back into the map and drawn instead. ; In: $FA8C unitHitFlashTable, $F76C unitTypeTable, $FAF0 unitTerrainUnderTable ; Out: $FA8C aged, map cells and screen updated for expired markers; zp_18 is the loop index ; Called from: updateGameFrame $41FB, once per engine step ; ---------------------------------------------------------------------- updateHitFlashTimers: lda #$63 ; 4860 walk the units from 99... sta scratch18 ; 4862 ...down to 0 in zp_18 L_4864: ldx scratch18 ; 4864 current unit index lda unitHitFlashTable,x ; 4866 $FA8C: 0 = no marker, else $80|damage with the countdown in bits 0-3 beq L_489D ; 4869 this unit is not flashing tay ; 486B keep the whole byte and #$F0 ; 486C high nibble = the marker bit plus the top bits of the damage sta L_4879+1 ; 486E patch it into the ORA operand at $487A tya ; 4871 the byte again and #$0F ; 4872 the countdown in the low nibble beq L_4881 ; 4874 expired: take the marker down sec ; 4876 prepare the subtraction sbc #$01 ; 4877 one engine step less L_4879: ora #$FF ; 4879 operand patched at $486E: put the high nibble back sta unitHitFlashTable,x ; 487B store the aged marker jmp L_489D ; 487E next unit L_4881: lda #$00 ; 4881 clear the marker... sta unitHitFlashTable,x ; 4883 ...so the unit stops flashing white lda unitTypeTable,x ; 4886 type byte bmi L_4890 ; 4889 bit 7 = the unit was destroyed txa ; 488B living unit: its index... ora #$80 ; 488C ...as the map byte $80|unit bne L_4898 ; 488E always taken (the value is >= $80) L_4890: lda unitTerrainUnderTable,x ; 4890 the terrain that was hidden under the wreck pha ; 4893 keep it for the redraw jsr removeUnitLeavingCell ; 4894 write that terrain into the map cell and free the unit's slot ($FAF0 = $FF) pla ; 4897 the terrain byte again L_4898: ldy scratch18 ; 4898 Y = the unit whose cell is redrawn jsr drawValueAtUnit ; 489A repaint the cell in the view and on the overview map L_489D: dec scratch18 ; 489D next unit down bpl L_4864 ; 489F until unit 0 rts ; 48A1 all markers aged ; ---------------------------------------------------------------------- ; drawUnitAtPosition - redraws unit Y where it stands: builds the map byte $80|unit and falls into ; drawValueAtUnit. ; In: Y = unit index ; Out: see drawValueAtUnit ; Called from: $3BDB (after damage), $4185/$41B1 (pending unit/group redraws), $43C7 (newly spotted ; enemy) and $4534 (end of a unit step) ; ---------------------------------------------------------------------- drawUnitAtPosition: tya ; 48A2 the unit index ora #$80 ; 48A3 a map cell holding a unit reads $80 | index ; ---------------------------------------------------------------------- ; drawValueAtUnit - draws cell value A at the position of unit Y. ; In: A = the cell byte to draw, Y = unit index ; Out: the cell is repainted in the 7x5 view and on the overview map; A/X/Y clobbered ; Called from: updateHitFlashTimers $489A, and by falling through from drawUnitAtPosition ; ---------------------------------------------------------------------- drawValueAtUnit: pha ; 48A5 keep the cell value ldx unitColTable,y ; 48A6 X = the unit's map column lda unitRowTable,y ; 48A9 the unit's map row... tay ; 48AC ...into Y as drawMapCell wants it pla ; 48AD the cell value again jmp drawMapCell ; 48AE $5C7C: repaint that cell everywhere it is visible ; ---------------------------------------------------------------------- ; updateIdleFlag - recomputes bit 7 of unitDestCol ($F7D0) for currentUnit: it is set when the ; destination equals the unit's map position (nothing left to do) and cleared while the unit still has ; somewhere to go. The other bits of the byte are kept. The result is delivered in A so the caller ; can copy the same bit into the display flags. ; In: $90FA currentUnit, $F640/$F6A4 position, $F7D0/$F834 destination ; Out: A = the new $F7D0 byte (bit 7 = at destination), Y = currentUnit, N reflects that bit ; Called from: processUnitStep $4278 (before the step) and $4383 (after it) ; ---------------------------------------------------------------------- updateIdleFlag: ldy currentUnit ; 48B1 the unit being stepped lda #$00 ; 48B4 assume 'still travelling'... sta L_48D7+1 ; 48B6 ...by clearing the ORA operand at $48D8 lda unitDestRowTable,y ; 48B9 destination row and #$3F ; 48BC bits 0-5 only: bit 7 is the repair flag cmp unitRowTable,y ; 48BE is the unit already on that row? bne L_48D2 ; 48C1 no: it still has to move lda unitDestColTable,y ; 48C3 destination column and #$3F ; 48C6 bits 0-5 only cmp unitColTable,y ; 48C8 is it on that column too? bne L_48D2 ; 48CB no: it still has to move lda #$80 ; 48CD bit 7 = at destination sta L_48D7+1 ; 48CF patch it into the ORA operand at $48D8 L_48D2: lda unitDestColTable,y ; 48D2 the destination column byte and #$7F ; 48D5 drop the old flag, keep the coordinate L_48D7: ora #$FF ; 48D7 operand patched above: 0 or $80 sta unitDestColTable,y ; 48D9 store the refreshed byte rts ; 48DC A = that byte, Y = currentUnit ; ---------------------------------------------------------------------- ; rebuildGroupTables - recomputes the 16 entries of groupStatusTable ($91F4), one per group key (7 ; groups per side plus the side bit). For every living member it works out the speed class (the unit ; type, or the per-player COMCEN SPEED setting for a comcen) and the terrain class of the ground it ; stands on (always 0 for a spy) and keeps the group's worst terrain class in bits 3-5 and the class ; of its slowest member in bits 0-2, so a group crosses country like its slowest member on the worst ; ground any member occupies. Bits 6-7 (group blitzing / group cloaked) are carried over from the ; previous value for groups that still have members. Scratch: $0400+g member count, $0410+g worst ; movement cost, $0420+g best movement rate, $0430+g the previous entry. ; In: unit arrays $F708/$F76C/$FAF0, $92B4/$92B5 comcen speed classes, $9236 comcenUnit, tables ; $405E terrainMoveCostTable, $4066 speedClassRateTable, $077E terrain classes ; Out: $91F4-$9203 rewritten, $0400-$043F used as scratch; zp_18/zp_19/zp_1A clobbered ; Called from: finishRound $465D, once per round ; ---------------------------------------------------------------------- rebuildGroupTables: ldy #$0F ; 48DD 16 group slots: 7 groups per side, keyed by bits 2-5 of the unit flags L_48DF: lda groupStatusTable,y ; 48DF the entry as it was last round sta play3Row10,y ; 48E2 $0430+g: keep it for its blitz/cloak bits lda #$00 ; 48E5 start from scratch sta groupStatusTable,y ; 48E7 clear the entry sta checksumXorConstant,y ; 48EA $0400+g = member count (also trainerPlaybook0200:play3Col12) sta play3Col28,y ; 48ED $0410+g = worst movement cost met so far lda #$FF ; 48F0 worse than any real rate sta play3Col44,y ; 48F2 $0420+g = best (lowest) movement rate met so far dey ; 48F5 next slot bpl L_48DF ; 48F6 until slot 0 lda #$63 ; 48F8 scan the units from 99... sta scratch1A ; 48FA ...down to 0 in zp_1A L_48FC: ldy scratch1A ; 48FC current unit lda unitTypeTable,y ; 48FE type byte bmi L_496D ; 4901 bit 7 = destroyed: a dead member does not slow the group down lda unitFlagsTable,y ; 4903 flag byte and #$40 ; 4906 bit 6 = the unit is in a group beq L_496D ; 4908 not a member lda unitFlagsTable,y ; 490A flag byte again and #$3C ; 490D group key in bits 2-5 lsr a ; 490F shift it down... lsr a ; 4910 ...to a group index 0-15 tax ; 4911 index the group tables with it inc checksumXorConstant,x ; 4912 one more member in this group (also trainerPlaybook0200:play3Col12) lda unitTypeTable,y ; 4915 type byte and #$07 ; 4918 unit type 0-4 = speed class cmp #$04 ; 491A COMCEN? bne L_4929 ; 491C ordinary unit: its type is its class lda comcenSpeedClass ; 491E $92B4 = the local player's COMCEN SPEED option cpy comcenUnit ; 4921 $9236 = the local player's comcen beq L_4929 ; 4924 yes: keep the own setting lda opponentComcenSpeedClass; 4926 $92B5 = the opponent's COMCEN SPEED option L_4929: sta scratch18 ; 4929 zp_18 = the member's speed class sec ; 492B prepare the subtraction sbc #$03 ; 492C class 3 = SPY beq L_4937 ; 492E a spy ignores the terrain: class 0 (A is 0 here) lda unitTerrainUnderTable,y ; 4930 terrain code hidden under the member tay ; 4933 index the class table with it lda D_077E,y ; 4934 $077E: terrain code -> movement class L_4937: sta scratch19 ; 4937 zp_19 = the member's terrain class tay ; 4939 index the cost table lda terrainMoveCostTable,y ; 493A $405E: movement points that class costs per step cmp play3Col28,x ; 493D worse ground than the group has met so far? bcc L_4956 ; 4940 no: leave the terrain class alone sta play3Col28,x ; 4942 remember the new worst cost lda scratch19 ; 4945 the class that caused it asl a ; 4947 shift it... asl a ; 4948 ...into... asl a ; 4949 ...bits 3-5 sta scratch19 ; 494A keep it lda groupStatusTable,x ; 494C the group entry and #$07 ; 494F keep the speed class bits 0-2 ora scratch19 ; 4951 insert the new terrain class sta groupStatusTable,x ; 4953 store the entry L_4956: ldy scratch18 ; 4956 the member's speed class again lda speedClassRateTable,y ; 4958 $4066: movement points that class earns per round cmp play3Col44,x ; 495B slower than the slowest member so far? bcs L_496D ; 495E no: leave the speed class alone sta play3Col44,x ; 4960 remember the new lowest rate lda groupStatusTable,x ; 4963 the group entry and #$F8 ; 4966 keep the terrain class bits 3-5 ora scratch18 ; 4968 insert the slower class into bits 0-2 sta groupStatusTable,x ; 496A store the entry L_496D: dec scratch1A ; 496D next unit down bpl L_48FC ; 496F until unit 0 ldx #$0F ; 4971 final pass over the 16 slots L_4973: lda checksumXorConstant,x ; 4973 did the group have any living member? (also trainerPlaybook0200:play3Col12) beq L_4980 ; 4976 no: the entry stays cleared lda play3Row10,x ; 4978 last round's entry and #$C0 ; 497B bit 7 = group cloaked, bit 6 = group blitzing ora groupStatusTable,x ; 497D add them to the freshly computed classes L_4980: sta groupStatusTable,x ; 4980 store the entry dex ; 4983 next slot bpl L_4973 ; 4984 until slot 0 rts ; 4986 the group table is up to date ; ---------------------------------------------------------------------- ; averageGroupMoveVector - averages the outstanding movement of one group. Over all 100 units it sums ; (unitDestCol & $7F) - unitCol and (unitDestRow & $7F) - unitRow as two sign-extended 16-bit totals, ; counting only living units whose authoritative flag byte has the member bit 6 set and whose group ; key (bits 2-5) equals cmdParam2, and divides both totals by the member count. Its only caller is ; followGroupMovement, so it runs when a unit joins a group and has to inherit the heading the group ; is already moving on. ; In: zp_75 cmdParam2 = the group key in bits 2-5 (group number plus side bit); unit arrays $F640 ; column, $F6A4 row, $F708 flags, $F76C type, $F7D0/$F834 destination ; Out: A = member count (Z=1 when the group is empty), X = average column delta, Y = average row ; delta; zp_18-zp_1C destroyed; self-modifies the sign-extension ADC operands at $49C6 and $49E3 ; Called from: followGroupMovement $4A04 ; ---------------------------------------------------------------------- averageGroupMoveVector: lda #$00 ; 4987 clear the accumulators sta scratch18 ; 4989 zp_18/zp_19 = 16-bit sum of the column deltas sta scratch19 ; 498B high byte of that sum sta scratch1A ; 498D zp_1A/zp_1B = 16-bit sum of the row deltas sta scratch1B ; 498F high byte of that sum sta scratch1C ; 4991 zp_1C = number of members found ldy #$63 ; 4993 walk the units from 99 down to 0 L_4995: lda unitTypeTable,y ; 4995 type byte bmi L_49E6 ; 4998 bit 7 = destroyed: not counted lda unitFlagsTable,y ; 499A flag byte and #$40 ; 499D bit 6 = the unit is in a group beq L_49E6 ; 499F not a member lda unitFlagsTable,y ; 49A1 flag byte again and #$3C ; 49A4 group key in bits 2-5 cmp cmdParam2 ; 49A6 zp_75 holds the wanted key in the same bit positions bne L_49E6 ; 49A8 a member of some other group inc scratch1C ; 49AA count this member ldx #$00 ; 49AC sign-extension byte for a positive delta lda unitDestColTable,y ; 49AE destination column and #$7F ; 49B1 drop bit 7 = at destination sec ; 49B3 prepare the subtraction sbc unitColTable,y ; 49B4 how far this member still has to travel sideways bpl L_49BB ; 49B7 positive: extend with 0 ldx #$FF ; 49B9 negative: extend with $FF L_49BB: stx L_49C5+1 ; 49BB patch the sign into the ADC operand at $49C6 clc ; 49BE clear carry for the 16-bit add adc scratch18 ; 49BF add the low byte to the running total sta scratch18 ; 49C1 store it lda scratch19 ; 49C3 high byte of the total L_49C5: adc #$FF ; 49C5 operand patched above: add the sign extension plus the carry sta scratch19 ; 49C7 store it ldx #$00 ; 49C9 the same again for the row lda unitDestRowTable,y ; 49CB destination row and #$7F ; 49CE drop bit 7 = repairing sec ; 49D0 prepare the subtraction sbc unitRowTable,y ; 49D1 how far this member still has to travel vertically bpl L_49D8 ; 49D4 positive: extend with 0 ldx #$FF ; 49D6 negative: extend with $FF L_49D8: stx L_49E2+1 ; 49D8 patch the sign into the ADC operand at $49E3 clc ; 49DB clear carry adc scratch1A ; 49DC add the low byte sta scratch1A ; 49DE store it lda scratch1B ; 49E0 high byte of the total L_49E2: adc #$FF ; 49E2 operand patched above sta scratch1B ; 49E4 store it L_49E6: dey ; 49E6 next unit down bpl L_4995 ; 49E7 until unit 0 lda scratch18 ; 49E9 column total, low byte ldy scratch19 ; 49EB column total, high byte (carries the sign) ldx scratch1C ; 49ED divide by the number of members jsr divideSigned16by8 ; 49EF $FCEC: signed 16 by 8 division pha ; 49F2 keep the average column delta lda scratch1A ; 49F3 row total, low byte ldy scratch1B ; 49F5 row total, high byte ldx scratch1C ; 49F7 the same divisor jsr divideSigned16by8 ; 49F9 average the row delta too tay ; 49FC Y = average row delta pla ; 49FD the column result tax ; 49FE X = average column delta lda scratch1C ; 49FF A = member count, Z=1 when the group turned out to be empty rts ; 4A01 back to followGroupMovement ; ---------------------------------------------------------------------- ; followGroupMovement - Gives the unit that is joining a group the movement the rest of the group ; already has: averageGroupMoveVector ($4987) returns the average (destination - position) vector of ; the members and the unit's new destination becomes its own cell plus that vector. The destination is ; applied through the shared destination setter sub_34D5, which clamps it to the map and maintains the ; at-destination bit. A group with no members leaves the unit untouched. ; In: Y = index of the joining unit; cmdParam2 ($75) = group key (unitFlagsTable bits 2-5), read by ; averageGroupMoveVector; unit position tables $F640/$F6A4 ; Out: cmdParam1/cmdParam2 = the new destination cell, written into unitDestColTable/unitDestRowTable ; by sub_34D5; scratch18/scratch19 = average column/row step, scratch1D = saved unit index ; Called from: joinUnitToGroup ($4ADE) only, i.e. the executor of command $88 JOIN GROUP ; ---------------------------------------------------------------------- followGroupMovement: sty scratch1D ; 4A02 remember the joining unit: averageGroupMoveVector needs Y for its own 100-unit scan jsr averageGroupMoveVector ; 4A04 average (destination - position) over the members of group cmdParam2: A = member count, X = average column step, Y = average row step beq L_4A34 ; 4A07 branch if the group has no members at all (count 0): leave this unit's destination alone stx scratch18 ; 4A09 keep the group's average column step (signed, may be negative) sty scratch19 ; 4A0B keep the group's average row step ldy scratch1D ; 4A0D point Y back at the joining unit ; ---------------------------------------------------------------------- ; The next two instructions look like a bug: the test was meant to be 'is the average vector (dx,dy) ; zero?', but TYA here loads the unit index rather than the average row step that was just stored in ; scratch19. The result is that the stand-still path below is only reachable for unit index 0, and ; then only when the average column step is zero as well. It is harmless in the usual case (adding ; zero to the unit's own cell gives the same destination), except for unit 0 joining a group whose ; average dx is 0 and dy is not: that unit is told to stand still. ; ---------------------------------------------------------------------- tya ; 4A0F A = the unit index (this was probably meant to be LDA scratch19, the average row step) ora scratch18 ; 4A10 OR in the average column step and test the pair for zero bne L_4A21 ; 4A12 branch unless both bytes are zero - true for every unit except index 0 with a zero average column step ; ---------------------------------------------------------------------- ; Stand-still: the destination becomes the unit's own cell. ; ---------------------------------------------------------------------- lda unitColTable,y ; 4A14 column of the cell the unit stands on sta cmdParam1 ; 4A17 cmdParam1 = destination column for sub_34D5 lda unitRowTable,y ; 4A19 row of the cell the unit stands on sta cmdParam2 ; 4A1C cmdParam2 = destination row jmp L_4A31 ; 4A1E go and apply the destination ; ---------------------------------------------------------------------- ; Follow the group: destination = own cell + the group's average movement vector. ; ---------------------------------------------------------------------- L_4A21: lda scratch19 ; 4A21 average row step of the group clc ; 4A23 plain add, no carry in adc unitRowTable,y ; 4A24 new destination row = own row + average row step sta cmdParam2 ; 4A27 cmdParam2 = destination row lda scratch18 ; 4A29 average column step of the group clc ; 4A2B plain add adc unitColTable,y ; 4A2C new destination column = own column + average column step sta cmdParam1 ; 4A2F cmdParam1 = destination column L_4A31: jsr setUnitDestination ; 4A31 shared destination setter: clamps to columns/rows 0-39, writes unitDestColTable/unitDestRowTable of unit Y, refreshes the at-destination bit 7 in unitDisplayFlagsTable and stands the unit up (clears the dug-in bit 6 of unitTypeTable) L_4A34: rts ; 4A34 back to joinUnitToGroup ; ---------------------------------------------------------------------- ; sendGroupMoveToCursor - Fire-button action of the battlefield target cursor while a whole group is ; selected (uiLevel $9209 >= 2). It queues command $81 with the selected unit as the group leader and ; the cell under the cursor (view origin + 3 columns, + 2 rows - the centre of the 7x5 tactical view) ; as the ordered position, then refreshes the leader's local at-destination flag. Unlike the ; single-unit order $80 it leaves the unit's member bit alone, so the leader stays in its group. ; In: selectedUnit ($90F8) = the group leader, viewOriginCol/viewOriginRow (zp_A5/zp_A6) = top-left ; cell of the tactical view ; Out: cmdParam0/1/2 = unit, column, row; 4 bytes ($81,unit,col,row) queued in the outgoing command ; ring by queueCmd4 ($560A); unitDisplayFlags bit 7 of the leader refreshed; C=0 always (the ; caller tests C) ; Called from: runTargetCursorLoop ($2218), the fire-click branch taken when uiLevel is 2 or more ; ---------------------------------------------------------------------- sendGroupMoveToCursor: lda #$03 ; 4A35 the cursor sits 3 cells right of the view origin (centre column of the 7-cell wide view) clc ; 4A37 plain add adc viewOriginCol ; 4A38 column of the cell under the cursor sta cmdParam1 ; 4A3A command argument 2 = ordered column lda #$02 ; 4A3C the cursor sits 2 cells below the view origin (centre row of the 5-cell high view) clc ; 4A3E plain add adc viewOriginRow ; 4A3F row of the cell under the cursor sta cmdParam2 ; 4A41 command argument 3 = ordered row lda selectedUnit ; 4A43 the unit the player selected acts as the group's leader sta cmdParam0 ; 4A46 command argument 1 = leader unit index lda #$81 ; 4A48 command $81 = move group keeping formation (4 bytes: unit, column, row) jsr queueCmd4 ; 4A4A queue $81 with its three arguments; it runs on both machines when the exchange is executed jsr updateUnitAtDestinationFlag; 4A4D immediate local feedback: set bit 7 of unitDisplayFlags[leader] if the ordered cell is the one it already stands on, clear it otherwise clc ; 4A50 C=0 = the order was accepted rts ; 4A51 back to the target cursor loop ; ---------------------------------------------------------------------- ; moveGroupKeepingFormation - Executor of command $81, run in lock step on both machines. It works out ; the offset between the leader's current destination and the newly ordered cell and subtracts that ; offset from the destination of every live member of the leader's group, so the leader ends up ; exactly on the ordered cell and the relative shape of the formation is preserved. Membership is ; taken from the authoritative unitFlagsTable, not from the UI copy. Members whose shifted destination ; falls off the map are clamped by sub_34D5, which does distort the formation at the map edges. ; In: cmdParam0 ($73) = leader unit index, cmdParam1/cmdParam2 = ordered column/row (already mirrored ; for a remote command by mirrorCmdCoordsIfRemote) ; Out: unitDestColTable/unitDestRowTable of every member rewritten through sub_34D5; cmdParam0 is ; overwritten with the group key, fillEndPtr (zp_4A) is the 99..0 loop counter; self-modifies the ; SBC operands at $4A8E (column offset) and $4A98 (row offset) ; Called from: cmdMoveGroupRelative ($5020), the handler of command $81 ; ---------------------------------------------------------------------- moveGroupKeepingFormation: ldy cmdParam0 ; 4A52 Y = the leader unit named by the command lda unitDestColTable,y ; 4A54 leader's current destination column and #$7F ; 4A57 drop bit 7, the at-destination/idle marker kept in the same byte sec ; 4A59 borrow clear for a true subtract sbc cmdParam1 ; 4A5A column offset = leader destination column - ordered column sta L_4A8D+1 ; 4A5C patch the SBC operand at $4A8E: every member is shifted by this column offset lda unitDestRowTable,y ; 4A5F leader's current destination row and #$7F ; 4A62 drop the idle marker sec ; 4A64 borrow clear sbc cmdParam2 ; 4A65 row offset = leader destination row - ordered row sta L_4A97+1 ; 4A67 patch the SBC operand at $4A98 with the row offset lda unitFlagsTable,y ; 4A6A authoritative flag byte of the leader and #$3C ; 4A6D keep group number (bits 2-4) and side (bit 5) ora #$40 ; 4A6F add the in-a-group bit 6 so the value can be compared against flags & $7C sta cmdParam0 ; 4A71 cmdParam0 now carries the group key instead of the leader index lda #$63 ; 4A73 start the scan at unit 99, the last slot sta fillEndPtr ; 4A75 zp_4A is the loop counter over the 100 unit slots ; ---------------------------------------------------------------------- ; Walk all 100 unit slots from 99 down to 0 and shift every live member of the leader's group by the ; same offset. zp_4A holds the index; Y is reloaded from it because sub_34D5 is called inside the ; loop. ; ---------------------------------------------------------------------- L_4A77: ldy fillEndPtr ; 4A77 index of the unit being considered lda unitTypeTable,y ; 4A79 its type byte; bit 7 marks a dead or empty slot bmi L_4A9E ; 4A7C skip dead units lda unitFlagsTable,y ; 4A7E authoritative flags of this unit and #$7C ; 4A81 group number, side and the in-a-group bit cmp cmdParam0 ; 4A83 does it belong to the leader's group? bne L_4A9E ; 4A85 no: try the next slot lda unitDestColTable,y ; 4A87 member's current destination column and #$7F ; 4A8A without the idle marker sec ; 4A8C borrow clear L_4A8D: sbc #$FF ; 4A8D operand patched at $4A5C: subtract the group's column offset sta cmdParam1 ; 4A8F cmdParam1 = the member's new destination column lda unitDestRowTable,y ; 4A91 member's current destination row and #$7F ; 4A94 without the idle marker sec ; 4A96 borrow clear L_4A97: sbc #$FF ; 4A97 operand patched at $4A67: subtract the group's row offset sta cmdParam2 ; 4A99 cmdParam2 = the member's new destination row jsr setUnitDestination ; 4A9B apply it to unit Y (clamped to 0-39; the leader lands exactly on the ordered cell) L_4A9E: dec fillEndPtr ; 4A9E next lower unit index bpl L_4A77 ; 4AA0 loop until slot 0 has been handled rts ; 4AA2 back to the command dispatcher ; ---------------------------------------------------------------------- ; addSelectedUnitToGroup - The 'ADDED' half of the group membership editor. isPointWithinGroupRange ; rejects a unit that is more than 8 cells from the group's current bounding box - then 'TOO FAR!' is ; shown and C=1 returned. Otherwise the local flag copy is updated at once so the viewport highlights ; the unit immediately, and command $88 is queued so that both machines perform the join in lock step. ; In: selectedGroupKey ($9206) = group key (group number in bits 2-4, side in bit 5), selectedUnit ; ($90F8) ; Out: C=0 accepted: unitDisplayFlags[unit] = (old & $83) | key | $40 and 3 bytes ($88, unit, key) ; queued by queueCmd3 ($560F); C=1 rejected, with the TOO FAR! message and sound 3 ; Called from: the JOIN GROUP menu ($61C6) and the group membership editor ($6483) ; ---------------------------------------------------------------------- addSelectedUnitToGroup: lda selectedGroupKey ; 4AA3 group being edited (bits 2-4 group number, bit 5 side) ldy selectedUnit ; 4AA6 Y below $80 means: test this unit's map cell against the members' map cells jsr isPointWithinGroupRange ; 4AA9 refuse a unit further than 8 cells from the group's bounding box bcc L_4AB4 ; 4AAC C=0: it is close enough, add it jsr showTooFarMessage ; 4AAE C=1: beep and print 'TOO FAR!' in the info panel jmp L_4AD2 ; 4AB1 return with C=1, nothing changed L_4AB4: ldx selectedUnit ; 4AB4 the unit to add lda unitDisplayFlagsTable,x ; 4AB7 its local flag copy, the one the viewport and the editor read and #$83 ; 4ABA keep the two facing bits and bit 7 (at destination); drop any old group bits ora selectedGroupKey ; 4ABC insert the new group number and side ora #$40 ; 4ABF and the in-a-group bit sta unitDisplayFlagsTable,x ; 4AC1 instant local feedback; the authoritative $F708 only changes when command $88 executes stx cmdParam1 ; 4AC4 command argument 1 = unit index lda selectedGroupKey ; 4AC6 the group it joins sta cmdParam2 ; 4AC9 command argument 2 = group key lda #$88 ; 4ACB command $88 = join group (3 bytes: unit, group key) jsr queueCmd3 ; 4ACD queue it for both machines clc ; 4AD0 C=0 = added rts ; 4AD1 back to the editor L_4AD2: sec ; 4AD2 C=1 = rejected as too far away rts ; 4AD3 back to the editor ; ---------------------------------------------------------------------- ; joinUnitToGroup - Executor of command $88. It first gives the unit the group's current movement ; through followGroupMovement, then writes the group key and the member bit into the authoritative ; unitFlagsTable and mirrors the new bits into the display copy with syncLocalGroupFlags. Only the two ; facing bits survive the mask, so joining a group also clears the unit's pending first-strike bit 7, ; and the side bit comes from the command argument rather than from the unit. ; In: cmdParam1 ($74) = unit index, cmdParam2 ($75) = group key, both fetched by readCmdArgs2 in ; cmdJoinGroup ; Out: unitFlagsTable[unit] = (old & $03) | key | $40 and unitDisplayFlags[unit] resynced; the unit's ; destination is set to the group's average movement; self-modifies the LDX operand at $4AE2 and ; the ORA operand at $4AE9 ; Called from: cmdJoinGroup ($5198), the handler of command $88 ; ---------------------------------------------------------------------- joinUnitToGroup: ldy cmdParam1 ; 4AD4 unit index argument of command $88 sty L_4AE1+1 ; 4AD6 patch the LDX # operand at $4AE2 with it - followGroupMovement clobbers X and Y lda cmdParam2 ; 4AD9 group key argument sta L_4AE8+1 ; 4ADB patch the ORA # operand at $4AE9 with the group key jsr followGroupMovement ; 4ADE let the new member pick up the group's average movement (Y still holds the unit index) L_4AE1: ldx #$FF ; 4AE1 operand patched at $4AD6: the joining unit lda unitFlagsTable,x ; 4AE3 its authoritative flag byte and #$03 ; 4AE6 keep only the facing bits 0-1; the old group bits and the first-strike bit 7 are dropped L_4AE8: ora #$FF ; 4AE8 operand patched at $4ADB: group number and side from the command ora #$40 ; 4AEA mark the unit as being in a group sta unitFlagsTable,x ; 4AEC this is the copy both machines agree on jmp syncLocalGroupFlags ; 4AEF mirror the new group bits into the display copy $FB54 and return from there ; ---------------------------------------------------------------------- ; removeSelectedUnitFromGroup - The 'REMOVED' half of the membership editor. It clears the member bit ; 6 in the selected unit's local flag copy straight away so the viewport highlight updates on the next ; frame, and queues the 2-byte command $89, whose executor cmdLeaveGroup ($519B) clears the same bit ; in the authoritative unitFlagsTable on both machines and re-syncs the copy. ; In: selectedUnit ($90F8) ; Out: unitDisplayFlags[unit] &= $BF, cmdParam0 = unit, 2 bytes ($89, unit) queued by queueCmd2 ; ($561F) ; Called from: the group membership editor ($64B1) ; ---------------------------------------------------------------------- removeSelectedUnitFromGroup: ldy selectedUnit ; 4AF2 the unit the cursor is on lda unitDisplayFlagsTable,y ; 4AF5 its local flag copy and #$BF ; 4AF8 clear bit 6 so it no longer counts as a group member for the display sta unitDisplayFlagsTable,y ; 4AFA store it back for immediate feedback lda #$89 ; 4AFD command $89 = leave group (2 bytes: unit) sty cmdParam0 ; 4AFF command argument = unit index jmp queueCmd2 ; 4B01 queue it and return from queueCmd2 ; ---------------------------------------------------------------------- ; isPointWithinGroupRange - Range test that stops a group from spreading over more than 8 cells. It ; builds the group's bounding box with computeGroupBounds and then demands that the tested point be ; less than 9 cells from each of the four edges of that box (which also bounds the box once the point ; has been added). Y below $80 tests unit Y's map position against the members' positions - used when ; a unit joins; Y = $80 tests the cell picked with the target cursor against the members' destinations ; - used when a member is re-positioned. An empty group always passes. ; In: A = group key, Y = unit index or $80; pickedCellCol/pickedCellRow ($91D8/$91D9) when Y = $80 ; Out: C=0 within range, C=1 too far; scratch18-scratch1D clobbered (bounding box and mode); ; self-modifies the LDY operand at $4B0F ; Called from: addSelectedUnitToGroup ($4AA9) and orderGroupNewPosition ($4BEE) ; ---------------------------------------------------------------------- isPointWithinGroupRange: sty L_4B0E+1 ; 4B04 patch the LDY # at $4B0F with the mode: a unit index, or $80 = use the picked cell jsr computeGroupBounds ; 4B07 A and Y are still the arguments: build the group's bounding box (positions or destinations) lda scratch1A ; 4B0A number of members found beq L_4B5A ; 4B0C an empty group has no box - accept anything L_4B0E: ldy #$FF ; 4B0E operand patched at $4B04 bpl L_4B1B ; 4B10 index below $80: measure the unit's own cell ldx pickedCellCol ; 4B12 $80: measure the cell picked with the target cursor ldy pickedCellRow ; 4B15 its row jmp L_4B22 ; 4B18 on to the four edge tests L_4B1B: ldx unitColTable,y ; 4B1B X = column of the unit being tested lda unitRowTable,y ; 4B1E its row tay ; 4B21 Y = row of the unit being tested ; ---------------------------------------------------------------------- ; Four edge tests, each |point - edge| < 9: right edge, left edge, bottom edge, top edge of the ; bounding box. Any distance of 9 or more fails the whole test. ; ---------------------------------------------------------------------- L_4B22: txa ; 4B22 column of the tested point sec ; 4B23 borrow clear sbc scratch1B ; 4B24 distance to the rightmost member bcs L_4B2C ; 4B26 already positive eor #$FF ; 4B28 otherwise negate to get the absolute value adc #$01 ; 4B2A (two's complement: EOR $FF then +1) L_4B2C: cmp #$09 ; 4B2C a group may never span more than 8 cells bcs L_4B5C ; 4B2E 9 or more away from the right edge: too far txa ; 4B30 column of the tested point again sec ; 4B31 borrow clear sbc scratch18 ; 4B32 distance to the leftmost member bcs L_4B3A ; 4B34 already positive eor #$FF ; 4B36 negate adc #$01 ; 4B38 +1 completes the two's complement L_4B3A: cmp #$09 ; 4B3A same 8-cell limit bcs L_4B5C ; 4B3C too far to the right of the left edge tya ; 4B3E row of the tested point sec ; 4B3F borrow clear sbc scratch1C ; 4B40 distance to the bottom-most member bcs L_4B48 ; 4B42 already positive eor #$FF ; 4B44 negate adc #$01 ; 4B46 +1 L_4B48: cmp #$09 ; 4B48 8-cell limit bcs L_4B5C ; 4B4A too far below the bottom edge tya ; 4B4C row of the tested point again sec ; 4B4D borrow clear sbc scratch19 ; 4B4E distance to the top-most member bcs L_4B56 ; 4B50 already positive eor #$FF ; 4B52 negate adc #$01 ; 4B54 +1 L_4B56: cmp #$09 ; 4B56 8-cell limit bcs L_4B5C ; 4B58 too far above the top edge L_4B5A: clc ; 4B5A C=0: the point is within 8 cells of all four edges rts ; 4B5B accepted L_4B5C: sec ; 4B5C C=1: too far from the group rts ; 4B5D rejected ; ---------------------------------------------------------------------- ; computeGroupBounds - Scans all 100 unit slots and accumulates the member count and the bounding box ; of one group. Membership is tested on the local flag copy unitDisplayFlagsTable ((flags & $7C) == ; key | $40) and dead slots are skipped. Bit 7 of the mode byte (the Y register on entry) selects ; whether the box is built from the members' current map cells or from their ordered destinations. An ; empty group leaves min = $FF and max = 0. ; In: A = group key (group number and side), Y = mode (bit 7 set = use destinations); unitTypeTable, ; unitDisplayFlagsTable, unitColTable/unitRowTable or unitDestColTable/unitDestRowTable ; Out: A = scratch1A = member count, X = scratch18 = min column, Y = scratch19 = min row, scratch1B = ; max column, scratch1C = max row, scratch1D = the mode byte; self-modifies the CMP operand at ; $4B80 ; Called from: isPointWithinGroupRange ($4B07) and buildGroupFormationSprite ($30C5), which draws the ; group's formation ghost from the destination box ; ---------------------------------------------------------------------- computeGroupBounds: ora #$40 ; 4B5E add the in-a-group bit to the group key sta L_4B7F+1 ; 4B60 patch the CMP # at $4B80 with the key every member must match sty scratch1D ; 4B63 remember the mode: bit 7 set = measure destinations, clear = measure positions lda #$00 ; 4B65 zero sta scratch1A ; 4B67 member count = 0 sta scratch1B ; 4B69 max column = 0 (only a member can raise it) sta scratch1C ; 4B6B max row = 0 lda #$FF ; 4B6D $FF sta scratch18 ; 4B6F min column = $FF so the first member always lowers it sta scratch19 ; 4B71 min row = $FF ldx #$63 ; 4B73 scan unit slots 99 down to 0 ; ---------------------------------------------------------------------- ; One pass over the 100 unit slots: count the members of the group and grow the bounding box around ; either their positions or their destinations. ; ---------------------------------------------------------------------- L_4B75: lda unitTypeTable,x ; 4B75 type byte of this slot bmi L_4BB5 ; 4B78 bit 7 = dead or empty slot, skip it lda unitDisplayFlagsTable,x ; 4B7A the UI copy of the flags - this is what the membership editor manipulates and #$7C ; 4B7D group number, side and the in-a-group bit L_4B7F: cmp #$FF ; 4B7F operand patched at $4B60: is this unit a member of the group? bne L_4BB5 ; 4B81 no: next slot inc scratch1A ; 4B83 count the member lda unitColTable,x ; 4B85 its map column bit scratch1D ; 4B88 test bit 7 of the mode byte bpl L_4B91 ; 4B8A positions requested: keep the map column lda unitDestColTable,x ; 4B8C destinations requested: use the ordered column instead and #$7F ; 4B8F without the at-destination marker in bit 7 L_4B91: cmp scratch1B ; 4B91 beyond the rightmost column so far? bcc L_4B97 ; 4B93 no sta scratch1B ; 4B95 grow the box to the right L_4B97: cmp scratch18 ; 4B97 left of the leftmost column so far? bcs L_4B9D ; 4B99 no sta scratch18 ; 4B9B grow the box to the left L_4B9D: lda unitRowTable,x ; 4B9D its map row bit scratch1D ; 4BA0 test the mode byte again bpl L_4BA9 ; 4BA2 positions requested lda unitDestRowTable,x ; 4BA4 destinations requested: use the ordered row and #$7F ; 4BA7 without the at-destination marker L_4BA9: cmp scratch1C ; 4BA9 below the bottom-most row so far? bcc L_4BAF ; 4BAB no sta scratch1C ; 4BAD grow the box downwards L_4BAF: cmp scratch19 ; 4BAF above the top-most row so far? bcs L_4BB5 ; 4BB1 no sta scratch19 ; 4BB3 grow the box upwards L_4BB5: dex ; 4BB5 next lower slot bpl L_4B75 ; 4BB6 loop until slot 0 has been examined lda scratch1A ; 4BB8 return the member count in A (Z=1 for an empty group) ldx scratch18 ; 4BBA X = minimum column of the box ldy scratch19 ; 4BBC Y = minimum row of the box rts ; 4BBE the maxima stay in scratch1B/scratch1C ; ---------------------------------------------------------------------- ; showTooFarMessage - Error feedback shared by the two group range checks: selects and clears the ; 14-column info panel (screen columns 25-38, rows 12-21), plays sound 3 (the rejection beep) and ; prints 'TOO FAR!' from the $C000 text overlay on screen row 16, in whatever colour the caller left ; in textColour. ; In: textColour (zp_3D) and textFillGlyph (zp_3E) as set by the caller ; Out: info panel window selected and blanked, sound 3 queued, the message printed (tail call into ; printString) ; Called from: addSelectedUnitToGroup ($4AAE) and orderGroupNewPosition ($4BF3) ; ---------------------------------------------------------------------- showTooFarMessage: jsr clearInfoPanel ; 4BBF select the info panel window (columns 25-38, rows 12-21) and blank it lda #$03 ; 4BC2 sound 3 = the short rejection beep jsr playSound ; 4BC4 queue it with the sound driver lda #$10 ; 4BC7 screen row 16, in the middle of the info panel jsr setCursorRow ; 4BC9 move the text cursor there lda #$31 ; 4BCC low byte of the string address ldy #$CB ; 4BCE $CB31 = ' TOO FAR!' in the text overlay jmp printString ; 4BD0 print it and return from printString ; ---------------------------------------------------------------------- ; orderGroupNewPosition - NEW POSITION action of the custom-formation editor. It highlights just the ; selected unit, redraws the viewport and runs the target cursor loop, which in this mode ; (cursorTargetMode negative) only reports the picked cell in pickedCellCol/pickedCellRow. If the pick ; was not aborted and lies within 8 cells of the group's destination box it queues command $98 ; unit,column,row, refreshes the local at-destination flag, counts the order in the pending-order pair ; $929D/$929E and prints 'RE-POSITIONED'; otherwise it shows TOO FAR! and re-centres the viewport on ; the unit. It always ends by restoring the editor's cursor state, because the cursor loop zeroes ; cursorTargetMode when the pick is aborted. ; In: selectedUnit ($90F8), selectedGroupKey ($9206), pickedCellCol/pickedCellRow ($91D8/$91D9) ; filled by runTargetCursorLoop, checkUiContinue ($C451) ; Out: command $98 queued (queueCmd4) or a message shown; highlightedUnit/highlightedGroupKey, ; lastOrderedGroupKey/pendingGroupOrderCount, cursorTargetMode = $80, textColour/textPadColour = ; 4; self-modifies the string pointer operands at $4C46/$4C48 ; Called from: the group membership/formation editor ($64AB) ; ---------------------------------------------------------------------- orderGroupNewPosition: lda #$FF ; 4BD3 $FF = no group highlighted ... sta highlightedGroupKey ; 4BD5 ... so only the single selected unit is drawn highlighted lda selectedUnit ; 4BD8 the unit being re-positioned sta highlightedUnit ; 4BDB highlight it in the viewport jsr drawViewport ; 4BDE redraw the 7x5 tactical view with the new highlight jsr runTargetCursorLoop ; 4BE1 run the target cursor; cursorTargetMode is negative here, so it only reports the picked cell in $91D8/$91D9 and returns jsr checkUiContinue ; 4BE4 may the modal UI continue? (0 = a screen change, SPACE or the end of the game aborted it) beq L_4BF9 ; 4BE7 aborted: skip the order and just restore the cursor ldy #$80 ; 4BE9 $80 = compare the picked cell with the members' destinations, not their positions lda selectedGroupKey ; 4BEB the group being edited jsr isPointWithinGroupRange ; 4BEE is the picked cell within 8 cells of the group's destination box? bcc L_4BFC ; 4BF1 C=0: accepted jsr showTooFarMessage ; 4BF3 C=1: beep and print 'TOO FAR!' jsr recenterViewportOnUnit ; 4BF6 scroll the viewport back so the unit is under the cursor again L_4BF9: jmp L_4C4F ; 4BF9 join the common tail ; ---------------------------------------------------------------------- ; The pick was accepted: send command $98 (move keeping group membership) for this unit. ; ---------------------------------------------------------------------- L_4BFC: lda pickedCellCol ; 4BFC column picked with the cursor sta cmdParam1 ; 4BFF command argument 2 = column lda pickedCellRow ; 4C01 row picked with the cursor sta cmdParam2 ; 4C04 command argument 3 = row ldy selectedUnit ; 4C06 the unit being re-positioned sty cmdParam0 ; 4C09 command argument 1 = unit index lda #$98 ; 4C0B command $98 = set destination without leaving the group (4 bytes) jsr queueCmd4 ; 4C0D queue it for both machines jsr updateUnitAtDestinationFlag; 4C10 local feedback: refresh bit 7 of unitDisplayFlags[unit] (at destination or not) ; ---------------------------------------------------------------------- ; Count how many re-position orders are outstanding for this group. The editor at $65E7 waits for ; pendingGroupOrderCount to come back to zero (each executed $98 decrements it in ; setGroupMemberDestination) before it lets the whole group be given a new goal. ; ---------------------------------------------------------------------- ldy cmdParam0 ; 4C13 the unit just ordered lda unitDisplayFlagsTable,y ; 4C15 its local flag copy and #$3C ; 4C18 group number and side, without the member bit cmp lastOrderedGroupKey ; 4C1A same group as the previous NEW POSITION order? bne L_4C25 ; 4C1D no: start a new count inc pendingGroupOrderCount ; 4C1F yes: one more order outstanding for this group jmp L_4C2D ; 4C22 on to the message L_4C25: sta lastOrderedGroupKey ; 4C25 remember the group these orders belong to lda #$01 ; 4C28 this is the first outstanding order ... sta pendingGroupOrderCount ; 4C2A ... for the new group ; ---------------------------------------------------------------------- ; Confirm the order with a centred 'RE-POSITIONED' in the info panel. ; ---------------------------------------------------------------------- L_4C2D: lda #$99 ; 4C2D low byte of $CB99 = 'RE-POSITIONED' ldy #$CB ; 4C2F high byte sta L_4C45+1 ; 4C31 patch the LDA # operand at $4C46 with the string pointer low byte sty L_4C47+1 ; 4C34 patch the LDY # operand at $4C48 with the high byte lda #$04 ; 4C37 colour 4 (purple) for the confirmation sta textColour ; 4C39 text colour sta textPadColour ; 4C3B and the colour of the blanks that pad the centred line jsr clearInfoPanel ; 4C3D blank the info panel lda #$10 ; 4C40 screen row 16 jsr setCursorRow ; 4C42 move the cursor there L_4C45: lda #$FF ; 4C45 operand patched at $4C31 L_4C47: ldy #$FF ; 4C47 operand patched at $4C34 jsr printStringCentred ; 4C49 print the string centred in the 14-column panel jsr recenterViewportOnUnit ; 4C4C scroll the viewport back onto the unit ; ---------------------------------------------------------------------- ; Common tail: the target cursor loop zeroes cursorTargetMode when the pick was aborted, so the ; editor's 'free cell pick' mode and its two cursor sprites are set up again here. ; ---------------------------------------------------------------------- L_4C4F: lda #$80 ; 4C4F $80 = free cell pick mode sta cursorTargetMode ; 4C51 restore it for the next round of the editor jsr setCursorShapeSquare ; 4C53 sprite 0 = the hollow square cell cursor jmp showCursorBoxSprite ; 4C56 sprite 2 = the centre box of the viewport, and return from there ; ---------------------------------------------------------------------- ; setGroupMemberDestination - Executor of command $98. If the unit's group key matches the group ; remembered in lastOrderedGroupKey it decrements pendingGroupOrderCount - this is the acknowledgement ; the formation editor waits for - and then sets the unit's destination to the ordered cell through ; sub_34D5. Unlike command $80 it never touches the group bits, so the re-positioned unit stays a ; member of its group. ; In: cmdParam0 ($73) = unit index, cmdParam1/cmdParam2 = column/row (already mirrored for a remote ; command), lastOrderedGroupKey ($929D) ; Out: unitDestColTable/unitDestRowTable of the unit written via sub_34D5; pendingGroupOrderCount ; ($929E) decremented when the group matches ; Called from: cmdMoveKeepGroup ($5476), the handler of command $98 ; ---------------------------------------------------------------------- setGroupMemberDestination: ldy cmdParam0 ; 4C59 the unit named by the command lda unitDisplayFlagsTable,y ; 4C5B its local flag copy and #$3C ; 4C5E group number and side cmp lastOrderedGroupKey ; 4C60 is it the group the local editor is waiting for? bne L_4C68 ; 4C63 no: this order was not counted dec pendingGroupOrderCount ; 4C65 one of the outstanding re-position orders has now been executed L_4C68: jmp setUnitDestination ; 4C68 set the destination (clamped, at-destination bit maintained) and return from there; the group bits are left alone ; ---------------------------------------------------------------------- ; syncLocalGroupFlags - Copies the authoritative group bits of unit X (unitFlagsTable & $7C: group ; number, side and member bit) into the local flag copy unitDisplayFlagsTable, keeping that byte's own ; bits 0, 1 and 7. Called after every command that changes group membership and when a unit is created ; or reset, so the UI copy can never drift away from the simulation. ; In: X = unit index ; Out: unitDisplayFlags[X] = (old & $83) | (unitFlagsTable[X] & $7C); self-modifies the ORA operand at ; $4C79 ; Called from: the move executor ($34D0), unit creation ($3DFA), joinUnitToGroup ($4AEF) and ; cmdLeaveGroup ($51A8) ; ---------------------------------------------------------------------- syncLocalGroupFlags: lda unitFlagsTable,x ; 4C6B authoritative flag byte of the unit and #$7C ; 4C6E group number (bits 2-4), side (bit 5) and the in-a-group bit 6 sta L_4C78+1 ; 4C70 patch the ORA # operand at $4C79 with those bits lda unitDisplayFlagsTable,x ; 4C73 the local copy the display reads and #$83 ; 4C76 keep its facing bits 0-1 and the at-destination bit 7 L_4C78: ora #$FF ; 4C78 operand patched at $4C70: the group bits from the simulation sta unitDisplayFlagsTable,x ; 4C7A store the merged byte rts ; 4C7D back to the caller ; formationRotateRules - two 3-byte rule sets that turn the FORMATION menu item into a coordinate ; transform for rotateGroupFormation. Index = the rotation mode carried by command $A3 (0 SWEEP LEFT, ; 1 SWEEP RIGHT, 2 REVERSE). $4C7E[mode] describes what happens to the member's column offset dx, ; $4C81[mode] what happens to its row offset dy: bit 7 = negate the component, bit 0 = store it as the ; new row offset ($75) instead of the new column offset ($74). The three modes are therefore (dy,-dx) ; = 90 degrees anticlockwise, (-dy,dx) = 90 degrees clockwise and (-dx,-dy) = 180 degrees. formationRotateRules: .byte $81,$01,$80 ; 4C7E ... rule for the column offset: mode 0 negate+store as row, mode 1 store as row, mode 2 negate+store as column formationRotateRulesDy: .byte $00,$80,$81 ; 4C81 ... rule for the row offset: mode 0 store as column, mode 1 negate+store as column, mode 2 negate+store as row ; ---------------------------------------------------------------------- ; rotateGroupFormation - Executor of command $A3 - the SWEEP LEFT / SWEEP RIGHT / REVERSE items of the ; FORMATION menu. It rotates the whole formation about the leader's destination cell: for every live ; member of the leader's group the offset (dx,dy) from that cell is transformed with the two rule ; bytes of formationRotateRules and the new destination leader+offset is applied with sub_34D5. During ; the setup phase the resulting row is clamped into the issuing player's own half of the map, so a ; rotation can never push units across the middle line. ; In: cmdParam1 ($74) = leader unit index, cmdParam2 ($75) = rotation mode 0-2 (both from ; readCmdArgs2), cmdFromOpponent ($76) bit 7, gamePhase (zp_B1) ; Out: unitDestColTable/unitDestRowTable of every member rewritten through sub_34D5; ; cmdParam1/cmdParam2 are reused as the working destination; ten self-modified immediates: $4CC4 ; loop index, $4CD0 group key, $4CDA/$4CFE leader column, $4CDC/$4CE5 column rule, $4CEF/$4D05 ; leader row, $4CF1/$4CFA row rule ; Called from: cmdSetFormation ($5599), the handler of command $A3 ; ---------------------------------------------------------------------- rotateGroupFormation: ldy cmdParam1 ; 4C84 the leader unit named by the command lda unitDestColTable,y ; 4C86 leader's destination column - the formation turns about this cell and #$7F ; 4C89 drop the at-destination marker in bit 7 sta L_4CD9+1 ; 4C8B patch the SBC at $4CDA: leader column, used to get each member's dx sta L_4CFD+1 ; 4C8E patch the LDA # at $4CFE: same value, used to rebuild the absolute column lda unitDestRowTable,y ; 4C91 leader's destination row and #$7F ; 4C94 drop the at-destination marker sta L_4CEE+1 ; 4C96 patch the SBC at $4CEF: leader row, used to get each member's dy sta L_4D04+1 ; 4C99 patch the LDA # at $4D05: same value for the absolute row lda unitFlagsTable,y ; 4C9C authoritative flags of the leader and #$3C ; 4C9F group number (bits 2-4) and side (bit 5) ora #$40 ; 4CA1 add the in-a-group bit so it can be compared with flags & $7C sta L_4CCF+1 ; 4CA3 patch the CMP # at $4CD0 with the group key ldy cmdParam2 ; 4CA6 rotation mode 0-2 (0 SWEEP LEFT, 1 SWEEP RIGHT, 2 REVERSE) lda formationRotateRules,y ; 4CA8 rule byte for the column offset sta L_4CDB+1 ; 4CAB patch the LDX # at $4CDC: bit 7 of the rule decides whether dx is negated and #$01 ; 4CAE bit 0 of the rule = which parameter byte the transformed value goes to sta L_4CE4+1 ; 4CB0 patch the LDX # at $4CE5: 0 = store as column, 1 = store as row lda formationRotateRulesDy,y; 4CB3 rule byte for the row offset sta L_4CF0+1 ; 4CB6 patch the LDX # at $4CF1: bit 7 = negate dy and #$01 ; 4CB9 bit 0 of that rule sta L_4CF9+1 ; 4CBB patch the LDX # at $4CFA: destination parameter for the transformed dy ldy #$63 ; 4CBE start at unit slot 99 sty L_4CC3+1 ; 4CC0 the loop index lives in the LDY # operand at $4CC4 (Y is needed by sub_34D5) ; ---------------------------------------------------------------------- ; Walk all 100 unit slots. For every live member of the leader's group: take its offset from the ; leader's destination, run the offset through the rotation rules, add it back to the leader's ; destination and order the member to the resulting cell. ; ---------------------------------------------------------------------- L_4CC3: ldy #$FF ; 4CC3 operand patched at $4CC0 and decremented at $4D24: the unit being considered lda unitTypeTable,y ; 4CC5 its type byte bmi L_4D24 ; 4CC8 bit 7 = dead or empty slot, skip it lda unitFlagsTable,y ; 4CCA authoritative flags and #$7C ; 4CCD group number, side and member bit L_4CCF: cmp #$FF ; 4CCF operand patched at $4CA3: a member of the leader's group? bne L_4D24 ; 4CD1 no: next slot lda unitDestColTable,y ; 4CD3 member's destination column and #$7F ; 4CD6 without the at-destination marker sec ; 4CD8 borrow clear L_4CD9: sbc #$FF ; 4CD9 operand patched at $4C8B: dx = member column - leader column L_4CDB: ldx #$FF ; 4CDB operand patched at $4CAB: only the N flag of the rule byte matters here bpl L_4CE4 ; 4CDD rule bit 7 clear: keep dx as it is eor #$FF ; 4CDF rule bit 7 set: negate dx ... clc ; 4CE1 ... carry clear ... adc #$01 ; 4CE2 ... +1 completes the two's complement L_4CE4: ldx #$FF ; 4CE4 operand patched at $4CB0: 0 = the transformed dx becomes the new column offset, 1 = the new row offset sta cmdParam1,x ; 4CE6 store it into cmdParam1 ($74) or cmdParam2 ($75) lda unitDestRowTable,y ; 4CE8 member's destination row and #$7F ; 4CEB without the at-destination marker sec ; 4CED borrow clear L_4CEE: sbc #$FF ; 4CEE operand patched at $4C96: dy = member row - leader row L_4CF0: ldx #$FF ; 4CF0 operand patched at $4CB6: N flag of the row rule bpl L_4CF9 ; 4CF2 rule bit 7 clear: keep dy eor #$FF ; 4CF4 negate dy ... clc ; 4CF6 ... carry clear ... adc #$01 ; 4CF7 ... +1 L_4CF9: ldx #$FF ; 4CF9 operand patched at $4CBB: which parameter byte receives the transformed dy sta cmdParam1,x ; 4CFB store it into cmdParam1 or cmdParam2 L_4CFD: lda #$FF ; 4CFD operand patched at $4C8E: leader's destination column clc ; 4CFF plain add adc cmdParam1 ; 4D00 absolute column = leader column + transformed column offset sta cmdParam1 ; 4D02 cmdParam1 = the member's new destination column L_4D04: lda #$FF ; 4D04 operand patched at $4C99: leader's destination row clc ; 4D06 plain add adc cmdParam2 ; 4D07 absolute row = leader row + transformed row offset ; ---------------------------------------------------------------------- ; During the setup phase each side may only place units in its own half of the map, so a rotation that ; would carry a unit across the middle line (row 20) is clamped. The local player always owns rows ; 20-39 of his own view, the opponent rows 0-19. ; ---------------------------------------------------------------------- ldx gamePhase ; 4D09 0 = the battle is running, non-zero = setup/menu phase beq L_4D1F ; 4D0B in battle a rotation may use the whole map ldx cmdFromOpponent ; 4D0D did this command come from the opponent? bpl L_4D19 ; 4D0F no: it is our own order cmp #$14 ; 4D11 remote order: rows must stay in the top half (0-19) bcc L_4D1F ; 4D13 already there lda #$13 ; 4D15 clamp to row 19, the last row of the opponent's half bne L_4D1F ; 4D17 always taken L_4D19: cmp #$14 ; 4D19 own order: rows must stay in the bottom half (20-39) bcs L_4D1F ; 4D1B already there lda #$14 ; 4D1D clamp to row 20, the first row of our own half L_4D1F: sta cmdParam2 ; 4D1F cmdParam2 = the member's new destination row jsr setUnitDestination ; 4D21 order unit Y to that cell (clamped to the map, at-destination bit maintained) L_4D24: dec L_4CC3+1 ; 4D24 next lower unit slot, straight in the LDY operand bpl L_4CC3 ; 4D27 loop until slot 0 has been handled rts ; 4D29 back to the command dispatcher ; commandHandlerTable - 39 little-endian handler addresses, one per command byte $80-$A6. The ; dispatcher in executeTurnCommands indexes it with (command & $3F) * 2 and copies the entry into the ; operand of the JSR at $4EEE. The $8D entry is $FFFF (rendered as irqVectorHi) because that command ; is consumed by the packet mergers and never reaches the dispatcher. Handlers marked 'immediate' ; below are likewise executed while the packets are merged. commandHandlerTable: D_4D2B = commandHandlerTable+1 .addr cmdMoveUnit ; 4D2A $80 MOVE unit (4 bytes: unit, column, row) .byte $1A ; 4D2C $81 move group keeping formation (4 bytes) .byte $50 ; 4D2D P .byte $23 ; 4D2E $82 cloak on, single unit (2 bytes) .byte $50 ; 4D2F P .byte $98 ; 4D30 $83 cloak on, whole group (2 bytes) .byte $50 ; 4D31 P .byte $4B ; 4D32 $84 cloak off, single unit (2 bytes) .byte $50 ; 4D33 P .byte $D2 ; 4D34 $85 cloak off, whole group (2 bytes) .byte $50 ; 4D35 P .byte $5D ; 4D36 $86 clear a boomer's target (2 bytes) .byte $51 ; 4D37 Q .byte $6B ; 4D38 $87 set a boomer's target (3 bytes) .byte $51 ; 4D39 Q .byte $85 ; 4D3A $88 join group (3 bytes: unit, group key) .byte $51 ; 4D3B Q .byte $9B ; 4D3C $89 leave group (2 bytes: unit) .byte $51 ; 4D3D Q .byte $AB ; 4D3E $8A place unit at a cell (4 bytes) - the setup phase .byte $51 ; 4D3F Q .byte $EE ; 4D40 $8B set the map seed (4 bytes: 3 seed bytes) .byte $51 ; 4D41 Q .byte $3E ; 4D42 $8C session control: abort, pause, resume, disconnect (2 bytes: reason) .byte $52 ; 4D43 R .byte $FF ; 4D44 $8D drone position report - never dispatched: the mergers run it at once, so the slot holds $FFFF .byte $FF ; 4D45 . .byte $36 ; 4D46 $8E detonate the drone (3 bytes: column, row) .byte $53 ; 4D47 S .byte $7B ; 4D48 $8F release drone control (1 byte) .byte $53 ; 4D49 S .byte $C0 ; 4D4A $90 comcen missile strike (3 bytes: column, row) .byte $53 ; 4D4B S .byte $2E ; 4D4C $91 self destruct, unit or its whole group (2 bytes) .byte $54 ; 4D4D T .byte $8B ; 4D4E $92 the drone reached its target (1 byte) .byte $53 ; 4D4F S .byte $70 ; 4D50 $93 blitz on, single unit (2 bytes) .byte $50 ; 4D51 P .byte $AA ; 4D52 $94 blitz on, whole group (2 bytes) .byte $50 ; 4D53 P .byte $62 ; 4D54 $95 blitz off, single unit (2 bytes) .byte $50 ; 4D55 P .byte $BE ; 4D56 $96 blitz off, whole group (2 bytes) .byte $50 ; 4D57 P .byte $A3 ; 4D58 $97 drone launched (1 byte) .byte $53 ; 4D59 S .byte $70 ; 4D5A $98 move but keep group membership (4 bytes) .byte $54 ; 4D5B T .byte $B1 ; 4D5C $99 menu-level link poll (1 byte) - the handler is the link keep-alive itself .byte $56 ; 4D5D V .byte $79 ; 4D5E $9A add energy to a unit (3 bytes: unit, amount) .byte $54 ; 4D5F T .byte $9E ; 4D60 $9B halt every member of a group (2 bytes) .byte $54 ; 4D61 T .byte $E1 ; 4D62 $9C set the recycler mode and cell (4 bytes) .byte $54 ; 4D63 T .byte $11 ; 4D64 $9D choose a side (2 bytes) .byte $55 ; 4D65 U .byte $0C ; 4D66 $9E comcen repaired / setup finished (1 byte) .byte $52 ; 4D67 R .byte $48 ; 4D68 $9F dig in, single unit (2 bytes) .byte $55 ; 4D69 U .byte $4C ; 4D6A $A0 dig out, single unit (2 bytes) .byte $55 ; 4D6B U .byte $6D ; 4D6C $A1 dig in, whole group (2 bytes) .byte $55 ; 4D6D U .byte $71 ; 4D6E $A2 dig out, whole group (2 bytes) .byte $55 ; 4D6F U .byte $96 ; 4D70 $A3 rotate the formation (3 bytes: leader, mode 0-2) .byte $55 ; 4D71 U .byte $9C ; 4D72 $A4 set the game type / option byte (2 bytes) .byte $55 ; 4D73 U .byte $A5 ; 4D74 $A5 set one setup option (3 bytes: kind, value) .byte $55 ; 4D75 U .byte $EA ; 4D76 $A6 set a custom map seed (4 bytes) - $51EA, rendered as raw bytes because the table was typed one entry short .byte $51 ; 4D77 Q ; commandLengthTable - total length in bytes (the command byte included) of each command ; $80-$A6, indexed by command & $3F. getCommandLength is the only reader; the packet mergers, ; the dispatcher, the outgoing packet builder and the comm module all use it to step over a command ; they do not want to look inside. commandLengthTable: .byte $04,$04,$02,$02,$02,$02,$02,$03; 4D78 ........ $80-$87: move 4, group move 4, cloak on/off and group cloak on/off 2 each, clear target 2, set target 3 .byte $03 ; 4D80 $88-$8F: join group 3, leave group 2, place unit 4, map seed 4, session control 2, drone report 4, detonate 3, release drone 1 .byte $02,$04,$04,$02,$04,$03,$01; 4D81 ....... .byte $03 ; 4D88 $90-$97: missile 3, self destruct 2, drone arrived 1, blitz on 2, group blitz on 2, blitz off 2, group blitz off 2, drone launched 1 .byte $02,$01,$02,$02,$02,$02,$01; 4D89 ....... .byte $04 ; 4D90 $98-$9F: move keeping group 4, menu poll 1, add energy 3, halt group 2, recycler 4, choose side 2, setup ready 1, dig in 2 .byte $01,$03,$02,$04,$02,$01,$02; 4D91 ....... .byte $02 ; 4D98 $A0-$A6: dig out 2, group dig in 2, group dig out 2, formation 3, game type 2, setup option 3, custom map seed 4 .byte $02,$02,$03,$02,$03,$04 ; 4D99 ...... ; ---------------------------------------------------------------------- ; getCommandLength - Looks the total length (command byte plus its parameters) of command A up in ; commandLengthTable and leaves it in currentCommandLength as well as in A. ; In: A = command byte $80-$A6 (bit 7 is ignored) ; Out: A = currentCommandLength ($92EE) = 1..4, X = the table index (command & $3F) ; Called from: the dispatcher ($4ECF), advancePacketCursor ($4F5F), the outgoing packet builder ; ($565C) and the solo trainer ($EA4C, $EA92) ; ---------------------------------------------------------------------- getCommandLength: and #$3F ; 4D9F strip bit 7: commands $80-$A6 become indices $00-$26 tax ; 4DA1 index the length table with it lda commandLengthTable,x ; 4DA2 1, 2, 3 or 4 bytes sta currentCommandLength ; 4DA5 the mergers and the dispatcher use this to skip to the next command rts ; 4DA8 length also returned in A ; ---------------------------------------------------------------------- ; serviceCommandExchange - Per-frame entry point of the lock-step command exchange, called from the ; main loop of every screen and from the overlays. It services the status-message system and then, ; unless the 10-frame throttle timer linkPollDelay is still running, falls into ; processCommandExchange. During film playback the throttle is ignored so records are consumed as fast ; as their time stamps allow. ; In: filmPlaybackMode ($0B9B) bit 7, linkPollDelay ($9163) counted down by the raster IRQ ; Out: as processCommandExchange, plus the status line refreshed ; Called from: the map/battlefield/console loops ($1AB0, $21B2, $600D), waitForExchangeIdle ($5915), ; the map generator overlay and the text engine ($C76E) ; ---------------------------------------------------------------------- serviceCommandExchange: jsr updateStatusMessage ; 4DA9 let the status line show/expire the current message bit filmPlaybackMode ; 4DAC is a game film being played back? bmi processCommandExchange ; 4DAF yes: never throttle, the film's own time stamps do the pacing ldy linkPollDelay ; 4DB1 frames still to wait before the next packet exchange beq processCommandExchange ; 4DB4 timer expired: try to exchange rts ; 4DB6 still throttled, come back next frame ; ---------------------------------------------------------------------- ; processCommandExchange - The heart of the lock-step protocol. During film playback it reads records ; from the film buffer whose time stamp (exchange slot commandSliceIndex plus the game clock) has ; come, copies their command bytes into the merged command buffer and executes them; a record header ; of $FE stops the film where it is and $FF runs the end-of-film handler. In live play it waits for ; the comm module to report a completed exchange ($E01D bit 6), then merges the packet just sent and ; the packet just received into one buffer - always with player 0's commands first, so both machines ; execute the same sequence - and falls into executeTurnCommands. ; In: filmPlaybackMode ($0B9B), linkSessionState ($0B9C), gameEndReason ($0BA8), playerSide ($0B9F), ; the comm module's $E01D/$E03B, film pointer zp_BC/zp_BD, commandSliceIndex ($923C), gameClock ; ($91CB) ; Out: mergedCmdBuffer filled and executed; ; mergedCmdLength/mergedCmdSplitIndex/linkPollDelay/linkTimeoutCounter/mergeFillIndex set; ; self-modifies $4DE9 (a copy of the time-stamp byte), $4DF4 (the previous record's clock) and ; $4E20 (offset of the first command byte in the record) ; Called from: serviceCommandExchange (fall-through and branches) and updateGameFrame ($41EE), which ; calls it once every ten units of the round ; ---------------------------------------------------------------------- processCommandExchange: lda filmPlaybackMode ; 4DB7 bit 7 = a game film is being replayed bpl L_4E39 ; 4DBA clear: this is a live game, use the link ; ---------------------------------------------------------------------- ; Film playback. Records are consumed from the film buffer until one is found whose time stamp is ; still in the future; several records may share one time stamp, so this is a loop. ; ---------------------------------------------------------------------- L_4DBC: lda gameEndReason ; 4DBC 0 while the game runs bne L_4DC9 ; 4DBF the game has ended: stop feeding records, only keep the link alive jsr compareFilmPtrToEnd ; 4DC1 has the film pointer reached the recorded end? bcc L_4DD2 ; 4DC4 no: there is another record to look at L_4DC6: jsr finishFilmPlayback ; 4DC6 end of the buffer: restore the final scores and clock and start the end sequence ; ---------------------------------------------------------------------- ; Nothing to execute this frame. If a link session exists (a film can be watched while the modem link ; is still up) give the menu-level packet handler a turn. ; ---------------------------------------------------------------------- L_4DC9: lda linkSessionState ; 4DC9 0 = no link, $80 = link up at menu level, 2 = game session beq L_4DD1 ; 4DCC no link at all: just return jmp pollMenuSession ; 4DCE service the menu-level link (carrier check plus keep-alive) and return from there L_4DD1: rts ; 4DD1 nothing to do this frame ; ---------------------------------------------------------------------- ; Read the next film record. Layout: [ splitIndex<<4 | length ] [ 1 or 2 time-stamp bytes ] [ length ; command bytes ]. The time-stamp byte holds the exchange slot within the round in its low nibble and ; the negated clock delta in its high nibble; a zero high nibble means the next byte is the absolute ; game clock. Header $FE = the film ends here, $FF = the recorded game was aborted (a real header can ; never reach $EF because a merged buffer holds at most 14 bytes). ; ---------------------------------------------------------------------- L_4DD2: ldy #$00 ; 4DD2 offset 0 of the record jsr readFilmByte ; 4DD4 read it from the film buffer in the RAM hidden under the I/O area sta mergedCmdLength ; 4DD7 park the header byte in mergedCmdLength for now iny ; 4DDA offset 1 jsr readFilmByte ; 4DDB the time-stamp byte sta L_4DE8+1 ; 4DDE patch the LDA # at $4DE9 so the byte can be re-read without touching the film again and #$0F ; 4DE1 low nibble = the exchange slot within the round (commandSliceIndex, one exchange per ten units processed) cmp commandSliceIndex ; 4DE3 only replay this record when the round has reached the same slot it was recorded in bne L_4DC9 ; 4DE6 this record belongs to another slot: not yet L_4DE8: lda #$FF ; 4DE8 operand patched at $4DDE: the time-stamp byte again lsr a ; 4DEA shift the high nibble down ... lsr a ; 4DEB ... lsr a ; 4DEC ... lsr a ; 4DED ... A = the clock delta code beq L_4DF8 ; 4DEE 0 = the long form: an absolute clock byte follows ora #$F0 ; 4DF0 sign-extend the 4-bit delta to a negative byte sec ; 4DF2 the writer subtracted one extra, add it back L_4DF3: adc #$00 ; 4DF3 operand patched at $4E01: the clock of the previous record, giving this record's clock jmp L_4DFC ; 4DF5 join the clock comparison L_4DF8: iny ; 4DF8 long form: offset 2 jsr readFilmByte ; 4DF9 read the absolute game clock of the record L_4DFC: cmp gameClock ; 4DFC is this record due now? bne L_4DC9 ; 4DFF no, it is in the future: come back next frame sta L_4DF3+1 ; 4E01 remember this clock as the base for the next record's delta iny ; 4E04 step past the time stamp sty L_4E1F+1 ; 4E05 patch the LDY # at $4E20: offset of the first command byte in this record lda mergedCmdLength ; 4E08 the header byte again cmp #$FE ; 4E0B $FE = the recorded game reached its own end beq L_4DD1 ; 4E0D freeze here: return every frame so the last picture stays up bcs L_4DC6 ; 4E0F $FF = the recorded game was aborted: run the end-of-film handler pha ; 4E11 keep the header byte and #$0F ; 4E12 low nibble = number of command bytes in the record sta mergedCmdLength ; 4E14 that is the length of the merged buffer for this exchange pla ; 4E17 header byte back lsr a ; 4E18 shift the high nibble down ... lsr a ; 4E19 ... lsr a ; 4E1A ... lsr a ; 4E1B ... A = where player 1's commands start sta mergedCmdSplitIndex ; 4E1C the dispatcher flips the owner flag at this index L_4E1F: ldy #$FF ; 4E1F operand patched at $4E05: first command byte of the record ldx #$00 ; 4E21 write into the merged buffer from index 0 ; ---------------------------------------------------------------------- ; Copy the record's command bytes out of the film buffer into the merged command buffer. ; ---------------------------------------------------------------------- L_4E23: jsr readFilmByte ; 4E23 next byte of the record sta mergedCmdBuffer,x ; 4E26 into the merged command buffer iny ; 4E29 next film offset inx ; 4E2A next buffer index cpx mergedCmdLength ; 4E2B all command bytes copied? bcc L_4E23 ; 4E2E no: keep copying jsr advanceFilmPtr ; 4E30 advance the film pointer by the whole record (Y = its length) jsr executeTurnCommands ; 4E33 execute the commands exactly as a live exchange would jmp L_4DBC ; 4E36 look for another record with the same time stamp ; ---------------------------------------------------------------------- ; Live play. Everything below runs once per completed packet exchange with the opponent (or with the ; solo trainer, which fakes the same exchange). ; ---------------------------------------------------------------------- L_4E39: lda linkSessionState ; 4E39 link state: 0 none, 2 game session agreed, $80 link up at menu level, $FF lost bpl L_4E41 ; 4E3C bit 7 clear: a game session is running, do the full exchange jmp pollMenuSession ; 4E3E menu level: only the carrier check and the menu packet handler, then return L_4E41: jsr exitIfGameEnded ; 4E41 returns without coming back here if the game is already over bne L_4E55 ; 4E44 the end-of-game countdown is running: no more exchanges lda isLinkActive ; 4E46 comm module flag: is the link (UART or trainer) running at all? (also trainerAiE000:isLinkActive) beq L_4E55 ; 4E49 no link: nothing to exchange ; ---------------------------------------------------------------------- ; Wait for the comm module to report 'this frame's packets have been swapped' (bit 6 of the exchange ; flag byte $E01D). Normally the routine simply returns and is called again next frame; once the ; watchdog has been waiting for about half a minute checkLinkTimeout returns C=1 and the game ; busy-waits here with PHONE TROUBLE on the status line until the packet arrives or the watchdog gives ; up and returns to the main menu. ; ---------------------------------------------------------------------- L_4E4B: bit exchangeFlags ; 4E4B $E01D bit 6 = the exchange is complete (also trainerAiE000:commInterfaceBlock) bvs L_4E56 ; 4E4E both packets are in: go and merge them jsr checkLinkTimeout ; 4E50 arm/advance the link watchdog (it counts in the raster IRQ) bcs L_4E4B ; 4E53 C=1 = already overdue: spin here instead of giving the frame back L_4E55: rts ; 4E55 not ready yet, try again next frame ; ---------------------------------------------------------------------- ; Both packets are available: reset the timers and merge the two command streams. ; ---------------------------------------------------------------------- L_4E56: lda sndVoiceSoundId ; 4E56 sound currently playing on the voice cmp #$15 ; 4E59 $15 = the 'waiting for the opponent' sound bne L_4E62 ; 4E5B some other sound: leave it alone lda #$00 ; 4E5D sound 0 = silence jsr playSound ; 4E5F stop the waiting sound now that the opponent answered L_4E62: ldy #$0A ; 4E62 10 frames ... sty linkPollDelay ; 4E64 ... before the next exchange is attempted ldx #$00 ; 4E67 0 also clears bit 7 ... stx linkTimeoutCounter ; 4E69 ... which stops the IRQ advancing the link watchdog stx mergeFillIndex ; 4E6C the merged command buffer starts out empty lda playerSide ; 4E6E which side is this machine playing? bne L_4E81 ; 4E71 player 1: the opponent's commands go first jsr mergeOutgoingPacket ; 4E73 player 0: our own commands go into the buffer first ldx mergeFillIndex ; 4E76 bytes written so far stx mergedCmdSplitIndex ; 4E78 = the index at which player 1's commands start jsr mergeIncomingPacket ; 4E7B then append the packet received from the opponent jmp L_4E8C ; 4E7E on to the dispatcher L_4E81: jsr mergeIncomingPacket ; 4E81 player 1: the opponent is player 0, so his commands go first ldx mergeFillIndex ; 4E84 bytes written so far stx mergedCmdSplitIndex ; 4E86 = the index at which player 1's (our own) commands start jsr mergeOutgoingPacket ; 4E89 then append the copy of what we just sent L_4E8C: ldx mergeFillIndex ; 4E8C total number of bytes in the merged buffer stx mergedCmdLength ; 4E8E and fall straight into executeTurnCommands ; ---------------------------------------------------------------------- ; executeTurnCommands - Runs every command of one exchange. It walks the merged buffer from index 0 to ; mergedCmdLength, keeping cmdFromOpponent up to date: the owner flag starts at 'remote' or 'own' ; depending on which side this machine plays and is toggled when the read index reaches ; mergedCmdSplitIndex, the boundary between the two players' blocks. Own commands decrement the ; outstanding-order count. A byte with bit 7 set is dispatched through commandHandlerTable with the ; self-modified JSR at $4EEE; a byte with bit 7 clear is a cancelled command (a handler may overwrite ; its own command byte, e.g. $8E becomes $0E at $534F) and is skipped by its table length. In live ; play the whole exchange is then appended to the game film, the packet is acknowledged and the next ; outgoing packet is built and sent. ; In: mergedCmdBuffer ($908F), mergedCmdLength ($92EB), mergedCmdSplitIndex ($92EC), playerSide ; ($0B9F), filmPlaybackMode ($0B9B) ; Out: the game state changed by the handlers; mergedCmdReadIndex, mergedCmdOwnerFlag, ; cmdFromOpponent, exchangeActivityFlag bit 0 and pendingOwnCmdCount updated; in live play the ; film grows and the next packet is transmitted ; Called from: processCommandExchange - by fall-through for a live exchange and by JSR at $4E33 for a ; film record ; ---------------------------------------------------------------------- executeTurnCommands: lda #$00 ; 4E91 start at the first byte ... sta mergedCmdReadIndex ; 4E93 ... of the merged command buffer ldy playerSide ; 4E96 which side is this machine? beq L_4E9D ; 4E99 player 0: the first block of commands is our own (owner flag 0) lda #$80 ; 4E9B player 1: the first block belongs to player 0, i.e. the opponent L_4E9D: sta mergedCmdOwnerFlag ; 4E9D owner of the block currently being executed ; ---------------------------------------------------------------------- ; Command loop. Every command in the buffer is executed on both machines in exactly this order, which ; is what keeps the two simulations identical. ; ---------------------------------------------------------------------- L_4EA0: ldy mergedCmdReadIndex ; 4EA0 index of the next command byte cpy mergedCmdLength ; 4EA3 past the end of the merged buffer? bcs L_4EF4 ; 4EA6 yes: the exchange has been executed lda exchangeActivityFlag ; 4EA8 activity flag read by the round timer ora #$01 ; 4EAB bit 0 = at least one command was executed this round sta exchangeActivityFlag ; 4EAD in the setup phase the clock only ticks while something happens cpy mergedCmdSplitIndex ; 4EB0 have we reached the boundary between the two players' commands? bne L_4EBD ; 4EB3 no: same owner as the previous command lda mergedCmdOwnerFlag ; 4EB5 current owner eor #$80 ; 4EB8 flip between own ($00) and opponent ($80) sta mergedCmdOwnerFlag ; 4EBA everything from here belongs to the other player L_4EBD: lda mergedCmdOwnerFlag ; 4EBD owner of this command sta cmdFromOpponent ; 4EC0 the handlers test bit 7 of this to mirror coordinates and to suppress local-only side effects bmi L_4EC7 ; 4EC2 a command from the opponent: nothing of ours to account for dec pendingOwnCmdCount ; 4EC4 one of our own queued orders has now been executed L_4EC7: ldy mergedCmdReadIndex ; 4EC7 index of the command byte lda mergedCmdBuffer,y ; 4ECA the command byte itself bmi L_4EDB ; 4ECD bit 7 set = a live command, dispatch it ; ---------------------------------------------------------------------- ; A byte with bit 7 clear is a command that was cancelled after it was queued (a handler can clear bit ; 7 of its own byte in the buffer, e.g. cmdDroneDetonate writes $0E over its $8E). Its length is still ; looked up so the reader can step over the whole command. ; ---------------------------------------------------------------------- jsr getCommandLength ; 4ECF look the length up anyway - the low 6 bits still identify the command clc ; 4ED2 plain add adc mergedCmdReadIndex ; 4ED3 skip the whole command ... sta mergedCmdReadIndex ; 4ED6 ... without executing it bne L_4EA0 ; 4ED9 always taken (the index can never wrap in a 14-byte buffer) ; ---------------------------------------------------------------------- ; Dispatch: copy the handler address out of commandHandlerTable into the operand of the JSR below and ; call it. The handler itself reads its parameters with readCmdArg1/2/3, which also advance ; mergedCmdReadIndex to the next command. ; ---------------------------------------------------------------------- L_4EDB: lda mergedCmdBuffer,y ; 4EDB the command byte and #$3F ; 4EDE $80-$A6 becomes index $00-$26 asl a ; 4EE0 two bytes per table entry tax ; 4EE1 X = byte offset into commandHandlerTable lda commandHandlerTable,x ; 4EE2 low byte of the handler address sta L_4EEE+1 ; 4EE5 patch the JSR operand at $4EEF lda D_4D2B,x ; 4EE8 high byte of the handler address sta L_4EEE+2 ; 4EEB patch the JSR operand at $4EF0 L_4EEE: jsr irqVectorHi ; 4EEE call the handler (the $FFFF shown here is only the unpatched placeholder from the $8D table slot) jmp L_4EA0 ; 4EF1 on to the next command ; ---------------------------------------------------------------------- ; End of the exchange. While a film is being replayed there is nothing to record and no packet to ; answer, so the routine simply returns. ; ---------------------------------------------------------------------- L_4EF4: lda filmPlaybackMode ; 4EF4 bit 7 = film playback bpl L_4EFA ; 4EF7 live game: record and answer rts ; 4EF9 film playback: done L_4EFA: jsr recordExchangeToFilm ; 4EFA append this exchange (header, time stamp, command bytes) to the film in the RAM under I/O ldx #$02 ; 4EFD comm module function 2 = acknowledge/consume the completed exchange jsr commJumpTable ; 4EFF clears $E01D/$E01E/$E01F so the next exchange can start (also trainerAiE000:commRequestEntry) jmp sendOutgoingPacket ; 4F02 build the next outgoing packet from the command ring and hand it to the comm module ; ---------------------------------------------------------------------- ; mergeIncomingPacket - Appends the packet just received from the opponent ($E020, $E01E bytes) to the ; merged command buffer, marking the block as remote. Commands are copied whole, their length taken ; from commandLengthTable. Three command bytes are special: $97 (drone launched) also clears the drone ; flags, $92 (drone arrived) sets them, while $8D (the opponent's drone position report) and $8C ; (session control: abort, pause, resume) are executed here and now and never enter the buffer; the ; abort case of $8C even unwinds this loop by dropping two return addresses. ; In: the comm module's receive buffer $E020-$E026 and its length $E01E, mergeFillIndex (zp_69) = ; bytes already in the merged buffer ; Out: mergedCmdBuffer filled from mergeFillIndex on, mergeFillIndex advanced, packetReadIndex (zp_6A) ; used as the cursor, cmdFromOpponent = $80, droneArrivalFlags possibly changed ; Called from: processCommandExchange ($4E7B for player 0, $4E81 for player 1) ; ---------------------------------------------------------------------- mergeIncomingPacket: lda rxPacketLength ; 4F05 number of bytes the opponent sent this exchange (also trainerAiE000:rxPacketLength) beq L_4F5D ; 4F08 empty packet: nothing to merge lda #$80 ; 4F0A $80 = these commands come from the opponent ... sta cmdFromOpponent ; 4F0C ... so cmdDroneMove and cmdSessionControl mirror their coordinates ldy #$00 ; 4F0E start at the first byte of the packet sty packetReadIndex ; 4F10 zp_6A is the read cursor inside the packet ; ---------------------------------------------------------------------- ; Walk the received packet command by command. ; ---------------------------------------------------------------------- L_4F12: ldy packetReadIndex ; 4F12 offset of the next command in the packet lda rxPacketBuffer,y ; 4F14 its command byte (also trainerAiE000:rxPacketBuffer) jsr advancePacketCursor ; 4F17 look up its length and point zp_6A at the following command cmp #$97 ; 4F1A $97 = the opponent launched his drone bne L_4F23 ; 4F1C not $97 ldx #$00 ; 4F1E clear all drone state ... stx droneArrivalFlags ; 4F20 ... a new drone flight starts; the $97 command itself is copied into the buffer as well L_4F23: cmp #$8D ; 4F23 $8D = the opponent's drone position report bne L_4F2E ; 4F25 not $8D iny ; 4F27 step to its first parameter byte jsr cmdDroneMove ; 4F28 run it immediately (it only moves the drone sprite and the view) and do not copy it jmp L_4F56 ; 4F2B on to the next command L_4F2E: cmp #$8C ; 4F2E $8C = session control (abort, pause, resume, disconnect) bne L_4F3C ; 4F30 not $8C iny ; 4F32 step to the reason byte lda rxPacketBuffer,y ; 4F33 read it (also trainerAiE000:rxPacketBuffer) jsr cmdSessionControl ; 4F36 act on it at once - an abort must work even mid-buffer - and do not copy it jmp L_4F56 ; 4F39 on to the next command L_4F3C: cmp #$92 ; 4F3C $92 = the opponent's drone reached its target bne L_4F45 ; 4F3E not $92 lda #$80 ; 4F40 remember it in the drone flags ... sta droneArrivalFlags ; 4F42 ... the drone screen watches this byte L_4F45: ldx mergeFillIndex ; 4F45 X = where in the merged buffer this command goes ; ---------------------------------------------------------------------- ; Copy the whole command (byte plus parameters) into the merged buffer. ; ---------------------------------------------------------------------- L_4F47: lda rxPacketBuffer,y ; 4F47 next byte of the command (also trainerAiE000:rxPacketBuffer) sta mergedCmdBuffer,x ; 4F4A into the merged command buffer inc mergeFillIndex ; 4F4D the buffer grew by one byte inx ; 4F4F next buffer position iny ; 4F50 next packet position dec currentCommandLength ; 4F51 one byte of this command done bne L_4F47 ; 4F54 keep going until the whole command has been copied L_4F56: ldy packetReadIndex ; 4F56 the cursor advancePacketCursor left cpy rxPacketLength ; 4F58 reached the end of the packet? (also trainerAiE000:rxPacketLength) bcc L_4F12 ; 4F5B no: next command L_4F5D: rts ; 4F5D the received packet has been merged ; ---------------------------------------------------------------------- ; advancePacketCursor - Helper of both packet mergers: keeps the command byte in A, looks its length ; up and leaves packetReadIndex pointing at the next command in the packet. ; In: A = command byte, Y = its offset inside the packet ; Out: A unchanged, packetReadIndex (zp_6A) = Y + length, currentCommandLength = the length ; Called from: mergeIncomingPacket ($4F17) and mergeOutgoingPacket ($4F7D) ; ---------------------------------------------------------------------- advancePacketCursor: pha ; 4F5E save the command byte - getCommandLength destroys A jsr getCommandLength ; 4F5F A = its total length, also stored in currentCommandLength tya ; 4F62 offset of this command clc ; 4F63 plain add adc currentCommandLength ; 4F64 plus its length sta packetReadIndex ; 4F67 = offset of the next command in the packet pla ; 4F69 hand the command byte back rts ; 4F6A the caller compares it against the special command bytes ; ---------------------------------------------------------------------- ; mergeOutgoingPacket - Appends the copy of the packet this machine just sent ($E027, $E01F bytes) to ; the merged buffer, marking the block as own, so the local commands are executed at exactly the same ; point in the sequence as on the opponent's machine. $8D is not copied - it only re-centres the ; viewport on the drone and closes one outstanding order - and $8C is executed at once, like in the ; incoming merger. ; In: the comm module's transmit copy $E027-$E02D and its length $E01F, mergeFillIndex (zp_69) ; Out: mergedCmdBuffer filled, mergeFillIndex advanced, packetReadIndex used as the cursor, ; cmdFromOpponent = 0, viewOriginCol/viewOriginRow or pendingOwnCmdCount possibly changed ; Called from: processCommandExchange ($4E73 for player 0, $4E89 for player 1) ; ---------------------------------------------------------------------- mergeOutgoingPacket: lda txPacketLength ; 4F6B number of bytes in the packet we just sent (also trainerAiE000:txPacketLength) beq L_4FC3 ; 4F6E we sent nothing: nothing to merge lda #$00 ; 4F70 0 = these are our own commands ... sta cmdFromOpponent ; 4F72 ... so their coordinates are already in our own frame of reference ldy #$00 ; 4F74 start at the first byte sty packetReadIndex ; 4F76 zp_6A is the read cursor inside the packet ; ---------------------------------------------------------------------- ; Walk our own packet command by command. ; ---------------------------------------------------------------------- L_4F78: ldy packetReadIndex ; 4F78 offset of the next command lda txPacketBuffer,y ; 4F7A its command byte (also trainerAiE000:txPacketBuffer) jsr advancePacketCursor ; 4F7D look up its length and point zp_6A at the next command cmp #$8D ; 4F80 $8D = our own drone position report bne L_4F94 ; 4F82 not $8D lda droneViewOriginCol ; 4F84 column the drone screen chose ... sta viewOriginCol ; 4F87 ... becomes the tactical view origin ... lda droneViewOriginRow ; 4F89 ... together with the row ... sta viewOriginRow ; 4F8C ... so the view follows the drone L_4F8E: dec pendingOwnCmdCount ; 4F8E the report counted as an outstanding order when it was queued jmp L_4FBC ; 4F91 and it is not copied into the merged buffer L_4F94: cmp #$8C ; 4F94 $8C = session control bne L_4FA2 ; 4F96 not $8C iny ; 4F98 step to the reason byte lda txPacketBuffer,y ; 4F99 read it (also trainerAiE000:txPacketBuffer) jsr cmdSessionControl ; 4F9C act on our own pause/abort at once, then also count it off jmp L_4F8E ; 4F9F and skip the copy L_4FA2: cmp #$92 ; 4FA2 $92 = our drone reached its target bne L_4FAB ; 4FA4 not $92 lda #$80 ; 4FA6 set the drone flag ... sta droneArrivalFlags ; 4FA8 ... the drone screen watches this byte; the command is copied as well L_4FAB: ldx mergeFillIndex ; 4FAB X = where in the merged buffer this command goes ; ---------------------------------------------------------------------- ; Copy the whole command into the merged buffer. ; ---------------------------------------------------------------------- L_4FAD: lda txPacketBuffer,y ; 4FAD next byte of the command (also trainerAiE000:txPacketBuffer) sta mergedCmdBuffer,x ; 4FB0 into the merged buffer iny ; 4FB3 next packet position inx ; 4FB4 next buffer position inc mergeFillIndex ; 4FB5 the buffer grew by one byte dec currentCommandLength ; 4FB7 one byte of this command done bne L_4FAD ; 4FBA until the whole command has been copied L_4FBC: ldy packetReadIndex ; 4FBC the cursor advancePacketCursor left cpy txPacketLength ; 4FBE end of our packet? (also trainerAiE000:txPacketLength) bcc L_4F78 ; 4FC1 no: next command L_4FC3: rts ; 4FC3 our own packet has been merged ; ---------------------------------------------------------------------- ; readCmdArgs3 - Argument reader for the 4-byte commands $80, $81, $8A, $8B, $98, $9C and $A6: takes ; the first parameter into cmdParam0 and falls through into readCmdArgs2, which picks up the other ; two. The read index ends up on the next command, matching commandLengthTable. ; In: mergedCmdReadIndex ($90B7) = offset of the command byte in mergedCmdBuffer ; Out: cmdParam0/cmdParam1/cmdParam2 ($73/$74/$75) = the three parameters, mergedCmdReadIndex advanced ; by 4, A/Y clobbered ; Called from: cmdMoveUnit ($5011), cmdMoveGroupRelative ($501A), cmdPlaceUnit ($51AB), cmdSetMapSeed ; ($51F3), cmdMoveKeepGroup ($5470) and cmdSetRecycler ($54E1) ; ---------------------------------------------------------------------- readCmdArgs3: ldy mergedCmdReadIndex ; 4FC4 offset of the command byte iny ; 4FC7 first parameter follows it lda mergedCmdBuffer,y ; 4FC8 read it sta cmdParam0 ; 4FCB cmdParam0, usually the unit or group inc mergedCmdReadIndex ; 4FCD count that byte, then fall into readCmdArgs2 for the remaining two ; ---------------------------------------------------------------------- ; readCmdArgs2 - Argument reader for the 3-byte commands $87, $88, $8E, $90, $9A, $A3 and $A5, and the ; tail of readCmdArgs3. It reads the two bytes that follow the command byte into cmdParam1/cmdParam2 ; and leaves the read index on the next command (advance 3, or 4 when entered from readCmdArgs3). ; In: mergedCmdReadIndex ($90B7), mergedCmdBuffer ($908F) ; Out: cmdParam1/cmdParam2 ($74/$75), mergedCmdReadIndex advanced, A/Y clobbered ; Called from: cmdSetTarget ($516B), cmdJoinGroup ($5185), cmdDroneDetonate ($5336), cmdMissileStrike ; ($53C0), cmdAddEnergy ($5479), cmdSetFormation ($5596), cmdSetupOption ($55A5) and readCmdArgs3 ; ---------------------------------------------------------------------- readCmdArgs2: ldy mergedCmdReadIndex ; 4FD0 current read offset (already past parameter 1 when entered from $4FC4) iny ; 4FD3 next byte lda mergedCmdBuffer,y ; 4FD4 read it sta cmdParam1 ; 4FD7 cmdParam1, usually the map column iny ; 4FD9 next byte lda mergedCmdBuffer,y ; 4FDA read it sta cmdParam2 ; 4FDD cmdParam2, usually the map row iny ; 4FDF step past it sty mergedCmdReadIndex ; 4FE0 the read index now points at the next command byte rts ; 4FE3 back to the handler ; ---------------------------------------------------------------------- ; mirrorCmdCoordsIfRemote - Point mirror for the map coordinates carried by a command. Both machines ; run one shared simulation while each player sees his own side at the bottom of the map, so every ; coordinate that arrives over the link has to be rotated 180 degrees (39-x, 39-y) before it is used. ; Own commands are left alone. ; In: cmdFromOpponent (zp_76) bit 7, cmdParam1 = column, cmdParam2 = row ; Out: cmdParam1/cmdParam2 replaced by 39-column/39-row for a remote command, untouched for an own ; one; A clobbered ; Called from: cmdMoveUnit, cmdMoveGroupRelative, cmdPlaceUnit, cmdMissileStrike, cmdMoveKeepGroup, ; cmdSetRecycler and mirrorCmdCoordsIfRemoteDec ; ---------------------------------------------------------------------- mirrorCmdCoordsIfRemote: bit cmdFromOpponent ; 4FE4 did this command come from the opponent? bpl L_4FF6 ; 4FE6 no: our own coordinates are already in the local frame lda cmdParam1 ; 4FE8 the column jsr mirrorMapCoordinate ; 4FEA 39 - column sta cmdParam1 ; 4FED back into cmdParam1 lda cmdParam2 ; 4FEF the row jsr mirrorMapCoordinate ; 4FF1 39 - row sta cmdParam2 ; 4FF4 back into cmdParam2 L_4FF6: rts ; 4FF6 coordinates are now in this machine's frame of reference ; ---------------------------------------------------------------------- ; readCmdArg1 - Argument reader for all the 2-byte commands - the per-unit and per-group orders ; $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2 and $A4. It reads the single byte after the ; command byte into cmdParam0 and steps the read index to the next command. ; In: mergedCmdReadIndex ($90B7), mergedCmdBuffer ($908F) ; Out: cmdParam0 ($73) = the unit index or group key, mergedCmdReadIndex advanced by 2, A/Y clobbered ; Called from: the cloak/blitz/dig/group handlers ($5031, $504B, $5062, $507E, $50FA, $515D, $519B, ; $542E, $549E, $5511, $555D, $5576, $559C) ; ---------------------------------------------------------------------- readCmdArg1: ldy mergedCmdReadIndex ; 4FF7 offset of the command byte iny ; 4FFA its single parameter follows lda mergedCmdBuffer,y ; 4FFB read it sta cmdParam0 ; 4FFE cmdParam0 = unit index or group key iny ; 5000 step past it sty mergedCmdReadIndex ; 5001 the read index now points at the next command rts ; 5004 back to the handler ; ---------------------------------------------------------------------- ; mirrorCmdCoordsIfRemoteDec - Coordinate mirror for the drone, which covers a 2x2 block of cells and ; is addressed by its top-left corner. For a remote command it mirrors the corner and then decrements ; both coordinates, giving 38-x / 38-y, which is the top-left corner of the mirrored block. No-op for ; an own command. ; In: cmdFromOpponent (zp_76), cmdParam1/cmdParam2 = the drone's top-left cell ; Out: cmdParam1/cmdParam2 = 38-column / 38-row for a remote command; A clobbered ; Called from: cmdDroneMove ($52DC) and cmdDroneDetonate ($5359) ; ---------------------------------------------------------------------- mirrorCmdCoordsIfRemoteDec: lda cmdFromOpponent ; 5005 did this command come from the opponent? bpl L_5010 ; 5007 no: leave the coordinates alone jsr mirrorCmdCoordsIfRemote ; 5009 mirror the corner cell (39-x, 39-y) dec cmdParam1 ; 500C the mirrored 2x2 block starts one column earlier ... dec cmdParam2 ; 500E ... and one row earlier L_5010: rts ; 5010 back to the drone handler ; ---------------------------------------------------------------------- ; cmdMoveUnit - Handler of command $80 MOVE (4 bytes: unit, column, row), the ordinary move order ; given by clicking a cell with a single unit selected. It reads and mirrors the arguments and ; tail-jumps into applyMoveOrderOld ($34C6), which drops the unit out of any group, re-syncs its ; display flags and sets the destination. ; In: the merged command buffer and its read index; cmdFromOpponent for the mirroring ; Out: unit destination/waypoint tables updated via $34C6 ; Called from: reached only through commandHandlerTable, entry $80 ; ---------------------------------------------------------------------- cmdMoveUnit: jsr readCmdArgs3 ; 5011 unit, column and row into cmdParam0/1/2 jsr mirrorCmdCoordsIfRemote ; 5014 a move ordered by the opponent is expressed in his own view of the map jmp applyMoveOrder ; 5017 leave the group, refresh the flags and set the destination ; ---------------------------------------------------------------------- ; cmdMoveGroupRelative - Handler of command $81 (4 bytes: unit, column, row) - the move order given ; while a whole group is selected. It reads and mirrors the arguments and tail-jumps into ; moveGroupKeepingFormation, which shifts every member of the named unit's group by the same offset so ; the formation is kept. ; In: the merged command buffer and its read index; cmdFromOpponent ; Out: destinations of all members of the leader's group ; Called from: reached only through commandHandlerTable, entry $81 ; ---------------------------------------------------------------------- cmdMoveGroupRelative: jsr readCmdArgs3 ; 501A leader unit, ordered column and row jsr mirrorCmdCoordsIfRemote ; 501D mirror the ordered cell for a command from the opponent jmp moveGroupKeepingFormation; 5020 shift the whole group by leader-destination minus ordered cell ; ---------------------------------------------------------------------- ; cmdCloakOn - Handler of command $82 CLOAK ON (2 bytes: unit). Cloak and blitz are mutually ; exclusive, so the unit's blitz bits are cleared first by calling cmdBlitzOff, which consumes the ; same argument; the read index is therefore saved in a self-modified operand and restored before the ; argument is read again. SPY units (type 3) are permanently cloaked and are rejected, everybody else ; gets bits 6-7 of unitPathColTable set (cloak ordered plus cloak active) and falls into the shared ; tail at $5058. ; In: the merged command buffer; unitTypeTable for the SPY test ; Out: unitPathColTable[unit] |= $C0 and unitPathRowTable[unit] &= $3F; pendingRedrawUnit and ; comcenCloakedFlag through the tail; self-modifies the LDA operand at $502D ; Called from: reached only through commandHandlerTable, entry $82 ; ---------------------------------------------------------------------- cmdCloakOn: ldy mergedCmdReadIndex ; 5023 current read index in the merged buffer sty L_502C+1 ; 5026 patch the LDA # at $502D with it - cmdBlitzOff will consume the argument too jsr cmdBlitzOff ; 5029 clear the unit's blitz bits first: a unit cannot be cloaked and blitzing at once L_502C: lda #$FF ; 502C operand patched at $5026: the saved read index sta mergedCmdReadIndex ; 502E rewind so the same argument can be read again jsr readCmdArg1 ; 5031 cmdParam0 = the unit ldy cmdParam0 ; 5034 Y = unit index lda unitTypeTable,y ; 5036 its type byte and #$07 ; 5039 keep only the type, dropping the dead and dug-in bits cmp #$03 ; 503B type 3 = SPY beq L_504A ; 503D spies are always cloaked: nothing to do lda unitPathColTable,y ; 503F waypoint column byte, whose top two bits hold the cloak state ora #$C0 ; 5042 bit 7 = cloak active, bit 6 = cloak ordered sta unitPathColTable,y ; 5044 the unit is now cloaked jmp L_5058 ; 5047 join the shared tail L_504A: rts ; 504A a SPY needs no cloak order ; ---------------------------------------------------------------------- ; cmdCloakOff - Handler of command $84 CLOAK OFF (2 bytes: unit); also used as a subroutine by ; cmdBlitzOn. It clears bits 6-7 of unitPathColTable. The shared tail at $5058 notes the unit in ; pendingRedrawUnit for an own command only - the opponent's cloaking must not trigger a redraw here - ; and then falls into updateComcenCloakLock. There is no SPY exemption on this side, unlike in the ; group handler. ; In: cmdParam0 after readCmdArg1, cmdFromOpponent ; Out: unitPathColTable[unit] &= $3F; pendingRedrawUnit ($923B) = unit for an own command; ; comcenCloakedFlag refreshed ; Called from: commandHandlerTable entry $84, and cmdBlitzOn ($5076) ; ---------------------------------------------------------------------- cmdCloakOff: jsr readCmdArg1 ; 504B cmdParam0 = the unit ldy cmdParam0 ; 504E Y = unit index lda unitPathColTable,y ; 5050 waypoint column byte and #$3F ; 5053 clear bits 6-7: cloak off sta unitPathColTable,y ; 5055 store it back ; ---------------------------------------------------------------------- ; Shared tail of the single-unit cloak handlers: only our own cloak changes are worth a local redraw, ; and the console's inspect lock has to follow the state of our own comcen. ; ---------------------------------------------------------------------- L_5058: lda cmdFromOpponent ; 5058 did this command come from the opponent? bmi L_505F ; 505A yes: no local redraw sty pendingRedrawUnit ; 505C ask the per-frame tick loop to redraw this unit L_505F: jmp updateComcenCloakLock ; 505F refresh 'my comcen is cloaked' and return from there ; ---------------------------------------------------------------------- ; cmdBlitzOff - Handler of command $95 BLITZ OFF (2 bytes: unit); also used as a subroutine by ; cmdCloakOn. It clears bits 6-7 of unitPathRowTable, the blitz ordered/active pair. ; In: the merged command buffer and its read index ; Out: unitPathRowTable[unit] &= $3F; cmdParam0 = unit, Y = unit ; Called from: commandHandlerTable entry $95, and cmdCloakOn ($5029) ; ---------------------------------------------------------------------- cmdBlitzOff: jsr readCmdArg1 ; 5062 cmdParam0 = the unit ldy cmdParam0 ; 5065 Y = unit index lda unitPathRowTable,y ; 5067 waypoint row byte, whose top two bits hold the blitz state and #$3F ; 506A clear bits 6-7: no longer blitzing sta unitPathRowTable,y ; 506C store it back rts ; 506F back to the dispatcher (or to cmdCloakOn) ; ---------------------------------------------------------------------- ; cmdBlitzOn - Handler of command $93 BLITZ ON (2 bytes: unit), the mirror image of cmdCloakOn. It ; saves and restores the read index around a call to cmdCloakOff (blitz cancels cloak), rejects SPY ; units, sets bits 6-7 of unitPathRowTable and tail-jumps into applyBlitzStepCost, which pays one ; energy point for the extra 25 movement points. ; In: the merged command buffer; unitTypeTable for the SPY test ; Out: unitPathRowTable[unit] |= $C0, unitPathColTable[unit] &= $3F, energy and movement points ; through $3EE2; self-modifies the LDA operand at $507A ; Called from: reached only through commandHandlerTable, entry $93 ; ---------------------------------------------------------------------- cmdBlitzOn: ldy mergedCmdReadIndex ; 5070 current read index sty L_5079+1 ; 5073 patch the LDA # at $507A - cmdCloakOff consumes the argument as well jsr cmdCloakOff ; 5076 clear the cloak bits first: a unit cannot be cloaked and blitzing at once L_5079: lda #$FF ; 5079 operand patched at $5073: the saved read index sta mergedCmdReadIndex ; 507B rewind so the argument can be read again jsr readCmdArg1 ; 507E cmdParam0 = the unit ldy cmdParam0 ; 5081 Y = unit index lda unitTypeTable,y ; 5083 its type byte and #$07 ; 5086 keep only the type cmp #$03 ; 5088 type 3 = SPY beq L_5097 ; 508A spies cannot blitz lda unitPathRowTable,y ; 508C waypoint row byte ora #$C0 ; 508F bit 7 = blitz active, bit 6 = blitz ordered sta unitPathRowTable,y ; 5091 the unit is now blitzing jmp applyBlitzStepCost ; 5094 charge the energy and grant the extra movement points, then return from there L_5097: rts ; 5097 a SPY cannot blitz: nothing to do ; ---------------------------------------------------------------------- ; cmdGroupCloakOn - Handler of command $83 (2 bytes: group key). It runs cmdGroupBlitzOff for the same ; group first (the two states are exclusive), saving and restoring the read index around it, then ; enters the patch block of the shared group routine at $50D4 with X = $80 so the body works on ; unitPathColTable with OR $C0 and sets bit 7 of the group's status entry. ; In: the merged command buffer; the group key argument carries the group number in bits 2-4 and the ; side in bit 5 ; Out: unitPathColTable of every living non-SPY member |= $C0, unitPathRowTable &= $3F, ; groupStatusTable[group] |= $80, pendingRedrawGroup and comcenCloakedFlag; self-modifies the LDA ; operand at $50A2 ; Called from: reached only through commandHandlerTable, entry $83 ; ---------------------------------------------------------------------- cmdGroupCloakOn: ldy mergedCmdReadIndex ; 5098 current read index sty L_50A1+1 ; 509B patch the LDA # at $50A2 - the blitz handler consumes the argument as well jsr cmdGroupBlitzOff ; 509E clear the group's blitz bits first L_50A1: lda #$FF ; 50A1 operand patched at $509B: the saved read index sta mergedCmdReadIndex ; 50A3 rewind so the group key can be read again ldx #$80 ; 50A6 X = $80: cloak variant, group status bit 7 bne L_50D4 ; 50A8 always taken: enter the cloak patch block ; ---------------------------------------------------------------------- ; cmdGroupBlitzOn - Handler of command $94 (2 bytes: group key). It runs cmdGroupCloakOff for the ; group first, then enters the shared patch block at $50C1 with X = $C0 and A = $40, so the body works ; on unitPathRowTable with OR $C0, calls applyBlitzStepCost for every member and sets ; groupStatusTable[group] = (old & $BF) | $40. ; In: the merged command buffer ; Out: unitPathRowTable of every living non-SPY member |= $C0, one energy point and 25 movement points ; per member, groupStatusTable[group] bit 6 set; self-modifies the LDA operand at $50B4 ; Called from: reached only through commandHandlerTable, entry $94 ; ---------------------------------------------------------------------- cmdGroupBlitzOn: ldy mergedCmdReadIndex ; 50AA current read index sty L_50B3+1 ; 50AD patch the LDA # at $50B4 - the cloak handler consumes the argument as well jsr cmdGroupCloakOff ; 50B0 clear the group's cloak bits first L_50B3: lda #$FF ; 50B3 operand patched at $50AD: the saved read index sta mergedCmdReadIndex ; 50B5 rewind so the group key can be read again ldx #$C0 ; 50B8 per-unit bits to OR in: blitz ordered + active lda #$40 ; 50BA bit 6 of the group's status entry = the group is blitzing bne L_50C1 ; 50BC always taken ; ---------------------------------------------------------------------- ; cmdGroupBlitzOff - Handler of command $96 (2 bytes: group key) and, at $50C1, the blitz half of the ; patch block that configures the shared group routine. With X = A = 0 it selects: no blitz cost, ; group status OR $00, group status AND $BF and table unitPathRowTable, so every member's blitz bits ; are cleared. ; In: the merged command buffer ; Out: unitPathRowTable of every member &= $3F, groupStatusTable[group] &= $BF; patches the operands ; at $512D, $514B and $514D and the table operands via applyGroupCloakOrBlitz ; Called from: commandHandlerTable entry $96, and cmdGroupCloakOn ($509E) ; ---------------------------------------------------------------------- cmdGroupBlitzOff: ldx #$00 ; 50BE X = 0: no bits to set in the members' waypoint byte txa ; 50C0 A = 0 as well L_50C1: sta L_514C+1 ; 50C1 patch the ORA # at $514D: bits to set in the group status entry sta L_512C+1 ; 50C4 patch the LDA # at $512D: non-zero here means 'charge the blitz cost per member' lda #$BF ; 50C7 keep everything but bit 6 ... sta L_514A+1 ; 50C9 ... patch the AND # at $514B (bit 6 = the group is blitzing) lda #$FC ; 50CC low byte of unitPathRowTable ($F8FC), the blitz table ldy #$F8 ; 50CE its high byte bne applyGroupCloakOrBlitz ; 50D0 always taken: run the shared body ; ---------------------------------------------------------------------- ; cmdGroupCloakOff - Handler of command $85 (2 bytes: group key) and, at $50D4, the cloak half of the ; patch block. With X = 0 it selects: no blitz cost, group status AND $7F, group status OR $00 and ; table unitPathColTable, so the cloak bits of every member are cleared. Entered with X = $80 by ; cmdGroupCloakOn, which turns the same block into the cloak-on variant. ; In: the merged command buffer; X = 0 (cloak off) or $80 (cloak on) ; Out: unitPathColTable of every member &= $3F (or |= $C0), groupStatusTable[group] bit 7 cleared or ; set, pendingRedrawGroup for an own command, comcenCloakedFlag refreshed ; Called from: commandHandlerTable entry $85, cmdGroupBlitzOn ($50B0) and cmdGroupCloakOn ($50A8) ; ---------------------------------------------------------------------- cmdGroupCloakOff: ldx #$00 ; 50D2 X = 0: clear the cloak bits L_50D4: lda #$00 ; 50D4 0 = no blitz cost in this variant ... sta L_512C+1 ; 50D6 ... patch the LDA # at $512D lda #$7F ; 50D9 keep everything but bit 7 ... sta L_514A+1 ; 50DB ... patch the AND # at $514B (bit 7 = the group is cloaked) stx L_514C+1 ; 50DE patch the ORA # at $514D: $00 for cloak off, $80 for cloak on lda #$98 ; 50E1 low byte of unitPathColTable ($F898), the cloak table ldy #$F8 ; 50E3 its high byte cpx #$00 ; 50E5 which variant was requested? beq applyGroupCloakOrBlitz ; 50E7 cloak off: the members' bits are cleared (OR $00) ldx #$C0 ; 50E9 cloak on: OR $C0 into every member's waypoint byte ; ---------------------------------------------------------------------- ; applyGroupCloakOrBlitz - The shared, heavily self-modified body of the four group cloak/blitz ; handlers; it is only ever reached by branching, never by JSR. It stores the chosen table address ; into the LDA/STA operands of the unit loop, reads the group key argument, notes the group in ; pendingRedrawGroup for an own cloak command, and walks all 100 unit slots skipping dead units, ; non-members and SPYs. For every member it optionally charges the blitz cost and rewrites table[unit] ; = (old & $3F) | patchedBits. Finally it updates the group's entry in groupStatusTable and falls into ; updateComcenCloakLock. ; In: the patched operands at $512D (blitz cost gate), $5134/$513B (table address), $5139 (per-unit ; bits), $514B/$514D (group status mask and bits); cmdParam0 = group key after readCmdArg1; ; unitFlagsTable and unitTypeTable ; Out: cloak or blitz bits of every matching unit, groupStatusTable[group], pendingRedrawGroup ; ($923A), comcenCloakedFlag ($9245) ; Called from: the four group handlers $83/$85/$94/$96 by branch ($50D0 and $50E7) ; ---------------------------------------------------------------------- applyGroupCloakOrBlitz: stx L_5138+1 ; 50EB patch the ORA # at $5139: the bits every member gets ($00 or $C0) sta L_5133+1 ; 50EE patch the low byte of the LDA abs,y at $5134 ... sta L_513A+1 ; 50F1 ... and of the STA abs,y at $513B sty L_5133+2 ; 50F4 patch the high byte of the LDA at $5135 ... sty L_513A+2 ; 50F7 ... and of the STA at $513C ($F898 cloak or $F8FC blitz) jsr readCmdArg1 ; 50FA cmdParam0 = the group key argument lda L_514A+1 ; 50FD the group status AND mask that was patched in cmp #$7F ; 5100 $7F means the cloak variant is running bne L_510D ; 5102 blitz: no redraw needed, cloaking is what changes the picture lda cmdFromOpponent ; 5104 did this command come from the opponent? bmi L_510D ; 5106 yes: the opponent's cloaking must not redraw our screen lda cmdParam0 ; 5108 our own group ... sta pendingRedrawGroup ; 510A ... is handed to the tick loop to be redrawn L_510D: lda cmdParam0 ; 510D the group key ora #$40 ; 510F add the in-a-group bit so it matches unitFlagsTable & $7C sta cmdParam0 ; 5111 and keep it there for the loop ldy #$63 ; 5113 scan unit slots 99 down to 0 ; ---------------------------------------------------------------------- ; One pass over all 100 unit slots. Dead slots, units of other groups and SPYs are skipped; SPYs are ; permanently cloaked and can never blitz, so they are exempt from both orders. ; ---------------------------------------------------------------------- L_5115: lda unitTypeTable,y ; 5115 type byte of this slot bmi L_513D ; 5118 bit 7 = dead or empty, skip it lda unitFlagsTable,y ; 511A authoritative flags and #$7C ; 511D group number, side and member bit cmp cmdParam0 ; 511F a member of the group named by the command? bne L_513D ; 5121 no: next slot lda unitTypeTable,y ; 5123 type byte again and #$07 ; 5126 keep only the type cmp #$03 ; 5128 type 3 = SPY beq L_513D ; 512A spies are exempt from group cloak and blitz orders L_512C: lda #$FF ; 512C operand patched at $50C4/$50D6: non-zero only for the blitz-on variant beq L_5133 ; 512E zero: no cost to charge jsr applyBlitzStepCost ; 5130 blitz on: pay one energy point for 25 extra movement points L_5133: lda irqVectorHi,y ; 5133 operand patched at $50EE/$50F4: the member's cloak ($F898) or blitz ($F8FC) byte and #$3F ; 5136 keep the waypoint coordinate in bits 0-5 L_5138: ora #$FF ; 5138 operand patched at $50EB: set both state bits, or none L_513A: sta irqVectorHi,y ; 513A operand patched at $50F1/$50F7: store the byte back L_513D: dey ; 513D next lower slot bpl L_5115 ; 513E loop until slot 0 has been examined ; ---------------------------------------------------------------------- ; Finally record the new state in the group's own entry so that the console and the group menu can ; show CLOAKED / BLITZ without scanning the units again. ; ---------------------------------------------------------------------- lda cmdParam0 ; 5140 the group key with its member bit and #$3C ; 5142 group number and side ... lsr a ; 5144 ... shifted down ... lsr a ; 5145 ... to give the 0-15 index (group 0-7 of side 0, 8-15 of side 1) tay ; 5146 Y = index into groupStatusTable lda groupStatusTable,y ; 5147 current status: speed class in bits 0-2, terrain class in bits 3-5, blitz bit 6, cloak bit 7 L_514A: and #$FF ; 514A operand patched at $50C9/$50DB: clear the bit this command owns L_514C: ora #$FF ; 514C operand patched at $50C1/$50DE: and set it if the order was 'on' sta groupStatusTable,y ; 514E store the new group status ; ---------------------------------------------------------------------- ; updateComcenCloakLock - The common tail of every cloak and blitz handler and the only writer of ; comcenCloakedFlag: it copies bit 7 of the local player's own comcen entry in unitPathColTable, so ; $9245 bit 7 is set exactly while that comcen is cloaked. The console reads it to lock the inspection ; panels ($2064, $2096) and both $6F00 overlays read it as well. ; In: comcenUnit ($9236) = 49 or 99, unitPathColTable; no arguments ; Out: comcenCloakedFlag ($9245) = unitPathColTable[own comcen] & $80; A and Y clobbered ; Called from: the fall-through from applyGroupCloakOrBlitz, the single-unit cloak tail ($505F) and ; both $6F00 overlays ($72E7) ; ---------------------------------------------------------------------- updateComcenCloakLock: ldy comcenUnit ; 5151 index of this player's own comcen (49 for side 0, 99 for side 1) lda unitPathColTable,y ; 5154 its waypoint column byte and #$80 ; 5157 bit 7 = the comcen is cloaked right now sta comcenCloakedFlag ; 5159 the console and the overlays test this byte rts ; 515C back to the dispatcher ; ---------------------------------------------------------------------- ; cmdClearTarget - Handler of command $86 CLEAR TARGET (2 bytes: BOOMER unit). getBoomerTarget turns ; the unit index into that unit's ordinal among all BOOMERs and the matching entry of ; boomerTargetTable is zeroed, so the BOOMER stops holding fire for a particular enemy. The carry that ; getBoomerTarget returns is not tested, so a non-BOOMER argument would clear a slot chosen by a ; leftover Y; only the unit menu of a BOOMER offers the item, so that never happens in practice. ; In: mergedCmdBuffer $908F / mergedCmdReadIndex $90B7 (the queued command), cmdParam0 after ; readCmdArg1 ; Out: boomerTargetTable $91DC[ordinal] = 0; mergedCmdReadIndex += 2; X = unit, Y = ordinal ; Called from: commandHandlerTable slot for $86 ($4D36), dispatched by executeTurnCommands at $4EEE ; ---------------------------------------------------------------------- cmdClearTarget: jsr readCmdArg1 ; 515D read the single argument byte into cmdParam0 and step mergedCmdReadIndex past this 2-byte command ldx cmdParam0 ; 5160 X = the BOOMER's unit index jsr getBoomerTarget ; 5162 $3C74: for a BOOMER (unitTypeTable & 7 == 2) returns Y = its ordinal among all BOOMERs lda #$00 ; 5165 0 = no target and no lock sta boomerTargetTable,y ; 5167 clear this BOOMER's slot of the 20-entry boomerTargetTable at $91DC rts ; 516A done - CLEAR TARGET touches nothing but the target table ; ---------------------------------------------------------------------- ; cmdSetTarget - Handler of command $87 SET TARGET (3 bytes: BOOMER unit, target unit). It only ; rearranges the parameter bytes so that setBoomerTargetCmd ($3C50) finds the BOOMER in cmdParam0 and ; the target in cmdParam2, then jumps there; the result is boomerTargetTable[BOOMER ordinal] = target ; | $80. The command is sent by sub_3C20 when the player puts the cursor on an enemy unit with a ; BOOMER selected. The EOR #$80 on cmdFromOpponent at $5171 is undone again at $517C without anything ; reading it in between, and the store at ; $5177 is immediately overwritten at $5180 - both are leftovers from an earlier version. ; In: the queued command in mergedCmdBuffer; cmdParam1 = BOOMER, cmdParam2 = target after ; readCmdArgs2 ; Out: boomerTargetTable $91DC[ordinal] = target | $80 via $3C50; cmdParam0 = BOOMER; read index += 3 ; Called from: commandHandlerTable slot for $87 ($4D38) ; ---------------------------------------------------------------------- cmdSetTarget: jsr readCmdArgs2 ; 516B read the two arguments: cmdParam1 = the BOOMER, cmdParam2 = the target unit; read index += 3 lda cmdFromOpponent ; 516E take a copy of the own/remote flag of the command being executed pha ; 5170 push it eor #$80 ; 5171 flip bit 7 = 'this command came from the opponent' ... sta cmdFromOpponent ; 5173 ... and store the flipped value back; nothing between here and $517C looks at it, so the flip does nothing (leftover) lda cmdParam2 ; 5175 A = the target unit index sta cmdParam0 ; 5177 cmdParam0 = target - overwritten again at $5180, so this store is dead sta cmdParam2 ; 5179 write cmdParam2 back over itself - a no-op pla ; 517B pull the untouched own/remote flag sta cmdFromOpponent ; 517C undo the EOR of $5171 lda cmdParam1 ; 517E A = the BOOMER's unit index sta cmdParam0 ; 5180 setBoomerTargetCmd wants the BOOMER in cmdParam0 and the target in cmdParam2 jmp setBoomerTargetCmd ; 5182 $3C50: boomerTargetTable[BOOMER ordinal] = cmdParam2 | $80 (target unit plus the 'locked' bit) ; ---------------------------------------------------------------------- ; cmdJoinGroup - Handler of command $88 JOIN GROUP (3 bytes: unit, group byte). It jumps to ; joinUnitToGroup ($4AD4), which makes the unit follow the group's movement and sets unitFlagsTable = ; (old & 3) | group | $40. The eight instructions at $5188-$5197 are a shuffle whose only lasting ; effect is cmdParam0 = group byte, and nothing downstream reads cmdParam0 - the whole block is dead ; code. ; In: the queued command; cmdParam1 = unit, cmdParam2 = group byte (id bits 2-4, side bit 5) ; Out: unitFlagsTable $F708[unit] and unitDisplayFlagsTable $FB54[unit] via $4AD4; read index += 3 ; Called from: commandHandlerTable slot for $88 ($4D3A) ; ---------------------------------------------------------------------- cmdJoinGroup: jsr readCmdArgs2 ; 5185 read the two arguments: cmdParam1 = unit index, cmdParam2 = group byte (id in bits 2-4, side in bit 5) ; ---------------------------------------------------------------------- ; Eight instructions of register shuffling whose only lasting effect is cmdParam0 = the group byte, ; and nothing reads cmdParam0 afterwards. Dead code left over from an earlier calling convention. ; ---------------------------------------------------------------------- lda cmdParam1 ; 5188 A = the unit index sta cmdParam0 ; 518A cmdParam0 = unit - overwritten again at $5192 lda cmdParam0 ; 518C read the same byte straight back sta cmdParam1 ; 518E store the unit index back into cmdParam1 - a no-op lda cmdParam2 ; 5190 A = the group byte sta cmdParam0 ; 5192 cmdParam0 = group byte; nothing downstream ever reads cmdParam0, so even this is pointless lda cmdParam0 ; 5194 read it back again sta cmdParam2 ; 5196 store the group byte back into cmdParam2 - another no-op jmp joinUnitToGroup ; 5198 $4AD4: make unit cmdParam1 follow group cmdParam2 and set unitFlagsTable = (old & 3) | group | $40 ; ---------------------------------------------------------------------- ; cmdLeaveGroup - Handler of command $89 (2 bytes: unit): clears bit 6 ('in a group') of the unit's ; unitFlagsTable entry and tail-jumps to syncLocalGroupFlags ($4C6B), which mirrors bits 2-6 into ; unitDisplayFlagsTable so the overview map stops drawing the unit's group marker. Sent by ; removeSelectedUnitFromGroup ($4AF2), the GROUP menu item REMOVED. ; In: the queued command; cmdParam0 = unit index ; Out: unitFlagsTable $F708[unit] &= $BF, unitDisplayFlagsTable $FB54[unit] refreshed; read index += 2 ; Called from: commandHandlerTable slot for $89 ($4D3C) ; ---------------------------------------------------------------------- cmdLeaveGroup: jsr readCmdArg1 ; 519B cmdParam0 = the unit index; read index += 2 ldx cmdParam0 ; 519E X = the unit lda unitFlagsTable,x ; 51A0 $F708 flags: facing bits 0-1, group id bits 2-4, side bit 5, 'in a group' bit 6 and #$BF ; 51A3 clear bit 6 - the unit no longer belongs to a group sta unitFlagsTable,x ; 51A5 write the flags back jmp syncLocalGroupFlags ; 51A8 $4C6B: copy bits 2-6 into unitDisplayFlagsTable so the map stops drawing the group marker ; ---------------------------------------------------------------------- ; cmdPlaceUnit - Handler of command $8A (4 bytes: unit, column, row): teleports a unit during the ; deployment phase. The unit's cloak bits (unitPathColTable & $C0) and blitz bits (unitPathRowTable & ; $C0) are first saved into the ORA operands at $51C8 and $51E4, then the new column is written to ; unitColTable, to unitPathColTable (keeping the cloak state) and to unitDestColTable with the ; 'arrived' bit 7, bit 7 of unitDisplayFlagsTable is set, and the new row goes to unitRowTable, ; unitDestRowTable and unitPathRowTable (keeping the blitz state). The $F000 map itself is not touched ; here. ; In: the queued command; cmdParam0 = unit, cmdParam1/cmdParam2 = column/row (mirrored when remote) ; Out: $F640, $F6A4, $F7D0, $F834, $F898, $F8FC and $FB54 of the unit; self-modifies $51C9 and $51E5 ; Called from: commandHandlerTable slot for $8A ($4D3E) ; ---------------------------------------------------------------------- cmdPlaceUnit: jsr readCmdArgs3 ; 51AB read three arguments: cmdParam0 = unit, cmdParam1 = column, cmdParam2 = row; read index += 4 jsr mirrorCmdCoordsIfRemote ; 51AE a remote player sends his own coordinates: convert them with 39-v so both machines agree ldy cmdParam0 ; 51B1 Y = the unit index into every one of the $64-spaced unit arrays ; ---------------------------------------------------------------------- ; Save the two flag pairs that live in the top bits of the waypoint bytes, so the rewrite below can ; put them back: cloak in unitPathColTable bits 6-7, blitz in unitPathRowTable bits 6-7. ; ---------------------------------------------------------------------- lda unitPathColTable,y ; 51B3 $F898 waypoint column: bits 0-5 column, bits 6-7 = cloak ordered / cloak active and #$C0 ; 51B6 keep only the two cloak bits sta L_51C8+1 ; 51B8 patch them into the ORA immediate at $51C8 so the rewrite below preserves them lda unitPathRowTable,y ; 51BB $F8FC waypoint row: bits 0-5 row, bits 6-7 = blitz ordered / blitz active and #$C0 ; 51BE keep only the two blitz bits sta L_51E4+1 ; 51C0 patch them into the ORA immediate at $51E4 ; ---------------------------------------------------------------------- ; Column: current position, waypoint (cloak preserved) and destination (marked as already reached). ; ---------------------------------------------------------------------- lda cmdParam1 ; 51C3 A = the new map column sta unitColTable,y ; 51C5 $F640 = the unit's current column L_51C8: ora #$FF ; 51C8 immediate operand patched at $51B8 with the saved cloak bits sta unitPathColTable,y ; 51CA waypoint column = the new column with the cloak state untouched and #$3F ; 51CD strip the cloak bits off again ora #$80 ; 51CF bit 7 of unitDestColTable means 'already standing on its destination' sta unitDestColTable,y ; 51D1 $F7D0 destination column = the new column, marked as reached lda unitDisplayFlagsTable,y ; 51D4 $FB54 draw flags of the unit ora #$80 ; 51D7 bit 7 = idle / at destination, the copy the drawing code looks at sta unitDisplayFlagsTable,y ; 51D9 write the draw flags back ; ---------------------------------------------------------------------- ; Row: current position, destination and waypoint (blitz preserved). ; ---------------------------------------------------------------------- lda cmdParam2 ; 51DC A = the new map row sta unitRowTable,y ; 51DE $F6A4 = the unit's current row sta unitDestRowTable,y ; 51E1 $F834 destination row = the same row (rows carry no flag bit) L_51E4: ora #$FF ; 51E4 immediate operand patched at $51C0 with the saved blitz bits sta unitPathRowTable,y ; 51E6 waypoint row = the new row with the blitz state untouched rts ; 51E9 note that the $F000 map cell is never updated here - the setup screen redraws the board itself ; ---------------------------------------------------------------------- ; cmdSetCustomMapSeed - Handler of command $A6 (4 bytes: three seed bytes): sets mapFlipFlag ($0B9D) = ; 1 and falls into cmdSetMapSeed. Overlay A sends $A6 instead of $8B at $79BD when the player picked ; the flipped (180-degree rotated) view of the map, so both machines build the battlefield the same ; way round. ; In: the queued command ; Out: mapFlipFlag = 1, then everything cmdSetMapSeed does ; Called from: the last entry of commandHandlerTable ($4D76, command $A6) ; ---------------------------------------------------------------------- cmdSetCustomMapSeed: lda #$01 ; 51EA command $A6: the sender chose the flipped map variant bne L_51F0 ; 51EC always taken - join the shared body ; ---------------------------------------------------------------------- ; cmdSetMapSeed - Handler of command $8B (4 bytes: three seed bytes): mapFlipFlag = 0, the three ; arguments are copied into the 24-bit map seed $0B95-$0B97, and bit 7 of gamePhase is cleared. The ; player who owns the map sends this at the start of a session; the other machine's ; waitForMapSeedCommand ($7C1C) spins on gamePhase bit 7 until this handler runs, after which both ; generate the identical 40x40 battlefield. ; In: the queued command; cmdParam0/1/2 = the seed bytes after readCmdArgs3 ; Out: mapFlipFlag $0B9D, mapSeed $0B95-$0B97, gamePhase zp_B1 &= $7F; read index += 4 ; Called from: commandHandlerTable slot for $8B ($4D40), and by fall-through from cmdSetCustomMapSeed ; ---------------------------------------------------------------------- cmdSetMapSeed: lda #$00 ; 51EE command $8B: the map in its generated orientation L_51F0: sta mapFlipFlag ; 51F0 $0B9D: 0 = map as generated, 1 = rotated 180 degrees (the F1 TO FLIP option on the map screen) jsr readCmdArgs3 ; 51F3 read the three seed bytes into cmdParam0/1/2; read index += 4 lda cmdParam0 ; 51F6 seed byte 0 sta mapSeed ; 51F8 $0B95: byte 0 of the 24-bit battlefield seed, which is the 5-letter map name packed 5 bits per letter lda cmdParam1 ; 51FB seed byte 1 sta mapSeed1 ; 51FD $0B96 lda cmdParam2 ; 5200 seed byte 2 sta mapSeed2 ; 5202 $0B97 lda gamePhase ; 5205 zp_B1 game phase and #$7F ; 5207 clear bit 7 = 'still waiting for the map seed' sta gamePhase ; 5209 this releases waitForMapSeedCommand ($7C1C) on the receiving machine rts ; 520B both machines now hold the same seed and will generate the identical battlefield ; ---------------------------------------------------------------------- ; cmdComcenRepairedOrSetupReady - Handler of the 1-byte command $9E, which means two different things. ; While the battle runs (gamePhase = 0) it is the result of a successful COMCEN repair from the REPAIR ; console tab ($1812): the stun counter in the low nibble of the sending side's COMCEN unitStunTable ; entry is cleared. In every other phase it is the READY / phase-sync acknowledgement of ; sendPhaseSyncAndWait ($7BF9): the sender's bit of gamePhase is cleared with getPauseClearMask, input ; is locked out for 32 frames, a fake SPACE key event is injected so any 'press space' loop falls ; through, and the chat state is reset. ; In: gamePhase zp_B1, cmdFromOpponent zp_76, playerSide $0B9F, perSideValueTable $0C8A ; Out: battle: unitStunTable $F960[comcen] &= $F0. Setup: zp_B1 masked, $0B7D = $20, $90EA = $20, ; $929C = 0. ; mergedCmdReadIndex += 1 in both cases ; Called from: commandHandlerTable slot for $9E ($4D66) ; ---------------------------------------------------------------------- cmdComcenRepairedOrSetupReady: inc mergedCmdReadIndex ; 520C a 1-byte command: just step the read index over it ldx gamePhase ; 520F zp_B1 is zero only while the battle itself is running beq L_5228 ; 5211 battle: this is a successful COMCEN repair jsr getPauseClearMask ; 5213 setup: A = the AND mask that clears the sending side's 'not ready' bit and gamePhase ; 5216 apply it to the phase byte sta gamePhase ; 5218 once both bits are gone sendPhaseSyncAndWait ($7BF9) stops waiting lda #$20 ; 521A $20 = 32 sta inputLockoutTimer ; 521C $0B7D: ignore keyboard and joystick for the next 32 frames sta newKeyEvent ; 521F inject key code $20 (SPACE) so any 'press space' wait loop falls through at once lda #$00 ; 5222 0 sta chatState ; 5224 $929C: abandon any chat line that was being typed rts ; 5227 phase sync done ; ---------------------------------------------------------------------- ; Battle phase: command $9E means the sending side's COMCEN has finished repairing itself, so clear ; its stun counter. ; ---------------------------------------------------------------------- L_5228: lda playerSide ; 5228 $0B9F = the side this machine plays, 0 or 1 ldy cmdFromOpponent ; 522B who sent the repair? bpl L_5231 ; 522D our own command: our own COMCEN was repaired eor #$01 ; 522F remote command: the other side's COMCEN L_5231: tax ; 5231 X = the sender's absolute side ldy comcenUnitIndexBySide,x ; 5232 $0C8A = {$31,$63}: unit 49 is side 0's COMCEN, unit 99 is side 1's lda unitStunTable,y ; 5235 $F960: low nibble = stun/knock-out counter, bit 4 spare, bits 5-7 = dig state and #$F0 ; 5238 clear the stun counter - the COMCEN is back on line sta unitStunTable,y ; 523A write it back, leaving the dig bits alone rts ; 523D done ; ---------------------------------------------------------------------- ; cmdSessionControl - Handler of command $8C (2 bytes: reason). It is never dispatched out of the ; merged buffer: both packet mergers call it directly ($4F36 and $4F9C) with A = the reason byte while ; the packets are being merged, so the effect is immediate and the bytes are not copied into the film. ; Reason 0 = the game is being abandoned: gameOverFlags = $C0, the message queue is cleared, 'GAME ; ABORTED.' is shown for a remote abort, the end-of-game countdown is armed, and the dispatcher is ; abandoned by pulling two return addresses off the stack. Reasons 1 and 2 return at once ($8C,$02 is ; the menu READY packet handled elsewhere). Reason 3 = link timeout, reason 5 = deliberate hang-up, ; anything else (4) = resume. Everything except reason 0 is skipped during film playback. ; In: A = reason byte, X/Y = the merge loop's registers, cmdFromOpponent zp_76, $0B9B, $0BA8 ; Out: pauseState zp_BE, $92A1, $0B7D, $929C, $92C9, $923F, zp_72, the message queue; returns through ; $532A ; with X/Y restored, or never returns at all for reason 0 ; Called from: mergeIncomingPacket $4F36 and mergeOutgoingPacket $4F9C ; ---------------------------------------------------------------------- cmdSessionControl: jsr saveRegsForCmdReturn ; 523E this handler runs inside the packet merge loop, so stash its X and Y into the operands at $532B/$532D sta cmdParam0 ; 5241 A holds the reason byte the merger fetched out of the packet cmp #$03 ; 5243 reasons 3 and up are the pause / hang-up / resume group bcs L_5272 ; 5245 branch to the pause group cmp #$00 ; 5247 reason 0 = the game is being abandoned beq L_524C ; 5249 handle the abort rts ; 524B reason 1 is unused; reason 2 is the menu-level READY packet handled by readyForNewGameHandshake ; ---------------------------------------------------------------------- ; Reason 0 - the game is being abandoned, by us or by the opponent. ; ---------------------------------------------------------------------- L_524C: lda #$C0 ; 524C $C0 = game finished, no result computed sta gameOverFlags ; 524E $923F game-over flags lda #$62 ; 5251 98 ticks of end-of-game countdown when no battle was running ldy gameEndReason ; 5253 $0BA8 is non-zero whenever no battle is in progress bne L_5266 ; 5256 no battle: keep the $62 countdown and say nothing jsr resetMessageQueue ; 5258 a battle was running: throw away everything queued for the status line lda cmdFromOpponent ; 525B who aborted? bpl L_5269 ; 525D our own abort - the local menu code has already set the countdown lda #$03 ; 525F message $03 jsr queueMessage ; 5261 'GAME ABORTED.' - the opponent quit on us lda #$71 ; 5264 113 ticks so the message can be read before the menu returns L_5266: sta gameEndCountdown ; 5266 $92C9 counts down to the return to the main menu ; ---------------------------------------------------------------------- ; Abandon the whole dispatcher. This handler was entered as JSR cmdSessionControl from inside ; mergeIncomingPacket/mergeOutgoingPacket, which themselves were entered as JSR from ; serviceCommandExchange, so four PLAs drop both return addresses and the JMP below returns straight ; to serviceCommandExchange's own caller. ; ---------------------------------------------------------------------- L_5269: pla ; 5269 discard the return address of this handler ... pla ; 526A (second byte) pla ; 526B ... and that of mergeIncomingPacket / mergeOutgoingPacket ... pla ; 526C ... so the RTS below lands in the caller of serviceCommandExchange instead ldx #$02 ; 526D comm function 2 = acknowledge and consume the finished exchange jmp commJumpTable ; 526F $E000; its RTS unwinds two levels because of the four PLAs above (also trainerAiE000:commRequestEntry) ; ---------------------------------------------------------------------- ; Reasons 3 and up - the pause, hang-up and resume group. All of them are ignored during film ; playback. ; ---------------------------------------------------------------------- L_5272: bit filmPlaybackMode ; 5272 $0B9B bit 7 = a game film is being replayed bmi L_52AB ; 5275 during playback the pause / resume traffic is ignored ldy #$00 ; 5277 0 sty chatState ; 5279 cancel any chat line being typed ldy #$3C ; 527C $3C = 60 frames, about one second sty inputLockoutTimer ; 527E hold the player's input off while the pause state changes cmp #$03 ; 5281 reason 3 = link timeout bne L_528E ; 5283 not the timeout reason sta pauseState ; 5285 zp_BE = 3: both sides counted as paused lda #$19 ; 5287 message $19 sta persistentMessageId ; 5289 'TIMEOUT. RUN/STOP TO RESUME.' stays on the line until someone resumes bne L_52A8 ; 528C always taken - go and queue the message L_528E: cmp #$05 ; 528E reason 5 = the link was hung up on purpose bne L_5296 ; 5290 not the hang-up reason lda #$80 ; 5292 bit 7 of pauseState = link dropped (DISCONNECT / SET VOICE) bne L_529D ; 5294 always taken L_5296: ldx pauseState ; 5296 any other reason (4 = RESUME) clears only the sender's own bit jsr getPauseClearMask ; 5298 A = $FD for our resume, $FE for his, $FC in a solo game and pauseState ; 529B clear that bit of the pause state L_529D: sta pauseState ; 529D store the new pause state bne L_52AB ; 529F someone is still paused - leave the message up sta persistentMessageId ; 52A1 A = 0: no persistent message any more sta messageTimer ; 52A4 zp_72 = 0 so the next message appears immediately lda #$1B ; 52A6 message $1B 'RESUMED' L_52A8: jsr queueMessage ; 52A8 put it on the status line L_52AB: jmp L_532A ; 52AB leave through the shared tail that restores the merge loop's X and Y ; ---------------------------------------------------------------------- ; getPauseClearMask - Returns the AND mask that clears the resuming side's bit of a two-bit ; ready/pause byte. cmdSessionControl applies it to pauseState (zp_BE) and ; cmdComcenRepairedOrSetupReady applies it to gamePhase (zp_B1). In a solo-trainer game there is no ; independent opponent, so both bits are cleared at once ($FC); otherwise an own command clears bit 0 ; ($FD) and a remote command clears bit 1 ($FE), and in the remote case the message 'OPPONENT READY.' ; is queued if his bit was still set. ; In: X = the pause/phase byte before this command, isSoloTrainer $0BA5, cmdFromOpponent zp_76 ; Out: A = $FC, $FD or $FE; may queue message $04; Y clobbered ; Called from: cmdSessionControl $5298 and cmdComcenRepairedOrSetupReady $5213 ; ---------------------------------------------------------------------- getPauseClearMask: lda #$FC ; 52AE mask that clears both pause bits at once ldy isSoloTrainer ; 52B0 $0BA5 bit 7 = PRACTICE WITH SOLO TRAINER bmi L_52C7 ; 52B3 solo: there is no independent opponent, so clear both bits together lda #$FD ; 52B5 mask that clears bit 0 = this machine's bit ldy cmdFromOpponent ; 52B7 who sent the command? bpl L_52C7 ; 52B9 our own command: bit 0 is the one to clear txa ; 52BB A = the pause/phase byte as it was before this command (passed in X) and #$02 ; 52BC bit 1 = the opponent was still flagged as not ready beq L_52C5 ; 52BE he was already ready - nothing to announce lda #$04 ; 52C0 message $04 jsr queueMessage ; 52C2 'OPPONENT READY.' L_52C5: lda #$FE ; 52C5 mask that clears bit 1 = the opponent's bit L_52C7: rts ; 52C7 A = the AND mask ; ---------------------------------------------------------------------- ; cmdDroneMove - Handler of command $8D (4 bytes: heading, column, row), the opponent's live drone ; telemetry. Only mergeIncomingPacket calls it, with Y pointing at the heading byte inside the ; received packet $E020; mergeOutgoingPacket deals with our own $8D by re-centring the tactical view ; instead, which is why the commandHandlerTable slot for $8D is a dummy - the byte never reaches the ; merged buffer. The handler mirrors the 2x2 drone origin into our coordinate frame and, while the ; COMCEN radar screen is showing and the drone has not already finished, parks sprite 0 on the drone's ; cell with the shape for the reversed heading. ; In: Y = index of the heading byte in $E020, cmdFromOpponent zp_76, droneArrivalFlags $92F9, $90FB ; Out: cmdParam0/1/2, droneControlFlags $920D bit 0, $9022, $92F6/$92F7, $9010/$9018, $91CC, $9008 and ; sprite 0's shape; X/Y restored from the operands patched by saveRegsForCmdReturn ; Called from: mergeIncomingPacket $4F28 ; ---------------------------------------------------------------------- cmdDroneMove: lda rxPacketBuffer,y ; 52C8 read straight out of the received packet at $E020; Y points at the heading byte (also trainerAiE000:rxPacketBuffer) sta cmdParam0 ; 52CB heading 0-7 of the opponent's drone iny ; 52CD next packet byte lda rxPacketBuffer,y ; 52CE fetch it (also trainerAiE000:rxPacketBuffer) sta cmdParam1 ; 52D1 drone map column in the sender's coordinate frame iny ; 52D3 next packet byte lda rxPacketBuffer,y ; 52D4 fetch it (also trainerAiE000:rxPacketBuffer) sta cmdParam2 ; 52D7 drone map row in the sender's coordinate frame jsr saveRegsForCmdReturn ; 52D9 remember the merge loop's X and Y for the common exit at $532A jsr mirrorCmdCoordsIfRemoteDec; 52DC mirror into our frame and step back one cell, because the drone occupies a 2x2 block lda droneArrivalFlags ; 52DF $92F9 tracks the life of the drone and #$30 ; 52E2 bit 4 = it reached its target, bit 5 = its kill burst has already been played bne L_532A ; 52E4 the drone is finished - ignore stale telemetry lda droneControlFlags ; 52E6 $920D drone state flags ora #$01 ; 52E9 bit 0 = an enemy drone is currently being tracked on our display sta droneControlFlags ; 52EB store the flags back ldy currentScreen ; 52EE $90FB: 0 = battlefield, 1 = COMCEN radar screen, 2 = drone screen, 3 = menus cpy #$01 ; 52F1 is the radar screen showing? bne L_532A ; 52F3 the enemy drone blip only exists on the radar screen ; ---------------------------------------------------------------------- ; Move sprite 0, the enemy drone blip, to the drone's cell on the 21x17 radar grid. The sprite is ; switched off first so that a drone which has flown off the grid simply disappears. ; ---------------------------------------------------------------------- lda spriteEnableShadow ; 52F5 $9022 shadow of VIC_SPR_ENA and #$FE ; 52F8 clear bit 0 - hide sprite 0 until we know the drone is inside the grid sta spriteEnableShadow ; 52FA store the sprite enable shadow back ldx cmdParam1 ; 52FD X = drone column stx droneViewOriginCol ; 52FF $92F6 = the tracked drone's map column ldy cmdParam2 ; 5302 Y = drone row sty droneViewOriginRow ; 5304 $92F7 = its map row jsr getUnitSpriteCentreOnGrid; 5307 $340B: converts map cell X,Y into the sprite X/Y of that cell's centre on the 21x17 radar grid; C=1 when off-grid bcs L_532A ; 530A off the visible grid - leave sprite 0 switched off sty spriteYShadow ; 530C $9018 = sprite 0 Y position shadow stx spriteXShadow ; 530F $9010 = sprite 0 X position shadow lda #$00 ; 5312 0 sta activeSpriteIndex ; 5314 $91CC = 0 so loadDirectionShapeSprite fills sprite 0's shape at $8A00 lda cmdParam0 ; 5317 the heading as the sender sees it clc ; 5319 clear carry for the add adc #$04 ; 531A +4 of the 8 compass points = the opposite direction ... cmp #$08 ; 531C past the eighth point? bcc L_5322 ; 531E no wrap needed sbc #$08 ; 5320 ... modulo 8; reversing the heading is exactly what the 180-degree map mirror demands L_5322: jsr loadDirectionShapeSprite; 5322 $3340: copy shape A out of the decompressed set at $0200 into sprite 0 and enable it lda #$01 ; 5325 VIC colour 1 = white sta spriteColourShadow ; 5327 $9008 = sprite 0 colour shadow L_532A: ldx #$FF ; 532A immediate operand patched by saveRegsForCmdReturn: restore the merge loop's X L_532C: ldy #$FF ; 532C immediate operand patched by saveRegsForCmdReturn: restore the merge loop's Y rts ; 532E back into the middle of the packet merge loop ; ---------------------------------------------------------------------- ; saveRegsForCmdReturn - Two-instruction helper for the handlers that are called from the middle of ; the packet merge loop instead of from the table dispatcher: it copies X and Y into the LDX/LDY ; immediate operands at ; $532B and $532D, so the shared exit at $532A hands the merger back exactly the loop registers it ; had. ; In: X, Y ; Out: self-modifies $532B and $532D ; Called from: cmdSessionControl $523E and cmdDroneMove $52D9 ; ---------------------------------------------------------------------- saveRegsForCmdReturn: stx L_532A+1 ; 532F write X into the LDX # operand of the shared exit at $532A sty L_532C+1 ; 5332 write Y into the LDY # operand at $532C rts ; 5335 done - the exit will now hand the caller back its own registers ; ---------------------------------------------------------------------- ; cmdDroneDetonate - Handler of command $8E (3 bytes: column, row). For our own command it clears the ; 'strike pending' bit 5 of droneControlFlags. If droneArrivalFlags bit 7 is already set the drone ; died or arrived before the order was exchanged, so the detonation is cancelled by overwriting the ; $8E byte in the merged buffer with $0E in place - bit 7 clear makes executeTurnCommands skip it ; while $0E & $3F still gives length 3, so the film keeps a correctly sized no-op. Otherwise the drone ; is marked detonated, the coordinates are mirrored, sound $11 plays, applyDroneBlast damages the 2x2 ; blast area and the blast-ring sprite is drawn; a remote detonation finally clears our copy of his ; drone state through the tail at $537E. ; In: the queued command, cmdFromOpponent zp_76, droneArrivalFlags $92F9, filmPlaybackMode $0B9B ; Out: $92F9, $920D, the command byte in mergedCmdBuffer, sound, blast damage and the marker sprite ; Called from: commandHandlerTable slot for $8E ($4D46) ; ---------------------------------------------------------------------- cmdDroneDetonate: jsr readCmdArgs2 ; 5336 cmdParam1 = target column, cmdParam2 = target row; read index += 3 lda cmdFromOpponent ; 5339 whose drone is detonating? bmi L_5345 ; 533B the opponent's - nothing local to clean up lda droneControlFlags ; 533D $920D drone state flags and #$DF ; 5340 clear bit 5 = 'a strike of ours is pending' (strikeWithDrone set it at $8187) sta droneControlFlags ; 5342 store the flags back L_5345: lda droneArrivalFlags ; 5345 $92F9 bit 7 = the drone has already finished its flight bpl L_5354 ; 5348 still airborne, so the strike counts ; ---------------------------------------------------------------------- ; The drone had already finished when the detonate order came round, so cancel the command in place ; instead of executing it. ; ---------------------------------------------------------------------- ldy mergedCmdReadIndex ; 534A readCmdArgs2 left the read index 3 past the command byte ... lda #$0E ; 534D $0E has bit 7 clear so executeTurnCommands skips it, and $0E & $3F still yields length 3 sta outgoingCmdRingEnd,y ; 534F $908C+readIndex is mergedCmdBuffer[command byte]: neuter the $8E in place, so the film records a correctly sized no-op bne L_537E ; 5352 always taken (A = $0E) ; ---------------------------------------------------------------------- ; A real detonation: mark the drone as burst, damage the 2x2 blast area and draw the ring. ; ---------------------------------------------------------------------- L_5354: lda #$40 ; 5354 $40 = the drone has detonated sta droneArrivalFlags ; 5356 $92F9 drone life flags jsr mirrorCmdCoordsIfRemoteDec; 5359 mirror the 2x2 blast origin into our frame for a remote strike lda filmPlaybackMode ; 535C $0B9B bit 7 = film playback bmi L_5365 ; 535F during playback everything is audible lda cmdFromOpponent ; 5361 whose strike is it? bpl L_536A ; 5363 our own strike is silent here - the drone screen already made its noise L_5365: lda #$11 ; 5365 sound $11 = the drone explosion jsr playSound ; 5367 hand it to the sound driver L_536A: lda #$19 ; 536A $19 = 25 points of blast damage jsr applyDroneBlast ; 536C overlay $72A1: damage whatever stands in the four cells of the 2x2 blast (also mapGenerator6F00:blobWriteTile) ldx cmdParam1 ; 536F blast column ldy cmdParam2 ; 5371 blast row lda #$01 ; 5373 marker style 1 = the drone burst ring (offsets 8,9 and colour 4 from the table at $33C2) jsr showExplosionMarker ; 5375 $33C8: put the blast-ring sprite on that cell and start its 90-frame timer jmp L_537E ; 5378 join the shared tail ; ---------------------------------------------------------------------- ; cmdDroneRelease - Handler of the 1-byte command $8F, queued by releaseDroneControl ($8230) when the ; player lets go of the drone. It only steps the read index and falls into the tail shared with ; cmdDroneDetonate, which for a remote command clears bits 0-5 of droneControlFlags - our copy of the ; opponent's drone state - while bits 6-7, which describe our own drone flight, are preserved. ; In: cmdFromOpponent zp_76, droneControlFlags $920D ; Out: mergedCmdReadIndex += 1; $920D &= $C0 for a remote command ; Called from: commandHandlerTable slot for $8F ($4D48); the tail at $537E also from cmdDroneDetonate ; ---------------------------------------------------------------------- cmdDroneRelease: inc mergedCmdReadIndex ; 537B 1-byte command $8F: step the read index over it L_537E: lda cmdFromOpponent ; 537E shared tail of $8E and $8F: whose drone was it? bpl L_538A ; 5380 our own drone state belongs to the drone screen, not here lda droneControlFlags ; 5382 $920D drone state flags and #$C0 ; 5385 keep bits 6-7 (our own flight) and clear bits 0-5, our copy of his drone state sta droneControlFlags ; 5387 store the flags back L_538A: rts ; 538A done ; ---------------------------------------------------------------------- ; cmdDroneShotDown - Handler of the 1-byte command $92. The only place that ever queues it is ; checkStrikeHitsDrone ($79CC), which runs right after a COMCEN missile is fired and asks whether the ; aim point falls on one of the 2x2 cells the tracked drone occupies: $92 therefore means 'a missile ; has shot the drone down', not 'the drone arrived'. It clears bits 6-7 of droneControlFlags (no drone ; flight in progress any more) and, for our own command, sets bit 4 of droneArrivalFlags so ; showDroneKillBurst ($79DD) plays the kill effect on the next frame. Both packet mergers additionally ; set droneArrivalFlags = $80 the moment a $92 is seen in either packet, and sendOutgoingPacket drops ; a queued $92 when the drone detonated first. ; In: cmdFromOpponent zp_76, droneControlFlags $920D, droneArrivalFlags $92F9 ; Out: $920D &= $3F; $92F9 |= $10 for an own command; mergedCmdReadIndex += 1 ; Called from: commandHandlerTable slot for $92 ($4D4E) ; (the survey called this cmdDroneArrived; renamed because $92 is the shot-down report, and the drone ; reaching its target is reported with $8E/$8F instead) ; ---------------------------------------------------------------------- cmdDroneShotDown: inc mergedCmdReadIndex ; 538B 1-byte command $92: step the read index over it lda droneControlFlags ; 538E $920D drone state flags and #$3F ; 5391 clear bits 6-7: no drone flight is in progress any more sta droneControlFlags ; 5393 store the flags back lda cmdFromOpponent ; 5396 who fired the missile that killed the drone? bmi L_53A2 ; 5398 the opponent scored the kill - nothing else to record here lda droneArrivalFlags ; 539A $92F9 drone life flags ora #$10 ; 539D bit 4 = we shot a drone down; showDroneKillBurst ($79DD) picks this up next frame and plays the effect sta droneArrivalFlags ; 539F store the flags back L_53A2: rts ; 53A2 done ; ---------------------------------------------------------------------- ; cmdDroneLaunch - Handler of the 1-byte command $97: droneArrivalFlags = 0 for a fresh drone in the ; air. The opponent's launch also raises the 'DRONE ALERT!' message with sound through overlay B's ; showDroneAlert. Either way, if that side still has drones one is spent and markComcenRevealed ; exposes the launching COMCEN for six rounds - the price of flying a drone. ; In: cmdFromOpponent zp_76, dronesLeft $92AE ; Out: $92F9 = 0, $92AE[side] decremented, COMCEN reveal timer and visibility bits via $403B ; Called from: commandHandlerTable slot for $97 ($4D58) ; ---------------------------------------------------------------------- cmdDroneLaunch: inc mergedCmdReadIndex ; 53A3 1-byte command $97: step the read index over it ldx #$00 ; 53A6 index 0 = this machine's allowances stx droneArrivalFlags ; 53A8 $92F9 = 0: a fresh drone is in the air lda cmdFromOpponent ; 53AB who launched? bpl L_53B4 ; 53AD our own launch - the drone screen has already told the player jsr showDroneAlert ; 53AF overlay $820F: sound 7 plus message $20 'DRONE ALERT!' ldx #$01 ; 53B2 index 1 = the opponent's allowances L_53B4: lda dronesLeft,x ; 53B4 $92AE[side] = drones that side still has beq L_53BF ; 53B7 already out of drones - nothing to charge dec dronesLeft,x ; 53B9 spend one drone jsr markComcenRevealed ; 53BC $403B: launching exposes that side's COMCEN for 6 rounds (X selects the side) L_53BF: rts ; 53BF done ; ---------------------------------------------------------------------- ; cmdMissileStrike - Handler of command $90 (3 bytes), which carries two different payloads. During a ; setup phase (gamePhase non-zero) the two bytes are the sender's drone and missile allowances and are ; stored in dronesLeft/missilesLeft for that side - this is what overlay A's editDronesAndMissiles ; sends. During the battle they are the target cell of a COMCEN missile: if the firing side still has ; missiles and the game has not ended, one missile is spent, markComcenRevealed exposes the firing ; COMCEN, the coordinates are mirrored for a remote shot and the unit standing on the cell takes 14 ; points of damage, halved to 7 for a COMCEN. The blast-ring sprite is drawn in either case and sound ; $0C plays unless it is our own live shot. ; In: the queued command, gamePhase zp_B1, cmdFromOpponent zp_76, gameEndReason $0BA8, the $F000 map ; Out: $92AE/$92B0[side], unit energy and score via $3B22, the blast-ring sprite, sound ; Called from: commandHandlerTable slot for $90 ($4D4A) ; ---------------------------------------------------------------------- cmdMissileStrike: jsr readCmdArgs2 ; 53C0 read the two payload bytes into cmdParam1/cmdParam2; read index += 3 lda gamePhase ; 53C3 zp_B1 is zero only while the battle runs beq L_53D9 ; 53C5 battle: the payload is a target cell ; ---------------------------------------------------------------------- ; Setup phase: the two bytes are the sender's drone and missile allowances, not a target cell. ; ---------------------------------------------------------------------- ldx #$00 ; 53C7 setup phase: the payload is the sender's drone/missile allowance; index 0 = us lda cmdFromOpponent ; 53C9 who sent it? bpl L_53CE ; 53CB our own settings inx ; 53CD index 1 = the opponent L_53CE: lda cmdParam1 ; 53CE first byte = number of drones sta dronesLeft,x ; 53D0 $92AE[side] lda cmdParam2 ; 53D3 second byte = number of missiles sta missilesLeft,x ; 53D5 $92B0[side] rts ; 53D8 allowances stored ; ---------------------------------------------------------------------- ; Battle phase: the two bytes are the target cell of a COMCEN missile. ; ---------------------------------------------------------------------- L_53D9: ldx #$00 ; 53D9 battle: index 0 = our own missile stock lda cmdFromOpponent ; 53DB who fired? bpl L_53E4 ; 53DD our own shot needs no mirroring jsr mirrorCmdCoordsIfRemote ; 53DF turn his aim point into our coordinates (39-v) ldx #$01 ; 53E2 index 1 = the opponent's missile stock L_53E4: lda missilesLeft,x ; 53E4 $92B0[side] beq L_542D ; 53E7 the firing side has no missiles left - ignore the shot lda gameEndReason ; 53E9 $0BA8 is non-zero once the game has ended bne L_542D ; 53EC no shots after the final whistle dec missilesLeft,x ; 53EE spend one missile jsr markComcenRevealed ; 53F1 firing gives the firing COMCEN away for 6 rounds ldx cmdParam1 ; 53F4 target column cpx #$28 ; 53F6 $28 = 40, the width of the battlefield bcs L_5416 ; 53F8 off the map: draw the ring but hit nothing ldy cmdParam2 ; 53FA target row cpy #$28 ; 53FC $28 = 40, the height of the battlefield bcs L_5416 ; 53FE off the map jsr readMapCell ; 5400 $25FA: A = the $F000 map byte of column X, row Y bpl L_5416 ; 5403 bit 7 clear = plain terrain, nobody is standing there and #$7F ; 5405 bits 0-6 = the index of the unit occupying the cell tay ; 5407 Y = the victim lda #$0E ; 5408 $0E = 14 points of missile damage cpy #$31 ; 540A unit 49 = side 0's COMCEN beq L_5412 ; 540C halve it cpy #$63 ; 540E unit 99 = side 1's COMCEN bne L_5413 ; 5410 an ordinary unit takes the full 14 L_5412: lsr a ; 5412 a COMCEN only takes half, 7 points L_5413: jsr applyHitToUnit ; 5413 $3B22: subtract the energy, score it, explode the unit or knock the COMCEN out L_5416: ldx cmdParam1 ; 5416 blast column ldy cmdParam2 ; 5418 blast row lda #$00 ; 541A marker style 0 = the missile blast ring (offsets 10,8 and colour 2 from the table at $33C2) jsr showExplosionMarker ; 541C $33C8: draw the ring sprite on the target cell lda cmdFromOpponent ; 541F whose shot? bmi L_5428 ; 5421 the opponent's shot is always audible lda filmPlaybackMode ; 5423 $0B9B bit 7 = film playback bpl L_542D ; 5426 our own shot is silent in live play - the radar screen made its own noise L_5428: lda #$0C ; 5428 sound $0C = missile impact jsr playSound ; 542A hand it to the sound driver L_542D: rts ; 542D done ; ---------------------------------------------------------------------- ; cmdSelfDestruct - Handler of command $91 (2 bytes: unit; bit 7 of the argument means 'the whole ; group this unit belongs to'). A plain unit index falls straight into destroyUnit. For a group the ; match value (unitFlagsTable & $3C) | $40 is patched into the CMP operand at $544D and units 99..0 ; are scanned, calling destroyUnit on every member; because the $3C mask keeps the side bit 5, a group ; can never span both armies. ; In: the queued command; cmdParam0 = unit index, bit 7 = group mode; unitFlagsTable $F708 ; Out: unitTypeTable $F76C entries set to $FF; self-modifies $544E; cmdParam0 reused as the loop ; counter ; Called from: commandHandlerTable slot for $91 ($4D4C) ; ---------------------------------------------------------------------- cmdSelfDestruct: jsr readCmdArg1 ; 542E cmdParam0 = the unit index; bit 7 set means 'the whole group this unit belongs to' lda cmdParam0 ; 5431 look at the argument bpl destroyUnit ; 5433 a plain unit index: destroy just that one and return through destroyUnit and #$7F ; 5435 strip the 'whole group' flag to get the unit index tay ; 5437 Y = the named unit lda unitFlagsTable,y ; 5438 its $F708 flags and #$3C ; 543B group id in bits 2-4 plus the side bit 5, so a group can never span both armies ora #$40 ; 543D bit 6 = 'in a group', which every member carries sta L_544D+1 ; 543F patch the CMP immediate of the scan loop with the group signature to look for ; ---------------------------------------------------------------------- ; Scan units 99..0 and destroy every living member whose group signature matches the one patched into ; the CMP at $544D. cmdParam0 doubles as the loop counter because destroyUnit reads its victim from ; there. ; ---------------------------------------------------------------------- lda #$63 ; 5442 $63 = 99, the highest unit index sta cmdParam0 ; 5444 reuse cmdParam0 as the loop counter, because destroyUnit reads its victim from there L_5446: ldy cmdParam0 ; 5446 Y = the unit currently being tested lda unitFlagsTable,y ; 5448 its $F708 flags and #$7C ; 544B group id, side and in-group bit L_544D: cmp #$FF ; 544D immediate operand patched at $543F with the group signature bne L_5454 ; 544F not a member of the group jsr destroyUnit ; 5451 blow this member up L_5454: dec cmdParam0 ; 5454 step down one unit bpl L_5446 ; 5456 keep going until unit 0 has been tested rts ; 5458 the whole group is gone ; ---------------------------------------------------------------------- ; destroyUnit - Removes unit cmdParam0 from play. A COMCEN (unit 49 or 99) is never allowed to ; self-destruct and returns unchanged. Everything else takes 52 points of damage through sub_3B52 - ; more than the 50-point maximum energy, so the kill is certain and runs through the normal explosion, ; sound, score and redraw path - and then has its slot freed outright by writing $FF into ; unitTypeTable. ; In: cmdParam0 zp_73 = unit index ; Out: unitTypeTable $F76C[unit] = $FF plus all the side effects of sub_3B52; Y = unit index ; Called from: cmdSelfDestruct (branch at $5433 and JSR at $5451) ; ---------------------------------------------------------------------- destroyUnit: ldy cmdParam0 ; 5459 Y = the unit to destroy (also the loop counter of cmdSelfDestruct) cpy #$31 ; 545B unit 49 = side 0's COMCEN beq L_546F ; 545D a COMCEN may never self-destruct cpy #$63 ; 545F unit 99 = side 1's COMCEN beq L_546F ; 5461 likewise protected lda #$34 ; 5463 $34 = 52 points, more than the 50-point maximum energy, so the kill is certain jsr applyHitToUnitNoAlert ; 5465 the damage path of applyHitToUnit without its attacker/SPY preamble: explosion, sound, score, redraw ldy cmdParam0 ; 5468 reload the unit index (sub_3B52 clobbers Y) lda #$FF ; 546A $FF = free slot, bit 7 set = dead sta unitTypeTable,y ; 546C $F76C: release the record so nothing draws or scans it again L_546F: rts ; 546F done ; ---------------------------------------------------------------------- ; cmdMoveKeepGroup - Handler of command $98 MOVE TO GOAL (4 bytes: unit, column, row), the order ; issued to each member of a group in turn by $4C0B. It reads and mirrors the arguments and jumps to ; setGroupMemberDestination ($4C59), which counts the order off pendingGroupOrderCount when the unit ; belongs to the group being ordered and then sets the destination through $34D5 without disturbing ; the group bits - unlike command $80, which drops the unit out of its group. ; In: the queued command; cmdParam0 = unit, cmdParam1/cmdParam2 = goal column/row ; Out: destination tables of the unit, pendingGroupOrderCount $929E; read index += 4 ; Called from: commandHandlerTable slot for $98 ($4D5A) ; ---------------------------------------------------------------------- cmdMoveKeepGroup: jsr readCmdArgs3 ; 5470 cmdParam0 = unit, cmdParam1/cmdParam2 = goal column/row; read index += 4 jsr mirrorCmdCoordsIfRemote ; 5473 mirror a remote player's goal into our coordinates jmp setGroupMemberDestination; 5476 $4C59: count the order off pendingGroupOrderCount and set the destination without touching the group bits ; ---------------------------------------------------------------------- ; cmdAddEnergy - Handler of command $9A (3 bytes: unit, amount), sent by the COMCEN energy repair on ; the REPAIR tab. Unless the game has already ended by a COMCEN knock-out ($0BA8 bit 7) the amount is ; added to bits 0-5 of the unit's unitEnergyTable entry and saturated at 50; the two visibility bits ; 6-7 are saved into the ORA operand at $5498 and put back afterwards. ; In: the queued command; cmdParam1 = unit, cmdParam2 = energy to add; gameEndReason $0BA8 ; Out: unitEnergyTable $F9C4[unit]; self-modifies $5499; read index += 3 ; Called from: commandHandlerTable slot for $9A ($4D5E) ; ---------------------------------------------------------------------- cmdAddEnergy: jsr readCmdArgs2 ; 5479 cmdParam1 = the unit, cmdParam2 = the energy to add; read index += 3 ldy cmdParam1 ; 547C Y = the unit being repaired lda gameEndReason ; 547E $0BA8 bit 7 set = a COMCEN has been knocked out bmi L_549D ; 5481 no repairs once the game has been decided lda unitEnergyTable,y ; 5483 $F9C4: bits 0-5 energy 0-50, bits 6-7 = spotted / visible flags tax ; 5486 keep the whole byte in X and #$C0 ; 5487 isolate the two visibility bits sta L_5498+1 ; 5489 patch them into the ORA immediate at $5498 so they survive the rewrite txa ; 548C get the byte back and #$3F ; 548D the current energy on its own clc ; 548F clear carry for the add adc cmdParam2 ; 5490 add the repair amount cmp #$32 ; 5492 $32 = 50 = full energy bcc L_5498 ; 5494 still below the cap lda #$32 ; 5496 saturate at 50 L_5498: ora #$FF ; 5498 immediate operand patched at $5489 with the saved visibility bits sta unitEnergyTable,y ; 549A store the repaired energy L_549D: rts ; 549D done ; ---------------------------------------------------------------------- ; cmdHaltGroup - Handler of command $9B (2 bytes: group byte), the group HALT order. Units 99..0 are ; scanned and for every living member (unitFlagsTable & $7C equal to the group byte | $40) the current ; position is copied into the destination and waypoint, the two 'arrived' bits (unitDestColTable bit 7 ; and unitDisplayFlagsTable bit 7) are set, the cloak bits of the waypoint column are preserved ; through the ORA operand patched at $54CF, and the blitz bits of the waypoint row are dropped because ; a plain row value is written over them. ; In: the queued command; cmdParam0 = group byte; unitTypeTable $F76C, unitFlagsTable $F708 ; Out: $F7D0, $F834, $F898, $F8FC and $FB54 of every member; self-modifies $54D0; read index += 2 ; Called from: commandHandlerTable slot for $9B ($4D60) ; ---------------------------------------------------------------------- cmdHaltGroup: jsr readCmdArg1 ; 549E cmdParam0 = the group byte (id in bits 2-4, side in bit 5); read index += 2 lda cmdParam0 ; 54A1 the group byte ora #$40 ; 54A3 bit 6 = 'in a group', so the byte can be compared with unitFlagsTable & $7C directly sta cmdParam0 ; 54A5 keep it as the match value ; ---------------------------------------------------------------------- ; Walk units 99..0 and stop every living member of the group where it stands. ; ---------------------------------------------------------------------- ldy #$63 ; 54A7 start at unit 99 and walk down to 0 L_54A9: lda unitTypeTable,y ; 54A9 $F76C, bit 7 set = dead or free slot bmi L_54DD ; 54AC skip dead units lda unitFlagsTable,y ; 54AE the unit's $F708 flags and #$7C ; 54B1 group id, side and in-group bit cmp cmdParam0 ; 54B3 does it belong to the group being halted? bne L_54DD ; 54B5 no - next unit lda unitColTable,y ; 54B7 the member's current column sta L_54CF+1 ; 54BA patch it into the ORA immediate at $54CF ora #$80 ; 54BD bit 7 = standing on its destination sta unitDestColTable,y ; 54BF $F7D0 destination column = where the unit already is lda unitDisplayFlagsTable,y ; 54C2 $FB54 draw flags ora #$80 ; 54C5 bit 7 = idle / at destination sta unitDisplayFlagsTable,y ; 54C7 store the draw flags back lda unitPathColTable,y ; 54CA $F898 waypoint column and #$C0 ; 54CD keep only the cloak bits 6-7 L_54CF: ora #$FF ; 54CF immediate operand patched at $54BA with the member's current column sta unitPathColTable,y ; 54D1 waypoint = current position, cloak state untouched lda unitRowTable,y ; 54D4 the member's current row sta unitDestRowTable,y ; 54D7 $F834 destination row = where the unit already is sta unitPathRowTable,y ; 54DA $F8FC waypoint row likewise; a row is below 64, so this also clears the blitz bits 6-7 L_54DD: dey ; 54DD step down one unit bpl L_54A9 ; 54DE continue until unit 0 has been tested rts ; 54E0 the whole group has stopped ; ---------------------------------------------------------------------- ; cmdSetRecycler - Handler of command $9C (4 bytes: mode, column, row), sent by overlay A's ; chooseRecycler ($8436) during the setup of the game types that have a recycler. recyclerMode[side] ; takes the mode, with side = the sender's absolute side (playerSide, EOR 1 when the command is ; remote); for modes 0 and 1 the coordinates are mirrored for a remote sender and stored in ; recyclerCol/recyclerRow[side]. Our own recycler is additionally painted onto the overview map, the ; enemy one is not - it has to be spotted first. ; In: the queued command; cmdParam0 = mode, cmdParam1/2 = column/row; playerSide $0B9F, ; cmdFromOpponent ; Out: $92A6/$92A8/$92AA indexed by side, map redraw via $3F27; self-modifies $550C; read index += 4 ; Called from: commandHandlerTable slot for $9C ($4D62) ; ---------------------------------------------------------------------- cmdSetRecycler: jsr readCmdArgs3 ; 54E1 cmdParam0 = mode, cmdParam1/cmdParam2 = column/row; read index += 4 lda playerSide ; 54E4 $0B9F = the side this machine plays ldy cmdFromOpponent ; 54E7 who sent the command? bpl L_54ED ; 54E9 our own choice eor #$01 ; 54EB remote: the sender is the other side L_54ED: tay ; 54ED Y = the sender's absolute side, the index of the per-side recycler tables sta L_550B+1 ; 54EE also stash it in the LDY immediate at $550B, because Y is reused before the draw call lda cmdParam0 ; 54F1 the chosen recycler mode sta recyclerMode,y ; 54F3 $92A6[side] cmp #$02 ; 54F6 modes 2 and up carry no coordinates bcs L_54FD ; 54F8 skip the mirror for those jsr mirrorCmdCoordsIfRemote ; 54FA mirror a remote placement into our coordinate frame L_54FD: lda cmdParam1 ; 54FD recycler column sta recyclerCol,y ; 54FF $92A8[side] lda cmdParam2 ; 5502 recycler row sta recyclerRow,y ; 5504 $92AA[side] lda cmdFromOpponent ; 5507 whose recycler is it? bmi L_5510 ; 5509 the enemy recycler stays hidden until it is spotted L_550B: ldy #$FF ; 550B immediate operand patched at $54EE with our own side index jsr drawRecyclerOnMap ; 550D $3F27: write the recycler glyph $67 into the map cell (or under whichever unit stands there) L_5510: rts ; 5510 done ; ---------------------------------------------------------------------- ; cmdChooseSide - Handler of command $9D (2 bytes: the sender's side-preference token), the ; side-selection handshake. In a solo game playerSide is simply forced to 0. In a modem game only the ; opponent's copy of the command matters: our own token was parked in $90F8 by sendChooseSide, and the ; player with the higher token becomes side 1 (the carry of the CPY is rotated into A). gamePhase is ; then set to $DF, whose clear bit 5 releases the wait loop at $797C. If both tokens are identical ; nobody wins: a fresh random token with bit 7 set is drawn and the code falls into sendChooseSide to ; send command $9D again, leaving gamePhase untouched so the loop keeps waiting. That only terminates ; because the two machines' game RNGs have drifted apart before a game starts (they are forced into ; step only once the map seed is exchanged). ; In: the queued command; cmdParam0 = his token, selectedUnit $90F8 = ours, isSoloTrainer $0BA5, ; zp_76 ; Out: playerSide $0B9F = 0 or 1 and gamePhase zp_B1 = $DF, or a retry through sendChooseSide ; Called from: commandHandlerTable slot for $9D ($4D64) ; ---------------------------------------------------------------------- cmdChooseSide: jsr readCmdArg1 ; 5511 cmdParam0 = the sender's side-preference token (0 or $FF from the setup screen, or a random tie-break byte) lda isSoloTrainer ; 5514 $0BA5 bit 7 = PRACTICE WITH SOLO TRAINER bpl L_551D ; 5517 modem game: settle it against the opponent lda #$00 ; 5519 solo: the human always plays side 0 beq L_552B ; 551B always taken L_551D: lda cmdFromOpponent ; 551D which copy of the command is this? bpl L_5532 ; 551F our own copy tells us nothing we did not already know lda #$00 ; 5521 build the resulting side number in A ldy selectedUnit ; 5523 $90F8 holds our own token, parked there by sendChooseSide cpy cmdParam0 ; 5526 compare it with his; carry set means our token is the higher one beq L_5533 ; 5528 identical tokens - nobody wins, so re-pick rol a ; 552A A = the carry: 1 when our token was higher, so we take side 1 L_552B: sta playerSide ; 552B $0B9F is 0 or 1 from now on lda #$DF ; 552E $DF clears bit 5 of the phase byte ... sta gamePhase ; 5530 ... which is the bit the side-selection loop at $797C spins on L_5532: rts ; 5532 the side handshake is finished L_5533: jsr nextGameRandom ; 5533 $FD5D: draw a fresh token; the two RNG states have drifted apart before a game starts, so the values probably differ ora #$80 ; 5536 bit 7 marks it as a tie-break retry token sta playerSide ; 5538 park it in playerSide, from where sendChooseSide picks it up below (fall-through) ; ---------------------------------------------------------------------- ; sendChooseSide - The sending half of the $9D exchange: copies playerSide into cmdParam0 and into ; selectedUnit $90F8 (where cmdChooseSide later finds our own token) and queues the 2-byte command ; $9D. At this point playerSide still holds a preference token - 0 or $FF chosen by the setup screen, ; or a random byte with bit 7 set after a tie - not yet a side number. ; In: playerSide $0B9F ; Out: cmdParam0 zp_73, selectedUnit $90F8, two bytes appended to the outgoing command ring ; Called from: overlay A's side-selection screen ($7976), and by fall-through from cmdChooseSide's tie ; path ; ---------------------------------------------------------------------- sendChooseSide: lda playerSide ; 553B our own preference token (0 or $FF from the setup screen, or a tie-break byte) sta cmdParam0 ; 553E the argument byte of command $9D sta selectedUnit ; 5540 $90F8 doubles as the 'our own pick' scratch that cmdChooseSide compares against lda #$9D ; 5543 command $9D CHOOSE SIDE jmp queueCmd2 ; 5545 append the 2-byte command to the outgoing ring ; ---------------------------------------------------------------------- ; cmdDigIn - Handler of command $9F DIG IN (2 bytes: unit): calls setUnitDigBits with $20, i.e. ; unitStunTable bits 5-7 = 001. The per-unit step code then advances those bits by $20 for every round ; the unit stays still until it counts as fortified. ; In: the queued command; cmdParam0 = unit after setUnitDigBits ; Out: unitStunTable $F960[unit] = (old & $1F) | $20 ; Called from: commandHandlerTable slot for $9F ($4D68) ; ---------------------------------------------------------------------- cmdDigIn: lda #$20 ; 5548 $20 = unitStunTable bits 5-7 set to 001, 'digging in' bne setUnitDigBits ; 554A always taken ; ---------------------------------------------------------------------- ; cmdDigOut - Handler of command $A0 DIG OUT (2 bytes: unit): setUnitDigBits with 0 clears the ; dig-progress bits 5-7 of unitStunTable, and bit 6 of unitTypeTable - the 'fortified' flag the combat ; code consults - is cleared as well. ; In: the queued command; cmdParam0 = unit ; Out: unitStunTable $F960[unit] &= $1F, unitTypeTable $F76C[unit] &= $BF ; Called from: commandHandlerTable slot for $A0 ($4D6A) ; ---------------------------------------------------------------------- cmdDigOut: lda #$00 ; 554C 0 = unitStunTable bits 5-7 cleared, neither digging nor dug in jsr setUnitDigBits ; 554E clear the dig bits and leave Y = the unit lda unitTypeTable,y ; 5551 $F76C of that unit and #$BF ; 5554 clear bit 6 = 'fortified', the flag the combat code consults sta unitTypeTable,y ; 5556 store the type byte back rts ; 5559 done ; ---------------------------------------------------------------------- ; setUnitDigBits - Shared prologue of cmdDigIn and cmdDigOut: patches the new dig bits into the ORA ; operand at $5567, reads the unit argument, leaves it in Y and falls into applyDigBitsToUnit. ; In: A = the new bits 5-7 ($20 or $00); the queued command ; Out: self-modifies $5568; cmdParam0 and Y = the unit; unitStunTable[unit] rewritten ; Called from: cmdDigIn (branch at $554A) and cmdDigOut ($554E) ; ---------------------------------------------------------------------- setUnitDigBits: sta L_5567+1 ; 555A patch the new dig bits into the ORA immediate at $5567 jsr readCmdArg1 ; 555D cmdParam0 = the unit index; read index += 2 ldy cmdParam0 ; 5560 Y = the unit, and it stays there for cmdDigOut ; ---------------------------------------------------------------------- ; applyDigBitsToUnit - The two-line worker shared by the single-unit and the group dig handlers: ; unitStunTable[Y] = (old & $1F) | patchedBits, so the low-nibble stun counter and bit 4 survive while ; the dig-state bits 5-7 are replaced wholesale. ; In: Y = unit index; the ORA operand at $5568 patched by setUnitDigBits or by the group entry at ; $5573 ; Out: unitStunTable $F960[Y] ; Called from: the group loop at $558F, and by fall-through from setUnitDigBits ; ---------------------------------------------------------------------- applyDigBitsToUnit: lda unitStunTable,y ; 5562 $F960: low nibble = stun counter, bit 4 spare, bits 5-7 = dig state and #$1F ; 5565 keep the stun counter and bit 4 L_5567: ora #$FF ; 5567 immediate operand patched at $555A or $5573 with the new dig bits sta unitStunTable,y ; 5569 store the new dig state rts ; 556C done ; ---------------------------------------------------------------------- ; cmdGroupDigIn - Handler of command $A1 (2 bytes: group byte): patches the dig bits to $20 and runs ; the shared group loop that follows. ; In: the queued command; cmdParam0 = group byte ; Out: unitStunTable $F960 of every living member = (old & $1F) | $20 ; Called from: commandHandlerTable slot for $A1 ($4D6C) ; ---------------------------------------------------------------------- cmdGroupDigIn: lda #$20 ; 556D $20 = dig bits 001 for every member bne L_5573 ; 556F always taken ; ---------------------------------------------------------------------- ; cmdGroupDigOut - Handler of command $A2 (2 bytes: group byte) and the entry of the shared group dig ; loop: patches the dig bits to 0, reads the group byte, ORs $40 into it so it can be compared with ; unitFlagsTable & $7C, and walks units 99..0 calling applyDigBitsToUnit for every living member. ; Unlike cmdDigOut it never clears bit 6 of unitTypeTable, so a group dug in as a group can only be ; un-fortified one unit at a time. ; In: the queued command; cmdParam0 = group byte; unitTypeTable $F76C, unitFlagsTable $F708 ; Out: unitStunTable $F960 of every member; self-modifies $5568; read index += 2 ; Called from: commandHandlerTable slot for $A2 ($4D6E), and by branch from cmdGroupDigIn ; ---------------------------------------------------------------------- cmdGroupDigOut: lda #$00 ; 5571 0 = clear the dig bits of every member L_5573: sta L_5567+1 ; 5573 patch the ORA immediate shared with applyDigBitsToUnit jsr readCmdArg1 ; 5576 cmdParam0 = the group byte; read index += 2 lda cmdParam0 ; 5579 the group byte ora #$40 ; 557B bit 6 = 'in a group', so it matches unitFlagsTable & $7C directly sta cmdParam0 ; 557D keep it as the match value ldy #$63 ; 557F walk units 99 down to 0 L_5581: lda unitTypeTable,y ; 5581 $F76C, bit 7 set = dead or free slot bmi L_5592 ; 5584 skip dead units lda unitFlagsTable,y ; 5586 the unit's $F708 flags and #$7C ; 5589 group id, side and in-group bit cmp cmdParam0 ; 558B is it a member of this group? bne L_5592 ; 558D no - next unit jsr applyDigBitsToUnit ; 558F rewrite this member's dig bits L_5592: dey ; 5592 step down one unit bpl L_5581 ; 5593 continue until unit 0 has been tested rts ; 5595 note that, unlike cmdDigOut, this never clears bit 6 of unitTypeTable ; ---------------------------------------------------------------------- ; cmdSetFormation - Handler of command $A3 (3 bytes: group leader unit, rotation mode 0-2): reads the ; two arguments and jumps to rotateGroupFormation ($4C84), which transforms every member's offset from ; the leader's destination with the rule pair at $4C7E/$4C81 and re-issues the destinations. ; In: the queued command; cmdParam1 = leader unit index, cmdParam2 = rotation mode ; Out: the destinations of all the leader's group members; read index += 3 ; Called from: commandHandlerTable slot for $A3 ($4D70) ; ---------------------------------------------------------------------- cmdSetFormation: jsr readCmdArgs2 ; 5596 cmdParam1 = the group leader's unit index, cmdParam2 = rotation mode 0-2; read index += 3 jmp rotateGroupFormation ; 5599 $4C84: rotate every member's offset from the leader using the rules at $4C7E ; ---------------------------------------------------------------------- ; cmdSetGameType - Handler of command $A4 (2 bytes): stores the argument into gameTypeOptions ($0BA3), ; which carries the game type in bits 0-2, the UNIT MENUS and DAMAGE switches in bits 3-4, CUSTOM ; rules in bit 6 and the custom-map flag in bit 7. Sent by overlay A's runMapTypeMenu ($833E) so both ; machines agree on the scenario before the map is generated. ; In: the queued command; cmdParam0 = the options byte ; Out: gameTypeOptions $0BA3; read index += 2 ; Called from: commandHandlerTable slot for $A4 ($4D72) ; ---------------------------------------------------------------------- cmdSetGameType: jsr readCmdArg1 ; 559C cmdParam0 = the options byte; read index += 2 lda cmdParam0 ; 559F the options byte sta gameTypeOptions ; 55A1 $0BA3: bits 0-2 game type, bit 3 UNIT MENUS, bit 4 DAMAGE, bit 6 CUSTOM rules, bit 7 custom map rts ; 55A4 both machines now agree on the scenario ; ---------------------------------------------------------------------- ; cmdSetupOption - Handler of command $A5 (3 bytes: kind, value), the generic carrier of overlay A's ; custom-rules editors. Kind 0 (HANDICAP) adds value*10 to the 16-bit counter $92A2/$92A4 indexed by ; the sender's absolute side; because that counter holds the points scored against a side, this is a ; head start credited to the sender's opponent, and the index has to be restored from the operand at ; $55C1 because multiply8x8 returns with X = 0. Kind 1 (TERRAIN PTS) stores the value in $92AC for the ; sender's opponent. Kind 2 and above - only kind 3, COMCEN SPEED, is ever sent - stores it in $92B4 ; indexed 0 = us, 1 = him. ; In: the queued command; cmdParam1 = kind, cmdParam2 = value; playerSide $0B9F, cmdFromOpponent ; zp_76 ; Out: $92A2/$92A4, $92AC or $92B4; self-modifies $55C2; read index += 3 ; Called from: commandHandlerTable slot for $A5 ($4D74) ; ---------------------------------------------------------------------- cmdSetupOption: jsr readCmdArgs2 ; 55A5 cmdParam1 = option kind, cmdParam2 = value; read index += 3 lda playerSide ; 55A8 $0B9F = the side this machine plays ldy cmdFromOpponent ; 55AB who sent the option? bpl L_55B1 ; 55AD our own setting eor #$01 ; 55AF remote: the other side L_55B1: tax ; 55B1 X = the sender's absolute side sta L_55C1+1 ; 55B2 stash it in the LDX immediate at $55C1, because multiply8x8 returns with X = 0 ldy cmdParam1 ; 55B5 the option kind bne L_55D1 ; 55B7 kinds 1 and up ; ---------------------------------------------------------------------- ; Kind 0 - HANDICAP: add value*10 to the 16-bit score already credited against the sender's side. ; ---------------------------------------------------------------------- lda cmdParam2 ; 55B9 kind 0 = HANDICAP: the value counts in units of ten points ldy #$0A ; 55BB multiplier 10 jsr multiply8x8 ; 55BD $FCCD: A = low byte and Y = high byte of value*10 clc ; 55C0 clear carry for the 16-bit add L_55C1: ldx #$FF ; 55C1 immediate operand patched at $55B2: restore the sender's side index adc sideScoreLo,x ; 55C3 $92A2/$92A3 = the 16-bit points scored against that side, so this is a head start for his opponent sta sideScoreLo,x ; 55C6 store the new low byte tya ; 55C9 product high byte adc sideScoreHi,x ; 55CA $92A4/$92A5 = the high halves of the two side scores sta sideScoreHi,x ; 55CD store the new high byte rts ; 55D0 done ; ---------------------------------------------------------------------- ; Kind 1 - TERRAIN PTS: the per-cell bonus for holding enemy ground, stored against the sender's ; opponent. ; ---------------------------------------------------------------------- L_55D1: cpy #$02 ; 55D1 kind 2 and up? bcs L_55DF ; 55D3 yes - COMCEN SPEED and friends txa ; 55D5 kind 1 = TERRAIN PTS eor #$01 ; 55D6 the bonus is credited against the sender's opponent tax ; 55D8 X = that side lda cmdParam2 ; 55D9 the value sta terrainPointsBySide,x ; 55DB $92AC[side]: points per cell of enemy ground held, read at $47A9 rts ; 55DE done ; ---------------------------------------------------------------------- ; Kind 2 and up - in practice only kind 3, COMCEN SPEED, indexed 0 = us / 1 = the opponent. ; ---------------------------------------------------------------------- L_55DF: ldx #$00 ; 55DF kind 2 and up (only kind 3, COMCEN SPEED, is ever sent): index 0 = us lda cmdFromOpponent ; 55E1 who sent it? bpl L_55E6 ; 55E3 our own setting inx ; 55E5 index 1 = the opponent L_55E6: lda cmdParam2 ; 55E6 the value sta comcenSpeedClass,x ; 55E8 $92B4/$92B5; the value 4 is what makes $34E1 refuse to move a COMCEN at all rts ; 55EB done ; ---------------------------------------------------------------------- ; queueCmdBytePending - Entry used by the 1-byte commands ($8F, $92, $97, $9E): counts one command in ; pendingOwnCmdCount and falls into queueCmdByte to append the single byte. ; In: A = the command byte ; Out: pendingOwnCmdCount $929F incremented, one byte appended to the outgoing ring ; Called from: $1814 in this file, and $79CC/$809A/$823C in both $6F00 overlays ; ---------------------------------------------------------------------- queueCmdBytePending: inc pendingOwnCmdCount ; 55EC $929F counts commands queued but not yet executed; this entry is used by the 1-byte commands ; ---------------------------------------------------------------------- ; queueCmdByte - Appends A to the 40-byte outgoing command ring at $9065, wrapping the write index ; $908D at 40, and always leaves an $FF end marker one byte past the new tail. It does nothing at all ; while a game film is being replayed, since the player issues no commands then. ; In: A = byte, outgoingCmdWriteIndex $908D, filmPlaybackMode $0B9B ; Out: outgoingCmdRing $9065[], $908D advanced; Y clobbered ; Called from: queueCmd4/queueCmd3/queueCmd2 ($560A, $5612, $5617, $561C, $5622, $5627) ; ---------------------------------------------------------------------- queueCmdByte: bit filmPlaybackMode ; 55EF $0B9B bit 7 = a film is being replayed bmi L_5609 ; 55F2 during playback the player issues no commands, so drop the byte ldy outgoingCmdWriteIndex ; 55F4 $908D = the write cursor of the 40-byte ring at $9065 sta outgoingCmdRing,y ; 55F7 store the byte in the ring iny ; 55FA advance the cursor cpy #$28 ; 55FB $28 = 40 bytes in the ring bcc L_5601 ; 55FD no wrap needed ldy #$00 ; 55FF wrap back to the start L_5601: sty outgoingCmdWriteIndex ; 5601 save the new write cursor lda #$FF ; 5604 $FF = end-of-queue marker sta outgoingCmdRing,y ; 5606 always keep a terminator one byte past the last command byte L_5609: rts ; 5609 done ; ---------------------------------------------------------------------- ; queueCmd4 - Queues a 4-byte command: the code in A followed by cmdParam0, cmdParam1 and cmdParam2 ; (used for ; $80 MOVE, $81, $8A, $8B, $8D, $98, $9C, $A6). It queues the code and cmdParam0 itself and then falls ; into ; queueCmd3, which counts the command once and queues the remaining two arguments. ; In: A = command code, cmdParam0/1/2 zp_73-zp_75 ; Out: four bytes in the outgoing ring, pendingOwnCmdCount $929F incremented once ; Called from: $183D, $349C, $4A4A, $4C0D, $5646 and overlay A ($79BF, $8438) ; ---------------------------------------------------------------------- queueCmd4: jsr queueCmdByte ; 560A 4-byte command: queue the command code held in A ... lda cmdParam0 ; 560D ... then cmdParam0, and fall into queueCmd3 for the rest ; ---------------------------------------------------------------------- ; queueCmd3 - Queues a 3-byte command: the code in A followed by cmdParam1 and cmdParam2 (the commands ; of length 3 are $87, $88, $8E, $90, $9A, $A3 and $A5). It is also the tail of queueCmd4, which ; arrives here with A = cmdParam0 and its command code already in the ring, which is why the count is ; incremented here. ; In: A = command code (or cmdParam0 when entered from queueCmd4), cmdParam1/cmdParam2 ; Out: three bytes in the outgoing ring, pendingOwnCmdCount $929F incremented once ; Called from: $1939, $3C48, $4ACD, $6630 and both $6F00 overlays ($7667, $81A4, $84E9, $850E, $8544, ; $85D8) ; ---------------------------------------------------------------------- queueCmd3: inc pendingOwnCmdCount ; 560F count the command once (queueCmd4 arrives here having already queued its code) jsr queueCmdByte ; 5612 queue the command code, or cmdParam0 when entered from queueCmd4 lda cmdParam1 ; 5615 second argument jsr queueCmdByte ; 5617 queue it lda cmdParam2 ; 561A third argument jmp queueCmdByte ; 561C queue it and return to the caller ; ---------------------------------------------------------------------- ; queueCmd2 - Queues a 2-byte command: the code in A followed by cmdParam0 (the commands of length 2 ; are the unit, group and reason commands $82-$86, $89, $8C, $91, $93-$96, $9B, $9D, $9F-$A2 and $A4). ; In: A = command code, cmdParam0 zp_73 ; Out: two bytes in the outgoing ring, pendingOwnCmdCount $929F incremented once ; Called from: $1AA9, $1C22, $20B8, $4B01, $5545, $5FA3, $62DE, $64E1, $65CD, $8340 and $C7A7 ; ---------------------------------------------------------------------- queueCmd2: inc pendingOwnCmdCount ; 561F count the command in pendingOwnCmdCount jsr queueCmdByte ; 5622 queue the command code held in A lda cmdParam0 ; 5625 its single argument jmp queueCmdByte ; 5627 queue it and return to the caller ; outPacketFill: byteTable, 1 bytes. work variable of sendOutgoingPacket (bytes placed in the outgoing ; packet) outPacketFill: .byte $00 ; 562A . number of bytes already placed in the outgoing packet by sendOutgoingPacket ; ---------------------------------------------------------------------- ; sendOutgoingPacket - Builds the packet for the next lock-step exchange and hands it to the comm ; module. If our drone is under joystick control, is not paused and has not finished, it is first ; stepped one cell and an $8D heading/column/row report is queued. Whole commands are then copied out ; of the outgoing ring into the comm module's transmit buffer at $E027 as long as the packet stays ; below 5 bytes; a $92 is dropped while droneArrivalFlags bit 6 says the drone detonated instead of ; arriving, and an $8E found while no drone flight is in progress flushes the entire ring and sends an ; empty packet. Finally $E01F is set to the packet length and the comm module is asked to transmit. ; In: outgoingCmdRing $9065 with $908D/$908E, droneControlFlags $920D, droneArrivalFlags $92F9, ; pauseState ; Out: $E027.., $E01F, outgoingCmdReadIndex $908E, outPacketFill $562A; self-modifies $566B ; Called from: executeTurnCommands $4F02, disconnectFromOpponent $1C25 and overlay A $7979 ; ---------------------------------------------------------------------- sendOutgoingPacket: bit droneControlFlags ; 562B $920D bit 6 = our own drone is under joystick control bvc L_5649 ; 562E no drone of ours to steer lda pauseState ; 5630 zp_BE is non-zero while either side is paused bne L_5649 ; 5632 do not step the drone while the game is paused lda droneArrivalFlags ; 5634 $92F9 bit 7 = the drone has already finished its flight bmi L_5649 ; 5637 nothing left to report jsr steerDrone ; 5639 overlay $731E: turn droneHeading toward the joystick and move the drone one cell (also mapGenerator6F00:forestBlockClassSw) jsr getDroneMapPosition ; 563C overlay $81FE: cmdParam1/cmdParam2 = droneViewOrigin + (6,5) = the drone's own map cell (also mapGenerator6F00:waitFramesServicingComm) lda droneHeading ; 563F $920B = the current 0-7 compass heading sta cmdParam0 ; 5642 first argument of the report lda #$8D ; 5644 command $8D DRONE POSITION jsr queueCmd4 ; 5646 queue heading, column and row ; ---------------------------------------------------------------------- ; Fill the outgoing packet from the command ring. Only whole commands are copied and the packet holds ; at most 4 command bytes, so a long burst of orders is spread over several exchanges. ; ---------------------------------------------------------------------- L_5649: ldy outgoingCmdReadIndex ; 5649 $908E = the read cursor of the outgoing ring lda #$00 ; 564C 0 sta outPacketFill ; 564E the packet starts out empty L_5651: cpy outgoingCmdWriteIndex ; 5651 has the read cursor caught the write cursor? beq L_56A3 ; 5654 the ring is empty - send what we have lda outgoingCmdRing,y ; 5656 the next command byte in the ring sta L_566A+1 ; 5659 patch it into the LDA # at $566A so the special-case tests below can look at it jsr getCommandLength ; 565C $4D9F: total length of the command from commandLengthTable, also left in $92EE clc ; 565F clear carry for the add adc outPacketFill ; 5660 how long the packet would be with this command in it cmp #$05 ; 5663 a packet carries at most 4 command bytes bcs L_56A3 ; 5665 it would not fit - leave it in the ring until next exchange ldx outPacketFill ; 5667 X = the write offset inside the packet buffer L_566A: lda #$FF ; 566A immediate operand patched at $5659 with the command byte ; ---------------------------------------------------------------------- ; Two special cases are filtered out here rather than at queue time, because whether they are still ; valid only becomes clear when the packet is built. ; ---------------------------------------------------------------------- cmp #$92 ; 566C $92 = 'the drone reached its target' bne L_5679 ; 566E not a $92 bit droneArrivalFlags ; 5670 $92F9 bit 6 = the drone detonated instead of being shot down bvc L_5689 ; 5673 bit 6 clear: the shot-down report is still valid, so copy it dex ; 5675 drop the stale $92: back the packet cursor up so the INX at $5696 cancels out jmp L_568F ; 5676 step over it in the ring without copying anything L_5679: cmp #$8E ; 5679 $8E = 'detonate the drone' bne L_5689 ; 567B not an $8E bit droneControlFlags ; 567D $920D bit 7 = a drone flight of ours really is in progress bmi L_5689 ; 5680 it is - copy the detonate command normally ldy outgoingCmdWriteIndex ; 5682 stale detonate: throw the whole ring away by moving the read cursor to the write cursor ... ldx #$00 ; 5685 ... and send an empty packet this exchange beq L_56A6 ; 5687 always taken ; ---------------------------------------------------------------------- ; Copy the remaining bytes of this command from the ring into the packet, wrapping the ring index at ; 40. ; ---------------------------------------------------------------------- L_5689: lda outgoingCmdRing,y ; 5689 copy one byte of the command sta txPacketBuffer,x ; 568C $E027 is the comm module's 7-byte transmit buffer (also trainerAiE000:txPacketBuffer) L_568F: iny ; 568F advance the ring cursor cpy #$28 ; 5690 $28 = 40 bytes in the ring bcc L_5696 ; 5692 no wrap needed ldy #$00 ; 5694 wrap back to the start L_5696: inx ; 5696 advance the packet cursor dec currentCommandLength ; 5697 $92EE counts the bytes of this command down bne L_5689 ; 569A copy the rest of the command stx outPacketFill ; 569C remember how full the packet is cpx #$04 ; 569F is there room for at least one more byte? bcc L_5651 ; 56A1 yes - look at the next command in the ring L_56A3: ldx outPacketFill ; 56A3 X = the finished packet length L_56A6: sty outgoingCmdReadIndex ; 56A6 the ring is consumed up to here stx txPacketLength ; 56A9 $E01F = the number of bytes the comm module should transmit (also trainerAiE000:txPacketLength) ldx #$01 ; 56AC comm function 1 = request a send jmp commJumpTable ; 56AE $E000; the modem driver queues the packet, the solo trainer throws it away (also trainerAiE000:commRequestEntry) ; ---------------------------------------------------------------------- ; pollMenuSession - Reached while a link is up but no game is running (commSessionState $0B9C bit 7): ; if the carrier is gone the session is torn down, otherwise the menu-level packet handler runs. Note ; that the commandHandlerTable slot for command $99 also points here, but nothing in the game ever ; queues a $99, so that entry is unreachable filler. ; In: the comm module's carrier byte $E03C ; Out: falls into handleMenuSessionPacket, or jumps to disconnectFromOpponent ; Called from: processCommandExchange $4DCE and $4E3E ; ---------------------------------------------------------------------- pollMenuSession: lda linkStatus ; 56B1 the comm module's carrier state byte (also trainerAiE000:linkStatus) and #$40 ; 56B4 bit 6 = carrier present bne handleMenuSessionPacket ; 56B6 still connected: service the menu-level traffic jmp disconnectFromOpponent ; 56B8 $1C17: carrier lost - tear the session down and hang up ; ---------------------------------------------------------------------- ; handleMenuSessionPacket - Menu-level receive path: re-arms the 10-frame link poll delay and returns ; unless a packet has arrived. A 2-byte [$8C,$05] means the opponent hung up, so we hang up too; ; [$8C,$02] means he is ready for a new game and puts 'OPPONENT READY FOR NEW GAME.' on the status ; line, rate limited by the middle byte of the frame counter so the message cannot repeat every ; exchange. Every other packet just falls into completeExchange to be consumed. ; In: $E01D, $E01E, $E020/$E021, frameCounterMid zp_46 ; Out: linkPollDelay $9163 = 10, message $1F, zp_46 reset; falls into completeExchange ; Called from: pollMenuSession (branch at $56B6) and the text engine's pause handler $C737 ; ---------------------------------------------------------------------- handleMenuSessionPacket: ldy #$0A ; 56BB 10 frames sty linkPollDelay ; 56BD $9163: do not poll the link again for 10 frames lda exchangeFlags ; 56C0 $E01D exchange flags (also trainerAiE000:commInterfaceBlock) bmi L_56C6 ; 56C3 bit 7 = a packet from the opponent has arrived rts ; 56C5 nothing received - come back next poll L_56C6: lda rxPacketLength ; 56C6 $E01E = length of the received packet (also trainerAiE000:rxPacketLength) cmp #$02 ; 56C9 only 2-byte menu packets mean anything here bne completeExchange ; 56CB wrong length - just finish the exchange lda rxPacketBuffer ; 56CD first byte of the received packet (also trainerAiE000:rxPacketBuffer) cmp #$8C ; 56D0 $8C = session control bne completeExchange ; 56D2 not a session-control packet lda rxPacketArg1 ; 56D4 the reason byte (also trainerAiE000:rxPacketArg1) cmp #$05 ; 56D7 reason 5 = the opponent hung up bne L_56DE ; 56D9 not a hang-up jmp hangUpModem ; 56DB $1B33: drop our end of the line too L_56DE: cmp #$02 ; 56DE reason 2 = READY FOR A NEW GAME bne completeExchange ; 56E0 some other reason - ignore it lda frameCounterMid ; 56E2 zp_46, the middle byte of the free-running frame counter, is abused as a crude rate limit cmp #$03 ; 56E4 about 3*256 frames since the last announcement bcc completeExchange ; 56E6 too soon - say nothing this time lda #$1F ; 56E8 message $1F jsr queueMessage ; 56EA 'OPPONENT READY FOR NEW GAME.' lda #$00 ; 56ED 0 sta frameCounterMid ; 56EF restart the rate limit (and, as a side effect, the frame counter's middle byte) ; ---------------------------------------------------------------------- ; completeExchange - Makes sure the packet exchange currently in flight finishes. Nothing pending ; returns at once; an already completed exchange is consumed; if our own packet is out or a send is ; already requested it only waits; otherwise an empty packet is sent to give the opponent something to ; answer. It then spins for up to $4B ticks of linkWaitTimer, servicing the status line, until the ; exchange completes, and consumes it. A timeout hangs the modem up. ; In: $E01D exchange flags, linkSessionState $0B9C ; Out: $E01F, linkWaitTimer zp_7F, the comm module's exchange state; or jumps to hangUpModem ; Called from: $1B2A, readyForNewGameHandshake $572B, handleMenuSessionPacket (four branches) and ; $EE06 ; ---------------------------------------------------------------------- completeExchange: ldy exchangeFlags ; 56F1 $E01D exchange flags (also trainerAiE000:commInterfaceBlock) beq L_572A ; 56F4 nothing in flight - nothing to finish tya ; 56F6 look at the flags and #$40 ; 56F7 bit 6 = the exchange has completed bne L_5725 ; 56F9 just consume it tya ; 56FB look at the flags again and #$20 ; 56FC bit 5 = our packet has already gone out bne L_5712 ; 56FE only his answer is missing - wait for it tya ; 5700 look at the flags again and #$01 ; 5701 bit 0 = a send has already been requested bne L_5712 ; 5703 wait for it lda #$00 ; 5705 0 sta txPacketLength ; 5707 $E01F = 0: nothing to say, so send a zero-length packet (also trainerAiE000:txPacketLength) lda linkSessionState ; 570A A carries the session state down to the comm module ldx #$01 ; 570D comm function 1 = request a send jsr commJumpTable ; 570F hand it over (also trainerAiE000:commRequestEntry) L_5712: lda #$4B ; 5712 $4B = 75 ticks; zp_7F is decremented every 8th frame, so roughly ten seconds sta linkWaitTimer ; 5714 arm the timeout L_5716: jsr updateStatusMessage ; 5716 $C4EF keeps the status line, chat and clock alive while we spin bit exchangeFlags ; 5719 $E01D exchange flags (also trainerAiE000:commInterfaceBlock) bvs L_5725 ; 571C bit 6: the exchange completed lda linkWaitTimer ; 571E the timeout counts down in the raster IRQ bne L_5716 ; 5720 keep waiting jmp hangUpModem ; 5722 the opponent never answered - hang up the modem L_5725: ldx #$02 ; 5725 comm function 2 = acknowledge and clear the finished exchange jmp commJumpTable ; 5727 $E000, then return to our caller (also trainerAiE000:commRequestEntry) L_572A: rts ; 572A nothing to do ; ---------------------------------------------------------------------- ; readyForNewGameHandshake - Run before a new game over an existing link: finishes any pending ; exchange, puts 'WAITING FOR OPPONENT...' up as a persistent message and then repeatedly sends ; [$8C,$02] and waits for the exchange to complete. When the opponent's packet is also [$8C,$02] the ; session state becomes 2 (game session), the message is cleared and the status line is wiped; ; [$8C,$05] means he hung up; anything else is consumed and the READY packet is sent again. ; In: $E01D/$E01E/$E020/$E021 ; Out: linkSessionState $0B9C = 2, persistentMessageId $92A1 cleared; or jumps to hangUpModem ; Called from: overlay A $795A ; ---------------------------------------------------------------------- readyForNewGameHandshake: jsr completeExchange ; 572B make sure no half-finished exchange is left over from the previous game lda #$0B ; 572E message $0B sta persistentMessageId ; 5730 'WAITING FOR OPPONENT...' stays on the status line for the whole handshake jsr queueMessage ; 5733 show it now L_5736: jsr buildReadyPacket ; 5736 put $8C,$02 = READY FOR A NEW GAME in the transmit buffer ldx #$01 ; 5739 comm function 1 = request a send jsr commJumpTable ; 573B hand it over (also trainerAiE000:commRequestEntry) L_573E: jsr updateStatusMessage ; 573E service the status line while waiting bit exchangeFlags ; 5741 $E01D exchange flags (also trainerAiE000:commInterfaceBlock) bvc L_573E ; 5744 spin until bit 6 says the exchange completed lda rxPacketLength ; 5746 length of the packet that came back (also trainerAiE000:rxPacketLength) cmp #$02 ; 5749 a menu packet is exactly 2 bytes bne L_5774 ; 574B anything else: consume it and try again lda rxPacketBuffer ; 574D first byte of his packet (also trainerAiE000:rxPacketBuffer) cmp #$8C ; 5750 $8C = session control bne L_5774 ; 5752 not session control - try again lda rxPacketArg1 ; 5754 his reason byte (also trainerAiE000:rxPacketArg1) cmp #$02 ; 5757 2 = he is ready for a new game as well bne L_576D ; 5759 not a READY - check for a hang-up lda #$02 ; 575B 2 = a game session has been agreed sta linkSessionState ; 575D $0B9C session state lda #$00 ; 5760 0 sta persistentMessageId ; 5762 take the WAITING message off the line ldx #$02 ; 5765 comm function 2 = consume the finished exchange jsr commJumpTable ; 5767 hand it over (also trainerAiE000:commRequestEntry) jmp clearMessageLine ; 576A $C39F wipes the status line and returns to overlay A's caller L_576D: cmp #$05 ; 576D reason 5 = he hung up instead bne L_5774 ; 576F some other reason - consume it and resend jmp hangUpModem ; 5771 $1B33: drop our end of the line too L_5774: ldx #$02 ; 5774 comm function 2 = consume whatever it was jsr commJumpTable ; 5776 hand it over (also trainerAiE000:commRequestEntry) jmp L_5736 ; 5779 and send our READY packet again ; ---------------------------------------------------------------------- ; buildReadyPacket - Fills the comm module's transmit buffer with the 2-byte packet $8C,$02 (session ; control, reason 2 = READY FOR A NEW GAME) and sets the transmit length $E01F to 2. ; In: none ; Out: $E027 = $8C, $E028 = $02, $E01F = 2 ; Called from: readyForNewGameHandshake $5736 ; ---------------------------------------------------------------------- buildReadyPacket: lda #$8C ; 577C command $8C session control sta txPacketBuffer ; 577E first byte of the comm module's transmit buffer (also trainerAiE000:txPacketBuffer) lda #$02 ; 5781 reason 2 = READY FOR A NEW GAME sta txPacketArg1 ; 5783 second byte of the transmit buffer (also trainerAiE000:txPacketArg1) sta txPacketLength ; 5786 $E01F = 2 bytes to transmit (the same value as the reason byte only by coincidence) (also trainerAiE000:txPacketLength) rts ; 5789 the packet is ready to be sent ; ---------------------------------------------------------------------- ; recordExchangeToFilm - Appends one lock-step exchange to the game film held in the RAM under the I/O ; area. Nothing is written when the exchange carried no commands, when the battle is not running, or ; when the film buffer is full. A record is [ (splitIndex << 4) | length ] [ time stamp, 1 or 2 bytes ; ] [ length command bytes ]; the split index says how many of the bytes belong to the first player, ; so playback can restore the own/remote flag exactly. Every byte goes through writeFilmByte, which ; banks the I/O area out around the store, and filmPtr is advanced by the length of the record at the ; end. ; In: mergedCmdLength $92EB, mergedCmdSplitIndex $92EC, mergedCmdBuffer $908F, filmPtr zp_BC/zp_BD, ; filmEndPtr $92F1/$92F2, gamePhase zp_B1 ; Out: film bytes written, filmPtr advanced; X = length, Y = record size ; Called from: executeTurnCommands $4EFA ; ---------------------------------------------------------------------- recordExchangeToFilm: lda mergedCmdLength ; 578A $92EB = number of command bytes merged for this exchange beq L_57BD ; 578D nothing happened - do not waste a film record lda gamePhase ; 578F zp_B1 is zero only during the battle bne L_57BD ; 5791 the setup screens and menus are not filmed jsr compareFilmPtrToEnd ; 5793 $591E: C = 1 once filmPtr has reached filmEndPtr bcs L_57BD ; 5796 the film buffer in the RAM under the I/O area is full ldx #$00 ; 5798 X = index into mergedCmdBuffer ldy #$00 ; 579A Y = offset within the film record being written lda mergedCmdSplitIndex ; 579C $92EC = how many of the merged bytes belong to the first player asl a ; 579F shift it ... asl a ; 57A0 ... into ... asl a ; 57A1 ... the high ... asl a ; 57A2 ... nibble of the header byte ora mergedCmdLength ; 57A3 low nibble = the total length; both always fit in four bits jsr writeFilmByte ; 57A6 $5893 stores at (filmPtr),Y with $01 = $34 so the RAM under $D000 is visible iny ; 57A9 next byte of the record jsr writeFilmTimestamp ; 57AA $57BE: one or two bytes of clock delta plus the sub-tick index; it advances Y itself ; ---------------------------------------------------------------------- ; Copy the exchange's command bytes verbatim; playback feeds exactly these bytes back through ; executeTurnCommands, which is why a film replays identically whether the opponent was a person or ; the solo trainer. ; ---------------------------------------------------------------------- L_57AD: lda mergedCmdBuffer,x ; 57AD copy the merged command bytes verbatim - this is what playback re-executes jsr writeFilmByte ; 57B0 write it to the film iny ; 57B3 next film byte inx ; 57B4 next command byte cpx mergedCmdLength ; 57B5 all of them written? bcc L_57AD ; 57B8 no - keep copying jsr advanceFilmPtr ; 57BA $57E7: filmPtr += Y, the length of the record just written L_57BD: rts ; 57BD done ; ---------------------------------------------------------------------- ; writeFilmTimestamp - Writes the time stamp of one game-film record at (filmPtr),Y. The stamp is the ; game clock, stored as a 4-bit delta against the clock of the previous record whenever the clock has ; moved by 0..14 ticks (the usual case, since the clock ticks down once per round), otherwise as a ; full byte. The low nibble of the first byte always carries commandSliceIndex ($923C), the number of ; the 10-unit slice of the round this exchange belongs to; a zero high nibble is what tells the reader ; that an absolute clock byte follows. The previous clock is kept in the SBC operand at $57C3, which ; startGameSession ($7911) clears when a new film starts. ; In: Y = offset of the next free byte inside the record; gameClock $91CB; commandSliceIndex $923C; ; filmPtr $BC/$BD; the previous clock in the operand L_57C2+1 ($57C3) ; Out: 1 or 2 bytes written into the film buffer under the I/O area; Y advanced past them; L_57C2+1 ; updated to the current clock ; Called from: recordExchangeToFilm ($57AA) for every recorded exchange and finalizeFilm ($5808) for ; the end-of-film marker. The playback counterpart is inside processCommandExchange at ; $4DE1-$4E04. ; ---------------------------------------------------------------------- writeFilmTimestamp: lda gameClock ; 57BE current game clock (a round counter that runs down to 0: 63, 127 or 254 at kick-off depending on the game type) clc ; 57C1 clear carry so the SBC below subtracts one extra: delta = clock - previousClock - 1 L_57C2: sbc #$00 ; 57C2 operand L_57C2+1 ($57C3) = clock stored with the previous record, reset to 0 by startGameSession ($7911) cmp #$F1 ; 57C4 delta $F1..$FF means the clock fell by 0..14 ticks, which fits into one nibble bcc L_57D2 ; 57C6 branch when the clock moved by more than 14 ticks (or not backwards at all): the long two-byte form is needed asl a ; 57C8 shift the delta nibble up into bits 4-7 of the time-stamp byte (1 of 4) asl a ; 57C9 shift the delta nibble up (2 of 4) asl a ; 57CA shift the delta nibble up (3 of 4) asl a ; 57CB shift the delta nibble up (4 of 4); the low nibble is now free ora commandSliceIndex ; 57CC low nibble = command slice number (unitOrderIndex/10, 0-9); playback compares it at $4DE3 and rejects the record if it does not match jmp L_57DC ; 57CF one byte is enough - go and store it L_57D2: lda commandSliceIndex ; 57D2 long form: a first byte whose high nibble is 0 tells the reader that an absolute clock byte follows jsr writeFilmByte ; 57D5 store it at (filmPtr),Y in the RAM hidden under the I/O area iny ; 57D8 step to the next byte of the record lda gameClock ; 57D9 second byte of the long form = the absolute game clock L_57DC: jsr writeFilmByte ; 57DC store the time-stamp byte lda gameClock ; 57DF the clock this record was stamped with... sta L_57C2+1 ; 57E2 ...becomes the base of the next record's delta (self-modifies the SBC operand at $57C3) iny ; 57E5 count the time-stamp byte that was just written rts ; 57E6 back to the caller with Y past the time stamp ; ---------------------------------------------------------------------- ; advanceFilmPtr - Adds the record length in Y to the 16-bit film write pointer filmPtr ($BC/$BD), so ; the next record starts where this one ended. ; In: Y = number of bytes just written; filmPtr $BC/$BD ; Out: filmPtr advanced by Y; A clobbered ; Called from: processCommandExchange ($4E30) after replaying a record, recordExchangeToFilm ($57BA) ; and finalizeFilm ($580B). ; ---------------------------------------------------------------------- advanceFilmPtr: tya ; 57E7 Y = number of bytes the record occupied clc ; 57E8 plain 16-bit add, no carry in adc filmPtr ; 57E9 add to the low byte of the film pointer sta filmPtr ; 57EB store it back bcc L_57F1 ; 57ED skip the high byte unless the add wrapped past $FF inc filmPtrHi ; 57EF carry into the high byte of the film pointer L_57F1: rts ; 57F1 done ; ---------------------------------------------------------------------- ; finalizeFilm - Closes a game film that was being recorded: appends the end-of-film marker record ; ($FE = the game played itself out, $FF = it was aborted) with a time stamp if the buffer still has ; room, then rewinds and fills in the header at $D000 - total film length, format id, final clock, ; both 16-bit scores and the checksum over $D000-$D032 - and finally leaves filmPtr pointing at the ; end of the recording again. ; In: filmPtr $BC/$BD, filmEndPtr $92F1/$92F2, gameEndReason $0BA8 (bit 0 = aborted), gameVersionByte ; $0B94, gameClock $91CB, the two scores $92A2-$92A5, playerSide $0B9F ; Out: end marker appended, header bytes $D000/$D001 (length), $D02F (format id), $D031 (final clock), ; $D00D-$D010 (scores) and $D033 (checksum) written; filmEndPtr = end of the recording; filmPtr = ; filmEndPtr ; Called from: endGameToMainMenu ($1B18) only, and only when a film was actually being recorded. ; ---------------------------------------------------------------------- finalizeFilm: jsr compareFilmPtrToEnd ; 57F2 C=1 when the recording has already reached the $DFF0 limit bcs L_580E ; 57F5 buffer full - there is no room for a marker, just write the header ldy #$FE ; 57F7 $FE = 'the game reached its own end'; playback at $4E0B simply stops on it lda gameEndReason ; 57F9 why the game ended: 0 running, 1 aborted, $08 time up, $A0/$C0 a comcen was knocked out and #$01 ; 57FC bit 0 is set only for reason 1 = the game was aborted beq L_5801 ; 57FE played out to the end - keep the $FE marker iny ; 5800 $FF = aborted game; playback at $4E0F turns this into finishFilmPlayback L_5801: tya ; 5801 the marker byte ldy #$00 ; 5802 offset 0 of the record = where a normal record keeps its length byte jsr writeFilmByte ; 5804 write the marker iny ; 5807 offset 1 jsr writeFilmTimestamp ; 5808 the marker carries a time stamp like any other record jsr advanceFilmPtr ; 580B filmPtr += 2 or 3 bytes of marker record ; ---------------------------------------------------------------------- ; Rewind and fill in the 52-byte film header at $D000. Offsets used here: $00/$01 length, ; $2F format id, $31 final clock, $0D-$10 the two scores, $33 checksum. The rest of the header ; ($02/$03 command-stream offset, $04 solo flag, $05 side, $06-$0B map name, $0C options, $11-$24 ; player names, $2B-$2D map seed, $30 starting clock) is written by initFilmStream, ; writeFilmSetupSnapshot and the save-film code in overlay A. ; ---------------------------------------------------------------------- L_580E: jsr saveFilmEndAndRewind ; 580E filmEndPtr = end of the recording, then filmPtr back to $D000 ldy #$00 ; 5811 header offset 0 lda filmEndPtr ; 5813 low byte of the end address... sec ; 5816 ...minus the start of the buffer sbc #$00 ; 5817 $D000 low byte: length = filmEndPtr - $D000 tax ; 5819 park the low byte of the length in X lda filmEndPtrHi ; 581A high byte of the end address... sbc #$D0 ; 581D ...minus $D0 = high byte of the length pha ; 581F keep the high byte on the stack, writeFilmByte needs A txa ; 5820 low byte of the length back into A jsr writeFilmByte ; 5821 $D000 = film length, low byte iny ; 5824 header offset 1 pla ; 5825 high byte of the length jsr writeFilmByte ; 5826 $D001 = film length, high byte lda gameVersionByte ; 5829 $0B94 = the game/film format id also used in the link version handshake ldy #$2F ; 582C header offset $2F jsr writeFilmByte ; 582E $D02F = format id; verifyFilmHeader refuses a film whose id differs from this build's iny ; 5831 offset $30 already holds the clock at kick-off (writeFilmSetupSnapshot wrote it) iny ; 5832 step over it to offset $31 lda gameClock ; 5833 clock left on the board when the game ended jsr writeFilmByte ; 5836 $D031 = final clock; finishFilmPlayback reads it back ldx playerSide ; 5839 start with the local player's score (playerSide 0 or 1) ldy #$10 ; 583C header offset $10 = high byte of the recorder's own score ; ---------------------------------------------------------------------- ; Write both 16-bit scores, own side first: $D00F/$D010 = this machine's score, ; $D00D/$D00E = the opponent's. X alternates between the two sides with EOR #$01 and Y walks ; down $10, $0F, $0E, $0D. finishFilmPlayback reads them back in exactly the same order, so a film ; always shows the scores from the point of view of whoever is watching it. ; ---------------------------------------------------------------------- L_583E: lda sideScoreHi,x ; 583E score high byte of side X ($92A4 + side) jsr writeFilmByte ; 5841 store it dey ; 5844 next header offset down lda sideScoreLo,x ; 5845 score low byte of side X ($92A2 + side) jsr writeFilmByte ; 5848 store it dey ; 584B next header offset down txa ; 584C side index eor #$01 ; 584D flip to the other side tax ; 584F back into X cpy #$0D ; 5850 stop once both score pairs ($D010..$D00D) are written bcs L_583E ; 5852 second pass for the opponent's score jsr computeFilmHeaderChecksum; 5854 sum $D000-$D032 into scratch18 (rewinds filmPtr as a side effect) ldy #$33 ; 5857 header offset $33 jsr writeFilmByte ; 5859 $D033 = header checksum; falls straight into setFilmPtrToEnd ; ---------------------------------------------------------------------- ; setFilmPtrToEnd - Points filmPtr ($BC/$BD) at filmEndPtr ($92F1/$92F2), i.e. back at the end of the ; film after the header has been read or written. ; In: filmEndPtr $92F1/$92F2 ; Out: filmPtr = filmEndPtr; A clobbered ; Called from: the tail of finalizeFilm (fall-through from $5859) and finishFilmPlayback ($59F9). ; ---------------------------------------------------------------------- setFilmPtrToEnd: lda filmEndPtr ; 585C end of the recording, low byte sta filmPtr ; 585F into the film pointer lda filmEndPtrHi ; 5861 end of the recording, high byte sta filmPtrHi ; 5864 into the film pointer rts ; 5866 done ; ---------------------------------------------------------------------- ; saveFilmEndAndRewind - Remembers the current film position as the end of the recording (filmEndPtr = ; filmPtr) and then rewinds filmPtr to the start of the buffer at $D000, ready for header access. ; In: filmPtr $BC/$BD ; Out: filmEndPtr $92F1/$92F2 = old filmPtr; filmPtr = $D000; A clobbered ; Called from: finalizeFilm ($580E) when a recording is closed and finishFilmPlayback ($59D0) when a ; replay reaches the end. ; ---------------------------------------------------------------------- saveFilmEndAndRewind: lda filmPtr ; 5867 current film position, low byte sta filmEndPtr ; 5869 becomes the end-of-film limit lda filmPtrHi ; 586C current film position, high byte sta filmEndPtrHi ; 586E becomes the end-of-film limit jmp rewindFilmPtr ; 5871 and rewind to $D000 so the header can be read or written ; ---------------------------------------------------------------------- ; readFilmByte - Reads one byte of the game film out of the 4 KB of RAM that lies underneath the I/O ; area ($D000-$DFFF). Interrupts are disabled, the CIA2 interrupt mask (the modem NMI) is cleared, ; $01 is switched from $35 to $34 so the RAM shows through, the byte is fetched and everything is put ; back, including the mask the comm module keeps in $E033. X is preserved through the self-modified ; LDX # at $5890. ; In: Y = offset from filmPtr $BC/$BD ; Out: A = the byte; X, Y, filmPtr unchanged; CPU_PORT back at $35 and CIA2_ICR back at the comm ; module's mask ; Called from: the film playback path in processCommandExchange ($4DD4-$4E23), verifyFilmHeader, ; computeFilmHeaderChecksum and finishFilmPlayback in this file, from the film directory/load code ; of overlay A ($7CDB-$7D35) and from the modem driver's $EE00 tail ($EE54-$EEAB). ; ---------------------------------------------------------------------- readFilmByte: stx L_5890+1 ; 5874 self-modify the LDX # at $5891 so X survives the call ldx #$7F ; 5877 $7F clears every mask bit when written to a CIA interrupt-control register sei ; 5879 no interrupts while the I/O area is banked away stx CIA2_ICR ; 587A disable all CIA2 interrupt sources, i.e. the modem UART's NMI nop ; 587D 3-cycle gap: the mask is written twice as a guard against an NMI arriving mid-store stx CIA2_ICR ; 587E second write of the mask ldx #$34 ; 5881 $34 = RAM everywhere except that the KERNAL/BASIC ROMs stay out; the $D000 I/O block disappears stx CPU_PORT ; 5883 switch the film buffer into view lda (filmPtr),y ; 5885 read the film byte inc CPU_PORT ; 5887 $34 -> $35 with one instruction: I/O visible again ldx ciaIcrShadow ; 5889 the comm module's copy of the CIA2 interrupt mask it wants installed (also trainerAiE000:ciaIcrShadow) stx CIA2_ICR ; 588C re-enable the modem NMI sources cli ; 588F interrupts back on L_5890: ldx #$FF ; 5890 operand patched at $5874: restores the caller's X rts ; 5892 A holds the film byte ; ---------------------------------------------------------------------- ; writeFilmByte - The write half of readFilmByte: stores A at (filmPtr),Y in the RAM under the I/O ; area, with the same 'sei / kill the CIA2 NMI / $01=$34 / restore' dance. X is preserved through the ; self-modified LDX # at $58AF. ; In: A = byte to store; Y = offset from filmPtr $BC/$BD ; Out: one byte written to the film buffer; A, X, Y unchanged ; Called from: recordExchangeToFilm and writeFilmTimestamp ($57A6-$57DC), finalizeFilm ($5804-$5859), ; writeFilmSetupSnapshot ($5936), initFilmStream ($59A1/$59A7) and the save-film code of overlay A ; ($79F9-$7AD0). ; ---------------------------------------------------------------------- writeFilmByte: stx L_58AF+1 ; 5893 self-modify the LDX # at $58B0 so X survives the call ldx #$7F ; 5896 $7F clears every mask bit of a CIA interrupt-control register sei ; 5898 no interrupts while the I/O area is banked away stx CIA2_ICR ; 5899 disable all CIA2 interrupt sources (the modem NMI) nop ; 589C 3-cycle gap between the two mask writes stx CIA2_ICR ; 589D second write of the mask ldx #$34 ; 58A0 $34 = the $D000 I/O block replaced by the RAM underneath stx CPU_PORT ; 58A2 switch the film buffer into view sta (filmPtr),y ; 58A4 store the film byte inc CPU_PORT ; 58A6 $34 -> $35: I/O visible again ldx ciaIcrShadow ; 58A8 the comm module's wanted CIA2 interrupt mask (also trainerAiE000:ciaIcrShadow) stx CIA2_ICR ; 58AB re-enable the modem NMI sources cli ; 58AE interrupts back on L_58AF: ldx #$FF ; 58AF operand patched at $5893: restores the caller's X rts ; 58B1 done ; ---------------------------------------------------------------------- ; copyPageUnderIo - Copies one 256-byte page with the I/O area banked out, used to move the unit ; tables in and out of the film's start-of-game snapshot. Source and destination are the operands of ; the LDA abs,Y at $58C3/$58C4 and the STA abs,Y at $58C6/$58C7; the caller patches them first. As ; disassembled both instructions show $FFFB (irqVectorHi) because that is the unpatched byte pair ; sitting in the file. ; In: source address in the operand at $58C3/$58C4, destination in the operand at $58C6/$58C7 ; Out: 256 bytes copied; A, X, Y clobbered; CPU_PORT and CIA2_ICR restored ; Called from: writeFilmSetupSnapshot ($5952/$596B) here, from overlay A ($7F87, $8184) and from the ; modem driver's $EE00 tail ($EE73, $EE8C); the operand bytes are also patched by all three. ; ---------------------------------------------------------------------- copyPageUnderIo: ldx #$7F ; 58B2 $7F clears every mask bit of a CIA interrupt-control register sei ; 58B4 no interrupts while the I/O area is banked away stx CIA2_ICR ; 58B5 disable all CIA2 interrupt sources (the modem NMI) nop ; 58B8 3-cycle gap between the two mask writes stx CIA2_ICR ; 58B9 second write of the mask ldx #$34 ; 58BC $34 = the $D000 I/O block replaced by the RAM underneath stx CPU_PORT ; 58BE bank the film buffer in ldy #$00 ; 58C0 copy 256 bytes, index 0 upwards L_58C2: lda irqVectorHi,y ; 58C2 source operand at $58C3/$58C4, patched by the caller (shown as $FFFB = the unpatched bytes) L_58C5: sta irqVectorHi,y ; 58C5 destination operand at $58C6/$58C7, patched by the caller (shown as $FFFB = the unpatched bytes) iny ; 58C8 next byte bne L_58C2 ; 58C9 loop until Y wraps back to 0, i.e. after all 256 bytes inc CPU_PORT ; 58CB $34 -> $35: I/O visible again ldx ciaIcrShadow ; 58CD the comm module's wanted CIA2 interrupt mask (also trainerAiE000:ciaIcrShadow) stx CIA2_ICR ; 58D0 re-enable the modem NMI sources cli ; 58D3 interrupts back on rts ; 58D4 done ; ---------------------------------------------------------------------- ; rewindFilmPtr - Sets filmPtr ($BC/$BD) to $D000, the first byte of the film / saved-game buffer in ; the RAM under the I/O area. ; In: - ; Out: filmPtr = $D000; A clobbered ; Called from: resetGameVariables ($0C5B), verifyFilmHeader, computeFilmHeaderChecksum, ; writeFilmSetupSnapshot, initFilmStream, the tail of saveFilmEndAndRewind ($5871) and the modem ; driver's $EE00 tail ($EE99). ; ---------------------------------------------------------------------- rewindFilmPtr: lda #$00 ; 58D5 low byte of $D000 sta filmPtr ; 58D7 into the film pointer lda #$D0 ; 58D9 high byte of $D000 - the film buffer starts right under the VIC registers sta filmPtrHi ; 58DB into the film pointer rts ; 58DD done ; ---------------------------------------------------------------------- ; verifyFilmHeader - Sanity check on a film that has just been loaded from disk: recomputes the ; checksum over header bytes $D000-$D032 and compares it with the stored one at $D033, then checks ; that the format id at $D02F matches this build's gameVersionByte ($0B94). On success it returns the ; film's solo/modem flag from $D004. ; In: the film header in the RAM under I/O ; Out: C=0 and A = $D004 (the isSoloTrainer flag the film was recorded with) when the film is good; ; C=1 when it is not, and the caller prints 'NOT A GAME FILM!' (message $09) ; Called from: the LOAD GAME FILM code of overlay A ($81F4). ; ---------------------------------------------------------------------- verifyFilmHeader: jsr rewindFilmPtr ; 58DE point filmPtr at the start of the header jsr computeFilmHeaderChecksum; 58E1 sum $D000-$D032 into scratch18 ldy #$33 ; 58E4 header offset $33 = the stored checksum jsr readFilmByte ; 58E6 read it cmp scratch18 ; 58E9 compare with the checksum just computed bne L_58F7 ; 58EB mismatch: this is not a valid film ldy #$2F ; 58ED header offset $2F = the format id the film was written with jsr readFilmByte ; 58EF read it cmp gameVersionByte ; 58F2 must equal this build's gameVersionByte $0B94 beq L_58F9 ; 58F5 same version - accept the film L_58F7: sec ; 58F7 C=1 tells the caller to reject the file rts ; 58F8 reject L_58F9: ldy #$04 ; 58F9 header offset $04 = the solo-trainer flag stored with the film jsr readFilmByte ; 58FB read it into A for the caller clc ; 58FE C=0 = the film is valid rts ; 58FF done ; ---------------------------------------------------------------------- ; computeFilmHeaderChecksum - Adds up the 51 header bytes $D000-$D032 into scratch18 ($18). The ADC ; inside the loop is never preceded by a CLC, so the carry out of each byte feeds into the next one; ; readFilmByte is careful not to disturb the carry. Also leaves filmPtr rewound to $D000. ; In: the film header in the RAM under I/O ; Out: scratch18 = the checksum; filmPtr = $D000; A/Y clobbered ; Called from: finalizeFilm ($5854) when writing a film and verifyFilmHeader ($58E1) when checking ; one. ; ---------------------------------------------------------------------- computeFilmHeaderChecksum: jsr rewindFilmPtr ; 5900 point filmPtr at $D000 so Y is a plain header offset lda #$00 ; 5903 start the running sum at zero sta scratch18 ; 5905 scratch18 accumulates the checksum ldy #$32 ; 5907 walk header offsets $32 down to $00 - 51 bytes, everything except the checksum byte itself clc ; 5909 clear the carry once; from here it chains from byte to byte L_590A: jsr readFilmByte ; 590A fetch header byte Y (readFilmByte leaves the carry alone) adc scratch18 ; 590D add it to the running sum, carrying in from the previous add sta scratch18 ; 590F keep the sum dey ; 5911 next header offset down bpl L_590A ; 5912 loop until offset 0 has been added and Y goes negative rts ; 5914 scratch18 now holds the checksum ; ---------------------------------------------------------------------- ; waitForExchangeIdle - Spins on the per-frame command exchange until the packet exchange in flight ; has completed, which the exchange code signals by clearing linkTimeoutCounter ($9223); ; checkLinkTimeout sets bit 7 of that counter while a packet is outstanding. ; In: linkTimeoutCounter $9223 ; Out: returns when $9223 is zero; everything serviceCommandExchange touches ; Called from: overlay A after sending setup commands ($797C, $7C09, $7C1C). ; ---------------------------------------------------------------------- waitForExchangeIdle: jsr serviceCommandExchange ; 5915 one pass of the lock-step exchange plus the status-line service ($4DA9) lda linkTimeoutCounter ; 5918 non-zero while a packet exchange is still outstanding (bit 7 is set by checkLinkTimeout) bne waitForExchangeIdle ; 591B keep servicing the link until the exchange settles rts ; 591D exchange complete ; ---------------------------------------------------------------------- ; compareFilmPtrToEnd - 16-bit unsigned compare of filmPtr against filmEndPtr. While recording, ; filmEndPtr is the $DFF0 buffer limit, so C=1 means the film buffer is full; while playing back it is ; the real end of the film, so C=1 means the replay has run out of records. ; In: filmPtr $BC/$BD, filmEndPtr $92F1/$92F2 ; Out: C=1 when filmPtr >= filmEndPtr; A clobbered ; Called from: processCommandExchange ($4DC1) on the playback path, recordExchangeToFilm ($5793) and ; finalizeFilm ($57F2). ; ---------------------------------------------------------------------- compareFilmPtrToEnd: lda filmPtr ; 591E film pointer, low byte cmp filmEndPtr ; 5920 compare with the limit, low byte (sets the borrow for the SBC below) lda filmPtrHi ; 5923 film pointer, high byte sbc filmEndPtrHi ; 5925 subtract the limit's high byte: C=1 when filmPtr >= filmEndPtr rts ; 5928 only the carry matters to the caller ; ---------------------------------------------------------------------- ; writeFilmSetupSnapshot - Writes the start-of-game snapshot into the film header so that a replay can ; rebuild the battlefield exactly: the 20 setup bytes $92A2-$92B5 (scores, recycler mode and position, ; terrain points, drones, missiles, comcen speeds) go to header offsets $34-$47, the eight 100-byte ; unit arrays $F640-$F95F (column, row, flags, type, destination column/row, waypoint column/row) are ; copied to $D048-$D367, and the game clock is stored at $D030. ; In: $92A2-$92B5, the unit arrays at $F640-$F95F, gameClock $91CB ; Out: $D034-$D047, $D048-$D367 and $D030 filled; filmPtr left at $D000; the copyPageUnderIo operands ; at $58C3-$58C7 left patched ; Called from: startGameSession in overlay A ($7B80) when a recorded game begins. ; ---------------------------------------------------------------------- writeFilmSetupSnapshot: lda #$34 ; 5929 point filmPtr at header offset $34 ($D034)... sta filmPtr ; 592B ...low byte lda #$D0 ; 592D ...high byte sta filmPtrHi ; 592F filmPtr = $D034, so Y counts from 0 through the setup block ldy #$00 ; 5931 start at the first setup byte ; ---------------------------------------------------------------------- ; Copy the 20 bytes of per-game settings $92A2-$92B5 (both 16-bit scores, recycler mode, recycler ; column and row per side, terrain points, drones left, missiles left, comcen reveal timers and the ; two comcen speed classes) into header offsets $34-$47. ; ---------------------------------------------------------------------- L_5933: lda sideScoreLo,y ; 5933 $92A2 + Y: the 20-byte block of per-game settings starting with the two 16-bit scores jsr writeFilmByte ; 5936 store it in the header iny ; 5939 next setting cpy #$14 ; 593A 20 bytes, $92A2 through $92B5 bcc L_5933 ; 593C loop over the whole settings block ; ---------------------------------------------------------------------- ; Snapshot the first eight unit arrays ($F640-$F95F, 800 bytes) into $D048-$D367. The copy is done ; with four 256-byte page copies whose destination is always source - $25F8; because 800 is not a ; multiple of 256 the four pages overlap, so only the first $20 bytes of the very first copy ; ($F640-$F65F) actually survive - the rest is redone by the three copies that follow. The arrays ; covered are column, row, flags/facing, type, destination column, destination row, waypoint column ; and waypoint row, i.e. everything needed to place the units again at the start of a replay. ; ---------------------------------------------------------------------- lda #$48 ; 593E destination low byte $48 sta L_58C5+1 ; 5940 into the STA operand of copyPageUnderIo ($58C6) lda #$D0 ; 5943 destination high byte $D0 sta L_58C5+2 ; 5945 -> destination $D048 lda #$40 ; 5948 source low byte $40 sta L_58C2+1 ; 594A into the LDA operand of copyPageUnderIo ($58C3) lda #$F6 ; 594D source high byte $F6 sta L_58C2+2 ; 594F -> source $F640, the unit column array jsr copyPageUnderIo ; 5952 copy $F640-$F73F to $D048-$D147 (only the first $20 bytes of this survive - see the block note) jsr getFilmSnapshotDestAddr ; 5955 A=$68, X=$D2: the top destination page $D268 sta L_58C5+1 ; 5958 destination low byte stx L_58C5+2 ; 595B destination high byte jsr getFilmSnapshotSrcAddr ; 595E A=$60, X=$F8: the matching top source page $F860 sta L_58C2+1 ; 5961 source low byte stx L_58C2+2 ; 5964 source high byte lda #$02 ; 5967 three more page copies to do (counter runs 2, 1, 0) sta scratch19 ; 5969 scratch19 is the page counter L_596B: jsr copyPageUnderIo ; 596B copy one page; first $F860->$D268, then $F760->$D168, then $F660->$D068 dec L_58C2+2 ; 596E step the source down one page dec L_58C5+2 ; 5971 step the destination down one page dec scratch19 ; 5974 one page done bpl L_596B ; 5976 loop while the counter is still non-negative - three iterations jsr rewindFilmPtr ; 5978 back to $D000 so Y is a plain header offset again ldy #$30 ; 597B header offset $30 lda gameClock ; 597D clock at kick-off; finalizeFilm later stores the final clock at offset $31 jmp writeFilmByte ; 5980 $D030 = starting clock, and return through writeFilmByte ; ---------------------------------------------------------------------- ; getFilmSnapshotDestAddr - Returns the constant address $D268 in A (low) and X (high): the highest ; destination page of the unit snapshot inside the film header, and the page just below the start of ; the command stream at $D368. It exists as a routine so that the modem driver's $EE00 tail can reach ; the same constant when it restores a snapshot. ; In: - ; Out: A = $68, X = $D2 ; Called from: writeFilmSetupSnapshot ($5955), initFilmStream ($59AA) and the modem driver's $EE00 ; tail ($EE76). ; ---------------------------------------------------------------------- getFilmSnapshotDestAddr: lda #$68 ; 5983 low byte of $D268 ldx #$D2 ; 5985 high byte of $D268 - the last page of the unit snapshot in the film header rts ; 5987 done ; ---------------------------------------------------------------------- ; getFilmSnapshotSrcAddr - Returns the constant address $F860 in A (low) and X (high): the highest ; source page of the unit snapshot, i.e. the last full page of the eight unit arrays that end at ; $F95F. Shared with the snapshot restore code in the modem driver's $EE00 tail. ; In: - ; Out: A = $60, X = $F8 ; Called from: writeFilmSetupSnapshot ($595E) and the modem driver's $EE00 tail ($EE7F). ; ---------------------------------------------------------------------- getFilmSnapshotSrcAddr: lda #$60 ; 5988 low byte of $F860 ldx #$F8 ; 598A high byte of $F860 - the top source page of the unit-array snapshot rts ; 598C done ; ---------------------------------------------------------------------- ; setFilmLimitToBufferEnd - Sets filmEndPtr to $DFF0, the highest address a recording may reach. ; While a film is being recorded compareFilmPtrToEnd tests against this value, so it is the point ; where recording silently stops; the last 16 bytes of the $D000 page are left unused. ; In: - ; Out: filmEndPtr $92F1/$92F2 = $DFF0; A clobbered ; Called from: startGameSession in overlay A ($78D1) at the start of every game. ; ---------------------------------------------------------------------- setFilmLimitToBufferEnd: lda #$F0 ; 598D low byte of $DFF0 sta filmEndPtr ; 598F into the recording limit lda #$DF ; 5992 high byte of $DFF0 - the last 16 bytes of the buffer are kept free sta filmEndPtrHi ; 5994 into the recording limit rts ; 5997 done ; ---------------------------------------------------------------------- ; initFilmStream - Prepares the film for recording once the snapshot is in place: writes the offset of ; the first command record ($0368) into header bytes $D002/$D003 and sets filmPtr to $D368, the first ; byte after the 800-byte unit snapshot. ; In: - ; Out: $D002/$D003 = $0368; filmPtr = $D368; A/X/Y clobbered ; Called from: startGameSession in overlay A ($7B8C), right after writeFilmSetupSnapshot. ; ---------------------------------------------------------------------- initFilmStream: jsr rewindFilmPtr ; 5998 point filmPtr at $D000 so Y is a plain header offset lda #$68 ; 599B low byte of the command-stream offset $0368 ldx #$02 ; 599D X builds the high byte of the offset and then of the address ldy #$02 ; 599F header offset $02 jsr writeFilmByte ; 59A1 $D002 = $68 iny ; 59A4 header offset $03 inx ; 59A5 X = 3 txa ; 59A6 high byte of the offset $0368 jsr writeFilmByte ; 59A7 $D003 = $03; the film records therefore start $0368 bytes into the file jsr getFilmSnapshotDestAddr ; 59AA A=$68, X=$D2 - the last snapshot page sta filmPtr ; 59AD filmPtr low byte = $68 inx ; 59AF one page past the snapshot: $D2 -> $D3 stx filmPtrHi ; 59B0 filmPtr = $D368, the first free byte for command records rts ; 59B2 done ; ---------------------------------------------------------------------- ; isCmdRingNearlyFull - Works out how much room is left in the 40-byte outgoing command ring at ; $9065-$908C and reports C=1 when fewer than 5 bytes are free, which is less than the longest command ; plus its arguments. checkFirePressed calls it first, so while the ring is congested the fire button ; simply does nothing and no order can be lost. Equal read and write indices mean the ring is empty, ; which is reported as plenty of room. ; In: outgoingCmdWriteIndex $908D, outgoingCmdReadIndex $908E ; Out: C=1 = nearly full (refuse new orders), C=0 = room to spare; A clobbered ; Called from: checkFirePressed ($0EB1) only. ; ---------------------------------------------------------------------- isCmdRingNearlyFull: lda outgoingCmdReadIndex ; 59B3 index the transmitter has reached sec ; 59B6 prepare a true subtraction sbc outgoingCmdWriteIndex ; 59B7 free space = readIndex - writeIndex when the reader is ahead beq L_59CC ; 59BA both indices equal: the ring is empty, so it is certainly not full bcs L_59C8 ; 59BC reader ahead of the writer: the difference is already the free space lda outgoingCmdReadIndex ; 59BE writer has wrapped past the reader... clc ; 59C1 prepare a plain add adc #$28 ; 59C2 ...so add the ring size of 40 bytes ($9065-$908C)... sec ; 59C4 prepare a true subtraction sbc outgoingCmdWriteIndex ; 59C5 ...before taking the difference again L_59C8: cmp #$05 ; 59C8 5 bytes is the safety margin - the longest command is 4 bytes bcc L_59CE ; 59CA less than 5 bytes free: report 'nearly full' L_59CC: clc ; 59CC C=0: there is room for another order rts ; 59CD done L_59CE: sec ; 59CE C=1: the caller must refuse the order rts ; 59CF done ; ---------------------------------------------------------------------- ; finishFilmPlayback - End of a game film: reached when the replay runs past the last record, when the ; record length byte is $FF or when the aborted-game end marker is read. It rewinds, restores the ; game clock and both 16-bit scores from the header (which finalizeFilm wrote when the film was made), ; marks the game as over, queues message 2 'END OF FILM.', locks the player out of the controls and ; starts the short end-of-game countdown. ; In: the film header at $D000, playerSide $0B9F ; Out: gameClock $91CB, scores $92A2-$92A5, gameEndReason $0BA8 incremented, filmPtr back at the end, ; gameOverFlags $923F = $C0, persistentMessageId $92A1 and chatMessageLine $0548 = $9E, ; gameEndCountdown $92C9 = $7D ; Called from: processCommandExchange ($4DC6) - both from the end-of-buffer test at $4DC4 and from the ; end-marker test at $4E0F. ; ---------------------------------------------------------------------- finishFilmPlayback: jsr saveFilmEndAndRewind ; 59D0 remember where the film ended and rewind filmPtr to $D000 ldy #$31 ; 59D3 header offset $31 = the clock the recorded game ended on jsr readFilmByte ; 59D5 read it sta gameClock ; 59D8 show the same time left as the original game did ldx playerSide ; 59DB the viewer's own side (0 or 1) ldy #$10 ; 59DE header offset $10 = high byte of the recorder's score, which becomes the viewer's ; ---------------------------------------------------------------------- ; Read both 16-bit scores back out of the header in the same own-side-first order finalizeFilm wrote ; them in, so a replay always shows US and THEM from the point of view of whoever is watching. ; ---------------------------------------------------------------------- L_59E0: jsr readFilmByte ; 59E0 score high byte from the header sta sideScoreHi,x ; 59E3 $92A4 + side dey ; 59E6 next header offset down jsr readFilmByte ; 59E7 score low byte from the header sta sideScoreLo,x ; 59EA $92A2 + side dey ; 59ED next header offset down txa ; 59EE side index eor #$01 ; 59EF flip to the other side tax ; 59F1 back into X cpy #$0D ; 59F2 stop once both score pairs ($D010..$D00D) have been read bcs L_59E0 ; 59F4 second pass for the other side inc gameEndReason ; 59F6 0 means 'a game is running'; anything else ends it - here it just has to become non-zero jsr setFilmPtrToEnd ; 59F9 put filmPtr back at the end so compareFilmPtrToEnd keeps saying 'finished' lda #$02 ; 59FC message 2 = 'END OF FILM.' ($8707, sound 3) jsr queueMessage ; 59FE queue it for the status line lda #$C0 ; 5A01 bits 7 and 6: game over, and bit 6 blocks the fire button in checkFirePressedInteractive sta gameOverFlags ; 5A03 $923F = $C0 lda #$9E ; 5A06 $9E is a bit-7 terminator that prints nothing sta persistentMessageId ; 5A08 a negative persistentMessageId makes the status bar fall back to the CLICKS/US/THEM score line sta chatMessageLine ; 5A0B and blank the chat line at $0548 lda #$7D ; 5A0E $7D: the IRQ counts this up towards $80 every 8 frames, so about 24 frames of grace sta gameEndCountdown ; 5A10 non-zero gameEndCountdown also stops runGameFrames and the cursor handling rts ; 5A13 done ; ---------------------------------------------------------------------- ; checkLinkTimeout - Watchdog used while the game is waiting for the opponent's packet. It arms ; linkTimeoutCounter ($9223) by setting bit 7, which is what makes the raster IRQ count it up (one ; step every 8 frames, or every 16 while gamePhase is $FF, saturating at $FF). Below $E1 it just ; returns C=0 and the caller goes back to its frame loop; from $E1 on it puts 'PHONE TROUBLE.' on the ; status line and returns C=1, which makes the caller busy-wait instead; at $FF the link is declared ; dead - during the battle it hangs up, redials and starts the wait again, in the setup phases it ; disconnects, says 'TRY AGAIN.' and jumps back to the main menu. ; In: linkTimeoutCounter $9223, gamePhase $B1 (bits 6-7 clear = the battle is running), ; sndVoiceSoundId $67C8 ; Out: C=1 = keep waiting in a tight loop, C=0 = come back next frame; $9223 bit 7 set; may set ; linkSessionState $0B9C = $FF, prevSoloTrainerFlag $0BA6 = $C8 and jump to returnToMainMenu ; Called from: waitForCarrier ($1BA3) and the wait-for-packet loop of processCommandExchange ($4E50). ; ---------------------------------------------------------------------- checkLinkTimeout: lda linkTimeoutCounter ; 5A14 how long the current packet has been outstanding L_5A17: ora #$80 ; 5A17 bit 7 arms the counter: updateFrameTimers ($12B4) only advances it while it is negative sta linkTimeoutCounter ; 5A19 write it back cmp #$E1 ; 5A1C $E1 = 97 steps past the armed value $80; at one step per 8 frames that is roughly 13 seconds of silence bcc L_5A57 ; 5A1E still early days - let the caller carry on with its normal frame loop cmp #$FF ; 5A20 $FF = the counter has saturated: the link is gone bne L_5A49 ; 5A22 between $E1 and $FE: only warn lda gamePhase ; 5A24 0 while the battle runs; bits 6-7 mark the side-selection, setup, film and menu phases and #$C0 ; 5A26 isolate those two phase bits beq L_5A42 ; 5A28 the battle is running: try to get the call back rather than throwing the game away lda #$FF ; 5A2A $FF = link lost sta linkSessionState ; 5A2C record it in the session state jsr disconnectFromOpponent ; 5A2F send the disconnect command and hang the modem up lda #$23 ; 5A32 message $23 = 'TRY AGAIN.' ($0608, sound 3) jsr showStatusMessage ; 5A34 show it on the status line lda #$C8 ; 5A37 $C8 in prevSoloTrainerFlag forces the GET OPPONENT ON PHONE prompt next time round sta prevSoloTrainerFlag ; 5A39 store it jsr waitFrames ; 5A3C A is still $C8, so this waits 200 frames before leaving the message jmp returnToMainMenu ; 5A3F abandon the game and rebuild the main menu L_5A42: jsr voicePauseReconnect ; 5A42 battle case: hang up, ask for a voice pause and dial again lda #$00 ; 5A45 restart the watchdog from zero... beq L_5A17 ; 5A47 ...always taken: re-enter at $5A17, which ORs in bit 7 and returns C=0 L_5A49: lda #$15 ; 5A49 sound $15 is the phone-trouble warble cmp sndVoiceSoundId ; 5A4B voice 0 of the sound driver beq L_5A55 ; 5A4E already complaining - do not restart the message lda #$22 ; 5A50 message $22 = 'PHONE TROUBLE.' ($05FA, sound $15) jsr showStatusMessage ; 5A52 show it on the status line L_5A55: sec ; 5A55 C=1: the caller keeps spinning here instead of running game frames rts ; 5A56 done L_5A57: clc ; 5A57 C=0: nothing wrong yet, carry on normally rts ; 5A58 done ; Overview-map tile patterns. On the strategic map each battlefield cell is only 2 multicolour pixels ; wide and 4 scanlines tall, so a whole cell is described by four bytes - one per scanline - each of ; which carries both horizontal halves of the character (bits 7-4 for the left cell of the pair, bits ; 3-0 for the right); the quadrant mask in quadrantPixelMask picks the half that is wanted. A pixel ; pair of %00 is background (VIC background 0 = 7 yellow), %01 is the screen RAM upper nibble (6 blue ; = own units), %10 the lower nibble (2 red = enemy units) and %11 the colour RAM nibble (the terrain ; colour). overviewPatternLo/Hi are a split pointer table indexed by the terrain index (map cell - ; $40, or cell - $20 for the $21-$3F contour set), 33 entries covering indices 0-$20; ; buildOverviewQuadrantNoColour reads them at $5C40/$5C44. overviewPatternLo: .byte $9B,$9F,$A3,$A7,$AB,$9F,$A3,$A7; 5A59 ........ terrain 0 = blank ($5A9B), 1-4 = the four lower-contour edges ($5A9F/A3/A7/AB) .byte $AB ; 5A61 terrain 5-8 repeat the same four contour edges, 9-12 all use the full-height stripe $5AAF .byte $AF,$AF,$AF,$AF,$B3,$B3,$B3; 5A62 ....... .byte $B3 ; 5A69 terrain 13-20 all use $5AB3 (a single solid scanline), 21-23 start the river set ($5AC7/CB/CF) .byte $B3,$B3,$B3,$B3,$C7,$CB,$CF; 5A6A ....... .byte $D3 ; 5A71 terrain 24 = $5AD3 (river), 25-28 = $5AC3 (river bank), 29-31 = $5ABB (forest stipple) .byte $C3,$C3,$C3,$C3,$BB,$BB,$BB; 5A72 ....... .byte $BB ; 5A79 terrain $20 = $5ABB, the last forest entry ; High bytes of the same 33 pointers - every pattern lives in this page, so all entries are $5A. overviewPatternHi: .byte $5A,$5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A7A ZZZZZZZZ all patterns are in page $5A (entries 0-7) .byte $5A ; 5A82 entries 8-15 .byte $5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A83 ZZZZZZZ .byte $5A ; 5A8A entries 16-23 .byte $5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A8B ZZZZZZZ .byte $5A ; 5A92 entries 24-31 .byte $5A,$5A,$5A,$5A,$5A,$5A,$5A; 5A93 ZZZZZZZ .byte $5A ; 5A9A entry 32, the last terrain index ; The 15 distinct 4-byte patterns, one row per pattern. Two of them are not reached through the ; pointer table at all: $5AB7 (solid) is the unit blob loaded at $5C14 and $5ABF (every other ; scanline) is the cloaked-unit blob loaded at $5C1C. In the bit art below $33 lights the right pixel ; of a half, $CC the left one and $FF both. overviewPatterns: .byte $00,$00,$00,$00 ; 5A9B .... $5A9B blank - terrain index 0 (map cell $40), all background .byte $00 ; 5A9F $5A9F contour edge: the lower two scanlines only .byte $00,$33,$33 ; 5AA0 .33 .byte $00 ; 5AA3 $5AA3 contour edge: lower two scanlines plus one pixel on scanline 1 .byte $CC,$33,$33 ; 5AA4 .33 .byte $33 ; 5AA7 $5AA7 contour edge: the top scanline only .byte $00,$00,$00 ; 5AA8 ... .byte $33 ; 5AAB $5AAB contour edge: top two scanlines .byte $CC,$00,$00 ; 5AAC ... .byte $33 ; 5AAF $5AAF full-height stripe - terrain indices 9-12 .byte $33,$33,$33 ; 5AB0 333 .byte $00 ; 5AB3 $5AB3 one solid scanline - terrain indices 13-$20's base shape .byte $FF,$00,$00 ; 5AB4 ... .byte $FF ; 5AB7 $5AB7 solid 2x4 blob = a unit; loaded directly at $5C14 and recoloured by the $55/$AA mask .byte $FF,$FF,$FF ; 5AB8 ... .byte $33 ; 5ABB $5ABB alternating stipple - the forest terrain .byte $CC,$33,$CC ; 5ABC .3. .byte $FF ; 5ABF $5ABF every other scanline = a cloaked unit; loaded directly at $5C1C .byte $00,$FF,$00 ; 5AC0 ... .byte $CC ; 5AC3 $5AC3 river bank .byte $FF,$FF,$33 ; 5AC4 ..3 .byte $33 ; 5AC7 $5AC7 river, notched at the top .byte $FF,$FF,$FF ; 5AC8 ... .byte $CC ; 5ACB $5ACB river, notched at the top on the other side .byte $FF,$FF,$FF ; 5ACC ... .byte $FF ; 5ACF $5ACF river, notched at the bottom .byte $FF,$FF,$33 ; 5AD0 ..3 .byte $FF ; 5AD3 $5AD3 river, notched at the bottom on the other side .byte $FF,$FF,$CC ; 5AD4 ... ; ---------------------------------------------------------------------- ; drawOverviewMap - Draws the whole strategic overview: the 40x40 battlefield squeezed into the 20x20 ; character window at screen (1,1)-(20,20), four map cells (a 2x2 block) per character. For every ; character it clears the 8-byte build buffer at $90B8, ORs in the four cell patterns as quadrants, ; overlays the quarter/midfield lines, copies the 8 scanlines into the $A000 bitmap, writes $62 into ; screen RAM (upper nibble 6 = blue for own units, lower nibble 2 = red for the enemy) and the terrain ; colour into colour RAM at +$4C00. ; In: the $F000 map, the unit arrays, the patched side branches at $5BF7/$5BFE ; Out: 400 characters of bitmap, screen RAM and colour RAM rewritten; vicBg0Shadow $9004 = 7 (yellow); ; mapCellPtr $48/$49, scratch18/scratch19 and glyphBuildBuffer clobbered ; Called from: manualLookupCheck ($09E3), the battlefield screen builder ($2682, $4020, $4137), ; overlay A/B in many places ($76B8, $7702, $774C, $7B0C, $7D5C, $83F8, $8553), the $0400 block ; ($0758) and the text engine ($C794). ; ---------------------------------------------------------------------- drawOverviewMap: lda #$07 ; 5AD7 colour 7 = yellow: the plain-ground background of the whole overview sta vicBg0Shadow ; 5AD9 VIC background 0 shadow; the IRQ copies it to $D021 lda #$00 ; 5ADC map pointer low byte... sta mapCellPtr ; 5ADE mapCellPtr low byte lda #$F0 ; 5AE0 ...and high byte: the first cell of the 40x40 map at $F000 sta mapCellPtrHi ; 5AE2 mapCellPtr high byte - the pointer now addresses map cell (0,0) lda #$01 ; 5AE4 first character row of the window sta scratch18 ; 5AE6 scratch18 = character row 1..20 ; ---------------------------------------------------------------------- ; Outer loop over the 20 character rows. Each pass covers two map rows; after the inner loop has ; walked 20 characters (40 map cells) the pointer is advanced by another 40 to skip the odd map row ; that the same characters already covered. ; ---------------------------------------------------------------------- L_5AE8: lda #$01 ; 5AE8 first character column of the window sta scratch19 ; 5AEA scratch19 = character column 1..20 ; ---------------------------------------------------------------------- ; Inner loop: build one overview character out of a 2x2 block of map cells. ; ---------------------------------------------------------------------- L_5AEC: ldx #$07 ; 5AEC 8 scanlines to clear lda #$00 ; 5AEE blank L_5AF0: sta glyphBuildBuffer,x ; 5AF0 clear scanline X of the character being built at $90B8 dex ; 5AF3 next scanline down bpl L_5AF0 ; 5AF4 until all 8 are blank ldx #$00 ; 5AF6 quadrant 0 = top-left ldy #$00 ; 5AF8 map offset 0 = the cell at (col, row) lda (mapCellPtr),y ; 5AFA read the map cell jsr buildOverviewQuadrant ; 5AFC OR its 2x4 pattern into the top-left quadrant and pick up its terrain colour iny ; 5AFF map offset 1 = the cell to the right inx ; 5B00 quadrant 1 = top-right lda (mapCellPtr),y ; 5B01 read the map cell jsr buildOverviewQuadrant ; 5B03 OR it into the top-right quadrant ldy #$28 ; 5B06 map offset $28 = 40 = the same column one map row down inx ; 5B08 quadrant 2 = bottom-left lda (mapCellPtr),y ; 5B09 read the map cell jsr buildOverviewQuadrant ; 5B0B OR it into the bottom-left quadrant iny ; 5B0E map offset $29 = the cell diagonally down-right inx ; 5B0F quadrant 3 = bottom-right lda (mapCellPtr),y ; 5B10 read the map cell jsr buildOverviewQuadrant ; 5B12 OR it into the bottom-right quadrant; its terrain colour is the one that ends up in colour RAM jsr calcCellPointers ; 5B15 bitmapPtr/screenPtr for character (scratch19, scratch18); returns Y = character row, X = column jsr drawOverviewQuarterLine ; 5B18 add the quarter or midfield line to scanline 0 if this character row carries one ldy #$07 ; 5B1B 8 scanlines to store L_5B1D: lda glyphBuildBuffer,y ; 5B1D scanline Y of the finished character sta (bitmapPtr),y ; 5B20 into the $A000 bitmap dey ; 5B22 next scanline bpl L_5B1D ; 5B23 until all 8 are written lda #$62 ; 5B25 $62: multicolour pair %01 -> colour 6 blue (own units), %10 -> colour 2 red (enemy units) ldy #$00 ; 5B27 one byte per character sta (screenPtr),y ; 5B29 into screen RAM at $8C00 lda screenPtrHi ; 5B2B screen RAM page... clc ; 5B2D carry must be clear before the page add below adc #$4C ; 5B2E ...+ $4C00 turns $8Cxx into $D8xx = colour RAM sta screenPtrHi ; 5B30 screenPtr now points at the colour RAM byte of this character lda tileColour ; 5B32 terrain colour of the last cell of the 2x2 block, left in tileColour by buildOverviewQuadrant sta (screenPtr),y ; 5B34 colour RAM supplies the %11 pixel pairs, i.e. the terrain itself lda mapCellPtr ; 5B36 advance the map pointer... clc ; 5B38 plain 16-bit add of the two-cell step adc #$02 ; 5B39 ...by two cells - one character wide sta mapCellPtr ; 5B3B store the advanced map pointer, low byte lda mapCellPtrHi ; 5B3D carry into the map pointer high byte adc #$00 ; 5B3F propagate the carry into the high byte sta mapCellPtrHi ; 5B41 store the advanced map pointer, high byte inc scratch19 ; 5B43 next character column lda scratch19 ; 5B45 the character column just finished cmp #$15 ; 5B47 columns 1 to 20 inclusive bcc L_5AEC ; 5B49 keep going along the row lda mapCellPtr ; 5B4B the row just drawn consumed 40 cells = one map row... clc ; 5B4D plain 16-bit add of the 40-cell step adc #$28 ; 5B4E ...so skip the second map row it also covered sta mapCellPtr ; 5B50 store the advanced map pointer, low byte lda mapCellPtrHi ; 5B52 carry into the map pointer high byte adc #$00 ; 5B54 propagate the carry into the high byte sta mapCellPtrHi ; 5B56 store the advanced map pointer, high byte inc scratch18 ; 5B58 next character row lda scratch18 ; 5B5A the character row just finished cmp #$15 ; 5B5C rows 1 to 20 inclusive bcs L_5B63 ; 5B5E all 20 rows done jmp L_5AE8 ; 5B60 next character row of the overview L_5B63: rts ; 5B63 the whole 20x20 overview has been redrawn ; Quadrant geometry of an overview character. Quadrant 0 = top-left, 1 = top-right, 2 = bottom-left, ; 3 = bottom-right; each is 2 multicolour pixels wide and 4 scanlines tall. quadrantPixelMask: .byte $F0,$0F,$F0,$0F ; 5B64 .... AND mask per quadrant: $F0 keeps the left pixel pair of the character, $0F the right one ; quadrantRowOffset: byteTable, 4 bytes. first scanline of quadrant 0-3: 0,0,4,4 (4 rows each) quadrantRowOffset: .byte $00,$00,$04,$04 ; 5B68 .... first scanline per quadrant: 0 for the two top quadrants, 4 for the two bottom ones ; ---------------------------------------------------------------------- ; drawOverviewQuarterLine - Puts the field markings back on top of an overview character. Modem Wars ; is played on a football pitch, and the overview shows the quarter lines at map rows 10, 20 and 30, ; which land on the first scanline of character rows 6, 11 and 16. On the midfield row (11) both ; pixel pairs of the character get colour 1, on rows 6 and 16 only the right-hand one; a half that ; already reads %1010 - a solid enemy-unit blob - is left alone so the line does not put a blue dot in ; the middle of a red unit. Only scanline 0 of the buffer at $90B8 is touched. ; In: Y = character row of the overview (1-20); glyphBuildBuffer $90B8 ; Out: glyphBuildBuffer[0] possibly modified; A/X/Y clobbered; self-modifies the operands at $5B95 and ; $5BA8 ; Called from: drawOverviewMap ($5B18) for every character and drawMapCell ($5CFD) when a single cell ; is redrawn. ; ---------------------------------------------------------------------- drawOverviewQuarterLine: ldx #$00 ; 5B6C X = 0 means 'draw both pixel pairs' cpy #$0B ; 5B6E character row 11 = map rows 20/21 - the midfield line beq L_5B7B ; 5B70 midfield: leave X at 0 so both halves get a marker cpy #$06 ; 5B72 character row 6 = map rows 10/11 - the first quarter line beq L_5B7A ; 5B74 row 6: only the right-hand pixel carries this quarter line cpy #$10 ; 5B76 character row 16 = map rows 30/31 - the third quarter line bne L_5BAC ; 5B78 any other row carries no field marking at all L_5B7A: dex ; 5B7A X = $FF: quarter lines are drawn in the right-hand pixel only L_5B7B: stx L_5B94+1 ; 5B7B self-modify the LDA # at $5B95 - it gates the left-hand half below lda glyphBuildBuffer ; 5B7E scanline 0 of the character being built and #$F0 ; 5B81 keep the left pixel pair (pixels 0 and 1) tay ; 5B83 hold it in Y while the right half is dealt with lda glyphBuildBuffer ; 5B84 scanline 0 again and #$0F ; 5B87 keep the right pixel pair (pixels 2 and 3) tax ; 5B89 hold it in X eor #$0A ; 5B8A %1010 = both pixels colour 2 = a solid enemy blob beq L_5B94 ; 5B8C leave an enemy unit alone rather than dotting it txa ; 5B8E the right half and #$0C ; 5B8F keep pixel 2, clear pixel 3 ora #$01 ; 5B91 pixel 3 (the rightmost of the character) := colour 1 tax ; 5B93 back into X L_5B94: lda #$FF ; 5B94 operand patched at $5B7B: $FF for a quarter line, $00 for the midfield line bmi L_5BA3 ; 5B96 quarter line: only the right-hand pixel is drawn, skip the left half tya ; 5B98 the left half eor #$A0 ; 5B99 %10100000 = both pixels colour 2 = a solid enemy blob beq L_5BA3 ; 5B9B leave an enemy unit alone tya ; 5B9D the left half again and #$C0 ; 5B9E keep pixel 0, clear pixel 1 ora #$10 ; 5BA0 pixel 1 := colour 1 tay ; 5BA2 back into Y L_5BA3: sty L_5BA7+1 ; 5BA3 self-modify the ORA # at $5BA8 with the finished left half txa ; 5BA6 the finished right half L_5BA7: ora #$FF ; 5BA7 operand patched at $5BA3: recombine the two halves sta glyphBuildBuffer ; 5BA9 write scanline 0 of the character back L_5BAC: rts ; 5BAC done (also the exit for rows that carry no line) ; ---------------------------------------------------------------------- ; buildOverviewQuadrant - Colour-aware entry of the overview cell renderer. It looks the terrain ; colour of map cell A up (terrainClassTable $07BE indexed by the terrain index, then ; terrainColourTable $07EA) and leaves it in tileColour ($32) for the caller to write into colour RAM, ; then falls into buildOverviewQuadrantNoColour to OR the pattern into quadrant X. A cell holding a ; unit is coloured after the terrain hidden under it. Cell $40 and any out-of-range value leave the ; colour untouched, so the character keeps the colour of the previous quadrant. ; In: A = map cell byte ($21-$6B terrain, or $80|unitIndex); X = quadrant 0-3; Y = anything ; (restored) ; Out: tileColour $32 = the terrain colour nibble; the quadrant ORed into glyphBuildBuffer $90B8; ; A/X/Y restored ; Called from: drawOverviewMap ($5AFC, $5B03, $5B0B, $5B12), once per cell of the 2x2 block. ; ---------------------------------------------------------------------- buildOverviewQuadrant: sty L_5C79+1 ; 5BAD stash Y in the LDY # at $5C7A so the exit can restore it stx L_5C77+1 ; 5BB0 stash the quadrant number in the LDX # at $5C78; $5C4F reads it back pha ; 5BB3 keep the raw cell byte for the pattern lookup tay ; 5BB4 test bit 7 of the cell byte bpl L_5BBA ; 5BB5 plain terrain cell - use it directly lda unitTerrainUnderAlias,y ; 5BB7 a unit stands here: colour after the terrain it hides ($FAF0 + index, reached as $FA70 + $80 + index) L_5BBA: sec ; 5BBA prepare a true subtraction of the terrain base sbc #$40 ; 5BBB terrain cells $41-$60 become indices 1-$20 beq L_5BD0 ; 5BBD cell $40 is the blank tile: keep the colour of the previous quadrant bcs L_5BC3 ; 5BBF cell above $40 - the index is already right adc #$20 ; 5BC1 cells below $40 (the $21-$3F contour set) become index cell-$20 L_5BC3: cmp #$21 ; 5BC3 33 valid terrain indices, 0 to $20 bcs L_5BD0 ; 5BC5 out of range: keep the colour of the previous quadrant tay ; 5BC7 index the class table with it ldx D_07BE,y ; 5BC8 terrainClassTable read as $07BE + index: class 0-8 (0 plain, 1 river, 2 forest, 3/5 contour, 4/6 hill, 7 recycler) lda terrainColourTable,x ; 5BCB $07EA: 7 yellow plain, 3 cyan river, 5 green forest, 8 orange contours and hills, 0 black sta tileColour ; 5BCE the caller stores this in colour RAM for the whole character L_5BD0: pla ; 5BD0 the cell byte again jmp L_5BDA ; 5BD1 enter buildOverviewQuadrantNoColour past the two saves we have already done ; ---------------------------------------------------------------------- ; buildOverviewQuadrantNoColour - ORs the 2x4 pixel pattern of map cell A into quadrant X of the ; 8-byte character buffer at $90B8, without touching the colour. Terrain cells pick their 4-byte ; pattern through overviewPatternLo/Hi. A cell holding a unit gets the solid blob $5AB7 (or $5ABF ; when the unit is cloaked, bit 7 of the waypoint-column array $F898) masked with overviewUnitPattern ; $922B, which is $55 (every pixel pair %01 = blue) for our own units and $AA (%10 = red) for the ; opponent's. A unit is drawn only if it is ours, if the omniscient replay view is on, if reveal-all ; is set or if it has been spotted (bit 7 of the energy array $F9C4); otherwise, and for dead units ; and while the comcen map system is knocked out ($9242 bit 7), the terrain hidden under the unit is ; drawn instead. ; In: A = map cell byte; X = quadrant 0-3; Y = anything (restored); overviewUnitPattern $922B, ; comcenBattleStatus $9242, filmPlaybackMode $0B9B, revealAllUnits $92FC, unit arrays ; $F76C/$F898/$F9C4/$FAF0 ; Out: four scanlines of glyphBuildBuffer $90B8 ORed; A/X/Y restored; self-modifies the operands at ; $5C19, $5C65, $5C66, $5C68, $5C6A, $5C74, $5C78, $5C7A ; Called from: drawMapCell ($5CF6) and, past its two save instructions, from buildOverviewQuadrant ; ($5BD1). ; ---------------------------------------------------------------------- buildOverviewQuadrantNoColour: sty L_5C79+1 ; 5BD4 stash Y in the LDY # at $5C7A so the exit can restore it stx L_5C77+1 ; 5BD7 stash the quadrant number in the LDX # at $5C78; $5C4F reads it back L_5BDA: ldy #$FF ; 5BDA $FF = no unit recolouring, let the pattern through unchanged sty L_5C67+1 ; 5BDC self-modify the AND # at $5C68 (the unit colour mask) tay ; 5BDF Y = the cell byte, so unit arrays are indexed through their $80-biased aliases bpl L_5C2B ; 5BE0 bit 7 clear = plain terrain: skip everything unit related lda comcenBattleStatus ; 5BE2 $9242 = the comcen's map system; bit 7 set means it has been knocked out bmi L_5C23 ; 5BE5 no map system: the overview shows bare terrain, units are invisible lda unitPathColTableAlias,y ; 5BE7 waypoint-column array $F898 + index (via the $F818 alias); bit 7 = the unit is cloaked sta L_5C18+1 ; 5BEA park it in the LDX # at $5C19 - the cloak test runs after the pattern has been chosen lda unitTypeTableAlias,y ; 5BED unit type array $F76C + index (via the $F6EC alias); bit 7 = destroyed bmi L_5C23 ; 5BF0 dead unit: draw the terrain it is lying on instead lda overviewUnitPattern ; 5BF2 $922B = $55: every pixel pair %01, i.e. screen RAM upper nibble = colour 6 blue cpy #$B2 ; 5BF5 $B2 = $80|50: C=1 for unit indices 50-99, the second half of the unit array D_5BF7: bcc L_5BFB ; 5BF7 opcode patched by patchOwnSideBranches ($7BE1): BCC for side 0, BCS for side 1, so it is taken for OUR units eor #$FF ; 5BF9 opponent's unit: $55 becomes $AA, every pixel pair %10 = screen RAM lower nibble = colour 2 red L_5BFB: sta L_5C67+1 ; 5BFB this mask recolours the solid $FF pattern (self-modifies the AND # at $5C68) D_5BFE: bcc L_5C14 ; 5BFE opcode patched by patchUnitSideBranches ($2A16), inverted when a replay is watching THEM: units of the watched side are always visible lda filmPlaybackMode ; 5C00 $0B9B: bit 7 = film playback, bits 0-1 = the viewpoint cmp #$82 ; 5C03 $80|2 = playback with the SHOW BOTH viewpoint bcs L_5C11 ; 5C05 omniscient replay: the enemy unit is drawn too lda revealAllUnits ; 5C07 $92FC bit 7 = reveal every unit (set when the game ends) bmi L_5C11 ; 5C0A reveal-all: draw the enemy unit lda unitEnergyTableAlias,y ; 5C0C energy array $F9C4 + index (via the $F944 alias); bit 7 = this enemy has been spotted bpl L_5C23 ; 5C0F never spotted: only the terrain it is standing on is shown L_5C11: lda L_5C67+1 ; 5C11 dead instruction - A is overwritten by the LDA # below and no flag is tested (leftover) L_5C14: lda #$B7 ; 5C14 pattern $5AB7 = the solid 2x4 unit blob ldy #$5A ; 5C16 high byte of the pattern address L_5C18: ldx #$FF ; 5C18 operand patched at $5BEA with the unit's waypoint-column byte; bit 7 = cloaked bpl L_5C49 ; 5C1A not cloaked: keep the solid blob lda #$BF ; 5C1C cloaked: pattern $5ABF, only every other scanline is drawn ldy #$5A ; 5C1E high byte of the pattern address jmp L_5C49 ; 5C20 go and plot it L_5C23: lda #$FF ; 5C23 $FF = no unit recolouring... sta L_5C67+1 ; 5C25 ...so the terrain pattern goes through untouched (self-modifies the AND # at $5C68) lda unitTerrainUnderAlias,y ; 5C28 terrain byte hidden underneath the unit ($FAF0 + index, via the $FA70 alias) ; ---------------------------------------------------------------------- ; Shared terrain path: turn the cell byte into a terrain index 0-$20 and fetch the matching 4-byte ; pattern out of the split pointer table at $5A59/$5A7A. ; ---------------------------------------------------------------------- L_5C2B: sec ; 5C2B prepare a true subtraction of the terrain base sbc #$40 ; 5C2C terrain cells $41-$60 become indices 1-$20 beq L_5C38 ; 5C2E cell $40 is the blank tile bcs L_5C34 ; 5C30 cell above $40 - the index is already right adc #$20 ; 5C32 cells below $40 (the $21-$3F contour set) become index cell-$20 L_5C34: cmp #$21 ; 5C34 33 valid terrain indices, 0 to $20 bcc L_5C3F ; 5C36 in range - look the pattern up L_5C38: lda #$9B ; 5C38 blank pattern $5A9B: nothing but background ldy #$5A ; 5C3A high byte of the pattern address jmp L_5C49 ; 5C3C go and plot it L_5C3F: tay ; 5C3F terrain index into Y lda overviewPatternLo,y ; 5C40 low byte of this terrain's 4-byte pattern pha ; 5C43 park it while the high byte is fetched lda overviewPatternHi,y ; 5C44 high byte (always $5A) tay ; 5C47 into Y for the store below pla ; 5C48 low byte back into A ; ---------------------------------------------------------------------- ; Plot the chosen 4-byte pattern into the quadrant: each of the four scanlines is masked with the unit ; colour mask, then with the half-of-the-character mask, and ORed into the build buffer. ; ---------------------------------------------------------------------- L_5C49: sta L_5C64+1 ; 5C49 self-modify the LDA abs,Y at $5C64 with the pattern address, low byte sty L_5C64+2 ; 5C4C ...and high byte ldx L_5C77+1 ; 5C4F the quadrant number saved on entry lda quadrantPixelMask,x ; 5C52 $F0 = the left pixel pair of the character, $0F = the right one sta L_5C69+1 ; 5C55 self-modify the second AND # at $5C6A lda quadrantRowOffset,x ; 5C58 0 for the top quadrants, 4 for the bottom ones tax ; 5C5B X = first scanline of the quadrant inside the 8-byte buffer clc ; 5C5C plain add, no carry in adc #$04 ; 5C5D each quadrant is 4 scanlines tall sta L_5C73+1 ; 5C5F self-modify the CPX # at $5C74 with the end scanline ldy #$00 ; 5C62 start at byte 0 of the pattern L_5C64: lda irqVectorHi,y ; 5C64 operand patched at $5C49: scanline Y of the chosen pattern L_5C67: and #$FF ; 5C67 operand patched at $5BDC/$5BFB/$5C25: $55 own unit, $AA enemy unit, $FF terrain L_5C69: and #$FF ; 5C69 operand patched at $5C55: keep only this quadrant's half of the byte ora glyphBuildBuffer,x ; 5C6B OR into the character being built at $90B8 sta glyphBuildBuffer,x ; 5C6E store it back iny ; 5C71 next pattern byte inx ; 5C72 next scanline of the character L_5C73: cpx #$FF ; 5C73 operand patched at $5C5F: first scanline + 4 bcc L_5C64 ; 5C75 four scanlines per quadrant L_5C77: ldx #$FF ; 5C77 operand patched at $5BB0/$5BD7: restore the caller's quadrant number L_5C79: ldy #$FF ; 5C79 operand patched at $5BAD/$5BD4: restore the caller's Y L_5C7B: rts ; 5C7B done (drawMapCell borrows this RTS for its blackout exit) ; ---------------------------------------------------------------------- ; drawMapCell - Redraws a single battlefield cell everywhere it can currently be seen. On the ; battlefield screen that means the 16x16 tile in the 7x5 tactical view (only when the cell is inside ; it) plus one 2x4-pixel quadrant of the strategic overview, which is rebuilt in place: the character ; is read back out of the bitmap, the quadrant's old pixels are cleared, the new pattern is ORed in, ; the field markings are restored and the character is written back. On the comcen radar screen it ; hands the work to drawScreen1Cell. Nothing is drawn at all during the comcen knock-out blackout. ; In: A = the map cell byte, X = map column, Y = map row (saved in savedRegA/X/Y $54-$56); ; currentScreen $90FB; viewOriginCol/Row $A5/$A6; comcenStunStatus $922A bit 6 ; Out: bitmap, screen RAM and colour RAM updated; A/X/Y restored to the values passed in; ; self-modifies the operands at $5CD1, $5CE9, $5CEF, $5CFA and $5CFC ; Called from: the unit redraw helpers at $3F89 and $437D and, as a tail jump, from $48AE - i.e. ; whenever a unit moves, is hit or is removed. ; ---------------------------------------------------------------------- drawMapCell: bit comcenStunStatus ; 5C7C $922A: bit 6 is set while the comcen knock-out blackout sequence runs bvs L_5C7B ; 5C7F blackout in progress - draw nothing (borrows the RTS at the end of the routine above) jsr saveRegsAXY ; 5C81 $C0E8: A/X/Y -> savedRegA/savedRegX/savedRegY = cell byte, column, row ldx currentScreen ; 5C84 0 = battlefield screen, 1 = comcen radar screen, 3 = menus beq L_5C8C ; 5C87 battlefield: the tactical view and the overview both need updating jmp L_5D0A ; 5C89 any other screen: skip straight to the screen-1 test L_5C8C: lda savedRegX ; 5C8C map column of the cell sec ; 5C8E prepare a true subtraction sbc viewOriginCol ; 5C8F position relative to the left edge of the tactical view cmp #$07 ; 5C91 the view is 7 cells wide; an unsigned compare also rejects columns left of the origin bcs L_5CA1 ; 5C93 outside the view - only the overview needs the update lda savedRegY ; 5C95 map row of the cell sec ; 5C97 prepare a true subtraction sbc viewOriginRow ; 5C98 position relative to the top edge of the tactical view cmp #$05 ; 5C9A the view is 5 cells tall bcs L_5CA1 ; 5C9C outside the view jsr drawViewportCell ; 5C9E redraw the 16x16 tile inside the tactical view ; ---------------------------------------------------------------------- ; Overview update: find the character that holds this cell, read it back out of the bitmap, erase just ; the quadrant belonging to the cell, OR the new pattern in, restore the field marking and write the ; character back. The colour RAM nibble is deliberately left alone - a single cell cannot change the ; colour of the 2x2 block it shares. ; ---------------------------------------------------------------------- L_5CA1: jsr restoreRegsAXY ; 5CA1 $C030: A/X/Y = cell byte, column, row again txa ; 5CA4 map column lsr a ; 5CA5 two map columns share one overview character clc ; 5CA6 the LSR above may have left the carry set - clear it before the add adc #$01 ; 5CA7 the overview window starts at screen column 1 tax ; 5CA9 X = character column tya ; 5CAA map row lsr a ; 5CAB two map rows share one overview character clc ; 5CAC the LSR above may have left the carry set - clear it before the add adc #$01 ; 5CAD the overview window starts at screen row 1 tay ; 5CAF Y = character row sty L_5CFB+1 ; 5CB0 self-modify the LDY # at $5CFC - the quarter-line pass needs the character row stx L_5CF9+1 ; 5CB3 self-modify the LDX # at $5CFA (the value it loads is never used - see $5CF9) jsr calcCellPointersXY ; 5CB6 bitmapPtr/screenPtr of that character cell ldy #$07 ; 5CB9 8 scanlines L_5CBB: lda (bitmapPtr),y ; 5CBB read scanline Y of the character out of the bitmap... sta glyphBuildBuffer,y ; 5CBD ...into the build buffer at $90B8 dey ; 5CC0 next scanline bpl L_5CBB ; 5CC1 until all 8 have been copied back lda savedRegY ; 5CC3 map row of the cell and #$01 ; 5CC5 odd row = the lower half of the character asl a ; 5CC7 quadrants 2 and 3 are the lower ones sta L_5CD0+1 ; 5CC8 self-modify the ADC # at $5CD1 lda savedRegX ; 5CCB map column of the cell and #$01 ; 5CCD odd column = the right half of the character clc ; 5CCF clear the carry for the quadrant add below L_5CD0: adc #$FF ; 5CD0 operand patched at $5CC8: quadrant = (row&1)*2 + (col&1), so 0-3 tax ; 5CD2 X = quadrant number pha ; 5CD3 keep it across the clearing loop lda quadrantPixelMask,x ; 5CD4 $F0 or $0F: the half of the byte this quadrant owns eor #$FF ; 5CD7 invert it into an AND mask that clears exactly those pixels sta L_5CE8+1 ; 5CD9 self-modify the AND # at $5CE9 lda quadrantRowOffset,x ; 5CDC first scanline of the quadrant, 0 or 4 tax ; 5CDF X = scanline index into the build buffer adc #$04 ; 5CE0 +4 scanlines; the carry is still clear from the ADC at $5CD0, whose result can never exceed 3 sta L_5CEE+1 ; 5CE2 self-modify the CPX # at $5CEF with the end scanline L_5CE5: lda glyphBuildBuffer,x ; 5CE5 scanline X of the character L_5CE8: and #$FF ; 5CE8 operand patched at $5CD9: blank this quadrant's pixels, leave the neighbour's alone sta glyphBuildBuffer,x ; 5CEA store it back inx ; 5CED next scanline L_5CEE: cpx #$FF ; 5CEE operand patched at $5CE2: first scanline + 4 bcc L_5CE5 ; 5CF0 four scanlines per quadrant pla ; 5CF2 quadrant number back off the stack tax ; 5CF3 into X for the call below lda savedRegA ; 5CF4 the new map cell byte jsr buildOverviewQuadrantNoColour; 5CF6 OR its pattern into the freshly cleared quadrant (the colour is left untouched) L_5CF9: ldx #$FF ; 5CF9 operand patched at $5CB3; dead - drawOverviewQuarterLine opens with LDX #$00 (leftover) L_5CFB: ldy #$FF ; 5CFB operand patched at $5CB0: the character row, which the quarter-line test needs jsr drawOverviewQuarterLine ; 5CFD put the quarter or midfield line back on scanline 0 if this row carries one ldy #$07 ; 5D00 8 scanlines L_5D02: lda glyphBuildBuffer,y ; 5D02 scanline Y of the rebuilt character sta (bitmapPtr),y ; 5D05 back into the $A000 bitmap dey ; 5D07 next scanline bpl L_5D02 ; 5D08 until all 8 are written L_5D0A: lda currentScreen ; 5D0A which screen is up cmp #$01 ; 5D0D screen 1 = the comcen radar/scanner map bne L_5D17 ; 5D0F not the radar screen - nothing more to do jsr restoreRegsAXY ; 5D11 A/X/Y = cell byte, column, row jsr drawScreen1Cell ; 5D14 redraw the cell in the radar window as well L_5D17: jmp restoreRegsAXY ; 5D17 leave with A/X/Y exactly as the caller passed them ; ---------------------------------------------------------------------- ; drawViewportCell - Draws one map cell as a 16x16 pixel tile (2x2 characters) in the 7x5 tactical ; view on the right of the battlefield screen. The screen position is character column 2*(col - ; viewOriginCol) + 25 and row 2*(row - viewOriginRow) + 1. renderMapTile reads the cell's own map ; coordinates out of viewOriginCol/Row, so those are borrowed for the duration and restored ; afterwards; it builds the 32-byte tile in the buffer at $04C8 and writes the four screen RAM colour ; bytes itself, and this routine copies the 32 bytes into the bitmap - the first 16 at the tile ; origin, the second 16 one character row (320 bytes) lower. ; In: savedRegA/X/Y ($54/$55/$56) = cell byte, map column, map row; viewOriginCol/Row $A5/$A6 ; Out: four character cells of the $A000 bitmap and the matching screen/colour RAM written; ; drawCellSaveCol/Row $90F6/$90F7 used as scratch; viewOriginCol/Row restored; self-modifies the ; operand at $5D58/$5D59 ; Called from: drawMapCell ($5C9E) and the view scroller scrollViewBitmap ($1F1B, $1F44) when a new ; row or column is exposed. ; ---------------------------------------------------------------------- drawViewportCell: lda viewOriginCol ; 5D1A current left edge of the tactical view sta drawCellSaveCol ; 5D1C keep it: viewOriginCol is about to be borrowed lda viewOriginRow ; 5D1F current top edge of the tactical view sta drawCellSaveRow ; 5D21 keep it too ldy savedRegY ; 5D24 map row of the cell ldx savedRegX ; 5D26 map column of the cell txa ; 5D28 map column of the cell sec ; 5D29 prepare a true subtraction sbc viewOriginCol ; 5D2A 0-6: position of the cell inside the 7-cell-wide view stx viewOriginCol ; 5D2C renderMapTile reads the cell's own column out of $A5 asl a ; 5D2E each cell is 2 characters wide clc ; 5D2F the ASL above may have set the carry - clear it before the add adc #$19 ; 5D30 the tactical view starts at screen character column 25 tax ; 5D32 X = character column of the tile tya ; 5D33 map row of the cell sec ; 5D34 prepare a true subtraction sbc viewOriginRow ; 5D35 0-4: position of the cell inside the 5-cell-tall view sty viewOriginRow ; 5D37 renderMapTile reads the cell's own row out of $A6 asl a ; 5D39 each cell is 2 characters tall clc ; 5D3A the ASL above may have set the carry - clear it before the add adc #$01 ; 5D3B the tactical view starts at screen character row 1 tay ; 5D3D Y = character row of the tile jsr calcCellPointersXY ; 5D3E bitmapPtr = the tile's top-left character, screenPtr = its screen RAM byte ldy #$00 ; 5D41 renderMapTile writes screen and colour RAM at (screenPtr),Y lda savedRegA ; 5D43 the map cell byte jsr renderMapTile ; 5D45 build the 32-byte 16x16 multicolour tile in $04C8 and colour the four screen cells lda bitmapPtr ; 5D48 bitmap destination, low byte... sta L_5D57+1 ; 5D4A ...into the STA operand at $5D58 lda bitmapPtrHi ; 5D4D ...high byte... sta L_5D57+2 ; 5D4F ...into the STA operand at $5D59 ldy #$00 ; 5D52 32 tile bytes, index 0 upwards L_5D54: lda play4Row12,y ; 5D54 byte Y of the tile buffer at $04C8 L_5D57: sta irqVectorHi,y ; 5D57 operand patched at $5D4A/$5D68: the current row of characters in the $A000 bitmap iny ; 5D5A next tile byte cpy #$20 ; 5D5B 32 bytes = four 8-byte character cells bcs L_5D74 ; 5D5D whole tile copied cpy #$10 ; 5D5F after 16 bytes the upper pair of characters is done bne L_5D54 ; 5D61 still in the upper row - keep copying clc ; 5D63 plain 16-bit add of the $130 step lda bitmapPtr ; 5D64 bitmap origin of the tile... adc #$30 ; 5D66 ...plus $130; Y is already $10, so the next store lands at origin + $140 = 320 bytes = one character row lower sta L_5D57+1 ; 5D68 new destination, low byte lda bitmapPtrHi ; 5D6B bitmap origin of the tile, high byte... adc #$01 ; 5D6D ...plus the carry and the $100 of the $130 step sta L_5D57+2 ; 5D6F new destination, high byte bne L_5D54 ; 5D72 always taken - a bitmap address in $A000-$BFFF never has a zero high byte L_5D74: lda drawCellSaveCol ; 5D74 the real left edge of the view sta viewOriginCol ; 5D77 put it back lda drawCellSaveRow ; 5D79 the real top edge of the view sta viewOriginRow ; 5D7C put it back rts ; 5D7E done ; ---------------------------------------------------------------------- ; drawScreen1Cell - Redraws one map cell on screen 1, the comcen radar/scanner map, where the window ; is 21 cells wide by 17 tall at character (3,3) and every cell is a single 8x8 glyph. If the cell ; holds our own comcen the window has to follow it, so the whole job is handed to the overlay routine ; at $7959; otherwise the cell is clipped against the window, the bitmap and colour RAM pointers are ; built from the row/column tables at $9300 with a +3 character offset, the radar palette is set (dark ; grey for our units and the terrain, white for the enemy) and the overlay routines at $74C3 and $7396 ; turn the cell byte into a glyph and plot it. viewOriginCol/Row are borrowed as the cell's ; coordinates while that happens. ; In: savedRegA/X/Y ($54/$55/$56) = cell byte, map column, map row; viewOriginCol/Row $A5/$A6 = ; top-left cell of the radar window; comcenUnit $9236 ; Out: one glyph in the bitmap and one byte in colour RAM; ; radarOwnUnitColour/radarEnemyUnitColour/radarTerrainColour $9237-$9239 set; bitmapPtr/screenPtr ; and drawCellSaveCol/Row $90F6/$90F7 used; viewOriginCol/Row restored ; Called from: drawMapCell ($5D14) and, in both $6F00 overlay variants, from $794F. ; ---------------------------------------------------------------------- drawScreen1Cell: lda savedRegA ; 5D7F the map cell byte bpl L_5D8D ; 5D81 plain terrain - nothing special about it and #$7F ; 5D83 strip the unit marker to get the unit index cmp comcenUnit ; 5D85 $9236 = 49 or 99, this player's own command centre bne L_5D8D ; 5D88 some other unit - draw it normally jmp scrollRadarOnComcenMove ; 5D8A the radar window is centred on our comcen, so a comcen move scrolls the whole window ($7959) L_5D8D: lda savedRegX ; 5D8D map column of the cell sec ; 5D8F prepare a true subtraction sbc viewOriginCol ; 5D90 column relative to the left edge of the radar window tax ; 5D92 keep it in X for the bitmap column table cmp #$15 ; 5D93 the radar window shows 21 columns bcc L_5D98 ; 5D95 inside the window L_5D97: rts ; 5D97 off the window - nothing to redraw L_5D98: lda savedRegY ; 5D98 map row of the cell sec ; 5D9A prepare a true subtraction sbc viewOriginRow ; 5D9B row relative to the top edge of the radar window tay ; 5D9D keep it in Y for the bitmap row table cmp #$11 ; 5D9E the radar window shows 17 rows bcs L_5D97 ; 5DA0 off the window lda #$0B ; 5DA2 dead instruction - the next one loads the very same value (leftover) lda #$0B ; 5DA4 colour $0B = dark grey sta radarOwnUnitColour ; 5DA6 $9237: the colour our own units are plotted in on the radar sta radarTerrainColour ; 5DA9 $9239: and the colour the terrain glyphs get lda #$01 ; 5DAC colour 1 = white sta radarEnemyUnitColour ; 5DAE $9238: enemy blips stand out against the grey clc ; 5DB1 plain 16-bit add of row base + column offset lda bitmapRowLoTableFrom3,y ; 5DB2 bitmap row address table $9300, offset by 3 rows - the window starts at character row 3 adc bitmapColLoTableFrom3,x ; 5DB5 column*8 table $9332, offset by 3 columns - and at character column 3 sta bitmapPtr ; 5DB8 bitmapPtr low byte lda bitmapRowHiTableFrom3,y ; 5DBA high half of the row table ($9319 + 3) adc bitmapColHiTableFrom3,x ; 5DBD high half of the column table ($935A + 3) sta bitmapPtrHi ; 5DC0 bitmapPtr high byte clc ; 5DC2 plain 16-bit add lda screenRowLoTableFrom3,y ; 5DC3 screen RAM row address table $9382, offset by 3 rows adc #$03 ; 5DC6 plus the 3-column indent of the window sta screenPtr ; 5DC8 screenPtr low byte lda screenRowHiTableFrom3,y ; 5DCA high half of the screen row table ($939B + 3) adc #$00 ; 5DCD propagate the carry sta screenPtrHi ; 5DCF screenPtr high byte clc ; 5DD1 plain 16-bit add txa ; 5DD2 the cell's column inside the window adc screenPtr ; 5DD3 add it to the row address sta screenPtr ; 5DD5 screenPtr low byte = row base + 3 + window column lda screenPtrHi ; 5DD7 screenPtr high byte... adc #$4C ; 5DD9 $8C00 + $4C00 = $D800: point straight at colour RAM sta screenPtrHi ; 5DDB screenPtr now addresses colour RAM directly lda viewOriginCol ; 5DDD current left edge of the radar window sta drawCellSaveCol ; 5DDF keep it: viewOriginCol is about to be borrowed lda viewOriginRow ; 5DE2 current top edge of the radar window sta drawCellSaveRow ; 5DE4 keep it too jsr restoreRegsAXY ; 5DE7 A = cell byte, X = map column, Y = map row stx viewOriginCol ; 5DEA getRadarCellGlyph reads the cell's own column out of $A5 sty viewOriginRow ; 5DEC ...and its row out of $A6 ldy #$00 ; 5DEE Y = 0, the offset drawRadarGlyph stores the colour byte at lda savedRegA ; 5DF0 the map cell byte jsr getRadarCellGlyph ; 5DF2 overlay routine $74C3: cell byte -> glyph index in A (bit 7 = noise) and colour in tileColour (also mapGenerator6F00:contourFillWithInterior) jsr drawRadarGlyph ; 5DF5 overlay routine $7396: plot the 8x8 glyph at (bitmapPtr) and its colour at (screenPtr),Y (also mapGenerator6F00:jitterForestSeedColumn) lda drawCellSaveCol ; 5DF8 the real left edge of the radar window sta viewOriginCol ; 5DFB put it back lda drawCellSaveRow ; 5DFD the real top edge of the radar window sta viewOriginRow ; 5E00 put it back rts ; 5E02 done ; ---------------------------------------------------------------------- ; clearOverviewWindow - Points the text window at the 20x20 overview panel (left column 1, top row 1, ; width 20, bottom row limit 21) and blanks it with the text engine's window clear. ; In: - ; Out: zp_39-zp_3C = 1/21/1/20, the overview area cleared ; Called from: mainStart ($0A98), clearCurrentScreenWindows ($2DF1) and overlay A ($76FC, $8235) ; ---------------------------------------------------------------------- clearOverviewWindow: lda #$01 ; 5E03 text window left edge = screen character column 1 sta textWindowLeft ; 5E05 textWindowLeft = 1 sta textWindowTop ; 5E07 and textWindowTop = 1: the overview frame starts at character (1,1) lda #$14 ; 5E09 20 characters wide sta textWindowWidth ; 5E0B textWindowWidth = 20 (the 40x40 map is drawn 4x4 pixels per cell inside this frame) lda #$15 ; 5E0D row limit 21 (exclusive), i.e. rows 1-20 sta textWindowBottom ; 5E0F textWindowBottom = 21 jmp bootDriveDataBit ; 5E11 tail-call the text engine's window clear ($C06A) and return to the caller (also textEngineC000:clearTextWindow) ; uiLevelTextColourTable: byteTable, 3 bytes. Info-panel text colour per uiLevel: $09 (browsing), $05 ; (unit selected), $04 (group). Read at $22AE (showUnitInfoPanel) and $64E7. uiLevelTextColourTable: .byte $09,$05,$04 ; 5E14 ... 3 bytes indexed by uiLevel ($9209): 0 browsing = colour 9, 1 unit selected = colour 5, 2 group = colour 4 ; ---------------------------------------------------------------------- ; handleOwnUnitClick - Entry taken by the battlefield main loop when the player clicks on one of his ; own, unstunned units (index already in selectedUnit $90F8). A unit that is a member of a group is ; handed to runGroupOptionsMenu; a lone unit falls through into runUnitOptionsMenu. ; In: selectedUnit $90F8, unitDisplayFlagsTable $FB54 bit 6 ; Out: control passes to one of the two option-menu routines, both of which end in exitUnitMenu ; Called from: the battlefield fire-button handler ($20CC) ; ---------------------------------------------------------------------- handleOwnUnitClick: ldy selectedUnit ; 5E17 index (0-99) of the own, unstunned unit the player just clicked on lda unitDisplayFlagsTable,y ; 5E1A its local flag copy $FB54: bit 7 idle, bit 6 in a group, bits 2-5 group key, bits 0-1 facing and #$40 ; 5E1D test bit 6 = 'this unit belongs to a group' beq runUnitOptionsMenu ; 5E1F lone unit: fall through into the single-unit branch jmp runGroupOptionsMenu ; 5E21 group member: the whole group is commanded instead ; ---------------------------------------------------------------------- ; runUnitOptionsMenu - Single-unit branch of handleOwnUnitClick, also entered by JMP from the group ; menu item DIRECT UNIT ($62F6). It highlights the unit and shows its info panel (which waits there ; until the stick moves or the button is pressed again); a deflected stick then starts SETTING ; DESTINATION, while a second click builds and runs the UNIT OPTIONS menu - but only when the UNIT ; MENUS option (gameTypeOptions $0BA3 bit 3) is on. The chosen item queues its command byte with the ; unit index as the argument. ; In: selectedUnit $90F8, unit arrays $F76C/$F898/$F8FC/$F960/$FB54, joyDirection zp_5D, gamePhase ; zp_B1, gameTypeOptions $0BA3 bit 3, comcenUnit $9236 ; Out: highlightedUnit/highlightedGroupKey/uiLevel/menuLoopMode and the menu state set, viewport and ; info panel redrawn, menuIdleSnapshot+1 patched, one of the commands $82/$84/$86/$93/$95/$9F/$A0 ; queued (or $87 through the target cursor, $88 through the join-group menu); falls into ; exitUnitMenu ; Called from: handleOwnUnitClick ($5E1F) and the group menu item DIRECT UNIT ($62F6) ; ---------------------------------------------------------------------- runUnitOptionsMenu: lda #$00 ; 5E24 0 into the three text-engine mode bytes below sta rawGlyphMode ; 5E26 rawGlyphMode off: printChar translates ASCII again sta textCentreWidth ; 5E28 textCentreWidth 0 = print left-aligned at the cursor sta textFillGlyph ; 5E2B fill/pad with glyph 0 (space) lda #$FF ; 5E2D $FF = no group highlighted sta highlightedGroupKey ; 5E2F clear the viewport group highlight lda selectedUnit ; 5E32 the clicked unit... sta highlightedUnit ; 5E35 ...is the one the viewport highlights jsr drawViewport ; 5E38 repaint the 7x5-cell tactical view with the new highlight lda #$01 ; 5E3B UI level 1 sta uiLevel ; 5E3D uiLevel 1 = a single unit is selected (info-panel colour 5) jsr setCursorShapeSquare ; 5E40 sprite 0 becomes the hollow-square cell cursor lda #$05 ; 5E43 colour 5 = the unit-level text colour (uiLevelTextColourTable[1]) sta textColour ; 5E45 ink colour for the info panel sta textPadColour ; 5E47 and the same colour for the blanks that pad centred lines jsr clearInfoPanel ; 5E49 clear the info-panel window (columns 25-38, rows 12-21) jsr showUnitInfoPanel ; 5E4C draw the unit's status there; that routine loops until the player deflects the stick, clicks again or the display is interrupted ; ---------------------------------------------------------------------- ; Work out what ended the info panel: the unit died, the stick was deflected (start a move order) or ; the fire button was pressed (open the options menu). ; ---------------------------------------------------------------------- ldy selectedUnit ; 5E4F re-read the unit: the battle kept running under the panel lda unitTypeTable,y ; 5E52 unit type byte $F76C, bit 7 = destroyed bmi L_5E65 ; 5E55 the unit was killed while its panel was up: just leave lda joyDirection ; 5E57 joystick nibble latched by the IRQ, active low ($0F = centred) cmp #$0F ; 5E59 still centred? beq L_5E68 ; 5E5B centred: then it was the fire button that ended the panel lda #$00 ; 5E5D clear the input lockout... sta inputLockoutTimer ; 5E5F ...so the destination cursor reacts to the stick immediately jsr promptForDestination ; 5E62 stick deflected: run SETTING / DESTINATION for this unit (ends in command $80) L_5E65: jmp exitUnitMenu ; 5E65 back to the battlefield loop with uiLevel 0 L_5E68: jsr checkFirePressed ; 5E68 was there a fresh fire-button press? (Z = 1 when yes) bne L_5E65 ; 5E6B no press - the panel simply timed out: leave lda gameTypeOptions ; 5E6D game type and option bits $0BA3 and #$08 ; 5E70 bit 3 = the UNIT MENUS option (set by the FULL WAR/$1D and DEFENDER/$1E presets, or by hand in a custom game) beq L_5E65 ; 5E72 this scenario has no unit option menus: leave ; ---------------------------------------------------------------------- ; Second click with the UNIT MENUS option on: re-establish the highlight and the cursor sprite ; (showUnitInfoPanel used sprites 1-4 for the unit picture) and set up the menu state. ; ---------------------------------------------------------------------- lda #$FF ; 5E74 $FF = no group highlight sta highlightedGroupKey ; 5E76 clear the group highlight again lda selectedUnit ; 5E79 the clicked unit... sta highlightedUnit ; 5E7C ...stays highlighted jsr drawViewport ; 5E7F repaint the view: showUnitInfoPanel moved the picture sprites and the path marker lda #$01 ; 5E82 UI level 1 sta uiLevel ; 5E84 uiLevel = 1 (unit) jsr setCursorShapeSquare ; 5E87 restore the square cursor sprite (the info panel may have used sprites 1-4) lda #$FF ; 5E8A $FF sta menuLoopMode ; 5E8C menuLoopMode $FF: the menu loop animates this unit's path marker (no group preview) lda #$01 ; 5E8F 1 sta uiLevel ; 5E91 redundant - uiLevel was already set to 1 at $5E84 lda #$00 ; 5E94 0 sta menuSelectedItem ; 5E96 start the selection on the first menu row ; ---------------------------------------------------------------------- ; Build the UNIT OPTIONS list. Re-entered from $5F22 when the menu loop reports that the unit's idle ; flag changed, because that swaps the DIG items for the BLITZ items. Every item is appended with ; addMenuItem, which also prints its name on the next row. ; ---------------------------------------------------------------------- buildUnitOptionsMenu: lda #$05 ; 5E99 unit-level colour 5 sta textColour ; 5E9B ink colour sta textPadColour ; 5E9D pad colour jsr clearInfoPanel ; 5E9F clear the info panel before printing the menu lda #$0A ; 5EA2 $CB0A = ' UNIT' + CR ldy #$CB ; 5EA4 high byte of the heading string jsr printString ; 5EA6 print the first heading line jsr printStringAtPointer ; 5EA9 print the string that follows it, $CB13 ' OPTIONS' + CR + CR ldy selectedUnit ; 5EAC unit index for the flag look-ups below ldx #$00 ; 5EAF 0 stx menuItemCount ; 5EB1 start with an empty item list ldx #$02 ; 5EB4 item id 2 = CLOAKING ON lda unitPathColTable,y ; 5EB6 $F898 (waypoint column), bit 6 = 'cloaking ordered' and #$40 ; 5EB9 test the cloak-order bit beq L_5EBF ; 5EBB not cloaked: offer CLOAKING ON ldx #$03 ; 5EBD already cloaked: item id 3 = NO CLOAKING L_5EBF: jsr addMenuItem ; 5EBF append the item and print its name ; ---------------------------------------------------------------------- ; The second item depends on what the unit is doing: a unit that stands still can DIG IN or DIG OUT, a ; unit that is moving can be given or denied a BLITZ order. The same flag byte is snapshotted into the ; menu loop so that the list is rebuilt when that state changes. ; ---------------------------------------------------------------------- ldy selectedUnit ; 5EC2 unit index lda unitDisplayFlagsTable,y ; 5EC5 $FB54 flags, bit 7 = the unit is standing still ora #$01 ; 5EC8 force the byte nonzero: 0 is the 'do not watch the unit' sentinel of the menu loop sta menuIdleSnapshot+1 ; 5ECA self-modify the LDA #imm at menuIdleSnapshot ($602A): the loop returns $80 when this bit 7 changes bpl L_5EDB ; 5ECD bit 7 clear = the unit is moving: offer the blitz items ldx #$11 ; 5ECF item id 17 = DIG OUT lda unitStunTable,y ; 5ED1 $F960, bits 5-7 = dug-in / digging state and #$E0 ; 5ED4 isolate the dig bits bne L_5EE8 ; 5ED6 already dug in or digging: DIG OUT inx ; 5ED8 otherwise item id 18 = DIG IN bne L_5EE8 ; 5ED9 always taken (X = $12) L_5EDB: ldx #$0A ; 5EDB item id 10 = SET BLITZ ldy selectedUnit ; 5EDD unit index lda unitPathRowTable,y ; 5EE0 $F8FC (waypoint row), bit 6 = 'blitz ordered' and #$40 ; 5EE3 test the blitz-order bit beq L_5EE8 ; 5EE5 blitz off: offer SET BLITZ inx ; 5EE7 blitz on: item id 11 = BLITZ OFF L_5EE8: jsr addMenuItem ; 5EE8 append the dig/blitz item ; ---------------------------------------------------------------------- ; JOIN GROUP is offered to every unit except the command centre; a BOOMER additionally gets SET TARGET ; or CLEAR TARGET while a battle is in progress. ; ---------------------------------------------------------------------- ldy selectedUnit ; 5EEB unit index cpy comcenUnit ; 5EEE is this the player's own command centre (unit 49 or 99)? beq L_5F0E ; 5EF1 the comcen cannot join a group and has no weapon target ldx #$07 ; 5EF3 item id 7 = JOIN GROUP jsr addMenuItem ; 5EF5 append it lda gamePhase ; 5EF8 gamePhase zp_B1 is 0 only while the battle runs bne L_5F0E ; 5EFA setup or film phase: no target item ldx selectedUnit ; 5EFC unit index jsr getBoomerTarget ; 5EFF C = 1 and A = the boomer's target byte if unit X is a BOOMER (type 2) bcc L_5F0E ; 5F02 not a boomer: no target item ldx #$04 ; 5F04 item id 4 = CLEAR TARGET tay ; 5F06 test the target byte's bit 7 bmi L_5F0B ; 5F07 bit 7 set = a target is locked: offer CLEAR TARGET ldx #$05 ; 5F09 no target yet: item id 5 = SET TARGET L_5F0B: jsr addMenuItem ; 5F0B append the target item L_5F0E: ldx #$0C ; 5F0E item id 12 = (EXIT), always the last row jsr addMenuItem ; 5F10 append it lda #$0F ; 5F13 screen row 15 sta menuFirstRow ; 5F15 the item list starts on row 15 of the info panel jsr resetPathMarker ; 5F18 put the mini-map path marker back on the unit jsr runMenuLoopWatchUnit ; 5F1B run the selector: A = chosen item id, Y = chosen row (or $80) cpy #$80 ; 5F1E $80 = the unit's idle flag changed while the menu was open bne L_5F25 ; 5F20 no: act on the chosen item jmp buildUnitOptionsMenu ; 5F22 yes: DIG vs BLITZ is wrong now, rebuild the whole menu ; ---------------------------------------------------------------------- ; Dispatch on the chosen item id. Every branch either queues a command byte through the common tail at ; $5F9E (argument = the unit index) or runs a sub-dialogue of its own. ; ---------------------------------------------------------------------- L_5F25: sta menuSelectedItem ; 5F25 keep the chosen item id (from here on $91D5 holds an id, not a row) ldy selectedUnit ; 5F28 leftover load - every path below reloads X or Y (dead) lda menuSelectedItem ; 5F2B the chosen item id cmp #$02 ; 5F2E CLOAKING ON? bne L_5F36 ; 5F30 no ldy #$82 ; 5F32 command $82 = cloak on (2 bytes: unit) bne L_5F3C ; 5F34 always taken L_5F36: cmp #$03 ; 5F36 NO CLOAKING? bne L_5F4A ; 5F38 no ldy #$84 ; 5F3A command $84 = cloak off (2 bytes: unit) L_5F3C: ldx selectedUnit ; 5F3C unit index lda unitPathColTable,x ; 5F3F flip the local copy of the cloak bit at once... eor #$40 ; 5F42 ...bit 6 of $F898... sta unitPathColTable,x ; 5F44 ...so the display shows the new state before the command is executed tya ; 5F47 command byte back into A bne L_5F9E ; 5F48 always taken: queue it L_5F4A: cmp #$04 ; 5F4A CLEAR TARGET? bne L_5F52 ; 5F4C no lda #$86 ; 5F4E command $86 = clear the boomer's target (2 bytes: unit) bne L_5F9E ; 5F50 always taken L_5F52: cmp #$05 ; 5F52 SET TARGET? bne L_5F6A ; 5F54 no jsr printSettingHeaderForLevel; 5F56 print 'SETTING' centred in the uiLevel colour lda #$61 ; 5F59 $CB61 = 'TARGET' ldy #$CB ; 5F5B high byte jsr printStringCentred ; 5F5D print it centred under SETTING lda #$05 ; 5F60 cursor mode 5 sta cursorTargetMode ; 5F62 zp_BB > 0 = range-limited attack cursor jsr runTargetCursorLoop ; 5F64 let the player pick the target; the cursor loop queues command $87 itself jmp exitUnitMenu ; 5F67 done L_5F6A: cmp #$07 ; 5F6A JOIN GROUP? bne L_5F74 ; 5F6C no jsr runJoinGroupMenu ; 5F6E run the group picker; it queues command $88 itself jmp exitUnitMenu ; 5F71 done L_5F74: cmp #$0A ; 5F74 SET BLITZ? bne L_5F7C ; 5F76 no ldy #$93 ; 5F78 command $93 = blitz on (2 bytes: unit) bne L_5F82 ; 5F7A always taken L_5F7C: cmp #$0B ; 5F7C BLITZ OFF? bne L_5F90 ; 5F7E no ldy #$95 ; 5F80 command $95 = blitz off (2 bytes: unit) L_5F82: ldx selectedUnit ; 5F82 unit index lda unitPathRowTable,x ; 5F85 flip the local blitz bit immediately... eor #$40 ; 5F88 ...bit 6 of $F8FC... sta unitPathRowTable,x ; 5F8A ...for instant menu feedback tya ; 5F8D command byte back into A bne L_5F9E ; 5F8E always taken L_5F90: cmp #$11 ; 5F90 DIG OUT? bne L_5F98 ; 5F92 no lda #$A0 ; 5F94 command $A0 = dig out (2 bytes: unit) bne L_5F9E ; 5F96 always taken L_5F98: cmp #$12 ; 5F98 DIG IN? bne exitUnitMenu ; 5F9A anything else (item 12 = (EXIT)) just closes the menu lda #$9F ; 5F9C command $9F = dig in (2 bytes: unit) L_5F9E: ldy selectedUnit ; 5F9E the unit index... sty cmdParam0 ; 5FA1 ...is the single argument of all of these commands jsr queueCmd2 ; 5FA3 queue 'command byte, unit' in the outgoing ring, then fall into exitUnitMenu ; ---------------------------------------------------------------------- ; exitUnitMenu - Shared tail of every unit/group menu path: drops back to UI level 0 and releases the ; info panel, then returns to the battlefield main loop. ; In: - ; Out: uiLevel $9209 = 0, infoPanelUnit $91D7 = $FF ; Called from: $5E65, $5F67, $5F71, $5F9A and the fall-through at $5FA3, $6339 and $64D7 ; ---------------------------------------------------------------------- exitUnitMenu: lda #$00 ; 5FA6 0 sta uiLevel ; 5FA8 uiLevel 0 = back to browsing the map lda #$FF ; 5FAB $FF sta infoPanelUnit ; 5FAD no unit owns the info panel any more rts ; 5FB0 return to the battlefield main loop ; ---------------------------------------------------------------------- ; addMenuItem - Appends item id X to menuItemIdList[menuItemCount++] and prints the item's name on the ; current text line. Ids 0-11 come from the strings at $C9D2, ids 12-18 from those at $CAB2; both are ; printed with printTableEntry, which strides 8 bytes, so the first group is indexed with 2*id (its ; strings are padded with leading blanks onto a 16-byte grid). Every string ends with a CR, so the ; cursor drops to the next row by itself. Ids 1 (MOVE TO GOAL) and 9 (SELF DESTRCT) exist in the ; string table but are never passed to this routine in this build. ; In: X = item id (0 FORMATION, 1 MOVE TO GOAL, 2 CLOAKING ON, 3 NO CLOAKING, 4 CLEAR TARGET, ; 5 SET TARGET, 6 MEMBERSHIP, 7 JOIN GROUP, 8 DIRECT UNIT, 9 SELF DESTRCT, 10 SET BLITZ, ; 11 BLITZ OFF, 12 (EXIT), 13 ENERGY, 14 BATTLE, 15 RADAR, 16 DRONE, 17 DIG OUT, 18 DIG IN); ; menuItemCount $91D4 and the text cursor ; Out: menuItemIdList[n] = id, menuItemCount incremented, one line printed; A/X/Y clobbered ; Called from: runUnitOptionsMenu ($5EBF, $5EE8, $5EF5, $5F0B, $5F10), runGroupOptionsMenu ($6268, ; $628E, $6293, $6298, $629D, $62A2) and runRepairWhichMenu ($6568, $6575) ; ---------------------------------------------------------------------- addMenuItem: txa ; 5FB1 item id into A ldy menuItemCount ; 5FB2 next free slot of the item list sta menuItemIdList,y ; 5FB5 remember which id sits on which row inc menuItemCount ; 5FB8 one more item sec ; 5FBB prepare an exact subtraction sbc #$0C ; 5FBC ids 12-18 come from the second string table bcc L_5FC7 ; 5FBE ids 0-11: the 16-byte-strided UNIT/GROUP OPTIONS strings tax ; 5FC0 X = id - 12 lda #$B2 ; 5FC1 $CAB2 = ' (EXIT)', base of the 8-byte console strings ldy #$CA ; 5FC3 high byte bne L_5FCE ; 5FC5 always taken L_5FC7: txa ; 5FC7 id 0-11 back into A asl a ; 5FC8 double it: printTableEntry strides 8 bytes but these strings sit on a 16-byte grid tax ; 5FC9 index for printTableEntry lda #$D2 ; 5FCA $C9D2 = 'FORMATION', base of the option strings ldy #$C9 ; 5FCC high byte L_5FCE: jmp printTableEntry ; 5FCE print the entry at base + 8*X; each string ends with CR so the cursor drops to the next row ; ---------------------------------------------------------------------- ; runMenuLoopWithFlag - Alternative entry for the console and setup menus: with C = 1 it behaves ; exactly like runMenuLoop; with C = 0 it puts $FF into the slow-down row operand menuSlowRowTest+1 so ; there is no extra pause before the last item, and leaves the unit-idle watch operand untouched. ; In: C flag, menu state menuItemCount/menuSelectedItem/menuFirstRow ; Out: as runMenuLoopWatchUnit ; Called from: runMainMenu ($1C59) and overlay A ($7F40, $835D, $83CE) ; ---------------------------------------------------------------------- runMenuLoopWithFlag: bcs runMenuLoop ; 5FD1 C = 1: behave exactly like runMenuLoop ldx #$FF ; 5FD3 C = 0: $FF disables the extra pause before the last row bmi L_5FE1 ; 5FD5 always taken; this path skips the store at $5FD9, so menuIdleSnapshot+1 keeps whatever the previous menu left there (harmless: these callers use the pseudo-units $F0/$FE/$FF, for which the watch is skipped anyway) ; ---------------------------------------------------------------------- ; runMenuLoop - Menu selector without the unit-idle watch: patches menuIdleSnapshot+1 ($602A) to 0 and ; falls into runMenuLoopWatchUnit, which then sets the slow-down row to itemCount-2. ; In: menu state menuItemCount/menuSelectedItem/menuFirstRow, selectedUnit $90F8, menuLoopMode $612E ; Out: A = the selected item id, Y = the selected row ; Called from: runJoinGroupMenu ($61BC), runRepairWhichMenu ($6581), runInGameOptionsMenu ($65A9), ; runFormationOptionsMenu ($661D) and runMenuLoopWithFlag ($5FD1) ; ---------------------------------------------------------------------- runMenuLoop: lda #$00 ; 5FD7 0 sta menuIdleSnapshot+1 ; 5FD9 patch menuIdleSnapshot+1 ($602A): 0 turns the unit-idle watch off ; ---------------------------------------------------------------------- ; runMenuLoopWatchUnit - The generic vertical menu selector used by every in-game option menu. It arms ; a 20-frame input lockout, highlights the current row, and then loops once per frame: it stirs the ; RNG, runs the update that suits the phase (updateGameFrame during a battle, the '*' / F1 hot keys ; plus serviceCommandExchange in the setup and film phases), keeps the info panel on the selected ; unit, blinks the selected row every 20 frames, updates the map feedback chosen by menuLoopMode (path ; marker, or a live preview of the group the current row stands for) and reads the joystick: up/down ; step through the rows without wrap-around, fire confirms. It returns early with A = Y = $80 when the ; watched unit's idle flag ($FB54 bit 7) no longer matches the snapshot in menuIdleSnapshot+1, and it ; forces the last item ((EXIT)) when the unit dies, the game-over countdown starts or checkUiContinue ; asks for an abort. ; In: menuItemCount $91D4, menuSelectedItem $91D5, menuFirstRow $91D6, menuLoopMode $612E, ; selectedUnit $90F8, joyDirection zp_5D, joyRepeatTimer zp_6C, menuBlinkTimer zp_6E, gamePhase ; zp_B1, gameEndCountdown $92C9, gameEndReason $0BA8 and the patched operands ; menuIdleSnapshot+1 / menuSlowRowTest+1 ; Out: A = menuItemIdList[menuSelectedItem], Y = menuSelectedItem (or A = Y = $80 meaning 'rebuild the ; menu'), menuSelectedItem updated, selectedGroupKey/highlightedGroupKey updated in group-preview ; mode, menuBlinkState, inputLockoutTimer $0B7D, joyRepeatTimer, menuMoveDirection+1 patched; the ; selected row is left highlighted ; Called from: runUnitOptionsMenu ($5F1B), runGroupOptionsMenu ($62AA) and through runMenuLoop ; ---------------------------------------------------------------------- runMenuLoopWatchUnit: ldx menuItemCount ; 5FDC number of rows dex ; 5FDF ... dex ; 5FE0 count - 2 = the row just before the last one L_5FE1: stx menuSlowRowTest+1 ; 5FE1 patch the CMP #imm at menuSlowRowTest ($60BA): landing on this row adds a pause lda #$14 ; 5FE4 20 frames sta inputLockoutTimer ; 5FE6 ignore held-over input for 20 frames... sta joyRepeatTimer ; 5FE9 ...and do not step the selection for 20 frames lda #$01 ; 5FEB 1 sta menuBlinkState ; 5FED menuBlinkState 1 = the selected row is currently shown normally L_5FF0: jsr toggleMenuRowHighlight ; 5FF0 highlight the selected row and restart the 20-frame blink timer ; ---------------------------------------------------------------------- ; One pass per frame while the menu is open: keep the world running, watch the unit, update the map ; feedback, blink the selected row and read the joystick. ; ---------------------------------------------------------------------- menuLoopFrame: jsr nextGameRandom ; 5FF3 stir the 24-bit game RNG once per pass lda gamePhase ; 5FF6 gamePhase zp_B1 cmp #$FF ; 5FF8 $FF = the pre-battle setup phase (overlay A resident) beq L_6002 ; 5FFA yes: poll overlay A's hot keys and #$C0 ; 5FFC bits 6/7 flag the non-battle phases beq L_6013 ; 5FFE both clear: a battle is running - do the full per-frame update bne L_600D ; 6000 otherwise only service the opponent link L_6002: jsr handleStarKeyToggle ; 6002 setup phase: overlay A's '*' hot key (DESTROY-mode toggle) lda isSoloTrainer ; 6005 $0BA5 bit 7 = solo trainer game bpl L_600D ; 6008 modem game: no F1 handler jsr handleDisplayToggleKey ; 600A trainer only: F1 switches the BEGINNER/STANDARD display (also trainerSetupEC00:storeGroupCentreRow) L_600D: jsr serviceCommandExchange ; 600D run one step of the lock-step command exchange jmp L_6068 ; 6010 skip the battle-only bookkeeping ; ---------------------------------------------------------------------- ; Battle phase: run one game frame, then check that the watched unit is still alive and still in the ; same movement state. ; ---------------------------------------------------------------------- L_6013: lda selectedUnit ; 6013 the selected unit... sta infoPanelUnit ; 6016 ...keeps owning the info panel while the menu is open jsr updateGameFrame ; 6019 one frame of the battle simulation and its display bit gamePhase ; 601C gamePhase bit 6 = paused / waiting for the opponent bvs L_6068 ; 601E paused: do not touch the unit or the map highlight ldx selectedUnit ; 6020 selected unit bmi L_6068 ; 6023 $F0/$FE/$FF are console pseudo-units, not real units lda unitTypeTable,x ; 6025 type byte, bit 7 = destroyed bmi menuLoopExitLast ; 6028 the unit died under the menu: close it on the last item menuIdleSnapshot: lda #$FF ; 602A operand patched at $5ECA/$6273 with the unit's $FB54 flags (or with 0 by runMenuLoop = watch disabled) beq L_6037 ; 602C 0: the caller does not want the unit watched eor unitDisplayFlagsTable,x ; 602E compare the snapshot with the live flags and #$80 ; 6031 only the 'standing still' bit matters beq L_6037 ; 6033 unchanged: carry on tay ; 6035 A = Y = $80 tells the caller to rebuild the menu rts ; 6036 early return with the rebuild code ; ---------------------------------------------------------------------- ; Map feedback while the menu is open: either animate the selected unit's path marker or, in the ; join-group picker, highlight the group that the current row stands for. ; ---------------------------------------------------------------------- L_6037: lda menuLoopMode ; 6037 $FF unit menu, $01 group menu, $00/$20 join-group picker (side bit) bmi L_6065 ; 603A unit menu: just animate the path marker lda menuLoopMode ; 603C re-read the mode and #$01 ; 603F odd = group menu bne L_6065 ; 6041 group menu: also just animate the path marker lda menuSelectedItem ; 6043 join-group picker: the selected row is the group number cmp #$08 ; 6046 never true - the list has 8 rows (0-7); probably an off-by-one, the (EXIT) row looks like it was meant to clear the highlight bne L_604F ; 6048 always taken lda #$FF ; 604A would mean 'no group highlighted' jmp L_6057 ; 604C unreachable in this build L_604F: asl a ; 604F group number... asl a ; 6050 ...times 4 = the group-id field of $FB54 ora menuLoopMode ; 6051 add the player's side bit ($20 for units 50-99) sta selectedGroupKey ; 6054 the group the highlight is previewing L_6057: cmp highlightedGroupKey ; 6057 is the viewport already showing that group? beq L_6068 ; 605A yes: nothing to redraw sta highlightedGroupKey ; 605C remember the highlighted group jsr drawViewport ; 605F repaint the viewport with the new group highlighted jmp L_6068 ; 6062 on to the blink handling L_6065: jsr animatePathMarkerToDestination; 6065 walk the mini-map path marker one step along the unit's route ; ---------------------------------------------------------------------- ; Blink the selected row, then decide whether the menu has to close (game over, or an abort request ; from checkUiContinue). ; ---------------------------------------------------------------------- L_6068: lda menuBlinkTimer ; 6068 zp_6E, counted down by the IRQ bne L_606F ; 606A not expired yet jsr toggleMenuRowHighlight ; 606C every 20 frames: flip the highlight of the selected row L_606F: lda gameEndCountdown ; 606F $92C9 counts down while the game-over sequence runs bne menuLoopExitLast ; 6072 game over: close the menu on the last item lda gameEndReason ; 6074 $0BA8 is nonzero once the game has ended bne L_6088 ; 6077 after the game the abort test is skipped so the console menus stay usable jsr checkUiContinue ; 6079 returns 0 when SPACE, a screen change or a pause wants this menu closed bne L_6088 ; 607C keep going menuLoopExitLast: ldy menuItemCount ; 607E number of items dey ; 6081 index of the last one sty menuSelectedItem ; 6082 force the selection onto the last item, normally (EXIT) jmp menuLoopReturnSelection ; 6085 return that selection ; ---------------------------------------------------------------------- ; Joystick handling: fire confirms the selection; up/down step through the rows without wrap-around, ; with the usual 16/6-frame auto-repeat plus an extra pause before the last row. ; ---------------------------------------------------------------------- L_6088: lda joyDirection ; 6088 joystick nibble, active low cmp #$0F ; 608A centred? bne L_6093 ; 608C deflected: try to move the selection jsr checkFirePressed ; 608E Z = 1 on a fresh fire-button press beq menuLoopReturnSelection ; 6091 fire: confirm the current selection L_6093: ldy joyRepeatTimer ; 6093 auto-repeat delay still running? bne L_60B7 ; 6095 yes: nothing to do this frame lda joyDirection ; 6097 joystick nibble ora #$0C ; 6099 force the left/right bits high: only up and down move the selection ldy #$FF ; 609B -1 = one row up cmp #$0E ; 609D $0E = bit 0 low = up beq L_60A7 ; 609F up ldy #$01 ; 60A1 +1 = one row down cmp #$0D ; 60A3 $0D = bit 1 low = down bne L_60B7 ; 60A5 left/right only: ignore L_60A7: tya ; 60A7 the step (-1 or +1) sty menuMoveDirection+1 ; 60A8 patch the LDY #imm at menuMoveDirection ($60BE) with the direction clc ; 60AB clear carry for the add adc menuSelectedItem ; 60AC candidate row = current + step jsr setJoystickRepeatDelay ; 60AF 16 frames before the first repeat, 6 between repeats (A is preserved) cmp menuItemCount ; 60B2 past the last row (or $FF above the first)? bcc menuSlowRowTest ; 60B5 in range L_60B7: jmp menuLoopFrame ; 60B7 out of range - the selection does not wrap: just keep looping menuSlowRowTest: cmp #$FF ; 60BA operand = itemCount-2, or $FF when runMenuLoopWithFlag turned the pause off bne L_60C6 ; 60BC not the slow-down row menuMoveDirection: ldy #$FF ; 60BE operand = the move direction stored at $60A8 bmi L_60C6 ; 60C0 moving up: no extra delay ldy #$1E ; 60C2 30 frames sty joyRepeatTimer ; 60C4 moving down onto the second-to-last row: pause so the stick cannot run straight into (EXIT) L_60C6: pha ; 60C6 keep the new row jsr unhighlightMenuRow ; 60C7 restore the colours of the row being left pla ; 60CA new row back sta menuSelectedItem ; 60CB it is the selection now lda #$02 ; 60CE sound 2 = menu-move click jsr playSound ; 60D0 play it jmp L_5FF0 ; 60D3 highlight the new row and loop menuLoopReturnSelection: lda menuBlinkState ; 60D6 0 = the row is already highlighted beq L_60DE ; 60D9 nothing to do jsr toggleMenuRowHighlight ; 60DB leave the chosen row highlighted L_60DE: ldy menuSelectedItem ; 60DE the chosen row lda menuItemIdList,y ; 60E1 A = its item id; Y still holds the row index rts ; 60E4 return to the menu's dispatcher ; ---------------------------------------------------------------------- ; unhighlightMenuRow - Removes the highlight from the selected menu row, but only if it is currently ; applied (menuBlinkState = 0); otherwise it returns at once. Falls into toggleMenuRowHighlight. ; In: menuBlinkState $92C5, menuSelectedItem, menuFirstRow, textWindowWidth zp_3C, textColour zp_3D ; Out: the row's colour bytes restored, menuBlinkState = 1 ; Called from: the menu loop ($60C7), runRepairWhichMenu ($6593), runInGameOptionsMenu ($65DD, $65E3) ; and overlay A ($845C) ; ---------------------------------------------------------------------- unhighlightMenuRow: lda menuBlinkState ; 60E5 0 = the row is currently EORed (highlighted) bne L_612D ; 60E8 already normal: nothing to undo ; ---------------------------------------------------------------------- ; toggleMenuRowHighlight - Flips the highlight of menu row menuFirstRow + menuSelectedItem by EORing ; the colour byte of every cell of that row with textColour, flips menuBlinkState and restarts the ; 20-frame blink timer. Special case for the REPAIR WHICH menu (selectedUnit = $FF, row < 4): if the ; comcen is stunned it throws its return address away and jumps into the menu loop's exit path so the ; menu closes on (EXIT); otherwise it also blinks that comcen system's marker on the schematic. ; In: menuSelectedItem, menuFirstRow, textWindowWidth zp_3C, textColour zp_3D, selectedUnit $90F8, ; comcenStunStatus $922A, menuBlinkState $92C5 ; Out: menuBlinkTimer zp_6E = 20, the row's colour bytes EORed, menuBlinkState flipped, screenPtr ; zp_B6/zp_B7 and the text cursor moved ; Called from: the menu loop ($5FF0, $606C, $60DB), unhighlightMenuRow and overlay A ($8066, $847B) ; ---------------------------------------------------------------------- toggleMenuRowHighlight: lda #$14 ; 60EA 20 frames sta menuBlinkTimer ; 60EC restart the blink countdown lda selectedUnit ; 60EE selected unit, or a console pseudo-unit bpl L_610B ; 60F1 a real unit: no schematic marker to blink cmp #$FF ; 60F3 $FF marks the REPAIR WHICH menu ($F0 = main menu, $FE = in-game options) bne L_610B ; 60F5 some other menu ldy menuSelectedItem ; 60F7 selected row cpy #$04 ; 60FA rows 0-3 are the four comcen systems bcs L_610B ; 60FC the (EXIT) row has no schematic marker lda comcenStunStatus ; 60FE $922A bit 7 = the comcen is knocked out bpl L_6108 ; 6101 comcen alive: blink its system marker as well pla ; 6103 comcen stunned: discard the return address... pla ; 6104 ...of the menu-loop caller... jmp menuLoopExitLast ; 6105 ...and force the loop to close on the last item (only valid when called from runMenuLoopWatchUnit) L_6108: jsr toggleSystemMarker ; 6108 blink the marker of the selected comcen system on the schematic ; ---------------------------------------------------------------------- ; EOR the colour byte of every cell of the selected row. In multicolour bitmap mode the text engine's ; screenPtr points into colour RAM $D800, which supplies the colour of the glyph pixels, so this makes ; the row alternate between its text colour and black. ; ---------------------------------------------------------------------- L_610B: lda menuFirstRow ; 610B first row of the item list clc ; 610E clear carry adc menuSelectedItem ; 610F + selection = screen row of the selected item ldy #$00 ; 6112 0 sty textCursorCol ; 6114 start at the left edge of the text window jsr setCursorRow ; 6116 set the row and recompute screenPtr (which points into colour RAM $D800 in multicolour mode); X and Y are preserved ldy textWindowWidth ; 6119 window width in characters dey ; 611B index of the last cell L_611C: lda (screenPtr),y ; 611C colour byte of this cell eor textColour ; 611E EOR with the text colour: a cell holding textColour turns black, and back again on the next call sta (screenPtr),y ; 6120 write it back dey ; 6122 previous cell bpl L_611C ; 6123 all cells of the row lda menuBlinkState ; 6125 flip the state flag... eor #$01 ; 6128 ...between 1 (normal) and 0 (EORed) sta menuBlinkState ; 612A store it L_612D: rts ; 612D done ; menuLoopMode: byteTable, 1 bytes. Variable byte embedded in code: map-highlight mode of the menu ; loop ($FF unit menu, $01 group menu, $00/$20 join-group picker side bit). menuLoopMode: .byte $00 ; 612E . map-highlight mode of the menu loop: $FF unit menu (animate the path marker), $01 group menu, $00/$20 join-group picker (the value is the player's side bit) ; ---------------------------------------------------------------------- ; runJoinGroupMenu - The JOIN GROUP picker. Lists the seven groups ALPHA..GROVER with their current ; member counts plus an (EXIT) row from screen row 13, preselecting the group the unit is already in. ; The menu loop runs in group-preview mode, so moving the selection highlights that group on the ; viewport. Picking a group calls addSelectedUnitToGroup, which queues command $88; if the unit is too ; far from the group (C = 1, TOO FAR! shown) the list is redrawn and the question repeated. ; In: selectedUnit $90F8, unitDisplayFlagsTable $FB54 (group number bits 2-4, side bit 5) ; Out: uiLevel = 2, menuItemCount = 8, menuFirstRow = 13, menuLoopMode = the unit's side bit, ; selectedGroupKey = the previewed/chosen group, command $88 unit,group queued; $9207 = $FF ; (dead) ; Called from: the UNIT OPTIONS item JOIN GROUP ($5F6E) ; ---------------------------------------------------------------------- runJoinGroupMenu: lda #$02 ; 612F UI level 2 sta uiLevel ; 6131 uiLevel 2 = group (info-panel colour 4) ldy selectedUnit ; 6134 the unit that wants to join lda unitDisplayFlagsTable,y ; 6137 $FB54 flags and #$1C ; 613A group number in bits 2-4 lsr a ; 613C shift it down... lsr a ; 613D ...to 0-6 tax ; 613E into X stx menuSelectedItem ; 613F preselect the group the unit currently belongs to ; ---------------------------------------------------------------------- ; (Re)draw the picker: heading, one row per group with its name and member count, then the (EXIT) row. ; ---------------------------------------------------------------------- drawJoinGroupList: lda #$00 ; 6142 0 sta rawGlyphMode ; 6144 ASCII translation on sta textCentreWidth ; 6146 no automatic centring sta textFillGlyph ; 6149 blank with glyph 0 lda #$04 ; 614B colour 4 = the group-level text colour sta textColour ; 614D ink sta textPadColour ; 614F pad jsr clearInfoPanel ; 6151 clear the info panel lda #$25 ; 6154 $CB25 = 'WHICH GROUP:' ldy #$CB ; 6156 high byte jsr printString ; 6158 print the heading jsr printNewline ; 615B next row lda #$00 ; 615E 0 sta cellColA ; 6160 zp_22 is reused here as the group counter 0-6 ; ---------------------------------------------------------------------- ; One row per group 0-6: the name from the 8-byte table at $C92C, a space, then the number of living ; members of that group on the clicking unit's side, right-aligned to two columns. ; ---------------------------------------------------------------------- L_6162: lda #$2C ; 6162 $C92C = the seven 8-byte group names ALPHA..GROVER ldy #$C9 ; 6164 high byte ldx cellColA ; 6166 group number jsr printTableEntry ; 6168 print the name at $C92C + 8*group lda #$A0 ; 616B $A0 = space jsr printChar ; 616D separate the name from the count lda cellColA ; 6170 group number... asl a ; 6172 ...times 4... asl a ; 6173 ...= the group-id field of the flag byte ldy selectedUnit ; 6174 the joining unit cpy #$32 ; 6177 index < 50 = one of player 0's units (a fixed test: the side comes from the index, not from who is playing) bcc L_617D ; 6179 side 0: leave bit 5 clear ora #$20 ; 617B side 1: set bit 5 of the group key L_617D: jsr countGroupMembers ; 617D A = number of living members of that group cmp #$0A ; 6180 two digits? bcs L_618B ; 6182 yes: no padding pha ; 6184 keep the count lda #$A0 ; 6185 $A0 = space jsr printChar ; 6187 right-align single-digit counts pla ; 618A count back L_618B: ldy #$00 ; 618B high byte of the value to print jsr printDecimal ; 618D print the member count jsr printNewline ; 6190 next row inc cellColA ; 6193 next group lda cellColA ; 6195 ... cmp #$07 ; 6197 seven groups bcc L_6162 ; 6199 loop lda #$B2 ; 619B $CAB2 = ' (EXIT)' as the eighth row ldy #$CA ; 619D high byte jsr printString ; 619F print it lda #$0D ; 61A2 screen row 13 sta menuFirstRow ; 61A4 the list starts on row 13 of the info panel lda #$08 ; 61A7 8 sta menuItemCount ; 61A9 7 groups + (EXIT) lda #$FF ; 61AC $FF sta unusedFlag9207 ; 61AE $9207 is written here and at $639B but never read anywhere - dead store ldx selectedUnit ; 61B1 the joining unit lda unitDisplayFlagsTable,x ; 61B4 $FB54 flags and #$20 ; 61B7 keep only the side bit sta menuLoopMode ; 61B9 menuLoopMode 0/$20: the loop previews the group the current row stands for jsr runMenuLoop ; 61BC run the selector without the unit-idle watch lda menuSelectedItem ; 61BF the selected row = the group number (0-6), 7 = (EXIT) cmp #$07 ; 61C2 (EXIT)? bcs L_61CE ; 61C4 yes: nothing to do jsr addSelectedUnitToGroup ; 61C6 queue $88 unit,group; C = 1 when the unit is too far from the group bcc L_61CE ; 61C9 joined jmp drawJoinGroupList ; 61CB TOO FAR! was shown: redraw the list and ask again L_61CE: rts ; 61CE back to the unit menu ; ---------------------------------------------------------------------- ; runGroupOptionsMenu - Group branch of handleOwnUnitClick. It takes the group key from the clicked ; unit's flags, highlights every member, prints the GROUP + name header and waits: a stick deflection ; sends the whole group SETTING DESTINATION, a click opens the GROUP OPTIONS menu (only when the UNIT ; MENUS option $0BA3 bit 3 is on) offering CLOAKING, DIG or BLITZ, MEMBERSHIP, FORMATION, DIRECT UNIT ; and (EXIT). The chosen item either queues a group command ($83/$85/$94/$96/$A1/$A2, argument = the ; group key), opens the group editor or the formation sub-menu, or hands the unit to ; runUnitOptionsMenu. ; In: selectedUnit $90F8, unit arrays $F898/$F8FC/$F960/$FB54, joyDirection zp_5D, gameTypeOptions ; $0BA3 bit 3 ; Out: selectedGroupKey/highlightedGroupKey = group key, highlightedUnit = $FF, uiLevel = 2, ; menuLoopMode = 1, cmdParam0 = group key with one group command queued, cursorTargetMode zp_BB ; set for the editor; ends with clearGroupHighlight and exitUnitMenu ; Called from: handleOwnUnitClick ($5E21) ; ---------------------------------------------------------------------- runGroupOptionsMenu: lda #$00 ; 61CF 0 sta rawGlyphMode ; 61D1 ASCII translation on sta textCentreWidth ; 61D3 no automatic centring sta textFillGlyph ; 61D6 blank with glyph 0 ldy selectedUnit ; 61D8 the clicked unit lda unitDisplayFlagsTable,y ; 61DB $FB54 flags and #$3C ; 61DE group key = group number (bits 2-4) plus the side bit 5 sta selectedGroupKey ; 61E0 this is the group being commanded sta highlightedGroupKey ; 61E3 and the group the viewport highlights lda #$01 ; 61E6 1 sta menuLoopMode ; 61E8 menuLoopMode 1 = animate the path marker, do not preview groups lda #$FF ; 61EB $FF sta highlightedUnit ; 61ED no single-unit highlight - the whole group is marked instead lda #$02 ; 61F0 2 sta uiLevel ; 61F2 uiLevel 2 = group jsr setCursorShapeSquare ; 61F5 square cell cursor jsr drawViewport ; 61F8 repaint the viewport with the group highlighted lda #$04 ; 61FB colour 4 = group level sta textColour ; 61FD ink sta textPadColour ; 61FF pad jsr clearInfoPanel ; 6201 clear the info panel lda #$0F ; 6204 screen row 15 sta textCursorRow ; 6206 heading row (printGroupTitle sets the column) jsr printGroupTitleAndName ; 6208 print 'GROUP' and the group's name ; ---------------------------------------------------------------------- ; Wait for the player: a stick deflection means 'move the whole group', a click opens the GROUP ; OPTIONS menu. The path marker keeps animating meanwhile. ; ---------------------------------------------------------------------- waitForGroupClick: lda selectedUnit ; 620B the clicked unit... sta infoPanelUnit ; 620E ...keeps the info panel jsr updateGameFrame ; 6211 one frame of the battle jsr animatePathMarkerToDestination; 6214 walk the path marker along the group's route lda joyDirection ; 6217 joystick nibble cmp #$0F ; 6219 centred? beq L_6226 ; 621B centred: look for a click jsr waitForGroupRepositionsDone; 621D deflected: first let any pending re-position orders be applied jsr promptForDestination ; 6220 then run SETTING / DESTINATION for the whole group (uiLevel 2 makes it command $81) jmp groupMenuDone ; 6223 done L_6226: jsr checkFirePressed ; 6226 fresh fire-button press? beq L_6233 ; 6229 clicked: open the GROUP OPTIONS menu jsr checkUiContinue ; 622B may this UI keep running? bne waitForGroupClick ; 622E yes: keep waiting L_6230: jmp groupMenuDone ; 6230 aborted L_6233: lda gameTypeOptions ; 6233 game type and option bits and #$08 ; 6236 bit 3 = the UNIT MENUS option beq L_6230 ; 6238 option off: no group menu in this scenario lda #$00 ; 623A 0 sta menuSelectedItem ; 623C start on the first row ; ---------------------------------------------------------------------- ; Build the GROUP OPTIONS list. Re-entered from $62B1 when the clicked unit's idle flag changed. Same ; first two items as the unit menu, then the group-only items. ; ---------------------------------------------------------------------- buildGroupOptionsMenu: lda #$04 ; 623F colour 4 sta textColour ; 6241 ink sta textPadColour ; 6243 pad jsr clearInfoPanel ; 6245 clear the info panel jsr printGroupTitle ; 6248 print 'GROUP' at column 4 lda #$13 ; 624B $CB13 = ' OPTIONS' + CR + CR ldy #$CB ; 624D high byte jsr printString ; 624F print the second heading line ldy selectedUnit ; 6252 leftover from the unit-menu version: Y is reloaded at $625A (dead) ldx #$00 ; 6255 0 stx menuItemCount ; 6257 empty item list ldy selectedUnit ; 625A the clicked unit stands for the whole group ldx #$02 ; 625D item id 2 = CLOAKING ON lda unitPathColTable,y ; 625F $F898 bit 6 = 'cloaking ordered' and #$40 ; 6262 test it beq L_6268 ; 6264 not cloaked: CLOAKING ON ldx #$03 ; 6266 item id 3 = NO CLOAKING L_6268: jsr addMenuItem ; 6268 append it ldy selectedUnit ; 626B unit index lda unitDisplayFlagsTable,y ; 626E $FB54 flags ora #$01 ; 6271 keep the snapshot nonzero sta menuIdleSnapshot+1 ; 6273 patch menuIdleSnapshot+1 ($602A) exactly as the unit menu does bpl L_6284 ; 6276 bit 7 clear = moving: offer the blitz items ldx #$11 ; 6278 item id 17 = DIG OUT lda unitStunTable,y ; 627A $F960 bits 5-7 = dug in / digging and #$E0 ; 627D isolate them bne L_628E ; 627F already digging: DIG OUT inx ; 6281 item id 18 = DIG IN bne L_628E ; 6282 always taken L_6284: ldx #$0A ; 6284 item id 10 = SET BLITZ lda unitPathRowTable,y ; 6286 $F8FC bit 6 = 'blitz ordered' and #$40 ; 6289 test it beq L_628E ; 628B blitz off: SET BLITZ inx ; 628D item id 11 = BLITZ OFF L_628E: jsr addMenuItem ; 628E append the dig/blitz item ldx #$06 ; 6291 item id 6 = MEMBERSHIP jsr addMenuItem ; 6293 append it ldx #$00 ; 6296 item id 0 = FORMATION jsr addMenuItem ; 6298 append it ldx #$08 ; 629B item id 8 = DIRECT UNIT jsr addMenuItem ; 629D append it ldx #$0C ; 62A0 item id 12 = (EXIT) jsr addMenuItem ; 62A2 append it lda #$0F ; 62A5 screen row 15 sta menuFirstRow ; 62A7 the list starts on row 15 jsr runMenuLoopWatchUnit ; 62AA run the selector: A = item id, Y = row (or $80) cpy #$80 ; 62AD $80 = the unit's idle flag changed bne L_62B4 ; 62AF no: act on the item jmp buildGroupOptionsMenu ; 62B1 yes: rebuild (DIG and BLITZ swap places) ; ---------------------------------------------------------------------- ; Dispatch on the chosen item id. All the command branches end at $62D7, which supplies the group key ; as the command's argument. ; ---------------------------------------------------------------------- L_62B4: sta menuSelectedItem ; 62B4 keep the chosen item id ldy selectedUnit ; 62B7 dead load, as in the unit menu lda menuSelectedItem ; 62BA the chosen item id cmp #$02 ; 62BD CLOAKING ON? bne L_62C5 ; 62BF no ldy #$83 ; 62C1 command $83 = cloak on (2 bytes: group key) bne L_62CB ; 62C3 always taken L_62C5: cmp #$03 ; 62C5 NO CLOAKING? bne L_62E4 ; 62C7 no ldy #$85 ; 62C9 command $85 = cloak off (2 bytes: group key) L_62CB: ldx selectedUnit ; 62CB the clicked unit lda unitPathColTable,x ; 62CE flip only its local cloak bit... eor #$40 ; 62D1 ...bit 6 of $F898... sta unitPathColTable,x ; 62D3 ...so the menu shows the new state at once (the handler does the whole group) tya ; 62D6 command byte back into A queueGroupCommand: pha ; 62D7 keep the command byte lda selectedGroupKey ; 62D8 the group key... sta cmdParam0 ; 62DB ...is the single argument of every group command pla ; 62DD command byte back jsr queueCmd2 ; 62DE queue 'command, group key' jmp groupMenuDone ; 62E1 done L_62E4: cmp #$06 ; 62E4 MEMBERSHIP? bne L_62F2 ; 62E6 no lda #$00 ; 62E8 0 sta cursorTargetMode ; 62EA zp_BB >= 0 selects membership mode L_62EC: jsr runGroupEditor ; 62EC run the group editor jmp groupMenuDone ; 62EF done L_62F2: cmp #$08 ; 62F2 DIRECT UNIT? bne L_62F9 ; 62F4 no jmp runUnitOptionsMenu ; 62F6 hand the clicked unit to the single-unit menu (which ends in exitUnitMenu) L_62F9: cmp #$00 ; 62F9 FORMATION? (item id 0) bne L_630A ; 62FB no jsr runFormationOptionsMenu ; 62FD SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT); Y = the chosen row cpy #$03 ; 6300 row 3 = CUSTOM bne groupMenuDone ; 6302 the three canned formations are sent by the sub-menu itself (command $A3) lda #$80 ; 6304 $80 sta cursorTargetMode ; 6306 zp_BB bit 7 selects custom-formation mode bmi L_62EC ; 6308 always taken: run the editor in formation mode L_630A: cmp #$0A ; 630A SET BLITZ? bne L_6312 ; 630C no ldy #$94 ; 630E command $94 = blitz on (2 bytes: group key) bne L_6318 ; 6310 always taken L_6312: cmp #$0B ; 6312 BLITZ OFF? bne L_6326 ; 6314 no ldy #$96 ; 6316 command $96 = blitz off (2 bytes: group key) L_6318: ldx selectedUnit ; 6318 the clicked unit lda unitPathRowTable,x ; 631B flip its local blitz bit... eor #$40 ; 631E ...bit 6 of $F8FC... sta unitPathRowTable,x ; 6320 ...for instant feedback tya ; 6323 command byte back into A bne queueGroupCommand ; 6324 always taken L_6326: cmp #$11 ; 6326 DIG OUT? bne L_632E ; 6328 no lda #$A2 ; 632A command $A2 = dig out (2 bytes: group key) bne queueGroupCommand ; 632C always taken L_632E: cmp #$12 ; 632E DIG IN? bne groupMenuDone ; 6330 anything else, including (EXIT): just close lda #$A1 ; 6332 command $A1 = dig in (2 bytes: group key) bne queueGroupCommand ; 6334 always taken groupMenuDone: jsr clearGroupHighlight ; 6336 drop the group highlight and repaint the viewport jmp exitUnitMenu ; 6339 uiLevel 0, no info-panel unit, return to the battlefield loop ; ---------------------------------------------------------------------- ; printGroupTitleAndName - Header of the group menu: prints GROUP (printGroupTitle) and, on the next ; row at column 4, the name of group (selectedGroupKey >> 2) & 7 from the 8-byte name table at $C92C, ; followed by a newline. ; In: selectedGroupKey $9206 ; Out: two lines printed, textCursorCol zp_3F = 4 ; Called from: runGroupOptionsMenu ($6208) ; ---------------------------------------------------------------------- printGroupTitleAndName: jsr printGroupTitle ; 633C print 'GROUP' + CR at column 4 lda #$04 ; 633F column 4 sta textCursorCol ; 6341 indent the name under the title jsr updateCursorPointers ; 6343 recompute the bitmap/colour pointers for the new column lda selectedGroupKey ; 6346 group key lsr a ; 6349 shift out the two low bits... lsr a ; 634A ... and #$07 ; 634B ...leaving the group number 0-6 (the side bit is dropped) tax ; 634D index for the name table lda #$2C ; 634E $C92C = the seven 8-byte group names ldy #$C9 ; 6350 high byte jsr printTableEntry ; 6352 print the name at $C92C + 8*group jmp printNewline ; 6355 and move to the next row ; ---------------------------------------------------------------------- ; printGroupTitle - Moves the text cursor to column 4 of the info panel and prints the string GROUP ; ($CB1F, which ends with a CR). ; In: the current text window ; Out: one line printed, textCursorCol zp_3F = 4 ; Called from: runGroupOptionsMenu ($6248) and printGroupTitleAndName ($633C) ; ---------------------------------------------------------------------- printGroupTitle: lda #$04 ; 6358 column 4 sta textCursorCol ; 635A indent the title jsr updateCursorPointers ; 635C recompute the bitmap/colour pointers lda #$1F ; 635F $CB1F = 'GROUP' (ends with CR) ldy #$CB ; 6361 high byte jmp printString ; 6363 print it and return to the caller ; ---------------------------------------------------------------------- ; clearGroupHighlight - Drops the viewport's group highlight and repaints the 7x5 tactical view. ; In: - ; Out: highlightedGroupKey $9205 = $FF, viewport redrawn ; Called from: runGroupOptionsMenu ($6336) and runGroupEditor ($64D4) ; ---------------------------------------------------------------------- clearGroupHighlight: lda #$FF ; 6366 $FF sta highlightedGroupKey ; 6368 no group highlighted any more jmp drawViewport ; 636B repaint the 7x5 viewport without the highlight ; ---------------------------------------------------------------------- ; printSettingHeader - Header of the group editor: clears the info panel in colour 4, moves to screen ; row 15 and prints SETTING centred, followed by MEMBERSHIP ($CA32) when cursorTargetMode zp_BB is ; positive or FORMATION ($C9D2) when its bit 7 is set. ; In: cursorTargetMode zp_BB ; Out: textColour/textPadColour = 4, two centred lines printed ; Called from: runGroupEditor ($63B0, and again after every hover info panel at $63F8) ; ---------------------------------------------------------------------- printSettingHeader: lda #$04 ; 636E colour 4 = group level sta textColour ; 6370 ink sta textPadColour ; 6372 pad jsr clearInfoPanel ; 6374 clear the info panel lda #$0F ; 6377 screen row 15 jsr setCursorRow ; 6379 position the cursor there jsr printSettingWord ; 637C print 'SETTING' centred, then a newline lda #$32 ; 637F $CA32 = 'MEMBERSHIP' (entry 6 of the 16-byte option strings) ldy #$CA ; 6381 high byte bit cursorTargetMode ; 6383 zp_BB: bit 7 set = custom formation mode bpl L_638B ; 6385 membership mode: print MEMBERSHIP ; ---------------------------------------------------------------------- ; printFormationCentred - Prints the word FORMATION ($C9D2) centred in the current text window; also ; the tail of printSettingHeader in custom-formation mode. ; In: the current text window ; Out: one centred line printed ; Called from: runFormationOptionsMenu ($6602) and by fall-through from printSettingHeader ; ---------------------------------------------------------------------- printFormationCentred: lda #$D2 ; 6387 $C9D2 = 'FORMATION' (entry 0 of the 16-byte option strings) ldy #$C9 ; 6389 high byte L_638B: jmp printStringCentred ; 638B print the word centred in the info panel ; ---------------------------------------------------------------------- ; runGroupEditor - Interactive editor for the group in selectedGroupKey, in MEMBERSHIP mode (zp_BB >= ; 0) or custom FORMATION mode (zp_BB bit 7 set). It halts the group with command $9B (formation mode ; first waits for pending re-positions), then lets the player scroll the viewport with the joystick ; and shows hover information for an own unit after 30 idle frames. Clicking an own unit adds it ; (command $88, ADDED / TOO FAR!) or removes it (command $89, REMOVED) in membership mode and asks for ; a NEW POSITION (command $98) in formation mode; the command centre is always refused with sound 3. ; Clicking empty ground or an enemy prints FINISHED and ends the editor. Only the part up to $6467 ; belongs to this chunk; the routine continues to $64D9. ; In: selectedGroupKey $9206, cursorTargetMode zp_BB (mode), joyDirection zp_5D, joyRepeatTimer ; zp_6C, ; inputIdleFrames $90EE, comcenUnit $9236, unitDisplayFlagsTable $FB54 and the side-branch opcode ; at groupEditorOwnSideBranch ($63E2) ; Out: selectedUnit $90F8 = the unit under the cursor (bit 7 set = empty cell or enemy), uiLevel = 2, ; infoPanelUnit = $FF, $9207 = $FF (dead), highlightedGroupKey, joyRepeatMask/joyHoldCounter, ; inputLockoutTimer, commands $9B/$88/$89/$98 queued, viewport scrolled and redrawn ; Called from: the GROUP OPTIONS items MEMBERSHIP and FORMATION/CUSTOM ($62EC) ; ---------------------------------------------------------------------- runGroupEditor: lda #$02 ; 638E 2 sta uiLevel ; 6390 uiLevel 2 = group jsr setCursorShapeSquare ; 6393 square cell cursor lda #$FF ; 6396 $FF sta infoPanelUnit ; 6398 the info panel belongs to the editor, not to a unit sta unusedFlag9207 ; 639B dead store - $9207 is never read (see $61AE) lda selectedGroupKey ; 639E the group being edited sta highlightedGroupKey ; 63A1 highlight all of its members jsr drawViewport ; 63A4 repaint the viewport lda #$00 ; 63A7 0 sta rawGlyphMode ; 63A9 ASCII translation on sta textCentreWidth ; 63AB no automatic centring sta textFillGlyph ; 63AE blank with glyph 0 jsr printSettingHeader ; 63B0 'SETTING' + MEMBERSHIP or FORMATION lda cursorTargetMode ; 63B3 zp_BB: bit 7 set = custom formation mode bpl L_63C0 ; 63B5 membership mode jsr waitForGroupRepositionsDone; 63B7 formation mode: wait until earlier re-position orders have been applied jsr sendHaltGroupCmd ; 63BA command $9B halts every member so the group stays put while it is edited jmp groupEditorActOnUnit ; 63BD formation mode starts straight away with the unit the player clicked on L_63C0: jsr sendHaltGroupCmd ; 63C0 membership mode: halt the group as well lda #$78 ; 63C3 120 frames jsr waitFramesOrInput ; 63C5 let the halt take effect; returns early on a click or a stick deflection ; ---------------------------------------------------------------------- ; Main editor loop: one game frame per pass, hover information for the unit under the cursor, click ; handling and joystick scrolling of the viewport. ; ---------------------------------------------------------------------- groupEditorLoop: jsr updateGameFrame ; 63C8 one frame of the battle jsr checkUiContinue ; 63CB may this UI keep running? bne L_63D3 ; 63CE yes jmp L_64D4 ; 63D0 SPACE / screen change / game over: leave the editor L_63D3: lda joyDirection ; 63D3 joystick nibble cmp #$0F ; 63D5 centred? bne groupEditorScroll ; 63D7 deflected: scroll the viewport jsr readCellUnderCursor ; 63D9 map byte under the cursor = the centre cell of the 7x5 view eor #$80 ; 63DC flip bit 7: a unit cell ($80|index) becomes positive, plain terrain becomes negative bmi L_63E6 ; 63DE empty cell: nothing selectable cmp #$32 ; 63E0 unit index < 50 = side 0 groupEditorOwnSideBranch: bcc L_63E6 ; 63E2 opcode byte patched by patchOwnSideBranches ($7BCF): $90 BCC for side 0, $B0 BCS for side 1, so the branch is taken when the unit is ours ora #$80 ; 63E4 an enemy unit counts as 'nothing under the cursor' L_63E6: sta selectedUnit ; 63E6 bit 7 set = empty cell or enemy unit lda inputIdleFrames ; 63E9 frames since the player last touched the controls cmp #$1E ; 63EC 30 frames bcc L_63FB ; 63EE still moving: no hover info lda selectedUnit ; 63F0 the cell under the cursor bmi L_63FB ; 63F3 only own units get an info panel jsr showUnitInfoPanel ; 63F5 show its status while the player hovers jsr printSettingHeader ; 63F8 then restore the editor's own heading L_63FB: jsr checkFirePressed ; 63FB fresh fire-button press? beq groupEditorActOnUnit ; 63FE clicked: act on the cell jsr checkUiContinue ; 6400 may this UI keep running? bne groupEditorScroll ; 6403 yes jmp L_64D4 ; 6405 aborted: leave the editor ; ---------------------------------------------------------------------- ; Scroll the viewport one cell per auto-repeat step. $6418-$6434 is an inline copy of ; setJoystickRepeatDelay ($0D49) - the same fourteen instructions, not a JSR. ; ---------------------------------------------------------------------- groupEditorScroll: lda joyRepeatTimer ; 6408 auto-repeat delay still running? bne groupEditorLoop ; 640A yes: next frame ldy joyDirection ; 640C joystick nibble as the table index lda #$00 ; 640E 0 sta inputLockoutTimer ; 6410 cancel the input lockout: scrolling must respond at once lda joystickDirectionTable,y; 6413 direction code 0-7 (N,S,E,W,NW,NE,SE,SW), $FF when centred or invalid bmi groupEditorLoop ; 6416 no usable direction ldy #$10 ; 6418 16 frames before the first repeat pha ; 641A keep the direction code inc joyHoldCounter ; 641B count how long the stick has been held lda joyHoldCounter ; 641E ... and joyRepeatMask ; 6421 the IRQ clears this mask while the stick is centred, so a new press restarts the count at 0 sta joyHoldCounter ; 6424 store the masked counter cmp #$01 ; 6427 first step after centring? bcc L_642D ; 6429 yes: keep the 16-frame delay ldy #$06 ; 642B held: repeat every 6 frames L_642D: sty joyRepeatTimer ; 642D arm the repeat timer lda #$7F ; 642F $7F sta joyRepeatMask ; 6431 let the hold counter grow again pla ; 6434 direction code back jsr scrollViewportByDirection; 6435 scroll the viewport one cell in that direction jmp groupEditorLoop ; 6438 next frame ; ---------------------------------------------------------------------- ; The player clicked: act on whatever the cursor stands on. Empty ground or an enemy finishes the ; editor, the command centre is refused, and any other own unit is added, removed or re-positioned by ; the code that follows this chunk. ; ---------------------------------------------------------------------- groupEditorActOnUnit: lda #$04 ; 643B colour 4 sta textColour ; 643D ink sta textPadColour ; 643F pad jsr clearInfoPanel ; 6441 clear the info panel for the result message lda #$10 ; 6444 screen row 16 jsr setCursorRow ; 6446 position the cursor there ldy selectedUnit ; 6449 the cell under the cursor bpl L_645D ; 644C one of our own units: act on it lda #$47 ; 644E $CB47 = 'FINISHED' ldy #$CB ; 6450 high byte jsr printStringCentred ; 6452 print it centred lda #$3C ; 6455 60 frames jsr waitFramesOrInput ; 6457 show it briefly jmp L_64D4 ; 645A clicking empty ground or an enemy ends the editor L_645D: cpy comcenUnit ; 645D is it the player's command centre (unit 49 or 99)? bne L_646A ; 6460 no: an ordinary unit lda #$03 ; 6462 sound 3 = rejected jsr playSound ; 6464 play it jmp L_64BE ; 6467 the comcen can never be a group member: ignore the click and go round again ; ---------------------------------------------------------------------- ; Click on one of our own units while the group editor is running. Y = the unit index (selectedUnit, ; already known to be alive, ours and not the comcen). Four cases: ; MEMBERSHIP mode (cursorTargetMode >= 0): outsider -> ADDED / TOO FAR!, member -> REMOVED ; FORMATION mode (cursorTargetMode < 0): outsider -> NOT IN GROUP, member -> NEW POSITION ; ---------------------------------------------------------------------- L_646A: lda unitDisplayFlagsTable,y ; 646A local flag copy of the clicked unit: facing 0-1, group id 2-4, side 5, member 6 and #$3C ; 646D keep the group key = group id (bits 2-4) plus the side bit ($20) cmp selectedGroupKey ; 646F does it already carry the key of the group being edited? beq L_6496 ; 6472 yes - go on to test the real membership bit L_6474: lda cursorTargetMode ; 6474 editor mode: >= 0 = MEMBERSHIP, < 0 = custom FORMATION bpl L_6483 ; 6476 MEMBERSHIP mode: try to enlist this outsider lda #$03 ; 6478 sound 3 = the error beep jsr playSound ; 647A FORMATION mode may only re-position units that are already members lda #$8D ; 647D 'NOT IN GROUP' ($CB8D), low byte ldy #$CB ; 647F ...high byte bne L_64B8 ; 6481 always taken (A = $8D) - go and print the refusal L_6483: jsr addSelectedUnitToGroup ; 6483 membership add: C=1 when the unit is more than 8 cells from the group (TOO FAR! already shown) bcc L_6490 ; 6486 accepted -> report ADDED lda #$3C ; 6488 60 frames jsr runGameFrames ; 648A let the real-time battle run on so TOO FAR! stays readable jmp L_64D1 ; 648D straight back to the cursor loop; the panel already holds the message L_6490: lda #$3B ; 6490 'ADDED' ($CB3B), low byte ldy #$CB ; 6492 ...high byte bne L_64B8 ; 6494 always taken (A = $3B) L_6496: lda unitDisplayFlagsTable,y ; 6496 the key matches - now check the 'is in a group' bit itself and #$40 ; 6499 bit 6 of the local flags beq L_6474 ; 649B key matches but the member bit is clear (stale key) - treat it as an outsider lda cursorTargetMode ; 649D editor mode again bpl L_64B1 ; 649F MEMBERSHIP mode: this member is being removed jsr printSettingHeaderForLevel; 64A1 FORMATION mode: 'SETTING' header in the info panel, in the uiLevel colour lda #$A6 ; 64A4 'NEW POSITION' ($CBA6), low byte ldy #$CB ; 64A6 ...high byte jsr printStringCentred ; 64A8 centred under SETTING jsr orderGroupNewPosition ; 64AB run the free cell pick and queue command $98 unit,col,row jmp L_64BE ; 64AE skip the ADDED/REMOVED print - orderGroupNewPosition has drawn its own feedback L_64B1: jsr removeSelectedUnitFromGroup; 64B1 clear the local member bit and queue command $89 unit lda #$40 ; 64B4 'REMOVED' ($CB40), low byte ldy #$CB ; 64B6 ...high byte L_64B8: jsr printStringCentred ; 64B8 print ADDED / REMOVED / NOT IN GROUP centred in the info panel jsr drawViewport ; 64BB redraw the 7x5 tactical view so the member highlight is current L_64BE: jsr checkUiContinue ; 64BE may the editor keep running? 0 = game over, screen change or SPACE beq L_64D4 ; 64C1 no - tear the editor down lda #$3C ; 64C3 60 frames jsr waitFramesOrInput ; 64C5 hold the message, but return early on a fresh stick move or button press lda selectedGroupKey ; 64C8 the group being edited... sta highlightedGroupKey ; 64CB ...is highlighted again (NEW POSITION had highlighted a single unit) jsr drawViewport ; 64CE redraw the viewport with the group highlight back L_64D1: jmp groupEditorLoop ; 64D1 back to the top of the group-editor cursor loop at $63C8 L_64D4: jsr clearGroupHighlight ; 64D4 highlightedGroupKey = $FF and redraw - no more group highlight jmp exitUnitMenu ; 64D7 uiLevel = 0, info-panel unit = $FF, return to the battlefield loop ; ---------------------------------------------------------------------- ; sendHaltGroupCmd - Queues the 2-byte command $9B with the key of the currently selected group. Its ; executor ($549E) stops every member: destination = current position, idle bit set, cloak and blitz ; orders cleared. The group editor halts the group first so the player edits settled units. ; In: selectedGroupKey ($9206) = group id in bits 2-4 plus the side bit $20 ; Out: cmdParam0 = the group key, 2 bytes appended to the outgoing command ring by queueCmd2 ; Called from: runGroupEditor at $63BA (formation mode) and $63C0 (membership mode) ; ---------------------------------------------------------------------- sendHaltGroupCmd: lda selectedGroupKey ; 64DA the group being edited sta cmdParam0 ; 64DD becomes the single argument byte of the command lda #$9B ; 64DF command $9B = halt every member of a group jmp queueCmd2 ; 64E1 queue code + argument and return through queueCmd2 ; ---------------------------------------------------------------------- ; printSettingHeaderForLevel - Clears the info panel in the colour that belongs to the current UI ; level, moves to row 15 and falls into printSettingWord, which prints SETTING centred plus a newline. ; The caller then prints the object of the order (TARGET, DESTINATION, NEW POSITION). ; In: uiLevel ($9209) 0-2, uiLevelTextColourTable at $5E14 ($09 browsing, $05 unit, $04 group) ; Out: textColour = textPadColour = that colour, info panel cleared, cursor on row 15 ; Called from: the SET TARGET item at $5F56, the NEW POSITION branch at $64A1, setUnitDestination ; ---------------------------------------------------------------------- printSettingHeaderForLevel: ldy uiLevel ; 64E4 0 = just browsing, 1 = one unit selected, 2 = a whole group lda uiLevelTextColourTable,y; 64E7 $09 / $05 / $04 - the info-panel colour byte for that level sta textColour ; 64EA colour of every character cell printed from here on sta textPadColour ; 64EC ...and of the blanks that pad a centred string jsr clearInfoPanel ; 64EE select and clear the info panel (columns 25-38, rows 12-21) lda #$0F ; 64F1 row 15, three rows below the top of the panel jsr setCursorRow ; 64F3 move the cursor there without touching the column ; ---------------------------------------------------------------------- ; printSettingWord - Prints the word SETTING ($CB4F) centred in the current text window and ends the ; line, leaving the cursor on the next row for the object word. ; In: the text window and colour set up by the caller ; Out: SETTING printed, cursor on the following row ; Called from: printSettingHeader at $637C, and by falling out of printSettingHeaderForLevel ; ---------------------------------------------------------------------- printSettingWord: lda #$4F ; 64F6 'SETTING' ($CB4F), low byte ldy #$CB ; 64F8 ...high byte jsr printStringCentred ; 64FA centred across the 14-column info panel jmp printNewline ; 64FD end the line and return through printNewline ; ---------------------------------------------------------------------- ; runSetDestinationScreen - The SETTING / DESTINATION screen. Prints the header, selects the plain ; move-order cursor and runs the target cursor loop, which issues the move order for the selected unit ; or, at uiLevel 2, for every member of the selected group. Entered as soon as the player pushes the ; stick after picking a unit or a group. ; In: selectedUnit ($90F8), uiLevel ($9209), selectedGroupKey ($9206) ; Out: cursorTargetMode = 0, a move order queued by runTargetCursorLoop ; Called from: the unit branch at $5E62 and the group branch at $6220 of handleOwnUnitClick ; promptForDestination - the console flow for 'set this unit's destination': print SETTING and ; DESTINATION, put the cursor in plain move mode and drive runTargetCursorLoop until the player ; commits or aborts, which queues the move order. Not to be confused with setUnitDestination $34D5, ; which is the engine routine that actually writes a unit's destination and applies the comcen speed ; rule. ; ---------------------------------------------------------------------- promptForDestination: jsr printSettingHeaderForLevel; 6500 'SETTING' in the colour of the current UI level jsr printNextStringCentred ; 6503 the string right after SETTING is 'DESTINATION' ($CB56) - print it centred too lda #$00 ; 6506 0 = plain move cursor: no range limit, the pick becomes a move order sta cursorTargetMode ; 6508 cursorTargetMode selects the behaviour of runTargetCursorLoop jsr runTargetCursorLoop ; 650A drive the cursor until the player commits or aborts rts ; 650D back to the caller (a JMP would have done) ; ---------------------------------------------------------------------- ; countGroupMembers - Counts the live units that belong to the group whose key is in A. Used by the ; WHICH GROUP: list to print the member count beside each group name. ; In: A = group key (group number * 4 plus the side bit $20) ; Out: A = scratch18 = number of members, scratch19 = key | $40, Y = $FF ; Called from: the join-group list builder at $617D ; ---------------------------------------------------------------------- countGroupMembers: ora #$40 ; 650E add bit 6, the 'is in a group' flag, to the pattern we look for sta scratch19 ; 6510 keep the wanted flag pattern here for the compare inside the loop ldy #$63 ; 6512 walk all 100 unit slots, 99 down to 0 lda #$00 ; 6514 member count starts at zero sta scratch18 ; 6516 scratch18 accumulates the member count ; ---------------------------------------------------------------------- ; Scan every unit slot; count the live ones whose local flags carry exactly this group key. ; ---------------------------------------------------------------------- L_6518: lda unitTypeTable,y ; 6518 unit type, bit 7 set = the unit is dead bmi L_6528 ; 651B dead units are not members lda unitDisplayFlagsTable,y ; 651D local flag copy and #$7C ; 6520 group id (bits 2-4), side (bit 5) and member bit (bit 6) cmp scratch19 ; 6522 same group and actually a member? bne L_6528 ; 6524 no inc scratch18 ; 6526 yes - one more member L_6528: dey ; 6528 next unit slot bpl L_6518 ; 6529 until index $FF wraps the sign bit lda scratch18 ; 652B return the count in A as well rts ; 652D the caller wants the count in A ; ---------------------------------------------------------------------- ; runRepairWhichMenu - Draws and runs the 'REPAIR WHICH:' menu of the REPAIR console tab. It opens a ; 7-column text window at column 3 spanning rows 9-19, prints the header, appends the four comcen ; systems (ENERGY, BATTLE, RADAR, DRONE) and (EXIT) as menu items, then loops: run the selector, and ; for anything but (EXIT) make one repair attempt on the chosen system. selectedUnit = $FF is the ; sentinel that makes the menu highlight blink the matching system marker on the comcen picture. ; In: the comcen state read by attemptSystemRepair; filmPlaybackMode ($0B9B) and gameOverFlags ; ($923F) through isPlayerInputAllowed; menuSelectedItem ($91D5) after each selector pass ; Out: text window $39-$3E, menu state $91D4/$91D5/$91D6/$91CE, selectedUnit = $FF; may queue the ; $9A energy-repair command and clear comcenSystemStatus[system]; window erased on exit ; Called from: repairTabAction at $1860, which has just erased the four system markers and set ; showOkMarkers = 1 so undamaged systems are shown too ; ---------------------------------------------------------------------- runRepairWhichMenu: lda #$00 ; 652E zero for the text modes and the menu counters below sta rawGlyphMode ; 6530 $43 = 0: strings are ASCII, control codes (CR) honoured sta textCentreWidth ; 6532 $90EF = 0: no centring field, text starts at the left edge sta textFillGlyph ; 6535 $3E = 0: glyph 0 is the blank used by the window clear sta menuItemCount ; 6537 the item list starts out empty sta menuSelectedItem ; 653A highlight will start on the first item lda #$09 ; 653D row 9, just under the top of the left map panel sta textWindowTop ; 653F first row of the menu window clc ; 6541 clear the carry for the add that follows adc #$0A ; 6542 the window is 10 rows tall sta textWindowBottom ; 6544 row 19 = row limit of the window lda #$03 ; 6546 column 3, inside the 22-column map panel sta textWindowLeft ; 6548 left edge of the menu window lda #$07 ; 654A 7 characters wide - enough for 'REPAIR' and ' ENERGY' sta textWindowWidth ; 654C wrap point and default centring width lda #$61 ; 654E video-matrix colour byte $61: %01 pixel pairs blue (6), %10 pairs white (1) sta textColour ; 6550 the clear below and the header are drawn in it jsr bootDriveDataBit ; 6552 paint the whole window blank and home the cursor to row 9 (also textEngineC000:clearTextWindow) jsr printNewline ; 6555 leave row 9 empty; the text starts on row 10 lda #$D6 ; 6558 'REPAIR' CR 'WHICH:' CR CR ($CBD6), low byte ldy #$CB ; 655A ...high byte jsr printString ; 655C two header rows plus a blank one lda textCursorRow ; 655F the trailing CRs left the cursor on row 13 sta menuFirstRow ; 6561 row 13 is the first row the highlight bar may sit on ldx #$0D ; 6564 menu item id $0D = ' ENERGY' (ids 12-16 index the strings at $CAB2) stx scratch1C ; 6566 id of the item currently being added ; ---------------------------------------------------------------------- ; Append the four comcen systems as menu items 13,14,15,16 = ENERGY, BATTLE, RADAR, DRONE. Each ; addMenuItem prints the name (its string ends in CR) so the rows fill downwards. ; ---------------------------------------------------------------------- L_6568: jsr addMenuItem ; 6568 record the id in menuItemIdList and print its name inc scratch1C ; 656B next system ldx scratch1C ; 656D addMenuItem takes the id in X cpx #$11 ; 656F $11 = one past ' DRONE', so ids $0D-$10 are added bcc L_6568 ; 6571 keep going while there are systems left ldx #$0C ; 6573 item id $0C = ' (EXIT)', always the last entry jsr addMenuItem ; 6575 menu now holds 5 items lda #$60 ; 6578 colour byte $60 for the rows the highlight will run over sta textColour ; 657A the item rows and the highlight bar use this ; ---------------------------------------------------------------------- ; Menu loop: pick a system, repair it, remove the highlight and start over until (EXIT). ; ---------------------------------------------------------------------- L_657C: lda #$FF ; 657C $FF = repair-menu mode... sta selectedUnit ; 657E ...so the blinking highlight also blinks the comcen system marker jsr runMenuLoop ; 6581 wait for the player to choose (returns the index in Y and in menuSelectedItem) ldy menuSelectedItem ; 6584 0 ENERGY, 1 BATTLE, 2 RADAR, 3 DRONE, 4 (EXIT) cpy #$04 ; 6587 the (EXIT) row beq L_6599 ; 6589 leave the menu jsr isPlayerInputAllowed ; 658B Z = 1 only when no film is playing and the game is not already over bne L_6593 ; 658E repairs are locked out - just re-run the menu jsr attemptSystemRepair ; 6590 one repair roll on the system in menuSelectedItem (1/2 for energy, 1/4 otherwise) L_6593: jsr unhighlightMenuRow ; 6593 un-highlight the row before the selector redraws it jmp L_657C ; 6596 back into the menu L_6599: lda #$66 ; 6599 colour byte $66: both nibbles blue, i.e. wipe the window out sta textColour ; 659B the clear below paints the whole window in it jmp bootDriveDataBit ; 659D erase the menu window and return to repairTabAction (also textEngineC000:clearTextWindow) ; ---------------------------------------------------------------------- ; runInGameOptionsMenu - Runs the MISC-tab options menu while a battle is in progress. The caller has ; already printed the items (ABORT GAME, VOICE PAUSE when a modem game is running, (EXIT)) and set ; menuItemCount / menuFirstRow. Every index other than 1 is handed back to the caller. Index 1 is ; VOICE PAUSE: with a carrier up it tells the opponent with command $8C reason 5 and then waits until ; voicePauseReconnect (called from the $C000 overlay at $C768) clears bit 7 of menuSelectedItem; ; without a carrier it just hangs up and redials itself. Afterwards the menu is restarted. ; In: menuItemCount ($91D4) and menuFirstRow ($91D6) set by the caller, filmPlaybackMode ($0B9B), ; isSoloTrainer ($0BA5), the comm module link flags at $E03C ; Out: Y = menuSelectedItem (0 = ABORT GAME, last = (EXIT)), selectedUnit = $FE, textColour = $60, ; command $8C/5 possibly queued, menu highlight removed ; Called from: miscTabAction at $1A78; tail-jumps to itself at $65E0 after a voice pause ; ---------------------------------------------------------------------- runInGameOptionsMenu: lda #$FE ; 65A0 $FE = plain menu: the highlight does not track a unit or a comcen marker sta selectedUnit ; 65A2 the menu selector reads this sentinel every frame lda #$60 ; 65A5 colour byte $60 for the menu rows (%01 blue, %10 black) sta textColour ; 65A7 colour of the item rows jsr runMenuLoop ; 65A9 run the selector; returns the chosen index in Y cpy #$01 ; 65AC index 1 = VOICE PAUSE bcc L_65E3 ; 65AE index 0 = ABORT GAME - let miscTabAction deal with it bne L_65E3 ; 65B0 index 2 = (EXIT) - likewise lda filmPlaybackMode ; 65B2 film record/playback flag bne L_65E3 ; 65B5 a film has no phone call to pause lda isSoloTrainer ; 65B7 bit 7 set = the solo trainer is the opponent bmi L_65E3 ; 65BA the trainer has no phone call either bit linkStatus ; 65BC comm module link flags; bit 6 = carrier present (also trainerAiE000:linkStatus) bvs L_65C7 ; 65BF carrier up - warn the opponent before hanging up jsr voicePauseReconnect ; 65C1 no carrier: hang up and re-open the link straight away jmp L_65DD ; 65C4 join the common tail: unhighlight and restart the menu L_65C7: lda #$05 ; 65C7 reason 5 of command $8C = 'hanging up for a voice call' sta cmdParam0 ; 65C9 the single argument byte lda #$8C ; 65CB command $8C = session control jsr queueCmd2 ; 65CD queue the two bytes for the opponent lda #$81 ; 65D0 bit 7 = 'waiting for the call to come back' sta menuSelectedItem ; 65D2 menuSelectedItem doubles as the wait flag ; ---------------------------------------------------------------------- ; Spin here while the players talk on the phone. updateGameFrame keeps the battle, the message system ; and the link running; voicePauseReconnect clears bit 7 when the modems have connected again. ; ---------------------------------------------------------------------- L_65D5: jsr updateGameFrame ; 65D5 one game frame (engine, messages, comm poll) lda menuSelectedItem ; 65D8 voicePauseReconnect clears bit 7 when the link is back bmi L_65D5 ; 65DB still waiting for the reconnect L_65DD: jsr unhighlightMenuRow ; 65DD take the highlight off the VOICE PAUSE row jmp runInGameOptionsMenu ; 65E0 and put the whole options menu up again L_65E3: jsr unhighlightMenuRow ; 65E3 the caller redraws, so drop the highlight first rts ; 65E6 Y still holds the chosen index ; ---------------------------------------------------------------------- ; waitForGroupRepositionsDone - Before a group screen edits a group, wait until the re-position orders ; already sent for that same group have been executed, so the editor works on settled positions. If ; the player gives up meanwhile (checkUiContinue = 0) the routine pops its own return address so the ; RTS lands in the caller's caller and the whole group operation is abandoned. ; In: selectedGroupKey ($9206), lastOrderedGroupKey ($929D), pendingGroupOrderCount ($929E) ; Out: nothing, but the return may unwind one stack level ; Called from: the GROUP screen at $621D and the group editor at $63B7 ; ---------------------------------------------------------------------- waitForGroupRepositionsDone: lda selectedGroupKey ; 65E7 the group we are about to work on cmp lastOrderedGroupKey ; 65EA the group the last batch of re-position orders was for bne L_65FE ; 65ED a different group - nothing of ours is in flight ; ---------------------------------------------------------------------- ; Wait while orders are still outstanding, keeping the real-time battle running. ; ---------------------------------------------------------------------- L_65EF: lda pendingGroupOrderCount ; 65EF re-position orders queued but not yet executed beq L_65FE ; 65F2 all settled - carry on jsr updateGameFrame ; 65F4 run one frame of the battle and the link jsr checkUiContinue ; 65F7 0 = game over, screen change or SPACE bne L_65EF ; 65FA keep waiting pla ; 65FC abandon: throw away the low byte of our return address... pla ; 65FD ...and the high byte, so the RTS below returns to the caller's caller L_65FE: rts ; 65FE return - normally, or one level higher when the wait was abandoned ; ---------------------------------------------------------------------- ; runFormationOptionsMenu - The FORMATION sub-menu of the group options. Prints the FORMATION OPTIONS ; heading and the five items (SWEEP LEFT, SWEEP RIGHT, REVERSE, CUSTOM, (EXIT)) as one multi-line ; string, then runs the selector. Items 0-2 are rotations and are sent straight to both machines as ; command $A3 leader,mode; 3 (CUSTOM) and 4 ((EXIT)) are returned to the caller. ; In: selectedUnit ($90F8) = the unit the formation is rotated about, menuFirstRow ($91D6) = 15, ; already set by the group menu at $62A7 ; Out: Y = menuSelectedItem, cmdParam1/cmdParam2 = leader and mode, command $A3 queued, item count 5 ; Called from: the group options menu at $62FD ; ---------------------------------------------------------------------- runFormationOptionsMenu: jsr clearInfoPanel ; 65FF select and clear the 14-column info panel (columns 25-38, rows 12-21) jsr printFormationCentred ; 6602 'FORMATION' ($C9D2) centred on row 12 lda #$13 ; 6605 ' OPTIONS' CR CR ($CB13), low byte ldy #$CB ; 6607 ...high byte jsr printString ; 6609 heading plus a blank row, leaving the cursor on row 15 lda #$8C ; 660C the five item lines at $CA8C, separated by CR ldy #$CA ; 660E ...high byte jsr printString ; 6610 SWEEP LEFT / SWEEP RIGHT / REVERSE / CUSTOM / (EXIT) on rows 15-19 lda #$00 ; 6613 start at the top of the list sta menuSelectedItem ; 6615 start the highlight on SWEEP LEFT lda #$05 ; 6618 five rows to choose from sta menuItemCount ; 661A five selectable rows jsr runMenuLoop ; 661D wait for the choice ldy menuSelectedItem ; 6620 0 SWEEP LEFT, 1 SWEEP RIGHT, 2 REVERSE, 3 CUSTOM, 4 (EXIT) cpy #$03 ; 6623 indices 3 (CUSTOM) and 4 ((EXIT)) are not rotations bcs L_6636 ; 6625 CUSTOM and (EXIT) are handled by the caller sty cmdParam2 ; 6627 third command byte = rotation mode 0-2 lda selectedUnit ; 6629 the group leader / clicked unit sta cmdParam1 ; 662C second command byte lda #$A3 ; 662E command $A3 = rotate the leader's formation jsr queueCmd3 ; 6630 queue code + two arguments ldy menuSelectedItem ; 6633 hand the choice back in Y L_6636: rts ; 6636 Y = the chosen index ; ---------------------------------------------------------------------- ; soundPlayerTick - The per-frame SID sound driver. It is skipped while startSound is still ; rebuilding the voice tables, writes the master volume and then advances the three voices. X holds ; voice*2 throughout the driver, because every per-voice value lives at a fixed offset plus voice*2 ; inside the 48-byte block at sndVoiceState. ; In: sndInitLock ($67C5), sndVolume ($67C2), the voice state at $6792-$67C1, the script pointers ; in zero page $D8-$DD ; Out: SID registers $D400-$D418, updated voice state, sndVoiceSoundId cleared for finished voices ; Called from: the raster IRQ at $128C (which first dequeues a request with $C8E9 and, if there is ; one, calls startSound at $1289) ; ---------------------------------------------------------------------- soundPlayerTick: lda sndInitLock ; 6637 non-zero while startSound is half way through filling the voice tables beq L_663D ; 663A safe to run rts ; 663C skip this tick rather than play a half-built voice L_663D: lda sndVolume ; 663D $0C while anything is sounding, 0 when the driver has been silenced sta SID_VOLUME ; 6640 $D418 bits 0-3 = master volume (bits 4-6 are the filter mode, left at 0) beq L_66BE ; 6643 volume 0 - nothing can be heard, so do no work ldx #$00 ; 6645 X = voice*2: SID voice 1 jsr updateSoundVoice ; 6647 advance SID voice 1 ldx #$02 ; 664A SID voice 2 jsr updateSoundVoice ; 664C advance SID voice 2 ldx #$04 ; 664F SID voice 3 - fall through into updateSoundVoice, whose RTS returns for us ; ---------------------------------------------------------------------- ; updateSoundVoice - Advances one SID voice by one frame. An idle voice (duration 0) is left alone. ; Otherwise the 16-bit frequency and pulse-width slides are added to the current values, the note ; duration is counted down and, when it expires, the voice's script is interpreted up to its next 'set ; duration' command; a script that ends without setting one releases the voice and frees its ; sndVoiceSoundId slot. Finally the whole voice image is written to the SID registers. ; In: X = voice*2 (0, 2 or 4), the voice state rows at $6792-$67C1 ; Out: SID voice registers at $D400 + voice*7, updated state, X preserved ; Called from: soundPlayerTick at $6647 and $664C, and by falling through at $664F ; ---------------------------------------------------------------------- updateSoundVoice: lda sndVoiceState,x ; 6651 duration counter of this voice ($6792 + voice*2) beq L_66BE ; 6654 0 = the voice is idle, nothing to do clc ; 6656 clear the carry for the 16-bit frequency add lda sndVoiceFreqStepLo,x ; 6657 per-tick frequency slide, low byte adc sndVoiceFreqLo,x ; 665A current oscillator frequency, low byte sta sndVoiceFreqLo,x ; 665D new oscillator frequency, low byte lda sndVoiceFreqStepHi,x ; 6660 slide high byte, with the carry from the low half adc sndVoiceFreqHi,x ; 6663 current frequency high byte, plus the carry from the low half sta sndVoiceFreqHi,x ; 6666 new oscillator frequency, high byte clc ; 6669 clear the carry for the 16-bit pulse-width add lda sndVoicePulseStepLo,x ; 666A per-tick pulse-width sweep, low byte adc sndVoicePulseLo,x ; 666D current pulse width, low byte sta sndVoicePulseLo,x ; 6670 new pulse width, low byte lda sndVoicePulseStepHi,x ; 6673 sweep high byte plus carry adc sndVoicePulseHi,x ; 6676 current pulse width high byte, plus carry sta sndVoicePulseHi,x ; 6679 new pulse width, high byte dec sndVoiceState,x ; 667C one tick off the remaining note duration bne L_6691 ; 667F note still running - just refresh the SID registers jsr runSoundScript ; 6681 duration expired: interpret the script up to its next duration command lda sndVoiceState,x ; 6684 did the script set a new duration? bne L_6691 ; 6687 yes - the voice carries on txa ; 6689 no, the sound is over: turn voice*2... lsr a ; 668A ...back into a plain voice number 0-2 tay ; 668B Y = voice number 0-2 lda #$00 ; 668C 0 = no sound playing on this voice sta sndVoiceSoundId,y ; 668E mark the voice free so requestSound may hand it to another sound ; ---------------------------------------------------------------------- ; Write the voice image out to the SID. Y = 0, 7 or $0E, the register offset of this voice. ; ---------------------------------------------------------------------- L_6691: ldy sidVoiceRegOffsetTable,x; 6691 $00 / $07 / $0E - distance of this voice's registers from $D400 lda sndVoiceFreqLo,x ; 6694 frequency low byte of this voice sta SID_V1_FREQ_LO,y ; 6697 $D400 + voice*7 = frequency low lda sndVoiceFreqHi,x ; 669A frequency high byte sta SID_V1_FREQ_HI,y ; 669D $D401 + voice*7 = frequency high lda sndVoicePulseLo,x ; 66A0 pulse width low byte sta SID_V1_PW_LO,y ; 66A3 $D402 + voice*7 = pulse width low lda sndVoicePulseHi,x ; 66A6 pulse width high byte sta SID_V1_PW_HI,y ; 66A9 $D403 + voice*7 = pulse width high (only bits 0-3 of it matter) lda sndVoiceAttackDecay,x ; 66AC attack/decay byte sta SID_V1_AD,y ; 66AF $D405 + voice*7 = attack (high nibble) and decay (low nibble) lda sndVoiceSustainRelease,x; 66B2 sustain/release byte sta SID_V1_SR,y ; 66B5 $D406 + voice*7 = sustain level and release rate lda sndVoiceCtrlByte,x ; 66B8 waveform + gate byte, e.g. $41 pulse, $21 sawtooth, $81 noise, gate off = bit 0 clear sta SID_V1_CTRL,y ; 66BB $D404 + voice*7 - written last so the gate change lands after the new envelope L_66BE: rts ; 66BE shared exit of soundPlayerTick and updateSoundVoice ; ---------------------------------------------------------------------- ; Script byte $80-$FF: 'jitter the pitch'. Only bit 7 matters; the value of the byte is ignored. The ; current map/scenario random byte is added to the frequency low byte, which is what gives the shot ; and drone scripts their rasp. Note there is no CLC first, so the carry left by the BIT/ fetch ; sequence can add one extra - harmless for a noise effect. Reached only by the BMI at $66CD. ; ---------------------------------------------------------------------- L_66BF: lda scenarioSeed0 ; 66BF byte 0 of the 24-bit scenario seed, used here as a cheap random value adc sndVoiceFreqLo,x ; 66C1 add it to the oscillator frequency low byte sta sndVoiceFreqLo,x ; 66C4 and fall straight back into the script interpreter ; ---------------------------------------------------------------------- ; runSoundScript - Interprets one voice's script bytecode until it sets a note duration (or ends). ; Byte formats: ; $80-$FF add the scenario random byte to the frequency low byte (handler at $66BF) ; $40-$7F loop/jump: bits 0-5 select a counter slot ($06 = loop counter A, $07 = counter B), ; followed by a 16-bit target address, low byte first. A counter of 0 jumps always; ; otherwise it is decremented and the jump is taken while it is still non-zero. ; $00-$0A store the following byte (two bytes for slots from $12 up) into the state slot named by ; sndCommandSlotTable: 0 duration (and stop here), 1 waveform/gate, 2 loop counter A, ; 3 loop counter B, 4/5 spare, 6 frequency, 7 pulse width, 8 AD/SR, 9 frequency slide, ; 10 pulse-width slide. A duration of 0 ends the sound. ; In: X = voice*2, the script pointer in zero page ($D8 + X), sndScriptByte ; Out: the voice state rows updated, the script pointer advanced, sndScratch clobbered ; Called from: updateSoundVoice at $6681, and re-entered from its own $66EB / $6710 / $6719 ; ---------------------------------------------------------------------- runSoundScript: jsr fetchSoundScriptByte ; 66C7 next script byte, also stored in sndScriptByte for the test below bit sndScriptByte ; 66CA N = bit 7, V = bit 6, Z = 1 only when the byte is 0 (the 'set duration' command) bmi L_66BF ; 66CD $80-$FF: jitter the frequency bvs L_66EE ; 66CF $40-$7F: loop or jump php ; 66D1 remember Z: command 0 must stop the interpreter after it has run tay ; 66D2 Y = command number 0-10 txa ; 66D3 X = voice*2 clc ; 66D4 clear the carry before adding the slot offset adc sndCommandSlotTable,y ; 66D5 plus the offset of that command's slot inside the 48-byte state block tay ; 66D8 Y now indexes the exact byte to write jsr fetchSoundScriptByte ; 66D9 operand byte sta sndVoiceState,y ; 66DC store it in the slot cpy #$12 ; 66DF slots from $12 on (frequency, pulse, AD/SR, the two slides) are 16-bit bcc L_66EA ; 66E1 one-byte slot - done iny ; 66E3 second byte of the pair, at slot+1 jsr fetchSoundScriptByte ; 66E4 high byte of a 16-bit parameter sta sndVoiceState,y ; 66E7 stored one byte further on, in slot+1 L_66EA: plp ; 66EA recover 'was the command 0?' bne runSoundScript ; 66EB any other command - keep interpreting rts ; 66ED command 0 set the note duration; stop until it runs out ; ---------------------------------------------------------------------- ; Loop/jump command $40-$7F. The two script bytes after it are the target address, low byte first; ; they are pushed and only stored into the pointer if the jump is taken. ; ---------------------------------------------------------------------- L_66EE: and #$3F ; 66EE bits 0-5 = which counter slot ($06 = loop A, $07 = loop B) sta sndScratch ; 66F0 keep the slot offset jsr fetchSoundScriptByte ; 66F3 target address low byte pha ; 66F6 push it jsr fetchSoundScriptByte ; 66F7 target address high byte pha ; 66FA push it as well (so it comes off the stack first) clc ; 66FB clear the carry before indexing the counter txa ; 66FC voice*2 adc sndScratch ; 66FD plus the counter slot tay ; 6700 Y = this voice's copy of that counter lda sndVoiceState,y ; 6701 this voice's copy of the selected loop counter beq L_6713 ; 6704 a counter of 0 means 'jump unconditionally' sec ; 6706 set the carry for the subtract sbc #$01 ; 6707 count one repetition off sta sndVoiceState,y ; 6709 write the decremented repeat count back bne L_6713 ; 670C repetitions left - take the jump pla ; 670E loop finished: drop the target high byte... pla ; 670F ...and the low byte jmp runSoundScript ; 6710 carry on with the byte after the jump L_6713: pla ; 6713 top of stack is the high byte pushed at $66FA sta soundScriptPtrHi,x ; 6714 script pointer high byte for this voice ($D9 + voice*2) pla ; 6716 then the low byte sta soundScriptPtr,x ; 6717 script pointer low byte ($D8 + voice*2) jmp runSoundScript ; 6719 continue interpreting at the jump target ; ---------------------------------------------------------------------- ; fetchSoundScriptByte - Reads the next byte of a voice's script through the zero-page pointer at ; $D8 + voice*2, keeps a copy in sndScriptByte for the BIT test in runSoundScript, and advances the ; pointer. ; In: X = voice*2, the 16-bit pointer at zero page $D8/$D9 + X ; Out: A = the byte, sndScriptByte = the byte, pointer incremented ; Called from: runSoundScript at $66C7, $66D9, $66E4, $66F3 and $66F7 ; ---------------------------------------------------------------------- fetchSoundScriptByte: lda (soundScriptPtr,x) ; 671C (zp,X) with X = voice*2 picks this voice's script pointer sta sndScriptByte ; 671E kept so runSoundScript can BIT it without disturbing A inc soundScriptPtr,x ; 6721 advance the pointer low byte bne L_6727 ; 6723 no page crossing inc soundScriptPtrHi,x ; 6725 carry into the high byte L_6727: rts ; 6727 A holds the byte, the pointer now points past it ; ---------------------------------------------------------------------- ; startSound - Starts sound A (0-28) on the voices its table row names. Each row of soundPointerTable ; holds three script addresses, one per voice; a zero entry means this sound does not use that voice, ; which is then left playing whatever it had. For every voice it does use, the script pointer is ; loaded, the voice's eight state slots are zeroed, the sound id is recorded and the duration is set ; to 2 so the script is first interpreted on the next-but-one driver tick. ; In: A = sound id 0-28, soundPointerTable at $6831 ; Out: sndVolume, sndCurrentSoundId, sndScratch, sndInitLock, sndStartVoiceIndex, sndVoiceSoundId, ; the zero-page script pointers $D8-$DD and the voice state; X and Y clobbered ; Called from: the raster IRQ at $1289 with the id dequeued by $C8E9 ; ---------------------------------------------------------------------- startSound: ldy #$03 ; 6728 3 = the number of voices, used as the loop counter... sty sndInitLock ; 672A ...and, while it is non-zero, as the 'driver busy' lock read by soundPlayerTick ldy #$0C ; 672D volume 12 of 15 sty sndVolume ; 672F the driver writes this to $D418 every tick sta sndCurrentSoundId ; 6732 remember the id; it is copied into every voice slot this sound takes asl a ; 6735 id*2 sta sndScratch ; 6736 keep id*2 so it can be added to id*4 below asl a ; 6739 id*4 clc ; 673A clear the carry for the id*4 + id*2 sum adc sndScratch ; 673B id*4 + id*2 = id*6, the row stride of soundPointerTable tax ; 673E X = byte offset of this sound's row ldy #$00 ; 673F Y = voice*2, starting at voice 0 sty sndStartVoiceIndex ; 6741 plain voice number 0-2, kept in parallel with Y ; ---------------------------------------------------------------------- ; Once per voice: take the script address from the table row, or skip the voice if it is 0. ; ---------------------------------------------------------------------- L_6744: lda soundPointerTableHi,x ; 6744 high byte of this voice's script address (soundPointerTable+1, indexed by X) beq L_6775 ; 6747 0 = this sound does not use this voice; leave it playing sta a:soundScriptPtrHi,y ; 6749 absolute,Y because there is no STA zero page,Y - writes $D9 + voice*2 lda soundPointerTable,x ; 674C low byte of the script address sta a:soundScriptPtr,y ; 674F writes $D8 + voice*2 tya ; 6752 voice*2 for this voice pha ; 6753 save voice*2 across the clear loop, which walks Y through the whole block ; ---------------------------------------------------------------------- ; Clear this voice's two bytes in each of the eight 6-byte rows of the state block, i.e. duration, ; waveform, both loop counters, the spares, frequency, pulse, AD/SR and the two slides. Y starts at ; voice*2 and steps by 6 until it passes the end of the 48-byte block. ; ---------------------------------------------------------------------- L_6754: lda #$00 ; 6754 the value written into every slot sta sndVoiceState,y ; 6756 first byte of the row for this voice sta sndVoiceCtrlByte,y ; 6759 second byte of the row for this voice tya ; 675C current offset inside the state block clc ; 675D clear the carry for the +6 row step adc #$06 ; 675E next 6-byte row tay ; 6760 Y = the same voice's slot in the next row cmp #$30 ; 6761 $30 = 48 bytes = eight rows: stop once Y has walked past the block bcc L_6754 ; 6763 keep clearing while Y is still inside the 48-byte block ldy sndStartVoiceIndex ; 6765 voice number 0-2 lda sndCurrentSoundId ; 6768 the sound now playing sta sndVoiceSoundId,y ; 676B record it so requestSound can weigh priorities against it pla ; 676E get voice*2 back tay ; 676F voice*2 back in Y lda #$02 ; 6770 duration 2 ticks... sta sndVoiceState,y ; 6772 ...so the script is first run on the second driver tick, not this one L_6775: inx ; 6775 next 16-bit entry of the table row inx ; 6776 table entries are 16-bit, so step X by two iny ; 6777 next voice*2 iny ; 6778 and Y by two, to the next voice's slot inc sndStartVoiceIndex ; 6779 next voice number dec sndInitLock ; 677C 3, 2, 1, 0 - and reaching 0 also releases soundPlayerTick again bne L_6744 ; 677F three voices per sound rts ; 6781 every voice this sound uses is now armed ; sidVoiceRegOffsetTable - 5 bytes, indexed by voice*2 (0, 2, 4); the two odd bytes are padding. Value ; = distance of that voice's SID registers from $D400: $00, $07, $0E. Read at $6691. sidVoiceRegOffsetTable: .byte $00 ; 6782 voice 0 -> $D400, (pad), voice 1 -> $D407, (pad), voice 2 -> $D40E .byte $00,$07,$00,$0E ; 6783 .... ; sndCommandSlotTable - 11 bytes, indexed by script command 0-10. Value = the offset of that ; command's state slot from sndVoiceState ($6792); the driver adds voice*2 to it. Slots from $12 up ; hold 16-bit values and take two operand bytes. Read at $66D5. sndCommandSlotTable: .byte $00 ; 6787 0 duration, 1 waveform/gate, 2 loop A, 3 loop B, 4/5 spare, 6 frequency, 7 pulse width .byte $01,$06,$07,$0C,$0D,$12,$18; 6788 ....... .byte $1E ; 678F 8 AD/SR, 9 frequency slide, 10 pulse-width slide .byte $24,$2A ; 6790 $* ; sndVoiceState - 48 bytes of work RAM: eight 6-byte rows, each row holding one parameter for all ; three voices at row + voice*2. The driver indexes everything from here. ; $6792 row 0: +0 note duration in ticks, +1 waveform/gate byte ; $6798 row 1: +0 loop counter A, +1 loop counter B ; $679E row 2: +0/+1 two spare slots (script commands 4 and 5, never used by the shipped scripts) ; $67A4 row 3: 16-bit oscillator frequency ; $67AA row 4: 16-bit pulse width ; $67B0 row 5: +0 attack/decay, +1 sustain/release ; $67B6 row 6: 16-bit frequency slide added every tick ; $67BC row 7: 16-bit pulse-width sweep added every tick ; startSound zeroes one voice's two bytes in every row; the whole block is RAM, the $00s below are ; only the state the loader happens to leave behind. sndVoiceState: .byte $00 ; 6792 note duration of voice 0 (voices 1 and 2 at $6794 and $6796) ; Waveform/gate byte written to $D404 + voice*7: $11 triangle, $21 sawtooth, $41 pulse, ; $81 noise, each with the gate in bit 0; clearing bit 0 ($00/$20/$80) starts the release. sndVoiceCtrlByte: .byte $00 ; 6793 waveform/gate byte of voice 0 ($6795 and $6797 for voices 1 and 2) .byte $00,$00,$00,$00,$00,$00,$00,$00; 6794 ........ .byte $00,$00,$00,$00,$00,$00,$00,$00; 679C ........ ; 16-bit oscillator frequency, voice 0 at $67A4/$67A5, voice 1 at $67A6/$67A7, voice 2 at ; $67A8/$67A9. Written to $D400/$D401 + voice*7 every tick. sndVoiceFreqLo: .byte $00 ; 67A4 oscillator frequency of voice 0, low byte sndVoiceFreqHi: .byte $00 ; 67A5 oscillator frequency of voice 0, high byte (then voices 1 and 2) .byte $00,$00,$00,$00 ; 67A6 .... ; 16-bit pulse width -> $D402/$D403 + voice*7 (only the low 12 bits are used by the SID). sndVoicePulseLo: .byte $00 ; 67AA pulse width of voice 0, low byte sndVoicePulseHi: .byte $00 ; 67AB pulse width of voice 0, high byte (then voices 1 and 2) .byte $00,$00,$00,$00 ; 67AC .... ; Envelope: attack/decay -> $D405 + voice*7, sustain/release -> $D406 + voice*7. sndVoiceAttackDecay: .byte $00 ; 67B0 attack/decay of voice 0 sndVoiceSustainRelease: .byte $00 ; 67B1 sustain/release of voice 0 (then voices 1 and 2) .byte $00,$00,$00,$00 ; 67B2 .... ; 16-bit signed frequency slide added to the frequency once per tick (the pitch bends). sndVoiceFreqStepLo: .byte $00 ; 67B6 per-tick frequency slide of voice 0, low byte sndVoiceFreqStepHi: .byte $00 ; 67B7 per-tick frequency slide of voice 0, high byte (then voices 1 and 2) .byte $00,$00,$00,$00 ; 67B8 .... ; 16-bit signed pulse-width sweep added to the pulse width once per tick. sndVoicePulseStepLo: .byte $00 ; 67BC per-tick pulse-width sweep of voice 0, low byte sndVoicePulseStepHi: .byte $00 ; 67BD per-tick pulse-width sweep of voice 0, high byte (then voices 1 and 2) .byte $00,$00,$00,$00 ; 67BE .... ; Sound driver work RAM, six separate bytes: ; $67C2 sndVolume master volume written to $D418 every tick ($0C playing, 0 silent) ; $67C3 sndScriptByte the script byte just fetched, kept for the BIT test at $66CA ; $67C4 sndScratch id*2 in startSound, loop slot number in runSoundScript ; $67C5 sndInitLock non-zero while startSound rebuilds the tables; also its voice counter ; $67C6 sndCurrentSoundId the id startSound is installing ; $67C7 sndStartVoiceIndex the plain voice number 0-2 startSound is working on sndVolume: .byte $00 ; 67C2 master volume written to $D418 every tick sndScriptByte: .byte $00 ; 67C3 the script byte last fetched sndScratch: .byte $00 ; 67C4 scratch: id*2 in startSound, loop slot in runSoundScript sndInitLock: .byte $00 ; 67C5 non-zero = startSound is busy, soundPlayerTick must not run sndCurrentSoundId: .byte $00 ; 67C6 the sound id startSound is installing sndStartVoiceIndex: .byte $00 ; 67C7 voice number 0-2 startSound is working on ; sndVoiceSoundId - the sound id currently playing on each of the three voices, 0 = free. The driver ; clears an entry when that voice's script ends; requestSound in the $C000 overlay ($C87D priority ; classes) reads it to decide whether a new sound may take a busy voice. $67CA (voice 2) is read on ; its own at $3F07 and $C909/$C913 to see whether the comcen engine sound is running. sndVoiceSoundId: .byte $00 ; 67C8 voice 0 and voice 1 .byte $00 ; 67C9 . ; Voice 2 - the drone/comcen engine voice; tested by name elsewhere. sndVoice2SoundId: .byte $00 ; 67CA sound id playing on voice 2 (0 = free) ; ---------------------------------------------------------------------- ; unpackRleTo0200 - Run-length decoder that always unpacks into the two pages $0200-$03FF. The first ; byte of the source block is the escape marker; in the stream 'marker, count, value' emits value ; count times and any other byte is a literal. The source pointer is kept as the self-modified ; operand of fetchRleByte. ; In: A/Y = address of the packed block ; Out: $0200-$03FF filled, rleMarkerByte ($9246) = the marker, rleUnpackFlag ($9247) = 0, ; glyphPtr/glyphPtrHi, scratch1A and scratch1C clobbered, fetchRleByte's operand left past the ; end of the block ; Called from: overlay B at $7596 (block at $FD70) and $7FF0 (block at $FE24), and the $C000 overlay ; at $CD10 for the five unit pictures at $CD52/$CDD3/$CE3B/$CECD/$CF3A; each caller then copies ; the unpacked page somewhere else ; ---------------------------------------------------------------------- unpackRleTo0200: sta L_67D6+1 ; 67CB self-modify: low byte of the LDX operand at $67D6, which reads the marker sty L_67D6+2 ; 67CE and its high byte ldx #$00 ; 67D1 X = 0 for the flag store below stx rleUnpackFlag ; 67D3 $9247 = 0 (the overlay-B bitmap unpacker keeps a state flag here) ; ---------------------------------------------------------------------- ; The operand of this LDX was patched two instructions ago. The $FFFF the disassembly shows is simply ; what the loader left in the file - the instruction is never executed unpatched. ; ---------------------------------------------------------------------- L_67D6: ldx irqVectorHi ; 67D6 first byte of the block = the escape marker (operand patched at $67CB/$67CE) stx rleMarkerByte ; 67D9 keep the marker; the overlay-B unpacker at $865E compares against the same byte tax ; 67DC A still holds the source low byte inx ; 67DD step past the marker byte bne L_67E1 ; 67DE no page crossing iny ; 67E0 carry into the source high byte L_67E1: stx fetchRleByte+1 ; 67E1 point fetchRleByte at the first packed byte (self-modified operand $681B) sty fetchRleByte+2 ; 67E4 and its high byte ($681C) lda #$00 ; 67E7 destination low byte sta glyphPtr ; 67E9 destination pointer = $0200 lda #$02 ; 67EB destination page $02 sta glyphPtrHi ; 67ED the unpacked data always lands at $0200 lda #$02 ; 67EF two pages of output sta scratch1A ; 67F1 two pages to fill: $0200-$03FF ldy #$00 ; 67F3 offset inside the current page ldx #$01 ; 67F5 run counter: one byte still owed before the next stream byte is read ; ---------------------------------------------------------------------- ; Emit one byte per pass. X counts down the bytes still owed by the current run; when it reaches 0 ; the next stream byte is fetched, which is either a literal (run of 1) or the escape marker followed ; by a count and a value. ; ---------------------------------------------------------------------- L_67F7: dex ; 67F7 one byte of the current run consumed bne L_680E ; 67F8 still repeating - store the same value again jsr fetchRleByte ; 67FA next byte of the packed stream ldx #$01 ; 67FD assume a literal: a run of exactly one cmp rleMarkerByte ; 67FF is it the escape marker? bne L_680E ; 6802 no - store it as it stands jsr fetchRleByte ; 6804 escape: the repeat count comes next sta scratch1C ; 6807 remember how many copies to emit jsr fetchRleByte ; 6809 and then the byte to repeat ldx scratch1C ; 680C X = how many copies to emit L_680E: sta (glyphPtr),y ; 680E store into $0200 + page*256 + Y iny ; 6810 next byte of the current page bne L_67F7 ; 6811 until the page is full inc glyphPtrHi ; 6813 next destination page dec scratch1A ; 6815 one page finished bne L_67F7 ; 6817 two pages done? rts ; 6819 $0200-$03FF now hold the unpacked block ; ---------------------------------------------------------------------- ; fetchRleByte - Returns the next byte of the packed stream and advances the pointer. The pointer is ; the absolute operand of the LDA below ($681B/$681C), patched by whoever starts an unpack. ; In: the self-modified operand at $681B/$681C ; Out: A = X = the byte, the operand incremented by one ; Called from: unpackRleTo0200 at $67FA/$6804/$6809, and from overlay B's bitmap unpacker at ; $8659/$8663/$8668, which sets $681B itself at $8644 and shares rleMarkerByte ; ---------------------------------------------------------------------- fetchRleByte: lda irqVectorHi ; 681A read through the self-modified pointer; the $FFFF shown is only the loader's filler tax ; 681D keep a copy - the routine hands the byte back in X as well lda #$01 ; 681E the pointer is advanced by one byte clc ; 6820 clear the carry for the 16-bit increment adc fetchRleByte+1 ; 6821 advance the stream pointer low byte sta fetchRleByte+1 ; 6824 the operand of the LDA above is the pointer itself lda #$00 ; 6827 only the carry has to reach the high byte adc fetchRleByte+2 ; 6829 and its high byte sta fetchRleByte+2 ; 682C high byte of the advanced stream pointer txa ; 682F return the byte in A rts ; 6830 byte returned in A (and in X) ; soundPointerTable - 29 rows of 3 words, 6 bytes per row, indexed by sound id * 6. Word n is the ; script address for SID voice n, or $0000 when the sound leaves that voice alone. startSound reads ; the high byte through soundPointerTableHi (= this label + 1) and the low byte through this label. ; Sound 0 points all three voices at sndScriptSilence, so 'play sound 0' stops everything; ids $1A, ; $1B and $1C do the same for one voice each. The names below come from the call sites of each id: ; $01 button click, $02 menu/typing click, $03 error beep, $04 alert chime, $05 incoming chat ; character, $06 phone prompt, $07 drone alert, $08 comcen hit, $09 unit destroyed, $0A no known ; caller, $0B boomer shot, $0C missile blast, $0D win fanfare, $0E loss fanfare, $0F drone launch, ; $10 drone turn, $11 drone strike, $12 repair failed, $13 repair succeeded, $14 comcen engine loop, ; $15 phone trouble, $16 own comcen moving, $17 damage/radar sweep, $18 attempting repair, $19 game ; over / warning chord. soundPointerTable: soundPointerTableHi = soundPointerTable+1 .addr sndScriptSilence ; 6831 sound $00 = stop all three voices .addr sndScriptSilence ; 6833 .addr sndScriptSilence ; 6835 .addr sndScriptButtonClick ; 6837 .addr CPU_DDR ; 6839 .addr CPU_DDR ; 683B .addr sndScriptMenuClick ; 683D .addr CPU_DDR ; 683F .addr CPU_DDR ; 6841 .addr sndScriptErrorBeep ; 6843 .addr CPU_DDR ; 6845 .addr CPU_DDR ; 6847 .addr sndScriptAlertChime ; 6849 .addr CPU_DDR ; 684B .addr CPU_DDR ; 684D .addr sndScriptChatKeyClick ; 684F .addr CPU_DDR ; 6851 .addr CPU_DDR ; 6853 .addr sndScriptPhonePromptV0 ; 6855 .addr sndScriptPhonePromptV1 ; 6857 .addr sndScriptSilence ; 6859 .addr sndScriptDroneAlert ; 685B .addr CPU_DDR ; 685D .addr CPU_DDR ; 685F .addr CPU_DDR ; 6861 .addr sndScriptComcenHit ; 6863 .addr CPU_DDR ; 6865 .addr CPU_DDR ; 6867 .addr sndScriptUnitDestroyed ; 6869 .addr CPU_DDR ; 686B .addr CPU_DDR ; 686D .addr sndScriptUnusedShot ; 686F .addr CPU_DDR ; 6871 .addr CPU_DDR ; 6873 .addr sndScriptBoomerShot ; 6875 .addr CPU_DDR ; 6877 .addr sndScriptMissileBlast ; 6879 .addr CPU_DDR ; 687B .addr CPU_DDR ; 687D .addr sndScriptWinFanfareV0 ; 687F .addr sndScriptWinFanfareV1 ; 6881 .addr sndScriptWinFanfareV2 ; 6883 .addr sndScriptLossFanfareV0 ; 6885 .addr sndScriptLossFanfareV1 ; 6887 .addr sndScriptLossFanfareV2 ; 6889 .addr CPU_DDR ; 688B .addr CPU_DDR ; 688D .addr sndScriptDroneLaunch ; 688F .addr CPU_DDR ; 6891 .addr CPU_DDR ; 6893 .addr sndScriptDroneTurn ; 6895 .addr CPU_DDR ; 6897 .addr CPU_DDR ; 6899 .addr sndScriptDroneStrike ; 689B .addr CPU_DDR ; 689D .addr CPU_DDR ; 689F .addr sndScriptRepairFail ; 68A1 .addr CPU_DDR ; 68A3 .addr CPU_DDR ; 68A5 .addr sndScriptRepairOk ; 68A7 .addr CPU_DDR ; 68A9 .addr CPU_DDR ; 68AB .addr sndScriptComcenEngine ; 68AD .addr sndScriptPhoneTroubleV0 ; 68AF .addr sndScriptPhoneTroubleV1 ; 68B1 .addr sndScriptSilence ; 68B3 .addr CPU_DDR ; 68B5 .addr CPU_DDR ; 68B7 .addr sndScriptOwnComcenMove ; 68B9 .addr CPU_DDR ; 68BB .addr CPU_DDR ; 68BD .addr sndScriptDamageSweep ; 68BF .addr CPU_DDR ; 68C1 .addr CPU_DDR ; 68C3 .addr sndScriptRepairAttempt ; 68C5 .addr sndScriptGameOverV0 ; 68C7 .addr sndScriptGameOverV1 ; 68C9 .addr sndScriptGameOverV2 ; 68CB .addr sndScriptSilence ; 68CD .addr CPU_DDR ; 68CF .addr CPU_DDR ; 68D1 .addr CPU_DDR ; 68D3 .addr sndScriptSilence ; 68D5 .addr CPU_DDR ; 68D7 .addr CPU_DDR ; 68D9 .addr CPU_DDR ; 68DB .addr sndScriptSilence ; 68DD ; Sound script data - the 854 bytes $68DF-$6C34 are sound-driver bytecode interpreted by ; runSoundScript. The scripts are packed back to back; the labels through to $6C1B are exactly the ; entry points soundPointerTable names, plus the shared end-of-sound tail at $68E3. Several scripts ; jump into the middle of another one ($6ACE, $6B09, $6B24, $6C1E) to share a tail or a loop body, ; which is why the pieces below are not independent. ; ; sndScriptSilence: waveform 0 (gate off) then a 60-tick rest. Used by sound $00 on all three voices ; to stop everything, as the unused third voice of sounds $06 and $15, and by the single-voice stop ; sounds $1A, $1B and $1C. sndScriptSilence: .byte $01,$00,$00,$3C ; 68DF ...< ; Waveform 0, duration 0. A duration of 0 makes updateSoundVoice free the voice, so this is the ; common 'end of sound' tail every '46 E3 68' jump in the scripts points at. sndScriptEnd: .byte $01,$00,$00,$00 ; 68E3 .... ; Sound $15 (PHONE TROUBLE) voice 0: one tone, then an unconditional jump into the voice 1 script at ; $68F6 so both voices run the same alternating body. sndScriptPhoneTroubleV0: .byte $06,$5D,$0F,$08,$00,$30,$46,$F6; 68E7 .]...0F. .byte $68 ; 68EF h ; Sound $15 voice 1: two-tone alarm looping for ever at $68F9 (only stopped by sound $00). sndScriptPhoneTroubleV1: .byte $06,$54,$14,$08,$00,$10,$07,$00; 68F0 .T...... .byte $01,$01,$41,$00,$19,$01,$00,$00; 68F8 ..A..... .byte $19,$46,$F9,$68 ; 6900 .F.h ; Sound $01, voice 0: the fire-button click played by checkFirePressed. sndScriptButtonClick: .byte $08,$00,$80,$06,$00,$62,$07,$00; 6904 .....b.. .byte $08,$01,$41,$00,$01,$46,$E3,$68; 690C ..A..F.h ; Sound $03, voice 0: the error beep (TOO FAR!, rejected key, disk failure). sndScriptErrorBeep: .byte $08,$08,$00,$06,$00,$29,$07,$00; 6914 .....).. .byte $08,$01,$41,$00,$0F,$46,$E3,$68; 691C ..A..F.h ; Sound $02, voice 0: the short click when a menu selection moves or a chat key is taken. sndScriptMenuClick: .byte $06,$00,$18,$07,$00,$08,$01,$41; 6924 .......A .byte $00,$01,$46,$E3,$68 ; 692C ..F.h ; Sound $04, voice 0: the chime that goes with the alert messages (SPOTTED..., STUNNED...). sndScriptAlertChime: .byte $08,$00,$60,$06,$C6,$70,$07,$00; 6931 ..`..p.. .byte $08,$01,$41,$00,$0F,$46,$E3,$68; 6939 ..A..F.h ; Sound $05, voice 0: the click for each character received from the opponent's keyboard. sndScriptChatKeyClick: .byte $08,$00,$50,$06,$64,$32,$02,$03; 6941 ..P.d2.. .byte $01,$21,$00,$04,$01,$00,$00,$02; 6949 .!...... .byte $46,$49,$69,$00,$00 ; 6951 FIi.. ; Sound $0C, voice 0: the comcen missile explosion (a falling sawtooth, freq slide $0095). sndScriptMissileBlast: .byte $08,$00,$80,$01,$21,$06,$60,$16; 6956 ....!.`. .byte $09,$95,$00,$00,$1E,$01,$00,$00; 695E ........ .byte $1E,$00,$00 ; 6966 ... ; Sound $07, voice 0: two-note warning that the opponent has launched a drone. sndScriptDroneAlert: .byte $01,$21,$08,$00,$60,$02,$03,$06; 6969 .!..`... .byte $9C,$1A,$00,$14,$06,$4A,$12,$00; 6971 .....J.. .byte $14,$46,$70,$69,$46,$E3,$68; 6979 .FpiF.h ; Sound $06 voice 0: the two-voice attention chord behind the 'PICK UP PHONE' / 'HANG UP PHONE' ; messages (message ids $17 and $18 in messageSoundTable). sndScriptPhonePromptV0: .byte $08,$00,$20,$07,$00,$08,$02,$02; 6980 .. ..... .byte $06,$82,$54,$01,$41,$00,$04,$01; 6988 ..T.A... .byte $00,$00,$03,$46,$88,$69,$06,$45; 6990 ...F.i.E .byte $4B,$01,$41,$00,$04,$01,$00,$00; 6998 K.A..... .byte $03,$02,$02,$06,$BF,$5D,$01,$41; 69A0 .....].A .byte $00,$04,$01,$00,$00,$03,$46,$A3; 69A8 ......F. .byte $69,$06,$82,$54,$01,$41,$00,$04; 69B0 i..T.A.. .byte $01,$00,$00,$03,$06,$BF,$5D,$01; 69B8 ......]. .byte $41,$00,$04,$01,$00,$00,$03,$00; 69C0 A....... .byte $00 ; 69C8 . ; Sound $06 voice 1: the second part of the same chord. sndScriptPhonePromptV1: .byte $08,$00,$20,$07,$00,$08,$02,$02; 69C9 .. ..... .byte $06,$C1,$2C,$01,$41,$00,$04,$01; 69D1 ..,.A... .byte $00,$00,$03,$46,$D1,$69,$06,$BD; 69D9 ...F.i.. .byte $36,$01,$41,$00,$04,$01,$00,$00; 69E1 6.A..... .byte $03,$06,$BF,$31,$01,$41,$00,$04; 69E9 ...1.A.. .byte $01,$00,$00,$03,$06,$C1,$2C,$01; 69F1 ......,. .byte $41,$00,$04,$01,$00,$00,$03,$06; 69F9 A....... .byte $BB,$3B,$01,$41,$00,$04,$01,$00; 6A01 .;.A.... .byte $00,$03,$06,$C1,$2C,$01,$41,$00; 6A09 ....,.A. .byte $04,$01,$00,$00,$03,$00,$00; 6A11 ....... ; Sound $08, voice 1: noise burst played when a comcen is hit. sndScriptComcenHit: .byte $01,$81,$08,$00,$FB,$02,$0E,$06; 6A18 ........ .byte $5A,$02,$00,$03,$01,$80,$06,$90; 6A20 Z....... .byte $01,$00,$03,$46,$1F,$6A,$00,$00; 6A28 ...F.j.. ; Sound $09, voice 1: the noise-into-tone whoosh when a unit is destroyed. sndScriptUnitDestroyed: .byte $01,$81,$08,$06,$9B,$06,$BC,$02; 6A30 ........ .byte $00,$04,$01,$80,$09,$12,$00,$02; 6A38 ........ .byte $08,$06,$E8,$03,$00,$08,$46,$41; 6A40 ......FA .byte $6A,$46,$E3,$68 ; 6A48 jF.h ; Sound $0A, voice 1. No call site for id $0A was found anywhere in the game, so this script looks ; like a leftover shot effect; it is the only script that uses the bit-7 'jitter' command. sndScriptUnusedShot: .byte $01,$21,$08,$00,$38,$06,$5C,$12; 6A4C .!..8.\. .byte $80,$02,$04,$09,$5C,$12,$00,$01; 6A54 ....\... .byte $09,$A4,$ED,$00,$01,$46,$57,$6A; 6A5C .....FWj .byte $09,$5C,$12,$00,$01,$01,$20,$09; 6A64 .\.... . .byte $D4,$FE,$00,$14,$46,$E3,$68; 6A6C ....F.h ; Sound $0B, voice 1: the boomer's shot (noise with a downward slide). sndScriptBoomerShot: .byte $01,$81,$08,$07,$00,$06,$84,$03; 6A73 ........ .byte $09,$D8,$FF,$00,$12,$46,$E3,$68; 6A7B .....F.h ; Sound $11, voice 2: the drone detonation / strike flash. sndScriptDroneStrike: .byte $01,$41,$08,$00,$50,$07,$00,$08; 6A83 .A..P... .byte $06,$40,$9C,$09,$38,$FF,$00,$2D; 6A8B .@..8..- .byte $01,$00,$00,$02,$09,$00,$00; 6A93 ....... ; Sound $17, voice 2: the tone under the expanding damage/radar frames ($3D2E, $79F5). sndScriptDamageSweep: .byte $01,$81,$08,$09,$00,$06,$00,$02; 6A9A ........ .byte $00,$1E,$46,$E3,$68 ; 6AA2 ..F.h ; Sound $0D voice 0: the winning chord (WE WON ON POINTS / BY A KNOCKOUT). Voices 0 and 1 set their ; own pitch and then jump into the shared tail at $6ACE inside the voice 2 script. sndScriptWinFanfareV0: .byte $01,$21,$08,$00,$47,$06,$47,$06; 6AA7 .!..G.G. .byte $09,$A6,$00,$46,$CE,$6A ; 6AAF ...F.j ; Sound $0D voice 1: same body, different pitch; also jumps to the tail at $6ACE. sndScriptWinFanfareV1: .byte $01,$21,$08,$00,$47,$06,$47,$06; 6AB5 .!..G.G. .byte $09,$5D,$00,$46,$CE,$6A ; 6ABD .].F.j ; Sound $0D voice 2: the third note and, from $6ACE on, the shared decay all three voices run. sndScriptWinFanfareV2: .byte $01,$21,$08,$00,$47,$06,$47,$06; 6AC3 .!..G.G. .byte $09,$16,$00,$00,$78,$09,$00,$00; 6ACB ....x... .byte $00,$5A,$01,$20,$00,$14,$46,$E3; 6AD3 .Z. ..F. .byte $68 ; 6ADB h ; Sound $0E voice 0: the losing chord (WE LOST ON POINTS / WE WERE KNOCKED OUT). Voices 0 and 1 jump ; into the shared tail at $6B09 inside the voice 2 script. sndScriptLossFanfareV0: .byte $01,$21,$08,$00,$4D,$06,$87,$21; 6ADC .!..M..! .byte $00,$2D,$09,$C2,$FF,$46,$09,$6B; 6AE4 .-...F.k ; Sound $0E voice 1: same body one interval down; also jumps to $6B09. sndScriptLossFanfareV1: .byte $01,$21,$08,$00,$4D,$06,$6B,$2F; 6AEC .!..M.k/ .byte $00,$2D,$09,$A5,$FF,$46,$09,$6B; 6AF4 .-...F.k ; Sound $0E voice 2: the third note plus the shared falling tail from $6B09. sndScriptLossFanfareV2: .byte $01,$21,$08,$00,$4D,$06,$3C,$32; 6AFC .!..M.<2 .byte $00,$2D,$09,$9F,$FF,$00,$78,$09; 6B04 .-....x. .byte $00,$00,$00,$14,$01,$20,$00,$C8; 6B0C ..... .. .byte $46,$E3,$68 ; 6B14 F.h ; Sound $0F, voice 2: the drone launch, which then settles into the engine loop at $6B24. sndScriptDroneLaunch: .byte $01,$81,$08,$0A,$20,$06,$E8,$03; 6B17 .... ... .byte $09,$0A,$00,$00,$F0,$01,$81,$08; 6B1F ........ .byte $00,$20,$06,$48,$0D,$09,$00,$00; 6B27 . .H.... .byte $00,$01,$07,$96,$00,$01,$41,$00; 6B2F ......A. .byte $01,$46,$24,$6B ; 6B37 .F$k ; Sound $10, voice 2: the pitch wobble as the drone turns one step, ending by jumping back into the ; engine loop of sndScriptDroneLaunch at $6B24. sndScriptDroneTurn: .byte $06,$48,$0D,$08,$99,$20,$02,$07; 6B3B .H... .. .byte $09,$0E,$00,$01,$81,$00,$01,$07; 6B43 ........ .byte $96,$00,$01,$41,$00,$01,$46,$43; 6B4B ...A..FC .byte $6B,$02,$07,$01,$81,$09,$F2,$FF; 6B53 k....... .byte $00,$01,$01,$41,$00,$01,$46,$56; 6B5B ...A..FV .byte $6B,$46,$24,$6B ; 6B63 kF$k ; Sound $18, voice 2: the repeating warble under 'ATTEMPTING REPAIR...'. sndScriptRepairAttempt: .byte $06,$08,$07,$08,$00,$20,$07,$00; 6B67 ..... .. .byte $08,$01,$41,$09,$C8,$00,$00,$06; 6B6F ..A..... .byte $09,$50,$FB,$00,$01,$09,$38,$FF; 6B77 .P....8. .byte $00,$06,$09,$00,$05,$00,$01,$46; 6B7F .......F .byte $72,$6B ; 6B87 rk ; Sound $13, voice 2: the rising sweep that reports a successful repair. sndScriptRepairOk: .byte $06,$A4,$06,$01,$21,$08,$00,$20; 6B89 ....!.. .byte $02,$1E,$09,$C8,$00,$00,$02,$09; 6B91 ........ .byte $6A,$FF,$00,$01,$46,$93,$6B,$46; 6B99 j...F.kF .byte $E3,$68 ; 6BA1 .h ; Sound $12, voice 2: the two-counter stutter that reports a failed repair. sndScriptRepairFail: .byte $06,$E0,$15,$01,$21,$08,$00,$20; 6BA3 ....!.. .byte $02,$03,$03,$0A,$09,$06,$FF,$00; 6BAB ........ .byte $01,$09,$96,$00,$00,$01,$47,$AF; 6BB3 ......G. .byte $6B,$09,$A8,$FD,$00,$01,$46,$AD; 6BBB k.....F. .byte $6B,$46,$E3,$68 ; 6BC3 kF.h ; Sound $16, voice 2: played while the player's own comcen is moving in a live game. sndScriptOwnComcenMove: .byte $06,$E8,$03,$08,$00,$40,$09,$04; 6BC7 .....@.. .byte $00,$03,$02,$02,$0F,$01,$81,$00; 6BCF ........ .byte $01,$01,$21,$00,$01,$46,$D4,$6B; 6BD7 ..!..F.k .byte $09,$FC,$FF,$47,$D2,$6B,$09,$00; 6BDF ...G.k.. .byte $00 ; 6BE7 . ; Sound $14, voice 2: the comcen engine loop, kept running while the comcen is moving and stopped ; again with sound $1C. sndScriptComcenEngine: .byte $06,$E8,$03,$08,$00,$10,$01,$81; 6BE8 ........ .byte $00,$01,$01,$21,$00,$01,$46,$EE; 6BF0 ...!..F. .byte $6B,$00,$00 ; 6BF8 k.. ; Sound $19 voice 0: the alarm chord used for GAME OVER, TIMEOUT, RESUMED and the LAST QTR WARNING. sndScriptGameOverV0: .byte $08,$00,$70,$07,$00,$08,$01,$41; 6BFB ..p....A .byte $02,$16,$06,$06,$B3,$00,$01,$06; 6C03 ........ .byte $8B,$96,$00,$01,$46,$05,$6C,$46; 6C0B ....F.lF .byte $E3,$68 ; 6C13 .h ; Sound $19 voice 1: sets its own pitch and jumps into the middle of the voice 2 script at $6C1E, so ; voices 1 and 2 share one body. sndScriptGameOverV1: .byte $06,$FA,$A8,$46,$1E,$6C ; 6C15 ...F.l ; Sound $19 voice 2: the body both it and voice 1 run from $6C1E. sndScriptGameOverV2: .byte $06,$00,$AE,$01,$41,$07,$00,$08; 6C1B ....A... .byte $02,$16,$08,$00,$70,$00,$01,$08; 6C23 ....p... .byte $00,$30,$00,$01,$46,$25,$6C,$46; 6C2B .0..F%lF .byte $E3,$68 ; 6C33 .h ; unitGlyphPointerTable - 20 words, one per unit picture, indexed by unitType*4 + facing. The ; viewport tile builder computes the index at $289A-$28A7 as (unitFlagsTable[unit] & 3) + ; unitGlyphBaseTable[type] - $2C, where unitGlyphBaseTable ($0799) holds $2C,$30,$34,$38,$3C for ; GRUNT, RIDER, BOOMER, SPY and COMCEN, so the subtraction turns it into type*4 + facing. It then ; copies the 32 bytes at the pointer into the cell buffer at $28B5. unitGlyphPointerTable: .addr unitGlyphGruntUp ; 6C35 GRUNT facing 0 .addr unitGlyphGruntDown ; 6C37 .addr unitGlyphGruntLeft ; 6C39 .addr unitGlyphGruntRight ; 6C3B .addr unitGlyphRiderUp ; 6C3D .addr unitGlyphRiderDown ; 6C3F .addr unitGlyphRiderLeft ; 6C41 .addr unitGlyphRiderRight ; 6C43 .addr unitGlyphBoomerUp ; 6C45 .addr unitGlyphBoomerDown ; 6C47 .addr unitGlyphBoomerLeft ; 6C49 .addr unitGlyphBoomerRight ; 6C4B .addr unitGlyphSpyUp ; 6C4D .addr unitGlyphSpyDown ; 6C4F .addr unitGlyphSpyLeft ; 6C51 .addr unitGlyphSpyRight ; 6C53 .addr unitGlyphComcenUp ; 6C55 .addr unitGlyphComcenDown ; 6C57 .addr unitGlyphComcenLeft ; 6C59 .addr unitGlyphComcenRight ; 6C5B ; Unit glyph shapes - the 640 bytes $6C5D-$6EDC are 20 unit pictures of 32 bytes, one for each unit ; type and facing, in the order unitGlyphPointerTable indexes them (type*4 + facing). A picture is one ; map cell of the 7x5 tactical view: 2x2 character cells = 16x16 pixels, stored as top-left, ; top-right, bottom-left and bottom-right, 8 bytes each, which is why the bit art below shows each ; picture as four separate 8x8 blocks rather than one square. The bitmap is multicolour, so a pixel is ; a pair of bits and every pixel drawn here is %11, whose colour comes from colour RAM - that is how ; one shape serves both sides in their own colour. Facings 0 and 1 are a vertical mirror pair and 2 ; and 3 a horizontal pair; the survey reads them as up, down, left, right. ; ; GRUNT, facing 0 (up) - glyph index 0. 32 bytes: TL, TR, BL, BR. unitGlyphGruntUp: .byte $00 ; 6C5D ........ .byte $00 ; 6C5E ........ .byte $00 ; 6C5F ........ .byte $03 ; 6C60 ......## .byte $03 ; 6C61 ......## .byte $03 ; 6C62 ......## .byte $0F ; 6C63 ....#### .byte $0F ; 6C64 ....#### .byte $00 ; 6C65 ........ .byte $00 ; 6C66 ........ .byte $00 ; 6C67 ........ .byte $00 ; 6C68 ........ .byte $00 ; 6C69 ........ .byte $00 ; 6C6A ........ .byte $C0 ; 6C6B ##...... .byte $C0 ; 6C6C ##...... .byte $33 ; 6C6D ..##..## .byte $3F ; 6C6E ..###### .byte $3F ; 6C6F ..###### .byte $3F ; 6C70 ..###### .byte $0F ; 6C71 ....#### .byte $00 ; 6C72 ........ .byte $00 ; 6C73 ........ .byte $00 ; 6C74 ........ .byte $30 ; 6C75 ..##.... .byte $F0 ; 6C76 ####.... .byte $F0 ; 6C77 ####.... .byte $F0 ; 6C78 ####.... .byte $C0 ; 6C79 ##...... .byte $00 ; 6C7A ........ .byte $00 ; 6C7B ........ .byte $00 ; 6C7C ........ ; GRUNT, facing 1 (down) - glyph index 1. 32 bytes: TL, TR, BL, BR. unitGlyphGruntDown: .byte $00 ; 6C7D ........ .byte $00 ; 6C7E ........ .byte $00 ; 6C7F ........ .byte $0F ; 6C80 ....#### .byte $3F ; 6C81 ..###### .byte $3F ; 6C82 ..###### .byte $3F ; 6C83 ..###### .byte $33 ; 6C84 ..##..## .byte $00 ; 6C85 ........ .byte $00 ; 6C86 ........ .byte $00 ; 6C87 ........ .byte $C0 ; 6C88 ##...... .byte $F0 ; 6C89 ####.... .byte $F0 ; 6C8A ####.... .byte $F0 ; 6C8B ####.... .byte $30 ; 6C8C ..##.... .byte $0F ; 6C8D ....#### .byte $0F ; 6C8E ....#### .byte $03 ; 6C8F ......## .byte $03 ; 6C90 ......## .byte $03 ; 6C91 ......## .byte $00 ; 6C92 ........ .byte $00 ; 6C93 ........ .byte $00 ; 6C94 ........ .byte $C0 ; 6C95 ##...... .byte $C0 ; 6C96 ##...... .byte $00 ; 6C97 ........ .byte $00 ; 6C98 ........ .byte $00 ; 6C99 ........ .byte $00 ; 6C9A ........ .byte $00 ; 6C9B ........ .byte $00 ; 6C9C ........ ; GRUNT, facing 2 (left) - glyph index 2. 32 bytes: TL, TR, BL, BR. unitGlyphGruntLeft: .byte $00 ; 6C9D ........ .byte $00 ; 6C9E ........ .byte $00 ; 6C9F ........ .byte $0F ; 6CA0 ....#### .byte $0F ; 6CA1 ....#### .byte $3F ; 6CA2 ..###### .byte $3C ; 6CA3 ..####.. .byte $3F ; 6CA4 ..###### .byte $00 ; 6CA5 ........ .byte $00 ; 6CA6 ........ .byte $00 ; 6CA7 ........ .byte $00 ; 6CA8 ........ .byte $00 ; 6CA9 ........ .byte $C0 ; 6CAA ##...... .byte $C0 ; 6CAB ##...... .byte $FC ; 6CAC ######.. .byte $3F ; 6CAD ..###### .byte $3C ; 6CAE ..####.. .byte $3F ; 6CAF ..###### .byte $0F ; 6CB0 ....#### .byte $0F ; 6CB1 ....#### .byte $00 ; 6CB2 ........ .byte $00 ; 6CB3 ........ .byte $00 ; 6CB4 ........ .byte $FC ; 6CB5 ######.. .byte $C0 ; 6CB6 ##...... .byte $C0 ; 6CB7 ##...... .byte $00 ; 6CB8 ........ .byte $00 ; 6CB9 ........ .byte $00 ; 6CBA ........ .byte $00 ; 6CBB ........ .byte $00 ; 6CBC ........ ; GRUNT, facing 3 (right) - glyph index 3. 32 bytes: TL, TR, BL, BR. unitGlyphGruntRight: .byte $00 ; 6CBD ........ .byte $00 ; 6CBE ........ .byte $00 ; 6CBF ........ .byte $00 ; 6CC0 ........ .byte $00 ; 6CC1 ........ .byte $03 ; 6CC2 ......## .byte $03 ; 6CC3 ......## .byte $3F ; 6CC4 ..###### .byte $00 ; 6CC5 ........ .byte $00 ; 6CC6 ........ .byte $00 ; 6CC7 ........ .byte $F0 ; 6CC8 ####.... .byte $F0 ; 6CC9 ####.... .byte $FC ; 6CCA ######.. .byte $3C ; 6CCB ..####.. .byte $FC ; 6CCC ######.. .byte $3F ; 6CCD ..###### .byte $03 ; 6CCE ......## .byte $03 ; 6CCF ......## .byte $00 ; 6CD0 ........ .byte $00 ; 6CD1 ........ .byte $00 ; 6CD2 ........ .byte $00 ; 6CD3 ........ .byte $00 ; 6CD4 ........ .byte $FC ; 6CD5 ######.. .byte $3C ; 6CD6 ..####.. .byte $FC ; 6CD7 ######.. .byte $F0 ; 6CD8 ####.... .byte $F0 ; 6CD9 ####.... .byte $00 ; 6CDA ........ .byte $00 ; 6CDB ........ .byte $00 ; 6CDC ........ ; RIDER, facing 0 (up) - glyph index 4. 32 bytes: TL, TR, BL, BR. unitGlyphRiderUp: .byte $00 ; 6CDD ........ .byte $00 ; 6CDE ........ .byte $00 ; 6CDF ........ .byte $00 ; 6CE0 ........ .byte $03 ; 6CE1 ......## .byte $33 ; 6CE2 ..##..## .byte $33 ; 6CE3 ..##..## .byte $0F ; 6CE4 ....#### .byte $00 ; 6CE5 ........ .byte $00 ; 6CE6 ........ .byte $00 ; 6CE7 ........ .byte $00 ; 6CE8 ........ .byte $00 ; 6CE9 ........ .byte $30 ; 6CEA ..##.... .byte $30 ; 6CEB ..##.... .byte $C0 ; 6CEC ##...... .byte $0F ; 6CED ....#### .byte $FF ; 6CEE ######## .byte $F0 ; 6CEF ####.... .byte $C0 ; 6CF0 ##...... .byte $00 ; 6CF1 ........ .byte $00 ; 6CF2 ........ .byte $00 ; 6CF3 ........ .byte $00 ; 6CF4 ........ .byte $C0 ; 6CF5 ##...... .byte $FC ; 6CF6 ######.. .byte $3C ; 6CF7 ..####.. .byte $0C ; 6CF8 ....##.. .byte $00 ; 6CF9 ........ .byte $00 ; 6CFA ........ .byte $00 ; 6CFB ........ .byte $00 ; 6CFC ........ ; RIDER, facing 1 (down) - glyph index 5. 32 bytes: TL, TR, BL, BR. unitGlyphRiderDown: .byte $00 ; 6CFD ........ .byte $00 ; 6CFE ........ .byte $00 ; 6CFF ........ .byte $C0 ; 6D00 ##...... .byte $F0 ; 6D01 ####.... .byte $FF ; 6D02 ######## .byte $0F ; 6D03 ....#### .byte $0F ; 6D04 ....#### .byte $00 ; 6D05 ........ .byte $00 ; 6D06 ........ .byte $00 ; 6D07 ........ .byte $0C ; 6D08 ....##.. .byte $3C ; 6D09 ..####.. .byte $FC ; 6D0A ######.. .byte $C0 ; 6D0B ##...... .byte $C0 ; 6D0C ##...... .byte $33 ; 6D0D ..##..## .byte $33 ; 6D0E ..##..## .byte $03 ; 6D0F ......## .byte $00 ; 6D10 ........ .byte $00 ; 6D11 ........ .byte $00 ; 6D12 ........ .byte $00 ; 6D13 ........ .byte $00 ; 6D14 ........ .byte $30 ; 6D15 ..##.... .byte $30 ; 6D16 ..##.... .byte $00 ; 6D17 ........ .byte $00 ; 6D18 ........ .byte $00 ; 6D19 ........ .byte $00 ; 6D1A ........ .byte $00 ; 6D1B ........ .byte $00 ; 6D1C ........ ; RIDER, facing 2 (left) - glyph index 6. 32 bytes: TL, TR, BL, BR. unitGlyphRiderLeft: .byte $00 ; 6D1D ........ .byte $00 ; 6D1E ........ .byte $3C ; 6D1F ..####.. .byte $3C ; 6D20 ..####.. .byte $0C ; 6D21 ....##.. .byte $0F ; 6D22 ....#### .byte $0F ; 6D23 ....#### .byte $0F ; 6D24 ....#### .byte $00 ; 6D25 ........ .byte $00 ; 6D26 ........ .byte $00 ; 6D27 ........ .byte $00 ; 6D28 ........ .byte $C0 ; 6D29 ##...... .byte $C0 ; 6D2A ##...... .byte $00 ; 6D2B ........ .byte $F0 ; 6D2C ####.... .byte $0F ; 6D2D ....#### .byte $0F ; 6D2E ....#### .byte $0F ; 6D2F ....#### .byte $0C ; 6D30 ....##.. .byte $3C ; 6D31 ..####.. .byte $3C ; 6D32 ..####.. .byte $00 ; 6D33 ........ .byte $00 ; 6D34 ........ .byte $F0 ; 6D35 ####.... .byte $00 ; 6D36 ........ .byte $C0 ; 6D37 ##...... .byte $C0 ; 6D38 ##...... .byte $00 ; 6D39 ........ .byte $00 ; 6D3A ........ .byte $00 ; 6D3B ........ .byte $00 ; 6D3C ........ ; RIDER, facing 3 (right) - glyph index 7. 32 bytes: TL, TR, BL, BR. unitGlyphRiderRight: .byte $00 ; 6D3D ........ .byte $00 ; 6D3E ........ .byte $00 ; 6D3F ........ .byte $00 ; 6D40 ........ .byte $0C ; 6D41 ....##.. .byte $0F ; 6D42 ....#### .byte $03 ; 6D43 ......## .byte $3F ; 6D44 ..###### .byte $00 ; 6D45 ........ .byte $00 ; 6D46 ........ .byte $F0 ; 6D47 ####.... .byte $F0 ; 6D48 ####.... .byte $C0 ; 6D49 ##...... .byte $C0 ; 6D4A ##...... .byte $C0 ; 6D4B ##...... .byte $C0 ; 6D4C ##...... .byte $3F ; 6D4D ..###### .byte $03 ; 6D4E ......## .byte $0F ; 6D4F ....#### .byte $0C ; 6D50 ....##.. .byte $00 ; 6D51 ........ .byte $00 ; 6D52 ........ .byte $00 ; 6D53 ........ .byte $00 ; 6D54 ........ .byte $C0 ; 6D55 ##...... .byte $C0 ; 6D56 ##...... .byte $C0 ; 6D57 ##...... .byte $C0 ; 6D58 ##...... .byte $F0 ; 6D59 ####.... .byte $F0 ; 6D5A ####.... .byte $00 ; 6D5B ........ .byte $00 ; 6D5C ........ ; BOOMER, facing 0 (up) - glyph index 8. 32 bytes: TL, TR, BL, BR. unitGlyphBoomerUp: .byte $00 ; 6D5D ........ .byte $0F ; 6D5E ....#### .byte $03 ; 6D5F ......## .byte $03 ; 6D60 ......## .byte $03 ; 6D61 ......## .byte $03 ; 6D62 ......## .byte $03 ; 6D63 ......## .byte $C3 ; 6D64 ##....## .byte $00 ; 6D65 ........ .byte $C0 ; 6D66 ##...... .byte $00 ; 6D67 ........ .byte $00 ; 6D68 ........ .byte $00 ; 6D69 ........ .byte $00 ; 6D6A ........ .byte $00 ; 6D6B ........ .byte $0C ; 6D6C ....##.. .byte $FF ; 6D6D ######## .byte $FF ; 6D6E ######## .byte $FF ; 6D6F ######## .byte $FF ; 6D70 ######## .byte $FF ; 6D71 ######## .byte $C0 ; 6D72 ##...... .byte $00 ; 6D73 ........ .byte $00 ; 6D74 ........ .byte $FC ; 6D75 ######.. .byte $FC ; 6D76 ######.. .byte $FC ; 6D77 ######.. .byte $FC ; 6D78 ######.. .byte $FC ; 6D79 ######.. .byte $0C ; 6D7A ....##.. .byte $00 ; 6D7B ........ .byte $00 ; 6D7C ........ ; BOOMER, facing 1 (down) - glyph index 9. 32 bytes: TL, TR, BL, BR. unitGlyphBoomerDown: .byte $00 ; 6D7D ........ .byte $00 ; 6D7E ........ .byte $C0 ; 6D7F ##...... .byte $FF ; 6D80 ######## .byte $FF ; 6D81 ######## .byte $FF ; 6D82 ######## .byte $FF ; 6D83 ######## .byte $FF ; 6D84 ######## .byte $00 ; 6D85 ........ .byte $00 ; 6D86 ........ .byte $0C ; 6D87 ....##.. .byte $FC ; 6D88 ######.. .byte $FC ; 6D89 ######.. .byte $FC ; 6D8A ######.. .byte $FC ; 6D8B ######.. .byte $FC ; 6D8C ######.. .byte $C3 ; 6D8D ##....## .byte $03 ; 6D8E ......## .byte $03 ; 6D8F ......## .byte $03 ; 6D90 ......## .byte $03 ; 6D91 ......## .byte $03 ; 6D92 ......## .byte $0F ; 6D93 ....#### .byte $00 ; 6D94 ........ .byte $0C ; 6D95 ....##.. .byte $00 ; 6D96 ........ .byte $00 ; 6D97 ........ .byte $00 ; 6D98 ........ .byte $00 ; 6D99 ........ .byte $00 ; 6D9A ........ .byte $C0 ; 6D9B ##...... .byte $00 ; 6D9C ........ ; BOOMER, facing 2 (left) - glyph index 10. 32 bytes: TL, TR, BL, BR. unitGlyphBoomerLeft: .byte $00 ; 6D9D ........ .byte $00 ; 6D9E ........ .byte $00 ; 6D9F ........ .byte $FF ; 6DA0 ######## .byte $3F ; 6DA1 ..###### .byte $3F ; 6DA2 ..###### .byte $3F ; 6DA3 ..###### .byte $3F ; 6DA4 ..###### .byte $00 ; 6DA5 ........ .byte $00 ; 6DA6 ........ .byte $00 ; 6DA7 ........ .byte $C0 ; 6DA8 ##...... .byte $00 ; 6DA9 ........ .byte $00 ; 6DAA ........ .byte $03 ; 6DAB ......## .byte $03 ; 6DAC ......## .byte $3F ; 6DAD ..###### .byte $3F ; 6DAE ..###### .byte $3F ; 6DAF ..###### .byte $3F ; 6DB0 ..###### .byte $3F ; 6DB1 ..###### .byte $FF ; 6DB2 ######## .byte $00 ; 6DB3 ........ .byte $00 ; 6DB4 ........ .byte $FF ; 6DB5 ######## .byte $03 ; 6DB6 ......## .byte $03 ; 6DB7 ......## .byte $00 ; 6DB8 ........ .byte $00 ; 6DB9 ........ .byte $C0 ; 6DBA ##...... .byte $00 ; 6DBB ........ .byte $00 ; 6DBC ........ ; BOOMER, facing 3 (right) - glyph index 11. 32 bytes: TL, TR, BL, BR. unitGlyphBoomerRight: .byte $00 ; 6DBD ........ .byte $00 ; 6DBE ........ .byte $00 ; 6DBF ........ .byte $03 ; 6DC0 ......## .byte $00 ; 6DC1 ........ .byte $00 ; 6DC2 ........ .byte $C0 ; 6DC3 ##...... .byte $C0 ; 6DC4 ##...... .byte $00 ; 6DC5 ........ .byte $00 ; 6DC6 ........ .byte $00 ; 6DC7 ........ .byte $FF ; 6DC8 ######## .byte $FC ; 6DC9 ######.. .byte $FC ; 6DCA ######.. .byte $FC ; 6DCB ######.. .byte $FC ; 6DCC ######.. .byte $FF ; 6DCD ######## .byte $C0 ; 6DCE ##...... .byte $C0 ; 6DCF ##...... .byte $00 ; 6DD0 ........ .byte $00 ; 6DD1 ........ .byte $03 ; 6DD2 ......## .byte $00 ; 6DD3 ........ .byte $00 ; 6DD4 ........ .byte $FC ; 6DD5 ######.. .byte $FC ; 6DD6 ######.. .byte $FC ; 6DD7 ######.. .byte $FC ; 6DD8 ######.. .byte $FC ; 6DD9 ######.. .byte $FF ; 6DDA ######## .byte $00 ; 6DDB ........ .byte $00 ; 6DDC ........ ; SPY, facing 0 (up) - glyph index 12. 32 bytes: TL, TR, BL, BR. unitGlyphSpyUp: .byte $00 ; 6DDD ........ .byte $00 ; 6DDE ........ .byte $00 ; 6DDF ........ .byte $00 ; 6DE0 ........ .byte $30 ; 6DE1 ..##.... .byte $30 ; 6DE2 ..##.... .byte $3C ; 6DE3 ..####.. .byte $0C ; 6DE4 ....##.. .byte $00 ; 6DE5 ........ .byte $00 ; 6DE6 ........ .byte $00 ; 6DE7 ........ .byte $00 ; 6DE8 ........ .byte $30 ; 6DE9 ..##.... .byte $30 ; 6DEA ..##.... .byte $F0 ; 6DEB ####.... .byte $C0 ; 6DEC ##...... .byte $0F ; 6DED ....#### .byte $03 ; 6DEE ......## .byte $03 ; 6DEF ......## .byte $03 ; 6DF0 ......## .byte $00 ; 6DF1 ........ .byte $00 ; 6DF2 ........ .byte $00 ; 6DF3 ........ .byte $00 ; 6DF4 ........ .byte $C0 ; 6DF5 ##...... .byte $00 ; 6DF6 ........ .byte $00 ; 6DF7 ........ .byte $00 ; 6DF8 ........ .byte $00 ; 6DF9 ........ .byte $00 ; 6DFA ........ .byte $00 ; 6DFB ........ .byte $00 ; 6DFC ........ ; SPY, facing 1 (down) - glyph index 13. 32 bytes: TL, TR, BL, BR. unitGlyphSpyDown: .byte $00 ; 6DFD ........ .byte $00 ; 6DFE ........ .byte $00 ; 6DFF ........ .byte $03 ; 6E00 ......## .byte $03 ; 6E01 ......## .byte $03 ; 6E02 ......## .byte $0F ; 6E03 ....#### .byte $0C ; 6E04 ....##.. .byte $00 ; 6E05 ........ .byte $00 ; 6E06 ........ .byte $00 ; 6E07 ........ .byte $00 ; 6E08 ........ .byte $00 ; 6E09 ........ .byte $00 ; 6E0A ........ .byte $C0 ; 6E0B ##...... .byte $C0 ; 6E0C ##...... .byte $3C ; 6E0D ..####.. .byte $30 ; 6E0E ..##.... .byte $30 ; 6E0F ..##.... .byte $00 ; 6E10 ........ .byte $00 ; 6E11 ........ .byte $00 ; 6E12 ........ .byte $00 ; 6E13 ........ .byte $00 ; 6E14 ........ .byte $F0 ; 6E15 ####.... .byte $30 ; 6E16 ..##.... .byte $30 ; 6E17 ..##.... .byte $00 ; 6E18 ........ .byte $00 ; 6E19 ........ .byte $00 ; 6E1A ........ .byte $00 ; 6E1B ........ .byte $00 ; 6E1C ........ ; SPY, facing 2 (left) - glyph index 14. 32 bytes: TL, TR, BL, BR. unitGlyphSpyLeft: .byte $00 ; 6E1D ........ .byte $00 ; 6E1E ........ .byte $00 ; 6E1F ........ .byte $00 ; 6E20 ........ .byte $00 ; 6E21 ........ .byte $03 ; 6E22 ......## .byte $03 ; 6E23 ......## .byte $3C ; 6E24 ..####.. .byte $00 ; 6E25 ........ .byte $00 ; 6E26 ........ .byte $00 ; 6E27 ........ .byte $F0 ; 6E28 ####.... .byte $F0 ; 6E29 ####.... .byte $00 ; 6E2A ........ .byte $00 ; 6E2B ........ .byte $00 ; 6E2C ........ .byte $3C ; 6E2D ..####.. .byte $03 ; 6E2E ......## .byte $03 ; 6E2F ......## .byte $00 ; 6E30 ........ .byte $00 ; 6E31 ........ .byte $00 ; 6E32 ........ .byte $00 ; 6E33 ........ .byte $00 ; 6E34 ........ .byte $00 ; 6E35 ........ .byte $00 ; 6E36 ........ .byte $00 ; 6E37 ........ .byte $F0 ; 6E38 ####.... .byte $F0 ; 6E39 ####.... .byte $00 ; 6E3A ........ .byte $00 ; 6E3B ........ .byte $00 ; 6E3C ........ ; SPY, facing 3 (right) - glyph index 15. 32 bytes: TL, TR, BL, BR. unitGlyphSpyRight: .byte $00 ; 6E3D ........ .byte $00 ; 6E3E ........ .byte $00 ; 6E3F ........ .byte $3C ; 6E40 ..####.. .byte $3C ; 6E41 ..####.. .byte $03 ; 6E42 ......## .byte $03 ; 6E43 ......## .byte $00 ; 6E44 ........ .byte $00 ; 6E45 ........ .byte $00 ; 6E46 ........ .byte $00 ; 6E47 ........ .byte $00 ; 6E48 ........ .byte $00 ; 6E49 ........ .byte $00 ; 6E4A ........ .byte $00 ; 6E4B ........ .byte $F0 ; 6E4C ####.... .byte $00 ; 6E4D ........ .byte $03 ; 6E4E ......## .byte $03 ; 6E4F ......## .byte $3C ; 6E50 ..####.. .byte $3C ; 6E51 ..####.. .byte $00 ; 6E52 ........ .byte $00 ; 6E53 ........ .byte $00 ; 6E54 ........ .byte $F0 ; 6E55 ####.... .byte $00 ; 6E56 ........ .byte $00 ; 6E57 ........ .byte $00 ; 6E58 ........ .byte $00 ; 6E59 ........ .byte $00 ; 6E5A ........ .byte $00 ; 6E5B ........ .byte $00 ; 6E5C ........ ; COMCEN, facing 0 (up) - glyph index 16. 32 bytes: TL, TR, BL, BR. unitGlyphComcenUp: .byte $00 ; 6E5D ........ .byte $0F ; 6E5E ....#### .byte $0F ; 6E5F ....#### .byte $3F ; 6E60 ..###### .byte $3F ; 6E61 ..###### .byte $FF ; 6E62 ######## .byte $F0 ; 6E63 ####.... .byte $FF ; 6E64 ######## .byte $00 ; 6E65 ........ .byte $C0 ; 6E66 ##...... .byte $C0 ; 6E67 ##...... .byte $F0 ; 6E68 ####.... .byte $F0 ; 6E69 ####.... .byte $FC ; 6E6A ######.. .byte $3C ; 6E6B ..####.. .byte $FC ; 6E6C ######.. .byte $FF ; 6E6D ######## .byte $FF ; 6E6E ######## .byte $FF ; 6E6F ######## .byte $FF ; 6E70 ######## .byte $FF ; 6E71 ######## .byte $FF ; 6E72 ######## .byte $FF ; 6E73 ######## .byte $00 ; 6E74 ........ .byte $FC ; 6E75 ######.. .byte $FC ; 6E76 ######.. .byte $FC ; 6E77 ######.. .byte $FC ; 6E78 ######.. .byte $FC ; 6E79 ######.. .byte $FC ; 6E7A ######.. .byte $FC ; 6E7B ######.. .byte $00 ; 6E7C ........ ; COMCEN, facing 1 (down) - glyph index 17. 32 bytes: TL, TR, BL, BR. unitGlyphComcenDown: .byte $00 ; 6E7D ........ .byte $00 ; 6E7E ........ .byte $FF ; 6E7F ######## .byte $FF ; 6E80 ######## .byte $FF ; 6E81 ######## .byte $FF ; 6E82 ######## .byte $FF ; 6E83 ######## .byte $FF ; 6E84 ######## .byte $00 ; 6E85 ........ .byte $00 ; 6E86 ........ .byte $FC ; 6E87 ######.. .byte $FC ; 6E88 ######.. .byte $FC ; 6E89 ######.. .byte $FC ; 6E8A ######.. .byte $FC ; 6E8B ######.. .byte $FC ; 6E8C ######.. .byte $FF ; 6E8D ######## .byte $FF ; 6E8E ######## .byte $F0 ; 6E8F ####.... .byte $FF ; 6E90 ######## .byte $3F ; 6E91 ..###### .byte $3F ; 6E92 ..###### .byte $0F ; 6E93 ....#### .byte $0F ; 6E94 ....#### .byte $FC ; 6E95 ######.. .byte $FC ; 6E96 ######.. .byte $3C ; 6E97 ..####.. .byte $FC ; 6E98 ######.. .byte $F0 ; 6E99 ####.... .byte $F0 ; 6E9A ####.... .byte $C0 ; 6E9B ##...... .byte $C0 ; 6E9C ##...... ; COMCEN, facing 2 (left) - glyph index 18. 32 bytes: TL, TR, BL, BR. unitGlyphComcenLeft: .byte $00 ; 6E9D ........ .byte $00 ; 6E9E ........ .byte $FF ; 6E9F ######## .byte $FF ; 6EA0 ######## .byte $FF ; 6EA1 ######## .byte $FF ; 6EA2 ######## .byte $FF ; 6EA3 ######## .byte $FF ; 6EA4 ######## .byte $00 ; 6EA5 ........ .byte $00 ; 6EA6 ........ .byte $C0 ; 6EA7 ##...... .byte $F0 ; 6EA8 ####.... .byte $FC ; 6EA9 ######.. .byte $FF ; 6EAA ######## .byte $3F ; 6EAB ..###### .byte $3F ; 6EAC ..###### .byte $FF ; 6EAD ######## .byte $FF ; 6EAE ######## .byte $FF ; 6EAF ######## .byte $FF ; 6EB0 ######## .byte $FF ; 6EB1 ######## .byte $FF ; 6EB2 ######## .byte $00 ; 6EB3 ........ .byte $00 ; 6EB4 ........ .byte $3F ; 6EB5 ..###### .byte $3F ; 6EB6 ..###### .byte $3F ; 6EB7 ..###### .byte $FC ; 6EB8 ######.. .byte $F0 ; 6EB9 ####.... .byte $C0 ; 6EBA ##...... .byte $00 ; 6EBB ........ .byte $00 ; 6EBC ........ ; COMCEN, facing 3 (right) - glyph index 19. 32 bytes: TL, TR, BL, BR. unitGlyphComcenRight: .byte $00 ; 6EBD ........ .byte $00 ; 6EBE ........ .byte $03 ; 6EBF ......## .byte $0F ; 6EC0 ....#### .byte $3F ; 6EC1 ..###### .byte $FF ; 6EC2 ######## .byte $FC ; 6EC3 ######.. .byte $FC ; 6EC4 ######.. .byte $00 ; 6EC5 ........ .byte $00 ; 6EC6 ........ .byte $FF ; 6EC7 ######## .byte $FF ; 6EC8 ######## .byte $FF ; 6EC9 ######## .byte $FF ; 6ECA ######## .byte $FF ; 6ECB ######## .byte $FF ; 6ECC ######## .byte $FC ; 6ECD ######.. .byte $FC ; 6ECE ######.. .byte $FC ; 6ECF ######.. .byte $3F ; 6ED0 ..###### .byte $0F ; 6ED1 ....#### .byte $03 ; 6ED2 ......## .byte $00 ; 6ED3 ........ .byte $00 ; 6ED4 ........ .byte $FF ; 6ED5 ######## .byte $FF ; 6ED6 ######## .byte $FF ; 6ED7 ######## .byte $FF ; 6ED8 ######## .byte $FF ; 6ED9 ######## .byte $FF ; 6EDA ######## .byte $00 ; 6EDB ........ .byte $00 ; 6EDC ........ ; padding6EDD - 35 zero bytes filling out the last sector of the main program to $6EFF. Nothing reads ; them; the first byte the game does use above this is the overlay window at $6F00. padding6EDD: .byte $00,$00,$00,$00,$00,$00,$00,$00; 6EDD ........ unused filler to the end of the $0800-$6EFF image .byte $00,$00,$00,$00,$00,$00,$00,$00; 6EE5 ........ .byte $00,$00,$00,$00,$00,$00,$00,$00; 6EED ........ .byte $00,$00,$00,$00,$00,$00,$00,$00; 6EF5 ........ .byte $00,$00,$00 ; 6EFD ...