4 Commits

Author SHA1 Message Date
5c3e3dfe47 re: making turrets targets does not save the escort — the effect is inside the noise
The Stage-02 outcome write-up ended by naming "turrets near the asset must
become targets" as the fix worth ~50% of the escort damage. That was an
inference from a co-presence attribution, not a measurement. Run it and it
does not hold.

Also corrects the run labelling: pilot.py gained the SYLPH_KILL_TURRETS gate
two minutes before mission02 started, so mission02 was already a treatment run,
not a second baseline. Only mission01 (0 of 3968 pilot frames targeting a
turret) is the baseline.

At a common t=428s: baseline 46.9% escort hull, treatment 44.5% and 53.0%. The
two runs of the same arm differ by more than either differs from the baseline,
and the escort still reaches zero at t~590-670s in all three. So the
transferable finding is the power limit: one 430s flight cannot resolve an
effect below ~9 percentage points, and every single-run pilot conclusion,
including this one's, is inside it.

What does reproduce: the assault is scripted (onset 166/167/166s), and the
e007/e010 damage split is 50/50 in all three arms including the one that never
fires at a turret -- so that attribution measures the wave script, not us.

Also records that the viewer's include_external hypothesis in BACKLOG is dead
(it defaults true and is threaded through unchanged).
2026-08-11 05:29:52 +00:00
f4d59c5783 re: Stage 02 is lost at ~11 minutes, and the pilot's survival rule is what guarantees it
First session whose deliverable was the mission's ENDING rather than a
measurement (mission_run.sh, 500 s, hull of every entity at 2 Hz). The ACROPOLIS
is untouched to t=170 s then falls at ~53 HP/s with no let-up, reaching zero at
t=640-720 s — so "no mission completed" is not an artifact of the 240 s
time-boxes, and not of the 600 s cap on a blocking tool call. A longer session
would only watch the loss arrive.

Attributing the damage by co-presence, exactly two classes are ever near the
asset: e007 turrets (8483) and e010 bombers (8334). pilot.py treats turrets as
keep-out zones at 2500 units and never as targets — the rule that made it
survive — so roughly half the escort damage comes from the one class it is
designed to avoid. Survival and the objective are in direct conflict and the
pilot resolves it entirely for survival: WARSHIPS 0000, WARPLANES 0009,
REMAINING OB rising 004 -> 008, our hull untouched at 1500/1500 with 120
missiles spent. That is unspent risk budget, not a good run.

Also corrects launch_mission.sh: a harness-tracked BACKGROUND task does not keep
the display alive (lost 11 s in, at the turn boundary) — the
one-blocking-foreground-call rule stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:57:43 +00:00
530555de9f re: the weapons are the control — sibling-default inheritance is unit-schema-specific, not engine-wide
Four rules from 21 units invites the coincidence objection, so run the identical
sweep against the Weapon/Shell capture, which has COMPLETE coverage (126
records). It finds no sibling rule at all: the one 100%-agreement candidate has a
single distinct value and is really a constant default. Weapon defaults vary per
record exactly as unit defaults do, so "defaults are computed" is general while
"defaults come from a sibling field" is not.

Size_Y <- Size_X survives, and is now checked at the raw-token level rather than
through the sub-record merge: e105, f105 and f101 each declare Size_X/Size_Z/
Size_Radius and no Size_Y, and each reads back its own Size_X at runtime. The
two two-unit hypotheses are demoted to coincidence-not-excluded.

Also records a negative for planning: Stage 01, the only other reachable stage,
adds four uncaptured units that are variants of already-captured ones, so it
would re-measure rather than test the rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:09:14 +00:00
69b4a2e569 re: a unit field left unset on disc is not a global constant — Size_Y inherits Size_X
Attacks the 21/110 unit-coverage limit from the cheap side: if a defaulted field
always took one runtime value, the captured units would pin it for all 110. Only
6 of 24 confirmed defaulted fields behave that way. The other 18 vary per unit,
so the default is computed.

Asking which OTHER field of the same unit holds that value — counting only
non-zero cases, and checking the two fields sit at different offsets so the
layout solver cannot be aliasing them — gives four rules. Size_Y <- Size_X is
solid: seven unrelated ships (e105 600, e106 300, e108 80, e201 300, f101 400,
f105 700, f106 200) each omit it on disc and each shows its own Size_X live,
while the two fields differ freely when both are on disc. Size_Radius fits both
min(X,Z) and the median of the three axes and cannot yet be separated;
e010_ADAN_Attacker_S is what rules out the simpler Size_X rule. FCSRange and
DefencePoint rest on two independent units each and ship as HYPOTHESIS.

Applied across the disc the rules recover 65 (unit, field) values in units that
have never been visited. Falsification test recorded: load any uncaptured stage
and compare one predicted value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:44:24 +00:00
10 changed files with 530 additions and 5 deletions

View File

