Files
Sylpheed/export-probe/manifest.json
Sylpheed port agent fda417a5db port(exporter): the archive list is data, not a hardcoded constant
crates/sylpheed-export read dat/GP_TITLE.pak and nothing else. authored/flow.json
records LOAD GAME, TUTORIAL, OPTIONS and NEW GAME's difficulty chain as MEASURED
destinations that are blocked because 'not a GP_TITLE build, so there is no
screen file to go to'. The blocker was neither the disc nor the reader --
examples/probe_archives.rs finds screen builds in 24 archives using the EXISTING
detector -- it was that constant.

The list now comes from authored/screen_names.json export_archives, whose
'archives' map was ALREADY keyed by archive path. Absent, it defaults to
dat/GP_TITLE.pak, so an old authored tree exports what it always did.

Sprite groups are mapped per archive rather than derived from the filename:
sprites live at sprites/<group>/<screen>/, unnamed builds are named build_NN by
entry index which restarts at 0 in every archive, so two archives sharing a group
would collide. An unmapped archive is rejected rather than given a directory a
typo could invent.

NO BEHAVIOUR CHANGE, and verified as such before anything is added: exported to a
separate tree with the current authored data and diffed -- export/screens and
export-probe/screens are IDENTICAL, manifest screen list identical.

The archive list itself is unchanged in this commit. Adding one is next, and
separately, so any regression is attributable to the archive rather than to the
refactor.
2026-09-03 17:57:06 +00:00

170 lines
17 KiB
JSON

