re: the boot intro and the attract loop are the same video, and the

new-game intro has a name

Q9, answered twice over -- once from the disc and once from frames I had
already captured and not used.

The movie manifest's first eight slots are the whole boot-side flow, and
the slot key is the role: LOGO1-4 -> logo1-4.wmv (not on the disc, which
is why the splash is a screen), ADVERTISE_MOVIE -> ADV.wmv, STAFF_ROLL ->
the credits reel, MS00A -> S00A.wmv, MS01A -> S01A.wmv. So the new-game
intro is S00A.wmv, decoded -- which is the half I could not test at
runtime, because A on NEW GAME hangs the emulator.

And there is no separate boot-intro slot: ADV.wmv IS the advertise movie,
the boot just plays it first. Confirmed independently by matching 19
attract frames against five candidates -- 15 hit ADV with a playhead that
advances monotonically at the sampling rate and ends at 137 s, its full
length, with the title back on the next sample. So the attract movie
plays to its end; nothing cuts it short.

That corrects me. Two iterations ago I recorded the attract movie as
"~85 s, so probably not ADV.wmv" -- arithmetic on a start I never
observed, since sampling began 39 s in. REFUTED and METHOD both take it,
along with the matcher's real failure mode: a near-black frame has no
signature, and its runner-up is not evidence.

Skippability I did NOT settle, and the corpus contradicts itself: one
page says A skips a movie every time, while the boot harness deliberately
never taps during one because it breaks the title. Named the one-boot
test rather than picking a side.
This commit is contained in:
Sylpheed RE agent
2026-08-28 18:29:39 +00:00
parent e64965b3f9
commit e81dcad632
7 changed files with 187 additions and 2 deletions

View File

