DARKER Research archive

Darker demo archaeology

The shared demo browser links maps, model banks, scenario records, key tables, source graphics and comparison overlays. Original files under demo/ are read-only inputs; darker-cpp is outside this work. Labels “1993” and “1995” identify the supplied packages, not proven dates for all their contents.

Recovered material and scope

All four executables decode to 65,536-byte images using the existing LZ/XOR stream decoder. Early streams begin at file offset 02BC; the later stream at 02BE. The early canonical resource directory is at image offset 0D27, the later one at 17CE. The resource decoder strictly consumes 31 early and 55 later packed resources. Unlike the retail unpacker, these demo images have not yet been matched against complete DOSBox runtime memory snapshots.

tools/demo_releases.py produces hashes, uncompressed resources, strings, indexed source sheets and viewer data. The model opcode numbers differ by release; commands are mapped during interpretation, never by editing operand bytes. Seven demo maps are integrated into the shared 2D and 3D explorers; all occupied cells have supported definitions. Some unplaced definitions are still unsupported. Model states and placement retain retail-derived conventions pending equivalent native demo verification.

tools/verify_demo_vertices.py checked 875 static definitions / 48,092 vertex emissions against original demo instructions, using two distant projections and a one-pixel tolerance. This is not an exact test of polygon rasterization, state selection, dynamic transforms or map placement.

Package Surface A Surface B Underground
1993 maps 00/12 00/13 00/14, 00/15
1993 banks 00/8 00/9 00/10, 00/11
1995 maps 00/38 00/39 00/40
1995 banks 00/10 00/11 00/12

Native configuration selection confirms consecutive bank/map pairings: 1993 X2 code at 8222 uses 16-byte records at 1870; 1995 code at 9CCE uses 22-byte records at 2609. Surface A's normal Caero configuration selects bitmap/palette resource 2, not 1. The initially orange-faced 1995 viewer used the wrong source palette. Later underground configuration 4 additionally loads palette resource 5. Raw source sheets intentionally keep their own stored palettes.

Map differences and unplaced models

tools/compare_demo_maps.py exports exact cell differences and images linked from the demo browser. Coordinates retain the shared 2D map orientation.

Before → after Added Removed Changed raw type ID
1993 Surface A → B 366 27 5
1995 Surface A → B 47 486 175
1993 tunnel 1 → 1995 tunnel 1 166 178 47
1995 Surface A → retail Delphi 972 29 34

Added/removed counts compare occupancy. Raw IDs across banks do not guarantee matching geometry, and unchanged cells can still contain different models. The visual identification of Surface B as a Halon-coloured version of A is consistent with their extensive shared layout. The later first tunnel is not simply an early tunnel with a branch deleted: there are additions too.

The 1993 Surface A type 13 dish has zero direct placements. Model tiles and the selector now say “unused on maps” when that is true across every exported map for the bank. This does not rule out linked-state or runtime use. Surface B type 13 is a different definition; type IDs must always include the bank.

Executables and startup

X1 and X2 differ by removal of CALL F04E at X1:648F, nearby address adjustments, and padding. The routine remains in X2 but is not called at that startup point. Its port/register sequence matches GF1/Gravis UltraSound initialization, including registers 41, 45, 49, 0E and 4C plus a DRAM test. Register identifiers can be compared with the Linux GF1 definitions. This is evidence for a disabled hardware path, not a swapped sound backend. No separate gameplay change has been identified between X1 and X2.

The complete decoded-image normalization below now accounts for X versus X1. The substantive additional difference is X's CALL F45F at F517, omitted in X1. That routine uses a wrapper at 15FE/1602 issuing INT 33h with AH=60h, first AL=7Fh and BX=1. The wrapper shifts AX before the caller checks 3FB0. Further calls retrieve records and set device flags. Device identity remains unresolved; calling this a different sound-card engine would be premature.

The original X/X1 splash stall is now reproduced and a startup workaround verified. Earlier inconclusive SDL-crash probes are superseded by the debugger and playtest results below.

Hidden commands and keys

Both canonical demos have exactly these three command records:

There is no Level X record. The menu retains the *3 comparison. The native matcher rejects retail-only phrases and case/punctuation changes; see command-verification.json and tools/verify_demo_code.py.

The active command must not be labelled “infinite boost” by analogy with retail. In 1995 it changes the immediate XOR mask in the Z handler at 9A91 from zero to 0C. Z consequently toggles the engine-setting word at 6AE7 by 0C00, and also replaces the craft callback at [BP+5E] with RET at 9A90. tools/verify_demo_cheat_effects.py executes the original matcher and Z handler with four initial settings, with and without the command. Examples: 250 becomes 3322, and 600 becomes 3672; these are internal settings, not measured speeds. Full flight consequences remain to be traced.

Playtest (1995 demo): after entering Jason's phrase, pressing Z appeared to start an engine sound, with no obvious additional gameplay effect. Audio was patchy. This is observed behaviour, not confirmation of a speed increase or engine-enable flag change. The same session started with the engine off; boost could launch the craft from the hangar, after which it gradually fell. The initial engine-off state is not yet established for every supplied save.

Retail's cheat shortcuts are Z (player freeze, enabled by Lyndon's phrase) and X (level transition, enabled by Level X). C and V dispatch to the no-op handler in both retail and the 1995 demo. The 1993 X/C/V handlers therefore must not be described as retail cheat shortcuts.

In 1993 the patch target is 7CE9, a NOP immediately before RET and the boost handler. No gameplay effect is established. Its only literal 7CE9 word reference is the patch instruction; this is not proof of unreachability.

All 83 flight event slots are exposed with disassembly. Notable differences:

Held steering, fire, brakes and look-around use separate paths; an event-table no-op does not establish that a key is unused. Command-line prefix tables are also exported. Early accepts AF/BF/BR/CL/EY/LI/LO/MO/SE; later replaces MO with IN and adds RA. README PrimFire/SecFire spellings should not be assumed to match this parser.

Fonts and other assets

tools/demo_fonts.py recovers both font resources (early 00/7, later 00/9): 98 glyphs at offset 0 and 95 glyphs at offset 7965. The last glyph data ends exactly at the 12,629-byte resource boundary. All four stored horizontal alignments decode to identical coverage and pattern pixels.

Both demos' decoded glyphs match each other. The first font matches retail's interface font for 97 of 98 shared slots; slot 127 differs. Its glyph set is smaller. The compact font is predominantly the same design as retail: 77 of 95 shared slots have identical bitmap dimensions, coverage and pattern, each moved down exactly one pixel in retail. These include all ASCII letters and digits. The earlier statement that this was a different font conflated vertical placement with glyph shape.

