Flight physics and energy beacons: first logic pass
2026-09-18. Addresses are offsets in the canonical unpacked 64 KiB image. This identifies the core routines and verifies selected arithmetic against the original machine code. It is not a complete decompilation of the flight callbacks.
Artifacts:
What is worth doing next
The major static visual assets are accessible already. Remaining valuable asset work includes playable Sound Images music/effects, scripted playback/composition of the recovered animation frames, the gate and fountain transform commands, and precise sprite-sheet/font layout metadata. Those are useful parallel milestones, but need not precede game-logic recovery. The raw archive resources are preserved.
Update path and state
B0CE–B0D8 computes a timer difference ([0BF0] - [0BF8]) and stores it in the
self-modifying immediate at 7A2B. 3DDC–3DF5 feeds that step to input handling
and writes two resulting control quantities at 7EBA and 7ECF.
79BB–7A38 walks object lists, preserves old positions, loads this same step in
CX, and calls the function pointer at object offset +66.
The player object is at SS:D986. BP-relative object accesses default to SS;
plain absolute references use the code/data segment. Confusing these segments
would produce a superficially plausible but incorrect decompilation.
| Location | Current interpretation | Evidence |
|---|---|---|
Object +04,+05,+06 |
Fractional bytes for the three position components | Accumulated with carry by 8208, 826E |
Object +08,+0A,+0C |
Horizontal X, horizontal Y, vertical position words | Same integration routines; 8450 uses +0C for altitude |
Object +24,+26,+28 |
Angular motion state | 8292–8308, 8067, 8394; full units still to be documented |
Object +2A |
Heading angle | Indexes horizontal sine/cosine movement |
Object +2C |
Pitch angle | Vertical/horizontal projection and altitude control |
Object +2E |
Bank/roll angle | First steering channel, heading/pitch coupling |
Object +30 |
Angular response parameter | 83D4; populated by BF53 |
Object +38,+3A,+3C |
Previous position words | 7A18–7A27 |
Object +3E |
Combined speed magnitude | 8254: square both speed components, integer square root |
Object +40 |
Vertical velocity component | 826E |
Object +42 |
Forward/horizontal velocity component | 8208 |
Object +66 |
Current update callback | Dispatcher 7A2D |
Position words plus fractional bytes form 24-bit accumulators. Horizontally,
256 position-word units represent one map cell; that corresponds to 2,048 native
model units. The vertical position is used directly against the beacon height
2,400. These are engine units, not calibrated metres or mph. Angles wrap in
16 bits; angle >> 6 selects one of 1,024 sine samples.
Flight callbacks
| Entry | Role |
|---|---|
7E7F |
Caero initial engine/reserve startup state |
7EB6 |
Normal Caero flight update after callback transition |
8108 |
Normal Skimma flight/shield update |
7E49 |
Transitional callback initially used by configuration 3; can switch to 8108 |
D510 |
Underground configuration callback; not traced in this pass |
7D9A |
Automatic landing callback; see flight-controls.md |
7EAD is a small but important state-machine trick: it pops its call's return
address into the object's callback pointer and jumps there. Thus the initial
Caero callback is not its steady-state physics function.
The Caero update combines steering, bank-related turning, altitude assistance,
forward-speed response, vertical response and energy accounting. These are
handcrafted integer dynamics, rather than a general rigid-body or aerodynamic
library. 8067 selects manual pitch behavior versus altitude assistance using
flag 7ED2; 80A4 reads the desired-height term and current height. 80C4 adds
a speed-dependent correction. The vertical target calculation near 8004
contains a default -90 term, patched from the craft definition at BD16–BD19,
and an altitude-dependent term. It should not be promoted to a universal gravity
constant without completing the surrounding equations.
Skimma 8108 uses a response term derived from the square of
max(0, 2047 - speed), so control response depends on speed. Its forward target
uses the selected speed parameter plus speed >> 3; the brake substitutes 130
for the selected parameter (81C9–81E0). The known speed selections are 248,
500 and, for the upgraded configuration, 640. These are internal target
parameters, not directly established road/air speeds.
Shared integration, with checked rounding
8208 updates forward velocity and horizontal position. 826E updates vertical
velocity and height. In the tested bounded range, their velocity update is:
delta = arithmetic_shift_right((target - old_velocity) * step, 16)
new_velocity = old_velocity + delta + (delta == 0 ? 1 : 0)
mid_velocity = old_velocity + arithmetic_shift_right(new_velocity-old_velocity, 1)
The forward target has already been multiplied by pitch cosine. Horizontal
integration forms a signed travel term from mid_velocity * step, then projects
it with heading sine/cosine into the two 24-bit position accumulators. Vertical
integration accumulates the high 24 bits of 2 * mid_velocity * step.
The CMP AX,1 / ADC AX,BX sequence gives a surprising result: with old velocity
100 and target 100, the next velocity is 101, not 100. A direct original-code
probe confirms this. Signed shifts also make opposite headings differ by one
fractional position unit in the recorded example. A faithful C++ version must
preserve these operations, carry behavior and wrap boundaries; ordinary float
interpolation would change them.
The angular helper family is 83B3–8411: bounded angular error, response gain,
damping and midpoint-like integration. 7AD6–7C72 converts held keys and Ctrl
into smoothed input. These are identified but not yet independently decompiled
and verified to the same extent as the linear integration routines.
A further initialization dependency surfaced during verification: 9D6E–9D79
sets up copying the first 256 sine words to 9C4A, performed by REP MOVSW at
03DC. This extends the table for cosine lookups that cross its end, overwriting
startup code. The probe recreates that copy. Reading only the untouched unpacked
image gives incorrect fourth-quadrant physics—a useful warning for a full
static disassembly and reimplementation.
Beacon selection: a regular lattice, not a proximity search
All 225 Delphi beacons occupy the 15×15 lattice of map coordinates
0, 9, 18, …, 126 on each axis. Startup 0396–03A9 generates a 256-byte coordinate
lookup table at world-segment A800. For in-city coordinates it selects the
nearest lattice coordinate from the integer cell index; its exact wrap behavior
is recorded in the JSON rather than replaced with an assumed modulo formula.
8450 looks up one candidate cell using those two coordinate mappings. It does
not scan a list of towers or sum contributions from several towers. It returns
zero if the selected coordinates are outside the map or that cell's type is not
- Map initialization
BB75–BB8Bgives these lattice type-1 cells state byte FF. The second cell byte is therefore a beacon strength input, not just a universal building-destruction flag.
Distance law
Inputs to 8450 are DX/BX (horizontal position words), AL/AH (their fractional
bytes) and SI (height). Given the selected tower's column and row:
rx = 16*(x_word - tower_column*256) + floor(x_fraction/16) - 2048
ry = 16*(y_word - tower_row*256) + floor(y_fraction/16) - 2048
rz = 2*height - 4800
distance_term = floor((rx*rx + ry*ry + rz*rz) / 16384)
denominator = max(256, distance_term) + 1024
energy_value = floor(cell_state * 65793 / denominator)
This is the recovered arithmetic for normal in-map, nonnegative-height inputs. The original is word arithmetic, including signed comparisons, not an unbounded mathematical function. It has an explicit signed height cutoff: at 11,616 and above it immediately returns zero. Horizontal and vertical terms correspond to a centre at the cell midpoint and height 2,400, matching the tower's light disc. The minimum distance term makes a flat maximum-strength region near that centre; farther away it is an inverse-squared-distance-style falloff with an added denominator constant. It contains no line-of-sight/occlusion test.
Original-code examples for a full-strength (FF) beacon:
| Position | Returned energy value |
|---|---|
| At beacon centre, height 2,400 | 13,107 |
| One cell horizontally away, same height | 8,191 |
| Two cells horizontally away, same height | 3,276 |
| Directly below it at height 0 | 6,904 |
| Directly above it at height 11,615 | 771 |
| Height 11,616 | 0 |
At the centre, state 128 gives 6,579; state zero gives zero. These are intermediate engine values, not percentages or energy units per second.
From beacon strength to recharge
7F35 calls the beacon function. 7F38–7F3F forces its result to zero unless
the engine flag is exactly 1. 4550 receives the indicator value
floor(min(24, energy_value >> 8)/2), ranging from 0 to 12.
The following code uses multiple accumulators, rather than directly adding the raw beacon result to a single fuel counter:
| Address | Role in this path |
|---|---|
7F8F word |
Intermediate energy buffer; filled here and drawn down earlier in the flight update |
7FA6 word |
Accumulated energy reserve, capped at CFFF; high nibble becomes display value at 4551 |
8540 word / 8541 high byte |
Boost reserve, capped here at BFFF; Enter spends 32 from the high byte |
7F0D word / 7F0E high byte |
Active boost amount, consumed in the forward-target calculation |
For a normal small positive input step t, the Caero transforms CX to
T = floor(257*t/2) before this accounting. The initial boost gain is:
if energy_value > 250:
gain = floor((energy_value - 250) * T / 131072)
else:
gain = 0
if gain != 0:
gain += T >> 8
The low-strength portion (capped at 250) replenishes the intermediate buffer.
For energy values at least 256, up to T >> 2 transfers from that buffer into
the main reserve, limited by available buffer and remaining capacity. 8529
updates the two reserve caps and the energy display. The probe compares this
accounting against the original instructions for boundary strengths, engine off,
and both cheat states, starting from empty reserves.
The Jason Brooke special replaces the multiply at 7F7B with MOV DX,CX.
On the energy_value > 250 branch, the initial boost gain becomes T >> 1
instead of the distance-dependent product above. The engine gate and branch
threshold still apply in this path. This is a much more specific mechanism than
simply freezing or setting the reserve to infinity, and is consistent with the
observation of effectively unlimited boost recharges during play.
Faults and partial output
Playtesting confirms faulty/disabled beacons later in the game. The code implements partial output during fades, not just binary switching:
- The per-frame call at
3E27runsC510. A configured run of map state bytes changes over a 256-timer-unit interval. - Script opcodes
12and13select horizontal and vertical beacon runs through handlersC470andC467. They consume a packed lattice coordinate byte and a count. The cell-byte strides are 18 and 2,304 respectively: nine columns or nine rows in the expanded map.C54Cinstalls the run and deadline. - Script opcode
14, handlerC1A0, complements the fade-mode byte atC526. Initial mode FF is installed by2DC2. - In dimming mode the updater caps each selected state byte at the descending value. Existing weaker/off beacons are not accidentally brightened.
- In restoration mode it assigns the ascending value, ending at FF. The updater then disables itself. This is one configured run at a time, not a separate independently timed fade controller for every beacon.
- Opcode
11, handlerC442, consumes the next coordinate from the mission header queue. Setup BD98 binds C449 to F004; it is not a random sequence. C569consumes that whole FF-terminated list and sets its beacons to zero, but its caller is the mission-exit path at 3ECB, not initial setup. The earlier initial-outage interpretation is corrected in blackout mission analysis.
The original horizontal-run setup and updater were executed for two beacons:
| Elapsed timer units | Dimming strength | Restoring strength |
|---|---|---|
| 0 | 255 | 0 |
| 1 | 254 | 1 |
| 64 | 191 | 64 |
| 128 | 127 | 128 |
| 192 | 63 | 192 |
| 255 | 0 | 255 |
| 256 and later | 0 | 255 |
Since 8450 multiplies by that state byte, these intermediate values really
reduce charging capability; they are not merely a visual fade. The routine
itself has no source-cell write or drain caused by drawing charge.
These findings establish implemented scripted fade behavior. Exact mission activation/timing and the full mission-script control flow are not yet decoded. No persistent arbitrary intermediate-strength setting has been established. The timer's real-seconds conversion remains open, so the interval is not claimed to be 256 frames or a particular number of seconds.
Verification and next steps
PYTHONPATH=/tmp/darker-python python3 tools/verify_flight_physics.py
Passed 209 beacon evaluations, 28 reserve-accounting cases, 200 vertical integration cases, four cardinal horizontal integrations, the zero-error rounding probe, and two beacon-fade timelines. Startup coordinate lookup generation runs as original x86; the sine extension reproduces its identified startup copy. Craft/map states are controlled fixtures. This does not validate entire callbacks, collisions, landing, long trajectories, or every arithmetic overflow case.
A productive next step is a small fixed-width C++ flight-state module driven by recorded inputs, checked frame by frame against original-code execution. Finish the angular helpers and normal Caero callback first, then the Skimma branch, collision/damage and landing. Preserve exact sine values, startup mutations and self-modifying state as explicit data fields. This provides a stronger foundation for a full disassembly/decompilation than assigning names to a linear sweep of mixed code, data and dynamically overwritten regions.
Flight-speed calibration
We timed the starting Caero from beacon cell (0,9) to (0,126), holding Enter with the Jason Brooke special enabled. Travel took approximately 15–16 seconds, with small boost gaps and steering inefficiencies. The straight-line separation is 117 cells, or thirteen nine-cell beacon intervals. This gives average route progress of 7.31–7.80 cells/second, equivalent to about 1.15–1.23 seconds per nine-cell interval. It is not a demonstrated maximum speed; initial velocity, precise height and endpoint crossing are unspecified.
The subsequent timer-setup probe supports approximately 500.075 source ticks per second (see model-animation-and-states.md), so this run spans roughly 7,501–8,001 timer units. In the level, straight, continuous approximation to 8208, with transformed step approximately 128.5 times the source tick count, this progress corresponds to an average internal forward velocity of roughly 3,800–4,100. Integer truncation, actual timestep distribution, non-level flight and steering prevent treating that estimate as an exact recovered velocity.
The code explains why the cheat does not establish a single maximum speed: it changes boost recharge, whereas each Enter activation sets the active boost high byte to 38h; the flight callback consumes that boost and also feeds current speed and beacon strength back into the next target (7F50–7F68). The craft-25 record's low byte at +0E is CFh, loaded and shifted left three by BD16–BD22 to initialize the forward-response parameter to 1,656. These are distinct from an observed cells-per-second value. No constants have been adjusted to fit the observation, and a precise ideal traversal time is not claimed.
Recorded observation. A steady Skimma crossing, after settling at a chosen speed and approximately constant height, would provide a cleaner independent check of position scaling and integration. Record ordinary/upgraded craft, −/= or Enter setting, exact beacon endpoints and whether timing begins after acceleration.
HUD terminology correction: the manual/gameplay mapping identifies display 4551 as the Weapon Status Display charge meter, and 454E/454F as the Caero Power Storage Device cell indicators/next-cell charge meter. References above to the reserve feeding 4551 describe the traced arithmetic and should not be read as identifying it with the stored boost cells. See corrected HUD identities.
Streetlight source-type check
A further native 8450 probe tested all 256 cell type bytes at the selected lattice position, with full strength and the same centre/height fixture. Only type 1 produced charge; type 11 returned zero. Results.
This rules out the small streetlight model as a direct source through this charging routine. It does not by itself explain the low meter reading near lamps: residual tower contribution, downstream buffers or another path would need a full moving-player trace. No undocumented streetlight contribution has been established.
Follow-up: angular and impact verification
See angular flight helpers and impact arithmetic for native differential checks of steering gain/damping, angle folding, shared object damage and the isolated building-state mutation. Complete flight callbacks and player collision response remain open.