@@ -58,3 +58,26 @@ class cannot hide behind `e106` passing.
handedness, node-instance recursion) is not re-breaking a correct assembly — compare
the viewer's placement against `assemble_ship`'s output directly before blaming the
format layer.
---
## Viewer: `include_external` is already on — that hypothesis is dead
**Checked 2026-08-11.** The item above names "first that it passes
`include_external = true` (`iso_loader.rs:4012`)" as the cheap first step. It
does: `ShipBrowser::show_external` defaults to `true`
(`iso_loader.rs:643`), the checkbox reads it (`ui.rs:1593`) and it is threaded
through `RequestShipRender``build_ship_model``assemble_ship` unchanged
(`ui.rs:1689`, `iso_loader.rs:4012`). So a ship rendered by the viewer is the
full external assembly, not the bare hull.
The viewer also does not have a transform stack of its own to blame: it bakes
`ScenePart::apply` straight into the vertices and rotates normals by the same
`p.m` (`iso_loader.rs:4030-4062`), so its placement is `assemble_ship`'s output
by construction. What remains unexcluded, in order of cheapness: the mirror
handling (`det < 0` reverses triangle winding only — a reflected part keeps its
reflected geometry), `Xbg7Model::models_named` resolving the wrong sub-model when
a resource name repeats, and the exhaust cones. **Next step is a visual**: the
diagnosis has run out of things it can settle by reading, so the viewer needs to
be run against a known-good class (`e106`) and its render compared with
`ship_render`'s.

View File