The other 18 slots have changed stored bitmaps/dimensions: 35–37, 43, 60–62, 64, 91–95 and 123–127. In particular, demo < / > at slots 60 and 62 are 3×5 pixels with top offset 1; retail replaces them with 12×11 graphical arrows at top offset 0. Retail also adds slots 128 and 129. The suggestion that the one-pixel shift accommodates the larger arrows is plausible, but the reason for that layout decision is not independently documented. The report now separates bitmap changes from top-offset changes.

Both demos are selectable in the shared font browser. Demo cursor advances still follow the retail convention; native demo advance logic is not yet verified. Retail portable font downloads currently remain retail-only.

Source cockpit/effects sheets are integrated as versioned selections. Demo instrument masks, live HUD transfer rules, effects recipes and audio playback remain to be reconstructed. Byte-identical retail resources are explicitly listed in each manifest; identity does not establish identical runtime use. The corrected 1995 fountain uses slots 5–10 rather than retail's 9–14; see animation details.

Scenarios and supplied saves

The extracted scenario resources are early 00/18–30 and later 00/42–54. There are 195 framed records, including short placeholders. Each record has a length, a relative script entry pointer and a configuration byte; it does not have the retail multilingual header. Text runs are not yet split into initial briefing, radio events and completion messages. Opcode semantics, objects and routes remain open.

Playtesting confirms the silly early radio messages actually appear during the first supplied save. They must not be categorized as unused strings.

Each save file has four records and a three-byte trailer: record strides are 1573 (early) and 2178 (later). CRC16/XMODEM over everything except the final two bytes agrees with the stored checksum. Names and record prefixes are exported, but the retail save editor is not compatible with these layouts.

Development provenance and remaining work

The early README explicitly says 21 May 1993. The same package includes a Microsoft Anti-Virus report dated 01-03-95 (date order ambiguous) and a file identified as Word for Macintosh 5.0. Printable document runs mention Palatino and LaserWriter. This supports Macintosh document production, not Macintosh game development or a specific assembler. The antivirus report is packaging history, not a current security assessment.

No assembler/compiler brand is established. Release-specific model command numbers, self-modifying code and executable-resident definitions fit the previous assembly-oriented findings but do not identify the author's tools.

Priorities remaining: full X/X1 normalization and startup investigation; native scenario/save/object decoding; flight and weapon comparison; complete held controls; demo HUD/effect reconstruction; early audio; native font cursor metrics and portable exports; geometry/state alias and removed-content audit.

Native save selection and first radio timeline

tools/demo_scenario_setup.py now executes the original save-slot lookup and scenario-selection instructions, stopping at the resource-loader call. It loads original save bytes into synthetic SS memory; it does not edit save files or bypass their checksum on disk. Eight controlled first-character probes per release distinguish name-driven selection from coincidence.

In 1993, the game path at 7EB6–7EDC selects resource 18 and record (first_name_byte - 1) & 7, ignoring the saved stage for this choice:

Supplied slot Record Configuration Map Model slot
Adrian Armstrong 0 1 12, Surface A 24
Christine Craven 2 4 14, underground 1 24
Bernard Biggins 1 3 13, Surface B 26
Edith Edwards 4 4 14, underground 1 24

Christine and Edith therefore select distinct scenarios on the same map. The fourth map, resource 15, is not selected by these four original names. A/B/C/D/E/F/G/H select records 0–7 respectively in controlled probes. This is a demo shortcut, not evidence that arbitrary renamed or checksum-invalid save files will be accepted by the full loader. The fifth menu entry, NIGHTMARE, stores the slot-4 sentinel and chooses resource 30 record 0. It is a visible special mode, not a fifth saved record.

In 1995, the four names are used as informational menu choices. Code 089F–08D9 shows their text, then only index 2, “A GLIMPSE UNDERGROUND”, sets the selected slot to 1 and jumps into the game path. The others return to the menu. Playtest confirms that these non-playable entries display text and return to the menu. Four native branch checks verify these destinations after text presentation; rendering the text itself is outside those checks.

The separate fifth entry NIGHTMARE enters the Run menu, from which “Run this game” starts the surface scenario. The later game path selects resource 54 record 1 for ordinary slot values, configuration 4 / map 40 / model slot 24. Its slot-4 sentinel selects resource 54 record 0, configuration 1 / map 38. Hypothetically invoking the game path with every ordinary slot selects the same underground record; this does not mean all four menu entries launch that level.

The early resource 18 record 0 has six initial object records (definition slots 18,18,18,20,20,22), then two empty groups. Their header high bits are preserved without assigning retail objective semantics. The player script begins at resource offset 005A; the separate header entry points to 0182. These are distinct script contexts and should not be conflated.

tools/trace_demo_radio.py executes the original 8427 dispatcher and 763C/7649/7656 message setup routines with that player context:

Script clock Radio message
0 Radio messages are displayed like this
3000 Skimma Supa and Assassin craft have been sighted
6000 The sky is full of Halon craft
9000 My tunny's full of Kraft cheese slices
12000 Your weapons aren't very powerful are they!
15000 Never mind:- They haven't got any weapons at all!

Each message lasts 2000 script-clock units. The intervening opcode 0F schedules a 3000-unit delay (operand 60 × 50). Opcode 09 schedules the radio message with duration operand 40 × 50, followed by width and length-prefixed text. The sequence is elapsed-time driven, not an enemy-proximity check. The harness verifies every clock value from 0 through 15000; real-time seconds and interruptions by other game systems are not established here. Reports and the timeline are linked from the demo browser.

The 1995 startup clear at 44D9–44E6 resets the HUD/engine state block, including the E-toggle byte at 4CDC, to zero. This supports the engine-off observation, but later script or craft-specific changes must be traced before calling it the final initial state for every configuration.

Weapon selectors, boost acceptance and the demo Z handler

tools/verify_demo_player_controls.py executes the original handlers with synthetic state. It checks all 256 input byte values for each early X/C/V handler, six later engine/charge combinations, and the later indirect player dispatch. Results are in player-control-verification.json.

The firing path at 95BF reads 36C9 and 36CA independently. Ordinary primary selection at 953E checks the availability mask at 952F. With that mask zero, a native test rejects normal selection of weapon 1, but X still sets the primary selector to 1. C likewise writes its selector directly rather than using the availability-checked selection path. Direct selection is not proof that a chosen weapon will fire successfully in every craft/context.

tools/verify_demo_initial_weapons.py checks the early header-entry scripts for the four supplied saves. All four begin with zero mask at save offset +1E. Adrian's record 0 executes opcode 14 with operand A9, XORing the mask with 03FF: selection IDs 1–10 become available. The other three selected header scripts leave the mask zero at this stage. This check precedes world and player scripts; it does not establish their final in-flight availability. Not every enabled selection ID has its own number-key binding.

