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:
@@ -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