@@ -22,7 +22,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| XBG7 mesh | 🟡/❔ | `sylpheed-formats/src/mesh.rs` + `tests/mesh_disc.rs` ([xbg7](structures/xbg7-mesh.md)) | weapons/props: declaration-driven variable stride (36 models), GPU-confirmed. **Stage containers: 5662 sub-models across 22 stages** via content-anchored grouped pools (`stage_models`). Quantized hero bodies (DeltaSaber `f004`) still declined |
| Capital-ship part placement | 🟡 | `sylpheed-formats/src/ship.rs` (static) + [runtime capture](ship-placement-runtime-capture.md) | hull placement static-exact; external parts approximate statically. **Runtime capture** (Canary F10 → VS-constant WorldView) gives ground truth — validated on `e106` destroyer; not yet baked into the viewer |
| Weapon fields defaulted on disc | ✅ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv) the disc does not carry. Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check |
| Unit (craft/vessel) fields defaulted on disc | ✅/🟡 | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — [values](captures/unit-runtime-fields.csv) |
| Unit (craft/vessel) fields defaulted on disc | ✅/🟡 | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — but a defaulted field is **not** a global constant: `Size_Y` provably inherits `Size_X` (7 independent units, 6 distinct values), and three more sibling rules are recorded ❔, recovering 65 values in units never visited — [values](captures/unit-runtime-fields.csv) |
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` (screen-level draw order) not yet decoded |
## Runtime / dynamic-capture technique

View File

@@ -0,0 +1,161 @@
run,turret_rule,t_s,acropolis_hull,e007_alive,e007_killed_cum
mission01,off,0,25000.0,0,0
mission01,off,10,25000.0,5,0
mission01,off,20,25000.0,6,0
mission01,off,30,25000.0,6,0
mission01,off,40,25000.0,6,0
mission01,off,50,25000.0,8,0
mission01,off,60,25000.0,11,0
mission01,off,70,25000.0,11,3
mission01,off,80,25000.0,8,3
mission01,off,90,25000.0,8,3
mission01,off,100,25000.0,10,3
mission01,off,110,25000.0,14,3
mission01,off,120,25000.0,16,3
mission01,off,130,25000.0,17,3
mission01,off,140,25000.0,20,3
mission01,off,150,25000.0,23,3
mission01,off,160,25000.0,26,4
mission01,off,170,24509.5,33,4
mission01,off,180,24509.5,37,4
mission01,off,190,24419.5,38,5
mission01,off,200,24212.5,41,5
mission01,off,210,23447.5,40,5
mission01,off,220,23447.5,40,5
mission01,off,220,23447.5,40,5
mission01,off,230,23447.5,41,5
mission01,off,230,23447.5,41,5
mission01,off,240,22912.0,43,5
mission01,off,240,22867.0,43,5
mission01,off,250,22667.0,42,6
mission01,off,250,22647.0,42,6
mission01,off,260,22334.5,44,6
mission01,off,260,22177.9,44,6
mission01,off,270,21101.2,46,6
mission01,off,270,21101.2,45,6
mission01,off,280,20292.6,46,6
mission01,off,280,20279.1,46,6
mission01,off,290,19598.5,58,6
mission01,off,290,19598.5,58,6
mission01,off,300,18725.2,58,6
mission01,off,300,18658.5,47,6
mission01,off,310,18231.9,47,6
mission01,off,310,18231.9,47,6
mission01,off,320,17425.2,47,6
mission01,off,330,17046.9,49,6
mission01,off,340,16044.8,49,7
mission01,off,350,15461.5,50,7
mission01,off,360,14956.3,51,7
mission01,off,370,14425.5,51,8
mission01,off,380,13910.5,51,8
mission01,off,390,13418.9,49,11
mission01,off,400,13218.9,45,12
mission01,off,410,12517.9,44,12
mission01,off,420,12161.3,44,13
mission01,off,430,11601.3,44,13
mission02,on,0,25000.0,0,0
mission02,on,10,25000.0,5,0
mission02,on,20,25000.0,6,0
mission02,on,30,25000.0,5,0
mission02,on,40,25000.0,5,1
mission02,on,50,25000.0,7,1
mission02,on,60,25000.0,10,1
mission02,on,70,25000.0,7,4
mission02,on,80,25000.0,7,4
mission02,on,90,25000.0,7,4
mission02,on,100,25000.0,7,4
mission02,on,110,25000.0,9,4
mission02,on,120,25000.0,14,4
mission02,on,130,25000.0,16,4
mission02,on,140,25000.0,19,4
mission02,on,150,25000.0,19,4
mission02,on,160,25000.0,23,5
mission02,on,170,24730.0,28,5
mission02,on,180,24730.0,32,5
mission02,on,190,24730.0,36,5
mission02,on,200,24640.0,43,6
mission02,on,210,24190.0,46,6
mission02,on,220,24055.0,47,6
mission02,on,220,24055.0,47,6
mission02,on,230,24055.0,49,6
mission02,on,230,24055.0,49,6
mission02,on,240,23474.5,52,6
mission02,on,240,23474.5,52,6
mission02,on,250,23474.5,53,6
mission02,on,250,23474.5,53,6
mission02,on,260,23474.5,54,6
mission02,on,260,23474.5,54,6
mission02,on,270,22697.0,54,6
mission02,on,270,22697.0,54,6
mission02,on,280,22091.2,56,6
mission02,on,280,22091.2,55,7
mission02,on,290,21577.9,55,7
mission02,on,290,21555.4,55,7
mission02,on,300,20799.1,56,7
mission02,on,310,19781.1,55,7
mission02,on,320,18925.8,56,8
mission02,on,330,18592.5,56,8
mission02,on,340,17432.5,57,8
mission02,on,350,16932.6,59,8
mission02,on,360,16171.6,58,9
mission02,on,370,15017.6,58,11
mission02,on,380,14217.2,59,11
mission02,on,390,13322.2,59,12
mission02,on,400,12808.9,58,13
mission02,on,410,12090.6,61,16
mission02,on,420,11440.6,66,19
mission02,on,430,11127.3,55,19
mission03,on,0,25000.0,0,0
mission03,on,10,25000.0,3,0
mission03,on,20,25000.0,8,0
mission03,on,30,25000.0,5,0
mission03,on,40,25000.0,6,0
mission03,on,50,25000.0,8,0
mission03,on,60,25000.0,9,0
mission03,on,70,25000.0,16,0
mission03,on,80,25000.0,10,0
mission03,on,90,25000.0,11,0
mission03,on,100,25000.0,10,2
mission03,on,110,25000.0,15,2
mission03,on,120,25000.0,12,2
mission03,on,130,25000.0,10,4
mission03,on,140,25000.0,11,6
mission03,on,150,25000.0,14,6
mission03,on,160,25000.0,17,7
mission03,on,170,24685.0,24,7
mission03,on,180,24640.0,23,7
mission03,on,190,24640.0,27,7
mission03,on,200,24460.0,26,8
mission03,on,210,24100.0,28,8
mission03,on,220,24100.0,29,9
mission03,on,230,24100.0,28,11
mission03,on,230,24100.0,28,11
mission03,on,240,23810.0,35,11
mission03,on,240,23810.0,36,11
mission03,on,250,23630.0,36,12
mission03,on,250,23630.0,36,12
mission03,on,260,23630.0,38,13
mission03,on,260,23630.0,38,13
mission03,on,270,22998.0,39,13
mission03,on,270,22908.0,39,13
mission03,on,280,22197.5,40,13
mission03,on,280,22188.5,40,13
mission03,on,290,21602.4,38,13
mission03,on,290,21602.4,38,13
mission03,on,300,21042.4,39,13
mission03,on,300,20975.8,39,13
mission03,on,310,20529.1,41,15
mission03,on,310,20529.1,41,15
mission03,on,320,19812.5,38,17
mission03,on,320,19767.5,38,17
mission03,on,330,19434.2,39,18
mission03,on,340,18785.9,37,19
mission03,on,350,18339.2,37,19
mission03,on,360,17528.1,38,19
mission03,on,370,16901.5,37,19
mission03,on,380,16260.7,35,19
mission03,on,390,15708.2,37,19
mission03,on,400,15346.2,37,19
mission03,on,410,14412.1,36,20
mission03,on,420,13951.9,37,22
mission03,on,430,13211.9,35,23
1 run turret_rule t_s acropolis_hull e007_alive e007_killed_cum
2 mission01 off 0 25000.0 0 0
3 mission01 off 10 25000.0 5 0
4 mission01 off 20 25000.0 6 0
5 mission01 off 30 25000.0 6 0
6 mission01 off 40 25000.0 6 0
7 mission01 off 50 25000.0 8 0
8 mission01 off 60 25000.0 11 0
9 mission01 off 70 25000.0 11 3
10 mission01 off 80 25000.0 8 3
11 mission01 off 90 25000.0 8 3
12 mission01 off 100 25000.0 10 3
13 mission01 off 110 25000.0 14 3
14 mission01 off 120 25000.0 16 3
15 mission01 off 130 25000.0 17 3
16 mission01 off 140 25000.0 20 3
17 mission01 off 150 25000.0 23 3
18 mission01 off 160 25000.0 26 4
19 mission01 off 170 24509.5 33 4
20 mission01 off 180 24509.5 37 4
21 mission01 off 190 24419.5 38 5
22 mission01 off 200 24212.5 41 5
23 mission01 off 210 23447.5 40 5
24 mission01 off 220 23447.5 40 5
25 mission01 off 220 23447.5 40 5
26 mission01 off 230 23447.5 41 5
27 mission01 off 230 23447.5 41 5
28 mission01 off 240 22912.0 43 5
29 mission01 off 240 22867.0 43 5
30 mission01 off 250 22667.0 42 6
31 mission01 off 250 22647.0 42 6
32 mission01 off 260 22334.5 44 6
33 mission01 off 260 22177.9 44 6
34 mission01 off 270 21101.2 46 6
35 mission01 off 270 21101.2 45 6
36 mission01 off 280 20292.6 46 6
37 mission01 off 280 20279.1 46 6
38 mission01 off 290 19598.5 58 6
39 mission01 off 290 19598.5 58 6
40 mission01 off 300 18725.2 58 6
41 mission01 off 300 18658.5 47 6
42 mission01 off 310 18231.9 47 6
43 mission01 off 310 18231.9 47 6
44 mission01 off 320 17425.2 47 6
45 mission01 off 330 17046.9 49 6
46 mission01 off 340 16044.8 49 7
47 mission01 off 350 15461.5 50 7
48 mission01 off 360 14956.3 51 7
49 mission01 off 370 14425.5 51 8
50 mission01 off 380 13910.5 51 8
51 mission01 off 390 13418.9 49 11
52 mission01 off 400 13218.9 45 12
53 mission01 off 410 12517.9 44 12
54 mission01 off 420 12161.3 44 13
55 mission01 off 430 11601.3 44 13
56 mission02 on 0 25000.0 0 0
57 mission02 on 10 25000.0 5 0
58 mission02 on 20 25000.0 6 0
59 mission02 on 30 25000.0 5 0
60 mission02 on 40 25000.0 5 1
61 mission02 on 50 25000.0 7 1
62 mission02 on 60 25000.0 10 1
63 mission02 on 70 25000.0 7 4
64 mission02 on 80 25000.0 7 4
65 mission02 on 90 25000.0 7 4
66 mission02 on 100 25000.0 7 4
67 mission02 on 110 25000.0 9 4
68 mission02 on 120 25000.0 14 4
69 mission02 on 130 25000.0 16 4
70 mission02 on 140 25000.0 19 4
71 mission02 on 150 25000.0 19 4
72 mission02 on 160 25000.0 23 5
73 mission02 on 170 24730.0 28 5
74 mission02 on 180 24730.0 32 5
75 mission02 on 190 24730.0 36 5
76 mission02 on 200 24640.0 43 6
77 mission02 on 210 24190.0 46 6
78 mission02 on 220 24055.0 47 6
79 mission02 on 220 24055.0 47 6
80 mission02 on 230 24055.0 49 6
81 mission02 on 230 24055.0 49 6
82 mission02 on 240 23474.5 52 6
83 mission02 on 240 23474.5 52 6
84 mission02 on 250 23474.5 53 6
85 mission02 on 250 23474.5 53 6
86 mission02 on 260 23474.5 54 6
87 mission02 on 260 23474.5 54 6
88 mission02 on 270 22697.0 54 6
89 mission02 on 270 22697.0 54 6
90 mission02 on 280 22091.2 56 6
91 mission02 on 280 22091.2 55 7
92 mission02 on 290 21577.9 55 7
93 mission02 on 290 21555.4 55 7
94 mission02 on 300 20799.1 56 7
95 mission02 on 310 19781.1 55 7
96 mission02 on 320 18925.8 56 8
97 mission02 on 330 18592.5 56 8
98 mission02 on 340 17432.5 57 8
99 mission02 on 350 16932.6 59 8
100 mission02 on 360 16171.6 58 9
101 mission02 on 370 15017.6 58 11
102 mission02 on 380 14217.2 59 11
103 mission02 on 390 13322.2 59 12
104 mission02 on 400 12808.9 58 13
105 mission02 on 410 12090.6 61 16
106 mission02 on 420 11440.6 66 19
107 mission02 on 430 11127.3 55 19
108 mission03 on 0 25000.0 0 0
109 mission03 on 10 25000.0 3 0
110 mission03 on 20 25000.0 8 0
111 mission03 on 30 25000.0 5 0
112 mission03 on 40 25000.0 6 0
113 mission03 on 50 25000.0 8 0
114 mission03 on 60 25000.0 9 0
115 mission03 on 70 25000.0 16 0
116 mission03 on 80 25000.0 10 0
117 mission03 on 90 25000.0 11 0
118 mission03 on 100 25000.0 10 2
119 mission03 on 110 25000.0 15 2
120 mission03 on 120 25000.0 12 2
121 mission03 on 130 25000.0 10 4
122 mission03 on 140 25000.0 11 6
123 mission03 on 150 25000.0 14 6
124 mission03 on 160 25000.0 17 7
125 mission03 on 170 24685.0 24 7
126 mission03 on 180 24640.0 23 7
127 mission03 on 190 24640.0 27 7
128 mission03 on 200 24460.0 26 8
129 mission03 on 210 24100.0 28 8
130 mission03 on 220 24100.0 29 9
131 mission03 on 230 24100.0 28 11
132 mission03 on 230 24100.0 28 11
133 mission03 on 240 23810.0 35 11
134 mission03 on 240 23810.0 36 11
135 mission03 on 250 23630.0 36 12
136 mission03 on 250 23630.0 36 12
137 mission03 on 260 23630.0 38 13
138 mission03 on 260 23630.0 38 13
139 mission03 on 270 22998.0 39 13
140 mission03 on 270 22908.0 39 13
141 mission03 on 280 22197.5 40 13
142 mission03 on 280 22188.5 40 13
143 mission03 on 290 21602.4 38 13
144 mission03 on 290 21602.4 38 13
145 mission03 on 300 21042.4 39 13
146 mission03 on 300 20975.8 39 13
147 mission03 on 310 20529.1 41 15
148 mission03 on 310 20529.1 41 15
149 mission03 on 320 19812.5 38 17
150 mission03 on 320 19767.5 38 17
151 mission03 on 330 19434.2 39 18
152 mission03 on 340 18785.9 37 19
153 mission03 on 350 18339.2 37 19
154 mission03 on 360 17528.1 38 19
155 mission03 on 370 16901.5 37 19
156 mission03 on 380 16260.7 35 19
157 mission03 on 390 15708.2 37 19
158 mission03 on 400 15346.2 37 19
159 mission03 on 410 14412.1 36 20
160 mission03 on 420 13951.9 37 22
161 mission03 on 430 13211.9 35 23

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -0,0 +1,181 @@
# Why Stage 02 is never won — the escort sinks at ~11 minutes (2026-08-10)
**Status: ✅ measured, one 500 s run.** The standing open item since 2026-07-29 was
"no mission completed". This is the first session whose deliverable was the
*ending* rather than a measurement, and it settles why: **the mission is lost
before it can be won, and the pilot's survival policy is what guarantees it.**
Run: `tools/re-capture/mission_run.sh 500 mission01` — boot → Stage 02 in flight →
`pilot.py` (escort-weighted targeting, target commitment, guided missiles) for
500 s, with every entity's hull sampled at 2 Hz and a screenshot every 30 s.
Artifacts at `/sylph-home/re/mission01/` (9 MB `mission.jsonl`, not committed).
## The escort's decay is linear, and it ends the mission
| t (s) | ACROPOLIS hull | % |
|---|---|---|
| 0160 | 25000 | 100 % |
| 180 | 24510 | 98.0 |
| 280 | 20279 | 81.1 |
| 380 | 13799 | 55.2 |
| 480 | 8541 | 34.2 |
| 485 (end) | 8182 | 32.7 |
Untouched until **t ≈ 170 s**, then **≈53 HP/s** with no let-up — so the asset
reaches zero at **t ≈ 640 s**, and the whole-run average rate puts it at 722 s.
Either way the escort is dead at **1012 minutes**, and "the ACROPOLIS is sunk"
is a defeat condition ([mission-escort-state](mission-escort-state.md)).
This also retires a suspicion: the 240 s time-box of earlier runs was *not*
hiding a win, and the ~500 s ceiling of a single blocking tool call is **not**
the binding constraint. A longer session would simply watch the loss arrive.
## What is actually killing it — and the conflict that follows
Attributing damage by co-presence (which hostiles are within 3000 units of the
asset in the sample where its hull drops, damage split evenly among the classes
present — suggestive, not per-shot proof), only **two** classes are ever near it:
| class | samples present | attributed damage |
|---|---|---|
| `UN_e007_ADAN_Turret` | 200 | 8483 |
| `UN_e010_ADAN_Attacker_S` | 194 | 8334 |
Roughly half the damage comes from **turrets** — and `pilot.py` treats turrets as
**keep-out zones at 2500 units, never as targets**. That rule is not arbitrary: a
turret is what shot down every pilot before 2026-07-30, and it is why the craft
now survives. But it means **the policy that keeps the pilot alive also
guarantees the escort dies.** Survival and the objective are in direct conflict,
and the pilot currently resolves it entirely in favour of survival.
The HUD at t≈485 s says the same thing from the game's side:
- `YOU KILLED WARSHIPS` **0000** — not one warship in 500 s, across every run ever;
- `YOU KILLED WARPLANES` **0009** — fighters only;
- `REMAINING OB` **004 → 008** — objectives are being *added* by waves faster than
any are cleared, so the pilot is not touching the objective set at all;
- SHIELD and ARMOR bars full, hull **1500/1500**, 120 missiles spent.
![Stage 02 at t≈485 s](captures/mission01-t485.png)
## The conclusion that matters
The pilot optimises the wrong thing. It maximises survival and fighter kills;
the mission scores **objectives** and **the escort**, and the fighter population
(134 → 92) is close to irrelevant to both. An untouched 1500/1500 hull at the
moment the escort passes 33 % is not a good run — it is **unspent risk budget**.
Concretely, for the next attempt, in priority order:
(**Step 1 below was run on 2026-08-11 and did not hold — see the follow-up A/B at
the end of this file before acting on it.**)
1. **Turrets near the asset must become targets**, not keep-out zones — accepting
hull damage is the only way to cut ~50 % of the incoming escort damage. The
keep-out rule should be scoped to turrets that are *not* threatening the
asset, rather than applied globally.
2. **Engage warships.** `WARSHIPS 0000` forever means the objective class has
never been attacked; `REMAINING OB` rising is the scoreboard saying so.
3. Re-check whether the escort damage rate actually falls once turrets die —
that is the experiment that tells us whether (1) is sufficient or whether the
bombers need dedicated intercept too.
## Method note, learned the hard way
A harness-tracked **background** task does *not* protect the display: the same
run launched in the background lost Xvfb 11 s in, at the turn boundary
(`skip_intro` exit 3, "DISPLAY LOST"). The comment in `launch_mission.sh` saying
the script may be run as a tracked background task is **wrong**; the
one-blocking-foreground-call rule still stands, which caps a single attempt at
the tool's 600 s timeout. And do not pipe a long run through `tail` — the first
attempt printed nothing because `timeout` killed the pipeline before it flushed;
the on-disk artifacts are what survived.
---
# Follow-up A/B (2026-08-11): making turrets targets does **not** save the escort
**Status: ✅ measured (3 runs), and it falsifies the causal claim above.** The
section above ends by naming step 1 — "turrets near the asset must become
targets" — as the fix worth ~50 % of the escort damage. That was an *inference
from an attribution*, never a measurement. It has now been run, and it does not
hold.
## First, a correction to the run labelling
`pilot.py` gained the `SYLPH_KILL_TURRETS` gate at 21:20:28 on 2026-08-10;
`/sylph-home/re/mission02` started at **21:22:25**, i.e. *after* it, with the
gate **on**. So `mission02` was never a second baseline — it is a treatment run
that the previous session produced but never reported. The arms are:
| run | turret rule | evidence (`pilot.log`) |
|---|---|---|
| `mission01` | **off** (baseline) | `tgt=e007` on **0** of 3968 lines |
| `mission02` | **on** | `tgt=e007` on 1588 of 3844 lines |
| `mission03` | **on** (this session) | `tgt=e007` on 1218 of 3514 lines |
`mission01` predates the edit, so the *only* difference between the arms is the
gate. Everything else — build, save slot, nav route, loadout — is identical.
## The result, all three runs truncated to a common t = 428 s
| run | rule | ACROPOLIS @428 s | % | decay rate | turrets seen | turrets killed | attack onset |
|---|---|---|---|---|---|---|---|
| `mission01` | off | 11735 | 46.9 % | 50.3 HP/s | 92 | 30 | 166 s |
| `mission02` | on | 11127 | 44.5 % | 52.7 HP/s | 98 | 34 | 167 s |
| `mission03` | on | 13257 | 53.0 % | 44.4 HP/s | 88 | 41 | 166 s |
**The two runs of the *same* arm differ by 8.5 percentage points (2130 HP) —
more than either differs from the baseline.** The baseline sits *between* the two
treatment runs on escort hull and on decay rate. So the effect of the turret rule
is **not resolvable at n=1 per arm**, and the honest statement is: it was not
demonstrated. Extrapolating each run's own rate, the escort still reaches zero at
**t ≈ 590670 s** in every arm — the mission is lost in all three.
The rule does do the mechanical thing it was written to do (turret kills 30 → 34,
41; 12181588 frames spent with a turret as the committed target). It just does
not convert into escort hull.
![Stage 02 at t≈430 s with the turret rule on — WARSHIPS 0000, REMAINING OB 008](captures/mission03-turretrule-t430.png)
## What *is* reproducible across all three runs
These are the numbers to build on, because they repeat to within a percent:
- **The assault on the ACROPOLIS is scripted, not emergent.** Onset at
**166 / 167 / 166 s** — ±1 s across three runs with completely different pilot
behaviour in between. Nothing the pilot does moves it.
- **The damage split is a property of the scenario, not of our targeting.**
`e007` turrets vs `e010` bombers is **50.6/49.4, 50.0/50.0, 51.0/49.0** — and
the baseline, in which we never fired at a turret at all, splits the same way.
A co-presence attribution that is invariant to whether we attack one of the two
classes is measuring the wave script, not our contribution.
- **Turrets die anyway**: 30 of 92 are already dead by t=428 s in the baseline,
from friendly fire. The turret rule adds ~411 kills on top of that, which is
why its escort-hull effect is small enough to be buried in run-to-run noise.
- `WARSHIPS 0000` and `REMAINING OB` rising (004 → 008) in **every** run,
including this one.
Per-run series: [`captures/escort-decay-ab.csv`](captures/escort-decay-ab.csv)
(10 s grid, hull + turret alive/killed counts for all three runs).
## The methodological finding, which is the transferable one
**A single 430 s flight cannot resolve an escort-hull effect smaller than ~9
percentage points.** Every pilot conclusion drawn from one run — including step 1
above, and including anything drawn from `mission03` alone — is inside the noise.
Each run costs ~10 minutes of wall clock and one blocking foreground call, so a
properly powered A/B is ~6 runs ≈ 1 hour. **Any future pilot tuning must budget
that, or not be believed.**
## What this means for the reason we wanted a win
Story progress was wanted only because unit definitions instantiate per stage, so
unit-field coverage is stuck at 21/110 ([unit struct](structures/unit-struct-runtime.md)).
This result says the pilot route to that coverage is expensive and unproven: the
escort dies on a script at ~1011 minutes, our best lever moved it by less than
the noise, and the objective class (`WARSHIPS`) has never been touched in any run
ever. **Before spending another hour on pilot tuning, the cheaper lever to price
is the save file** — if the profile's save data can be read and understood, stage
unlock state is a much shorter path to the same coverage, and the save format is
itself something the reimplementation needs. Marked `NEEDS-HUMAN` as a direction
choice; no save-file modification has been attempted.

View File

@@ -272,3 +272,84 @@ take-off, ~12 minutes in mid-combat, and after GAME OVER. 14 objects, the same
is no need to play it, and no need to survive it.
Stages captured so far: `Ttrl` (BASIC CONTROLS), Stage 02.
## A defaulted unit field is not a global constant — some inherit from a sibling
**Confidence: 🟡 for `Size_Y`, ❔ for the rest. Analysis 2026-08-10, offline, from
[`captures/unit-runtime-fields.csv`](../captures/unit-runtime-fields.csv).**
The coverage limit above (21 of 110 units, growing only with story progress) is
worth attacking from the other side first: *if* a field the disc leaves unset
always took the same runtime value, the 21 captured units would pin that default
for all 110 and no further missions would be needed.
**It does not.** Restricting to the 150 values that are both ✅ CONFIRMED and
come from a field the disc leaves defaulted, only 6 of 24 fields have a single
value across every unit that defaults them (`HP`→10, `MassScore`→0,
`MaximumVelocity`→0, `RadarRange`→0, `DestroyMotionTime`→0, `Size_Z`→0.1). The
other 18 take several distinct values — so the default is computed per unit.
Where from? For each defaulted value, ask which *other* field of the same unit
holds exactly that value. Counting only cases where the value is **non-zero**
(otherwise `0 == 0` inflates every pair) and checking that the two fields are at
**different offsets** (so the match is not the layout solver aliasing them):
| defaulted field | takes the value of | support | independent units |
|---|---|---|---|
| `Size_Y` (`0x034`) | `Size_X` (`0x030`) | 9/9 | **7**, 6 distinct values |
| `Size_Radius` (`0x050`) | `min(Size_X, Size_Z)` | 4/4 | 4, 3 distinct values |
| `FCSRange` (`0x2a4`) | `RadarRange` (`0x2a0`) | 4/4 | 2 |
| `DefencePoint` (`0x2bc`) | `AttackVesselPoint` (`0x2b4`) | 6/6 | 2 |
`Size_Y ← Size_X` is the one to trust: seven unrelated ships (`e105` 600,
`e106` 300, `e108` 80, `e201` 300, `f101` 400, `f105` 700, `f106` 200) each omit
`Size_Y` on disc and each shows its own `Size_X` at runtime. When both fields
*are* on disc they differ freely (14 distinct `Size_Y` values against 13 of
`Size_X`), so this is a default rule, not one value stored twice.
`Size_Radius`'s formula is **not yet separable**: `min(Size_X, Size_Z)` and "the
median of the three axes" fit all four units identically. `UN_e010_ADAN_Attacker_S`
is what rules out the simpler `Size_Radius ← Size_X` (X=100, Y=40, Z=50, radius
**50**). The last two rules rest on two independent units each and are ❔ —
recorded so they can be falsified, not relied on.
**Why it matters for the reimplementation:** filling a missing `Size_Y` with `0`
or with a global constant gives the game's largest hulls a wrong lateral extent
(`f105` 700, `e105` 600, `f101` 400 — all defaulted on disc). Applied across the
disc, the rules recover **65 (unit, field) values in units that have never been
visited**: `Size_Y` in 21 of the 21 units that omit it, `Size_Radius` in 22 of 26,
`FCSRange` in 14 of 56, `DefencePoint` in 8 of 60.
### Cross-check against the weapons: this is NOT an engine-wide mechanism
The obvious worry is that four rules from 21 units are coincidence. The
`Weapon`/`Shell` capture is the control: **complete coverage, 126 records**, with
the same "defaulted on disc" classification. Running the identical sweep there
(confirmed rows, non-zero values, offsets required to differ) finds **no sibling
rule at all** — the single 100 %-agreement candidate (`Shell.Length ←
`Shell.Volume`, 5 records) has one distinct value, i.e. it is really the constant
`Length → 10` coinciding with `Volume = 10`. Weapon defaults vary per record just
as unit defaults do (10 of 14 `Weapon` fields, 16 of 17 `Shell` fields), so the
phenomenon is general; the *sibling* explanation is not.
So `Size_Y ← Size_X` is **specific to the unit schema** (plausibly the size block
defaulting its axes), not a property of IDXD default resolution. Two consequences:
the rule cannot be justified by appeal to a general mechanism, and the two
two-unit hypotheses (`FCSRange`, `DefencePoint`) lose the support they would have
borrowed from one — treat them as **coincidence-not-excluded** until a new stage
tests them.
`Size_Y ← Size_X` itself survives this scrutiny, and was re-checked at the raw
token level rather than through the sub-record merge: `UN_e105_ADAN_Cruiser`,
`UN_f105_TCAF_Cruiser` and `UN_f101_TCAF_Acropolis` each declare `Size_X`,
`Size_Z` and `Size_Radius` and **no `Size_Y` at all**, and each reads back its own
`Size_X` (600 / 700 / 400) at runtime.
**How to falsify:** the rules predict a specific number for units in stages not
yet captured. Load any new stage, snapshot, and compare — one disagreement kills
the rule. Note what is *not* a useful test: Stage 01, the only other reachable
stage, adds just four uncaptured units (`e010`/`e106` variants) whose predictions
are the same numbers their already-captured base variants gave, so it would
re-measure rather than test. A real test needs a stage with unfamiliar classes,
i.e. story progress — which is now the *only* thing story progress is needed for
here.