In 1995, Enter's handler accepts boost with the engine flag both zero and one. With charge 31 it does nothing; with 32 or 64 it subtracts 32 and sets boost byte 6AD5 to 38h. It does not change the engine-enable byte 4CDC. This directly supports the ability to boost out of the hangar with the engine off. The harness stops before audio enqueue at 4138 and does not simulate the subsequent trajectory.

The later Z handler always installs return-only callback 9A90 at [BP+5E]. The indirect dispatcher at A392 then returns without running the previous player update. A second Z installs the same callback; unlike retail's freeze handler, it does not swap the old callback back. Other game paths may later replace that field. This is verified independently of entering a cheat.

What Jason's phrase actually changes

1993: the command writes 0C at 7CE9, replacing NOP immediately before RET. No execution path through that patch has yet been identified. The only literal 7CE9 word reference found is the patch itself, which is insufficient to prove unreachability. A stale development patch is a hypothesis, not a confirmed no-op. There is no established infinite-boost effect.

1995: the command changes the Z handler's XOR immediate from 00 to 0C. Z then toggles word 6AE7 by 0C00, in addition to its unconditional callback replacement. Without the phrase the XOR changes nothing. The patched word is used as a multiplier/setting by flight calculations (including 6AE6, 6DA4 and 6E71), but Z has also bypassed the normal player callback. Changing that word is therefore not evidence of an actual speed increase. Playtesting revealed an engine sound starting but no obvious flight effect; its downstream audio consumer has not yet been traced. Do not label either demo command “unlimited boost” by analogy with retail.

1995 craft-specific engine sound gating

The per-frame sound gatherer at 4012 loads the object's definition pointer from [BP+44], then calls the callback at definition +10h. It gathers that object's sound only when the callback returns ZF=1. The five 22-byte player configuration records provide these callbacks:

Configuration Craft model slot Callback Eligibility
0 26 42EA Object flag 08h clear
1, Surface A Caero 24 42EF E-toggle byte 4CDC equals 1
2 25 42EA Object flag 08h clear
3 26 42EA Object flag 08h clear
4, underground Caero 24 42EA Object flag 08h clear

Configuration 1 additionally replaces sound parameter SI with 8800h when object flag 08h is set, while remaining eligible. Do not equate that flag with engine enable or assign it a stall meaning without tracing its writers. Notably, underground Caero uses a different gate from surface Caero and its gate does not read E's engine-switch byte.

tools/verify_demo_engine_audio.py executes all five callbacks with both engine settings and both flag states, before and after the original Jason handler followed by Z. All 20 paired cases return identical eligibility and sound parameters. Thus there is no direct change to this sound gate from Jason+Z. A later frame effect, cached state after bypassing the player update, or another sound source still needs investigation to explain the engine-sound observation. These isolated checks do not reproduce synthesis.

The 1993 patch at 7CE9 still has no identified direct near CALL/JMP target or literal pointer other than the patch writer. The preceding Z handler ends with RET at 7CE8; Enter's boost handler begins at 7CEB. This further supports, but does not prove, a stale-patch explanation. Indirect arithmetic and runtime code changes prevent a universal reachability claim from a static scan alone.

Qualification: early X/C/V storage is craft-dependent

The three bytes 36C9–36CB are shared controls/status storage. For Caero, 36C9/36CA are read as primary/secondary selectors, while the HUD routine also writes the word at 36CB. For Skimma configurations, ordinary number selection at 953E uses those same three bytes, toggling their low bits. Native checks with model slot 26 and mask 0007 verify that selecting 1/2/3 updates 36C9/36CA/36CB respectively. Therefore V is not merely an unrelated display experiment across every craft: it directly modifies the third Skimma selection byte as well. X/C/V force each byte through 0–3, whereas the normal selection path enforces availability and low-bit selection rules. Actual firing effects of every forced state remain unverified.

Flight arithmetic across the three releases

tools/compare_demo_flight.py executes the original instructions with matched synthetic inputs; results are in analysis/demos/flight-comparison.json. The early executable used here is X2, not X or X1.

Component 1993 X2 1995 X Retail
Horizontal integration entry 54DC 6DCA 8208
Vertical integration entry 55E0 6ECE 826E
Horizontal velocity storage Immediate at CS:54E6 SS:[BP+42] SS:[BP+42]
Vertical velocity storage Immediate at CS:55E1 SS:[BP+40] SS:[BP+40]
Vertical-target slice 52F9–5322 6B9C–6BE3 7FD1–8018

All three produce exactly matching velocity and position bytes for 1,000 horizontal and 1,000 vertical test vectors per release, including differing headings, pitch angles, signed velocities, targets and update steps. This is sampled equivalence of these kernels, not proof that whole flight callbacks are identical. The early version's self-modifying velocity immediates are a particularly useful development clue: later code moves those values into the object record while retaining the integration arithmetic.

There is a real difference upstream. The early vertical-target calculation lacks the later downward-pitch adjustment to the power contribution, and handles braking differently. The 1995 and retail slices agree in all 1,012 tested cases, including braking and altitude-assist flag combinations.

For example, with speed input 2000, power input 16000, height 1000 and the unpatched bias of −90, the isolated calculations produce:

Pitch contribution Brake Altitude assist 1993 target 1995 / retail target
−300 Off Off −169 −319
−300 On Off −413 −286
0 Off Off 131 131
0 On Off −113 14
300 Off Off 431 431
300 On Off 187 314
0 Off On 131 14

These are internal vertical targets, not measured speeds or acceleration in physical units. The early slice has no corresponding altitude-assist flag; its last row repeats the same calculation. The harness supplies intermediate pitch/power values directly and retains the executable's initial −90 bias. Craft setup can patch that bias. It does not execute engine-power generation, boost decay, steering, collision response or the complete per-frame callback. Consequently, the agreement between 1995 and retail here does not contradict the observation that those builds feel different in flight.

Next useful checks are complete craft-specific callback traces, including engine-off starts and boost decay, followed by clock calibration. Equal integration arithmetic alone cannot establish equal real-time behaviour.

Boost consumption, engine gating and recharge

tools/compare_demo_boost.py executes the original Caero slices, with results in analysis/demos/boost-comparison.json. It checks 1,000 consumption cases per release, 48 engine-gate cases, 84 recharge cases and 12 repeated decay sequences. These extend the kernel comparisons above but still do not execute complete craft callbacks or reconstruct trajectories.

Consumption is unchanged

The matched consumption slices are 5217–524D (1993), 6ABA–6AF0 (1995) and 7EF3–7F29 (retail). For the tested positive input steps t from 1 to 127:

T = (257 * t) >> 1
q = T >> 1
buffer_spent = min(intermediate_buffer, 16 * t)
boost_spent = min(active_boost, q)
intermediate_buffer -= buffer_spent
active_boost -= boost_spent
forward_target = setting * (2 * boost_spent + (buffer_spent >> 1)) / q
                 + previous_update_extra

