This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/re/INDEX.md
Sylpheed RE agent d79f59d24e re: timer_set's second argument is a SECOND countdown, not a warning threshold
mission-phase-timers.md left 180 open as "a limit and a warning
threshold is the obvious reading, but not established".  Reading
sub_822639B8 -- ScriptPhase::Update 0x82263528, same dt as the
stopwatch bank -- settles it the other way:

    if running: [+304] += dt
    if armed:   [+308] -= dt   while [+308] > 0
                else [+312] -= dt, clamped at 0

[+312] is never compared with [+308]; it is decremented, and only in
the A<=0 arm.  Two sequential countdowns.  Disc-wide the second
argument is 180 in all 29 timer_set sites while the first varies
(600 x19, 1200 x8, 900, 1800).

Built-ins 123-127 are vtable slots 90-94 on five scalars at
[phase+304..320].  125 and 126 have ZERO call sites in all 28 scripts:
the script arms, starts and stops this clock but never reads it.

Corrects mission-phase-timers.md, which merged this clock with
stopwatch 0 -- timer_resume starts [+304], set_flag(0) one instruction
later starts the stopwatch the timeline's kind=0 reads.

Docs only; all seven ISL artefacts regenerate byte-identical.
2026-08-27 09:03:18 +00:00

131 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RE knowledge index
Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README](README.md).
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
Promote to a prose `structures/…md` file when a format needs behavioural notes beyond layout.
## Data structures / formats
| Format | Conf. | Spec (parser + tests) | Notes |
|--------|-------|-----------------------|-------|
| IPFB `.pak` archive | ✅ | `sylpheed-formats/src/pak.rs` + `tests/pak_idxd_disc.rs` | header + 12-byte TOC, Z1/zlib payloads |
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔ |
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat``pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
| LSTA sprite list | ✅ | `sylpheed-formats/src/lsta.rs` | A display list of inline elements: **T8aD sprites and `PRMD` primitives**. The `count` at `0x04` is **exact and counts both**`count == T8aD + PRMD` for **64/64** lists on the disc, which retires the old "a few entries disagree" note (it compared sprites against a total including primitives). **All 1 281 sprite frames decode** after the T8aD rectangle-list fix |
| IXUD subtitle + caption text | ✅ | `sylpheed-formats/src/ixud.rs` + `movie_subtitle.rs` ([container](structures/idxd-container.md) · [movie link](movie-subtitle-link.md)) | **The IXUD record/field table is decoded and wired in (2026-08-26)**`IxudObject` mirrors `IdxdObject`; uniform 16-byte records, 12-byte fields, every offset in **chars**, and the word at `0x08` is record 0's hash, not a schema id. Verified disc-wide: **1104/1104** objects, **1476/1476** records, **628 165/628 165** named fields reproducing their `ixud_hash` (`tests/ixud_records_disc.rs`). **Caption text: 537 → 8800 lines, which is 8800 of 8800 distinct keys.** Two steps — generalising the key parser from `MSG_DEMO_*` to all **eight** families (`ACRO ADAN ADPL BIRD DEMO RHIN TCAF` use `MSG_<FAM>_<id>_<page>_<line>`, `VOICE` alone inserts a family letter) took 537 → 8074; switching from **token adjacency to record fields** took it to 8800. ⚠️ An earlier "1.3 % of the game's text" figure of mine counted *occurrences across blocks* — the honest denominator is **8800 distinct keys**, so the real starting point was 6.1 %. The `DEMO` control shows why the field route matters: token adjacency finds 537 lines there, fields find **541** — it was dropping lines in the one family it was written for. | timed cues. **The movie↔subtitle↔voice link is solved — statically**, and as of 2026-08-25 read from the IDXD **record table** rather than scraped from the string pool: **104 cutscene slots binding 101 distinct movies**, 99 slots / 96 movies with a subtitle, 99 / 96 with a voice track, 22 / 22 with a telop. ⚠️ The previous counts (94 / 83 / 21) were the numbers of **distinct pool strings** — a repeat reference contributes no token, so 13 later `VOICE_D_450..454` references read as "no binding". **All 18 hokyu movies are bound**, not five. 93 of the 94 distinct subtitle members resolve; `SUBTITLE_S12B.tbl` resolves in none of the six languages — a dangling reference on the disc. The ~104 **script ids are no longer ❔**: they are literal positional field keys in `BASE_INFO`, each naming its record, and all 104 resolve. `movie_manifest::parse` now reads the record table; CSV regenerated by `examples/movie_map_csv.rs` |
| Fonts (ttf/otf/ttc) | ✅ | `sylpheed-formats/src/font.rs` | standard OpenType, parsed via ttf-parser |
| XBG7 mesh | ✅/🟡 | `sylpheed-formats/src/mesh.rs` + `tests/mesh_disc.rs` ([xbg7](structures/xbg7-mesh.md)) | **6 294 resources, 6 209 decode (98.7 %), 82 searched-and-missed** (2026-08-12, up from 5 480 / 87.1 %). Five evidence-driven fixes got there: **distinct anchor assignment** (no two resources may claim one buffer — proved by a capture showing the container holds both mirrored `e106` hull halves), the connectivity cap replaced by a **winding-consistency gate at 0.70**, **structural requirements on pre-pivot sub-meshes** (index range, then exact pool coverage), and **filtering after the assignment** so a subset query cannot differ from the full decode. Validated against a runtime capture that names the file offset of every buffer the engine drew: **46/46 drawn buffers claimed, 45 anchored exactly**. **No real mesh now decodes differently in different containers** — all 89 remaining cross-container disagreements are interchangeable 24-vertex bounding boxes, which no anchoring rule can pin (monotone order re-tested and refuted). Remaining misses attribute to the degeneracy/extent gate (42), winding (31) and coverage (9); the first was probed and its "obvious" fix refuted. Every decoded sub-mesh covers its own vertex pool. **The `[index buffer][vertex buffer]` layout is now runtime-verified** (2026-08-13): with the F10 capture extended to log each draw's index buffer, all **42** drawn `Stage_S02` buffers match our decoded index count exactly, all 42 have their index union cover the pool exactly, and the 30 single-block cases all sit at `pad ≤ 3` — so `e106_eng_02_l`'s old rejection was the connectivity gate, not a misplaced index buffer. The `indices=` mystery was the capture keeping only the **first of several index batches** per buffer. **And comparing index VALUES found the biggest silent defect yet**: the anchor took the first `pad` that validated, so a block whose index data sits at pad 2 was read **one element late** — 76/93 captured runs matched, all 17 differences a one-element shift. Scoring pads by degenerate triangles + winding fixes it: **93/93** captured runs now match byte for byte, disc-wide degenerate runs **582 → 1** (the grouped path had the same bug; and two resources were anchored on a degenerate lookalike earlier in file order), **590 of 8 850** sub-meshes re-wired with 10 vertex anchors moved, resources decoded unchanged at 6 209. Cross-container minority decodes 89 → 96 — *because* the decoder improved: `_rou_f402_dead` now has a majority (32×25×8) so its seven wrong copies are named instead of hidden. One dirty run remains, blocked by distinct assignment on a 24-vertex box. **Then the descriptor gave up its last structural secret**: it declares a vertex layout **per sub-mesh** (`n201_01` → strides 24/24/24/**28**, capture-confirmed), and grouped selection must prefer the candidate explaining the **whole** pool rather than the first whose pivot validates — together they take never-decoding resources **85 → 47** (**6 247 / 6 294 = 99.25 %** decode), put `n201_01` on all four capture-proven offsets and raise the stage-05 capture oracle to **128/128**. The residual 47 is 30 pose/proxy composites (0.010-unit marker boxes), 6 `.DAT` particle composites, 8 damage/LOD variants and 3 props — not a threshold away |
| Capital-ship part placement | ✅ | `sylpheed-formats/src/ship.rs` (static) + [runtime capture](ship-placement-runtime-capture.md) | Placement is **sound** (hull static-exact against the `e106` capture; cross-id mounting genuinely narrow, 2 pairs across 335 ships). The XBG7 mis-decode this row used to blame for "ships assemble wrong" — a shared turret ~100× too large in some containers — is **fixed** (2026-08-12, the exact-coverage requirement): `e303_wep_01` now decodes 49×23×42 everywhere and places at ±179 on the `e106` hull, and no real mesh disagrees across containers. A composite-node audit confirmed the assembler itself never applied a bad scale (all nodes scale 1.0, orthonormal). Still open: `static_assembly_matches_runtime_capture` walks capture parts only, so **extra** static placements cannot fail it |
| 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). ⚠️ **CORRECTED 2026-08-26: the disc DOES carry a third of them.** Re-read through the [record table](structures/idxd-container.md), **1 514 of the 4 393** rows labelled `defaulted-on-disc` have a value on disc (2 879 genuinely absent) — measured here as an upper bound, since a field is counted when it appears in *any* record of the object; a per-record count gives ~1 448. The old reader could not name a record, so per-record fields read as absent. Spot-checked exactly: `wep_05`/`wep_60` `TriggerShotCount` = **4**, `wep_02` `Power` = **100.0**, `wep_60` `Power` = **1000.0** (refuting the recorded "C band ≈150…500" bracket), `wep_25` `MaximumRange` = **4000.0**, `wep_11/28/36/70` `LoadingCount` = **6/5/5/0**. The runtime capture's numbers all match the disc — what is withdrawn is the premise that it was reaching values the disc lacks. See [weapon-datasheet-runtime](weapon-datasheet-runtime.md) 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**. **Re-derived independently 2026-08-13 from the loader's own key strings** (`sub_82341A20`; the field name for each store is a string in the image): **159 fields**, agreeing with this solver on **25 of 25 shared offsets**, verified at **406 values matching the disc and 0 disagreeing** over 11 live objects spanning UNIT and VESSEL — landed as `data/unit_definition_layout.txt` + `sylpheed_formats::unit_layout` + a no-emulator test, with **121 defaulted fields** read out ([live-unit-definitions](live-unit-definitions.md)). ⚠️ **CORRECTED 2026-08-26:** those fields are **on the disc**. The ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the `Generic` / `Shield` / `Mass` / `SE` records — which is why a reader that could not name a record saw them as absent. And **"18 of 23 vessel records are missing at least one of `Size_X/Y/Z/HP`" is false: 0 of 114** objects with a `Generic.Type` (43 Craft + 71 Vessel) miss any of the four. See [unit-struct-runtime](structures/unit-struct-runtime.md) Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — ❌ **the sibling-default rules are WITHDRAWN (2026-08-25)**`Size_Y` is on disc for **114/114** unit tables and **differs from `Size_X` in 90**; the old reader missed it exactly when the two were equal (a string-pool dedup artefact, cross-tab `seen+equal = 0` for all three pairs), so the rule was re-deriving the condition that hid the field. It also predicts wrong twice — `UN_e104_ADAN_Carrier.DefencePoint` and `UN_e011_ADAN_Attacker_B_HF_Wayne.FCSRange`. Read the record table instead — [values](captures/unit-runtime-fields.csv) |
| Arsenal develop economy | ✅/❔ | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order — the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered |
| 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` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly |
| UI screen paint order | ✅ | [runtime screen object](structures/ui-screen-runtime.md) + [title capture](ui-title-paint-order-capture.md) | **SOLVED**: the game's screen object keeps a second, reordered list of its elements — the child array at `+0x30` — and that is the paint order, not the declaration table. Read live from guest memory (found by the item vtable `0x820b30b4`) and checked against the draw capture: the seven nameable elements sit at child slots 0, 6, 7, 13, 16, 17, 22, strictly ascending, exactly as captured; it also settles the one pair no static field could order. 🟡 deriving that order from the bundle — what the port needs — is still open |
| Scripted input / profile traps | ✅/🟡 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Why a scripted run appears unable to press Ⓐ: **F10 opens the emulator menu bar**, and any Xenia UI makes `XamInputGetKeystrokeEx` return SUCCESS with an empty keystroke *before* any driver is asked (Canary now logs `[RE-INPUT] … swallowed by IsUIActive`); the title needs a **signed-in profile** (hence `--create_profile_if_none`); and a **FIFO trace consumer that exits stalls the emulator**, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question |
| Title-screen guest crash | ✅ | [title-crash-stl-tree](title-crash-stl-tree.md) | The guest throws **`std::out_of_range`** from its cache-manager flush (`sub_823070B0`, an STL map/set erase that builds `'invalid map/set<T> iterator'`); the access violation after it is only the throw **returning**, because this build does not unwind guest EH. Trigger found and controlled: an **incomplete on-disc cache** (`~/.local/share/Xenia/cache/aab216c3`) throws ~100 s into a boot, a complete one never does — 2 runs each way. ❌ `mem_watch`, the handoff's suspect #1, is **eliminated**: cold cache + `--mem_watch=false` throws anyway |
| Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. **A third save, taken after developing exactly one Arsenal weapon** (Light Machine Gun MG I, 4000 P), moves exactly three things: `+24` Points 4101→101 (which **separates it from `+28`**, that did not move), `+8` clear ratio 5→6 (so the ratio counts *collection*, not only stages), and two entries of the 54-byte blob — `2→4` for the item bought and `0→2` for the successor the game announced as newly developable, giving the blob its alphabet ✅ *0 locked / 2 developable / 4 developed* (only the `4`s are stored — `2` is re-derived at load). **Saves can also be written back**: three derived header fields (length at `+0x30`, payload length at `+0x8c`, `adler32` at `+0x8e`) are all that stand between a parse and a hand-written save that the title loads, and [`savegame_edit.py`](../../tools/re-capture/savegame_edit.py) re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the [economy note](arsenal-develop-economy.md) |
## Runtime / dynamic-capture technique
| Technique | Conf. | Spec | Notes |
|-----------|-------|------|-------|
| Scripted input without a virtual controller | ✅ | [`tools/re-capture/pad.py`](../../tools/re-capture/pad.py) + Canary `--hid=file` | The old `vgamepad` path created its device through `/dev/uinput`, which is **not namespaced** — a pad made inside the container registers with the HOST's input stack, so every scripted press leaked to the desktop. Canary now carries a header-only driver that reads pad state from a **text file** (`--hid=file --pad_file=…`): no kernel device, nothing leaves the container, and analogue values are exact. ⚠️ The trap: 360 menus poll **`XamInputGetKeystrokeEx`**, not `GetState` — with `GetKeystroke` stubbed the pad looks completely dead on a title screen while its own log shows the press arriving. Implemented edge-triggered, no auto-repeat (scripted input wants one event per press). Proven end to end: boot → title → main menu → EXTRAS from the file alone |
| Live guest-memory write | ✅ | [`tools/re-capture/gpoke.py`](../../tools/re-capture/gpoke.py) | Write companion to `gmem.py`, same guest-VA → `/dev/shm` map; prints before/after per word. Used to confirm the challenge gate's cleared-stage mask on the running game |
| Live guest-memory read | ✅ | [`tools/re-capture/gmem.py`](../../tools/re-capture/gmem.py) | Canary backs the guest address space with `/dev/shm/xenia_memory_*`; guest VAs map in through Xenia's fixed table. Full-RAM search ~0.2 s (sparse, `SEEK_DATA`). No debugger, no emulator patch, game keeps running |
| IDXD object layout solver | ✅ | [`tools/re-capture/weapon_runtime.py`](../../tools/re-capture/weapon_runtime.py) | Scan RAM for a class's vtable → enumerate its objects → brute-force `(field, offset, encoding)` against the disc records. Accepts a binding only on **zero** contradictions. Generalizes to any IDXD-backed definition |
| Live entity state, anchored on the definition | ✅ | [`tools/re-capture/own_state.py`](../../tools/re-capture/own_state.py) · [autopilot](autopilot-memory-driven.md) | An undamaged craft holds its definition's own numbers, so a *solved definition field* locates the matching live field without a value scan: definition `HP` (1500) → **hull at `position+0x154`**, confirmed by a trace across a death (30/60/90 per hit, negative at 0). Reusable for any live counter whose maximum the definition carries |
| Mission / escort state, every entity's hull | ✅ | [`tools/re-capture/mission_state.py`](../../tools/re-capture/mission_state.py) · [escort state](mission-escort-state.md) | `hull = position + 0x154` is a property of the **entity class**, not of the player object: at t=0 it equals each entity's own definition `HP` across 7 classes and 5 distinct HP values (turret 100, fighter 500, destroyer 10000, cruiser 30000, **ACROPOLIS 25000**), falls under fire (780 damage events in 240 s), goes negative at death, and the object then leaves the heap. So an escort objective is scoreable live — `UN_f101_TCAF_Acropolis` measured at 25000 → 23038 over 240 s, attack starting only at t≈170 s. `REMAINING OB` counts objectives, not hostiles (012 on the HUD vs 118 live ADAN); its address **is found** — see [mission-objective-counter](structures/mission-objective-counter.md) — but it is **run-dependent** and must be re-derived per run (refuted as a constant 2026-08-26) |
| In-flight control mapping | ✅/🟡 | [`tools/re-capture/fire_probe.sh`](../../tools/re-capture/fire_probe.sh) · [controls](flight-controls-runtime.md) | Measured by holding each pad input and photographing the HUD ammo counters: **`RB` = nose gun** (6000→5956 in 4 s, ~11 rounds/s, HEAT rises), **`Y` = main mount** (missiles, 300→299), d-pad = **tactical map** overlay, nothing else moves a counter. No target-cycle input exists — the `TARGET` marker is present with nothing pressed, so targeting is automatic and a missile lock is **time-on-target**. That, not target choice or ballistics, is what caps lethality at 2 kills per 98 missiles |
| Throttle → speed law | ✅/🟡 | [flight-speed-law](flight-speed-law.md) | **The throttle selects a TARGET SPEED**, it does not add thrust: no input settles at ~420 (`CruisingVelocity` 350), `RT` at ~1 530 (`MaximumVelocity` 1200), `LT` at ~125 (`MinimumVelocity` 100), and releasing either returns to cruise. `Acceleration`/`Deceleration` govern the convergence rate (measured ~440560 units/s² against 500/600). 🟡 measured world speeds run ≈1.21.3× the definition numbers while the HUD shows the definition value exactly, so world units are a constant (~1.25) multiple of the definition's velocity unit |
| Input → dynamics calibration | ✅ | [`tools/re-capture/ctrl_probe.py`](../../tools/re-capture/ctrl_probe.py) · [`binq.py`](../../tools/re-capture/binq.py) | Hold each pad input in turn and measure the craft's speed as displacement/s of its own position triple — no speed field needed first. Settled the throttle: **`RT` accelerates, `LT` brakes, and the setting persists** (488 → 1510 → 174 units/s), overturning an earlier field-scan conclusion |
## Functions / code paths
| Function | Conf. | Reimpl. | Summary |
|----------|-------|---------|---------|
| Achievement table + XAM read path | ✅/🟡 | [achievements](structures/achievements.md) | The XEX's `XACH` resource (`.pe` `0x8FBCBC`, 36-byte records) defines **24 achievements summing to 1000G** — the retail total, which self-checks the stride and field offsets. `GamePart_Debriefing` (`0x8218CF38``0x82191B18`) does two things: it walks the on-disc `ACHIEVEMENTS_REQUIREMENTS` list (`tables.pak` #16, entries `ACHIEVEMENT01…24`, in achievement-id order — its `ShootDownAircrafts`/`ShootDownShips`/`ShootDownWeight`/`GetAllWeapons`/`GetAllAchievements` types line up with ids 1924 exactly as `XACH` names them), evaluating each and setting a bit; **and it enumerates `XACHIEVEMENT_DETAILS` from XAM** — 36-byte records confirmed by the `0x38E38E39`+`srawi 3` divide-by-36, `dwId` at `+0`, `dwFlags & 0x00020000` (`…_ACHIEVED`) at `+32`, behind a waited-then-closed async handle. **So earned state comes from the console profile, not the 545-byte save** — and the masks it builds are `1 << dwId`, i.e. **bit = the 1-based id**. **The challenge missions do NOT gate on this** — an earlier claim here, refuted by finding `+80`'s sole writer: it is `GamePart_StageClear` setting `1 << stage`, so that word is a *cleared-stage* mask and the shared "24" was a coincidence. `GetAllAchievements`/`GetAllWeapons` are requirement **types**, not debug cheats |
| Challenge-mission gate + stage-config switch | ✅ | [challenge-mission-gate](challenge-mission-gate.md) | **`GamePart_ChallengeMission` gates each of the six challenge missions on a CLEARED-STAGE bit**: `REQUIREMENT` absent or `"Always"` → available, else `n = atoi(v)` and it tests bit `n` of singleton `+80` (`n < 24`) or bit `n-24` of `+1956` (`n ≥ 24`) — which is the disc's own stage numbering (story 116 and tutorial 1823 below 24, challenge 2429 above). Word A's **sole writer** is `0x821C1820` in `GamePart_StageClear`, doing `1 << (this+84)` where `this+84` is the stage number (it also indexes a 20-byte per-stage record array and the debriefing `STAGE` sprite list). So `TimeAttack` needs **stage 16** — the last story mission — and the rest chain off challenge stages 2529. The six-mission table is on disc in `tables.pak` (schema `54a10697`). Separately, the stage loader picks its config section from a **mission-kind field at `object+144`**: `3``EXTRA`, `5`/`6``CHALLENGE`, anything else → `FILE`; two further sites treat `{3,5,6}` as one class. Constructed as `0` (`sub_821783D8`) and only ever *cleared* inside the class, so the kind comes from the launching GamePart, **not** from the stage number — which is a mechanism (🟡, unproven) for why patching the save's stage field to a challenge stage kills the load. Same note carries the **GamePart id table** (`0x820A1630`, 29 ids, `GP_CHALLENGE` = 26, cross-checked against the image's own `RegisterToFactory<26, …>` text) and the disc's **three stage families**`S01``S16` story, `S18``S23` tutorial, `S24``S29` challenge, plus `Test`, matching `weapon.tbl`'s 16 + 6 + 6 key set exactly. `GP_CHALLENGE.pak` holds **0 IDXD objects** — it is the menu screen; challenge missions reuse `GP_MAIN_GAME_E.pak`'s stage records |
## All RE notes (generated)
Every file under `docs/re/`. **Search this table before starting a new
investigation** — the index was previously hand-maintained and listed 20 of 43
files, which is how the same ground got covered twice.
| Note | Title | Status |
|------|-------|--------|
| [`SESSION-2026-08-11.md`](SESSION-2026-08-11.md) | Session log — 2026-08-11 (autonomous run) | — |
| [`arsenal-develop-economy.md`](arsenal-develop-economy.md) | The Arsenal develop economy, and what the save's blob indexes | — |
| [`autopilot-knowledge-sources.md`](autopilot-knowledge-sources.md) | Where the autopilot's missing knowledge lives on the disc | 🟡 this is a map, not the knowledge itself — it says which files |
| [`autopilot-memory-driven.md`](autopilot-memory-driven.md) | Memory-driven autopilot — build log and current state | 🟢 IT FLIES, KILLS AND SURVIVES — but it loses the mission anyway. |
| [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) | Getting past the title screen in the container — three traps and one blocker | ✅ CONFIRMED for the three traps (each reproduced, and two of them |
| [`challenge-mission-gate.md`](challenge-mission-gate.md) | Challenge / EX missions — the stage set, the GamePart graph, and the kind field | ✅ for the static structure (stage set, GamePart ids, the config-section |
| [`dynamic-re-state-restore.md`](dynamic-re-state-restore.md) | The container's dynamic-RE state is not durable — how to rebuild it | ✅ CONFIRMED by rebuilding it (2026-08-23). Everything the dynamic |
| [`flight-controls-runtime.md`](flight-controls-runtime.md) | In-flight control mapping — measured, not assumed | ✅ for the weapon bindings (ammo counters move), 🟡 for the rest (HUD |
| [`flight-speed-law.md`](flight-speed-law.md) | The throttle is a TARGET-SPEED selector — measured against the definition (2026-08-13) | ✅ for the shape of the law, 🟡 for the unit scale. |
| [`guest-stalls.md`](guest-stalls.md) | The guest stalls, often — and the probe now detects it | ✅ the stall witness works and is validated against independent |
| [`live-unit-definitions.md`](live-unit-definitions.md) | Live unit definitions from a running Stage 02 (2026-08-12) | — |
| [`mission-arrival-watch.md`](mission-arrival-watch.md) | Six runs, no arrival — and an accidental control | ✅ the deployment structure reproduces exactly; ✅ losses require the |
| [`mission-clock-advances.md`](mission-clock-advances.md) | The mission clock is running — the runs were too short in *game* time | 🔴 "the phase clock is stopped" is refuted; ✅ something advances |
| [`mission-escort-state.md`](mission-escort-state.md) | Escort / mission state from guest RAM — every entity's hull | ✅ CONFIRMED (2026-07-30). Capture: tools/re-capture/mission_state.py, |
| [`mission-freeze-and-ob-flag.md`](mission-freeze-and-ob-flag.md) | An in-mission freeze, and the first cut at what `REMAINING OB` counts | — |
| [`mission-freeze-resume-spin.md`](mission-freeze-resume-spin.md) | The in-mission freeze — the resume-spin lead is DEAD, and the freeze is a guest-side spin | 🔴 the reading this file was named for is REFUTED (2026-08-23, later |
| [`mission-liveness-probe.md`](mission-liveness-probe.md) | A motion-independent live roster — and what `n` probably is | ✅ the enumeration method; ✅ the hunting pilot gets kills; 🔴 "no |
| [`mission-objectives-text.md`](mission-objectives-text.md) | The mission script in plain English — and why phase 1 never completed | ✅ the localised string tables are decoded and readable; ✅ the phase |
| [`mission-outcome-stage02.md`](mission-outcome-stage02.md) | Why Stage 02 is never won — the escort sinks at ~11 minutes (2026-08-10) | ✅ measured, one 500 s run. The standing open item since 2026-07-29 was |
| [`mission-per-record-strength.md`](mission-per-record-strength.md) | Per-record craft strength — the measurement works, the run does not reproduce | ✅ the per-record measurement is internally consistent; 🔴 it does not |
| [`mission-phase-deployment.md`](mission-phase-deployment.md) | Enemies come into play per PHASE, deployed at phase start | 🟡 strongly supported and internally consistent, one unexplained |
| [`mission-phase-objectives.md`](mission-phase-objectives.md) | What each phase asks for — from the game's own objective text | ✅ pinned by a disc test (crates/sylpheed-formats/tests/phase_objectives_disc.rs). |
| [`mission-phase-runtime.md`](mission-phase-runtime.md) | Where the runtime phase state is *not* | ✅ the stage tables are resident in guest RAM, so the static decode |
| [`mission-wave-arrivals.md`](mission-wave-arrivals.md) | The arrival timetable, and what the entity table is really counting | ✅ the static arrival timetable is in Route_S<NN>.tbl; 🟡 the runtime |
| [`movie-subtitle-link.md`](movie-subtitle-link.md) | The movie ↔ subtitle ↔ voice link ✅ (static) | — |
| [`pilot-never-fires.md`](pilot-never-fires.md) | `pilot.py` never pulls the trigger — the proximal cause, measured | ✅ ROOT CAUSE FOUND AND FIXED (2026-08-23, later the same day) — the |
| [`probe-harness.md`](probe-harness.md) | A shared probe harness — so the same lessons stop being re-learned | ✅ built and verified on a live run. |
| [`remaining-ob-hunt.md`](remaining-ob-hunt.md) | Hunting REMAINING OB by correlation — method works, run did not finish | ✅ the correlation method is sound and demonstrated; 🔴 the hunt is |
| [`roster-to-craft-link.md`](roster-to-craft-link.md) | The 116 roster records and the ~300 live craft are not directly linked | 🔴 a direct pointer link is refuted in both directions; ✅ the two |
| [`ship-placement-capture-generalisation.md`](ship-placement-capture-generalisation.md) | Capital-ship placement — does the `e106` result generalise? (WIP, 2026-07-31) | 🚧 WIP, time-boxed session. Two results so far: a static audit across all |
| [`ship-placement-runtime-capture.md`](ship-placement-runtime-capture.md) | Capital-ship part placement — runtime capture (ground truth) | ✅✅ STATIC ASSEMBLY IS EXACT — no captures needed anymore |
| [`structures/achievements.md`](structures/achievements.md) | Achievements — the 24-entry table, and where the earned state comes from | — |
| [`idxd-legacy-reader-audit.md`](idxd-legacy-reader-audit.md) | The legacy IDXD string-pool reader vs the real field table — what the old numbers got wrong | 🟡 shape CONFIRMED by hand (`FCSRange`, `ShieldRatio`, hangar `Model`); disc-wide rates are single-source |
| [`structures/idxd-container.md`](structures/idxd-container.md) | The IDXD/IXUD container — record/field table, and the two beliefs it withdraws | ✅ CONFIRMED disc-wide, 7 750/7 750 objects and 1 271 462/1 271 462 named fields, zero failures |
| [`structures/hud-glyph-quad.md`](structures/hud-glyph-quad.md) | The HUD's glyph quad — vtable `0x820B2A64` | ✅ CONFIRMED for the object layout and the atlas size, read live off |
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` leading-stream offset is `first_riff % 2048`, not the constant 1392 | ✅ CONFIRMED by decoding — 85 of 140 sampled banks yield more audio (median 70×), 54 identical controls. ⚠️ The *cause* is a segment-packing phase, not a header: `X = (cumulative .pNN start) mod 2048`. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
| [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer — two sequential countdowns, not a limit and a warning | ✅ CONFIRMED — read from `sub_822639B8`; the threshold reading is |
| [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | ✅ CONFIRMED — the advance is read from `sub_822710D0`, the unit from |
| [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue |
| [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value |
| [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — |
| [`structures/mcol-collision.md`](structures/mcol-collision.md) | `MCOL` — same container and map parameters as `REGN` | 🟡 OPENED — `POF0` at `data_size+16` 11/11, bbox pad words 11/11, `extent == maxmin` 11/11, and bbox + cell-size distributions identical to `REGN` (2/6/3 and 2/9); everything past `0x40` ❔ |
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a per-map spatial grid (and `MCOL` beside it) | ✅ CONFIRMED for the header, which self-checks on all 11 objects on |
| [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) — container ✅ exact, 3 fields named ✅, rest ❔ (2026-08-11) | ✅ CONFIRMED for the container and the chunk layout — parsed off the |
| [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio — `sound.pak` / `.slb` / XMA1 | — |
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across |
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
| [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | ✅ CONFIRMED by measurement over all 965 screen builds on the disc, |
| [`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md) | The paint order comes from a layer key in the T8aD sprite header | ✅ CONFIRMED on both screens whose paint order has been measured — |
| [`structures/ui-prm-primitives.md`](structures/ui-prm-primitives.md) | `.prm` elements are untextured full-screen quads, and `kind` bit `0x10` says so | ✅ CONFIRMED statically across all 965 screen builds on the disc. |
| [`structures/ui-rat-layout.md`](structures/ui-rat-layout.md) | `.rat` — the UI element layout / animation record | ✅ CONFIRMED for placement (2026-07-28). The retail UI can be |
| [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md) | A keyframe is the start of a ramp, not a pose that is held | ✅ CONFIRMED against the framebuffer capture of the running title |
| [`structures/ui-screen-runtime.md`](structures/ui-screen-runtime.md) | The UI screen object at runtime — found in live guest memory | ✅ CONFIRMED for the object's identity and its element array (five |
| [`structures/unit-group-table.md`](structures/unit-group-table.md) | `stage\UnitGroup_S<NN>.tbl` — the per-stage squadron roster | ✅ container format and field semantics, validated across all 28 stage |
| [`structures/unit-struct-runtime.md`](structures/unit-struct-runtime.md) | Runtime `Unit` struct (craft / vessel definitions) — read from live guest memory | — |
| [`structures/weapon-struct-runtime.md`](structures/weapon-struct-runtime.md) | Runtime `Weapon` / `Shell` structs — read from live guest memory | — |
| [`structures/xbg7-mesh.md`](structures/xbg7-mesh.md) | XBG7 — mesh geometry (inside XPR2 model containers) | — |
| [`title-crash-stl-tree.md`](title-crash-stl-tree.md) | The title-screen crash is an STL `map`/`set` erase on a bad iterator | ✅ CONFIRMED — the guest throws std::out_of_range from an STL |
| [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously |
| [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but |
| [`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md) | The title screen's paint order, measured from the guest's draw submissions | ✅ CONFIRMED — the order in which the running game paints the title |
| [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad |
| [`weapon-datasheet-runtime.md`](weapon-datasheet-runtime.md) | Weapon DATA SHEET — runtime capture (Route B) | 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a |
| [`xpr2-colour-check.md`](xpr2-colour-check.md) | XPR2 colours: channel order ✅ confirmed against the running game | — |