@@ -32,10 +32,10 @@ authored version can be deleted.
| 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) | | 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 | 🟡 mostly answered | **measured** which screen each button opens (4 of 5; `NEW GAME` untested — it hangs). The **GamePart id is a name match** onto the decoded id table, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | | Q4 | button → GamePart | 🟡 mostly answered | **measured** which screen each button opens (4 of 5; `NEW GAME` untested — it hangs). The **GamePart id is a name match** onto the decoded id table, 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) | | 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 the **attract cycle is timed**: ~810 s idle on the title → fade to black → ~85 s of video → title again, plate and all. The driver is still not decoded | | Q6 | boot sequence + what drives it | 🟡 partial | order observed and the attract cycle timed: ~810 s idle → fade to black → **`ADV.wmv` in full (137 s)** → title again. The **movie manifest** supplies the boot-side asset order (slot key = role); the code that decides to advance is still not decoded |
| 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) | | 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 | ❔ open | cue table complete, event binding is not | | Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not |
| Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified | | 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. 🟡 skippability unsettled — [`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) | | 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) | | 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) |
@@ -125,6 +125,20 @@ authored version can be deleted.
brightness: the incoming screen's own element animations dominate it and run brightness: the incoming screen's own element animations dominate it and run
much longer than the quad. 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.
🟡 **Whether a movie is skippable is unsettled** — the corpus says Ⓐ skips one
every time, while the boot harness deliberately never taps during a movie
because it breaks the title. Do not rely on either until it is retested.
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the * **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
executable's own registration strings. ✅ This is the screen vocabulary; which executable's own registration strings. ✅ This is the screen vocabulary; which
button reaches which entry is Q4 and is *not* part of it. button reaches which entry is Q4 and is *not* part of it.

View File

@@ -152,6 +152,7 @@ files, which is how the same ground got covered twice.
| [`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 | | [`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 | | [`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 |
| [`screen-transitions.md`](screen-transitions.md) | Between two screens — a fade through black, and where its timing lives | ✅ the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; `GP_TITLE` = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc | | [`screen-transitions.md`](screen-transitions.md) | Between two screens — a fade through black, and where its timing lives | ✅ the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; `GP_TITLE` = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc |
| [`movie-binding.md`](movie-binding.md) | Which movie plays where — boot intro, attract loop, new-game intro | ✅ decoded from the movie manifest (`ADVERTISE_MOVIE``ADV.wmv`, `MS00A``S00A.wmv`); attract identity confirmed independently by frame matching; 🟡 skippability unsettled |
| [`ready-room-probe.md`](ready-room-probe.md) | S1 — the Ready Room probe: no-go, and not for the reason expected | ✅ it is 2D and enumerates (60 builds), but the pak is briefing/tactical-map content; and `kind == 0x3002` finds 0 buttons there | | [`ready-room-probe.md`](ready-room-probe.md) | S1 — the Ready Room probe: no-go, and not for the reason expected | ✅ it is 2D and enumerates (60 builds), but the pak is briefing/tactical-map content; and `kind == 0x3002` finds 0 buttons there |
| [`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-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 | | [`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 |

View File

@@ -160,3 +160,12 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
counts — they pin each wave's join exactly, so a straddle can be realigned counts — they pin each wave's join exactly, so a straddle can be realigned
rather than thrown away. 4 of 32 BGM banks look like counterexamples until this rather than thrown away. 4 of 32 BGM banks look like counterexamples until this
is applied. is applied.
* **Do not time a video whose start you did not see.** An attract cycle sampled
from partway through measured "≈85 s", which was used to argue it was not the
137 s `ADV.wmv`. It was `ADV.wmv`; the sampling began 39 s in. Identify a video
by **content** — match frames against the candidates and check the matched
playhead advances monotonically — before reasoning about its duration.
* **A near-black frame has no signature.** In frame-matching, low-contrast frames
drive every correlation toward zero and the ranking becomes noise; one control
frame scored 0.000 and another tied the wrong movie at 0.98. Read a dark
frame's runner-up as *no evidence*, not as evidence for the runner-up.

View File

@@ -35,6 +35,14 @@ neighbourhood, not just the line.
## Screens, classes and RTTI ## Screens, classes and RTTI
* "the attract movie runs ~85 s, so it is not `ADV.wmv` (137 s)" → **mine, and
wrong.** Sampling began 39 s into the movie, so what was timed was its tail.
The attract movie **is** `ADV.wmv`, played in full.
[`movie-binding.md`](movie-binding.md)
* "the boot intro and the attract movie are different videos" → one asset, one
manifest slot (`ADVERTISE_MOVIE`).
* "the new-game intro is unidentified" → `MS00A``S00A.wmv`, decoded from the
movie manifest.
* "`GP_READY_ROOM.pak` holds the Ready Room screen" → its 317 distinct element * "`GP_READY_ROOM.pak` holds the Ready Room screen" → its 317 distinct element
names contain **none** of the six visible labels; it is the briefing / names contain **none** of the six visible labels; it is the briefing /
tactical-map content behind the `BRIEFINGS` item. tactical-map content behind the `BRIEFINGS` item.

View File

@@ -0,0 +1,33 @@
Frame-signature match of captured frames against five candidate movies.
32x18 normalised grayscale, movie frames cropped to the 675/720 the game
surface shows, 1 fps sampling, best correlation over the whole movie.
CONTROL - frames captured during the BOOT movie, known to be ADV.wmv:
p2.png -> ADV @116s 0.644 (runner-up S13A 0.438)
p3.png -> ADV @121s 0.994 (runner-up S03A 0.650)
p4.png -> S13A @ 35s 0.984 (runner-up ADV 0.926)
p5.png -> ADV @129s 0.933 (runner-up S13A 0.809)
p6.png -> ADV @134s 0.992 (runner-up S13A 0.499)
p7.png -> S15B @ 88s 0.000 (runner-up S13A 0.000)
p8.png -> S13A @ 65s 0.576 (runner-up ADV 0.534)
TEST - frames captured during the ATTRACT loop, 5 s apart:
attract01.png -> ADV @ 39s 0.990 (runner-up S13A 0.487)
attract02.png -> ADV @ 45s 0.961 (runner-up S13A 0.411)
attract03.png -> S03A @ 37s 0.731 (runner-up ADV 0.422)
attract04.png -> ADV @ 56s 0.955 (runner-up S03A 0.532)
attract05.png -> ADV @ 62s 0.934 (runner-up S15B 0.245)
attract06.png -> S15B @ 65s 0.885 (runner-up ADV 0.743)
attract07.png -> ADV @ 75s 0.961 (runner-up S03A 0.355)
attract08.png -> ADV @ 80s 0.938 (runner-up S03A 0.753)
attract09.png -> ADV @ 85s 0.974 (runner-up S03A 0.484)
attract10.png -> ADV @ 92s 0.892 (runner-up S13A 0.754)
attract11.png -> S03A @ 24s 0.606 (runner-up ADV 0.588)
attract12.png -> ADV @102s 0.994 (runner-up S03A 0.374)
attract13.png -> ADV @108s 0.977 (runner-up S13A 0.496)
attract14.png -> ADV @113s 0.836 (runner-up S06B 0.464)
attract15.png -> ADV @119s 0.958 (runner-up S15B 0.424)
attract16.png -> S13A @ 32s 0.996 (runner-up ADV 0.980)
attract17.png -> ADV @131s 0.865 (runner-up S13A 0.769)
attract18.png -> ADV @137s 0.993 (runner-up S13A 0.502)
attract19.png -> S13A @ 65s 0.551 (runner-up ADV 0.534)

99
docs/re/movie-binding.md Normal file
View File

@@ -0,0 +1,99 @@
# Which movie plays where — the boot intro, the attract loop, the new-game intro
**Status:**`CONFIRMED`. The bindings are **decoded** from the movie manifest,
and the attract movie is independently **measured** from captured frames. One
part — whether playback is skippable — is 🟡 and the corpus contradicts itself
about it; that is written down rather than resolved by assertion.
Answers [MISSION Q9](../port/MISSION.md), and contributes to Q6.
## The manifest names the boot-side slots — decoded
The movie manifest (`dat/tables.pak` entry `0x5b983a08`, schema `0x067025b9`,
[`movie_manifest.rs`](../../crates/sylpheed-formats/src/movie_manifest.rs)) is an
array of slots whose **key is the semantic role**, with a parallel array of value
records in the same order. Its first eight slots are the whole boot-side flow:
| slot key | movie | also bound |
|---|---|---|
| `LOGO1``LOGO4` | `logo1.wmv``logo4.wmv` | — |
| **`ADVERTISE_MOVIE`** | **`ADV.wmv`** | `VOICE_ADV` |
| `STAFF_ROLL` | `SYLPH_HD720p_8M-CBR_2ch.wmv` | subtitle table |
| **`MS00A`** | **`S00A.wmv`** | `SUBTITLE_S00A`, `VOICE_S00A`, a `pwterop_s01a.prt` overlay |
| `MS01A` | `S01A.wmv` | `SUBTITLE_S01A`, `VOICE_S01A` |
The alignment is not in doubt here even though the manifest has valueless slots
elsewhere: `logo1..4` zip 1:1 onto `LOGO1..4`, and `MS00A``S00A.wmv`,
`MS01A``S01A.wmv` are the manifest's own anchor rule.
**The `logo1``logo4` slots are bound to `.wmv` files that are not on the disc**
already established, and the reason the developer splash is a screen, not a video.
## ✅ The boot intro and the attract movie are the same asset
There is **no separate boot-intro slot**. `ADV.wmv` is bound to
`ADVERTISE_MOVIE` — it *is* the advertise movie, and the boot simply plays it
first. For the port that means one video, not two.
### Measured independently, from captured frames
Frames captured 5 s apart during an attract cycle were matched against five
candidate movies by frame signature (32×18 normalised grayscale, movie frames
cropped to the 675 of 720 rows the game surface shows, 1 fps sampling).
`tools/re-capture/frame_match.py`; full table in
[`data/attract-frame-match.txt`](data/attract-frame-match.txt).
**15 of 19 attract frames matched `ADV.wmv`, and the matched timestamps advance
monotonically at the sampling rate** — 39, 45, 56, 62, 75, 80, 85, 92, 102, 108,
113, 119, 131, 137 s — ending at 137 s, which is `ADV.wmv`'s full length, after
which the title returned. That is not a similarity score, it is a playhead.
**The control was run first and behaves the same way.** Frames captured during
the *boot* movie — known to be `ADV.wmv` — matched `ADV` at 116, 121, 129, 134 s.
Its failure mode is worth stating: on near-black frames the correlation collapses
(one control frame scored 0.000, another tied `S13A` 0.984 against `ADV` 0.926).
Those are exactly the 4 attract frames that did not match. **A dark frame carries
no signature; it is not evidence for the runner-up.**
### 🔴 This corrects an earlier claim of mine
Two iterations ago I recorded the attract movie as "≈85 s, so probably not
`ADV.wmv`'s 137 s". That was wrong, and the mistake was arithmetic on an
unobserved start: my sampling began when the movie was already **39 s in**, so
what I timed was the *tail* of it, not the whole thing.
## ✅ The new-game intro is `S00A.wmv`
Slot `MS00A` — the prologue story intro, 93.87 s, with its own subtitle table,
`VOICE_S00A`, and a text overlay. Decoded from the manifest, **not** measured:
Ⓐ on `NEW GAME` hangs the emulator
([`menu-navigation-semantics.md`](menu-navigation-semantics.md)), so this
binding has not been watched happening.
## 🟡 Skippable — the corpus contradicts itself, and I did not settle it
* [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) states
plainly that "Ⓐ during a movie skips the movie, every time".
* But `boot_menu.sh` / `skip_intro.sh` as they stand **deliberately do not tap
during a movie**, logging `movie … -> waiting it out (tapping breaks the
title)`, and the same document records run G: 88 Ⓐ presses through the boot
left a permanent black screen.
Both cannot be the whole story. **The test**: boot, tap Ⓐ exactly once well
inside the movie, and record whether the title arrives early. One boot, and it
was not run this iteration.
## ✅ What ends the attract movie
Nothing intervenes: it **plays to its end**. The last matched frame is `ADV` at
137 s — the file's full duration — and the title is back on the next sample. So
the attract cycle is: title, idle ~810 s, fade to black, `ADV.wmv` in full, back
to the title (which redraws `PRESS Ⓐ BUTTON`).
## What this contributes to Q6
The manifest is **data the game reads** to sequence the boot side: the slot keys
are in play order and the key *is* the role. That is not the whole driver — it
says what plays, not what decides to advance — but it is the first file-side
piece of Q6's second half, and it means the boot-side asset order does not have
to be authored.

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Match a captured game frame against candidate .wmv files by frame signature.
Static + previously-captured frames. See docs/re/movie-binding.md."""
import numpy as np, subprocess, sys, os
from PIL import Image
W,H=32,18
def sig_img(path, crop_bottom=False):
im=Image.open(path).convert("L")
if crop_bottom: # movie frame -> what the capture can see
w,h=im.size; im=im.crop((0,0,w,int(h*675/720)))
a=np.asarray(im.resize((W,H)),dtype=np.float32).ravel()
return (a-a.mean())/(a.std()+1e-6)
def movie_sigs(path, fps=1.0):
out=f"/tmp/q9/fr_{os.path.basename(path).replace('.wmv','')}"
os.makedirs(out, exist_ok=True)
if not os.listdir(out):
subprocess.run(["ffmpeg","-v","error","-i",path,"-vf",f"fps={fps},scale=160:90",
"-y",f"{out}/f%05d.png"],check=True)
fs=sorted(os.listdir(out))
return [(int(f[1:6]), sig_img(os.path.join(out,f), crop_bottom=True)) for f in fs]