Division is unsigned integer division; the final addition wraps to 16 bits. The bounded probes avoid division overflow. Larger input steps need the exact 16-bit MOV CH,CL/shift behaviour rather than extrapolating this formula. The setting is an internal multiplier, not speed in physical units.

Boost therefore contributes 2 * setting while at least q remains. It does not linearly fade with the remaining counter: only the last partial update has a reduced contribution. Smooth changes in velocity occur downstream in the integrator. With zero intermediate buffer, zero extra, setting 500 and active boost 3800h, all three builds give the same sequences:

Input step Updates to consume boost Initial target Last target
1 224 1000 1000
4 56 1000 782
16 14 1000 945
64 4 1000 486

These are update counts, not frames at an established rate or seconds. Enter writes the high byte to 38h; these sequences explicitly start with low byte zero. A retained nonzero low byte can change the final partial update.

Engine off does not cancel a burst

After forming the forward target and calling horizontal integration, all three Caero paths query beacon energy and force it to zero unless the engine byte equals exactly 1. The gate addresses are 525C (engine byte 36D8), 6AFF (4CDC) and 7F38 (4552). Native checks cover values 0, 1, 2 and FFh.

That gate does not clear the active boost counter or existing velocity. This is consistent with boosting out of the hangar with the engine off, followed by loss of sustained flight. The precise descent still requires a complete callback trace. This finding concerns these Caero paths, not every craft's engine semantics.

Retail recharges more generously

For the same nonnegative beacon-energy input E and transformed step T, the unmodified code calculates boost-reserve gain as follows:

P = max(0, E - 250) * T
1993 and 1995: gain = P >> 18
retail:        gain = P >> 17
               if gain != 0: gain += T >> 8

The early slices are 5290–52A2 and 6B33–6B45; the retail slice is 7F6C–7F89. These are gains before reserve accumulation and capacity limits. Retail's cheat patch is not applied. At input step 4 (T = 514):

Beacon-energy input 1993 gain 1995 gain Retail gain
250 0 0 0
1000 1 1 4
6579 12 12 26

Thus retail changes recharge balancing even though the boost-consumption arithmetic remains the same. This comparison supplies equal beacon inputs; it does not establish equal beacon fields, update rates or elapsed recharge times in the three running games.

Steering, launch state and hardware timer comparison

tools/compare_demo_steering.py records its native checks in analysis/demos/steering-startup-comparison.json.

Steering changed even where position integration did not

Helper 1993 X2 1995 X Retail
Angular damping/integration 57DD 7043 83DF
Gain followed by angular response 57CE 7034 83D4
Bounded angular-error response 57B2 701C 83BC

The two demos' first two helpers agree on 2,000 full-width random register vectors per helper. Retail changes rounding in both stages: the demos only apply the gain-stage carry correction when the shifted result is zero, whereas retail rounds with ADD/ADC unconditionally. Retail also rounds damping and angular displacement, and incorporates the high byte of the doubled timestep. These differences matter for faithful arithmetic; the random vectors include unusually large steps and must not be read as a distribution of normal play.

The bounded-error helper changes more visibly. For small angular errors the 1993 path doubles the error before applying the time/gain calculation. Its larger-error branches use further doubling and overflow/parity tests, giving an asymmetric, discontinuous response; it is not a simple modern clamp. 1995 instead limits the one's-complement magnitude at 2000h; retail raises that limit to 2800h. Negative limits retain the extra unit from the one's-complement operation. With zero initial rate, gain 256 and step 16:

Error 1993 turn / new rate 1995 turn / new rate Retail turn / new rate
−8192 −64 / −1024 −32 / −512 −32 / −512
−1024 −8 / −128 −4 / −64 −4 / −64
−16 −1 / −2 −1 / −1 0 / −1
1024 8 / 128 4 / 64 4 / 64
8192 31 / 511 32 / 512 32 / 512
10240 31 / 511 32 / 512 40 / 640

These are internal angular units and helper inputs, not mouse displacement or a measured turn rate. The helper is used by pitch correction and other control paths; it does not mean every player steering input is twice as strong in 1993.

Initial charge and the transition into flight

The initial Caero callbacks are 5183, 6A24 and 7E7F respectively. Both demos advance an accumulator by the input step when the engine's low bit is set; retail advances it by seven times that step and changes the conversion into reserve pips. The apparent sevenfold arithmetic change does not give sevenfold charging: the output thresholds also change.

Starting with zero accumulator and engine on, at step 16 the demos first show a reserve high byte of 32 after 32 updates. Retail still has zero at 32 updates and has 32 by update 40. Engine off leaves the accumulator and reserve at zero in these fixtures. These initial callbacks differ from ordinary beacon recharge.

All three detect a nonzero active-boost high byte and replace their own object callback with the following steady update, using a CALL/POP-return-address trick. Native checks verify replacement with 51CB, 6A6C and 7EB6, including engine-off cases. Thus the launch transition itself does not require the engine to be on. Supplied save/setup state can provide reserves; these zero-state fixtures are not a claim about the initial contents of every demo save.

Same programmed timer rate

Executing the actual startup timer fragments produces the same writes in all three releases: port 43h receives 36h, then port 40h receives 52h and 09h. That selects PIT channel 0 mode 3 with divisor 0952h (2,386), nominally about 500.075 interrupts per second using the existing 1,193,180 Hz reference.

Release Setup fragment Divisor source Programming routine
1993 033F–0346 14DC 15A0
1995 0347–034E 21B6 227A
Retail 0321–0328 1774 1838

A different startup PIT rate therefore does not explain the demos' flight feel. Interrupt handling, elapsed-step limits, frame scheduling and later timer reconfiguration still need comparison before equating measured time throughout all three games. These checks capture port writes, not hardware delivery.

Controlled complete Caero update traces

tools/trace_demo_flight_updates.py executes the complete steady Caero callback and its called arithmetic routines, replacing only the beacon query with a constant-return supplier. analysis/demos/flight-update-traces.json contains 18 traces of 128 updates, covering engine off, darkness, powered flight, roll input, pitch input, and powered flight without initial boost. The demo browser provides a selector and numeric samples.

Each release starts at height 2400 with zero velocities, angular gain 256, setting 1656, default vertical bias −90 and step 4. These deliberately common values isolate code differences; they are not each release's original scenario configuration. The harness does not run the input sampler, collision handling, world dispatcher or mission scripts, and its constant beacon does not weaken as the ship moves. Treat the output as controlled arithmetic experiments, not playable or recorded trajectories.

With initial boost 3800h and no steering input, final values after 128 updates include:

