This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/docs/port/HANDOFF.md
Sylpheed RE agent c760cdc509 re: the boot sequence is not data-driven -- closing Q6 with the negative
rather than leaving it amber

Q6's second half asked what the game READS to decide the boot order. The
answer is nothing, and the value here is the reach rather than a find.

Four places checked, the order in none of them: config.ini's [SYSTEM] is
empty and it is the disc's only config; the movie manifest carries the
boot-side assets but no transitions; the requested GamePart id was
already shown to exist only as a stack argument in flight, with no
persistent field and no literal store; and the string GP_ADVERTISE_DEMO
has zero xrefs of any kind, so nothing in the code reads the attract
entry of the id table. A transition is a call with an id argument.

Traced as far as it goes cheaply: the RegisterToFactory<0,
GamePart_Title> string is referenced from exactly one site,
sub_8280E148, which also takes the address of sub_821C7D98 -- where a
factory template puts its creator. Marked amber, because that is position
and convention rather than proof, and sub_821C7D98 has zero .rdata
references, which fits a new+ctor thunk and not a state machine. The
substantial function in that neighbourhood is sub_821C6458 and I did not
read it.

So Q6 closes as answered with the driver classified as code rather than
data, which means the port AUTHORS the sequence -- and that is fine,
because the sequence itself is measured end to end and the handoff now
carries it in one line.
2026-08-28 18:53:14 +00:00

18 KiB
Raw Blame History

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 sui-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
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
Q4 button → GamePart answered measured which screen all 5 buttons open — NEW GAMEDIFFICULTYSELECT DATA, not a hang. The GamePart id is still a name match, not a measurement — 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
Q6 boot sequence + what drives it answered sequence measured end to end; the driver is code, not data — four search spaces closed, so the port authors the sequence — 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
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 yetmenu-audio-cues.md
Q9 video binding + playback rules answered decoded from the movie manifest: ADVERTISE_MOVIEADV.wmv (boot intro and attract are one asset), MS00AS00A.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
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
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

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. 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).

  • Highlighted states pair by nameptbtn01.ratptbtn01f.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.

  • 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 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 GAMEDIFFICULTY (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, EXTRASGP_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.170.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: LOGO1LOGO4logo1.wmvlogo4.wmv (not on the disc — this is why the splash is a screen), ADVERTISE_MOVIEADV.wmv, STAFF_ROLLSYLPH_HD720p_8M-CBR_2ch.wmv, MS00AS00A.wmv (the new-game intro, 93.9 s, with subtitle + VOICE_S00A + a text overlay), MS01AS01A.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. 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).

  • The boot sequence is not data-driven — the port authors it. Four places were checked and the order is in none: config.ini's [SYSTEM] is empty, the movie manifest carries assets not transitions, the requested GamePart id lives only as a stack argument in flight (no persistent field, no literal store), and the string GP_ADVERTISE_DEMO has zero xrefs. A transition is a call with an id argument, chosen by code. The sequence itself is fully measured: splash → ADV.wmv → title + PRESS Ⓐ → (idle ~810 s → ADV.wmv in full → title) → Ⓐ → main menu, with Ⓑ from the main menu returning to the title.

  • 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). 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. logo1logo4 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_001BGM_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.