ship: index-less brg/eng/sld parts never matched their GN frame — 34 ships assembled without a bridge

Tier 3 matched a part to its hardpoint by trailing index, so `e105_brg`
compared "01" == "" against GN_Bridge_01 and fell through silently. The runtime
capture is what exposed it: the game draws the bridge and places it at
[0, 70, -1850] rel e105_bdy_01, and assemble_ship emitted nothing there.

With no index to match on, take the lowest-numbered frame of the category.
Diffing assemble_ship part counts across every container: 34 (stage, ship)
entries gain parts — e102 +2 (bridge and engine), e104 +1, e105 +1, Stages
02-29. ship_audit is unchanged, so nothing regressed, and the capture now agrees
to dT 0.03 / dR 0.000.

Also fixes the diff itself: correlate_frames compared static against a rotation
sampled from the first block, which can belong to another INSTANCE of the class.
Scoped to the position-agreeing cluster, e105_eng_01 goes 1.711 -> 0.000 and
both e106 nacelles to 0.000. The one remaining rotation delta (e106_wep_02_01,
0.134) is a turret whose rotation varies by 0.182 between blocks that agree on
its position — the runtime disagrees with itself more than with the assembler.
The new rotVar column makes that distinction visible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 19:16:06 +00:00
parent 3d9f21f030
commit ca500c171e
4 changed files with 88 additions and 31 deletions

View File

@@ -15,9 +15,12 @@ and to 0.000 in rotation for every part that does not move; see
[`ship-placement-capture-generalisation.md`](ship-placement-capture-generalisation.md)
§4. So look at **the viewer**: first that it passes `include_external = true`
(`iso_loader.rs:4012` — with `false` an e106 loses its bridge and both nacelles,
5 parts instead of 11), then its own transform stack. Two narrow format-side leftovers
remain: `e105_brg` is never produced by `assemble_ship`, and `e105_eng_01`'s rotation
differs by 1.711.
5 parts instead of 11), then its own transform stack.
One real format-side bug was found on the way and is **fixed**: index-less parts
(`e105_brg`) never matched their `GN_Bridge_01` hardpoint, so 34 (stage, ship) entries
`e102`, `e104`, `e105` across Stages 0229 — assembled without a bridge. The other
apparent exception (`e105_eng_01` rotation) was an aggregation artefact and is 0.000.
The original report and its reasoning follow.

View File

@@ -192,16 +192,32 @@ for, and it is the opposite of the assumption in it: `assemble_ship` is right, s
viewer's "capital ships assemble wrong" is the viewer's own transform stack (the
backlog's own "worth ruling out first, cheaply").
Two real exceptions, both narrow:
Both first-pass exceptions were chased down, and neither survives as an open question:
- **`e105_brg` is never produced by `assemble_ship`**, at either `include_external`
setting, although the runtime draws it and places it at `[0.0, 70.0, -1850.0]`
relative to `e105_bdy_01` (4/4 blocks, spread ≤0.13). ❔ A genuine missing part.
- **Rotation differs only on parts that move**: `e106_wep_02_01` (turret, dR 0.093),
`e106_eng_01`/`eng_02` (dR ~0.097) and `e105_eng_01` (dR **1.711**). A turret aiming
and a nacelle gimballing at capture time is expected and is not an assembly error;
1.711 on `e105_eng_01` is too large for that and is ❔ **unexplained — NEEDS-HUMAN**
(either a static rotation bug on that one part, or the part is articulated).
- **`e105_brg` was genuinely missing — a real assembler bug, now fixed.** Tier 3
matched a part to its `GN_*` hardpoint by trailing index, so an index-less part
(`e105_brg`) compared `"01" == ""` against `GN_Bridge_01` and fell through silently.
With no index to match on, take the lowest-numbered frame of the category. The
runtime is the check: `e105_brg` now assembles at `[0.0, 70.0, -1850.0]` relative to
`e105_bdy_01`, **dT 0.03, dR 0.000** against the capture.
Reach measured by diffing `assemble_ship` part counts over all containers before and
after: **34 (stage, ship) entries gain parts**`e102` +2 (bridge *and* engine),
`e104` +1, `e105` +1, across Stages 0229. Every one of those ships was assembling
without its bridge. `ship_audit` is unchanged (still exactly the `f002_bdy_05`
outlier), so nothing regressed.
- ✅ **The rotation deltas were an artefact of my own aggregation, plus one real
articulation.** The static diff was comparing against a rotation taken from the
first sampled block, which can belong to *another instance* of the class; scoping it
to the position-agreeing cluster drops `e105_eng_01` from dR 1.711 to **0.000** and
both `e106` nacelles to **0.000**. What remains is `e106_wep_02_01` at dR 0.134 — and
that part's rotation varies by **0.182 between blocks that agree on its position**,
i.e. the runtime disagrees with itself more than it disagrees with the assembler.
It is a turret aiming, not an assembly error. `correlate_frames` now prints that
`rotVar` column precisely so "the part moved" cannot be mistaken for "the rotation
is wrong".
Final numbers, three classes, 21 parts: **worst dT 0.43, worst dR 0.000** for every
part that is not articulating.
`include_external` matters and is a caller-side trap: with `false` an `e106` assembles
as **5** parts and with `true` as **11** — the engine cluster, the bridge and the
@@ -223,7 +239,9 @@ bridge — which looks exactly like "assembles wrong".
frames. The `e106`-derived rules generalise; the MULTIKEY hypothesis in §1 is *not*
needed to explain anything observed so far (`f105` has 2 multikey tracks and still
matches exactly).
- ❌ Still open, both narrow and both evidenced: `e105_brg` is missing from
`assemble_ship`, and `e105_eng_01`'s rotation differs by 1.711 — NEEDS-HUMAN.
- ▶ Next: chase those two, and point the viewer investigation at the viewer
(`include_external`, node-instance recursion), not at the format layer.
- ✅ One real assembler bug found and fixed by this route: index-less `brg`/`eng`/`sld`
parts never matched their `GN_*` frame, so **34 (stage, ship) entries** assembled
without a bridge (and `e102` also without its engine). Verified against the capture.
- ▶ Next: the viewer itself (`include_external`, node-instance recursion) the format
layer is now measured, not assumed. A per-ship regression table over the checked-in
captures would keep it that way.