docs/re: the main menu HAS been reached — the Ⓐ blocker is withdrawn

The previous entry called the title's Ⓐ a hard blocker. It is not: the main menu
is reached and screenshotted (NEW GAME / LOAD GAME / TUTORIAL / OPTIONS /
EXTRAS). What is true is narrower — Ⓐ advances the title only intermittently,
about one attempt in four, with the press verifiably delivered every time and no
Xenia UI active.

Three candidate causes were eliminated with measurements rather than argument:

* IsUIActive is now observable (Canary logs when it swallows a keystroke) and it
  never fires on the failing runs;
* the driver filter is fine — the game polls with flags=3 and the file pad
  reports Controller=1, so FilterDrivers keeps it;
* the game makes no content/user/signin call on the press at all — tracing every
  Xam call around it shows only input polling.

And two traps in my own measuring rig, which cost more than the bug and are
written down so nobody repeats them: a FIFO trace consumer that exits STALLS the
emulator (the guest stops polling — indistinguishable from a dead pad, and it
produced two runs of false evidence), and phase-A's kernel.return events carry a
placeholder return_value of literally 0, so "every keystroke call returns
SUCCESS" was an artifact of the logger, not a finding.
This commit is contained in:
Sylpheed RE agent
2026-08-18 22:18:51 +00:00
parent 44ff5454f9
commit f5603aa107
4 changed files with 54 additions and 54 deletions

View File

@@ -26,7 +26,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| 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 | ✅/🟡 | [title paint order](ui-title-paint-order-capture.md) | **Measured from the guest's draw stream** (new Canary hook `log_ui_draws`, F10): the title screen paints 11 draws a frame through one sprite shader, and the submission order is background → `back2` glow → `ptlogo1`+`tm``ptlogo2``ptcopyright` → the `PRESS Ⓐ BUTTON` plate. That refutes declaration order, reverse order, keyframe-start order and resting-time order, and shows the screen composites **two bundles** (build 7 + the one-element build 2). 🟡 the rule that produces the order is still unknown |
| Scripted input / profile traps | ✅/🔴 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Three reasons a scripted run "cannot press Ⓐ": **F10 opens the emulator menu bar**, which makes `IsUIActive()` true and zeroes every guest keystroke; the file-pad is not the problem (it delivers, and Ⓐ skips movies); and the title needs a **signed-in profile**, which the container cannot type into ImGui — hence Canary's new `--create_profile_if_none`. 🔴 Past that, Ⓐ crashes the guest with `--mem_watch=true` and stalls it with `--mem_watch=false` — a cheap second reproduction of the Ready-Room crash, 40 s from a cold boot |
| 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) |