While chasing the draw-stream question I found THREE xenia instances running simultaneously (started 15:39, 15:44, 16:12), which violates the "one emulator at a time" hard rule and confounds the finding I recorded last iteration. All three read the same /tmp/xenia_pad.txt and share display :98. A press written to that file is delivered to EVERY instance, while `screenshot` grabs whichever window is topmost -- not necessarily the one that acted on it. So "(A) was delivered and the screen did not change" may simply be two different emulators, and the keystroke-level confirmation proves only that SOME instance received it. The navigation.md entry claiming the boot title's "2 of 2" is no longer 2 of 2 is withdrawn as unsupported, pending a clean re-run. The cause was mine. run-canary's lockfile is the IMPLEMENTATION of the one-at-a- time rule; a kill -9 orphans it, and the obvious unblock -- rm -f the lock -- also disables the guard for every later launch. I did that more than once today. METHOD gains two entries. A lockfile is the rule, not an obstacle to it: clear a stale lock only after confirming zero live instances, and COUNT them rather than trusting a kill landed, because a plain kill is asynchronous and a -9 on a stuck process can take seconds. When a guard blocks you, the question is whether the condition it guards against is present, not how to remove the guard. And a third instance of pgrep -f matching the shell that runs it -- this time it killed a cleanup command halfway through, leaving the emulators alive and the lock in place. Already recorded for wait-loops; promoted to "reach for -C first". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
358 lines
19 KiB
Markdown
358 lines
19 KiB
Markdown
# Playing the game: every screen, and how you move through it
|
||
|
||
**Written from the player's chair.** What is on screen, what you press, what
|
||
happens. This is the document you would hand someone who has never seen the game
|
||
and wants to reach a mission — or who needs to script that journey.
|
||
|
||
Internal names (`ptbtn03`, `GP_LOAD`, build numbers) appear only as footnotes,
|
||
because they are how *we* find things, not what the game shows anyone.
|
||
|
||
**Status:** filling in. §1–§4 now carry what the committed oracle frames actually
|
||
show; what is still ❔ is what no capture answers. This page exists to be filled
|
||
in by playing, not to look finished.
|
||
|
||
> ## ⚠️ Fill this in from the real game
|
||
>
|
||
> The oracle is **Xenia Canary running the actual title**, captured. Not
|
||
> `sylpheed-cli`, not the Explorer, not any renderer of ours — those are tools
|
||
> for verifying our decoding, they are hypotheses under test, and they have been
|
||
> wrong. A row here with no capture behind it is a guess wearing a table cell.
|
||
>
|
||
> Describe what you *saw*. "The cursor starts on the second item" is worth more
|
||
> than any amount of reasoning about the layout data.
|
||
|
||
Confidence: ✅ seen in a capture · 🟡 inferred · ❔ unknown.
|
||
|
||
---
|
||
|
||
## 1. From power-on to the main menu
|
||
|
||
| # | What you see | What you do | What happens |
|
||
|---|---|---|---|
|
||
| 1 | **SQUARE ENIX** in white on black, the two dots in red, `™` after it ✅ | nothing | fades on to the next logo |
|
||
| 2 | **GAME ARTS**, **SETA** and **studio anima** stacked on black ✅ | nothing | fades on into the cinematic |
|
||
| 3 | The opening cinematic | **Ⓐ** skips it ✅ | ends into the title |
|
||
| 4 | **Title screen** — the wordmark appears **first, with no prompt**; `PRESS Ⓐ BUTTON` fades in **2.13 s** later, above the 2006/2007 Square Enix copyright line, and then pulses about every 2.2 s ✅ | press **Ⓐ** | goes to the main menu ✅ |
|
||
| 5 | **Main menu** | — | see §2 |
|
||
|
||
✅ **The order is publisher then developer, confirmed in three cold boots
|
||
(2026-08-29)** — `SQUARE ENIX` for ~4.3 s, a ~0.25 s black hold, then
|
||
`GAME ARTS` / `SETA` / `studio anima` for ~3.5 s, and both dwells are declared on
|
||
the disc (240 and 195 keyframe units). ⚠️ There is a **third** SQUARE ENIX
|
||
wordmark about ten seconds in — bloomed, below centre — and it is the opening
|
||
card of the intro movie, not a splash.
|
||
[the three frames side by side](../re/captures/boot-order/splash-order-two-runs.png) ·
|
||
[`boot-order-and-splash-dwell.md`](../re/boot-order-and-splash-dwell.md)
|
||
|
||
Both logo screens are **still pictures the game draws**, not video — neither is a
|
||
`.wmv` on the disc. Captures:
|
||
[publisher](../re/captures/title-builds/live-splash-publisher.png) ·
|
||
[developer](../re/captures/title-builds/live-splash-developer.png) ·
|
||
[title](../re/captures/title-builds/live-title-press-a.png).
|
||
|
||
⚠️ **One Ⓐ skips the cinematic**, and it is worth a lot of time: the title
|
||
arrived at **57 s** with the skip against **193 s** without it ✅.
|
||
|
||
⚠️ **The title screen has two states that look identical.** The one that ends
|
||
the boot accepts Ⓐ. The one the attract loop returns to, after the game has sat
|
||
untouched and looped back, **accepts nothing** — Ⓐ, START, B, BACK, X, Y, dozens
|
||
of presses, no response. Nothing on screen distinguishes them ✅. If you are
|
||
scripting: use the *first* title after boot, and do not press anything during the
|
||
boot.
|
||
|
||
⚠️ **The title is not input-ready for about ten seconds** after it appears ✅.
|
||
And even then Ⓐ registers roughly half the time, with nothing yet found that
|
||
predicts which ✅ — budget retries.
|
||
🔴 **Refutation attempt, 2026-08-29 — both halves of that came out wrong on the
|
||
runs I could test.** Two boots, Ⓐ pressed **7.29 s** and **7.28 s** after the
|
||
title art settled (5.15 s and 5.15 s after the prompt appeared): **accepted both
|
||
times, first press, no retry**, and each went straight on to the main menu. Ⓑ on
|
||
the menu was then also accepted first press, both runs.
|
||
⚠️ Reach: **n = 2**, so "half the time" is only made unlikely (2/2 has p ≈ 0.25
|
||
under it), not excluded — but *"not input-ready for about ten seconds"* is
|
||
contradicted outright, because 7.3 s worked twice. Keep the retry budget; drop
|
||
the ten-second wait. Evidence:
|
||
[run 1](../re/data/plate-timing-run1.tsv) · [run 2](../re/data/plate-timing-run2.tsv) ·
|
||
[`title-plate-delay-measured.md`](../re/title-plate-delay-measured.md).
|
||
|
||
⚠️ **The prompt takes 2.13 s to arrive, measured twice (2.138 s / 2.132 s).**
|
||
Timed from the moment the wordmark stops animating, not from the moment it first
|
||
appears — the build-in itself varies by half a second between runs.
|
||
|
||
---
|
||
|
||
## 2. Main menu
|
||
|
||
Five options in a vertical stack, roughly centred, with a highlighted state on
|
||
the focused one.
|
||
|
||
> ✅ **The focused option carries a small ring to the left of its label, and the
|
||
> ring turns — continuously, about once every 2.2 s.** It has a bright head, so
|
||
> you can see it go round. It is the **only** thing moving on this screen once it
|
||
> has settled: the labels, the bracket and the footer are all completely still
|
||
> (temporal std exactly 0.000 over 20 s). Ⓑ
|
||
> [five frames, 4 s apart](../re/captures/focus-ring/ring-single-frames-4s-apart.png) ·
|
||
> [the measurement](../re/focus-ring-spin-measured.md)
|
||
|
||
| position | label | what it opens |
|
||
|---|---|---|
|
||
| 1 | **NEW GAME** | a **DIFFICULTY** prompt, then **SELECT DATA** ✅ |
|
||
| 2 | **LOAD GAME** | the save-slot list ✅ |
|
||
| 3 | **TUTORIAL** | the lesson list ✅ |
|
||
| 4 | **OPTIONS** | the settings menu ✅ |
|
||
| 5 | **EXTRAS** | a three-item submenu ✅ |
|
||
|
||
Read off [`live-main-menu.png`](../re/captures/title-builds/live-main-menu.png);
|
||
destinations off
|
||
[`q4-destinations.png`](../re/captures/menu-nav/q4-destinations.png) and
|
||
[`newgame-difficulty.png`](../re/captures/newgame-path/newgame-difficulty.png).
|
||
|
||
The screen is the title art gone dim, with the wordmark ghosted behind the list
|
||
and a bracket of glowing rule-lines drawn around it. The focused item is bright
|
||
white with a **spinning ring** to its left; the others are dim blue. Every item
|
||
carries a small dot-in-circle at the left end of its underline — that is on all
|
||
five all the time and is *not* the cursor.
|
||
|
||
**Moving around ✅**
|
||
|
||
| you press | what happens |
|
||
|---|---|
|
||
| ⬆ / ⬇ | one item, and it **wraps** at both ends |
|
||
| ⬅ / ➡ | nothing |
|
||
| Ⓐ | opens the focused item |
|
||
| Ⓑ | 🟡 back to the title — see the warning below |
|
||
|
||
❔ **Which item is focused when the menu opens is not fixed.** Four boots of the
|
||
same harness opened on `TUTORIAL`, `TUTORIAL`, `NEW GAME`, `NEW GAME`. Do not
|
||
assume the top item, and do not assume the middle one either.
|
||
|
||
> ⚠️ **The main menu is the one screen whose footer does not offer Ⓑ.** It reads
|
||
> `⊙ : Select Ⓐ : OK` — every submenu adds `Ⓑ : Back`. Measured: **zero**
|
||
> red-Ⓑ glyph pixels anywhere in the frame, on two captures, with the same
|
||
> detector finding the glyph on `EXTRAS` and `DIFFICULTY` ✅.
|
||
> ✅ **But Ⓑ does leave it, and the objection that stood here is refuted
|
||
> (2026-08-29).** This page used to say the title "returns on its own after
|
||
> ~8–10 s idle", so an observer could not tell Ⓑ from the timer. That timer
|
||
> belongs to the **title**, not to this screen: the main menu was held untouched
|
||
> for **≥ 60 s** and never moved. Ⓑ is delivered and is the only input in ≥ 100 s
|
||
> before the return, so the ordering is measured — the *latency* is not
|
||
> ([the measurement](../re/menu-idle-and-b-2026-08-29.md)).
|
||
|
||
*Internals: `GP_TITLE.pak` build 5; buttons `ptbtn01`–`ptbtn05` top to bottom.*
|
||
|
||
---
|
||
|
||
## 3. The rest of the menus
|
||
|
||
One section each, in the shape of §2: what is on screen, what the cursor does,
|
||
what each choice leads to, and what a wrong choice shows you.
|
||
|
||
### New game ✅
|
||
Ⓐ on `NEW GAME` does **not** start a mission. It opens **DIFFICULTY** —
|
||
`EASY` / `NORMAL` / `HARD` / `BACK`, opening focused on **NORMAL** ✅ — and Ⓐ
|
||
there opens **SELECT DATA**, a save-slot picker headed
|
||
`Current Storage: Dummy HDD` that asks you to choose a file for the auto-save.
|
||
Pick one and a movie plays ✅.
|
||
[DIFFICULTY](../re/captures/difficulty-screen.png)
|
||
|
||
### Load game ✅
|
||
A vertical list of numbered slots, **8 rows visible**, scrolling as a carousel —
|
||
one capture shows the order `19, 20, 01, 02, 03, 04` with `01` focused, so the
|
||
list runs past the end and back round to the start ✅. Each row shows
|
||
`Difficulty`, `Flight Time` and `Clear Ratio`; a **Details** panel to the right
|
||
gives `STAGE`, `Game Status`, `Points` and `Times Cleared`, and an empty slot
|
||
leaves every one of those blank ✅. `Current Storage: Dummy HDD` sits along the
|
||
top.
|
||
|
||
Its footer offers more than the other menus:
|
||
`⊙ : Select Ⓐ : OK Ⓑ : Back Ⓧ : Delete Ⓨ : Select Storage` ✅.
|
||
[capture](../re/captures/menu-nav/q4-destinations.png) (left panel)
|
||
|
||
❔ Still open: the overwrite / delete confirmation, and where its cursor starts.
|
||
|
||
Known: `title → LOAD GAME → slot 01 → YES → READY ROOM → TAKE OFF` reaches
|
||
flight ✅.
|
||
|
||
### Tutorial ✅
|
||
A list of lessons in two headed groups, with a one-line description shown on the
|
||
left for whichever is focused ✅ — e.g. `BASIC CONTROLS` reads
|
||
*"Learn how to move and attack"*. Opens focused on the first entry.
|
||
|
||
| group | lessons |
|
||
|---|---|
|
||
| **Level 1** | `BASIC CONTROLS`, `HEADS-UP DISPLAY`, `RADAR` |
|
||
| **Level 2** | `SUPPLY AND SPECIAL MOVES`, `RADIO ORDERS`, `ADVANCED CONTROLS` |
|
||
| — | `BACK` |
|
||
|
||
[capture](../re/captures/menu-nav/q4-destinations.png) (middle panel)
|
||
|
||
### Options ✅ (one level in)
|
||
`GAME SETTINGS` · `CONTROL SETTINGS` · `SOUND SETTINGS` · `SCREEN SETTINGS` ·
|
||
`BACK`, opening focused on the first ✅.
|
||
[capture](../re/captures/menu-nav/q4-destinations.png) (right panel)
|
||
|
||
❔ Still open: what is inside each of the four, what each setting ranges over, and
|
||
whether a change needs confirming.
|
||
|
||
### Extras ✅
|
||
Three items: `MISSION SELECT` · `MOVIE THEATER` · `BACK`, opening focused on
|
||
`MISSION SELECT` ✅. The cursor wraps here too — it is a menu rule, not a
|
||
per-screen one ✅.
|
||
[capture](../re/captures/title-builds/live-extras.png)
|
||
|
||
❔ `MOVIE THEATER` has never been opened.
|
||
|
||
### Mission select ✅ — and the "stuck cursor" is explained
|
||
The stage list on the left (**8 rows visible of 16**, with a scrollbar), a detail
|
||
panel showing the stage's name, a picture, `High Score` and `Best Time`, and a
|
||
**Wide Area Space Map** on the right with the named systems on it. The chosen
|
||
difficulty is printed top-right. Footer:
|
||
`⊙ : Select Ⓐ : OK Ⓑ : Back Ⓨ : Difficulty` ✅.
|
||
|
||
⚠️ **"Stage select would not move" — sixteen d-pad presses never left Stage 01 —
|
||
is now explained: the other fifteen stages were LOCKED** ✅. A locked row is
|
||
drawn *dimmer than an unfocused one*: measured, the labels sit at three distinct
|
||
brightnesses — focused **254**, unlocked **183**, locked **104** — and on a save
|
||
with the story unlocked the same rows read 183, with the cursor able to reach
|
||
**Stage16** at the bottom of the scrolled list.
|
||
[the measurement](../re/menu-navigation-semantics.md#-mission-select-the-cursor-was-stuck-because-the-stages-were-locked) ·
|
||
[locked](../re/captures/mission-select-stage01-only.png) ·
|
||
[unlocked](../re/captures/mission-select-all-story-unlocked.png) ·
|
||
[at Stage16](../re/captures/mission-select-ends-at-stage16.png)
|
||
|
||
So: if you are scripting a run, **check what the save has unlocked** before
|
||
concluding the list is broken. ❔ Whether the list wraps past Stage16, and
|
||
whether a locked row is skipped or simply unreachable, is not settled.
|
||
|
||
### Briefing and Ready Room ❔
|
||
❔ What you read, what you choose, and what finally launches the mission.
|
||
|
||
### Hangar / Arsenal ❔
|
||
❔ How a weapon is fitted, how one is bought, what a purchase prompt says.
|
||
|
||
---
|
||
|
||
## 4. Prompts, confirmations and alerts
|
||
|
||
The interrupting layer — and the thing most likely to break a scripted run,
|
||
because it looks like a menu and does not behave like one.
|
||
|
||
| prompt | when | where the cursor starts | notes |
|
||
|---|---|---|---|
|
||
| `Save game?` | after a mission | **YES** ✅ | |
|
||
| `Do you want to develop this weapon?` | Arsenal, on purchase | ❔ | |
|
||
| overwrite / delete confirmation | saving, loading | ❔ | |
|
||
| "not enough points" or similar refusal | Arsenal | ❔ | ❔ does it need dismissing |
|
||
|
||
⚠️ **A short d-pad tap is ignored inside a prompt.** Sixty milliseconds moves the
|
||
cursor on a menu and does *nothing* in a dialog ✅. So a script that works
|
||
through the menus will silently accept the default in every prompt it meets —
|
||
which is how you end up overwriting a save you meant to keep. **Confirm which
|
||
side the cursor is on before pressing Ⓐ.**
|
||
|
||
---
|
||
|
||
## 4b. What a screen change looks like ✅ measured, three of them
|
||
|
||
Every screen carries a full-screen black quad (`pteff00.prm`) that paints last.
|
||
A screen change is that quad ramping to opaque on the way out, and the **incoming**
|
||
screen's own copy of it starting opaque and clearing on the way in — so a
|
||
transition is two screens' quads, not one shared effect. The outgoing ramp's length
|
||
is on the disc and matches the game **three for three**; the black between them
|
||
does not, and is not a constant.
|
||
|
||
| you press | going | outgoing ramp | black between | incoming clears over |
|
||
|---|---|---|---|---|
|
||
| Ⓑ | main menu → title | 5 frames | **none — they cross-fade** | 8 frames |
|
||
| Ⓐ | title → main menu | 4 frames | ~3 frames | 5 frames |
|
||
| Ⓑ | EXTRAS → main menu | 5 frames | **2 frames, fully blank** | 5 frames |
|
||
|
||
⚠️ **Ⓑ is not "the cancel animation".** Ⓑ out of the main menu cross-fades — the
|
||
title is already drawing while the menu is still fading — while Ⓑ out of EXTRAS
|
||
goes properly black first. Same button, two different-looking moves, and if you are
|
||
scripting against "the screen goes black" one of them will not do it.
|
||
|
||
⚠️ **Ⓐ off the title is slow to start.** About 25 rendered frames (~0.8 s) pass
|
||
between a delivered press and anything changing on screen; the other two start
|
||
immediately. A script that presses and then looks 0.5 s later sees the title still
|
||
up and can conclude the press was dropped.
|
||
|
||
Timings are in *rendered frames* at ~30 Hz, from the emulator's own draw stream, so
|
||
they do not stretch when the emulator runs slow —
|
||
[`screen-transitions.md`](../re/screen-transitions.md) ·
|
||
[`data/fade-three-transitions.txt`](../re/data/fade-three-transitions.txt).
|
||
|
||
## 5. Flying
|
||
|
||
Not started, and the game teaches it better than we could: **play the in-game
|
||
tutorials and write down what they say.** They are the game's own explanation of
|
||
its controls, which is exactly the document we want.
|
||
|
||
To fill in: the control map, and whether there is more than one scheme · throttle
|
||
and boost · targeting — ⚠️ target select is Ⓐ pressed **twice**; a sweep that
|
||
only ever tapped once concluded targeting was automatic, and was wrong ✅ ·
|
||
switching weapons · the pause menu and what it offers · what the HUD tells you,
|
||
element by element · how a mission ends, win or lose, and what you see next.
|
||
|
||
---
|
||
|
||
## 6. If you are scripting this
|
||
|
||
Traps that read as bugs but are not, all measured ✅:
|
||
|
||
* **F10 opens Xenia's own menu bar**, and while any emulator UI is up the game
|
||
is handed an empty keystroke before any pad driver is asked. The controller
|
||
looks dead and is not.
|
||
* **A signed-in profile is required** — `--create_profile_if_none`.
|
||
* **A trace consumer that exits stalls the emulator**, which also reads as a dead
|
||
pad.
|
||
* Cold boot is slow; ~25 s once the shader and code caches are warm.
|
||
* **`pkill -f xenia_canary` kills the shell that ran it**, because `-f` matches
|
||
the whole command line and your own `bash -c` contains the pattern. The script
|
||
dies before the emulator does, silently, with no output at all. Kill by process
|
||
**name**: `ps -o pid= -C xenia_canary | xargs -r kill -9`. (The same trap is in
|
||
`METHOD.md` for `pgrep` wait-loops; it cost another launch on 2026-08-30.)
|
||
* **`kill -9` on xenia ORPHANS `/tmp/xenia-canary.lock`**, and the next
|
||
`run-canary` refuses with *"an emulator is already running"* — to **stderr**,
|
||
where a polling script never looks. A probe of mine then sampled a dead display
|
||
for **484 s**, reporting `other` every 4 s, because `screen_id.py` on an empty
|
||
screen returns `other` and *"not the title yet"* is indistinguishable from
|
||
*"there is no emulator"*. Kill with a plain `kill` so it can clear its own lock;
|
||
if you must use `-9`, `rm -f /tmp/xenia-canary.lock` after. **And assert the
|
||
emulator is alive before entering any wait loop** — `ps -C xenia_canary` — so
|
||
the loop cannot spend its whole deadline on nothing.
|
||
* 🔴🔴 **THE ENTRY BELOW IS WITHDRAWN — the experiment was confounded.** When it
|
||
ran, **three emulators were live at once** (started 15:39, 15:44 and 16:12 on
|
||
2026-08-30), all reading the same `/tmp/xenia_pad.txt` and sharing display `:98`.
|
||
A press written to that file is delivered to **every** instance, and `screenshot`
|
||
grabs whichever window is topmost — which need not be the one that acted on it.
|
||
So "Ⓐ was delivered and the screen did not change" may simply be *two different
|
||
emulators*, and the keystroke-level confirmation proves only that **some**
|
||
instance received it. ⚠️ The cause was mine: `run-canary`'s lockfile is the
|
||
implementation of the "one emulator at a time" rule, and I had been clearing it
|
||
with `rm -f` to get past a stale one — which disables the guard for the next
|
||
launch too. **Clear a stale lock only after confirming zero live instances**
|
||
(`ps -C xenia_canary --no-headers | wc -l`). The claim below is unsupported and
|
||
needs a clean re-run before anyone relies on it.
|
||
|
||
* ~~🔴 **Ⓐ on a settled boot title does NOT reliably reach the menu.**~~ This page's
|
||
§1 and `canary-scripted-input-traps.md` record "the boot title accepts a single
|
||
Ⓐ (2 of 2 runs)". A run on 2026-08-30 gated on the **plate pulse** (glyph in
|
||
[500, 2500] held 12 consecutive samples), fired at t=484.5 s with glyph 1723 —
|
||
a verified settled boot title, not the attract one — pressed Ⓐ, and **the press
|
||
was delivered** (`[file-pad] keystroke vk=5800 down` / `up`, 8 `[RE-INPUT]`
|
||
lines). Twenty seconds later every captured frame still classified as the title.
|
||
So the sample is no longer 2 of 2, and a script that presses once and proceeds
|
||
can be left on the title with nothing in its log to say so. **Confirm the screen
|
||
changed; do not infer it from a delivered press.**
|
||
* **`screen_id.py` reports `menu` during the attract loop.** Two boots on
|
||
2026-08-30 logged `menu` at t=106 s and t=418 s while the game was still in the
|
||
intro movie. A wait-loop that breaks on `menu` will act on the wrong screen; the
|
||
loops here break on `title` for that reason.
|
||
* **`screen_id.py` cannot tell EXTRAS from the main menu** — both are dark blue
|
||
`GP_TITLE` screens and it answers `menu` for either. Use
|
||
[`which_title_screen.py`](../../tools/re-capture/which_title_screen.py), which
|
||
separates them by ~11 RMSE against ~18 within-class, and read the **margin**: a
|
||
margin under ~1 means "neither", which is what you get on a screen outside
|
||
`GP_TITLE` entirely.
|