From ba8202b4572a8d6c287aca5d28cf4352765c5fd5 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Mon, 31 Aug 2026 02:44:39 +0000 Subject: [PATCH] re: DIFFICULTY is a dialog -- DLG_SELECT_DIFFICULTY, GP_DIALOG entries 2/3 Closes a negative of mine whose failed assumption I had named: I searched for an 8-record btn-named build in an archive of its own, assuming DIFFICULTY's four items pair with f variants the way GP_TITLE's screens do. It has its own prefix and is not a GamePart screen at all. Three independent routes agree. The image lists DLG_SELECT_DIFFICULTY among the DLG_* dialog names at 0x820A41BB, and GP_DIFFICULTY appears zero times. GP_DIALOG entries 2/3 are the only builds there with pcbtn00..03 -- four buttons at design rows 259/329/399/469, spacing 70, an EN/JP pair. And my capture of the running screen puts its four rows within 4 px of those, with spacing 70.5/69.5/70.0 against the disc's 70/70/70. Reach stated: the entries are identified by button count and geometry, not by a binding from the DLG_ name to a pak entry. No such binding was found. Also notes the consequence for Q6's count-match: NEW GAME opens a DIALOG from an external archive, which still matches the count but is not the same category as OPTIONS or TUTORIAL opening a GamePart. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- .../examples/dialog_button_rows.rs | 46 +++++++++++++ .../examples/find_difficulty_names.rs | 66 +++++++++++++++++++ docs/re/data/difficulty-is-a-dialog.txt | 66 +++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 crates/sylpheed-formats/examples/dialog_button_rows.rs create mode 100644 crates/sylpheed-formats/examples/find_difficulty_names.rs create mode 100644 docs/re/data/difficulty-is-a-dialog.txt diff --git a/crates/sylpheed-formats/examples/dialog_button_rows.rs b/crates/sylpheed-formats/examples/dialog_button_rows.rs new file mode 100644 index 00000000..65788f26 --- /dev/null +++ b/crates/sylpheed-formats/examples/dialog_button_rows.rs @@ -0,0 +1,46 @@ +//! Is `GP_DIALOG` entry 2 the `DLG_SELECT_DIFFICULTY` screen? +//! +//! The image lists `DLG_SELECT_DIFFICULTY` among the `DLG_*` names at +//! `0x820A41BB`, so DIFFICULTY is a DIALOG, not a GamePart screen with its own +//! pak — which is why a search for an 8-record `btn` build in a difficulty-named +//! archive found nothing. `GP_DIALOG` entry 2 carries `pcbtn00`..`03`: four +//! buttons, matching EASY / NORMAL / HARD / BACK. +//! +//! CONTROL: `GP_TITLE` entry 5's five buttons must come back at the rows the disc +//! is independently known to place them (162/242/322/401/482, spacing 80). +//! +//! cargo run -p sylpheed-formats --example dialog_button_rows +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +fn rows(ar: &PakArchive, entry: usize, what: &str) { + let Ok(by) = ar.read(&ar.entries()[entry]) else { return }; + let Some(b) = ui_layout::parse_build(&by) else { return }; + let mut v: Vec<(i32, String)> = b + .elements + .iter() + .filter(|e| { + let n = &e.name; + (n.starts_with("pcbtn") || n.starts_with("ptbtn")) && !n.contains('f') + }) + .map(|e| (e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32), e.name.clone())) + .collect(); + v.sort_by_key(|r| r.0); + println!("\n{what} (entry {entry}):"); + for (y, n) in &v { + println!(" y {y:5} {n}"); + } + if v.len() > 1 { + let sp: Vec = v.windows(2).map(|w| w[1].0 - w[0].0).collect(); + println!(" spacing {sp:?}"); + } +} + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let t = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE"); + rows(&t, 5, "CONTROL: GP_TITLE main menu (must be 162/242/322/401/482)"); + let d = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG"); + rows(&d, 2, "GP_DIALOG candidate for DLG_SELECT_DIFFICULTY"); + rows(&d, 3, "GP_DIALOG entry 3 (the pair)"); +} diff --git a/crates/sylpheed-formats/examples/find_difficulty_names.rs b/crates/sylpheed-formats/examples/find_difficulty_names.rs new file mode 100644 index 00000000..caec503e --- /dev/null +++ b/crates/sylpheed-formats/examples/find_difficulty_names.rs @@ -0,0 +1,66 @@ +//! Where is the `DIFFICULTY` screen? Search by NAME, not by structure. +//! +//! A previous pass searched every pak for a build with exactly 8 `btn`-named +//! records, on the assumption that DIFFICULTY's four items (EASY / NORMAL / HARD +//! / BACK) pair with `f` focus variants the way GP_TITLE's screens do. Nothing +//! plausible turned up, and the assumption was mine — recorded as a negative +//! narrower than "not found" (data/gp-title-holds-three-button-screens.txt). +//! +//! This drops the structural assumption and looks for the words instead, across +//! every sprite AND record name in every build on the disc. +//! +//! CONTROL: the same scan must find `ptbtn11` in GP_TITLE — a name whose home is +//! independently known — when asked for it. A name scan that finds nothing +//! proves nothing unless it can find something. +//! +//! cargo run -p sylpheed-formats --example find_difficulty_names +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +const WANTED: &[&str] = &["easy", "normal", "hard", "diff", "level", "rank"]; + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let mut paks: Vec<_> = std::fs::read_dir(root.join("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 control = false; + let mut hits = 0usize; + for p in &paks { + let Ok(ar) = PakArchive::open(p) else { continue }; + let pname = p.file_name().unwrap().to_string_lossy().to_string(); + 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 all: Vec = b + .sprites + .keys() + .chain(b.records.keys()) + .cloned() + .collect(); + if pname == "GP_TITLE.pak" && all.iter().any(|n| n.contains("ptbtn11")) { + control = true; + } + let m: Vec<&String> = all + .iter() + .filter(|n| { + let l = n.to_lowercase(); + WANTED.iter().any(|w| l.contains(w)) + }) + .collect(); + if !m.is_empty() { + hits += 1; + let mut s: Vec = m.iter().map(|x| (*x).clone()).collect(); + s.sort(); + s.dedup(); + println!(" {pname:28} entry {i:3} {:?}", &s[..s.len().min(6)]); + } + } + } + println!("\ncontrol (found ptbtn11 in GP_TITLE): {}", if control { "PASSED" } else { "FAILED" }); + println!("{hits} build(s) carried a difficulty-ish name"); +} diff --git a/docs/re/data/difficulty-is-a-dialog.txt b/docs/re/data/difficulty-is-a-dialog.txt new file mode 100644 index 00000000..83fb5e3a --- /dev/null +++ b/docs/re/data/difficulty-is-a-dialog.txt @@ -0,0 +1,66 @@ +# Where does the DIFFICULTY screen live? ✅ DECODED 2026-08-31. +# It is a DIALOG -- `DLG_SELECT_DIFFICULTY`, GP_DIALOG.pak entries 2/3. +# +# CLOSES A NEGATIVE OF MINE. gp-title-holds-three-button-screens.txt recorded +# "not an 8-record btn-named build anywhere on the disc", with the failed +# assumption named as mine: I assumed DIFFICULTY's four items pair with `f` focus +# variants in an archive of its own, the way GP_TITLE's screens do. Both halves +# of that were wrong -- it has its own button prefix and it is not a GamePart +# screen at all. +# +################################################################################ +# ROUTE 1 -- THE IMAGE. /image/sylpheed.pe, 3 occurrences of "DIFFICULTY": +# 0x820A2548 STAGE | DIFFICULTY | TITLE | FADE | BASE_EXTRA ... +# (a GamePartTask::RegisterToFactory name list) +# 0x820A3377 RECORD_DIFFICULTY (a results/record field) +# 0x820A41BB DLG_LEADERBOARD_MENU_NEXT | DLG_SYSTEM_PAUSE | +# **DLG_SELECT_DIFFICULTY** | DLG_MISSION_OBJECTIVE | +# DLG_MESSAGE_BOX | DLG_MESSAGE_BOX_YES_NO | ... +# +# ✅ The third is the answer: DIFFICULTY is one of the game's DLG_* dialogs. +# ⚠️ "GP_DIFFICULTY" appears 0 times in the image, which is consistent. +# +################################################################################ +# ROUTE 2 -- THE DISC. GP_DIALOG.pak entries 2 and 3 are the only builds in that +# archive carrying `pcbtn00`..`pcbtn03` -- FOUR buttons, matching +# EASY / NORMAL / HARD / BACK. Design rows and spacing: +# +# entry 2 y 259 / 329 / 399 / 469 spacing 70, 70, 70 +# entry 3 identical (the EN/JP pair, as everywhere else) +# +# ✅ CONTROL: the same reader on GP_TITLE entry 5 returns 162/242/322/401/482, +# spacing 80 -- the rows that archive is independently known to place. +# +################################################################################ +# ROUTE 3 -- THE ORACLE. My own capture of the running DIFFICULTY screen +# (captures/menu-nav/live-difficulty-opens-normal.png), with the disc-grounded +# calibration capture_y = 64.82 + 0.9919 * design_y: +# +# design row predicted measured in the capture residual +# 259 321.7 323.5 +1.8 +# 329 391.2 394.0 +2.8 +# 399 460.6 463.5 +2.9 +# 469 530.0 533.5 +3.5 +# measured spacing 70.5 / 69.5 / 70.0 against the disc's 70 / 70 / 70 +# +# Four rows, all under 4 px, with the spacing agreeing exactly. The residual is a +# uniform ~+2.8 px offset, which is the calibration's own systematic and not a +# mismatch. +# +# => THREE INDEPENDENT ROUTES: the executable names it a dialog, the disc has a +# four-button dialog build, and the running game draws its rows where that +# build says they are. +# +# 📌 WHY IT MATTERS BEYOND THE LOCATION: DIFFICULTY being a DIALOG explains why +# NEW GAME's destination is not a screen in GP_TITLE, and it means the "four menu +# items load an external archive" reading of the event numbers +# (boot-config-and-gamepart-registry.md's count-match) is looser than it looked -- +# NEW GAME opens a dialog from GP_DIALOG.pak, which is an external archive, but a +# dialog is not the same kind of thing as OPTIONS or TUTORIAL opening a GamePart. +# The count still matches; the categories are not uniform. +# +# ⚠️ REACH: entries 2/3 are identified by button count and geometry, not by a +# name binding DLG_SELECT_DIFFICULTY to a pak entry. No such binding was found -- +# the DLG_* names live in a string list, and what maps a name to an archive entry +# is not decoded. Another 4-button dialog with the same rows would be +# indistinguishable by this evidence.