Condition 1993 height / vertical velocity 1995 Retail
Engine off, raw beacon 6579 2189 / −154 2190 / −153 2194 / −152
Engine on, raw beacon 6579 2790 / 345 2801 / 353 2802 / 354

For each release, engine-off with that raw beacon gives exactly the same saved samples as engine-on with zero beacon. Both conditions retain forward movement after the boost is exhausted, while height falls. This directly reproduces the qualitative boost-then-sink behaviour under controlled conditions. Steering experiments also expose the early pitch-correction difference; full traces retain angles, velocities, positions and boost state rather than reducing all differences to a single speed multiplier.

Runtime tuning, elapsed time, Skimma and underground controls

The next comparison is reproducible with compare_demo_runtime_setup.py, trace_demo_skimma.py and compare_demo_tunnel_controls.py. Their reports are linked from the demo browser. Originals remain unchanged.

Actual craft parameters qualify the matched-input experiments

The original definition expanders at 83AF (1993), 9E8C (1995) and BF53 (retail) were executed for all 16 configuration records. The early angular response byte is multiplied by 4, not the later 8. Other expanded motion words are preserved in the report without speculative physical names.

Configuration 1993 gain / initial setting 1995 Retail
0, release-specific 96 / 1704 96 / 1704 416 / 256
1, surface Caero 400 / 1688 416 / 1688 416 / 1656
2, Skimma 360 / 256 368 / 256 352 / 256
3, other Skimma configuration 400 / 256 416 / 256 384 / 256
4, underground Caero 320 / 1704 336 / 1704 176 / 96
5, early-only extra configuration 400 / 1704 — —

These are initial internal values, not measured flight speeds. Configuration 0 changes role between releases; configuration numbering does not guarantee identical behaviour. The early-only configuration 5 uses callback 9C5C, whereas the early underground Caero configuration 4 uses 9CBC. The later underground callback is BE69; retail uses D510. Different callbacks can use or override settings differently, so the underground setting change must not be interpreted as a seventeenfold speed change.

The 1993 surface Caero's expanded gain is 400 versus 416 later, despite its much larger raw byte (100 versus 52). This is why byte comparisons alone, or testing only an angular helper with identical gain, cannot establish the relative responsiveness of the complete configured craft.

The report also preserves the later per-configuration vertical bias bytes: −110, −90, −106, −110 and −90. No equivalent per-configuration field is claimed for the early format; its shared initial −90 instruction remains separate.

Elapsed-time limiting was added after the early demo

All three use the same startup PIT divisor, but their interrupt bookkeeping is not identical:

The later demo writes 80 to its cap at 450E and restores FFFF at 460D; retail does likewise at 3D30 and 3ECE. Eighty source ticks are approximately 160 ms. Thus these later clock paths stop accumulating game time beyond that interval while the frame snapshot remains unchanged. They do not queue an unlimited catch-up step after a stall. This is a plausible contributor to different behaviour on slow emulation, not a measurement of actual stalls.

Native probes cover 216 interrupt/frame combinations, including wraparound, zero increment, elapsed values either side of the cap, and the uncapped FFFF setting. Early output reflects the whole elapsed interval; later output is capped as described. Peripheral interrupt work and input sampling remain outside these probes.

Skimma changes are more than different configuration bytes

Complete callback traces now cover entries 53EE, 6CD7 and 8108. The browser includes 12 matched-input Skimma experiments, each 256 updates, with neutral controls, braking, roll input and pitch input. All use setting 256, angular gain 256, initial height 2400 and bias −90 to isolate code changes. These are deliberately not original Skimma configurations or recorded gameplay.

Static instruction comparisons explain several differences:

These changes interact with fixed-point rounding. In the low-speed synthetic retail fixture, the position word can remain unchanged over many updates while velocity increases; fractional and product truncation must be retained. Do not convert the timestep change into a blanket “retail Skimma is half as fast” claim.

Underground is a separate control system

The early Caero tunnel callback at 9CBC smooths both control inputs, computes a roll target, and queries tunnel geometry at 9A51 before steering and movement. The later BE69 callback uses a different geometry query at BB24 and separate heading/pitch response calculations. Retail's D510 adds a wrapper with state-bit tests, reserve handling and an auxiliary angular-state object at SS:0F40, before its main path at D5C9. These are substantial code changes, not just new maps or altered craft constants.

The smoothing helpers also differ. Early 9D44 scales input by 16 and divides by timestep, limiting the result around ±384. Later BF78 scales by 64 and limits magnitude at 1536. Retail D86B receives already-prepared input and limits magnitude at 3072; its helper does not perform that division. Equal AX inputs therefore do not mean equal mouse movements across these paths.

For illustration, input 64, step 16 and zero previous output produces:

Helper First four successive outputs
1993 16, 32, 48, 64
1995 64, 128, 192, 256
Retail 64, 64, 64, 64

The report contains 720 native helper calls. Complete tunnel trajectories remain unverified: they need the geometry-query results, control preparation and collision paths, rather than transplanting the surface Caero fixture.

Preliminary fog / field-of-view comparison

A targeted check found the distance-shade table generator's arithmetic loop byte-identical across 1993 (7C41–7C69), 1995 (98FA–9922) and retail (B756–B77E). The corresponding setup calls at 82B3, 9D6A and BD03 supply SI=28 and BL=1, with CX=60 normally or 28 on the configuration>=4 branch. The associated setup byte is 15 or 8 in all three. This supports shared shade-ramp generation and configuration-dependent settings; it does not establish identical world-distance scaling, draw cutoff or final colours. The early/later configuration discriminator is F002, retail F003.

Field-of-view equivalence has not yet been established. Existing native vertex checks install synthetic projection coefficients, so they cannot verify the original camera's focal scale or viewport. The shared modern city viewer is also not evidence for original-game FOV or fog distance. Trace original camera setup and distance-to-shade selection before asserting complete equivalence.

X and X1 startup workaround

Disabling GLX in the private Xvfb server resolved the earlier host-side testing problem. With otherwise matched DOSBox settings, original X and X1 remain at the splash while original X2 reaches credits. Enabling DOSBox GUS alone did not release X1. Captures are under analysis/demos/startup/noglx/.

Interrupting the original executables in the DOSBox debugger identifies the actual stalls (traces in analysis/demos/startup/debugger/):

Executable Repeating instructions
X E24: IN AL,42h; E26: INC AL; E28: JNS E24
X1 EEC: IN AL,42h; EEE: INC AL; EF0: JNS EEC

Both sampled runs repeatedly read 54h from PIT channel 2; incrementing it to 55h leaves the sign flag clear, so the loop never exits. This is a hardware delay inside the startup sound probe, not the splash waiting for a key. Setting port 61h bit 0 in a small COM helper before launch did not fix it; the exact earlier timer/gate setup and emulator interaction are not yet fully explained. Do not describe that pre-launch helper as a working fix.

