Run 3 used the proven path rather than my own probe: launched exactly as
boot_menu.sh does (DISPLAY=:98, --apu=sdl, /dev/shm/xenia_* cleared, the
existing profile signed in) and drove skip_intro.sh, the detector that is
documented to work and that is stricter than mine (glyph >= 800 AND a
static frame).
It classified every one of 20 samples over 604 s as "movie", waited them
all out, and timed out. A direct check at 604 s says it was right: zero
green-glyph pixels, screen_id = other, warm mean (69,53,40), correlation
0.09 with build 7. The game really was playing attract movies for ten
minutes.
Three runs, two locales, two launch paths, ~35 minutes of emulator time,
no interactive title. Either the attract loop is far longer than the
600-780 s windows tried, or something regressed since
live-title-press-a.png was captured -- that one came from a pad-driven
boot (88b3ce9, "booting to the main menu and walking it").
Also settled a discrepancy I had flagged without checking: there is no
contradiction with title_states_capture.sh. It waits on screen_id.py, a
LOOSER oracle than is_title.py, and was written precisely to test whether
the interactive title draws ptbtn00 and the attract one does not. It
never claimed to reach the interactive title.
Stopping this line. The capture blocks none of the five menu screens and
has now cost five iterations. The locale mechanism, the fixed oracle and
the launch recipe are all committed, so what remains is patience with the
attract loop, not tooling. METHOD line on knowing when to stop paying for
a non-blocking answer.
Emulator stopped, lock cleared, locale English.
222 lines
13 KiB
Markdown
222 lines
13 KiB
Markdown
# Primary objective — answer everything the menu port needs
|
||
|
||
**Status:** active, set 2026-08-28. This replaces "work the RE backlog" as the
|
||
agent's primary objective. It does not change what the agent *does* — it is still
|
||
reverse engineering — it changes what earns attention: an item is worth doing
|
||
when the menu port is blocked on it.
|
||
|
||
## Who builds what
|
||
|
||
**You do not build the port.** A separate agent will build it, from what you
|
||
produce. Your deliverable is decoded, verified, written-down answers plus the
|
||
reference data that proves them.
|
||
|
||
| | container agent (you) | port agent |
|
||
|---|---|---|
|
||
| decodes the disc | ✅ | ❌ — consumes your answers |
|
||
| measures the running game | ✅ | ❌ — no emulator |
|
||
| writes `docs/re/` and `docs/port/HANDOFF.md` | ✅ | reads them |
|
||
| Godot project, asset pipeline, transcoding | ❌ | ✅ |
|
||
|
||
If you find yourself designing an export schema or writing GDScript, you have
|
||
crossed the line. Stop and go back to the question you were answering.
|
||
|
||
`crates/sylpheed-viewer` is also **not yours to change** for this objective. The
|
||
Explorer is the human's tool for exploring and verifying the RE work, it keeps its
|
||
static-data-only rule, and the port does not depend on it.
|
||
|
||
## The target
|
||
|
||
Someone else has to build this, from your answers alone:
|
||
|
||
```
|
||
developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus
|
||
```
|
||
|
||
No gameplay, no 3D, no HUD, no missions. If an answer is not needed to put those
|
||
five screens on a display and let a person move through them with a d-pad and Ⓐ,
|
||
it is not in this objective.
|
||
|
||
## What is already answered
|
||
|
||
Do not re-derive these. They are in `docs/re/` and the
|
||
[disc atlas](../re/disc-atlas.html):
|
||
|
||
* **The screen archive.** `GP_TITLE.pak` holds the whole title-side tree — build 4
|
||
the title with animating wordmarks, build 5 the five-button main menu, builds
|
||
6/8/9 submenus, and the developer splash as the `palogo` bundle in the same pak.
|
||
* **Buttons are identifiable as data.** Element kind `0x3002` is a button, `0x0`
|
||
decoration, `0x10` a primitive; buttons sort top-to-bottom by resting Y; each
|
||
pairs with an `f`-suffixed highlighted variant.
|
||
* **The screen vocabulary.** The GamePart id table, 29 entries at `.rdata
|
||
0x820A1630`, confirmed by the executable's own factory-registration strings.
|
||
* **The resting pose rule** — the hold, not the longest dwell — and that a
|
||
keyframe is the *start of a ramp*.
|
||
* **Screen composition**, pixel-accurate for the tutorial pause menu and the title
|
||
main menu, via `sylpheed-cli screen render`.
|
||
* **The logo splash is not a video.** `logo1`–`logo4` are manifest-bound with no
|
||
`.wmv` on the disc; the splash is the RATC screen, which already renders.
|
||
|
||
## The open questions — these are the objective
|
||
|
||
Ordered by what blocks the port earliest. Each is done when its **gate** exists:
|
||
a written `docs/re/` result with the evidence, and reference data committed
|
||
alongside it.
|
||
|
||
| | Question | Gate |
|
||
|---|---|---|
|
||
| **Q1** | **What is a keyframe time?** Values run 16…269. 60 Hz frames would make the title intro ~4.5 s — plausible and untested. Also: is the ramp linear, or eased? | A measured answer against the running game, not an inference. Everything animated downstream depends on this number |
|
||
| **Q2** | **Which build is which screen state?** Confirm build↔state for splash, title/PRESS Ⓐ, main menu and each submenu | A table, each row confirmed against a capture of the real screen |
|
||
| **Q3** | **Paint order for these six screens.** Solved at runtime, unsolved from the file — the declaration table is provably not it | Either a rule derived from the bundle, or six measured orders and a clear statement that no file-side rule was found |
|
||
| **Q4** | **What does each button do?** Labels are baked into the sprites; no decoded field says which GamePart a button opens | Button → GamePart id, from code or from driving the game. Say which |
|
||
| **Q5** | **Navigation semantics.** Initial focus, wrap-around at the ends, whether left/right does anything, what B does on each screen | Observed behaviour, per screen |
|
||
| **Q6** | **The boot sequence, and what drives it.** Order is observable; the *data or code* that sequences it is not decoded. Include the attract loop and what returns to the title | The sequence, plus whatever the game reads to decide it |
|
||
| **Q7** | **Transitions.** What happens visually between screens — the `pteff00.prm` quads, a fade, a cut — and its timing | Described and timed against a capture |
|
||
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
|
||
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
|
||
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
||
| **S1** | ~~**Ready Room probe.**~~ **DONE 2026-08-28 — [no-go](../re/ready-room-probe.md).** It is 2D and enumerates fine, but the pak is briefing/tactical-map content, not the Ready Room menu | ✅ go/no-go written |
|
||
|
||
## 🟡 Needs one more run — a Japanese-locale capture
|
||
|
||
Recorded rather than worked around, per "do not improvise around a blocker".
|
||
|
||
Two questions have converged on needing **one capture we cannot take**: a running
|
||
capture of `GP_TITLE` **build 7**, the Japanese title screen.
|
||
|
||
* Q1's keyframe-time association. The shifted reading is favoured 26× by a
|
||
calibration-free measurement, and the only render it changes on the whole disc
|
||
is build 7 ([`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md)).
|
||
* What `rest()` should return for a plateau-less element. The one element that
|
||
discriminates, `ptlogo_eff3.t32`, is also in build 7
|
||
([`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)).
|
||
|
||
**🔴 The "cannot" was wrong, and is withdrawn (2026-08-29).** It is true that
|
||
`user_language` is only ever `DECLARE_int32` here, with no `DEFINE` and no entry
|
||
in `xenia-canary.config.toml`, so there is no flag to pass — and passing an
|
||
unknown one is specifically dangerous, because `run-canary`'s own header records
|
||
that xenia calls `ShowSimpleMessageBox` from `ParseLaunchArguments` *before*
|
||
logging starts. But the cvar is not the only route, and I stopped at the first
|
||
one I checked.
|
||
|
||
**The language is persisted, and canary's own file is writable.**
|
||
`kernel_state.cc` builds `XConfig` over `<storage_root>/xconfig.settings`, which
|
||
exists here at `/sylph-home/re/.local/share/Xenia/xconfig.settings` (6 680 B).
|
||
`user.language` is a **big-endian u32 at file offset `0x912`**, located by three
|
||
independent landmarks rather than guessed:
|
||
|
||
| landmark | expected | found |
|
||
|---|---|---|
|
||
| `music_volume` (`User+449`) | `0.7f` | BE float at 2727 → `User` base `0x8e6` |
|
||
| `language` (`User+44`) | `1` = `kEnglish`, the hard-coded default | **1** at `0x912` |
|
||
| `country` (`User+64`) | United States | `103` at `0x926` |
|
||
|
||
`XLanguage::kJapanese = 2` (`xbox.h:307`). Writing 2 there and restoring
|
||
afterwards is using canary's own persistence, not patching its code.
|
||
|
||
**🟡 Still not settled — two runs, and the reason moved again.**
|
||
|
||
*Run 1 (2026-08-28)* reported "title not seen" in 787 s. **That was a broken
|
||
tool, not the game.** `wait_title.sh` still carried the single-pixel oracle that
|
||
`is_title.py` was written to replace — a 1280×720 coordinate sampled against the
|
||
1279×675 game surface, so it always reads the copyright line. Fixed; it now
|
||
delegates to `is_title.py`.
|
||
|
||
*Run 2 (2026-08-29)*, with the working oracle and the profile flag the English
|
||
captures use, **still did not reach the interactive title.** Not one frame in the
|
||
run showed a single green-Ⓐ glyph pixel, and content correlation against either
|
||
build-7 render never exceeded **0.22**. The game sat in the attract movie
|
||
throughout, polling `XamInputGetKeystrokeEx` (601 calls) — i.e. alive and waiting
|
||
for input, not hung.
|
||
|
||
**✅ The control was run (2026-08-29), and it removes the locale from the
|
||
picture.** Same flags, same oracle, English locale: **75 samples over 734 s, every
|
||
one glyph = 0.** The English boot does not present the interactive title either.
|
||
Canary was alive throughout and polling `XamInputGetKeystrokeEx` (1 801 calls);
|
||
the frame at 734 s has content but correlates only **0.15** with build 4's render
|
||
and 0.05 with the main menu, i.e. it is an attract-movie frame, not title art.
|
||
|
||
So the Japanese run was **not** failing because of the locale — neither locale
|
||
reaches the interactive title in ~12 minutes of no pad input. **🔴 Tried the proven path too, and I am stopping this line (2026-08-29).**
|
||
Run 3 launched exactly the way `boot_menu.sh` does — `DISPLAY=:98`, `--apu=sdl`,
|
||
`/dev/shm/xenia_*` cleared, the existing profile signed in — and drove
|
||
`skip_intro.sh`, the detector that is documented to work. It classified **every
|
||
one of 20 samples over 604 s as "movie"** and waited them all out, then timed
|
||
out. A direct check at 604 s confirms it was right: zero green-glyph pixels,
|
||
`screen_id` = other, warm mean (69,53,40), correlation **0.09** with build 7. The
|
||
game really was playing attract movies for ten minutes.
|
||
|
||
So: **three runs, two locales, two launch paths, ~35 minutes of emulator time, no
|
||
interactive title.** Either the attract loop is far longer than the 600–780 s
|
||
windows tried, or something has regressed since `live-title-press-a.png` was
|
||
captured (that one came from a pad-driven boot — commit `88b3ce9`, "booting to
|
||
the main menu and walking it").
|
||
|
||
⚠️ **Not worth more loop iterations.** This capture does not block any of the
|
||
five menu screens, and it has now cost five. It is written down so a later
|
||
session with a reason to spend an hour on the boot path can pick it up; the
|
||
locale mechanism, the fixed oracle and the launch recipe are all in place, so
|
||
what is left is patience with the attract loop, not tooling.
|
||
|
||
The locale is restored to English; `set_console_language.py ja` flips it back in
|
||
one command.
|
||
|
||
⚠️ Neither question blocks the five menu screens. Q1's *interpolation law* is
|
||
settled and only multi-keyframe absolute timing is open; `rest()` differs from
|
||
its alternative on **one** element across all five screens, and the current
|
||
answer there is the defensible one.
|
||
|
||
## Known unknowns — say so, do not fill them in
|
||
|
||
Some of these may turn out to be undecodable. That is a valid, useful answer, and
|
||
it is better than a guess, because the port agent will otherwise have to author
|
||
the mapping by hand and needs to know it is authoring rather than transcribing.
|
||
|
||
For each question, the answer is one of:
|
||
|
||
* **decoded** — here is the field, here is the disc-wide check;
|
||
* **measured** — not on the disc in any form we found, but here is what the
|
||
running game does, and here is the capture;
|
||
* **undecodable, with reach** — we looked here, here and here, and this is why it
|
||
is not there.
|
||
|
||
Never a fourth thing. In particular: if Q4 ends as "read the labels off the sprite
|
||
images by eye", say exactly that — it is then an authored mapping on the port
|
||
side, not a disc fact, and mislabelling it would put a guess into the port wearing
|
||
the badge of a measurement.
|
||
|
||
## The Ready Room probe (S1) — one iteration, then stop
|
||
|
||
`GP_READY_ROOM.pak` is the largest UI archive on the disc, 1 106 entries, and only
|
||
**6 of its names resolve**. It is also ISL-scripted. That is either a week or a
|
||
quarter, and one cheap test tells you which.
|
||
|
||
Our screen catalog enumerates bundles by **content**, not by name — `is_build` /
|
||
`is_composable` read the bytes — so unrecoverable *paths* do not necessarily mean
|
||
unrenderable *screens*.
|
||
|
||
```bash
|
||
sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak"
|
||
sylpheed-cli screen render --build <n> "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak" /tmp/rr.png
|
||
```
|
||
|
||
Report how many builds it finds, whether any composite looks like a Ready Room,
|
||
and **whether the room is 2D at all** or 3D with a UI overlay — if it is 3D the
|
||
answer is no-go by definition, not "try harder".
|
||
|
||
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
|
||
authority.
|
||
|
||
## Handing it over
|
||
|
||
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it
|
||
current as you answer questions: it is a summary with links into `docs/re/`, not a
|
||
second copy of the findings. An answer that is not reachable from HANDOFF.md has
|
||
not been delivered.
|
||
|
||
## Out of scope
|
||
|
||
3D, gameplay, HUD, missions, save/load, localisation beyond English, the Godot
|
||
project itself, any asset pipeline, and any archive outside `GP_TITLE`,
|
||
`tables.pak`, `sound.pak` and `dat/movie/` — except for the S1 probe.
|