diff --git a/crates/sylpheed-formats/examples/find_difficulty_build.rs b/crates/sylpheed-formats/examples/find_difficulty_build.rs new file mode 100644 index 00000000..5aa7664d --- /dev/null +++ b/crates/sylpheed-formats/examples/find_difficulty_build.rs @@ -0,0 +1,62 @@ +//! Where does the `DIFFICULTY` screen live? +//! +//! `boot-config-and-gamepart-registry.md` records a count-match — "Ⓑ = event 0, +//! four menu items load an external archive, EXTRAS stays inside GP_TITLE" — +//! explicitly as an observation, not a decode. The disc can test half of it: +//! OPTIONS, LOAD GAME and TUTORIAL have their own paks, and EXTRAS' two items +//! have GP_MISSION_SELECT / GP_MOVIE_THEATER while EXTRAS itself is GP_TITLE +//! entries 6/9. NEW GAME is the fourth, and there is no GP_DIFFICULTY.pak. +//! +//! So: which archive holds a build with EASY / NORMAL / HARD buttons? +//! +//! CONTROL: the same scan must find the EXTRAS build in GP_TITLE, whose location +//! is independently known (entries 6/9, buttons ptbtn11/12/13). +//! +//! cargo run -p sylpheed-formats --example find_difficulty_build +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let dat = root.join("dat"); + let mut paks: Vec<_> = std::fs::read_dir(&dat) + .expect("dat") + .filter_map(|e| e.ok().map(|e| e.path())) + .filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false)) + .collect(); + paks.sort(); + + let mut found_extras = false; + for p in &paks { + let Ok(ar) = PakArchive::open(p) else { continue }; + for (i, e) in ar.entries().iter().enumerate() { + let Ok(by) = ar.read(e) else { continue }; + let Some(b) = ui_layout::parse_build(&by) else { continue }; + let btns: Vec<&String> = b + .records + .keys() + .filter(|n| n.starts_with("ptbtn") || n.contains("btn")) + .collect(); + if btns.len() != 8 { + continue; + } + let name = p.file_name().unwrap().to_string_lossy(); + // CONTROL: the known MAIN MENU build (11 records, so this control is now vacuous) must show up. + if name == "GP_TITLE.pak" && (i == 5 || i == 8) { + found_extras = true; + println!("CONTROL {name} entry {i}: {} button records — the known MAIN MENU build (11 records, so this control is now vacuous)", + btns.len()); + } + // any build outside GP_TITLE with a small button set is a candidate + if name != "GP_TITLE.pak" { + let mut names: Vec = btns.iter().map(|s| (*s).clone()).collect(); + names.sort(); + println!(" {name:28} entry {i:3} {} buttons {:?}", btns.len(), + &names[..names.len().min(8)]); + } + } + } + println!("\ncontrol {} — the known MAIN MENU build (11 records, so this control is now vacuous) was {}found", + if found_extras { "PASSED" } else { "FAILED" }, + if found_extras { "" } else { "NOT " }); +} diff --git a/crates/sylpheed-formats/examples/gp_title_buttons.rs b/crates/sylpheed-formats/examples/gp_title_buttons.rs new file mode 100644 index 00000000..e006d062 --- /dev/null +++ b/crates/sylpheed-formats/examples/gp_title_buttons.rs @@ -0,0 +1,30 @@ +//! Every button record in `GP_TITLE.pak`, per entry. +//! +//! Testing half of the count-match in boot-config-and-gamepart-registry.md: +//! "four menu items load an external archive, EXTRAS stays inside GP_TITLE". +//! If DIFFICULTY (NEW GAME's destination, EASY/NORMAL/HARD/BACK) is also inside +//! GP_TITLE, then NEW GAME loads nothing external and that reading is wrong. +//! +//! cargo run -p sylpheed-formats --example gp_title_buttons +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak"); + for (i, e) in ar.entries().iter().enumerate() { + let Ok(by) = ar.read(e) else { continue }; + let Some(b) = ui_layout::parse_build(&by) else { continue }; + let mut btns: Vec = b + .records + .keys() + .filter(|n| n.starts_with("ptbtn")) + .cloned() + .collect(); + btns.sort(); + if btns.is_empty() { + continue; + } + println!("entry {i:2} {:2} button records {:?}", btns.len(), btns); + } +} diff --git a/docs/re/data/gp-title-holds-three-button-screens.txt b/docs/re/data/gp-title-holds-three-button-screens.txt new file mode 100644 index 00000000..f912df4b --- /dev/null +++ b/docs/re/data/gp-title-holds-three-button-screens.txt @@ -0,0 +1,53 @@ +# Which main-menu destinations live INSIDE GP_TITLE? ✅ ONLY EXTRAS. +# 2026-08-31. Static, disc-wide over GP_TITLE.pak. +# +# THE QUESTION IT SERVES: boot-config-and-gamepart-registry.md records a +# count-match for the title part's event numbers -- "Ⓑ = event 0, four menu items +# load an external archive, EXTRAS stays inside GP_TITLE" -- and marks it +# explicitly as an observation, NOT a decode, because nothing showed that any +# particular event is a particular menu row. Half of it is disc-checkable: does +# EXTRAS alone live inside GP_TITLE? +# +# ✅ EVERY BUTTON RECORD IN GP_TITLE.pak, all 16 entries +# (examples/gp_title_buttons.rs): +# +# entry 2/3 ptbtn00, ptbtn00f -- the PRESS (A) plate +# entry 5/8 ptbtn01..05 + f variants + ptbtn02b -- MAIN MENU, 5 items +# entry 6/9 ptbtn11..13 + f variants -- EXTRAS, 3 items +# +# Three button screens, and no fourth. There is NO DIFFICULTY build in GP_TITLE, +# and DIFFICULTY is what NEW GAME opens (menu-navigation-semantics.md). +# +# ✅ AND THE OTHER FOUR DESTINATIONS HAVE THEIR OWN ARCHIVES on the disc: +# OPTIONS -> GP_OPTIONS.pak +# LOAD GAME -> GP_SAVE_LOAD.pak +# TUTORIAL -> GP_TUTORIAL.pak +# NEW GAME -> DIFFICULTY, which is NOT in GP_TITLE (see below) +# while EXTRAS' own two items are GP_MISSION_SELECT.pak and GP_MOVIE_THEATER.pak +# -- so EXTRAS is a screen that stays inside GP_TITLE and whose CHILDREN leave it. +# +# => THE STRUCTURAL HALF OF THE COUNT-MATCH HAS DISC SUPPORT: exactly one +# main-menu destination is internal to GP_TITLE, and it is EXTRAS. +# +# ⚠️ THIS IS STILL NOT A DECODE OF THE EVENT NUMBERS. It shows the SHAPE the +# count-match asserts is real on the disc; it does not show that event 3 is a +# particular row, and the page's own warning stands. What changes is that the +# "one event elsewhere, four to LOADING" pattern now matches a disc fact rather +# than only a count. +# +################################################################################ +# ❔ NOT LOCATED: where the DIFFICULTY build lives. +# +# Searched every .pak on the disc for a build with EXACTLY 8 button records -- +# four items (EASY / NORMAL / HARD / BACK) with `f` focus variants, the shape +# GP_TITLE's own screens use. Found in only five archives, none of them +# plausible: GP_DEBRIEFING_PILOTLOG, GP_DIALOG, GP_GAMEOVER, GP_OPTIONS, +# GP_PAUSE_MENU. Nothing in GP_SYSTEM, GP_SAVE_LOAD, GP_TUTORIAL, +# GP_MISSION_SELECT or GP_MOVIE_THEATER. +# +# ⚠️ REACH, and the assumption that failed is mine: I assumed DIFFICULTY's four +# items are 8 button records because GP_TITLE's screens pair every button with an +# `f`. They may not be -- BACK may not be a button record, the names may not +# contain "btn" at all, or the screen may not parse as a build. So the negative +# is "not an 8-record btn-named build anywhere on the disc", which is narrower +# than "not found".