The working X2 build already omits CALL F04E. Applying that same omission to X/X1 is sufficient to reach credits, the game-selection menu and the first saved game locally:

Rebuilt output Original image offset Change
XFIX.EXE 63C7 CALL F04E → three NOP bytes
X1FIX.EXE 648F CALL F04E → three NOP bytes
X2RAW.EXE None Unmodified-image reconstruction control

tools/build_demo_startup_workaround.py validates the packed and unpacked input hashes, recovers the original relocation/startup metadata, reconstructs DOS MZ files and records hashes and changes in WORKAROUND.json. It refuses to overwrite an existing output directory. No original executable, resource or save is modified. The bypass skips that sound detection/initialisation path; it is not a repaired hardware driver. X's extra INT 33h device probe remains intact and does not block the tested startup.

The ready playtest copy is /tmp/darker-1993-playtest. In DOSBox, run STARTX.BAT or STARTX1.BAT. The original files remain alongside the patched ones. Recreate it in a new directory with:

PYTHONPATH=tools python3 tools/build_demo_startup_workaround.py --output /tmp/new-darker-playtest

Startup screenshots and run manifests are in startup/probe-bypass/ and startup/playable/ beneath analysis/demos. X2RAW reaches the expected credits too, providing a control for the rebuilt executable format. Passing startup is not evidence of complete gameplay or audio equivalence between X, X1 and X2; the working copies now make that comparison possible.

Both patched binaries were additionally exercised with Escape, Escape, number-row 1, Return. Captures in analysis/demos/startup/selection/ show the first save loaded into its cockpit/hangar in both cases. This verifies initial gameplay entry, not prolonged flight, every save or audio output.

Complete X / X1 / X2 decoded-image comparison

tools/compare_early_demo_builds.py audits all three 64 KiB decoded images. The reproducible report is analysis/demos/early-build-normalization.json. Both comparisons finish with zero unexplained bytes, after accounting for explicit layout changes and address expressions. This is stronger evidence than comparing disassembly text or ignoring all immediate operands.

The substantive differences are:

Pair Change
X → X1 Remove CALL F45F at X:F517, the extended INT 33h device probe
X1 → X2 Remove CALL F04E at X1:648F, the sound-hardware probe that stalls locally

The rest of the decoded changes are explained by layout:

The normalizer retains numeric operands and accepts changed words only when an explicit address-map equation explains them. It records address words, negated address displacements and near CALL/JMP displacements separately. Two less obvious relocation forms required tracing:

  1. X:F680 holds 57CFh, while X1:F67D holds 570Ch. This is not a changed gameplay constant: the value patches the relative CALL operand at 9EBF / 9F7F. Adding it to the respective return address reaches the corresponding routine at F690 / F68D in each build.
  2. The table at X:F6BC / X1:F6BA contains relative linked-patch offsets, not ordinary absolute pointers. Executing the original walker at F000 produces 42 writes in each image. All write destinations and encoded targets agree under the address map.

The audit checks the inserted/deleted bytes explicitly and verifies complete, non-overlapping source and destination coverage for the X/X1 image comparison. Every normalized word and renderer write is available in the JSON report. This is structural evidence, not a formal proof of every runtime state; packed-loader implementation and external device behaviour are outside scope.

No additional gameplay, physics, rendering algorithm, model/resource definition or scenario-selection change was identified in these decoded binaries. The observation that the working copies look alike is therefore consistent with the code. These appear to be hardware/startup variants of the same game snapshot, rather than three progressively developed gameplay revisions. The extended INT 33h device identity remains unresolved; its effects could still matter with the intended driver/hardware present. The binaries alone also do not establish their chronological build order.

Distinct demo scenarios and stored presentations

tools/analyse_demo_scenarios.py exports scenario-analysis.json and the demo browser now has a distinct-script selector. All 195 framed records reduce to 17 distinct byte sequences: eight in 1993 and nine in 1995. Each release contains 90 identical copies of its 14-byte configuration-1 placeholder. Those copies contain no setup objects, beacon sequence or damage-cell entries, and both entry scripts immediately stop. They are reserved slots, not evidence of ninety additional missions. Other small records select different configurations or initial positions.

The most substantial stored sequence is 1995 00_054 / 0, a 1,382-byte surface scenario with thirty setup objects. Its player script includes:

Between these messages are delays, a conditional checkpoint loop, 24 horizontal/vertical beacon runs, and nine operations on the supplied beacon sequence. This is a scripted surface presentation rather than just an unused briefing string. This is the visible fifth menu entry NIGHTMARE: selecting it sets the slot-4 sentinel and enters the Run menu. “Run this game” then selects this surface sequence. A GLIMPSE UNDERGROUND separately selects 00_054 / 1, the short underground scenario. The previous description of the surface sequence as only a stored presentation omitted this normal menu route; it is accessible without a patch.

Beacon-run operands encode a coordinate on the nine-cell beacon lattice and a run count. The handlers select horizontal or vertical traversal, not restoration versus shutdown. In 1993 the high operand byte packs count and fade direction. In 1995 direction is shared runtime state: the updater XORs its ramp with a byte initially FF, and another opcode toggles that byte. The map-state writes traverse eighteen bytes horizontally or 2,304 bytes vertically (nine two-byte cells or nine 256-byte rows). The presentation's text and programmed sweeps therefore agree with a staged city blackout, although this report does not simulate the complete presentation.

The early counterpart 1993 00_030 / 0 has eight setup objects, timed radio messages, three next-beacon operations and a three-beacon vertical run whose packed direction is ascending. It includes the unfinished spelling “They're in a strange, half-writeen formation”. The first ordinary early save still has the six-object radio-joke script documented above.

1995 00_042 / 0 contains seven setup objects and stored enemy-name text including Skimma Supa, Stalker, Assassin and Terrablitz. Its decoded player entry changes one byte and stops; the name text is not reached by that linear entry. Printable text is retained separately so it cannot be mistaken for an executed radio timeline.

Scope: setup layouts and operand sizes come from the original loaders and opcode handlers. The browser decodes header, player and stored object entry points, retaining native handler addresses, raw bytes and checkpoint edges. It stops at mode changes and at the branch-dependent altitude opcode rather than inventing a continuation. Object callbacks may later resume scripts; those paths, collision/AI events and full runtime reachability are not yet covered. No original executable, data pack or save file was changed.

Verified Nightmare menu connection

tools/demo_scenario_setup.py now executes the missing menu branches in both original decoded images, in addition to the existing scenario-selector checks:

Step 1993 X2 1995 X
Store selection 4 and switch to menu state 1 088A 0895
Caption branch displaying NIGHTMARE 099B 1436
Run action dispatch, action index 0 06C1 06C9
Game entry 7EA6 4462
Selected scenario 00_030 / 0 00_054 / 0

