export: reach the splash by authored entry index, and key names by entry

There were TWO splash screens and this port had neither. Entries 11/14 are the
developer logos; entries 10/13 are the SQUARE ENIX publisher wordmark, the first
thing the boot shows, which nothing in this project had noticed.

They have no .rat layout child so `is_build` cannot see them, and the RE agent
established that no CONTENT rule can either: design size fails (every extra
composable bundle sampled is 1280x720, same as every screen) and element count
fails (fragments run 2..15 elements in GP_OPTIONS/GP_SAVE_LOAD, the splash
halves are 3 and 7 -- the ranges overlap).

So `screen_builds` is `is_build` plus an authored allow-list of ENTRY INDICES,
each carrying a `why` that says it is a locator and not a claim. An allow-list
rather than a loosened predicate because this is safe in GP_TITLE and would not
be in general: there, widening adds exactly four bundles and all four are real
screens, zero fragments.

screen_names.json is rekeyed from enumeration ordinal to pak entry. Widening
renumbers ordinals, and a name that moves when the enumeration rule changes is
not a name -- the file always called the entry the stronger locator, and it is
now the only stable one. The two unnamed plates therefore renamed build_10/11 ->
build_12/15; they were always locators, and now they locate the right thing.

All 16 export and validate.
This commit is contained in:
Sylpheed port agent
2026-08-29 08:27:41 +00:00
parent 74b61b6ba4
commit ab3ca243c5
4 changed files with 214 additions and 38 deletions

53
authored/flow.json Normal file
View File

@@ -0,0 +1,53 @@
{
"format": "sylpheed.flow/1",
"_": [
"The boot sequence. AUTHORED, and it has to be: HANDOFF Q6 closed this with a",
"negative -- the order is in none of the four places it could have been. It is",
"not in config.ini's empty [SYSTEM], not in the movie manifest (which carries",
"assets, not transitions), not in a persistent GamePart field (the requested id",
"lives only as a stack argument in flight), and `GP_ADVERTISE_DEMO` has zero",
"xrefs of any kind. A transition is a call with a name argument, chosen by code.",
"",
"So this file REPRODUCES AN OBSERVATION. The sequence below is what the RE",
"agent watched the game do, not what any file on the disc says it does. Nothing",
"here may be presented as decoded."
],
"boot": [
{
"screen": "publisher_logo",
"why": "The SQUARE ENIX wordmark is the first thing the boot shows -- RE agent, 2026-08-29. Entry 10 of the pair; 13 is its region twin and the port shows one, not both."
},
{
"screen": "developer_logos",
"why": "GAME ARTS / SETA / studio anima, after the publisher wordmark. HANDOFF Q2."
},
{
"screen": "title",
"why": "HANDOFF Q2/Q6: the boot reaches the title after the splashes. The intro video (ADVERTISE_MOVIE -> ADV.wmv) plays between the splash and the title in the real boot and is SKIPPED here -- it is P4, and the sequencer names the gap rather than pretending the order is different."
}
],
"dwell": {
"_": [
"DELIBERATELY EMPTY. Each screen's dwell is its own keyframe group -- the",
"publisher wordmark reaches its hold at t=235 (3.92 s) and the developer",
"logos at t=190 (3.17 s), both read from the disc. Holding beyond that would",
"be a number nobody has measured, so the sequencer holds for zero extra time",
"and the pacing is the disc's own.",
"",
"When a capture times the real boot, the extra hold per screen goes here."
]
},
"screens": {
"_": [
"What each button does. NOT FILLED IN -- that is P5. HANDOFF Q4 measured the",
"destination screens and the RE agent later decoded that a transition is a",
"lookup by NAME, giving a candidate vocabulary (TITLE_SCREEN, TITLE_MENU,",
"LOADING, DIFFICULTY, EXTRA_MENU, TUTORIAL_MENU). Those are the right `goto`",
"targets when this is written, marked as the name match they are."
]
}
}

View File

