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

629 lines
46 KiB
ArmAsm

; ============================================================================
; $FBB8-$FFD1 - resident high memory: line walker, random numbers, arithmetic helpers
; ============================================================================
; Track 29 sectors 11-15, loaded at $FA00 and then moved up by $1B8 so that it ends just below the CPU
; vectors. $FFDE-$FFE7 holds the 10-byte disk serial the two sides compare.
.setcpu "6502"
.include "c64.inc"
.include "zeropage.inc"
; ---- references to code/data outside this file ----
lastStepRowDelta := $90F4
lastStepColDelta := $90F5
bitmapRowLoTable := $9300
bitmapRowHiTable := $9319
bitmapColLoTable := $9332
bitmapColHiTable := $935A
; Contents
; --------
; $FBB8 initLineStepper Sets up a DDA/Bresenham walk over the 40x40 battlefield map from the cell
; ($93,$92) = (column,row) to the cell ($95,$94).
; $FBFA stepLine Walks the line set up by initLineStepper one cell further: adds |dRow| to the
; row accumulator $8C and |dCol| to the column accumulator $8D, and each time an accumulator reaches the major length
; $8E it wraps it, moves $92 (row) / $93 (column) by that axis's step sign and reports the applied delta in $90F4 /
; $90F5.
; $FC58 plotFatPixel Plots one 'fat' pixel of colour A (0-3) at fat-pixel column X (0-159) and row Y
; (0-95) into the $A000 multicolour bitmap: a fat pixel is one multicolour pixel wide and two raster lines tall, so it
; draws bitmap rows 2Y and 2Y+1 through plotBitmapPixelRow.
; $FC74 plotBitmapPixelRow Second half of plotFatPixel: writes one multicolour pixel pair into the bitmap.
; $FCCD multiply8x8 Unsigned 8 x 8 -> 16 bit multiply, shift-and-add, 8 rounds: the multiplier is
; shifted out of $19 one bit at a time while the 16-bit product is shifted right through A (high half) and $1A (low
; half).
; $FCEC divideSigned16by8 Divides the signed 16-bit value Y:A (Y = high byte, A = low byte) by the
; unsigned 8-bit divisor X.
; $FD0A divideUnsigned16by8 Unsigned restoring division, 8 rounds: the dividend low byte in $19 is shifted
; left into the running remainder in A, the divisor $18 is subtracted whenever it fits, and the quotient bits are
; collected in the low end of $19.
; $FD1E addRandomJitter Scatters a map coordinate: returns the base value A plus or minus 2*k, where k
; is the number of consecutive 1 bits at the bottom of a fresh scenario-RNG byte (a geometric distribution, so small
; offsets are the common case) capped at the limit in Y, and the sign is bit 7 of the RNG's middle byte $5B.
; $FD4A nextScenarioRandom Pseudo random generator of the scenario/map seed: a 24-bit shift register on
; $5A/$5B/$5C fed by a non-linear function of $5A ((seed0 >> 2) minus seed0, with the carry the shifts left), returning
; $5A EOR $5B.
; $FD5D nextGameRandom The lock-step game random generator: identical code to nextScenarioRandom but
; on the state $57/$58/$59, which the start-up block seeds with the fixed constants $49,$19,$02 at $0309-$0313, so both
; machines of a modem game walk the same sequence and stay in step.
.org $FBB8
; ----------------------------------------------------------------------
; initLineStepper - Sets up a DDA/Bresenham walk over the 40x40 battlefield map from the cell
; ($93,$92) = (column,row) to the cell ($95,$94). Computes |dRow| -> $8F and |dCol| -> $90, the
; per-step signs $96/$97 (+1 or -1), the major-axis length -> $8E (wrap threshold) and $91 (steps
; still to walk), clears both error accumulators and preloads the MINOR one with majorLen/2 so the
; stair steps fall in the middle of a cell. The walk itself is done one cell at a time by stepLine.
; In: $92 = start row, $93 = start column, $94 = target row, $95 = target column
; Out: A = $91 = number of steps = max(|dRow|,|dCol|), Z set when start and target are the same cell;
; $8E = major length; $8F/$90 = |dRow|/|dCol|; $96/$97 = row/column step; $8C/$8D = 0 and
; majorLen/2
; Called from: the movement/line-of-sight code of the main program ($1D57, $1D83, $2512, $306F, $3771,
; $3A82, $42EC, $4559), the $6F00 overlay ($775D, $77AC, $784F and $83A4 = the shot/route line in
; the zoom window) and the opponent module ($E80E getStepDirection, $E96F)
; ----------------------------------------------------------------------
initLineStepper:
ldx #$01 ; FBB8 assume the row steps downwards (+1)
lda lineEndRow ; FBBA target row of the line
sec ; FBBC no borrow into the subtraction
sbc lineCurRow ; FBBD dRow = target row - current row
bcs L_FBC7 ; FBBF branch if the target is at or below the start (dRow >= 0)
dex ; FBC1 target is above the start, so ...
dex ; FBC2 ... X = $FF: the row steps upwards (-1)
eor #$FF ; FBC3 negate dRow to get its magnitude ...
adc #$01 ; FBC5 ... (C is clear after the borrow, so EOR $FF + 1 is a two's complement negate)
L_FBC7:
sta lineDeltaRow ; FBC7 $8F = |dRow|, the length of the row axis
stx lineStepRow ; FBC9 $96 = row increment applied per major step (+1 or -1)
ldx #$01 ; FBCB assume the column steps rightwards (+1)
lda lineEndCol ; FBCD target column of the line
sec ; FBCF no borrow into the subtraction
sbc lineCurCol ; FBD0 dCol = target column - current column
bcs L_FBDA ; FBD2 branch if the target is at or right of the start (dCol >= 0)
dex ; FBD4 target is left of the start, so ...
dex ; FBD5 ... X = $FF: the column steps leftwards (-1)
eor #$FF ; FBD6 negate dCol ...
adc #$01 ; FBD8 ... to get its magnitude
L_FBDA:
sta lineDeltaCol ; FBDA $90 = |dCol|, the length of the column axis
stx lineStepCol ; FBDC $97 = column increment applied per major step (+1 or -1)
ldx #$00 ; FBDE start both Bresenham error accumulators at zero
stx lineErrRow ; FBE0 $8C = row accumulator
stx lineErrCol ; FBE2 $8D = column accumulator; X = 0 also means 'the column is the major axis'
lda lineDeltaCol ; FBE4 |dCol| ...
cmp lineDeltaRow ; FBE6 ... against |dRow|: the longer axis is the major one
beq L_FBEC ; FBE8 on a tie call the row the major axis (perfect diagonal)
bcs L_FBF0 ; FBEA branch if |dCol| > |dRow|: the column is major and A already holds the major length
L_FBEC:
ldx #$01 ; FBEC the row is major, so the accumulator to preload is $8C+1 = $8D (the column one)
lda lineDeltaRow ; FBEE major length = |dRow|
L_FBF0:
sta lineStepsLeft ; FBF0 $91 = steps still to walk = the major-axis length
sta lineMajorLen ; FBF2 $8E = major length; stepLine wraps an accumulator whenever it reaches this
lsr a ; FBF4 half the major length ...
sta lineErrRow,x ; FBF5 ... preloaded into the MINOR accumulator ($8C when the column is major, $8D when the row is) to centre the stair steps
lda lineStepsLeft ; FBF7 return the number of steps
rts ; FBF9 Z is set when the two cells are identical (nothing to walk)
; ----------------------------------------------------------------------
; stepLine - Walks the line set up by initLineStepper one cell further: adds |dRow| to the row
; accumulator $8C and |dCol| to the column accumulator $8D, and each time an accumulator reaches the
; major length $8E it wraps it, moves $92 (row) / $93 (column) by that axis's step sign and reports
; the applied delta in $90F4 / $90F5. The major axis always fires, so $A9 ends at 0 for an orthogonal
; step and 1 for a diagonal one; with no steps left nothing moves, both deltas and $A9 are 0.
; In: the line state left by initLineStepper: $8C-$8E, $8F, $90, $91, $92, $93, $96, $97
; Out: A = $91 = steps still to walk (Z = line finished); $92/$93 advanced by one cell; $90F4 = row
; delta applied (-1/0/+1), $90F5 = column delta applied; $A9 = 1 for a diagonal step, else 0
; Called from: the main program ($1D86, $2517, $3079, $3080, $3083, $36A5, $3AA7, $42EF, $4562), the
; $6F00 overlay ($7760, $7763, $7766, $77AF, $77B2, $7834, $785B, $83F0, $83FD, $8403) and the
; opponent module ($E811, $E972); callers loop until the returned count is zero
; ----------------------------------------------------------------------
stepLine:
lda #$00 ; FBFA nothing has moved yet in this step
sta lastStepColDelta ; FBFC $90F5 = column delta applied by this step (read at $1D89 and $E819)
sta lastStepRowDelta ; FBFF $90F4 = row delta applied by this step (read at $1D8C and $E821)
lda lineStepsLeft ; FC02 steps still to walk
sta isDiagonalStep ; FC04 park the count in $A9 for a moment; it stays 0 if the line is finished
beq L_FC4A ; FC06 line already at its target: report no movement
lda #$FF ; FC08 $A9 = -1; every axis that fires below increments it ...
sta isDiagonalStep ; FC0A ... so it ends at 0 for an orthogonal and 1 for a diagonal step
lda lineErrRow ; FC0C row error accumulator
clc ; FC0E clear the carry before the add
adc lineDeltaRow ; FC0F advance it by the row-axis length |dRow|
sta lineErrRow ; FC11 keep the updated row error
cmp lineMajorLen ; FC13 has it reached the major-axis length?
bcc L_FC2A ; FC15 no: the row does not move this step
inc isDiagonalStep ; FC17 count the row axis as having fired
lda lineStepRow ; FC19 row step (+1 down / -1 up)
sta lastStepRowDelta ; FC1B report it to the caller in $90F4
clc ; FC1E clear the carry before the add
adc lineCurRow ; FC1F current map row ...
sta lineCurRow ; FC21 ... moves one cell along the line
lda lineErrRow ; FC23 wrap the row accumulator ...
sec ; FC25 no borrow into the subtraction
sbc lineMajorLen ; FC26 ... by subtracting the major length
sta lineErrRow ; FC28 store the wrapped row accumulator
L_FC2A:
lda lineErrCol ; FC2A column error accumulator
clc ; FC2C clear the carry before the add
adc lineDeltaCol ; FC2D advance it by the column-axis length |dCol|
sta lineErrCol ; FC2F keep the updated column error
cmp lineMajorLen ; FC31 has it reached the major-axis length?
bcc L_FC48 ; FC33 no: the column does not move this step
inc isDiagonalStep ; FC35 count the column axis too: $A9 becomes 1 = diagonal step
lda lineStepCol ; FC37 column step (+1 right / -1 left)
sta lastStepColDelta ; FC39 report it to the caller in $90F5
clc ; FC3C clear the carry before the add
adc lineCurCol ; FC3D current map column ...
sta lineCurCol ; FC3F ... moves one cell along the line
lda lineErrCol ; FC41 wrap the column accumulator ...
sec ; FC43 no borrow into the subtraction
sbc lineMajorLen ; FC44 ... by subtracting the major length
sta lineErrCol ; FC46 store the wrapped column accumulator
L_FC48:
dec lineStepsLeft ; FC48 one step of the line used up
L_FC4A:
lda lineStepsLeft ; FC4A return the steps still to walk
rts ; FC4C Z is set once the line has reached its target cell
; Three scratch bytes of the multicolour plotter below. They live in code space because the plotter is
; called from the $6F00 overlay and must not depend on any overlay variable.
plotColorIndex:
.byte $00 ; FC4D plotColorIndex: colour 0-3 handed from plotFatPixel to plotBitmapPixelRow
plotColorBits:
.byte $00 ; FC4E plotColorBits: the colour pattern already masked down to the pixel pair being written
plotKeepMask:
.byte $00 ; FC4F plotKeepMask: first the mask of that pixel pair, then (inverted) the bits of the byte to keep
; multicolorFillPatterns - 4 bytes, indexed by plotColorIndex: colour index 0-3 replicated into all
; four multicolour pixel pairs of a bitmap byte ($00 = %00000000, $55 = %01010101, $AA, $FF).
multicolorFillPatterns:
.byte $00,$55,$AA,$FF ; FC50 .U.. colour 0/1/2/3 in every pixel pair
; multicolorPixelMasks - 4 bytes, indexed by (pixel column AND 3): which pixel pair of the byte is
; addressed, leftmost first ($C0 = bits 7-6, $30 = 5-4, $0C = 3-2, $03 = 1-0).
multicolorPixelMasks:
.byte $C0,$30,$0C,$03 ; FC54 .0.. bit mask of pixel pair 0/1/2/3 inside a bitmap byte
; ----------------------------------------------------------------------
; plotFatPixel - Plots one 'fat' pixel of colour A (0-3) at fat-pixel column X (0-159) and row Y
; (0-95) into the $A000 multicolour bitmap: a fat pixel is one multicolour pixel wide and two raster
; lines tall, so it draws bitmap rows 2Y and 2Y+1 through plotBitmapPixelRow. X and Y are stashed in
; the LDX/LDY operands at the end of plotBitmapPixelRow and come back unchanged.
; In: A = colour index 0-3, X = fat-pixel column 0-159, Y = fat-pixel row 0-95
; Out: one or two bitmap bytes updated; A/X/Y as described (X and Y preserved); $FC4D-$FC4F and
; $B4/$B5 clobbered
; Called from: the $6F00 overlay only - $83E6 draws the shot/route line in the zoom window and
; $84F5/$84FC/$8505/$850C draw the four pixels of plotDiamondMarker
; Bug: the range check rejects a point only when the column AND the row are out of range (both tests
; branch INTO the plot path); it should be BCS to the RTS. Harmless in practice because the overlay
; clips the coordinates itself at $83AC-$83CA.
; ----------------------------------------------------------------------
plotFatPixel:
cpx #$A0 ; FC58 fat-pixel column past the right edge? (the bitmap is 160 multicolour pixels wide)
bcc L_FC61 ; FC5A column in range: plot - note this also skips the row test
cpy #$60 ; FC5C fat-pixel row past the bottom? (192 raster lines / 2 lines per fat pixel)
bcc L_FC61 ; FC5E row in range: plot, even though the column was already rejected
rts ; FC60 only reached when both coordinates are out of range
L_FC61:
stx L_FCC8+1 ; FC61 stash the caller's column in the LDX # operand at L_FCC8+1 (restored on the way out)
sty L_FCCA+1 ; FC64 stash the caller's row in the LDY # operand at L_FCCA+1
sta plotColorIndex ; FC67 hand the colour index to plotBitmapPixelRow
tya ; FC6A fat-pixel row ...
asl a ; FC6B ... x2 = the upper of the two bitmap rows it covers
pha ; FC6C keep it for the second half
tay ; FC6D Y = bitmap row 2Y
jsr plotBitmapPixelRow ; FC6E draw the upper raster line
pla ; FC71 recover the upper bitmap row ...
tay ; FC72 Y = the upper bitmap row again
iny ; FC73 ... and step to the lower one (2Y+1), then fall straight into the plotter again
; ----------------------------------------------------------------------
; plotBitmapPixelRow - Second half of plotFatPixel: writes one multicolour pixel pair into the bitmap.
; For bitmap row Y (0-191) and pixel column X (0-159) it builds the byte address $A000 + 320*(Y/8) +
; 8*(X/4) + (Y AND 7) from the four tables in the $9300 data block, then merges plotColorIndex's two
; bits into the pixel pair (X AND 3) of that byte. Entered by JSR for the upper raster line and by
; falling through for the lower one.
; In: X = pixel column 0-159, Y = bitmap row 0-191, $FC4D = colour index 0-3
; Out: one bitmap byte updated; $B4/$B5 = its address; $FC4E = colour bits, $FC4F = keep mask; A
; clobbered, X and Y reloaded with the values plotFatPixel stashed at $FC61/$FC64
; Called from: plotFatPixel ($FC6E and the fall-through at $FC73) only
; ----------------------------------------------------------------------
plotBitmapPixelRow:
tya ; FC74 bitmap row
and #$07 ; FC75 raster line inside its 8-line character row
sta L_FC83+1 ; FC77 patch it into the ADC # operand at L_FC83+1
tya ; FC7A bitmap row again
lsr a ; FC7B divide by 8 ...
lsr a ; FC7C ...
lsr a ; FC7D ... = character row 0-24
tay ; FC7E index the row tables with it
clc ; FC7F clear the carry before the address add
lda bitmapRowLoTable,y ; FC80 low byte of $A000 + 320*charRow (25-entry table in the $9300 block)
L_FC83:
adc #$FF ; FC83 + the raster line within the row (operand written at $FC77)
sta bitmapPtr ; FC85 $B4 = low byte of the target address
lda bitmapRowHiTable,y ; FC87 matching high byte ($A0..$BE)
adc #$00 ; FC8A carry out of the low half
sta bitmapPtrHi ; FC8C $B5 = high byte of the target address
txa ; FC8E pixel column
and #$03 ; FC8F which of the four pixel pairs of the byte (0 = leftmost)
tay ; FC91 index the pixel-pair mask table with it
lda multicolorPixelMasks,y ; FC92 $C0/$30/$0C/$03 = the two bits of that pair
sta plotKeepMask ; FC95 park the pair mask
ldy plotColorIndex ; FC98 colour index stored by plotFatPixel
and multicolorFillPatterns,y; FC9B $00/$55/$AA/$FF; the AND leaves the colour only in this pixel pair
sta plotColorBits ; FC9E the bits to OR into the byte
lda plotKeepMask ; FCA1 the pair mask again ...
eor #$FF ; FCA4 ... inverted ...
sta plotKeepMask ; FCA6 ... = every bit of the byte that must survive
txa ; FCA9 pixel column once more
lsr a ; FCAA divide by 4 (4 multicolour pixels per byte) ...
lsr a ; FCAB ... = character column 0-39
tay ; FCAC index the column tables with it
clc ; FCAD clear the carry before the 16-bit add
lda bitmapColLoTable,y ; FCAE low byte of 8*column (40-entry table at $9332)
adc bitmapPtr ; FCB1 add it to the row address ...
sta bitmapPtr ; FCB3 low byte of the bitmap byte address
lda bitmapColHiTable,y ; FCB5 high byte of 8*column (0 or 1 - column 32 onwards crosses $100)
adc bitmapPtrHi ; FCB8 carry on into the high byte
sta bitmapPtrHi ; FCBA $B4/$B5 now point at the exact bitmap byte
ldy #$00 ; FCBC read/modify/write through the pointer with no offset
lda (bitmapPtr),y ; FCBE read the byte
and plotKeepMask ; FCC0 clear the two bits of the addressed pixel pair
ora plotColorBits ; FCC3 drop the new colour into them
sta (bitmapPtr),y ; FCC6 write the byte back to the bitmap
L_FCC8:
ldx #$FF ; FCC8 restore the caller's fat-pixel column (operand written at $FC61)
L_FCCA:
ldy #$FF ; FCCA restore the caller's fat-pixel row (operand written at $FC64)
rts ; FCCC back to plotFatPixel, or to its caller after the second raster line
; ----------------------------------------------------------------------
; multiply8x8 - Unsigned 8 x 8 -> 16 bit multiply, shift-and-add, 8 rounds: the multiplier is shifted
; out of $19 one bit at a time while the 16-bit product is shifted right through A (high half) and $1A
; (low half).
; In: A = multiplicand, Y = multiplier
; Out: A = $18 = product low byte, Y = $19 = product high byte, $1A clobbered, X = 0
; Called from: $55BD (kill value x 10 added to the side score), the $6F00 overlay ($814A film page x
; 10, $846B value x 255 before printDecimal) and the trainer's $EC00 tail ($ECD4)
; Note: the survey header had the outputs the wrong way round - A holds the LOW byte on return, which
; is what $55C3 and printDecimal (A = low, Y = middle) expect.
; ----------------------------------------------------------------------
multiply8x8:
sta scratch18 ; FCCD $18 = multiplicand, added in on every set multiplier bit
sty scratch19 ; FCCF $19 = multiplier, consumed bit by bit
lda #$00 ; FCD1 A = high half of the running product
sta scratch1A ; FCD3 $1A = low half of the running product
ldx #$08 ; FCD5 8 multiplier bits to go
L_FCD7:
lsr scratch19 ; FCD7 shift the next multiplier bit into carry
bcc L_FCDE ; FCD9 bit clear: nothing to add this round
clc ; FCDB clear the carry before the add
adc scratch18 ; FCDC bit set: add the multiplicand into the high half
L_FCDE:
ror a ; FCDE shift the 16-bit product right one place ...
ror scratch1A ; FCDF ... carrying the bit that falls out into the low half
dex ; FCE1 one round of the multiply done
bne L_FCD7 ; FCE2 next multiplier bit
tay ; FCE4 Y = product high byte
sta scratch19 ; FCE5 $19 = product high byte
lda scratch1A ; FCE7 product low byte ...
sta scratch18 ; FCE9 ... into $18 as well
rts ; FCEB returns A = low byte, Y = high byte, X = 0
; ----------------------------------------------------------------------
; divideSigned16by8 - Divides the signed 16-bit value Y:A (Y = high byte, A = low byte) by the
; unsigned 8-bit divisor X. A negative dividend is negated first and the quotient negated again
; afterwards, so the remainder left in Y is always that of the absolute value.
; In: X = divisor, A = dividend low byte, Y = dividend high byte (its bit 7 is the sign)
; Out: A = quotient (low 8 bits, sign restored), Y = remainder of |dividend|, $18 = divisor, $19 =
; |quotient|, X = 0
; Called from: $41E4 (game clock modulo 10 or 50 - the caller tests the remainder with CPY #$00),
; $49EF/$49F9 (average of a group's column and row), the trainer ($E8EA, $E8F6, $E902, $E90D) and
; the drone-flight AI ($EE86, $EE96, $EEA6, $EEB6)
; ----------------------------------------------------------------------
divideSigned16by8:
stx scratch18 ; FCEC $18 = divisor
sta scratch19 ; FCEE $19 = dividend low byte
tya ; FCF0 dividend high byte carries the sign
bpl divideUnsigned16by8 ; FCF1 non-negative: divide it as it stands (A is already the high byte)
lda scratch19 ; FCF3 negative dividend: take its low byte ...
eor #$FF ; FCF5 one's complement of the low byte ...
clc ; FCF7 clear the carry for the +1
adc #$01 ; FCF8 ... plus one = two's complement
sta scratch19 ; FCFA store the negated low byte
tya ; FCFC then the high byte ...
eor #$FF ; FCFD one's complement of the high byte ...
adc #$00 ; FCFF ... taking the carry out of the low half (16-bit two's complement)
jsr divideUnsigned16by8 ; FD01 divide the magnitude
eor #$FF ; FD04 negate the quotient again ...
clc ; FD06 clear the carry for the +1
adc #$01 ; FD07 ... so the sign of the result matches the dividend
rts ; FD09 Y still holds the remainder of the absolute value
; ----------------------------------------------------------------------
; divideUnsigned16by8 - Unsigned restoring division, 8 rounds: the dividend low byte in $19 is shifted
; left into the running remainder in A, the divisor $18 is subtracted whenever it fits, and the
; quotient bits are collected in the low end of $19. Entered from divideSigned16by8, never called
; directly.
; In: A = dividend high byte (the running remainder to start with), $19 = dividend low byte, $18 =
; divisor
; Out: A = $19 = quotient low byte, Y = remainder, X = 0
; Called from: divideSigned16by8 ($FCF1 for a non-negative dividend, $FD01 for a negated one)
; Note: only 8 quotient bits are produced, so a quotient above 255 silently wraps, and there is no
; divide-by-zero guard (X = 0 gives quotient $FF).
; ----------------------------------------------------------------------
divideUnsigned16by8:
ldx #$08 ; FD0A 8 quotient bits
L_FD0C:
asl scratch19 ; FD0C shift the dividend left, its top bit into carry ...
rol a ; FD0E ... and on into the running remainder
cmp scratch18 ; FD0F does the divisor fit into the remainder?
bcc L_FD17 ; FD11 no: this quotient bit stays 0
sbc scratch18 ; FD13 yes: subtract it (C is set, so no borrow)
inc scratch19 ; FD15 and set the quotient bit that was just shifted in at bit 0
L_FD17:
dex ; FD17 one quotient bit done
bne L_FD0C ; FD18 next quotient bit
D_FD1A:
tay ; FD1A Y = remainder (the label D_FD1A is an artifact of the boot loader's block move at $C200, not a data item)
lda scratch19 ; FD1B A = quotient
rts ; FD1D back to divideSigned16by8 or straight to its caller
; ----------------------------------------------------------------------
; addRandomJitter - Scatters a map coordinate: returns the base value A plus or minus 2*k, where k is
; the number of consecutive 1 bits at the bottom of a fresh scenario-RNG byte (a geometric
; distribution, so small offsets are the common case) capped at the limit in Y, and the sign is bit 7
; of the RNG's middle byte $5B. Y = 0 returns the base unchanged; a result that went negative is
; clamped to 0.
; In: A = base value, Y = maximum number of bits to count (0 = no jitter); scenario RNG $5A-$5C
; Out: A = jittered value 0..127; $18 (bit count) and $19 (limit) clobbered; the RNG is advanced once
; Called from: the map generator only - $7036 (hill site column), $738B and $7398 (forest cluster
; row/column); each caller retries while the result is >= 40, i.e. off the 40x40 map
; ----------------------------------------------------------------------
addRandomJitter:
sta L_FD43+1 ; FD1E patch the base value into the ADC # operand at L_FD43+1
sty scratch19 ; FD21 $19 = how many 1 bits may still be counted
tya ; FD23 the limit once more
beq L_FD42 ; FD24 a limit of 0 means 'no jitter at all'
lda #$00 ; FD26 start the count of 1 bits at zero
sta scratch18 ; FD28 $18 = number of 1 bits counted so far
jsr nextScenarioRandom ; FD2A one byte from the scenario/map RNG
L_FD2D:
lsr a ; FD2D shift the next random bit out
bcc L_FD36 ; FD2E stop at the first 0 bit - hence the geometric distribution
inc scratch18 ; FD30 count this 1 bit
dec scratch19 ; FD32 and use up one of the allowed counts
bne L_FD2D ; FD34 keep counting until a 0 bit or the limit
L_FD36:
lda scratch18 ; FD36 number of consecutive 1 bits
asl a ; FD38 offset = 2 * that count (so 0, 2, 4, ... cells)
bit scenarioSeed1 ; FD39 bit 7 of $5B, just stirred by the RNG, picks the direction
bpl L_FD42 ; FD3B clear: add the offset
eor #$FF ; FD3D set: negate the offset ...
clc ; FD3F clear the carry for the +1 of the negate
adc #$01 ; FD40 ... so it is subtracted instead
L_FD42:
clc ; FD42 drop the carry the negate may have left
L_FD43:
adc #$FF ; FD43 add the base value (operand written at $FD1E)
bpl L_FD49 ; FD45 keep it while the result is still positive
lda #$00 ; FD47 a coordinate that fell below 0 is clamped to the map edge
L_FD49:
rts ; FD49 the callers reject anything >= 40 and try again
; ----------------------------------------------------------------------
; nextScenarioRandom - Pseudo random generator of the scenario/map seed: a 24-bit shift register on
; $5A/$5B/$5C fed by a non-linear function of $5A ((seed0 >> 2) minus seed0, with the carry the shifts
; left), returning $5A EOR $5B. This seed IS the map: it is zeroed at $7731, taken from the game RNG
; at $77E7 or decoded from the 5-character MAP ID the players type (5 bits per character through the
; alphabet at $890C, $77F7-$7821), and only the battlefield/unit generator consumes it. Byte-for-byte
; the same code as nextGameRandom, on different state.
; In: the 24-bit state $5A-$5C
; Out: A = random byte ($5A EOR $5B), state advanced, C = 0
; Called from: addRandomJitter ($FD2A), randomUnitIndex ($4805) and the map generator throughout
; ($6FA9-$7889)
; ----------------------------------------------------------------------
nextScenarioRandom:
lda scenarioSeed0 ; FD4A byte 0 of the scenario seed
lsr a ; FD4C shift it right ...
lsr a ; FD4D ... twice
sbc scenarioSeed0 ; FD4E minus the original byte, using whatever carry the shifts left (the non-linear part)
lsr a ; FD50 the bit that will be fed back is now in carry
rol scenarioSeed2 ; FD51 roll it into byte 2, whose top bit goes to carry
rol scenarioSeed1 ; FD53 roll that into byte 1, whose top bit goes to carry
ror scenarioSeed0 ; FD55 and that one into the top of byte 0 - the 24-bit register has advanced
clc ; FD57 callers rely on C = 0 on return
lda scenarioSeed0 ; FD58 result = byte 0 ...
eor scenarioSeed1 ; FD5A ... EOR byte 1
rts ; FD5C return the random byte with C = 0
; ----------------------------------------------------------------------
; nextGameRandom - The lock-step game random generator: identical code to nextScenarioRandom but on
; the state $57/$58/$59, which the start-up block seeds with the fixed constants $49,$19,$02 at
; $0309-$0313, so both machines of a modem game walk the same sequence and stay in step.
; In: the 24-bit state $57-$59
; Out: A = random byte ($57 EOR $58), state advanced, C = 0
; Called from: everywhere - the main program ($0CCD, $1316, $180B, $1947, $1BDF, $29C1, $3D75, $5533,
; $5FF3), both $6F00 overlays, the sound driver ($C862), the trainer AI and the drone AI
; Note: docs/overview.md calls $FD5D-$FD6F 'the IRQ handler region'; that is wrong - the IRQ vector
; points at $1298 and later $10F1, and this is just the RNG.
; ----------------------------------------------------------------------
nextGameRandom:
lda gameRngState0 ; FD5D byte 0 of the game RNG state (seeded $49)
lsr a ; FD5F shift it right ...
lsr a ; FD60 ... twice
sbc gameRngState0 ; FD61 minus the original byte, carry included
lsr a ; FD63 the feedback bit lands in carry
rol gameRngState2 ; FD64 roll it into byte 2 (seeded $02)
rol gameRngState1 ; FD66 roll on into byte 1 (seeded $19)
ror gameRngState0 ; FD68 and into the top of byte 0
clc ; FD6A callers rely on C = 0 on return
lda gameRngState0 ; FD6B result = byte 0 ...
eor gameRngState1 ; FD6D ... EOR byte 1
rts ; FD6F return the random byte with C = 0
; radarDroneShapesRle - 180 bytes, $FD70-$FE23: an RLE-compressed set of eight 24x21 sprite shapes
; whose first byte ($01) is the escape marker. runMissileScreen ($7587, comcen screens overlay)
; points A/Y at it with LDA #$70 / LDY #$FD ($7592-$7594) and calls unpackRleTo0200 ($67CB) at $7596;
; the unpacker always writes exactly 512 bytes to $0200-$03FF, so the block becomes eight shapes on a
; 64-byte stride (63 sprite bytes plus one spare byte each). loadDirectionShapeSprite ($3340) copies
; $0200 + 64*shape into sprite 0 ($8A00) or sprite 5 ($8B40); on this screen the caller is
; cmdDroneMove at $5322, which passes (heading + 4) AND 7 - the heading turned 180 degrees, because a
; received command's coordinates are mirrored - to draw the opponent's drone blip. Decoded, the eight
; shapes are a small top-down aircraft glyph in the middle byte of sprite rows 7-13 (north =
; $08,$1C,$7F,$08,$08,$08,$1C), in compass order N, NE, E, SE, S, SW, W, NW. Correction: the first
; survey pass labelled this arrowGlyphTable, read the escape/count/value triples as '2-byte record
; headers' and stated that 'no static reference to this table exists anywhere in the disassembly ...
; it may be unused leftover data'. That is wrong: the block is live data, decompressed every time the
; missile/radar screen is entered. No reference appears in XREF.txt only because the address reaches
; the unpacker as two immediate operands in A/Y, never as an absolute operand.
radarDroneShapesRle:
.byte $01,$01,$16,$00,$08,$00,$00,$1C; FD70 ........ escape marker $01, then the compressed shape stream
.byte $00,$00,$7F,$00,$00,$08,$00,$00; FD78 ........
.byte $08,$00,$00,$08,$00,$00,$1C,$01; FD80 ........
.byte $2D,$00,$11,$00,$00,$0E,$00,$00; FD88 -.......
.byte $06,$00,$00,$0A,$00,$00,$51,$00; FD90 ......Q.
.byte $00,$20,$00,$00,$10,$01,$2D,$00; FD98 . ....-.
.byte $04,$00,$00,$04,$00,$00,$46,$00; FDA0 ......F.
.byte $00,$7F,$00,$00,$46,$00,$00,$04; FDA8 ....F...
.byte $00,$00,$04,$01,$2D,$00,$10,$00; FDB0 ....-...
.byte $00,$20,$00,$00,$51,$00,$00,$0A; FDB8 . ..Q...
.byte $00,$00,$06,$00,$00,$0E,$00,$00; FDC0 ........
.byte $11,$01,$2D,$00,$1C,$00,$00,$08; FDC8 ..-.....
.byte $00,$00,$08,$00,$00,$08,$00,$00; FDD0 ........
.byte $7F,$00,$00,$1C,$00,$00,$08,$01; FDD8 ........
.byte $2D,$00,$04,$00,$00,$02,$00,$00; FDE0 -.......
.byte $45,$00,$00,$28,$00,$00,$30,$00; FDE8 E..(..0.
.byte $00,$38,$00,$00,$44,$01,$2D,$00; FDF0 .8..D.-.
.byte $10,$00,$00,$10,$00,$00,$31,$00; FDF8 ......1.
.byte $00,$7F,$00,$00,$31,$00,$00,$10; FE00 ....1...
.byte $00,$00,$10,$01,$2D,$00,$44,$00; FE08 ....-.D.
.byte $00,$38,$00,$00,$30,$00,$00,$28; FE10 .8..0..(
.byte $00,$00,$45,$00,$00,$02,$00,$00; FE18 ..E.....
.byte $04 ; FE20 .
; Last token of the stream above: marker $01, count $17, value $00 = the 23 zero bytes that pad out
; the eighth shape. It only carries a label because the survey mistook it for a table start.
radarDroneShapesRleEnd:
.byte $01,$17,$00 ; FE21 ... $01,$17,$00 = run of 23 zeros ending the radar drone shapes
; droneScreenShapesRle - 367 bytes, $FE24-$FF92: the second RLE stream, escape marker $05.
; enterDroneScreen ($7FE6, comcen screens overlay) loads A/Y with LDA #$24 / LDY #$FE ($7FEC-$7FEE)
; and calls unpackRleTo0200 ($67CB) at $7FF0, which again expands it to exactly 512 bytes at
; $0200-$03FF = eight shapes on a 64-byte stride. The drone screen copies one of them into sprite 5
; through loadDirectionShapeSprite ($802A when the screen is reset, $810B at launch,
; $829E on every camera step) as the heading indicator; the shapes are the big top-down drone
; silhouettes in the same compass order N, NE, E, SE, S, SW, W, NW. Correction: the survey pass called
; this aircraftShapeTable, started it three bytes early at
; $FE21 (those three bytes are the last run of the $FD70 stream, not a header), described the
; $05,$xx run tokens as record headers and judged it 'probably stale data' that 'nothing
; references'. All of that is wrong - see the note at $FD70 for why the reference does not show up in
; XREF.txt.
droneScreenShapesRle:
.byte $05,$05,$0A,$00,$10,$00,$00,$10; FE24 ........ escape marker $05, then the compressed silhouette stream
.byte $00,$02,$38,$80,$0F,$FF,$E0,$0F; FE2C ..8.....
.byte $FF,$E0,$0F,$FF,$E0,$08,$7C,$20; FE34 ......|
.byte $00,$38,$00,$00,$10,$00,$00,$10; FE3C .8......
.byte $00,$00,$10,$00,$00,$38,$00,$00; FE44 .....8..
.byte $7C,$00,$00,$EE,$05,$15,$00,$40; FE4C |......@
.byte $00,$00,$F0,$00,$01,$F0,$00,$00; FE54 ........
.byte $78,$80,$00,$3F,$00,$00,$1F,$00; FE5C x..?....
.byte $00,$1F,$00,$00,$1F,$80,$00,$1F; FE64 ........
.byte $E0,$00,$21,$E0,$07,$C0,$F0,$03; FE6C ..!.....
.byte $C0,$60,$00,$C0,$40,$00,$C0,$00; FE74 .`..@...
.byte $00,$40,$05,$18,$00,$0F,$00,$00; FE7C .@......
.byte $07,$00,$00,$07,$80,$00,$07,$00; FE84 ........
.byte $04,$07,$00,$06,$0F,$00,$07,$1F; FE8C ........
.byte $80,$03,$FF,$E0,$07,$1F,$80,$06; FE94 ........
.byte $0F,$00,$04,$07,$00,$00,$07,$00; FE9C ........
.byte $00,$07,$80,$00,$07,$00,$00,$0F; FEA4 ........
.byte $05,$18,$00,$40,$00,$00,$C0,$00; FEAC ...@....
.byte $00,$C0,$40,$03,$C0,$60,$07,$C0; FEB4 ..@..`..
.byte $F0,$00,$21,$E0,$00,$1F,$E0,$00; FEBC ..!.....
.byte $1F,$80,$00,$1F,$00,$00,$1F,$00; FEC4 ........
.byte $00,$3F,$00,$00,$78,$80 ; FECC .?..x.
D_FED2:
.byte $01,$F0,$00,$00,$F0,$00,$00,$40; FED2 .......@ the label here is an artifact of the boot loader's block move (STA $FED2,Y at $C203); this is just more compressed shape data
.byte $05,$15,$00,$EE,$00,$00,$7C,$00; FEDA ......|.
.byte $00,$38,$00,$00,$10,$00,$00,$10; FEE2 .8......
.byte $00,$00,$10,$00,$00,$38,$00,$08; FEEA .....8..
.byte $7C,$20,$0F,$FF,$E0,$0F,$FF,$E0; FEF2 | ......
.byte $0F,$FF,$E0,$02,$38,$80,$00,$10; FEFA ....8...
.byte $00,$00,$10,$05,$18,$00,$04,$00; FF02 ........
.byte $00,$06,$00,$04,$06,$00,$0C,$07; FF0A ........
.byte $80,$1E,$07,$C0,$0F,$08,$00,$0F; FF12 ........
.byte $F0,$00,$03,$F0,$00,$01,$F0,$00; FF1A ........
.byte $01,$F0,$00,$01,$F8,$00,$02,$3C; FF22 .......<
.byte $00,$00,$1F,$00,$00,$1E,$00,$00; FF2A ........
.byte $04,$05,$11,$00,$01,$E0,$00,$01; FF32 ........
.byte $C0,$00,$03,$C0,$00,$01,$C0,$00; FF3A ........
.byte $01,$C0,$40,$01,$E0,$C0,$03,$F1; FF42 ..@.....
.byte $C0,$0F,$FF,$80,$03,$F1,$C0,$01; FF4A ........
.byte $E0,$C0,$01,$C0,$40,$01,$C0,$00; FF52 ....@...
.byte $03,$C0,$00,$01,$C0,$00,$01,$E0; FF5A ........
.byte $05,$12,$00,$04,$00,$00,$1E,$00; FF62 ........
.byte $00,$1F,$00,$02,$3C,$00,$01,$F8; FF6A ....<...
.byte $00,$01,$F0,$00,$01,$F0,$00,$03; FF72 ........
.byte $F0,$00,$0F,$F0,$00,$0F,$08,$00; FF7A ........
.byte $1E,$07,$C0,$0C,$07,$80,$04,$06; FF82 ........
.byte $00,$00,$06,$00,$00,$04 ; FF8A ......
; $FF90-$FF92 is not a block of its own. It is the last token of the compressed stream that starts
; at $FE24: escape marker $05, count $0E, value $00 - the 14 zero bytes that pad the eighth shape out
; to its 64-byte slot and bring the unpacker's output to exactly 512 bytes. Decoding the stream byte
; by byte confirms it: the run that begins here is consumed at $FF92, and the very next byte, $FF93,
; is the first byte of droneViewfinderSprite, the 63-byte uncompressed reticle that launchDrone copies
; into sprite 2 at $80EB. So nothing at the top of this block is spare:
; $FD70-$FFD1 is two compressed sprite sets and one raw sprite, back to back, ending exactly at
; $FFD1 just below the CPU vectors.
; Correction: the survey pass labelled $FF90 unknownShapeTail, gave it 66 bytes and called it 'most
; likely unused leftover memory ... stale bytes that resemble the end of the $8800 message block'. It
; is neither unused nor related to $8800 - that block's tail ($897F-$89FF) is an unrelated
; $00/$FF dither pattern and does not match these bytes.
droneScreenShapesRleEnd:
.byte $05,$0E,$00 ; FF90 ... $05,$0E,$00 = run of 14 zeros ending the drone silhouettes
; droneViewfinderSprite - 63 bytes of raw, uncompressed sprite data (21 rows x 3 bytes): the drone
; camera's viewfinder, a frame of four corner brackets with a centre tick on each side and a scale bar
; across the middle. launchDrone copies it into sprite 2's shape block with the loop at
; $80EB-$80F2 (LDY #$3F, so it also reads the sprite's unused 64th byte from $FFD2, one byte past
; the block the loader moved up here), colours the sprite black ($80E4), parks it at X = $AC / Y = $88
; over the camera window and enables it. This is why the loader moves the $FA00 block up by $1B8: the
; sprite has to end exactly at $FFD1, immediately below the CPU vectors.
droneViewfinderSprite:
; drone camera viewfinder, 21 rows of 24 pixels
.byte $00,$08,$00 ; FF93 ............#...........
.byte $00,$00,$00 ; FF96 ........................
.byte $0F,$08,$78 ; FF99 ....####....#....####...
.byte $08,$00,$08 ; FF9C ....#...............#...
.byte $08,$08,$08 ; FF9F ....#.......#.......#...
.byte $08,$00,$08 ; FFA2 ....#...............#...
.byte $00,$00,$00 ; FFA5 ........................
.byte $00,$00,$00 ; FFA8 ........................
.byte $00,$00,$00 ; FFAB ........................
.byte $00,$00,$00 ; FFAE ........................
.byte $49,$00,$49 ; FFB1 .#..#..#.........#..#..#
.byte $00,$00,$00 ; FFB4 ........................
.byte $00,$00,$00 ; FFB7 ........................
.byte $00,$00,$00 ; FFBA ........................
.byte $00,$00,$00 ; FFBD ........................
.byte $08,$00,$08 ; FFC0 ....#...............#...
.byte $08,$08,$08 ; FFC3 ....#.......#.......#...
.byte $08,$00,$08 ; FFC6 ....#...............#...
.byte $0F,$08,$78 ; FFC9 ....####....#....####...
.byte $00,$00,$00 ; FFCC ........................
.byte $00,$08,$00 ; FFCF ............#...........