{
"format": "sylpheed.manifest/1",
"exporter": "sylpheed-export 0.1.0",
"formats_rev": "8b6dbcf",
"disc": "/disc",
"screens": [
{
"name": "build_00",
"file": "screens/title/build_00.json",
"sprites": 7
},
{
"name": "build_01",
"file": "screens/title/build_01.json",
"sprites": 7
},
{
"name": "press_start",
"file": "screens/title/press_start.json",
"sprites": 2
},
{
"name": "press_start_jp",
"file": "screens/title/press_start_jp.json",
"sprites": 2
},
{
"name": "title",
"file": "screens/title/title.json",
"sprites": 18
},
{
"name": "main_menu",
"file": "screens/title/main_menu.json",
"sprites": 20
},
{
"name": "extras",
"file": "screens/title/extras.json",
"sprites": 20
},
{
"name": "title_jp",
"file": "screens/title/title_jp.json",
"sprites": 24
},
{
"name": "main_menu_jp",
"file": "screens/title/main_menu_jp.json",
"sprites": 20
},
{
"name": "extras_jp",
"file": "screens/title/extras_jp.json",
"sprites": 20
},
{
"name": "publisher_logo",
"file": "screens/title/publisher_logo.json",
"sprites": 2
},
{
"name": "developer_logos",
"file": "screens/title/developer_logos.json",
"sprites": 6
},
{
"name": "build_12",
"file": "screens/title/build_12.json",
"sprites": 9
},
{
"name": "publisher_logo_r",
"file": "screens/title/publisher_logo_r.json",
"sprites": 2
},
{
"name": "developer_logos_r",
"file": "screens/title/developer_logos_r.json",
"sprites": 6
},
{
"name": "build_15",
"file": "screens/title/build_15.json",
"sprites": 9
}
],
"videos": [
{
"name": "ADV",
"file": "video/ADV.ogv",
"command": "ffmpeg -hide_banner -loglevel error -y -i /disc/dat/movie/ADV.wmv -c:v libtheora -q:v 8 -c:a libvorbis -q:a 5 -af pan=stereo|FL=0.4142*FL+0.2929*FC+0.2929*BL |FR=0.4142*FR+0.2929*FC+0.2929*BR -ac 2 export-probe/video/ADV.ogv",
"why": "HANDOFF Q9: ADVERTISE_MOVIE -> ADV.wmv, and the boot intro and the attract movie are the SAME asset -- there is no separate boot slot.",
"duration_s": 137.437458,
"fps": 30.0
},
{
"name": "S00A",
"file": "video/S00A.ogv",
"command": "ffmpeg -hide_banner -loglevel error -y -i /disc/dat/movie/S00A.wmv -c:v libtheora -q:v 8 -c:a libvorbis -q:a 5 -af pan=stereo|FL=0.4142*FL+0.2929*FC+0.2929*BL |FR=0.4142*FR+0.2929*FC+0.2929*BR -ac 2 export-probe/video/S00A.ogv",
"why": "HANDOFF Q9: MS00A -> S00A.wmv is the new-game intro. P7.",
"duration_s": 93.778583,
"fps": 30.0
}
],
"audio": [
{
"kind": "se",
"name": "back",
"file": "audio/se/back.ogg",
"command": "ffmpeg -hide_banner -loglevel error -y -i export-probe/audio/se/.back.xma.wav -c:a libvorbis -q:a 5 export-probe/audio/se/back.ogg",
"why": "HANDOFF Q8, measured: the (B) back cue, 4 096 B / 0.344 s, reproduced across two boots. No `name_match` for the same reason as `confirm` -- Q8 names no identifier for it. AUTHORED because it is measured, not decoded: authored/audio.json se.back. Located in Static.slb at 0xec0 for 2 packet(s); the ASSEMBLY is sylpheed_formats::media::se_wave_riff, which refuses a short read.",
"peak_dbfs": -5.699542,
"duration_s": 0.343833
},
{
"kind": "se",
"name": "confirm",
"file": "audio/se/confirm.ogg",
"command": "ffmpeg -hide_banner -loglevel error -y -i export-probe/audio/se/.confirm.xma.wav -c:a libvorbis -q:a 5 export-probe/audio/se/confirm.ogg",
"why": "HANDOFF Q8, measured: the (A) confirm cue, 12 288 B / 1.016 s. NO `name_match`: Q8 is explicit that (A)'s wave was not separated between `SE_UI_DECIDE` and `SE_UI_SUB_WIN_OPN`, so naming it would invent the one thing the measurement did not settle. AUTHORED because it is measured, not decoded: authored/audio.json se.confirm. Located in Static.slb at 0x5d6c0 for 6 packet(s); the ASSEMBLY is sylpheed_formats::media::se_wave_riff, which refuses a short read.",
"peak_dbfs": 0.178538,
"duration_s": 1.016
},
{
"kind": "se",
"name": "move",
"file": "audio/se/move.ogg",
"command": "ffmpeg -hide_banner -loglevel error -y -i export-probe/audio/se/.move.xma.wav -c:a libvorbis -q:a 5 export-probe/audio/se/move.ogg",
"why": "HANDOFF Q8, measured: the d-pad move cue, 8 192 B / 0.533 s, reproduced across two boots. Left/right play nothing at all, which is a measurement too and is why there is no `left`/`right` row here rather than a silent file. AUTHORED because it is measured, not decoded: authored/audio.json se.move. Located in Static.slb at 0x1ec0 for 4 packet(s); the ASSEMBLY is sylpheed_formats::media::se_wave_riff, which refuses a short read.",
"peak_dbfs": -1.397579,
"duration_s": 0.533354,
"name_match": "SE_UI_CURSOR"
},
{
"kind": "bgm",
"name": "main_menu",
"file": "audio/bgm/main_menu.ogg",
"command": "ffmpeg -hide_banner -loglevel error -y -i export-probe/audio/bgm/.main_menu.0.xma.wav -i export-probe/audio/bgm/.main_menu.1.xma.wav -filter_complex amix=inputs=2:normalize=0,volume=0.500000 -ss 9.44 -t 61.87 -c:a libvorbis -q:a 5 export-probe/audio/bgm/main_menu.ogg",
"why": "MEASURED, HANDOFF Q10 -- NOT a port choice. `GamePart_Title`'s phase handler `sub_821C5580` plays cue 1103 = `BGM_103`, and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. Static code, disc census and runtime all agree; see docs/re/menu-audio-cues.md and docs/re/structures/bgm-two-stems.md. The name carries its `.slb` extension because that is what `sound.pak` hashes -- `BGM_103` alone resolves to nothing, which is how the first draft of this file failed. ✅ AUDITED 2026-08-31 -- the THREE legs are three, and that is now measured rather than asserted. Prompted by the Decoder's point that a decorative second support is worse than none, since a conclusion with two supports reads as better evidenced than one and apparent redundancy is itself the misinformation. Read literally, 'disc census' and 'runtime' could be ONE comparison -- declared wave sizes matched byte-for-byte against the probe -- which would make three legs two. It is a real third leg only if the census EXCLUDES alternatives: if another bank carried the same two sizes, the byte match would not distinguish BGM_103. Measured with this port's own reader (`crates/sylpheed-export/examples/bgm_size_census.rs`): of 32 readable BGM_* banks on the disc, EXACTLY ONE carries waves of that size. The census therefore excludes, the static-code leg names the cue independently, and the three legs stand. ✅ AND THE EXCLUSION IS TIGHTER THAN I STATED. The Decoder attempted to refute it from their own census tool rather than this port's reader: of 32 census rows, exactly one bank carries EITHER of those wave sizes -- not merely both together, which is what I measured. A collision would therefore need to reproduce a single size, not a pair, and none does. authored/audio.json bgm.main_menu names bank BGM_103.slb. Of its 2 sub-wave(s), 2 are SUMMED into one file and the sum is scaled by 1/2, which is the smallest constant that cannot clip. MEASURED, HANDOFF Q10: a bank is exactly TWO waves of identical duration (32/32 banks on the disc), sample-synchronous -- transient correlation peaks at lag 0.00 s over +/-5 s and both stop at the same millisecond. Concatenating them plays the piece twice, the second time as a bass-less stem; that was the previous reading and it is refuted. Emitting two files would be wrong for a second reason: MODDING rule 1 is one logical asset, one file, and handing a modder two stems to line up by hand is the reassembly the exporter exists to have already done. WHAT IS SUMMED IS SETTLED; WHAT WAVE 1 IS, IS NOT -- HANDOFF calls it quieter, far more L/R-decorrelated and almost bass-free, so it reads as a surround-rear pair OR a second intensity layer, and `ChannelMask` is 0x0002 on both so the file will not say. A unity sum is right under either reading; a weighting would only be justified once that is settled. MEASURED, and this field's own history is why it says so first. The bed loops; the loop is a RUNTIME field -- `loop_start`/`loop_end` in the XMA decoder context, set by `XMASetLoopData` and logged by Xenia -- and the cycle was watched directly: three wraps, both contexts wrapping at the same instant every time, mean 61.81 s against the 61.93 s authored in `loop_end_s`, 0.2 % apart from instruments sharing nothing. The export is TRIMMED to that window, because Godot loops a whole file and a loop region therefore has to BE the file. ⚠️ The window's START is not measured and is authored as 0, which is known to be wrong -- see `loop_start_why`. 🔴 EVERY SENTENCE THAT PRECEDED THIS ONE WAS REFUTED, and the previous text survived in the manifest for two days after the corrections were written. It said the loop would be `AUDIBLY WRONG AT THE SEAM [refuted]`, that `no loop-point field has been identified [refuted] anywhere`, and that trimming `would INVENT a loop point`. All three are false: the field exists, the 3.4 s of near-silence was the PORT'S loop and not the game's, and the trim is now what the measurement says. The corrections went into `loop_end_why` and `loop_start_why`; this field is the one the exporter concatenates into `manifest.json`, so the export went on telling readers the refuted story. A correction that does not reach the artifact a consumer reads has not been made. 📌 CITATIONS ADDED 2026-09-01, and their absence was found by `audit-kinds` the moment this field got a `kind` -- it had 1 400 characters of prose and nothing openable, which is exactly the state the audit exists to catch and could not see while the field was unlabelled. The wrap measurement is docs/re/data/menu-bgm-loop-measured.txt and the start is docs/re/data/menu-bgm-loop-start.txt; the bank's two-stem structure is docs/re/structures/bgm-two-stems.md.",
"peak_dbfs": -4.201146,
"duration_s": 61.87,
"loop_mode": "restart"
},
{
"kind": "voice",
"name": "ADV",
"file": "audio/voice/ADV.ogg",
"command": "ffmpeg -hide_banner -loglevel error -y -i export-probe/audio/voice/.ADV.0.xma.wav -i export-probe/audio/voice/.ADV.1.xma.wav -i export-probe/audio/voice/.ADV.2.xma.wav -filter_complex [0:a]anull,pan=mono|c0=0.500000*c0+0.500000*c1[m0];[1:a]anull,pan=mono|c0=c0[m1];[2:a]anull,pan=mono|c0=0.500000*c0+0.500000*c1[m2];[m0]volume=0.4142[w0];[m1]volume=0.2929[w1];[m2]volume=0.2929[w2];[w0][w1][w2]amix=inputs=3:normalize=0[a] -map [a] -c:a libvorbis -q:a 5 export-probe/audio/voice/ADV.ogg",
"why": "DECODED, not authored: the movie manifest in tables.pak binds ADV to a voice cue, and sylpheed_formats::media::resolve_movie_voice_region walks movie -> token -> sound id -> byte region [433425776, 437044592) of the continuous voice stream. NOT matched by filename: RT01A's voice lives inside VOICE_ADV.slb, so the name is right for this movie by luck and wrong for others. ✅ ALL 3 region chunk(s) are exported, summed. Keeping ONE was REFUTED FROM THE OUTPUT SIDE 2026-08-30: a recording of the game's own 6-channel output over the intro decomposes as capture = 0.600 x movie + residual, and the residual is THREE signals at three positions -- front pair (r 0.918), rear pair (r 0.929), and a centre whose partner LFE is empty to -115 dB. The load-bearing number is LFE reproducing to -115.73 dBFS: where nothing is added the two decoders agree exactly, so the other residuals are ADDED CONTENT and not codec mismatch. ⚠️ WHICH stream sits at which position is NOT determined -- the assignment is by position, not content -- so this is a mono sum divided by the count, never a positional downmix. The three downmix weights sum to one whatever the assignment, so the total is right and the distribution is the only thing unclaimed. ⚠️ The movie's OWN track is WMA Pro 5.1 and carries the bed; these streams are additional. 3 kept under presentation `all`, folded to mono on each stream's own live channels. Chunks found, in region order: chunk 0 1294396 B (137.324 s); chunk 1 1118268 B (137.324 s); chunk 2 1171516 B (137.324 s). Decoded length 137.324 s against the movie's 137.437 s (delta -0.114 s); NOT trimmed to fit -- a clamp would hide a resolution error, and the runtime stops the voice when the video ends.",
"peak_dbfs": -2.866331,
"duration_s": 137.32394
},
{
"kind": "voice",
"name": "S00A",
"file": "audio/voice/S00A.ogg",
"command": "ffmpeg -hide_banner -loglevel error -y -i export-probe/audio/voice/.S00A.0.xma.wav -i export-probe/audio/voice/.S00A.1.xma.wav -filter_complex [0:a]anull,pan=mono|c0=0.500000*c0+0.500000*c1[m0];[1:a]anull,pan=mono|c0=c0[m1];[m0][m1]amix=inputs=2:normalize=1[a] -map [a] -c:a libvorbis -q:a 5 export-probe/audio/voice/S00A.ogg",
"why": "DECODED, not authored: the movie manifest in tables.pak binds S00A to a voice cue, and sylpheed_formats::media::resolve_movie_voice_region walks movie -> token -> sound id -> byte region [452294000, 455499120) of the continuous voice stream. NOT matched by filename: RT01A's voice lives inside VOICE_ADV.slb, so the name is right for this movie by luck and wrong for others. ✅ ALL 3 region chunk(s) are exported, summed. Keeping ONE was REFUTED FROM THE OUTPUT SIDE 2026-08-30: a recording of the game's own 6-channel output over the intro decomposes as capture = 0.600 x movie + residual, and the residual is THREE signals at three positions -- front pair (r 0.918), rear pair (r 0.929), and a centre whose partner LFE is empty to -115 dB. The load-bearing number is LFE reproducing to -115.73 dBFS: where nothing is added the two decoders agree exactly, so the other residuals are ADDED CONTENT and not codec mismatch. ⚠️ WHICH stream sits at which position is NOT determined -- the assignment is by position, not content -- so this is a mono sum divided by the count, never a positional downmix. The three downmix weights sum to one whatever the assignment, so the total is right and the distribution is the only thing unclaimed. ⚠️ The movie's OWN track is WMA Pro 5.1 and carries the bed; these streams are additional. 2 kept under presentation `all`, folded to mono on each stream's own live channels. Chunks found, in region order: chunk 0 1810492 B (93.694 s); chunk 1 1263676 B (93.694 s); chunk 2 98364 B (93.694 s, SILENT). Also dropped: chunk 2 (93.694 s, 98364 B, peak SILENT). A chunk marked SILENT carries no signal and contributes nothing; one of a different duration is not a concurrent stream of this take. 🔴 THIS SENTENCE USED TO SAY the leading chunk was `the TAIL of the kept stream [refuted]`, on a sliding envelope correlation of r=0.998. The CORRELATION was sound and the INTERPRETATION was refuted: `resolve_movie_voice_region` was starting 238 packets inside the first stream, so what matched end-flush was a START-TRUNCATED SIMULTANEOUS stream, not a duplicate tail -- which is why it aligned at the end. Fixed in formats-pin-2026-08-30; that chunk is now kept at full length and nothing is dropped for that reason any more. Decoded length 93.694 s against the movie's 93.779 s (delta -0.085 s); NOT trimmed to fit -- a clamp would hide a resolution error, and the runtime stops the voice when the video ends.",
"peak_dbfs": -2.827022,
"duration_s": 93.69375
}
],
"warnings": [
"Screen builds from 1 only (dat/GP_TITLE.pak). Other archives on the disc also contain UI builds and are not exported. Only the two movies MISSION section 6 puts in scope.",
"The four splash bundles (entries 10/13 publisher, 11/14 developer) have no .rat layout child, so `is_build` cannot see them and no content rule can: element count and design size both overlap with two-element fragments in other archives. They are located by ENTRY INDEX from authored/screen_names.json `also_export`, which is a locator and not a claim -- see each one's name_why.",
"video/*.ogv: the 5.1->stereo fold is NOT the matrix MISSION §6 pins. §6 fixes it at FL = 1.0*FL + 0.707*FC + 0.707*BL (a human decision, 2026-08-29); this export ships that matrix scaled by 0.4142 -- same weighting, 7.65 dB quieter. Measured over the whole of both movies, float-decoded so nothing is pre-clamped: under the PINNED matrix ADV peaks at +4.26 dBFS with 4406 samples at or over full scale (1874 more than 1 dB over, longest clamped run 0.333 ms), while S00A peaks at -1.34 dBFS and never clips. So the pin overloads ADV and this constant is over-broad for S00A; the smallest single scalar under which neither clamps is 1/1.6339 = 0.612. NOT changed on the exporter's own authority -- the level of a mix is what §6 reserves to a human. See docs/port/DECISIONS.md."
]
}