GameParts exist at all Q6's second half, advanced but not closed, and the negative is the point. config.ini is the disc's ONLY config file -- one find over the whole extract -- and its own Shift-JIS header calls it the "Application / GamePart initial settings table". Its [SYSTEM] section, which that same comment says holds what the game and every game part share, is EMPTY. So the boot order is not in disc-side configuration at all, and that search space is now closed rather than merely unexplored. What the file DOES carry is the language: XC_LANGUAGE_* -> eng/jpn/deu/ fra/esp/ita, defaulting to eng. That is the mechanism behind the EN/JP build pairs in GP_TITLE and the <lang>.pak families -- a question the corpus had described but never traced to its input. Then the registry. Pulling every RegisterToFactory diagnostic string binds 24 of the 29 GamePart ids to a C++ class, and five ids have no registration site: 1, 2, 16, 18, 28. Id 1 is GP_ADVERTISE_DEMO, which agrees with what I measured two iterations ago -- the attract loop is the TITLE replaying ADV.wmv, not a transition into an advertise part. Marked amber, not green: it is an argument from an error message, not from code. Two things fall out for Q4: ids 3 and 4 are the same class (GamePart_SaveLoad, one part with two ids), and the menu buttons' ids now match the executable's own class names rather than a list of table names. Still a name match, one level closer to the code. What is still missing is the transitions themselves, and I say so: the manifest gives the boot-side assets, config.ini the language, the registry which parts exist. What decides to advance is in GamePart_Title's code and that dig has not been started.
235 lines
17 KiB
Markdown
235 lines
17 KiB
Markdown
# Handoff — what the menu port needs, and where it stands
|
||
|
||
The single page the **port agent** reads. Everything here is produced by the
|
||
container agent's reverse engineering; nothing here is a design decision about
|
||
the port itself.
|
||
|
||
Keep it current. It is a summary with links into `docs/re/`, not a second copy of
|
||
the findings — but an answer that is not reachable from this page has not been
|
||
delivered.
|
||
|
||
## How to read an answer
|
||
|
||
Every row below is one of exactly three things, and the distinction is the point:
|
||
|
||
| | meaning | what the port should do |
|
||
|---|---|---|
|
||
| **decoded** | a field on the disc, with a disc-wide check | read it from the data |
|
||
| **measured** | not on the disc in any form we found, but the running game does *this* | hardcode it, and cite this page |
|
||
| **undecodable** | we looked in these places, it is not there, here is the reach of the negative | author it by hand, knowingly |
|
||
|
||
There is no fourth kind. If a row says *measured* or *undecodable*, the port is
|
||
**authoring** that value, not transcribing it — and it should be kept somewhere a
|
||
human can see it is a human decision, so that when it is later decoded the
|
||
authored version can be deleted.
|
||
|
||
## Status
|
||
|
||
| | Question | State | Answer / link |
|
||
|---|---|---|---|
|
||
| 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 | ✅ answered | **measured** which screen all **5** buttons open — `NEW GAME` → `DIFFICULTY` → `SELECT DATA`, not a hang. The **GamePart id is still a name match**, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||
| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 1× `NEW GAME`); ⬆⬇ 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 timed; the **manifest** gives the boot-side assets, **`config.ini`** the language, the **registration strings** which parts exist — but the *transitions* are in `GamePart_Title`'s code and are not decoded — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
|
||
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
|
||
| Q8 | menu audio bindings | 🟡 mostly answered | the **UI cue vocabulary is decoded** (`SE_UI_CURSOR`/`DECIDE`/`CANSEL`/`IMPOSI`, all in `Static.slb` per `BANK_SE`); the event binding is a **name match**, and the SE audio is **not extractable yet** — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |
|
||
| Q9 | video binding + playback rules | ✅ answered | **decoded** from the movie manifest: `ADVERTISE_MOVIE`→`ADV.wmv` (boot intro *and* attract are one asset), `MS00A`→`S00A.wmv` is the new-game intro, `STAFF_ROLL`→the credits reel. ✅ **one Ⓐ skips a movie** (title at 57 s vs a 193 s baseline) — [`movie-binding.md`](../re/movie-binding.md) |
|
||
| Q10 | music-bank sub-wave roles (intro+loop?) | ✅ answered | **two stems of one performance, played together** — sample-synchronous, equal duration, 32/32 banks. **Concatenating is wrong.** Not a seamless loop either — [`structures/bgm-two-stems.md`](../re/structures/bgm-two-stems.md) |
|
||
| S1 | Ready Room go/no-go | ✅ **no-go** | it is 2D and enumerates fine (60 builds), but `GP_READY_ROOM.pak` holds **briefing/tactical-map** content, not the six-button Ready Room menu — [`ready-room-probe.md`](../re/ready-room-probe.md) |
|
||
|
||
## Already settled — the port can rely on these today
|
||
|
||
* **`GP_TITLE.pak` is eight screens, each shipped twice — English and Japanese.**
|
||
Build 4 is the English title art and 7 its Japanese twin; **2/3 are the
|
||
`PRESS Ⓐ BUTTON` plate, a build of their own** composited over the title and
|
||
faded in a beat later; 5/8 are the five-button main menu; 6/9 are the `EXTRAS`
|
||
submenu — the **only** submenu inside this archive. Builds 0/1 and 10/11 are a
|
||
`DELTASABER / SYLPHEED A.I.` plate that was **never seen running**, in the boot
|
||
path, any title-side screen, or the attract loop. ✅ measured against live
|
||
captures for the four English screens the boot path shows;
|
||
[`ui-title-build-map.md`](../re/ui-title-build-map.md).
|
||
**Withdrawn:** the earlier "builds 6/8/9 are submenus" — 8 is the Japanese main
|
||
menu. The other four main-menu buttons leave the archive (`GP_SAVE_LOAD`,
|
||
`GP_TUTORIAL`, `GP_OPTIONS`, `GP_MISSION_SELECT` are the likely destinations by
|
||
name — an inference, not a measurement).
|
||
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
|
||
decoration, `0x10` = primitive. ✅ decoded **for the title-side screens**.
|
||
⚠️ `0x3002` is one member of a `0x3000` family with sub-bits, and it is not the
|
||
only button kind on the disc: `GP_READY_ROOM`'s 902 bundles contain **zero**
|
||
`0x3002` and use `0x3000` / `0x3004` / `0x300c` / `0x3008` instead. Nothing in
|
||
this milestone changes — every screen in scope is `GP_TITLE` — but do not ship
|
||
`kind == 0x3002` as a general button test.
|
||
(The kind is the 4th `u32` of the 60-byte declaration entry, at `+40`.)
|
||
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
|
||
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
|
||
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
|
||
(measured and refuted, see
|
||
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
|
||
* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a
|
||
naming convention that holds for all 54 real pairs, not a decoded field.
|
||
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
|
||
a keyframe is the **start of a ramp**.
|
||
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
|
||
* **That ramp is linear, and it runs at 2 keyframe time units per rendered
|
||
frame.** Measured frame-by-frame off the running game's own draw stream: a
|
||
declared 15-unit fade lands on `round(255·k/15)` for all seven of its samples,
|
||
with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames.
|
||
**measured**, not decoded — the disc says `t=30`, it does not say what a `t` is.
|
||
The seconds conversion (`1 unit = 1/60 s`, so a 30 fps screen) rests on a
|
||
measured 27.6 present-frames/second and is the one part still worth re-testing;
|
||
[`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md) names the test.
|
||
If it turns out the game presents at 60 Hz, every duration halves — nothing
|
||
else on this page changes.
|
||
* **The paint order is derivable from the file.** Each `T8aD` sprite header
|
||
carries a **`u16` layer key at `+0x0A`** (the upper half of the 32-bit word at
|
||
`+0x08` is zero in all 21 184 sprites on the disc). Paint order is that key,
|
||
**stable-sorted** so equal keys keep declaration order; elements with no sprite
|
||
(`.prm` primitives, `.tbm`) have no key and take an implied one — the backdrop
|
||
and dim quads sort early, the screen-transition fade (`pteff00.prm`,
|
||
`pfeff00.prm`) sorts **last**. ✅ decoded. Checked against five paint orders read
|
||
off the running game, one of them (`GP_SAVE_LOAD`'s slot-list header, 6
|
||
instances) **exact and independent of the screens the rule was fitted to**, and
|
||
against a fresh `EXTRAS` capture. It reorders 341 of the disc's 965 builds, so
|
||
it is not a no-op dressed as a rule.
|
||
🟡 **The one residual: ties.** Where two elements share a key the game
|
||
sometimes paints them in an order nothing predicts — eight candidates refuted,
|
||
including declaration order, RATC child order, keyframe times, resting X/Y and
|
||
`kind`. Measured cost: on the three screens with ground truth it changes the
|
||
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. **Initial
|
||
focus is not stable**: four boots of the same script gave `TUTORIAL`,
|
||
`TUTORIAL`, `NEW GAME`, `NEW GAME`. Do not hardcode it; pick one and say you picked it. All **measured**, none of it on the disc.
|
||
* **Each button's destination is measured; its GamePart id is not.**
|
||
**`NEW GAME` → `DIFFICULTY`** (`EASY`/`NORMAL`/`HARD`/`BACK`, opening on
|
||
`NORMAL`) **→ `SELECT DATA`** — it does *not* hang; the run then hits the
|
||
already-documented `sub_823070B0` cache crash, which is not a menu problem.
|
||
`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. 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.
|
||
|
||
* **A screen change is a fade through black.** Each screen carries a full-screen
|
||
black `.prm` quad that paints last (`pteff00.prm` / `pfeff00.prm`) whose
|
||
keyframe group *is* the transition: black at `T0`, clear by `T1`, clear until
|
||
`T2`, then back to black on exit. ✅ decoded, with a disc-wide check — and in
|
||
`GP_TITLE` exactly the six **screen** builds carry it while the six overlays do
|
||
not. The fade-in length is `T1 − T0` and is read from the file (0.87 s for
|
||
`EXTRAS`, 0.97 s main menu, 4.08 s title). ❔ **The fade-OUT length is not on
|
||
the disc** — the last keyframe has no time slot; **measured ~0.4 s**, twice.
|
||
The black hold measures 0.17–0.23 s. ⚠️ Do not time the fade-in off a capture's
|
||
brightness: the incoming screen's own element animations dominate it and run
|
||
much longer than the quad.
|
||
|
||
* **The movie manifest names every boot-side video by role.** `dat/tables.pak`
|
||
entry `0x5b983a08`: `LOGO1`–`LOGO4` → `logo1.wmv`–`logo4.wmv` (**not on the
|
||
disc** — this is why the splash is a screen), **`ADVERTISE_MOVIE` → `ADV.wmv`**,
|
||
`STAFF_ROLL` → `SYLPH_HD720p_8M-CBR_2ch.wmv`, **`MS00A` → `S00A.wmv`** (the
|
||
new-game intro, 93.9 s, with subtitle + `VOICE_S00A` + a text overlay),
|
||
`MS01A` → `S01A.wmv`. ✅ decoded.
|
||
**The boot intro and the attract movie are the SAME asset** — there is no
|
||
separate boot slot, and 15 of 19 captured attract frames match `ADV.wmv` with a
|
||
monotonically advancing playhead ending at its full 137 s. One video, not two.
|
||
✅ The attract movie **plays to its end**; nothing cuts it short.
|
||
✅ **A movie is skippable with a single Ⓐ.** Measured: one tap ~45 s into the
|
||
boot brought the title at ~57 s against a ~193 s no-input baseline over three
|
||
boots, with Canary's own keystroke counter proving exactly one press was
|
||
delivered — and the skipped-to title is **fully functional** (`PRESS Ⓐ` plate
|
||
present, Ⓐ opens the main menu). What breaks the boot is *hammering*: 88
|
||
presses left a permanent black screen. One press is fine.
|
||
|
||
* **The menu's sound events are named on the disc.** `tables.pak`'s `SOUNDS`
|
||
record carries 322 `SE_*` cues, and the low block is the UI vocabulary — named
|
||
after the *event*: `SE_UI_CURSOR` (2), `SE_UI_DECIDE` (3), `SE_UI_CANSEL` (4),
|
||
`SE_UI_IMPOSI` (5, the error), `SE_UI_SUB_WIN_OPN`/`_CLS` (8/9),
|
||
`SE_UI_SPLASH_IN`/`_OUT` (12/13). ✅ decoded, full list in
|
||
[`data/se-ui-cues.txt`](../re/data/se-ui-cues.txt). They all live in **one**
|
||
bank — `BANK_SE` is a single field naming `Static.slb`, and 0 of the 322 has
|
||
its own `FILES` entry.
|
||
🟡 **Which event fires which cue is a name match**, not a measurement — strong,
|
||
because these are the authors' own event names, but nobody has watched the game
|
||
emit cue 2 on a d-pad press. The port is authoring it.
|
||
❔ **The SE audio cannot be exported yet.** `Static.slb`'s 8 353 472 readable
|
||
bytes contain **0 `RIFF`, 0 `seek`, 0 `WAVE`** — the boundary marker that works
|
||
for all 7 620 other banks is simply absent, so an individual cue's wave is not
|
||
locatable. Next step is `Pj_Silph.xgs` (in `sound.pak`, TOC 9454).
|
||
|
||
* **`config.ini` picks the language, and that is what picks the EN/JP build.**
|
||
The disc's **only** config file (400 bytes, at the root; one `find` over the
|
||
whole extract). Its `[LANGUAGE]` section maps the console's `XC_LANGUAGE_*`
|
||
value to `eng`/`jpn`/`deu`/`fra`/`esp`/`ita`, defaulting to `eng` — that code
|
||
selects `GP_TITLE`'s English or Japanese build and the `<lang>.pak` families.
|
||
✅ decoded.
|
||
❔ Its `[SYSTEM]` section — which the file's own comment says holds what the
|
||
game and every game part share — is **empty**, so the boot *order* is not in
|
||
disc-side configuration at all.
|
||
* **Five GameParts are named but never registered.** Pulling every
|
||
`RegisterToFactory<N, class silph::GamePart_X>` diagnostic string binds **24 of
|
||
the 29 ids to a C++ class**
|
||
([`data/gamepart-class-ids.txt`](../re/data/gamepart-class-ids.txt)). Ids `1`,
|
||
`2`, `16`, `18`, `28` have no registration site — including
|
||
**`GP_ADVERTISE_DEMO` (1)**, which agrees with the measurement that the attract
|
||
loop is the *title* replaying `ADV.wmv` rather than a separate part. 🟡 an
|
||
argument from a diagnostic string, not from the code.
|
||
Also: **`3` and `4` are both `GamePart_SaveLoad`** — one part, two ids.
|
||
|
||
* **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.
|
||
* **The logo splash is a screen, not a video.** `logo1`–`logo4` are
|
||
manifest-bound with no `.wmv` on the disc. ✅
|
||
* **Sprites carry their own labels.** No font rendering or localisation is needed
|
||
for this milestone. ✅ — and the localisation is *already baked in*: the
|
||
Japanese screens are separate builds in the same pak, not a text swap.
|
||
|
||
## Facts the port will trip over
|
||
|
||
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||
plays only Ogg Theora natively. How to handle that is the port's decision, not
|
||
ours — but it is not optional.
|
||
* **The disc holds 3.3 GB of video.** Only the boot intro and the one new-game
|
||
intro are in scope.
|
||
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
|
||
highest-offset entry in `sound.pak` and its size field is an allocation size. A
|
||
reader must allow a short read there and only there.
|
||
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
||
for spoken lines and discards half a music mix.
|
||
* **A music bank is TWO STEMS THAT PLAY TOGETHER — do not concatenate.**
|
||
✅ The "10 KB + 4.47 MB + 4.67 MB" reading was wrong: the 10 KB is the bank
|
||
header, and a bank is exactly **two waves of identical duration** (32/32 banks
|
||
on the disc). `BGM_001`'s two are **sample-synchronous** — transient
|
||
correlation peaks at lag 0.00 s over ±5 s, and both stop at the same
|
||
millisecond, 167.663 s. Wave 1 is quieter, far more L/R-decorrelated and has
|
||
almost no bass, so it reads as a surround-rear pair or a second intensity
|
||
layer — 🟡 which of those is unsettled, and `ChannelMask` is `0x0002` on both,
|
||
so the file will not say. Today's 347 s concatenation plays the piece twice,
|
||
the second time as a bass-less stem.
|
||
❔ **And it is not a seamless loop**: `BGM_001` fades out at 167.663 s and is
|
||
followed by 6.15 s of silence, with no loop-point field identified. A menu loop
|
||
is authored.
|
||
❔ **Which bank is the menu's music is not on the disc** — all 32 BGM cues are
|
||
named `BGM_001`…`BGM_109`, no semantic name in `SOUNDS`, `FILES` or the bank
|
||
headers. The port is choosing a track.
|
||
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
||
number of XMA1 packets from any known data offset.
|
||
|
||
## Reference data
|
||
|
||
Committed alongside the findings, so the port can be built without a disc in the
|
||
loop during development:
|
||
|
||
* `sylpheed-cli screen info --build <n> GP_TITLE.pak` — the element table, per
|
||
build, with pivots, kinds, focus links, keyframes and resting poses.
|
||
* `sylpheed-cli screen render` — the reference composite. When the port draws a
|
||
screen, this is what it should be diffed against; where they disagree, one of
|
||
them is wrong and the disagreement is worth reporting back.
|
||
* `docs/re/captures/` — framebuffer captures of the real screens, for anything
|
||
that has to be checked against the game rather than against our renderer.
|