re: the title menu wraps -- Q5 measured, Q4 driven, and one of my own

method lines withdrawn

Q5, measured off two boots: up/down move one item and WRAP at both ends
on the 5-item main menu and the 3-item EXTRAS alike; left/right do
nothing; B goes up one level and restores focus to the item you came
from (4/4); B on the main menu returns to the title; B on the title does
nothing. The menu opens on TUTORIAL -- the middle item -- 2/2, though a
third recorded run implies NEW GAME, so that one is reproducible rather
than invariant and says so.

Q4 by driving: LOAD GAME opens the save-slot list, TUTORIAL the lesson
list, OPTIONS the settings menu, EXTRAS build 6, MISSION SELECT the stage
list. NEW GAME is not tested -- A on it hangs the emulator and this
iteration needed the session. The GamePart ID behind each is NOT
measured: it is the entry of the decoded id table whose name matches the
screen I saw, and the page says so rather than wearing the badge.

And the withdrawal. Last iteration I wrote that these menus drop d-pad
presses shorter than ~0.3s. They do not. Once wrap is measured, every
press count I had is exactly right -- four presses moved four steps
THROUGH the bottom, which lands one above where a non-wrapping menu
would. I invented hardware flakiness instead of testing the ends of the
list. METHOD keeps the withdrawal rather than deleting the line.

Also: label brightness is not a cursor oracle here -- the background art
outshines the highlight on some rows. menu_focus.py reads the focus ring
instead, 254 vs <82, no tuning.
This commit is contained in:
Sylpheed RE agent
2026-08-28 17:53:46 +00:00
parent bebdb18a02
commit 4e745c8177
9 changed files with 129 additions and 10 deletions

View File

@@ -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 |

View File

@@ -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.

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -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.