DARKER Research archive

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.

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

  1. Map initialization BB75–BB8B gives 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 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.