1. NO AUTO-REPEAT. A 2.0 s held DOWN moves the cursor exactly once. The counter passes its control first: a single 0.12 s tap gives exactly 1 spike and the hold gives 1, with the move spike at 0.0202-0.0220 against a 0.0003-0.0038 noise floor. The port had flagged that the hedge 'at the durations tried' was carrying the claim, and it was -- nothing recorded a HELD direction. 2. B ON THE SETTLED TITLE DOES NOTHING. Twenty seconds after a delivery-confirmed B the screen is still the title with PRESS (A) BUTTON up, read off a capture that names itself. This is the run the previous attempt could not be: it waited for the plate pulse, the title's own settled signature, instead of pressing during the build-in. 3. THE PLATE IS RE-DRAWN after B from the menu -- pressed at 351.2 s, pulse detected at 358.5 s. That was the other unevidenced half of the B-on-menu row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
274 lines
16 KiB
Markdown
274 lines
16 KiB
Markdown
# 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) |
|
||
| **⬆⬇ — one item per press** | one item per press | ✅ *indirect but sound*: the wrap montage's **4 presses from `EXTRAS` landing on `OPTIONS`** only counts out if each press moves exactly one |
|
||
| **⬆⬇ — no auto-repeat** | ✅ **none** — a **2.0 s held ⬇ moves the cursor exactly once** | measured 2026-08-30, [`data/nav-autorepeat-and-settled-b.txt`](data/nav-autorepeat-and-settled-b.txt). ✅ **The counter passes its control**: a single 0.12 s tap gives exactly **1** spike, and the hold gives **1**. Spike 0.0202–0.0220 against a 0.0003–0.0038 floor. ⚠️ one hold, 2.0 s, ⬇, main menu |
|
||
| **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** — measured 2026-08-30, delivery-confirmed, **≤ 0.4 s**, and with **no loading screen** in between. ✅ **and the plate IS re-drawn**: Ⓑ on the menu at 351.2 s, plate pulse detected at 358.5 s — ~7 s later (2026-08-30) | **1 run** — [`../re/data/b-on-main-menu.txt`](data/b-on-main-menu.txt) · [mid-build-in](captures/title-builds/live-b-on-menu-title-buildin.png) · [settled](captures/title-builds/live-b-on-menu-title-settled.png). The footer point below still stands |
|
||
| **Ⓑ on the title** | ✅ **nothing** — 20 s after a delivery-confirmed Ⓑ the screen is still the title, `PRESS Ⓐ BUTTON` up | measured 2026-08-30, [capture](captures/title-builds/live-b-on-settled-title-no-effect.png) · [series](data/nav-autorepeat-and-settled-b.txt). ✅ **This run waited for the plate pulse — the title's own settled signature — before pressing**, which is what the earlier attempt failed to do; its press landed during the build-in and was confounded. The 4.3 % of pixels that move are the plate's pulse and the sweeps (glyph 730, inside the 714–1520 pulse band) |
|
||
|
||
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 — **8 rows visible of 16**, and rows below the first are **locked** on a fresh save ([below](#-mission-select-the-cursor-was-stuck-because-the-stages-were-locked)) | [`mission-select-stage01-only.png`](captures/mission-select-stage01-only.png) | 🟡 `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" was a dead route — WITHDRAWN 2026-08-29
|
||
|
||
This section used to say: *`0x828A690C` holds a live screen id (1 title, 3 main
|
||
menu, 4 extras) and `0x828F38AC` the cursor; read them while pressing Ⓐ and the
|
||
transition becomes a measurement, under `--gpu=null` with no screenshots.*
|
||
|
||
**Do not do that. Those words are monotonic counters, not state.**
|
||
[`menu-state-in-memory.md`](menu-state-in-memory.md) withdrew that identity **on
|
||
the same day it was published** — driving deep and then pressing Ⓑ three times
|
||
sends the "cursor" `36 → 38 → 40 → 41`, and a cursor returns when you go back.
|
||
The values match across runs because the same key sequence produces the same
|
||
count, not because `3` *means* main menu. With the sign-in fix the sequence runs
|
||
`3 → 5`, skipping `4` entirely, so "4 = extras" was never a screen at all.
|
||
|
||
This page kept recommending the route for three days after the page it cited had
|
||
killed it. ⚠️ **A cross-reference is not a citation unless you re-read the target**
|
||
— the two documents disagreed in the corpus and either would have been believed
|
||
on its own.
|
||
|
||
✅ **What those words are still good for**: all three advance if and only if the
|
||
game responds to input, and are stable when it does not. Read them as *"did the
|
||
game react?"*, never as *"which screen is this?"*. Screen identity still has to
|
||
come off the framebuffer.
|
||
|
||
❔ **So a measured button→GamePart-id binding remains unfinished**, and for the
|
||
reason it always was: no screen/state enum has been located in guest memory. The
|
||
ids in the table below stay a name match.
|
||
|
||
## ✅ `NEW GAME` — measured 2026-08-28, and it is not a hang
|
||
|
||
The one destination this page could not test has been driven. **Ⓐ on `NEW GAME`
|
||
does not hang.** It opens two more menus first:
|
||
|
||
```
|
||
NEW GAME -> DIFFICULTY -> SELECT DATA -> guest crash
|
||
```
|
||
|
||
* **`DIFFICULTY`** — `EASY` / `NORMAL` / `HARD` / `BACK`, footer
|
||
`Ⓐ : OK Ⓑ : Back`, opening focused on **`NORMAL`**
|
||
([capture](captures/newgame-path/newgame-difficulty.png)). It sat unchanged for
|
||
90 s with the emulator healthy — a menu waiting for input, which is exactly
|
||
what "a standing hang" looks like to a screenshot loop that never presses
|
||
anything.
|
||
* Ⓐ on `NORMAL` → **`SELECT DATA`**, a save-slot picker headed
|
||
`Current Storage: Dummy HDD`, prompting to pick a file for the auto-save.
|
||
* Then the guest throws, and Xenia pauses with `PC: 0x82307128`
|
||
([capture](captures/newgame-path/newgame-selectdata-crash.png)).
|
||
|
||
**That crash is already in the corpus and is not new**: `0x82307128` is inside
|
||
`sub_823070B0`, the cache-manager STL erase documented in
|
||
[`title-crash-stl-tree.md`](title-crash-stl-tree.md), whose trigger is an
|
||
incomplete on-disc shader/code cache — not the menu path. The corpus also already
|
||
holds `captures/select-data-crash.png`.
|
||
|
||
So Q4's last row closes as **measured**:
|
||
|
||
| button | screen it opens |
|
||
|---|---|
|
||
| `NEW GAME` | **`DIFFICULTY`** (then `SELECT DATA`) |
|
||
|
||
🟡 The GamePart ids for these two are unmeasured, like the rest — `24 GP_DIALOG`
|
||
and `3 GP_LOAD` / `2 GP_SELECT_STORAGE` are name-match candidates and nothing
|
||
more.
|
||
|
||
### Initial focus — six data points now, and it still varies
|
||
|
||
This boot opened the main menu on **`NEW GAME`**. Running tally across four
|
||
boots of the same harness: `TUTORIAL`, `TUTORIAL`, `NEW GAME`, `NEW GAME`.
|
||
Unchanged conclusion: **do not hardcode it.**
|
||
|
||
✅ **Two more, 2026-08-29, and the split is now even.** A drive that pressed Ⓐ
|
||
with no d-pad movement ended in a **tutorial mission** (+0.960 against
|
||
`tutorial-mission-reached-then-crash.png`), so that boot opened on `TUTORIAL`. A
|
||
later boot read **`NEW GAME`** from a focus detector on the first menu frame.
|
||
|
||
**Tally: `TUTORIAL` ×3, `NEW GAME` ×3.** Six boots, same harness, no other item
|
||
ever seen. ⚠️ It is *not* uniform across the five buttons — only these two occur —
|
||
which is a constraint on whatever selects it, and something a future explanation
|
||
has to account for. Still **do not hardcode it**, and note that
|
||
`newgame_path.sh`'s "NEW GAME is the first item so no d-pad is needed" is wrong
|
||
half the time — see [`s00a-drive-blocked-by-focus.md`](s00a-drive-blocked-by-focus.md).
|
||
|
||
### The `NEW GAME` path completes — the `SELECT DATA` crash is state, not path
|
||
|
||
A second run of the same path, 2026-08-28, **did not crash**:
|
||
|
||
```
|
||
NEW GAME → DIFFICULTY → (Ⓐ on NORMAL) → SELECT DATA → (Ⓐ on a slot) → a movie plays
|
||
```
|
||
|
||
The previous run's throw at `PC 0x82307128` is therefore **not inherent to this
|
||
menu path** — the same six presses got through it. That is consistent with the
|
||
trigger `title-crash-stl-tree.md` already names (an incomplete on-disc cache) and
|
||
with nothing about `NEW GAME`. 🟡 n = 1 either way; do not read it as "fixed".
|
||
|
||
Worth recording because the first observation could easily have hardened into
|
||
"the new-game path crashes", which is what "A on NEW GAME hangs" had already
|
||
become once.
|
||
|
||
---
|
||
|
||
## 🟡 Refutation attempt 2026-08-29 — the main menu's own footer does **not** advertise Ⓑ
|
||
|
||
**Attempted claim:** this page's row *"Ⓑ on the main menu goes to the title,
|
||
which re-draws `PRESS Ⓐ BUTTON` after a beat"*.
|
||
|
||
**Why this row and not another.** It is one of only **two** rows in the Q5 table
|
||
with an **empty evidence cell** (the other is "Ⓑ on the title → nothing"); every
|
||
row that cites a capture cites one. And it is a rule the port will build on
|
||
directly — it is the only way out of the main menu.
|
||
|
||
**The measurement** — whole-frame colour test for the pad-glyph discs, run by
|
||
[`tools/re-capture/footer_and_locked_rows.py`](../../tools/re-capture/footer_and_locked_rows.py)
|
||
against the committed captures:
|
||
|
||
| capture | Ⓐ glyph px | Ⓑ glyph px |
|
||
|---|---|---|
|
||
| `live-main-menu.png` | 438 | **0** |
|
||
| `live-main-menu-options-focused.png` | 438 | **0** |
|
||
| `live-extras.png` | 440 | 514 |
|
||
| `difficulty-screen.png` | 438 | 518 |
|
||
|
||
**The control passes twice over.** The same detector, unchanged, finds the red Ⓑ
|
||
on the two screens that visibly have one; and the **Ⓐ** count is 438/438/440/438
|
||
across all four, i.e. the same glyph asset at the same size on every screen — so
|
||
a Ⓑ of that family would have been ~450–520 px and cannot have fallen under a
|
||
threshold. The negative is over the **whole frame**, not a guessed footer band:
|
||
`live-main-menu.png` contains **zero** red-glyph pixels anywhere.
|
||
|
||
So the main menu's legend reads `⊙ : Select Ⓐ : OK` where every submenu reads
|
||
`⊙ : Select Ⓐ : OK Ⓑ : Back`.
|
||
|
||
**Verdict: the claim SURVIVES, at reduced confidence, and the row is downgraded
|
||
to 🟡.** A legend is not behaviour — a game may accept an unadvertised Ⓑ — so an
|
||
absent glyph cannot refute a press that was actually observed. But:
|
||
|
||
* the observation has **no capture behind it**, and it is now the only Q5 row
|
||
contradicted by the game's own on-screen text;
|
||
* there is a **named confound**: the title-side screens auto-return on idle, and
|
||
"I pressed Ⓑ and ended up at the title, which drew `PRESS Ⓐ BUTTON` after a
|
||
beat" is also exactly what an idle timeout looks like to an observer who does
|
||
not hold the two apart. The corpus documents that timeout at ~8–10 s
|
||
([`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)).
|
||
|
||
**What would settle it:** press Ⓑ on the main menu and read `0x828A690C`, the
|
||
live screen id (`1` title, `3` main menu, `4` extras) — a transition inside a
|
||
second is a Ⓑ, one at ~8–10 s regardless of the press is the timeout. Cheap, and
|
||
it needs no screenshots.
|
||
|
||
🔴 **Not runnable here.** This container has no disc and no ISO, so there is no
|
||
oracle at all — see
|
||
[`capture-harness-status.md`](capture-harness-status.md#-2026-08-29--the-disc-is-not-in-the-decoder-container-at-all).
|
||
|
||
**For the port:** Ⓑ from the main menu to the title is **measured, single
|
||
observation, uncited, and unadvertised by the game**. Implement it — it is the
|
||
only exit — but treat it as authored rather than transcribed, and do not also
|
||
build the idle-return on the assumption that the two are distinct until someone
|
||
has separated them.
|
||
|
||
---
|
||
|
||
## ✅ MISSION SELECT: the cursor was stuck because the stages were **locked**
|
||
|
||
**Measured 2026-08-29 from committed captures**, no disc needed. Settles the
|
||
⚠️ open in [`../game/navigation.md`](../game/navigation.md): *"sixteen d-pad
|
||
presses never left Stage 01 — whether that is because only one stage was
|
||
unlocked, or because the list is driven some other way, is unknown"*.
|
||
|
||
The stage list has **three** label brightnesses, not two, and that is what
|
||
discriminates. Sampling the label strip (x 190…320) of each of the 8 visible
|
||
rows, 95th percentile of luminance:
|
||
|
||
| capture | row 1 | rows 2–8 |
|
||
|---|---|---|
|
||
| `mission-select-stage01-only.png` | **254** | **104** |
|
||
| `mission-select-all-story-unlocked.png` | **254** | **183** |
|
||
| `mission-select-ends-at-stage16.png` | 183 | 183 ×6, then **254** on row 8 |
|
||
|
||
* **254** = focused (the row carrying the spinning focus ring)
|
||
* **183** = unlocked, not focused
|
||
* **104** = **locked**
|
||
|
||
The all-unlocked capture is the control: it holds row 1 focused at the identical
|
||
254 while rows 2–8 move 104 → 183 as one uniform step. So the dim rows in the
|
||
Stage01-only capture are **not** "unfocused"; unfocused is 183, and they are
|
||
79 levels below it.
|
||
|
||
**And the cursor does move when they are unlocked.** In
|
||
`mission-select-ends-at-stage16.png` the list has scrolled to show Stage09…16,
|
||
the scrollbar thumb is at the bottom, and the focus ring is on **Stage16** — the
|
||
last row. Locked list: 16 presses, no movement. Unlocked list: the cursor reaches
|
||
the end.
|
||
|
||
| | |
|
||
|---|---|
|
||
| **rows visible at once** | **8** |
|
||
| **list length** | **16** (`Stage01`…`Stage16`; the scrollbar bottoms out at 16) |
|
||
| **why 16 presses did nothing** | every row below the first was locked |
|
||
|
||
⚠️ **Reach.** This is a still image, so it says the cursor *reached* Stage16, not
|
||
how it got there and not whether the list wraps — the scroll thumb bottoming out
|
||
at row 16 is consistent with either. Whether a *locked* row is skipped or simply
|
||
unreachable is likewise not separated: with only row 1 unlocked the two are the
|
||
same observation.
|