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:
53
authored/flow.json
Normal file
53
authored/flow.json
Normal 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."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user