@@ -1,17 +1,18 @@
{
"format": "sylpheed.screen_names/1",
"_": [
"Which GP_TITLE build is which screen. AUTHORED: the disc does not name its",
"Which GP_TITLE pak entry is which screen. AUTHORED: the disc does not name its",
"builds, so every name here is a decision. The identifications come from",
"HANDOFF Q2 (ui-title-build-map.md), which measured four of them against",
"framebuffer captures of the running game; the exporter stamps the name into",
"the screen file with name_source: \"authored\" so a reader can tell a",
"recovered name from an invented one.",
"HANDOFF Q2 (ui-title-build-map.md), which measured them against framebuffer",
"captures of the running game; the exporter stamps the name into the screen",
"file with name_source: \"authored\" so a reader can tell a recovered name from",
"an invented one.",
"",
"`build` is the index into the pak's list of screen builds -- what",
"`sylpheed-cli screen --build N` takes -- and is stable as long as the",
"enumeration rule is. The screen file also records the pak entry index,",
"which is the stronger locator.",
"KEYED BY PAK ENTRY INDEX, not by the enumeration ordinal. It used to be the",
"ordinal; widening the enumeration to reach the splash renumbers ordinals, and",
"a name that moves when the enumeration rule changes is not a name. The entry",
"was always described here as the stronger locator -- now it is the only",
"stable one.",
"",
"Delete an entry here the day the RE agent decodes a name field."
],
@@ -21,7 +22,10 @@
"name": "press_start",
"why": "HANDOFF Q2: builds 2/3 are the PRESS (A) BUTTON plate -- a build of its own, composited over the title and faded in a beat later. English of the EN/JP pair. Measured against a live capture."
},
"3": { "name": "press_start_jp", "why": "HANDOFF Q2: the Japanese twin of build 2. Out of scope for this milestone; named so it is not mistaken for a screen we need." },
"3": {
"name": "press_start_jp",
"why": "HANDOFF Q2: the Japanese twin of build 2. Out of scope for this milestone; named so it is not mistaken for a screen we need."
},
"4": {
"name": "title",
"why": "HANDOFF Q2: build 4 is the English title art. Measured against a live capture."
@@ -34,12 +38,57 @@
"name": "extras",
"why": "HANDOFF Q2: builds 6/9 are the EXTRAS submenu, the only submenu inside this archive. Measured against a fresh EXTRAS capture."
},
"7": { "name": "title_jp", "why": "HANDOFF Q2: the Japanese twin of build 4." },
"8": { "name": "main_menu_jp", "why": "HANDOFF Q2: the Japanese twin of build 5." },
"9": { "name": "extras_jp", "why": "HANDOFF Q2: the Japanese twin of build 6." }
"7": {
"name": "title_jp",
"why": "HANDOFF Q2: the Japanese twin of build 4."
},
"8": {
"name": "main_menu_jp",
"why": "HANDOFF Q2: the Japanese twin of build 5."
},
"9": {
"name": "extras_jp",
"why": "HANDOFF Q2: the Japanese twin of build 6."
},
"10": {
"name": "publisher_logo",
"why": "The SQUARE ENIX PUBLISHER wordmark -- the FIRST thing the boot sequence shows, before the developer logos. Measured by the RE agent 2026-08-29, render grid at docs/re/captures/title-builds/splash-both-halves-rendered.png. Entries 10/13 are region twins distinguished by the trademark glyph; 10 carries the (TM)."
},
"13": {
"name": "publisher_logo_r",
"why": "The region twin of entry 10, carrying (R) where 10 carries (TM). Named so it is not mistaken for a second screen the boot path needs."
},
"11": {
"name": "developer_logos",
"why": "The GAME ARTS / SETA / studio anima logos -- the developer splash, shown after the publisher wordmark. HANDOFF Q2 and the RE agent's 2026-08-29 render grid; draws 7/7 elements."
},
"14": {
"name": "developer_logos_r",
"why": "The region twin of entry 11, as 13 is to 10."
}
}
},
"unnamed": {
"dat/GP_TITLE.pak": "Builds 0/1 and 10/11 are a DELTASABER / SYLPHEED A.I. plate that was never seen running -- not in the boot path, not on any title-side screen, not in the attract loop (HANDOFF Q2). They export under their build index rather than a name we would be inventing."
"dat/GP_TITLE.pak": "Entries 0/1 and 12/15 are plates never seen running -- not in the boot path, not on any title-side screen, not in the attract loop (HANDOFF Q2). They export under their entry index rather than a name we would be inventing."
},
"also_export": {
"dat/GP_TITLE.pak": {
"10": {
"name": "publisher_logo",
"why": "LOCATED BY ENTRY INDEX, not by a rule. These four bundles declare their sprites directly and have no .rat layout child, so `is_build` cannot see them -- and the RE agent established that NO content rule can: design size fails (every extra composable bundle sampled is 1280x720, the same as every screen) and element count fails (fragments run 2..15 elements in GP_OPTIONS/GP_SAVE_LOAD while these are 3 and 7 -- the ranges overlap). Safe here and not in general: in GP_TITLE the widened set adds exactly these four and all four are real screens, zero fragments."
},
"11": {
"name": "developer_logos",
"why": "As entry 10: located by index because no content rule distinguishes a splash from a fragment. 7 elements, all drawn."
},
"13": {
"name": "publisher_logo_r",
"why": "As entry 10, region twin."
},
"14": {
"name": "developer_logos_r",
"why": "As entry 11, region twin."
}
}
}
}