View File

@@ -19,7 +19,9 @@ alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $
# that actually bought was the opposite: a process nothing owns is a process
# nothing keeps alive, and both were being reaped a couple of minutes in — the
# long-standing "Xvfb and the emulator die on their own every few minutes" note.
# Run this whole script as ONE tracked background task and leave Xvfb, openbox
# MEASURED WRONG 2026-08-10: a harness-tracked BACKGROUND task does not protect
# them either — the display was lost 11 s in, at the turn boundary. Run this
# whole script as ONE BLOCKING FOREGROUND call and leave Xvfb, openbox
# and xenia as its children: they then live exactly as long as the session does.
# `nohup` still shields them from a stray HUP; the exit-status wrapper means a
# death is reported with the server's own account instead of being inferred.

60
tools/re-capture/mission_run.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Attempt to COMPLETE a mission and record how it ends.
#
# Every previous flight session was time-boxed to 240 s to measure something
# (escort hull, lethality, ship placement) and none ever reached a mission
# outcome — "no mission completed" has been the standing open item. Unit
# definitions are instantiated per stage, so story progress is the only thing
# that grows unit coverage past 21/110, and that needs a WIN, not a survival.
#
# So this run is deliberately long and its only deliverable is the ENDING:
# screenshots throughout, every entity's hull sampled, and the pilot log kept
# whole (never tail-piped — a frozen log tail is what mission-end looks like
# from outside, and tailing throws away the transition).
#
# Runs as ONE tracked background task with Xvfb/openbox/xenia as plain nohup
# children — see docs/re/session-lifetime notes; do NOT setsid anything.
#
# Usage: mission_run.sh [flight_seconds] [tag]
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
SD="$(cd "$(dirname "$0")" && pwd)"
SECS="${1:-900}"
TAG="${2:-mission}"
SHOTS=/sylph-home/re/shots
OUT="/sylph-home/re/$TAG"
mkdir -p "$SHOTS" "$OUT"
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
python3 "$SD/entities2.py" self 0x130 "$OUT/cfg.json" || { echo "BIND FAILED"; exit 1; }
echo "=== initial entity table ==="
python3 "$SD/mission_state.py" scan "$OUT/cfg.json"
# A screenshot every 30 s for the WHOLE run: the outcome card (MISSION COMPLETE
# / GAME OVER) is on screen only briefly, so sampling must not stop early.
( n=$(( SECS / 30 + 4 ))
for i in $(seq 1 "$n"); do
printf '%s SHOT %03d\n' "$(date +%s)" "$i" >> "$OUT/shots.log"
screenshot "$SHOTS/$TAG-$(printf %03d "$i").png" >/dev/null 2>&1
sleep 30
done ) &
SHOTTER=$!
date +%s > "$OUT/t0"
python3 "$SD/mission_state.py" watch "$OUT/cfg.json" "$SECS" 2 "$OUT/mission.jsonl" \
> "$OUT/mission.log" 2>&1 &
WATCHER=$!
SYLPH_KILL_TURRETS="${SYLPH_KILL_TURRETS:-0}" python3 "$SD/pilot.py" "$OUT/cfg.json" "$SECS" > "$OUT/pilot.log" 2>&1
PILOT_RC=$?
wait $WATCHER 2>/dev/null
kill $SHOTTER 2>/dev/null
screenshot "$SHOTS/$TAG-end.png" >/dev/null 2>&1
cp -f "$SHOTS/$TAG-end.png" "$OUT/end.png" 2>/dev/null
echo "PILOT_RC=$PILOT_RC"
echo "--- last 5 pilot lines ---"; tail -5 "$OUT/pilot.log"
echo "--- shots: $(ls "$SHOTS/$TAG-"*.png 2>/dev/null | wc -l) ---"
echo "MISSION RUN DONE ($TAG, ${SECS}s)"

