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

579 lines
43 KiB
ArmAsm

; ============================================================================
; $EE00-$EFFF - solo trainer: drone flight AI (track 29 sectors 4-5)
; ============================================================================
; Swapped in by the trainer module ($E0D6) while its drone is in the air and swapped back out afterwards.
.setcpu "6502"
.include "c64.inc"
.include "zeropage.inc"
; ---- references to code/data outside this file ----
diagonalDirectionTable := $1FB2
directionDeltaTable := $20F0
headingToDirectionTable := $7316
mapCellToRadarGrid := $7A13
D_896F := $896F
D_8973 := $8973
D_8974 := $8974
D_8975 := $8975
D_8976 := $8976
D_8978 := $8978
D_8979 := $8979
currentScreen := $90FB
missilesLeft := $92B0
droneArrivalFlags := $92F9
rxPacketLength := $E01E
rxPacketBuffer := $E020
rxPacketArg1 := $E021
rxPacketArg2 := $E022
rxPacketArg3 := $E023
getStepDirection := $E80E
cellDistance := $E82A
scoreTargetBlock := $E9A6
mirrorBlockCornerForOpponent := $EA38
unitColTable := $F640
unitRowTable := $F6A4
unitStunTable := $F960
nextGameRandom := $FD5D
; Contents
; --------
; $EE00 flyTrainerDroneStep one flight step of the solo trainer's drone, run once per lock-step command
; exchange for as long as this track-29 s4-5 page is the resident $EE00 image.
; $EE15 pickDroneAimPointAndDecide decides between blowing the drone up where it stands and flying it one more
; cell.
; $EE2D midCheckDroneOnMapEE2D NOT an entry point in this image.
; $EE55 midStoreDetonateThresholdEE55 NOT an entry point in this image.
; $EE85 midScanDiagonalBlockEE85 NOT an entry point in this image.
; $EEB6 emitDroneDetonateCommand writes the 3-byte command $8E (detonate) plus the drone's current cell into the
; fabricated received packet.
; $EED0 maybeJinkDroneHeading takes the heading getStepDirection worked out and, under a narrow set of
; conditions, throws it away for a random one.
; $EEED midCheckFuelForJinkEEED NOT an entry point in this image.
; $EF0F steerDroneAndEmitMove turns the drone at most one 45-degree step towards the desired heading, moves
; it one cell that way and reports the move as command $8D.
; $EF9E droneDistanceToCell measures the distance from the cell in zp_22/zp_24 to the drone.
.org $EE00
; ----------------------------------------------------------------------
; flyTrainerDroneStep - one flight step of the solo trainer's drone, run once per lock-step command
; exchange for as long as this track-29 s4-5 page is the resident $EE00 image. The whole page has
; exactly one product: a fake 'packet received from the opponent' in rxPacketBuffer $E020 with its
; length in rxPacketLength $E01E, holding either $8F (drone released, 1 byte), $8E col row (detonate,
; 3 bytes) or $8D heading col row (fly one cell, 4 bytes). Everything else in the game - the command
; interpreter, the game film, the screens - then treats the AI's drone exactly like a modem
; opponent's. This entry only disposes of the two trivial cases (the flight is already over, or the
; fuel has run out) and falls through into pickDroneAimPointAndDecide for the real decision.
; In: droneArrivalFlags $92F9 (bit 7 = a $92 'drone finished' command has already gone through a
; packet merger, i.e. the drone was shot down or has arrived), aiDroneFuel $8973 (steps left, $64
; = 100 at launch), drone cell $8974/$8975, drone target unit $8976, aiDroneDetonateThreshold
; $8978, unit arrays $F640/$F6A4/$F960 and the map at $F000
; Out: $E01E and $E020.. = the fabricated opponent packet; $8973 decremented; on the flight path also
; $896F (heading) and $8974/$8975 (cell); zp_18, zp_19, zp_1C, zp_1D, zp_22, zp_24, zp_27, zp_28
; and the line-stepper state zp_8C-zp_97 clobbered
; Called from: the trainer module's buildTrainerReplyPacket - the 'jsr $EE00' at $E0A5, reached only
; while droneOverlayState $8977 has bit 7 set (this page loaded). Note that in the trainer's own
; $EE00 image the same address is just a fall-through label of fireMissileAtSpottedUnit, so that
; one JSR means two entirely different things depending on which page is resident.
; Is there still a drone to fly, and does it still have fuel? A drone flight costs two disk loads:
; $E0C1-$E0D9 of the trainer loads this page (track 29 s4-5) as soon as droneStateFlags $920D bit 0
; says a
; drone is airborne, and $E0AB-$E0BE loads the trainer's own tail (track 35 s14-15) back when that bit
; clears. While this page is resident the trainer issues no other orders at all, because all of its
; other think routines are in the page that was swapped out.
; ----------------------------------------------------------------------
flyTrainerDroneStep:
lda droneArrivalFlags ; EE00 droneArrivalFlags: bit 7 goes up the moment command $92 (drone finished) passes through mergeIncomingPacket ($4F42) or mergeOutgoingPacket ($4FA8)
bmi droneStepDone ; EE03 branch if the flight is already over (shot down by the player's missile, or arrived) - emit nothing this exchange
dec D_8973 ; EE05 spend one step of drone fuel; launchAiDrone loaded aiDroneFuel with $64 = 100 steps
bpl pickDroneAimPointAndDecide; EE08 still 0 or more steps left: go and decide where to fly
lda #$8F ; EE0A out of fuel: command $8F = cmdDroneRelease ($537B), which for a remote command clears $920D bits 0-5 - that is what makes the trainer swap this page back out next exchange
sta rxPacketBuffer ; EE0C the fabricated packet's only command byte (also trainerAiE000:rxPacketBuffer)
setRxPacketLengthOne:
lda #$01 ; EE0F $8F carries no arguments
sta rxPacketLength ; EE11 one byte of 'received' packet is waiting for mergeIncomingPacket ($4F05) (also trainerAiE000:rxPacketLength)
droneStepDone:
rts ; EE14 back to $E0A8 in the trainer module
; ----------------------------------------------------------------------
; pickDroneAimPointAndDecide - decides between blowing the drone up where it stands and flying it one
; more cell. Within 6 cells of its target unit it scores the 2x2 block the drone covers with
; scoreTargetBlock ($E9A6) and detonates as soon as that score reaches aiDroneDetonateThreshold $8978
; - or only three quarters of it when the target is stunned and so cannot walk out from under the
; blast. Otherwise it scores the four 2x2 blocks that touch the target's cell, keeps the best corner
; as the aim point in zp_95/zp_94, points the line stepper from the drone's cell at it and asks
; getStepDirection ($E80E) for a compass heading; 'no direction' means the drone is already standing
; on its aim cell, which detonates as well.
; In: $8976 drone target unit, unit position arrays $F640/$F6A4, drone cell $8974/$8975,
; aiDroneDetonateThreshold $8978, unitStunTable $F960, the map through scoreTargetBlock
; Out: zp_1C = best block score, zp_1D = highest-numbered player unit in it, zp_95/zp_94 = aim
; column/row, zp_93/zp_92 = line start column/row, Y = heading 0-7 with N clear when a direction
; was found; continues in emitDroneDetonateCommand or maybeJinkDroneHeading
; Called from: nothing - $EE08 falls into it. It is a block of flyTrainerDroneStep, not a subroutine.
; Phase 1 - how far is the drone from the unit it was launched at, and is the cluster underneath it
; worth spending the drone on? zp_1C is the running best/threshold that scoreTargetBlock compares
; against, so clearing it here means 'only a block worth 0 or more counts'.
; ----------------------------------------------------------------------
pickDroneAimPointAndDecide:
lda #$00 ; EE15 start the scoring with a best-so-far of 0...
sta scratch1C ; EE17 ...scoreTargetBlock ignores any 2x2 block that scores below zp_1C
loadTargetUnitCell:
ldy D_8976 ; EE19 Y = the unit the drone was sent at, picked once per round by chooseAiDroneTarget ($EEE0 of the trainer's own page)
lda unitColTable,y ; EE1C target's map column - unitColTable is the first of the 100-entry unit arrays at $F640, each $64 apart
sta cellColA ; EE1F point A of cellDistance = the target's cell
lda unitRowTable,y ; EE21 target's map row (unitRowTable = $F640 + $64)
sta cellRowA ; EE24 point A row
measureRangeToTarget:
jsr droneDistanceToCell ; EE26 A = X = distance in cells from the drone to the target; Y survives the call, which is why the target index can stay there
cmp #$06 ; EE29 6 cells is the radius inside which detonating is worth considering
bcs aimAtBlockNearTarget ; EE2B branch if the drone is still far out - just pick an aim cell and fly, Y still holds the target unit
; ----------------------------------------------------------------------
; midCheckDroneOnMapEE2D - NOT an entry point in this image. $EE2D is the 'ldx $8974 / bmi' on-map
; test in the middle of pickDroneAimPointAndDecide; the label exists only because the modem driver
; (track 34) has a real routine at this address and XREF merges all five images of $EE00-$EFFF.
; In: -
; Out: -
; Called from: nothing in this image; the two 'call:EDD2,EE13' references in XREF.txt belong to the
; trainer's own page.
; ----------------------------------------------------------------------
midCheckDroneOnMapEE2D:
ldx D_8974 ; EE2D drone column; it is a signed value and can go negative, because the flight code never clamps it to 0..39
bmi aimAtBlockNearTarget ; EE30 branch if the drone has drifted off the left/top edge of the map - it cannot be standing over anything
ldy D_8975 ; EE32 drone row for the block score. NOTE: this also destroys the target unit index in Y, which is a bug on the branch below
bmi aimAtBlockNearTarget ; EE35 branch if the drone row is off the map: aimAtBlockNearTarget then indexes the unit arrays with the drone row instead of the target unit index (probable bug, only reachable once the drone has walked off the map)
jsr scoreTargetBlock ; EE37 score the 2x2 block whose top-left corner is (X,Y) = the drone's cell: the sum of targetValueByUnitType ($E9F1 = grunt 1, rider 2, boomer 4, spy 1, comcen 4) over the units in its four cells, counted positive for the player's units 0-49 and negative for the AI's own 50-99
lda scratch1C ; EE3A the score of the block under the drone (still 0 if the block scored negative)
cmp D_8978 ; EE3C aiDroneDetonateThreshold, copied at trainer setup ($ECC9) out of aiPlanParamTable ($EC0A = $FF,$FF,$08,$0C,$0C), indexed by the opening play the trainer drew ($8966, 0-4); $FF for plays 0 and 1 means 'never detonate early', so those drones only ever blow up when they reach their aim cell
bcs emitDroneDetonateCommand; EE3F branch if the cluster under the drone is worth the threshold - blow up here
; ----------------------------------------------------------------------
; A stunned target cannot walk out of the blast, so accept a cheaper cluster: three quarters of the
; threshold. unitStunTable $F960 holds the stun/pinned countdown in its low nibble (bits 5-7 are the
; dig-in state, bit 7 = dug in).
; ----------------------------------------------------------------------
checkTargetStunned:
ldy D_8976 ; EE41 Y = the target unit index again (it was overwritten by the drone row at $EE32)
lda unitStunTable,y ; EE44 the target's stun byte
and #$0F ; EE47 low nibble = stun/pinned countdown; nonzero is what makes the status panel print STUN'D
beq aimAtBlockNearTarget ; EE49 branch if the target is not stunned - keep the full threshold and fly on
lda D_8978 ; EE4B aiDroneDetonateThreshold T
lsr a ; EE4E T/2
sta scratch18 ; EE4F park T/2
lsr a ; EE51 T/4
clc ; EE52 clear carry before the add (the LSRs left it holding a shifted-out bit)
adc scratch18 ; EE53 T/4 + T/2 = 3T/4
; ----------------------------------------------------------------------
; midStoreDetonateThresholdEE55 - NOT an entry point in this image. $EE55 is the 'sta zp_18' that
; parks the relaxed (3/4) detonate threshold used against a stunned target, inside
; pickDroneAimPointAndDecide. The label is imported from the other images of this page.
; In: -
; Out: -
; Called from: nothing in this image.
; ----------------------------------------------------------------------
midStoreDetonateThresholdEE55:
sta scratch18 ; EE55 zp_18 = the relaxed threshold
lda scratch1C ; EE57 score of the block under the drone
compareScoreWithThreshold:
cmp scratch18 ; EE59 test it against three quarters of the threshold
bcs emitDroneDetonateCommand; EE5B good enough over a stunned target - detonate
; ----------------------------------------------------------------------
; Phase 2 - choose an aim cell. The drone is a 2x2 object addressed by its top-left corner, so the
; four corners that put the target's own cell inside the drone footprint are (col,row), (col-1,row),
; (col-1,row-1) and (col,row-1). Each is handed to scoreTargetBlock, which keeps the best score in
; zp_1C, the highest player unit index in it in zp_1D and writes the winning corner straight into
; lineEndCol/lineEndRow (zp_95/zp_94) - exactly the two bytes the line stepper wants as its end point.
; Y = the target unit index on entry, except on the buggy path from $EE35.
; ----------------------------------------------------------------------
aimAtBlockNearTarget:
lda unitColTable,y ; EE5D target's column (Y is the target unit index; see the note at $EE32 for the one path where it is not)
sta scratch18 ; EE60 zp_18 = target column, kept because X gets reloaded for each of the four corners
sta lineEndCol ; EE62 seed the stepper's end column with the target cell itself, in case all four blocks score negative and none of them is recorded
lda unitRowTable,y ; EE64 target's row
storeAimRow:
sta scratch19 ; EE67 zp_19 = target row
sta lineEndRow ; EE69 seed the end row the same way
scoreFourBlocksAroundTarget:
lda #$FF ; EE6B $FF = 'no best block yet': scoreTargetBlock's 'bit zp_1C / bpl' test accepts any score when the running best is negative
sta scratch1C ; EE6D arm the scan
ldx scratch18 ; EE6F corner 1: X = the target's own column...
ldy scratch19 ; EE71 ...Y = its row, i.e. the block covering (col,row)..(col+1,row+1)
jsr scoreTargetBlock ; EE73 score it; on an improvement the corner lands in zp_95/zp_94 and the best player unit in zp_1D
ldx scratch18 ; EE76 corner 2: target column...
dex ; EE78 ...minus one
bmi scoreBlockNorthOfTarget ; EE79 skip both left-hand corners when the target stands in column 0
ldy scratch19 ; EE7B same row
jsr scoreTargetBlock ; EE7D score the block at (col-1,row)
ldx scratch18 ; EE80 corner 3: column...
dex ; EE82 ...minus one again
ldy scratch19 ; EE83 target row...
; ----------------------------------------------------------------------
; midScanDiagonalBlockEE85 - NOT an entry point in this image. $EE85 is the 'dey' that turns corner
; (col-1,row) into the diagonal corner (col-1,row-1) inside the four-block scan of
; pickDroneAimPointAndDecide. The label is an artifact of the T34/T35 images of this page (in the
; trainer's own page $EE85 is launchAiDrone).
; In: -
; Out: -
; Called from: nothing in this image.
; ----------------------------------------------------------------------
midScanDiagonalBlockEE85:
dey ; EE85 ...minus one: the diagonal corner (col-1,row-1)
bmi setLineStepperEndpoints ; EE86 skip it when the target stands in row 0
jsr scoreTargetBlock ; EE88 score the block at (col-1,row-1)
scoreBlockNorthOfTarget:
ldx scratch18 ; EE8B corner 4: the target's own column
ldy scratch19 ; EE8D target row...
dey ; EE8F ...minus one
bmi setLineStepperEndpoints ; EE90 skip when the target stands in row 0
jsr scoreTargetBlock ; EE92 score the block at (col,row-1)
; ----------------------------------------------------------------------
; Phase 3 - take one Bresenham step from the drone towards the chosen corner. initLineStepper ($FBB8)
; wants the start cell in zp_93/zp_92 (column/row) and the end cell in zp_95/zp_94, and
; getStepDirection ($E80E) runs it once and turns the resulting per-step deltas into a compass
; direction 0-7 (clockwise from north) in Y. A drone coordinate that has gone negative is clamped to
; 0 with the end point shifted by the same amount - except that the subtraction is written the wrong
; way round, so the shifted end point comes out negated (see the line comments).
; ----------------------------------------------------------------------
setLineStepperEndpoints:
lda D_8974 ; EE95 drone column
bpl storeLineStartColumn ; EE98 branch if it is still a sane 0..39 value - use it as the line start
sec ; EE9A prepare the borrow-free subtract
sbc lineEndCol ; EE9B A = droneCol - aimCol. To clamp the start to 0 and keep the vector, the end point needs aimCol - droneCol; this computes the negative of that (probable bug - reachable only after the drone has walked off the left edge)
sta lineEndCol ; EE9D store the shifted end column
lda #$00 ; EE9F and clamp the start column to the left edge
storeLineStartColumn:
sta lineCurCol ; EEA1 zp_93 = line-stepper start column
lda D_8975 ; EEA3 drone row, same treatment
bpl storeLineStartRow ; EEA6 branch if the row is on the map
sec ; EEA8 prepare the subtract
sbc lineEndRow ; EEA9 A = droneRow - aimRow (the same sign slip as the column above)
sta lineEndRow ; EEAB shifted end row
lda #$00 ; EEAD clamp the start row to the top edge
storeLineStartRow:
sta lineCurRow ; EEAF zp_92 = line-stepper start row
jsr getStepDirection ; EEB1 one line step from the drone towards the aim cell, converted to a heading: Y = 0-7 with N clear when the step deltas matched dirToColDelta/dirToRowDelta ($E68A/$E693), or Y = $FF with N set when nothing matched
bpl maybeJinkDroneHeading ; EEB4 branch with a usable heading. Falling through means the stepper did not move at all, i.e. the drone is already standing on its aim cell - so detonate
; ----------------------------------------------------------------------
; emitDroneDetonateCommand - writes the 3-byte command $8E (detonate) plus the drone's current cell
; into the fabricated received packet. Both coordinates go through mirrorBlockCornerForOpponent
; ($EA38, v -> 39-(v+1) = 38-v) because the engine treats everything in that buffer as having arrived
; from the remote player and runs mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) over the
; parameters, which turns them back into the real cell. The handler cmdDroneDetonate ($5336) then
; plays the blast and clears $920D bits 0-5, ending the flight.
; In: drone cell $8974/$8975 (top-left corner of the 2x2 drone)
; Out: $E020 = $8E, $E021/$E022 = pre-mirrored column/row, $E01E = 3; A, X, Y clobbered
; Called from: $EE3F (the cluster under the drone is worth the threshold), $EE5B (relaxed threshold
; over a stunned target) and $EEB4 (the drone is already on its aim cell). All three are
; branches, not calls.
; ----------------------------------------------------------------------
emitDroneDetonateCommand:
lda #$8E ; EEB6 command $8E = cmdDroneDetonate ($5336)
sta rxPacketBuffer ; EEB8 the packet's command byte (also trainerAiE000:rxPacketBuffer)
ldx D_8974 ; EEBB X = drone column
mirrorDroneCellIntoPacket:
ldy D_8975 ; EEBE Y = drone row
jsr mirrorBlockCornerForOpponent; EEC1 pre-compensate the engine's remote-coordinate mirror: X = 38-X, Y = 38-Y
stx rxPacketArg1 ; EEC4 argument 1 = column as the player's machine will read it (also trainerAiE000:rxPacketArg1)
sty rxPacketArg2 ; EEC7 argument 2 = row (also trainerAiE000:rxPacketArg2)
lda #$03 ; EECA command byte plus two arguments
sta rxPacketLength ; EECC hand the 3-byte packet to the engine (also trainerAiE000:rxPacketLength)
rts ; EECF back to $E0A8
; ----------------------------------------------------------------------
; maybeJinkDroneHeading - takes the heading getStepDirection worked out and, under a narrow set of
; conditions, throws it away for a random one. The drone only weaves when the human could actually
; shoot it down: he still has missile strikes, the drone's cell is inside the 21x17 radar window
; centred on his comcen, that radar/missile screen is the one currently on show, the drone has at
; least 12 steps of fuel left and it is at least 3 cells from its aim point. Then two exchanges in
; three (nextGameRandom >= $56) the heading becomes a random 0-7. Since steerDroneAndEmitMove turns
; at most one 45-degree step per exchange the result is a wobble, not a jump. Purpose inferred;
; medium confidence.
; In: Y = heading 0-7 from getStepDirection, missilesLeft $92B0 (the LOCAL player's missiles), drone
; cell $8974/$8975, comcenUnit $9236, currentScreen $90FB, aiDroneFuel $8973, aim cell zp_95/zp_94
; Out: zp_19 = the heading steerDroneAndEmitMove should turn towards; zp_22/zp_24/zp_27/zp_28
; clobbered; falls into steerDroneAndEmitMove
; Called from: nothing - $EEB4 branches into it.
; Evasive weave. Every one of the five tests below asks the same question from a different angle: can
; the human see this drone and do something about it? If not, the drone flies the straight line.
; ----------------------------------------------------------------------
maybeJinkDroneHeading:
sty scratch19 ; EED0 remember the heading the line stepper asked for
lda missilesLeft ; EED2 missile strikes the LOCAL (human) player has left - $9236/$92B0 are all the local player's own state
beq steerDroneAndEmitMove ; EED5 he cannot shoot at all - fly straight in
ldx D_8974 ; EED7 X = drone column for the radar-window test
stx cellColB ; EEDA redundant store: droneDistanceToCell ($EF9E) writes the same two bytes again before anything reads them
ldy D_8975 ; EEDC Y = drone row
testDroneOnPlayerRadar:
sty cellRowB ; EEDF redundant in the same way
jsr mapCellToRadarGrid ; EEE1 overlay B helper: X = col + 10 - comcenCol, Y = row + 8 - comcenRow, C = 1 when the cell falls outside the 21x17 radar grid. $9236 is deliberately left alone, so this is the HUMAN player's comcen window - unlike isUnitOnAiRadar ($EE6A of the trainer's own page), which forces $9236 to $63 (unit 99) first
bcs steerDroneAndEmitMove ; EEE4 off the player's radar - nothing to hide from
lda currentScreen ; EEE6 which game screen is on show
cmp #$01 ; EEE9 screen 1 = the comcen RADAR / MISSILE screen, the only place a missile strike can be aimed
requireRadarScreenShown:
bne steerDroneAndEmitMove ; EEEB the player is not watching - fly straight
; ----------------------------------------------------------------------
; midCheckFuelForJinkEEED - NOT an entry point in this image. $EEED is the 'lda $8973 / cmp #$0C'
; fuel test inside maybeJinkDroneHeading. The label is an artifact of the other images of this page.
; In: -
; Out: -
; Called from: nothing in this image.
; ----------------------------------------------------------------------
midCheckFuelForJinkEEED:
lda D_8973 ; EEED steps of fuel left
cmp #$0C ; EEF0 12 steps
bcc steerDroneAndEmitMove ; EEF2 nearly out of fuel - stop wasting steps and go straight in
lda lineEndCol ; EEF4 aim column chosen by the four-block scan
sta cellColA ; EEF6 point A of cellDistance = the aim cell
lda lineEndRow ; EEF8 aim row
sta cellRowA ; EEFA point A row
jsr droneDistanceToCell ; EEFC distance from the drone to its aim point
cmp #$03 ; EEFF 3 cells
requireRoomToWeave:
bcc steerDroneAndEmitMove ; EF01 almost there - do not risk missing the target for the sake of dodging
tax ; EF03 dead instruction: the distance is stashed in X but nothing reads it, and steerDroneAndEmitMove reloads X a few instructions later
jsr nextGameRandom ; EF04 the deterministic game RNG on $57-$59; it preserves X and Y
cmp #$56 ; EF07 $56 = 86, so 170 of the 256 possible bytes pass: weave about two exchanges in three
bcc steerDroneAndEmitMove ; EF09 no weave this exchange
and #$07 ; EF0B a random compass heading 0-7
sta scratch19 ; EF0D replace the sensible heading with it
; ----------------------------------------------------------------------
; steerDroneAndEmitMove - turns the drone at most one 45-degree step towards the desired heading,
; moves it one cell that way and reports the move as command $8D. Instruction for instruction this is
; overlay B's steerDrone ($731E-$7391), the routine that flies the human player's drone, with the AI's
; variables substituted ($896F/$8974/$8975 for droneHeading/droneCol/droneRow $920B/$92F6/$92F7), with
; the desired heading read from zp_19 instead of looked up from the joystick, and with the turn sound
; ($10) removed. That duplication is useful evidence: the heading encoding and the three lookup
; tables are shared between the player's drone and the AI's. Neither coordinate is clamped, so a
; drone that keeps missing can walk clean off the 40x40 map.
; In: zp_19 = desired heading 0-7 (clockwise from north), $896F = the drone's current heading,
; $8974/$8975 = drone cell
; Out: $896F turned by at most one step, $8974/$8975 advanced by one cell, $E020 = $8D, $E021 =
; (heading + 4) & 7, $E022/$E023 = pre-mirrored column/row, $E01E = 4
; Called from: nothing - it is the fall-through of maybeJinkDroneHeading and the target of all six 'do
; not weave' branches at $EED5, $EEE4, $EEEB, $EEF2, $EF01 and $EF09.
; Turn towards the desired heading the short way round. Headings are 0-7 clockwise from north, so the
; difference is read modulo 8: a positive difference of 1..3 turns clockwise (+1) and one of 4..7 is
; shorter anticlockwise (-1); a negative difference of -1..-4 turns anticlockwise and -5..-7
; clockwise.
; ----------------------------------------------------------------------
steerDroneAndEmitMove:
lda scratch19 ; EF0F desired heading
sec ; EF11 prepare the subtract
sbc D_896F ; EF12 difference from the drone's current heading
beq convertHeadingToStep ; EF15 already pointing the right way - no turn at all
bcc desiredHeadingBelowCurrent; EF17 branch when the desired heading is the lower number (difference came out negative)
ldx #$FF ; EF19 assume one step anticlockwise
cmp #$04 ; EF1B half a turn: a positive difference of 4 or more is shorter going the other way
bcs applyHeadingStep ; EF1D difference 4..7 - keep the -1 step
ldx #$01 ; EF1F difference 1..3: one step clockwise
bne applyHeadingStep ; EF21 always taken (X = 1, so Z is clear)
desiredHeadingBelowCurrent:
ldx #$01 ; EF23 negative difference: assume one step clockwise
pickShorterTurnBackwards:
cmp #$FC ; EF25 $FC = -4: differences of -1..-4 are shorter anticlockwise
bcc applyHeadingStep ; EF27 difference -5..-7 - keep the +1 step
ldx #$FF ; EF29 one step anticlockwise
applyHeadingStep:
txa ; EF2B A = the step, +1 or -1
addStepToHeading:
clc ; EF2C clear carry for the add
adc D_896F ; EF2D apply it to the current heading
and #$07 ; EF30 headings wrap inside 0-7
sta D_896F ; EF32 the drone's new heading
; ----------------------------------------------------------------------
; Turn the heading into a one-cell step. headingToDirectionTable ($7316) maps heading 0-7
; (N,NE,E,SE,S,SW,W,NW) to the game's direction code (0 N, 1 S, 2 E, 3 W, 4 NW, 5 NE, 6 SE, 7 SW).
; Codes 0-1 are pure row moves and 2-3 pure column moves, both indexing directionDeltaTable ($20F0 =
; -1,+1,+1,-1) directly; codes 4-7 index diagonalDirectionTable ($1FB2+4 = $30,$20,$21,$31), whose
; high
; nibble is the column index and low nibble the row index into that same delta table.
; ----------------------------------------------------------------------
convertHeadingToStep:
ldy D_896F ; EF35 the drone's heading
lookUpDirectionCode:
lda headingToDirectionTable,y; EF38 direction code 0-7 (also mapGenerator6F00:forestBlockCompareNe)
ldy #$00 ; EF3B row delta defaults to 'no move'
ldx #$00 ; EF3D column delta defaults to 'no move'
cmp #$04 ; EF3F codes 4-7 are the diagonals
bcs readDiagonalDeltaPair ; EF41 branch to the packed-pair path
cmp #$02 ; EF43 codes 2-3 are east/west
bcs readColDeltaEastWest ; EF45 branch to the column-only path
tay ; EF47 codes 0-1 index the delta table directly: entry 0 = -1 (north), entry 1 = +1 (south)
readRowDeltaNorthSouth:
lda directionDeltaTable,y ; EF48 the row delta
tay ; EF4B keep it in Y; the column delta stays 0
jmp advanceDroneCell ; EF4C join the common move code
readColDeltaEastWest:
tax ; EF4F codes 2-3: entry 2 = +1 (east), entry 3 = -1 (west)
lda directionDeltaTable,x ; EF50 the column delta
storeColDelta:
tax ; EF53 keep it in X; the row delta stays 0
jmp advanceDroneCell ; EF54 join the common move code
readDiagonalDeltaPair:
tay ; EF57 codes 4-7 index the packed diagonal table
loadPackedDiagonalDelta:
lda diagonalDirectionTable,y; EF58 $30 = NW, $20 = NE, $21 = SE, $31 = SW, as (columnIndex << 4) | rowIndex
pha ; EF5B save the packed byte for the second nibble
and #$0F ; EF5C low nibble = index of the row delta
tay ; EF5E index the delta table with it
lda directionDeltaTable,y ; EF5F row delta
tay ; EF62 Y = row delta
pla ; EF63 the packed byte back
lsr a ; EF64 shift the high nibble down...
lsr a ; EF65 ...four places...
lsr a ; EF66 ...to get the column...
lsr a ; EF67 ...delta index
tax ; EF68 index the delta table with it
lda directionDeltaTable,x ; EF69 column delta
tax ; EF6C X = column delta
; ----------------------------------------------------------------------
; Move the drone one cell and report it. Nothing here clamps the result to 0..39 - the
; negative-coordinate patch-up back at $EE95 is the only thing that copes with a drone that has left
; the map.
; ----------------------------------------------------------------------
advanceDroneCell:
txa ; EF6D column delta
clc ; EF6E clear carry for the add
adc D_8974 ; EF6F drone column
sta D_8974 ; EF72 the drone's new column
tax ; EF75 X = new column, ready for the mirror call
tya ; EF76 row delta
addRowDeltaToDroneRow:
clc ; EF77 clear carry for the add
adc D_8975 ; EF78 drone row
sta D_8975 ; EF7B the drone's new row
tay ; EF7E Y = new row
lda #$8D ; EF7F command $8D = drone move; the packet mergers execute it on the spot ($4F23) instead of queueing it
sta rxPacketBuffer ; EF81 the packet's command byte (also trainerAiE000:rxPacketBuffer)
lda D_896F ; EF84 the drone's heading
clc ; EF87 clear carry for the add
adc #$04 ; EF88 +4 headings = 180 degrees
and #$07 ; EF8A wrap into 0-7: the player sees the battlefield mirrored, so a drone flying south on the AI's map is flying north on his, and cmdDroneMove picks the sprite shape straight from this byte
sta rxPacketArg1 ; EF8C argument 1 = the heading (also trainerAiE000:rxPacketArg1)
jsr mirrorBlockCornerForOpponent; EF8F pre-mirror the new cell: X = 38-X, Y = 38-Y
stx rxPacketArg2 ; EF92 argument 2 = column (also trainerAiE000:rxPacketArg2)
sty rxPacketArg3 ; EF95 argument 3 = row (also trainerAiE000:rxPacketArg3)
lda #$04 ; EF98 command byte plus three arguments
storeRxPacketLength:
sta rxPacketLength ; EF9A the exchange's fabricated 'received' packet is complete (also trainerAiE000:rxPacketLength)
rts ; EF9D back to $E0A8
; ----------------------------------------------------------------------
; droneDistanceToCell - measures the distance from the cell in zp_22/zp_24 to the drone. It fills the
; drone's cell in as point B of cellDistance ($E82A) and tail-jumps into it, so the caller gets back
; the integer distance (the largest n with n*n <= dcol^2 + drow^2, saturating at 16) in A and X.
; Nothing along the way touches Y, which is what lets pickDroneAimPointAndDecide keep the target unit
; index there across the call.
; In: zp_22/zp_24 = the cell to measure from, drone cell $8974/$8975
; Out: A = X = distance 0-16, zp_27/zp_28 = the drone's cell, Y preserved
; Called from: $EE26 (range to the target unit) and $EEFC (range to the aim point) - the only two JSRs
; inside this page.
; ----------------------------------------------------------------------
droneDistanceToCell:
lda D_8974 ; EF9E drone column
sta cellColB ; EFA1 point B of cellDistance = the drone
lda D_8975 ; EFA3 drone row
sta cellRowB ; EFA6 point B row
jmp cellDistance ; EFA8 tail call: returns A = X = distance between (zp_22,zp_24) and the drone
; ----------------------------------------------------------------------
; staleCommTailBytes ($EFAB-$EFCF, 37 bytes) - dead in this image. Everything from here to $EFF4 is
; byte-for-byte identical in all four programs that are ever loaded at
; $EE00-$EFFF (the trainer's own tail from T35 s14-15, the modem driver from T34, the track-29 s0-3
; $EC00
; sub-overlay and this drone AI), because the map-generator overlay addresses the strings below by
; absolute address and they therefore may not move. These 37 bytes are simply what happened to
; precede them in the original source. In the trainer's own page the same bytes are the tail of
; pickDroneTargetAlongThreatAxis: they bump the ray radius and, on the way out, store the chosen unit
; in $8976 and scale the acceptance threshold $8979 down to 75%. In the modem driver they are the
; middle of the message 'INSERT DATA DISK AND PRESS SPACE.' starting at $EFAF. The line comments
; below describe that trainer-page meaning; nothing here runs in this image. Latent bug: overlay A's
; promptInsertDiskAndWaitSpace ($81B6) unconditionally installs $EFAF as message slot $1E and copies
; four characters into $EFB6-$EFB9. With this page (or the trainer's own, or the
; $EC00 sub-overlay) resident, $EFAF is the $C9 of 'cmp #$0F' - a complete one-character string 'I' -
; and
; the four-byte patch lands on top of code.
; ----------------------------------------------------------------------
staleCommTailBytes:
inc scratch1F ; EFAB trainer page: step the search ray outwards (zp_1F = radius)
lda scratch1F ; EFAD trainer page: the new radius
cmp #$0F ; EFAF trainer page: stop after radius 14. Modem page: this $C9 opcode byte is the 'I' that starts 'INSERT DATA DISK...', and is a whole terminated string on its own here
bcc storeRxPacketLength+1 ; EFB1 trainer page: keep walking the ray. Here the target, $EF9B, is the operand byte of the 'sta $E01E' at storeRxPacketLength - a branch into the middle of an instruction, which is the clearest proof that this block is not code in this image
staleTailStoreTargetUnit:
lda scratch1D ; EFB3 trainer page: the best player unit scoreTargetBlock found, $FF when none
bmi staleTailReturn ; EFB5 trainer page: nothing worth sending a drone at - leave $8976 alone
sta D_8976 ; EFB7 trainer page: that unit becomes the AI's next drone target
lda D_8979 ; EFBA trainer page: the target-value threshold
lsr a ; EFBD trainer page: /2
lsr a ; EFBE trainer page: /4
sta scratch18 ; EFBF trainer page: park threshold/4
lda D_8979 ; EFC1 trainer page: the threshold again
sec ; EFC4 trainer page: prepare the subtract
sbc scratch18 ; EFC5 trainer page: 3/4 of the threshold - the AI grows steadily less picky about drone targets
bcc staleTailReturn ; EFC7 trainer page: never let it go negative
sta D_8979 ; EFC9 trainer page: store the reduced threshold
staleTailReturn:
rts ; EFCC trainer page: return to chooseAiDroneTarget
; msgInsertDiskTail ($EFCD, 3 bytes) - 'CE' plus '.' with bit 7 set as the string terminator: the last
; three characters of the modem driver's message 'INSERT DATA DISK AND PRESS SPACE.'. Meaningless in
; this image, but it has to stay put so that the strings below keep their addresses.
msgInsertDiskTail:
.byte "CE",'.'|$80 ; EFCD end of '...PRESS SPACE.' in the modem-driver image of this page
; Shared, position-fixed strings ($EFD0-$EFF4). These belong to the map-generator/setup overlay
; (overlay A at $6F00-$87FF), not to the $E000 module at all: overlay A refers to them by hard-coded
; address, so every image of this page has to carry them at exactly these addresses. msgDataDiskName
; ($EFD0, 4 bytes, no terminator) - the literal 'DATA', which promptInsertDataDisk ($81B2) copies into
; the INSERT-DISK message before printing it.
msgDataDiskName:
.byte "DATA" ; EFD0 disk name patched into the INSERT-DISK prompt
; msgGameDiskName ($EFD4, 4 bytes, no terminator) - 'GAME', the other disk name, used by
; promptInsertGameDisk ($81A5).
msgGameDiskName:
.byte "GAME" ; EFD4 the other disk name
; msgCustomOff ($EFD8, 10 bytes) - 'CUSTOM OF' plus 'F' with bit 7 set (the string terminator).
; Overlay A installs it as message slot $1E for the CUSTOM rules switch and patches the word at $EFDF
; between 'OFF' and 'ON '.
msgCustomOff:
.byte "CUSTOM OF",'F'|$80 ; EFD8 message slot $1E for the CUSTOM switch; the ON/OFF word starts at $EFDF
; msgDestroyOff ($EFE2, 11 bytes) - 'DESTROY OF' plus the terminating 'F'. Message slot $1E for the
; DESTROY switch; the ON/OFF word overlay A patches sits at $EFEA.
msgDestroyOff:
.byte "DESTROY OF",'F'|$80 ; EFE2 message slot $1E for the DESTROY switch; the ON/OFF word starts at $EFEA
; msgAccepted ($EFED, 8 bytes) - 'ACCEPTE' plus the terminating 'D'. Overlay A prints it centred on
; row 15 when the fire button confirms a value in the custom-game editor.
msgAccepted:
.byte "ACCEPTE",'D'|$80 ; EFED printed centred on row 15 when a custom-game value is confirmed
; trailingPadEFF5 ($EFF5-$EFFF, 11 bytes) - $C0,$DF,$00,$FF,$00,$FF,$00,$FF,$00,$FF,$FF. Unreferenced
; by anything, and identical in every image of this page except the very last byte ($FF here and in
; the trainer's own tail, $00 in the modem driver). Almost certainly padding left by the mastering
; system rather than data the game ever reads.
trailingPadEFF5:
.byte $C0 ; EFF5 unreferenced trailing pad to the end of the page
.byte $DF,$00,$FF,$00,$FF,$00,$FF; EFF6 .......
.byte $00 ; EFFD last three pad bytes; the final $FF is $00 in the modem-driver image of this page
.byte $FF,$FF ; EFFE ..