View File

@@ -1,6 +1,5 @@
{
"format": "sylpheed.timing/1",
"keyframe_units_per_second": 60,
"why": [
"HANDOFF Q1. The disc says a keyframe is at `t=30`; it does not say what a",
@@ -20,20 +19,41 @@
],
"kind": "measured",
"source": "/reborn docs/port/HANDOFF.md Q1, docs/re/ui-keyframe-time-unit.md",
"ramp": "linear",
"ramp_why": [
"Also HANDOFF Q1, and part of the same measurement: the fade lands on the",
"linear value at every one of the seven sampled frames, so there is no ease."
],
"exit_ramp_seconds": null,
"exit_ramp_seconds": 0.4,
"exit_ramp_why": [
"NOT SET, deliberately. The last keyframe of every group carries no time --",
"the disc has no time slot there -- so the duration of the ramp INTO the",
"exit pose is unknown. HANDOFF Q7 measured the screen fade-out at ~0.4 s,",
"but that is the transition, which is P3's to author with its own evidence.",
"P2 plays the timed keyframes and holds; it never plays the exit ramp,",
"because it would have to invent how long it takes."
"HANDOFF Q7 + the RE agent's 2026-08-29 answer. MEASURED, not on the disc.",
"",
"Every element of a screen ends on exactly ONE untimed keyframe, so there is",
"exactly one unknown duration per screen -- the ramp INTO that final keyframe.",
"This is that duration. ~0.4 s, which is 24 units at 60 units/s.",
"",
"The alternative readings were tested and refuted. It is not a black quad laid",
"over a frozen screen: under that model a black rect scales every region by the",
"same 1-alpha, so the button-region / background-region brightness RATIO would",
"be constant through the fade. Measured on the RE agent's filmstrip it falls",
"6.495 -> 5.574 -> 3.105 -> 2.125 -> 1.935, a 3.4x monotonic drop. The screen",
"itself plays out: pteff00.prm ramps to opaque black while the button labels,",
"ptmsg, pteff10 and pteff12 all ramp to transparent, and ptframe1/2 hold.",
"",
"REACH, quoted from the RE agent rather than smoothed over: the filmstrip is",
"downsampled and the button region contains some background, so this pins the",
"DIRECTION, not 0.4 s to +/-0.05 s, and it is one transition pair. Treat the",
"number as approximate and the model as established."
],
"exit_ramp_units": 24,
"dwell_seconds": null,
"dwell_why": [
"NOT SET, and not needed. A screen's dwell is its OWN keyframe group: the",
"publisher wordmark reaches its hold at t=235 (3.92 s) and the developer logos",
"at t=190 (3.17 s), both read from the disc. Adding a hold on top of that would",
"be inventing a number nobody measured, so the sequencer holds for zero extra",
"time and the pacing you see is the disc's own.",
"",
"If a capture ever times the real boot, this is where that number goes."
]
}

View File

