-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathnotes.ref
More file actions
212 lines (174 loc) · 10 KB
/
notes.ref
File metadata and controls
212 lines (174 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
; Implementation notes about River Raid's engineering.
[Note:fps:Game loop runs at ~12 FPS]
The game loop runs at approximately 12 FPS despite the ZX Spectrum's 50 Hz
interrupt rate. This was measured programmatically using a custom FPS counter
that tracks #R$5EEF increments per 50 interrupts. The gap is caused by the
scroll routine taking multiple frame periods to complete.
[Note:interruptDrivenSound:Multi-channel interrupt-driven sound]
Sound routines are called from the ~50 Hz interrupt handler at #R$6BDB, one
frame of audio output per interrupt. Multiple sounds can play simultaneously
via a 6-bit bitmask at #R$6BB0 (fire, speed modes, low fuel, bonus life,
explosion).
The explosion sound's distinctively noisy character comes from an
uninitialized register: the DE register is not set by the caller, so it
retains whatever value the interrupted main loop code happened to leave. This
makes the pitch vary semi-randomly between frames, producing the chaotic
explosion effect.
; Technical decisions and tradeoffs
[Note:preShiftedSprites:Pre-shifted sprites (and wasted fighter frames)]
All sprites exist in 4 copies, each shifted by 2 pixels. The rendering
routine at #R$8B1E picks the correct pre-shifted frame based on the object's
X position bits 1-2. This trades memory for speed: a single table lookup
replaces what would otherwise be 0-6 bit-shift operations per scanline during
rendering.
However, fighters move at 4 pixels per frame — exactly one character cell
width. This means that at most 1-2 of the 4 shifted frames are ever actually
displayed for fighters. The remaining 2-3 frames (48-72 bytes per direction)
are wasted memory.
[Note:pixelCollisionDetection:Pixel-perfect collision via rendering pipeline]
The sprite renderer doubles as the collision detector. The two-phase pipeline
at #R$8B3C XOR-erases the old sprite, then OR-draws the new one; if OR-ing
onto non-zero screen data, a collision is flagged and dispatched via #R$6136.
There is no separate collision map — the framebuffer itself is the collision
surface.
This means any non-zero screen content — including attribute artifacts,
explosion debris, or data written via POKE during a pause — acts as a
collision surface. See also #LINK(Bugs#screenCollision)(Collision detection
works on raw screen pixels).
[Note:selfModifyingCode:Self-modifying code for blending modes]
The sprite renderer uses self-modifying code to switch between blending modes
at runtime. The opcodes <code>OPCODE_XOR_B</code> and <code>OPCODE_OR_B</code> are patched into the
instruction addresses #R$8C3C and #R$8C1B:
#LIST
{ #R$7046 patches in XOR mode (for fighters and tanks) }
{ #R$72EF restores OR mode (default blending) }
LIST#
Self-modifying code is also used for the fuel gauge at #R$6124, where a
computed sprite data offset is patched into an RLC instruction's operand.
[Note:scrollInBufferZone:Black-on-black buffer zone for scroll-in]
Attribute row 0 (#R$5800) is kept permanently black, making it an invisible 8-pixel
buffer. Objects spawning at the top scroll into view from behind it. This is
sufficient for 1-tile sprites (ship, helicopter) but not for balloons (16 px) or
fuel stations (25 px) — their lower portions appear immediately.
The overview text crawl uses a similar 1-column buffer at column 31 of char row
23: characters are printed there with black ink + paper and shifted left into
view by #R$8A1B.
[Note:singleProjectiles:Only one tank shell and one helicopter missile at a time]
Each projectile type has a single global state variable: #R$7383
(tank_shell_state) for tank shells and #R$5F73 (helicopter missile
coordinates) for helicopter missiles. When any tank tries to fire, it checks
<code>TANK_SHELL_BIT_FLYING</code> in #R$7383 and skips firing if the bit is
set. When any advanced helicopter tries to fire, it checks the Y coordinate
in #R$5F73 and returns if non-zero. All tanks share the one shell
slot; all advanced helicopters share the one missile slot.
[Note:asciiArithmetic:Arithmetic directly on ASCII digit strings]
Instead of binary addition followed by BCD-to-ASCII conversion, the game
manipulates score characters directly. The algorithm at #R$913B works like
schoolbook long addition:
#LIST
{ Load an ASCII digit character from the score buffer }
{ Increment it (INC adds 1 to the character code) }
{ Compare with '9'+1 (<code>CHAR_0</code> + 10) to detect overflow }
{ If overflow: store '0' and carry to the next higher digit }
{ If no overflow: store the updated digit and print it }
LIST#
This avoids binary math entirely — there is no binary score representation
anywhere in the game. The tradeoff: single-digit updates are fast, but adding
multi-digit values requires repeated single-digit increments with carry
propagation.
[Note:incrementThenCorrect:Increment-then-correct for bidirectional movement]
When a tank on a river bank moves (#R$72F8, #R$7302), the code always
increments the coordinate first, then checks which bank the tank is on. If
it is on the right bank, it subtracts twice the increment to effectively
decrement:
<pre>
; Instead of: The code does:
; IF left_bank INC coordinate ; always go right
; INC coordinate IF right_bank
; ELSE DEC coordinate ; undo + reverse
; DEC coordinate DEC coordinate
; END END
</pre>
This replaces a conditional branch (to choose increment vs decrement) with
unconditional arithmetic followed by a correction. It saves a few bytes but
looks counterintuitive when reading the code.
; Poor decisions
[Note:player2Afterthought:Player 2 logic was a late addition]
Player 2 handling is scattered across the codebase as duplicated branches
rather than parameterized code. Nearly every routine that touches scoring,
bridge display, status line, or death handling has a
<code>CP PLAYER_2 / JR Z</code> fork to a near-identical copy:
#LIST
{ Score increment: #R$913B (player 1) vs #R$9169 (player 2) }
{ Score printing: #R$914B (player 1) vs #R$9179 (player 2) }
{ Bridge display: #R$64BC (player 1) vs #R$64E5 (player 2) }
{ Plane movement: #R$65F3 and #R$6642 both branch for player 2 attributes }
{ Death handling: multiple routines with <code>CP PLAYER_2</code> forks }
LIST#
A parameterized design using the player number as a table index would have
eliminated the duplication. The pattern strongly suggests that single-player
mode was developed first and two-player support was bolted on afterward.
[Note:levelDataStorage:Level data stored wastefully]
The game dedicates nearly 24 KB to level definitions — almost half the total
program size:
#TABLE(default)
{ =h Data | =h Size | =h Location }
{ Terrain shapes | 48 levels × 256 bytes = 12,288 bytes | #R$9500 }
{ Object spawns | 48 levels × 256 bytes = 12,288 bytes | #R$C800 }
TABLE#
The terrain data is reasonably efficient (64 fragments × 4 bytes = 256 bytes
per level). However, the object spawn data uses 128 slots × 2 bytes per
level, where most slots are empty (X position = 0). A more compact encoding
— such as run-length compression for empty slots or delta-encoding for
terrain offsets — could save significant memory.
The straightforward storage suggests the level data was likely generated by
an external tool that prioritized simplicity over compactness.
[Note:duplicatedRendering:Island and bank rendering heavily duplicated]
Left bank and right bank rendering in #R$6990 (island edges) and #R$6AA3
(terrain fragments) follow identical logic but are implemented as separate
code paths with only operand differences. The duplication is remarkably
regular — same structure, same control flow, different constants.
This pattern could indicate systematic copy-paste development, or
alternatively, output from a high-level language compiler or macro assembler
that expanded separate invocations without optimizing the identical code.
; Unused code
[Note:unusedControlScreen:Unused control selection screen]
Text data at #R$8561 contains a formatted control selection menu that is
never displayed by the game code. It appears to be a remnant of an earlier
UI design, replaced by the current menu system implemented at #R$7AB9.
[Note:uselessTrampoline:Useless single-instruction trampoline]
The routine at #R$76AC is a single
unconditional <code>JP #R$708E</code> instruction. It adds no logic —
callers could jump to #R$708E directly. This is likely a remnant of
refactoring where an intermediate routine was removed but its entry point was
preserved.
[Note:deadCode:Dead frame offset calculation in balloon handler]
At #R$7695, 8 bytes calculate a frame offset from the frame counter. However,
the result is immediately overwritten by balloon-specific parameters in the
instructions that follow. This dead code is likely left over from a time when
this code path handled multiple object types before being specialized for
balloons only.
[Note:unusedSprites:Unused alternate explosion animation]
A 6-frame animation at #R$8AD8 shows a diamond shape expanding from a single
pixel to a full diamond (48 bytes total). This appears to be an
early or alternate explosion effect that was cut from the final game. The
sprites are already rendered as UDGs in the disassembly listing.
[Note:unusedData:Unused variables and data areas]
Several variables and data areas in the game binary are never accessed by any
code:
#TABLE(default)
{ =h Address | =h Description }
{ #R$5F6F | State variable: cleared during init but never read }
{ #R$64B4 | Data area (purpose unknown) }
{ #R$6C2B | Data area (purpose unknown) }
{ #R$7727 | Data area (purpose unknown) }
{ #R$8391 | Alternate road attributes (possibly debug or early version) }
{ #R$8B18 | Data area in sprite renderer }
{ #R$8B1B | Data area in sprite renderer }
{ #R$8C4A | Data area in sprite renderer }
TABLE#
Several of these areas begin with the bytes <code>$C3,$90,$EA</code> — the Z80 encoding of <code>JP $EA90</code>. Since
<code>$EA90</code> falls within the object spawn tables and has no special meaning in the
shipped game, this is likely a fill pattern from the development toolchain:
a debug trap that would redirect stray execution to a fixed address
(presumably a monitor or debugger) on the development system.