The four on-disk save records and the five visible choices were previously conflated. The fifth choice has its own hardcoded caption and special selector value. In 1995, NIGHTMARE INSTRUCTIONS is a different, ordinary information entry; it should not be confused with NIGHTMARE itself.

Native checks assert the selected byte remains 4, the menu state becomes 1, the caption branch reaches the literal NIGHTMARE, and Run dispatch reaches the game entry. Input polling, text drawing and full scenario simulation are outside this focused check. The report records these checks under nightmare_menu_checks; the browser now names the special selection explicitly.

Model reference audit and Dual Launch comparison

tools/audit_demo_content.py exports content-reference-audit.json. The demo browser now provides a bank selector, model links, reference details and a filter for models without an audited reference. The existing model browser's “unused on maps” badge retains its narrower, literal meaning.

The audit covers all seven maps, all seventeen distinct scenario setups, player configuration definitions, geometry aliases, exported states and city damage/alternate chains. Special model N+1 corresponds to executable definition slot N: bank loader loops at early 1928–1932 and later 26FB–2705 install the special-directory pointers at 16- and 22-byte strides. Special-object headers are not treated as city damage/alternate chains. All audited city link traversals completed without invalid-pointer errors.

The 1993 Surface A satellite dish, city type 13, has 88 faces and still has no audited reference. It is absent from the map, and its pool pointer 0A85 is not reached by a scenario special model, geometry alias or city state chain rooted in the audited content. This strengthens the unused-content hypothesis without proving no executable path can display it.

Conversely, 47 definitions in the 1995 underground bank have no direct map/scenario reference but share geometry reachable from referenced models through aliases or state chains. They should not be called discarded assets. The early fourth map has no player/scenario configuration among the decoded records; its many otherwise unreferenced special definitions partly reflect that absence. Counts of unreferenced definitions are not counts of unique removed objects. Dynamic projectile spawning and resumed object scripts remain outside this reference graph.

tools/verify_demo_weapon_paths.py executes 30 original instruction paths for the Dual Launch primary and follow-up firing handlers. It stops before projectile allocation, physics or audio:

Thus the paired-shot selection mechanism already exists in 1993, while the 1995 version adds explicit target-identity validation at this gate. This is not proof that ordinary 1993 gameplay supplies an incorrect candidate: the upstream candidate-list construction and subsequent detonation behaviour need separate tracing. See weapon-path-verification.json for the controlled inputs and outcomes.

One further static difference appears in the chargeable release path: later B4D7–B4EA contains direct PC-speaker port-61 input/output and a delay loop before projectile launch. The corresponding early 9651–966A path has no such block. This is a local speaker operation, not evidence of an additional music engine; its audible result and timing have not been tested here.

Direct PC-speaker operation on charged release

tools/trace_demo_speaker.py follows the original firing dispatch at B440, with weapon selection 9, rather than jumping straight into the port loop. Twelve controlled traces cover held/released triggers, previous trigger state, charges 0, 15, 16, 160 and FFFF, and six initial port-61 values. Port accesses are intercepted; no host speaker output is generated. Execution stops before projectile allocation at B533.

A nonzero released charge after the four-bit reduction enters B4D7–B4EA. Holding the trigger follows accumulation instead. No current or previous trigger produces no release. Along the successful dispatch/release path, the explicit data reads outside instruction operands are the weapon cost and callback, weapon energy, current trigger state, free-object availability and the player flag in SS. There is no sound-device-selection gate on this path. The operation is independent of the ordinary sound enqueue routine; a configured sound card therefore does not inherently suppress these writes. This is a firing-path result, not a full sound-driver startup/playback test.

The captured sequence is quite specific:

  1. Read port 61h and XOR bit 1.
  2. Write that value repeatedly, initially 256 times.
  3. Repeat with the opposite bit value, then 255 writes, then 254, down to 1.

That is 256 toggles and 32,896 OUT instructions. The inner loop overwrites CH, which explains why this is a fixed sequence despite CX being used as the outer LOOP counter. Tested released charges all produce the same pattern; charge changes projectile parameter DX, not the chirp length. Other bits are preserved from each read. In the controlled static-port model, the even number of toggles restores the initial port byte. Actual hardware status bits can change independently, and speaker/PIT configuration affects audibility. The decreasing software delays suggest an accelerating pulse train; its pitch and duration depend on CPU/emulator timing. No calibrated waveform or recording is claimed.

This is deliberate signal-generating code, but its design intent remains uncertain. Equivalent IN AL,61 / XOR AL,2 loops occur at early 58D2, 8745, 980E and later B737, BB54, F5DD, in addition to the charged release at B4DB. The later F5D9–F5ED block repeats its entire pulse sequence indefinitely, which makes reuse as development/error feedback plausible. That does not prove the weapon instance was a debug leftover. The other sites' complete callers have not been classified here.

The retail chargeable handler CA7D has no corresponding pulse loop: after reducing/resetting charge it checks the target and proceeds toward launch. The exact four-byte IN/XOR pattern occurs nowhere in the retail decoded image (a pattern result, not a claim that retail never accesses port 61). Full inputs, accessed addresses and one complete run-length sequence are in speaker-path-verification.json.

Reconstructed speaker audio previews

tools/render_demo_speaker.py now executes the finite early 58CE–58E2 and later B4D7–B4EB loops, timestamps each port transition and renders eight 48 kHz mono PCM WAV previews. The demo browser has timing and playback-rate selectors, including quarter-speed playback with pitch preservation disabled.

Timing assumption 1993 loop 1995 loop
386 16 MHz, no added waits 43.545 ms 43.624 ms
386 25 MHz, no added waits 27.869 ms 27.920 ms
386 33 MHz, no added waits 21.113 ms 21.151 ms
386 25 MHz, extra 1 μs per I/O 61.021 ms 61.072 ms

These are model estimates, not machine measurements. Real-mode timings come from Intel's 80386 Programmer's Reference Manual, chapter 17: IN immediate 12 clocks, OUT immediate 10, register DEC/MOV/XOR/SUB 2, register PUSH 2 and POP 4, conditional jump 7+m taken / 3 untaken, LOOP 11+m. The next instruction's component count is evaluated for each branch. Edges are placed at estimated OUT completion. No prefetch stalls, interrupts, memory waits or DOSBox cycle model are simulated. The added-I/O-delay profile is explicitly an illustrative sensitivity case, not a claimed motherboard.

The reconstruction assumes timer-channel-2 output is high and that bit 1 starts low, exposing the software pulse sequence directly. Other PIT states could modulate or suppress it. The IBM AT Technical Reference is the hardware reference; no PIT carrier frequency has been invented. The pulse train starts around 1.48 kHz at 16 MHz and accelerates rapidly, reaching above the audio band near its end. The early and later forms differ slightly in register-save overhead, not in their 256-toggle pattern.