View File

@@ -100,6 +100,18 @@ MISSILE_PERIOD = 2.0 # s between launches; 300 rounds is not unlimited
# configuration here — override with $SYLPH_ASSET for another stage.
ASSET_NAME = os.environ.get("SYLPH_ASSET", "Acropolis")
# Turrets are keep-out zones everywhere else in this file, and that rule is what
# stopped the pilot being shot down. But the 2026-08-10 outcome run measured the
# cost of it: of the damage that sinks the ACROPOLIS, roughly half comes from
# e007 turrets sitting 1.5-3 km off it (the other half from e010 bombers), and a
# turret is 100 HP -- one missile, or ~7 nose-gun hits. Several were already at
# 45-95 HP from stray fire and were never finished off, because nothing ever
# targets them. So the rule that keeps us alive is also what loses the escort.
# Gated rather than simply changed, so the measured baseline stays reproducible:
# SYLPH_KILL_TURRETS=1 lets DEFEND -- and only DEFEND -- treat a turret that is
# near the asset as a target. ENGAGE still avoids them.
KILL_TURRETS = os.environ.get("SYLPH_KILL_TURRETS") == "1"
class Pilot:
KP, KD = 2.2, 0.45
@@ -230,7 +242,9 @@ class Pilot:
"""
out = []
for off, nm, p, v, r, hard in hos:
if hard:
# A turret near the asset IS a killable objective (100 HP) when
# SYLPH_KILL_TURRETS is set; a capital hull never is.
if hard and not (KILL_TURRETS and "Turret" in nm):
continue
rel = a_p - p
d = float(np.linalg.norm(rel))
@@ -520,9 +534,12 @@ class Pilot:
self.set_throttle(0)
fire = True
# a turret inside its keep-out radius outranks the target
# A turret inside its keep-out radius outranks the target -- except the
# one we are deliberately attacking, or the keep-out would steer us off
# the very thing we chose to kill and neither goal would be served.
tgt_off = tgt[0] if tgt else None
for off, nm, p, v, r, hard in hos:
if not hard:
if not hard or (KILL_TURRETS and off == tgt_off):
continue
d = float(np.linalg.norm(p - me_p))
if d < self.TURRET_KEEPOUT: