# Animated geometry and linked model states

2026-09-18. [Model atlas](../analysis/models/index.html) ·
[City explorer](../analysis/city/3d.html) ·
[Native geometry, motion coefficients and state graph](../analysis/models/model-states.json).

## Results

The previously unsupported Delphi gate types 17/19/21/23 and fountain types
120–123 now decode. All 5,454 placed Delphi cells and all placed cells in the
other nine maps have decoded geometry. This is drawing-command coverage, not
completion of mission state, visibility, collision or original rasterization.

Following both city-model header links discovers 27 definitions without their
own city-directory entries: 22 in bank 30, one in bank 31, four in bank 32.
The complete city graph has 154, 60 and 132 distinct definitions respectively.
All parse successfully. Deliberately empty definitions remain empty.

The atlas includes a **Linked alternate / damage definitions** category and a
**Model state** selector. State choices with the same target are collapsed.
**Gate extension** and **Fountain clock** inspect recovered parameters; the
fountain plays at 500 source-clock units per second: a 4.096-second cycle,
calibrated to the video observation of slightly over four but under five
seconds. This replaces the initial 50-unit inspection speed. The original timer
rate has not yet been independently established; playback advances by elapsed
time rather than by rendered frame count. OBJ downloads contain the default pose; JSON preserves animation.
The city controls apply chosen high state bits globally for inspection, not as
an assertion about an actual mission's destruction or gate state. Camera,
selection and isolation survive pose changes.

## Gate commands: saved anchor and interpolation

The ordinary FCxx drawing interpreter dispatches:

| Opcode | Handler | Recovered operation |
| --- | --- | --- |
| `32 index` | `31B2` | Load parameter through `319E`, shift right once, save anchor |
| `35` | `31BB` | Replace anchor, retaining interpolation parameter |
| `38` | `3215` | Emit an additional interpolated vertex; retain coordinate registers |
| `3B index` | `334A` | Replace the current C-axis contribution with the parameter value |

Parameter lookup is `table[((cell_state & 31) + index) & 255]`, with the word
table at SS:7935. The city-state high bits select model definitions separately.

For a world-space inspection mesh, gate vertex interpolation is:

```
factor = (uint16(parameter) >> 1) / 32768
vertex = anchor + (current_point - anchor) * factor
```

`38` emits a new vertex: it does not move or replace the preceding one. That
cursor distinction is essential for subsequent polygon indices. `35` establishes
a second anchor for another group of vertices. The native implementation works
on depth and projected-coordinate accumulators with fixed-point intermediate
rounding; the viewer uses world-space interpolation with modern projection.

The landing routine writes parameter zero, increasing it by `step << 6` and
capping it at E800 in one path (`C656–C66E`). Thus the ordinary game range stops
at an interpolation factor of 0.90625, not 1. Other paths derive the parameter
from landing/takeoff state and approach distance. The inspection control is
labelled extension rather than assuming a universal open/closed state mapping.

## Fountain command: height replacement, not general scaling

The earlier description of `3B` as a scaling transform was incomplete.
`334A` multiplies the selected signed parameter by each C-axis projection
coefficient and replaces that axis's accumulated contributions. In native model
coordinates this sets C; it is not a multiplier applied to the existing vertex
height. Since the viewers use Y = -C, negative C is above the model origin.

Types 120–123 use parameters 9–14. `DB1A–DB69` computes these six values from
clock word 0BF8 and the stored sine table at 944A:

1. Initialize all six parameters to -384 (FE80).
2. Compute `phase = ((clock << 5) & FFFF) * 6`.
3. The high word selects a band, 0–5. The low word shifted right seven indexes
   the original 1,024-entry signed sine table.
4. Add `floor(amplitude[band] * sine / 65536)` to that band's height.
5. Subtract `floor(2 * amplitude[band+1] * sine / 65536)` from the next band's
   height, wrapping the destination from band five to band zero.

The stored amplitude bytes are 118, 154, 136, 112, 85, 60, 118. The seventh byte
supplies the wrapped band's amplitude. The complete phase cycle is 2,048 source
clock units. This produces a travelling height ripple across the mesh; it is
separate from the sphere-like screen discs in pool types 101–104.

## Alternate and damaged definitions

Only city-model headers are traversed as link records. Special-object headers
share the drawing-body offset but their first words are not city damage links;
blindly treating them as such yields invalid pointers.

Both city links are signed offsets relative to the current header: +0 is the
next damage definition, +2 the alternate definition. Selection follows the
alternate link first if state bit 80 is set, then follows the damage link
`(state & mask) >> 5` times. The mask is 20 for bank 30 and 60 for banks 31/32.
The original selection routine and installed bank masks verify this order.
Selected header heights also feed city placement; variants retain their own
height instead of inheriting the base definition's height.

`model-states.json` records every reachable city's links, directory aliases,
state destinations, native faces/lines/discs, transform commands and animation
coefficients. `linked-manifest.json` lists the 27 additional atlas entries.
Per-definition OBJ files use `bankN_linked_OFFSET.obj`. Existing type and special
OBJ names remain available. Original resources remain unchanged.

## Verification and limits

- [Parameter/state checks](../analysis/models/animation-parameter-verification.json):
  all 12,288 fountain height words across a full cycle match original x86
  execution exactly; all 3,128 high-bit city-state cases also match exactly.
- [Animated projection checks](../analysis/models/animation-projection-verification.json):
  ten animated definitions (including two linked gates), six parameter/clock
  combinations and two identity-axis projections. All emitted positions agree
  within one screen pixel of the original ordinary FCxx interpreter.
- [Vertex regression checks](../analysis/models/vertex-verification.json):
  496 polygon-bearing directory/special/extra entries, 35,112 vertex events;
  static comparisons remain exact, animated comparisons allow one pixel.
- [Browser animation checks](../analysis/models/animation-browser-check.json):
  visible pose changes, animation playback, 27 linked-model selections, state OBJ
  links, 24 map/state combinations and camera/selection preservation; no JS errors.
- Existing city checks still pass for all ten maps, GPU picking, orientation,
  isolation, movement and responsive rendering.

The modern viewers still flatten visibility branches and use their existing
inspection renderers. Near-plane/clipped FE-path rounding, DOS painter ordering,
distance shades, pixel-exact rasterization, mission-specific parameter scheduling
and actual game playback speed are not reproduced by these controls. State
inspection does not implement collision or destruction gameplay.

## Reproduction

```
python3 tools/extract_models.py
python3 tools/extract_model_states.py
python3 tools/inspect_model_components.py
python3 tools/model_viewer.py
python3 tools/city_scene.py
PYTHONPATH=/tmp/darker-python python3 tools/verify_animation_parameters.py
PYTHONPATH=/tmp/darker-python python3 tools/verify_model_animation.py
PYTHONPATH=/tmp/darker-python python3 tools/verify_model_vertices.py
NODE_PATH=/tmp/darker-browser/node_modules node tools/verify_model_animation_browser.js
NODE_PATH=/tmp/darker-browser/node_modules node tools/verify_city_browser.js
```

## In-game identifications and default beacon display

Gameplay and model inspection confirmed the following:

- Types 17/19/21/23 are directional hangar entrances, paired with landing-light
  models 18/20/22/24. In this bank only the placed orientations 17/18 and 21/22
  have distinct alternate definitions. The gate alternates expose interiors;
  the light alternates indicate activation/permission to enter, not interiors.
- Type 26 is a tunnel entrance; its alternate definition exposes the tunnel
  mouth's interior.
- Type 1 is the energy beacon. Its base model is off; its alternate has the lit
  top surface. The off disc uses fixed colour 0D, while the alternate disc uses
  dynamic token 1C. The model browser now labels these states by their observed
  meanings, without assigning those meanings universally to bit 80.

The city viewer defaults to **Beacons lit**, selecting state 80 for Delphi type
1 only and leaving other cells in state zero. **All base states (beacons off)**
restores the former view. Lit beacon discs use palette index 27, RGB (254,255,255),
the bright end of their dynamic shade range (equivalent to dynamic input 31 in
our recovered colour lookup). This matches the normal in-game appearance;
it is an inspection setting, not a claim to reproduce runtime intensity, faults,
flicker or charging state. Other dynamic discs retain their existing shade.

### Warehouse and home hangar: type 77

Visual inspection identifies bank-30 type 77's base model as a generic warehouse, and
its alternate as the populated player starting/return hangar. The atlas now
labels these “Warehouse exterior” and “Player hangar / populated interior”.
This is a gameplay identification; mission-specific activation is not newly
claimed from code in this pass.

The stored Delphi grid independently supports the approach arrangement. Using
zero-based (column, row) coordinates, the central line is:

- (49,112): type 18 landing approach lights.
- (49,113): type 17 entrance/gate.
- (49,114): type 77 warehouse/hangar.

Types 77 also occupy (48,114) and (50,114), flanking that central cell. There
are nine type-77 cells in the map in total, so the type alone does not identify
the player's home at every occurrence. Its base header is at bank-pool 6524h;
state bit 80 selects 66E3h. Damage-state bits do not select additional models
for this type in bank 30.

### Timer evidence following the cycle measurement

We measured a complete fountain cycle at close to four seconds, slightly
longer but below five. This prompted inspection of the timer setup. The fragment
at 0321 loads DX from 1774 (0952h = 2,386) and calls 1838. Executing that fragment
in Unicorn produces port writes `(43h,36h), (40h,52h), (40h,09h)`: PIT channel 0,
mode 3, divisor 2,386. Using the 1,193,180 Hz PIT frequency also encoded in the
music driver's timing constant gives approximately 500.075 interrupts/second,
and a 2,048-unit fountain period of **4.09538 seconds**. This supports the
500-unit viewer calibration independently of the visual estimate.

The interrupt routine at 0BEA increments the word stored at 0BF0 via the
self-modifying `ADD AX,1` at 0BF4, with an enable byte and maximum elapsed-step
handling. Frame update B0CE–B0D8 takes the difference from 0BF8 and advances that
snapshot; DB1A's fountain calculation reads 0BF8. These are timer units, not
rendered frames. Pause/enable and step limiting matter, and the whole game's
possible timer reconfiguration paths have not been exhaustively traced.

The executed setup result is recorded in
[timer-setup-verification.json](../analysis/models/timer-setup-verification.json).
The pre-existing shield-display startup deadline of 06FFh (1,791 units) would
be approximately 3.58 seconds at this rate; beacon fade intervals of 256 units
would be approximately 0.512 seconds. These provide possible independent
in-game observations, subject to identifying the correct visible transitions.

## 1995 demo fountain parameter slots

The 1995 demo uses slots **5–10**, rather than retail's **9–14**, for
fountain types 120–123. Its update routine at `4696–46E5` reads clock word
`178C` and writes six signed heights at `SS:6292`. Its sine table at `7906`
and seven amplitude bytes at `46E6` are identical to retail's; the baseline
is likewise -384. `tools/verify_demo_animation.py` executes the demo routine
for every one of the 2,048 clock positions and verifies all 12,288 height
words against the recovered calculation. Results are in
`analysis/demos/animation-verification.json`.

The initial demo viewer mistakenly initialized/animated retail slots 9–14.
This left demo slots 5–8 at zero, putting those vertices at the model's base,
and drove slots 9–10 with the wrong phase bands. The demo exports, model
browser and city explorer now use the demo's parameter range, including both
initial geometry and incremental animation updates. The shared preview speed
remains the retail-calibrated speed; this check verifies the demo's values per
clock tick, not a separately measured demo wall-clock period.