Each profile exports two files:

Both use 16× oversampling and polyphase antialias downsampling. There is 150 ms leading silence and at least 350 ms trailing silence. The listening filter is an explicit acoustic assumption, not a correction to the native port trace. No per-file normalization or synthetic pitch envelope is applied. All eight previews loaded successfully in Chromium; their timing selector and quarter-speed setting were checked. Generator assertions verify the 256 reads, 32,896 writes, 256 transitions and return to the initial bit state. The audio manifest includes every edge time, timing inputs, peak levels, sources and file links.

Closing follow-ups: hit cue, Nightmare routes and Dual Launch impacts

Skimma speaker condition

During playtesting, we heard the rising cue while firing at buildings, possibly a light tower on the early Halon-style surface. Ships and bare ground were not tested interactively. Native result-path checks now support a more specific rule:

Raycast result 1993 chirp 1995 chirp
Miss no no
Ground-plane interception without another hit yes no
Building hit yes yes
Dynamic object hit yes yes

The original aggregation routines are 9443 and B064. A scene collision or object-list collision returns carry set. The ground-plane clipping return (8CEE/A8D8 helpers) can return a nonzero AX with carry clear; an ordinary miss returns zero. Early caller 9806–9808 accepts carry or nonzero; later B731 accepts carry alone. The callers save flags across effect creation before making that decision. The chirp does not inspect damage dealt or require that the hit object is destroyed. No building-type/light-tower exception is present in this final cue decision.

tools/trace_demo_followups.py verifies twelve combinations using the original aggregation and caller instructions. Geometry helpers receive controlled results and visual-effect allocation is stubbed. This verifies propagation and the sound predicate, not collision geometry against every model. The later Skimma firing routine B719 creates an effect at the raycast result and plays the cue; that local routine does not call the projectile damage handler. That observation should not be generalized into a claim that all Skimma weapons in the demos cannot deal damage.

Nightmare timing and vehicle route correction

tools/trace_demo_nightmare.py runs both original player-script dispatchers, message setup and beacon scheduling. The later message sequence is:

Tick Approximate seconds at nominal clock Message
0 0 Tolly! - Are you alright?!
3,500 7 Enemy Persuit Detection box has failed
6,250 12.5 It may have overloaded...
8,500 17 Halon craft swarming from all directions!
53,500 107 As the lights fade in this Delphi city...
63,100 126.2 ... it's time to face the reality...
67,700 135.4 DARKER's much more than you dreamed of!

The checkpoint loop is a timed wait, not a condition requiring the player to kill enemies. The native script then schedules 24 descending beacon runs and nine individual descending fades. The final altitude opcode consumes its word on the tested original flag state and reaches STOP at record offset 02CA. Earlier linear decoding correctly left that branch unresolved. These clocks survive the 16-bit wrap; they are script timing, not measured wall time under DOSBox load. The early Nightmare player script completes at tick 20,200, with the previously documented four radio messages and beacon changes. Full events and executed command offsets are exported in nightmare-timelines.json.

Correction to the first scenario export: the later compact object records use ground-route callback 7500, not the ordinary scenario dispatcher. Their bytes must not be interpreted as radio, delay or object-mode opcodes. The decoder and browser now distinguish these contexts. Ordinary opcodes 00/01 also consume a coordinate word plus flags; their sizes are corrected.

1995 Nightmare has twenty ordinary flying-object records and ten ground vehicles (object indices 21–30, definition slots 30–32, special models 31–33). The flying records point to STOP at 02CB and otherwise use their normal AI callback; the player timeline contains no activation commands creating staged waves. The ground vehicles carry actual route programs:

The demo browser plots each of the ten tracks on the surface map, using original movement instructions and sine tables. Each phase's start and end are executed so turns commit their heading. Traces stop at destruction scheduling or the first repeated route address; collision, combat, vehicle firing and subsequent lifecycle updates are excluded. Dummy model height zero affects vertical placement, not the plotted horizontal route. The lines join phase-start samples and do not reproduce the full turning arcs.

Dual Launch behaviour before retail

The definition callbacks differ materially:

Release Primary update Follow-up update Both impact callbacks
1993 974C 974C 97C1
1995 B673 B642 B772

The early pair shares the ordinary homing updater. The later follow-up has a dedicated distance-dependent term:

min(200, (horizontal_distance_helper_result >> 1) + (abs(height_difference) >> 3))

Thirty native cases verify that term before common movement integration. It is smaller near the target capsule: this is an approach-speed adjustment, not the retail proximity-triggered paired area attack. The early updater contains no corresponding special adjustment. The examined demo callbacks therefore do not implement the retail paired-detonation mechanism; other possible interactions through generic collision are not excluded.

Six additional native 1995 impact checks cover primary/follow-up capsules against no classified hit, an object and a building. Both use the same B772 handler. The building branch ORs 20h into the cell's mutable state; the object branch increments the target's damage accumulator using the projectile and target parameters. Primary and follow-up strengths differ, but neither branch contains a special paired-capsule explosion. Tests use fixed RNG for impact perturbation and stop before visual-effect allocation. The 1993 shared impact callback selects an effect recipe; its complete downstream collision/lifecycle consequences are not simulated in this pass.

All cases, callback addresses and route samples are in followup-verification.json. The native checks pass, and Chromium checks covered both timelines, all ten route selections and all seventeen distinct scenario selections.

Nightmare victory wording: demo comparison

The retail Nightmare victory page (04_015 / 4) congratulates the player and then invites them to try the “ultimate challenge” of the normal campaign. Its promotional tone suggests a possible demo connection, but the available 1995 demo does not contain that message: searches of its unpacked X executable, all 55 decoded resources and the extracted scenario records found neither the victory paragraph nor its distinctive phrases (“You are victorious”, “Your darkest fears”, “ultimate challenge”). This does not establish when the retail wording was written or exclude another demo/build.

The 1995 demo does have a playable Nightmare mode, selected through its separate fifth menu entry. Its surface script (00_054 / 0) contains different promotional messages during play. The existing native dispatcher trace records “As the lights fade in this Delphi city...” at tick 53,500, “… it's time to face the reality...” at 63,100, and “DARKER's much more than you dreamed of!” at 67,700 (107, 126.2 and 135.4 seconds at the nominal 500 Hz clock). See Nightmare timings and the demo browser. These are timed in-game messages, not the retail fixed-frame victory presentation.

The demo connection is therefore plausible editorially, but direct reuse of the retail victory message from this 1995 demo is not supported by the surviving files. The message is also consistent with retail treating Nightmare as a standalone bonus challenge and inviting the player into the main campaign.