diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 85c3108..9773e93 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -30,8 +30,8 @@ authored version can be deleted. | Q1 | keyframe time unit + ramp shape | ✅ answered | ramp is **linear**; the clock advances **2 units per rendered frame**; working conversion **1 unit = 1/60 s** — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) | | Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`ui-title-build-map.md`](../re/ui-title-build-map.md) | | Q3 | paint order for the six screens | ✅ answered | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md) | -| Q4 | button → GamePart | ❔ open | labels are baked into sprites | -| Q5 | navigation semantics | ❔ open | | +| Q4 | button → GamePart | 🟡 mostly answered | **measured** which screen each button opens (4 of 5; `NEW GAME` untested — it hangs). The **GamePart id is a name match** onto the decoded id table, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | +| Q5 | navigation semantics | ✅ answered | **measured**: opens on `TUTORIAL` (not the top); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | | Q6 | boot sequence + what drives it | 🟡 partial | order observed, and the **attract cycle is timed**: ~8–10 s idle on the title → fade to black → ~85 s of video → title again, plate and all. The driver is still not decoded | | Q7 | transitions | ❔ open | | | Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not | @@ -94,6 +94,20 @@ authored version can be deleted. blend of **one element on one screen** (a title glow). Take the stable sort and accept that. +* **Menu movement, measured off the running game.** ⬆⬇ move one item per press + and **wrap at both ends** (5-item main menu and 3-item `EXTRAS` both). ⬅➡ do + nothing. Ⓑ goes up one level **and restores focus to the item you came from**; + Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. The main + menu opens focused on **`TUTORIAL`**, the middle item — 🟡 reproducible in this + harness (2/2) but a third recorded run implies `NEW GAME`, so re-test before + hardcoding it. All **measured**, none of it on the disc. +* **Each button's destination is measured; its GamePart id is not.** + `LOAD GAME` → the save-slot list, `TUTORIAL` → the lesson list, `OPTIONS` → + the settings menu, `EXTRAS` → `GP_TITLE` build 6, `EXTRAS ▸ MISSION SELECT` → + the stage list. ❔ `NEW GAME` is untested — Ⓐ on it hangs the emulator. The + GamePart ids (`3`, `25`, `8`, `5`, `7`) are the entries of the decoded id table + whose **names match the screens seen**; that binding is authored, not measured. + * **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the executable's own registration strings. ✅ This is the screen vocabulary; which button reaches which entry is Q4 and is *not* part of it. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 719c04b..a9831e1 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -149,6 +149,7 @@ files, which is how the same ground got covered twice. | [`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 | +| [`menu-navigation-semantics.md`](menu-navigation-semantics.md) | The title menu — how it moves, and where each button goes | ✅ measured: wraps both ends, Ⓑ restores focus, ⬅➡ inert; 4 of 5 destinations driven. 🟡 GamePart id is a name match, ❔ `NEW GAME` untested | | [`ui-title-build-map.md`](ui-title-build-map.md) | Which `GP_TITLE` build is which screen state | ✅ CONFIRMED for title / `PRESS Ⓐ` / main menu / `EXTRAS` against live captures; the archive is 8 screens × EN/JP, and "6/8/9 are submenus" is withdrawn | | [`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 | diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index b216164..279427a 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -117,16 +117,26 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the * **Measure animation in submitted frames, not in seconds.** `VdSwap` counts are the guest's own frames, so an emulator at 80 % of real time does not move them; a stopwatch reading does, silently and by an unknown factor. -* **This game's menus drop d-pad presses shorter than ~0.3 s.** `pad.py dpad` - defaults to 0.06 s, and its docstring says longer "auto-repeats and - overshoots". On the title main menu that default is *dropped*: four presses at - 0.12 s moved the cursor one step, four at 0.20 s moved it none, while - 0.30/0.50/0.80 s each moved it exactly one step and none of them repeated. A - scripted navigation that comes out one item short is this, not a wrong item - count — screenshot after every step and check the cursor rather than trusting - the press count. +* **~~This game's menus drop d-pad presses shorter than ~0.3 s.~~ WITHDRAWN + 2026-08-28 — the menu WRAPS, and I had not measured that.** The claim came from + reading a cursor that ended up "one item short"; once wrap-around at both ends + was measured ([`menu-navigation-semantics.md`](menu-navigation-semantics.md)), + every one of those press counts is exactly right — four presses at 0.12 s moved + four steps *through the bottom*, which lands one above where a non-wrapping + menu would put it. **No press was ever dropped.** The real lesson is the + general one: *a step count is only readable once you know the topology*, and I + invented a hardware-flakiness story rather than testing the ends of the list. + Still true and worth keeping: screenshot after every step and read the cursor, + rather than trusting arithmetic over the press count. * **A screenshot taken right after a transition can catch a screen mid-fade.** A grab 2.5 s after Ⓑ returned the game to the title showed the title art with no `PRESS Ⓐ BUTTON` plate; one second later the plate was there. That very nearly went into the corpus as "the returned title has no plate". Sample a changing screen several times before writing down what it does *not* contain. +* **Do not identify a menu cursor by label brightness.** The obvious oracle — + "the focused label is the brightest row" — fails on this game's menus, because + the background art is brighter behind some rows than the highlight is. It + confidently named the wrong item on a frame whose ring was plainly elsewhere. + Detect the **focus ring** in the gutter left of the labels instead + (`tools/re-capture/menu_focus.py`, 254 vs <82 — no threshold tuning needed), + and look at the PNG before believing either. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 0278c0a..38d2ffe 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -35,6 +35,12 @@ neighbourhood, not just the line. ## Screens, classes and RTTI +* "the title menus drop d-pad presses shorter than ~0.3 s" → **refuted by my + own data.** The menu **wraps at both ends**; every press registered, and the + "missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md). +* "the main menu opens with `NEW GAME` focused" → it opens on **`TUTORIAL`**, + 2/2 boots (🟡 a third recorded run implies `NEW GAME`, so this is reproducible, + not invariant). * "`GP_TITLE` builds 6/8/9 are three submenus" → **8 is the JAPANESE main menu**, and 6/9 are the English and Japanese `EXTRAS` submenu. `GP_TITLE` holds eight screens shipped twice (EN/JP), and exactly one submenu. See diff --git a/docs/re/captures/menu-nav/extras-wrap.png b/docs/re/captures/menu-nav/extras-wrap.png new file mode 100644 index 0000000..54bc0f3 Binary files /dev/null and b/docs/re/captures/menu-nav/extras-wrap.png differ diff --git a/docs/re/captures/menu-nav/q4-destinations.png b/docs/re/captures/menu-nav/q4-destinations.png new file mode 100644 index 0000000..dac3636 Binary files /dev/null and b/docs/re/captures/menu-nav/q4-destinations.png differ diff --git a/docs/re/captures/menu-nav/wrap-montage.png b/docs/re/captures/menu-nav/wrap-montage.png new file mode 100644 index 0000000..dcfa2f9 Binary files /dev/null and b/docs/re/captures/menu-nav/wrap-montage.png differ diff --git a/docs/re/menu-navigation-semantics.md b/docs/re/menu-navigation-semantics.md new file mode 100644 index 0000000..5874e2f --- /dev/null +++ b/docs/re/menu-navigation-semantics.md @@ -0,0 +1,77 @@ +# The title menu — how it moves, and where each button goes + +**Status:** ✅ `CONFIRMED` (**measured**, by driving the running game) for the +movement rules and for four of the five main-menu destinations. 🟡 the GamePart +*id* behind each destination is a **name match onto the decoded id table**, not a +measurement. ❔ `NEW GAME` deliberately untested. + +Answers [MISSION Q5](../port/MISSION.md) and most of Q4. Nothing here is on the +disc in any form found so far — the port is **authoring** these rules from this +page, not transcribing a field. + +## Q5 — movement + +Two boots via `tools/re-capture/boot_menu.sh`, cursor read off the focus ring +with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py). + +| | behaviour | evidence | +|---|---|---| +| **initial focus, main menu** | **`TUTORIAL`** — the *middle* item, not the top | 2/2 boots, the first frame after the menu appears | +| **initial focus, `EXTRAS`** | `MISSION SELECT` — the top item | [`extras-wrap.png`](captures/menu-nav/extras-wrap.png) | +| **up / down** | one item per press, no auto-repeat at the durations tried | | +| **wrap at the top** | ⬆ from the first item goes to the **last** | [`wrap-montage.png`](captures/menu-nav/wrap-montage.png), panels 1→2 | +| **wrap at the bottom** | ⬇ from the last item goes to the **first** | same, panels 3→4, and 4 presses from `EXTRAS` landing on `OPTIONS` — i.e. wrapping — is what makes the count come out | +| **left / right** | **nothing**, on the main menu | cursor unmoved across one ⬅ and one ➡ | +| **Ⓑ on a submenu** | returns to the parent **with focus restored to the item you entered from** — `LOAD GAME`→`LOAD GAME`, `TUTORIAL`→`TUTORIAL`, `OPTIONS`→`OPTIONS`, `EXTRAS`→`EXTRAS` | 4/4 | +| **Ⓑ on the main menu** | goes to the **title**, which re-draws `PRESS Ⓐ BUTTON` after a beat | | +| **Ⓑ on the title** | **nothing** | | + +Wrap holds on both screens tested — the 5-item main menu and the 3-item `EXTRAS` +submenu — so it is a menu rule, not a per-screen table. + +**🟡 Initial focus is reproducible but not established as invariant.** Both of my +boots opened on `TUTORIAL`, and both used `boot_menu.sh`. The run recorded in +[`menu-state-in-memory.md`](menu-state-in-memory.md) reached `EXTRAS` with *four* +downs from the main menu, which only works from `NEW GAME`. Either the harness +path matters or something persists. **Do not hardcode `TUTORIAL` without +re-testing it**; what is solid is that the menu does *not* open on the top item +in this harness. + +## Q4 — where each button goes + +Measured by driving: focus the item, press Ⓐ, read the screen's own title. + +| button | screen it opens | evidence | GamePart id | +|---|---|---|---| +| `NEW GAME` | ❔ **not tested** | — | — | +| `LOAD GAME` | the save-slot list, `LOAD GAME` / `Current Storage` | [`q4-destinations.png`](captures/menu-nav/q4-destinations.png) left | 🟡 `3 GP_LOAD` | +| `TUTORIAL` | the lesson list, `TUTORIAL`, Level 1 / Level 2 | same, middle | 🟡 `25 GP_TUTORIAL` | +| `OPTIONS` | `OPTIONS` — GAME / CONTROL / SOUND / SCREEN SETTINGS / BACK | same, right | 🟡 `8 GP_OPTIONS` | +| `EXTRAS` | **`GP_TITLE.pak` build 6** — MISSION SELECT / MOVIE THEATER / BACK | [`ui-title-build-map.md`](ui-title-build-map.md) | 🟡 `5 GP_EXTRAS` | +| `EXTRAS ▸ MISSION SELECT` | the stage list + Wide Area Space Map | | 🟡 `7 GP_MISSION_SELECT` | +| `EXTRAS ▸ MOVIE THEATER` | ❔ not tested | | 🟡 `6 GP_MOVIE_THEATER` | + +**Say which, as the gate asks.** The *screen* each button opens is **measured** — +I pressed the button and read the title off the framebuffer. The **GamePart id is +not measured**: it is the entry of the decoded 29-id table at `.rdata 0x820A1630` +([`challenge-mission-gate.md`](challenge-mission-gate.md) §3) whose *name* matches +the screen I saw. The table is decoded; the *binding* of a button to an entry in +it is a name match I made by eye. The port should treat these ids as authored. + +Worth noting that the ids and the paks are not one-to-one: `GP_EXTRAS` is id 5 +with **no pak of its own** — its artwork is a build inside `GP_TITLE.pak`. + +**❔ `NEW GAME` was deliberately not pressed.** Ⓐ on it leads to a standing +black-screen hang that ends the run +([`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md)), +and this iteration needed the session. It is the one destination still unmeasured. + +### The cheap way to finish this, and to make it a measurement + +`0x828A690C` holds a **live screen id** — `1` title, `3` main menu, `4` extras — +and `0x828F38AC` the cursor ([`menu-state-in-memory.md`](menu-state-in-memory.md)). +Reading those while pressing Ⓐ turns "the screen said OPTIONS" into a measured +transition, and works under `--gpu=null` with no screenshots at all. ⚠️ Note that +those values are **not** GamePart ids — `GP_TITLE` is GamePart 0 and `GP_EXTRAS` +is 5, but the word reads 1 and 4 — so it is a third enumeration and mapping it to +the id table is itself unfinished work. diff --git a/tools/re-capture/menu_focus.py b/tools/re-capture/menu_focus.py new file mode 100644 index 0000000..e75d78b --- /dev/null +++ b/tools/re-capture/menu_focus.py @@ -0,0 +1,11 @@ +import sys +from PIL import Image +import numpy as np +names=["NEW GAME","LOAD GAME","TUTORIAL","OPTIONS","EXTRAS"] +ys=[166,241,315,390,465] +for p in sys.argv[1:]: + a=np.asarray(Image.open(p).convert("L"),dtype=float) + # the focus RING sits in the gutter left of the label; nothing else is bright there + v=[a[y-20:y+20,500:542].max() for y in ys] + i=int(np.argmax(v)) + print(f"{p.split('/')[-1]:26s} -> {names[i]:10s} " + " ".join(f"{x:5.0f}" for x in v))