@@ -78,7 +78,11 @@ struct Manifest {
warnings: Vec<String>,
}
/// The authored `build index → name` map, keyed by archive path.
/// The authored `pak entry index → name` map, keyed by archive path.
///
/// Keyed by **entry**, not by the enumeration ordinal. The file itself always
/// called the entry "the stronger locator"; it is now also the only stable one,
/// because widening the enumeration to reach the splash renumbers the ordinals.
type NameMap = std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
#[derive(serde::Deserialize)]
@@ -96,6 +100,8 @@ fn load_names(authored: &Path) -> Result<NameMap> {
#[derive(serde::Deserialize)]
struct File {
archives: NameMap,
#[serde(default)]
also_export: AlsoExport,
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
@@ -104,17 +110,54 @@ fn load_names(authored: &Path) -> Result<NameMap> {
.archives)
}
/// Every RATC entry of a UI pak that parses as a screen build.
/// Extra pak entries to export that `is_build` does not accept, keyed by
/// archive. AUTHORED, and each carries its own `why`.
type AlsoExport =
std::collections::BTreeMap<String, std::collections::BTreeMap<String, NameEntry>>;
fn load_also_export(authored: &Path) -> Result<AlsoExport> {
let path = authored.join("screen_names.json");
if !path.exists() {
return Ok(AlsoExport::new());
}
#[derive(serde::Deserialize)]
struct File {
#[serde(default)]
also_export: AlsoExport,
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
Ok(serde_json::from_str::<File>(&raw)
.with_context(|| format!("parse {}", path.display()))?
.also_export)
}
/// Every RATC entry of a UI pak this exporter treats as a screen.
///
/// The filter is `is_build` — a bundle with a `.rat` layout child. The developer
/// splash declares its sprites directly and has none, so it is invisible here;
/// that is P3's problem and is recorded as a manifest warning rather than
/// silently widened.
fn screen_builds(ar: &PakArchive) -> Vec<(usize, Vec<u8>)> {
/// The rule is `is_build` — a bundle with a `.rat` layout child — **plus an
/// authored allow-list of entry indices**.
///
/// The allow-list exists because the splash screens declare their sprites
/// directly and have no `.rat` child, so `is_build` cannot see them, and **there
/// is no content rule that would**. The RE agent looked: design size fails
/// (every extra composable bundle sampled is 1280x720, the same as every
/// screen) and element count fails (fragments run 2..15 elements in
/// `GP_OPTIONS`/`GP_SAVE_LOAD` while the splash halves are 3 and 7 — the ranges
/// overlap). So the splashes are located **by entry index**, which is a locator
/// and not a claim, and each one says so in its own `why`.
///
/// This is safe here rather than in general: in `GP_TITLE` the widened set adds
/// exactly four bundles and all four are real screens, with zero fragments. In
/// another archive it would not be, which is why this is an allow-list and not
/// a widened predicate.
fn screen_builds(ar: &PakArchive, also: Option<&std::collections::BTreeMap<String, NameEntry>>)
-> Vec<(usize, Vec<u8>)>
{
let mut out = Vec::new();
for (i, e) in ar.entries().iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
if ui_layout::is_build(&bytes) {
let allowed = also.is_some_and(|m| m.contains_key(&i.to_string()));
if ui_layout::is_build(&bytes) || allowed {
out.push((i, bytes));
}
}
@@ -150,18 +193,26 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
let archive = "dat/GP_TITLE.pak";
let pak = disc.join(archive);
let ar = PakArchive::open(&pak).with_context(|| format!("open {}", pak.display()))?;
let builds = screen_builds(&ar);
let also = load_also_export(authored_dir)?;
let archive_also = also.get(archive);
let builds = screen_builds(&ar, archive_also);
println!("{archive}: {} screen build(s)", builds.len());
let archive_names = names.get(archive);
let mut screens = Vec::new();
for (build_idx, (entry, bytes)) in builds.iter().enumerate() {
let authored = archive_names.and_then(|m| m.get(&build_idx.to_string()));
let (name, name_source, why) = match authored {
// Keyed by ENTRY, not by the ordinal: widening the enumeration to reach
// the splash renumbers ordinals, and a name that moves when the rule
// changes is not a name.
let key = entry.to_string();
let named = archive_names
.and_then(|m| m.get(&key))
.or_else(|| archive_also.and_then(|m| m.get(&key)));
let (name, name_source, why) = match named {
Some(e) => (e.name.clone(), "authored", e.why.clone()),
// Nobody has identified this build. Emit a stable synthetic id and
// say in the file that the name is not a recovered one.
None => (format!("build_{build_idx:02}"), "index", None),
None => (format!("build_{entry:02}"), "index", None),
};
let ex = screen::export_build(
&out,
@@ -204,8 +255,11 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
warnings: vec![
"P0 scope: GP_TITLE screen builds only. No audio, no video, no other archive."
.into(),
"The developer-logo splash is not here: it declares its sprites directly and has \
no .rat layout child, so `is_build` does not see it. P3."
"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."
.into(),
],
};