# Darker demo archaeology

The shared [demo browser](../analysis/demos/index.html) 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](https://github.com/torvalds/linux/blob/master/include/sound/gus.h).
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](#x-and-x1-startup-workaround).


## Hidden commands and keys

Both canonical demos have exactly these three command records:

- `The Jason Brooke special`: writes a byte of executable code.
- `Lyndon's little cheat`: handler is just RET.
- `Jesus gives true life`: handler is just RET.

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:

- Early T/Y/U/I have effect-generation handlers, consistent with READ.ME;
  these event slots are no-ops in 1995.
- Early X/C/V cycle three bytes modulo four. They are not the retail Level X
  shortcut. Later X/C/V are no-ops.
- Early Z toggles player flag bit 1 directly. Later Z has the behaviour above.
- Early speed-setting constants are 130/300 and boost 800; later 250/500 and
  boost 600. These are code constants, not calibrated world-speed units.
- Keypad minus cycles a value in steps of eight, from 8 to 40; the later
  README calls it keypad plus. Full hardware/display meaning needs tracing.

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](../analysis/interface/index.html). 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](model-animation-and-states.md#1995-demo-fountain-parameter-slots).

## 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`.

- **1993 X** cycles primary selector `36C9` through 0–3.
- **1993 C** cycles secondary selector `36CA` through 0–3.
- **1993 V** cycles HUD byte `36CB` through 0–3. The normal routine at 4768
  also overwrites the word beginning there; its precise display semantics
  remain to be resolved. It is not established as a persistent gameplay mode.

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:

- 1993's 0CF9 path increments clock CFA and carries wrap into A724. Its frame
  code at 75ED subtracts the previous snapshot at 75CA and supplies the delta
  in CX. No elapsed-time cap exists in this interrupt slice.
- 1995's 1783 path computes elapsed time relative to snapshot 178C, clamps
  it to the word at 178F, then reconstructs the clock at 1784. The frame
  path at 929E writes the difference into dispatcher immediate A390.
- Retail uses the same capped arithmetic at 0BEF, with clock BF0, snapshot
  BF8, cap BFB and dispatcher immediate 7A2B.

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:

- Braking substitutes **60** in 1993 at 54B0, versus **130** at 6D9F and
  81D8 in the later releases, before adding the speed-derived term.
- The 1995 heading calculation at 6D74–6D7C doubles its signed product twice;
  1993's corresponding 5489–548D doubles it once. Retail keeps the later form.
- Retail changes a speed-dependent roll-response quantity from `>> 6` to
  `>> 5` (8131), while retaining the broader speed-squared calculation.
- Retail halves CX specifically around horizontal integration (81E2–81EF).
  Both demos send the unhalved transformed step to that helper. It restores
  CX before the vertical update, so this is not a global half-speed clock.
- The demos' Skimma vertical product is doubled before entering the shared
  vertical-target tail. Retail instead uses a term derived from half the
  forward velocity plus 0200h, without that final product doubling.

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:

- X1 inserts 200 zero bytes at 026E, shifting the following code/data by C8h.
- It removes eight bytes of `MOV AX,AX` filler at X:65D5, leaving the subsequent
  shift at C0h. Zero padding before the fixed F000 section contracts accordingly.
- Removing the device-probe call shifts its following startup/video code back
  three bytes. A later NOP insertion leaves its tables shifted back two bytes;
  padding restores the fixed FC00 interpreter area.
- X2's removed sound call shifts a short region back three bytes. Three inserted
  zero bytes restore the later layout. Its remaining changed operands track
  those relocated addresses.

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`](../analysis/demos/scenario-analysis.json) and the
[demo browser](../analysis/demos/index.html) 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:

- “Tolly! - Are you alright?!”
- “The Enemy Persuit Detection box has failed” and “It may have overloaded...”
- “Halon craft swarming from all directions!”
- “As the lights fade in this Delphi city...”
- “… it's time to face the reality...”
- “DARKER's much more than you dreamed of!”

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`](../analysis/demos/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:

- Early primary handler 966D and later B4EE switch primary selection **3 → 7**
  when their incoming firing flags permit launch.
- Early follow-up handler 967E accepts any **nonzero** candidate pointer at
  `[DI+2]`; it does not inspect that object's definition.
- Later handler B4FF requires a signed-high object pointer and checks the
  candidate's definition pointer against **2425**, definition slot 2—the
  primary capsule. A different definition is rejected.
- Both reset selection **7 → 3** when firing the follow-up or rejecting an
  invalid candidate. A valid candidate with the trigger released leaves 7
  selected. Accepted firing temporarily substitutes the capsule pointer as
  the projectile target.

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`](../analysis/demos/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`](../analysis/demos/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](https://read.seas.harvard.edu/~kohler/class/aosref/i386.pdf):
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](https://www.minuszerodegrees.net/manuals/IBM_5170_Technical_Reference_6280070_SEP85.pdf)
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:

- Listening WAV: quiet fixed gain, illustrative two-pole 150 Hz high-pass and
  two-pole 8 kHz low-pass response. This makes an audible approximation of a
  small speaker, **not a measured response from a particular PC**.
- Electrical-drive WAV: the unipolar bit waveform, with antialias resampling
  only. Its DC component is intentional; it is not an acoustic recording.

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](../analysis/demos/speaker/manifest.json) 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`](../analysis/demos/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:

- Route phases advance every **4,096 game ticks**, approximately 8.19 seconds.
- Commands include movement, turns, stationary phases and relative branches.
- Objects **23, 24, 25, 26 and 28** reach an explicit destruction-scheduling
  command, after respectively 61, 61, 38, 42 and 96 phases. Native B20B sets
  object flag 20 and a destruction deadline 256 ticks ahead.
- The other five traces encounter repeated route addresses. A repeated
  address does not necessarily mean the vehicle returned to its starting cell.

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`](../analysis/demos/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](../analysis/demos/nightmare-timelines.json) and [the demo browser](../analysis/demos/index.html). 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.
