Compare commits
8 Commits
auto/re-ti
...
formats-pi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eeae3006a | ||
|
|
d110cf38c7 | ||
|
|
b21c8e4118 | ||
|
|
9ca1eb50fd | ||
|
|
9a0ca0d71f | ||
|
|
f817dd5939 | ||
|
|
56cc7acfc3 | ||
|
|
49f109deb1 |
67
crates/sylpheed-formats/examples/decl_word_probe.rs
Normal file
67
crates/sylpheed-formats/examples/decl_word_probe.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! Does any unread declaration word POINT at the element's T8aD child?
|
||||
//!
|
||||
//! `pteff05.t32`/`pteff04.t32` resolve to no sprite because the `T8aD` they want
|
||||
//! is registered under the name `8AX`. Elimination says `8AX` is the one they
|
||||
//! mean -- one unresolved element, one unclaimed non-focus-state child, in 6 of
|
||||
//! 6 title-side builds. Elimination is not a pointer, so: the 60-byte
|
||||
//! declaration reads name[0..28], parent@32, kind@40, pivot@48/52. The words at
|
||||
//! +28, +36, +44 and +56 are unread. If one of them indexes the RATC child
|
||||
//! table, the RESOLVED elements are the control -- their child index is known,
|
||||
//! so a candidate field must reproduce it for them before it may be believed for
|
||||
//! the unresolved one.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_word_probe -- <pak> [entry]
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
const OFFS: [usize; 4] = [28, 36, 44, 56];
|
||||
|
||||
fn be32(b: &[u8], o: usize) -> u32 {
|
||||
if o + 4 > b.len() { return 0; }
|
||||
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: decl_word_probe <pak> [entry]");
|
||||
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
// Per candidate offset, across every build: control hits / control total.
|
||||
let mut hit = [0usize; 4];
|
||||
let mut tot = 0usize;
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
if want.is_some_and(|w| w != i) { continue; }
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
// Index space to test against: the T8aD children, in child order.
|
||||
let t8: Vec<&ratc::RatcChild> = kids.iter().filter(|c| c.kind == "T8aD").collect();
|
||||
if build.elements.iter().all(|el| el.sprite.is_some() || el.kind & 0x10 != 0) {
|
||||
continue;
|
||||
}
|
||||
println!("== entry {i} ({} elements, {} T8aD children)", build.elements.len(), t8.len());
|
||||
for (n, c) in t8.iter().enumerate() { println!(" child[{n:2}] {}", c.name); }
|
||||
for el in &build.elements {
|
||||
if el.kind & 0x10 != 0 { continue; }
|
||||
let d = &bytes[0x20 + el.index * 60..0x20 + (el.index + 1) * 60];
|
||||
let words: Vec<u32> = OFFS.iter().map(|&o| be32(d, o)).collect();
|
||||
// The control: for a RESOLVED element, which T8aD child is it?
|
||||
let truth = el.sprite.as_ref()
|
||||
.and_then(|s| t8.iter().position(|c| &c.name == s));
|
||||
if let Some(t) = truth {
|
||||
tot += 1;
|
||||
for (k, w) in words.iter().enumerate() {
|
||||
if *w as usize == t { hit[k] += 1; }
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" [{:2}] {:26} sprite={:?} child={:?} +28={} +36={} +44={} +56={}",
|
||||
el.index, el.name, el.sprite, truth,
|
||||
words[0] as i32, words[1] as i32, words[2] as i32, words[3] as i32
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("\nCONTROL: resolved elements whose child index a word reproduces, of {tot}:");
|
||||
for (k, o) in OFFS.iter().enumerate() {
|
||||
println!(" +{o:<3} {:3}/{tot}", hit[k]);
|
||||
}
|
||||
}
|
||||
51
crates/sylpheed-formats/examples/name_resolution.rs
Normal file
51
crates/sylpheed-formats/examples/name_resolution.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! Why does an element's sprite fail to resolve? Dump the two name spaces.
|
||||
//!
|
||||
//! `parse_build` resolves an element to a sprite by looking its DECLARED name up
|
||||
//! in (a) the `.rat` record table, then (b) the `T8aD` child table. `pteff05.t32`
|
||||
//! is in neither -- the `T8aD` it wants is registered as `8AX` -- so it resolves
|
||||
//! to None and `compose` drops it without recording it as missing. This prints
|
||||
//! both spaces, so the link between the two can be CHECKED rather than assumed.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example name_resolution -- <pak> <entry>
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: name_resolution <pak> [entry]");
|
||||
let want: Option<usize> = std::env::args().nth(2).and_then(|s| s.parse().ok());
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
if want.is_some_and(|w| w != i) {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let unresolved: Vec<&ui_layout::Element> = build
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.sprite.is_none() && el.kind & 0x10 == 0)
|
||||
.collect();
|
||||
let claimed: std::collections::HashSet<&str> =
|
||||
build.elements.iter().filter_map(|e| e.sprite.as_deref()).collect();
|
||||
let unclaimed: Vec<&String> =
|
||||
build.sprites.keys().filter(|k| !claimed.contains(k.as_str())).collect();
|
||||
if want.is_none() && unresolved.is_empty() && unclaimed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let un: Vec<&str> = unresolved.iter().map(|e| e.name.as_str()).collect();
|
||||
let uc: Vec<String> = unclaimed
|
||||
.iter()
|
||||
.map(|k| format!("{k}({} B)", build.sprites[*k].1))
|
||||
.collect();
|
||||
println!(
|
||||
"entry {i:3} {:2} elements UNRESOLVED {:?} UNCLAIMED {:?}",
|
||||
build.elements.len(), un, uc
|
||||
);
|
||||
if want.is_some() {
|
||||
for el in &build.elements {
|
||||
let mark = if el.sprite.is_none() && el.kind & 0x10 == 0 { " <-- UNRESOLVED" } else { "" };
|
||||
println!(" [{:2}] kind {:#06x} {:28} -> {:?}{mark}", el.index, el.kind, el.name, el.sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
crates/sylpheed-formats/examples/rat_leaf_placement.rs
Normal file
71
crates/sylpheed-formats/examples/rat_leaf_placement.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! Does a `.rat` leaf record decode with the same reader as a whole bundle?
|
||||
//!
|
||||
//! The port needs the position of `ptbtneff01.t32`, the focus ring, which is
|
||||
//! declared *inside* the nested `ptbtn0Nf.rat` leaf and is therefore invisible
|
||||
//! to anything that walks only a bundle's top-level elements.
|
||||
//!
|
||||
//! The leaf's first 32 bytes have the same shape as a bundle header --
|
||||
//! `"RATC"`, `0x3c` declaration-entry size at `+4`, element count at `+20`,
|
||||
//! design size at `+24`/`+28` -- so the hypothesis is that `parse_build` reads
|
||||
//! it unchanged. The control is the BASE record `ptbtn0N.rat`, whose single
|
||||
//! element's position is already known independently: the parent screen's
|
||||
//! `screen info` reports `ptbtn01.rat` resting at (542,162).
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example rat_leaf_placement -- <GP_TITLE.pak>
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: … <pak>");
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
|
||||
for (ei, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
// Only the title-family bundles carry ptbtn records.
|
||||
if !kids.iter().any(|c| c.name == "ptbtn01f.rat") {
|
||||
continue;
|
||||
}
|
||||
println!("=== pak entry {ei}");
|
||||
for c in &kids {
|
||||
if c.kind != "RATC" || !c.name.starts_with("ptbtn") {
|
||||
continue;
|
||||
}
|
||||
let leaf = &bytes[c.offset..c.offset + c.size];
|
||||
match ui_layout::parse_build(leaf) {
|
||||
None => println!(" {:16} …parse_build says no", c.name),
|
||||
Some(b) => {
|
||||
println!(
|
||||
" {:16} {}x{} {} element(s), fallback={}",
|
||||
c.name,
|
||||
b.design_w,
|
||||
b.design_h,
|
||||
b.elements.len(),
|
||||
b.from_fallback
|
||||
);
|
||||
for el in &b.elements {
|
||||
let r = el.rest();
|
||||
println!(
|
||||
" [{}] {:18} pivot ({:4},{:4}) rest ({:5},{:5}) kf {}",
|
||||
el.index,
|
||||
el.name,
|
||||
el.pivot_x,
|
||||
el.pivot_y,
|
||||
r.map(|k| k.x).unwrap_or(-1),
|
||||
r.map(|k| k.y).unwrap_or(-1),
|
||||
el.keyframes.len()
|
||||
);
|
||||
for (i, k) in el.keyframes.iter().enumerate() {
|
||||
println!(
|
||||
" kf{i} t={:?} pos=({},{}) scale={}%,{}% a={} rot={} tint={:#010x}",
|
||||
k.time, k.x, k.y, k.scale_x, k.scale_y,
|
||||
k.fade >> 24, k.rotation_deg, k.tint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
78
crates/sylpheed-formats/examples/ratc_child_names.rs
Normal file
78
crates/sylpheed-formats/examples/ratc_child_names.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! Is a RATC child's name the printable run before its magic, or the `opt ` block?
|
||||
//!
|
||||
//! `ratc::parse` names each child by scanning backwards for the ASCII run that
|
||||
//! precedes its magic. That is usually right, but it is a HEURISTIC, and the
|
||||
//! real format states the name explicitly: immediately before each child sits
|
||||
//!
|
||||
//! "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||||
//!
|
||||
//! -- the same `opt ` block `ui_layout::opt_link` already decodes for a button's
|
||||
//! focus link. When those 3 trailing bytes happen to be printable the heuristic
|
||||
//! reads THEM as the name: the title screens' full-resolution background comes
|
||||
//! out as `8AX` (bytes 38 41 58) instead of `pteff05.t32`, its element then
|
||||
//! resolves to no sprite, and `compose` drops the screen's background.
|
||||
//!
|
||||
//! This compares the two readings for every RATC child in the paks given.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ratc_child_names -- <pak>...
|
||||
use sylpheed_formats::{pak, ratc};
|
||||
|
||||
/// The name stated by the `opt ` block that ends just before `at`.
|
||||
fn opt_name(buf: &[u8], at: usize) -> Option<String> {
|
||||
// The block is short; search back a bounded window for the tag.
|
||||
let lo = at.saturating_sub(128);
|
||||
let win = &buf[lo..at];
|
||||
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
|
||||
let len = u32::from_be_bytes(buf[pos + 4..pos + 8].try_into().ok()?) as usize;
|
||||
if len == 0 || len > 64 || pos + 8 + len > at {
|
||||
return None;
|
||||
}
|
||||
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]).to_string();
|
||||
// It must be THIS child's block: name, NUL, then a short run to the magic.
|
||||
if at - (pos + 8 + len) > 8 {
|
||||
return None;
|
||||
}
|
||||
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut children = 0usize;
|
||||
let mut with_opt = 0usize;
|
||||
let mut agree = 0usize;
|
||||
let mut disagree: Vec<(String, usize, String, String)> = Vec::new();
|
||||
for path in std::env::args().skip(1) {
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
for c in &kids {
|
||||
children += 1;
|
||||
let Some(o) = opt_name(&bytes, c.offset) else { continue };
|
||||
with_opt += 1;
|
||||
if o == c.name {
|
||||
agree += 1;
|
||||
} else {
|
||||
disagree.push((short.clone(), i, c.name.clone(), o));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("RATC children scanned : {children}");
|
||||
println!(" with an `opt ` block: {with_opt}");
|
||||
println!(" scanned name AGREES : {agree}");
|
||||
println!(" scanned name DIFFERS : {}", disagree.len());
|
||||
let mut by_pair: std::collections::BTreeMap<(String, String), usize> = Default::default();
|
||||
for (_, _, scanned, opt) in &disagree {
|
||||
*by_pair.entry((scanned.clone(), opt.clone())).or_default() += 1;
|
||||
}
|
||||
println!("\ndistinct disagreements (scanned -> opt), with counts:");
|
||||
for ((s, o), n) in &by_pair {
|
||||
println!(" {s:24} -> {o:24} x{n}");
|
||||
}
|
||||
println!("\nfirst 20 occurrences:");
|
||||
for (p, i, s, o) in disagree.iter().take(20) {
|
||||
println!(" {p} entry {i:4} {s:20} -> {o}");
|
||||
}
|
||||
}
|
||||
119
crates/sylpheed-formats/examples/ratc_optless_children.rs
Normal file
119
crates/sylpheed-formats/examples/ratc_optless_children.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! The 60 RATC children that carry no `opt ` block — do they lack it, or is our
|
||||
//! window too small?
|
||||
//!
|
||||
//! [`ratc::parse`] now prefers the name a child's own `opt ` block states, and
|
||||
//! falls back to the old backwards printable-run scan when there is no block
|
||||
//! within 128 bytes. That fallback fires for 60 of the disc's 18 002 children
|
||||
//! (0.3 %), and the reach of the finding in `docs/re/structures/ratc-child-names.md`
|
||||
//! stops there: "whether they genuinely lack the block or sit past the search
|
||||
//! window is not established".
|
||||
//!
|
||||
//! This settles that. For every child with no accepted block it reports
|
||||
//!
|
||||
//! * whether an `opt ` tag exists at all further back, and how far;
|
||||
//! * which guard rejected a tag that WAS in the window (length, gap, charset);
|
||||
//! * the child's position in its bundle and its magic, in case the opt-less
|
||||
//! ones are structurally distinct (e.g. always the first child);
|
||||
//! * the raw bytes before the magic, so the fallback's answer can be judged.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ratc_optless_children -- <pak>...
|
||||
use sylpheed_formats::{pak, ratc};
|
||||
|
||||
/// Why a child has no accepted `opt ` name. Mirrors `ratc::opt_name`'s guards
|
||||
/// one for one, so a rejection here is the same rejection the parser made.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
enum Why {
|
||||
/// No `opt ` tag in the 128-byte window, and none anywhere before it either.
|
||||
NoTagAtAll,
|
||||
/// No tag in the window, but one exists further back, this many bytes away.
|
||||
TagBeyondWindow(usize),
|
||||
/// Tag found, but its BE32 length is 0 or > 64.
|
||||
BadLength(usize),
|
||||
/// Tag found, name ends more than 8 bytes before the magic — a neighbour's.
|
||||
GapTooBig(usize),
|
||||
/// Tag found, the named bytes are not all printable ASCII.
|
||||
NotGraphic,
|
||||
}
|
||||
|
||||
/// The parser's own window.
|
||||
const WINDOW: usize = 128;
|
||||
|
||||
fn classify(buf: &[u8], at: usize) -> Option<Why> {
|
||||
let lo = at.saturating_sub(WINDOW);
|
||||
let pos = match buf[lo..at].windows(4).rposition(|w| w == b"opt ") {
|
||||
Some(p) => lo + p,
|
||||
None => {
|
||||
// Widen to the whole buffer before the child: is it merely far away?
|
||||
return Some(match buf[..at].windows(4).rposition(|w| w == b"opt ") {
|
||||
Some(p) => Why::TagBeyondWindow(at - p),
|
||||
None => Why::NoTagAtAll,
|
||||
});
|
||||
}
|
||||
};
|
||||
let len = u32::from_be_bytes(buf.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
|
||||
if len == 0 || len > 64 || pos + 8 + len > at {
|
||||
return Some(Why::BadLength(len));
|
||||
}
|
||||
let gap = at - (pos + 8 + len);
|
||||
if gap > 8 {
|
||||
return Some(Why::GapTooBig(gap));
|
||||
}
|
||||
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]);
|
||||
if s.is_empty() || !s.chars().all(|c| c.is_ascii_graphic()) {
|
||||
return Some(Why::NotGraphic);
|
||||
}
|
||||
None // accepted — this child is not one of the 60
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut total = 0usize;
|
||||
let mut rows: Vec<(String, usize, usize, String, String, Why)> = Vec::new();
|
||||
let mut first_child_of_bundle = 0usize;
|
||||
for path in std::env::args().skip(1) {
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (ei, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
for (ci, c) in kids.iter().enumerate() {
|
||||
total += 1;
|
||||
let Some(why) = classify(&bytes, c.offset) else { continue };
|
||||
if ci == 0 {
|
||||
first_child_of_bundle += 1;
|
||||
}
|
||||
let lo = c.offset.saturating_sub(24);
|
||||
let hex = bytes[lo..c.offset]
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
rows.push((short.clone(), ei, ci, c.name.clone(), hex, why));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("RATC children scanned : {total}");
|
||||
println!(" with NO accepted `opt ` block: {}", rows.len());
|
||||
println!(" ... of which are child #0 : {first_child_of_bundle}");
|
||||
|
||||
let mut by_why: std::collections::BTreeMap<String, usize> = Default::default();
|
||||
for r in &rows {
|
||||
let k = match &r.5 {
|
||||
Why::TagBeyondWindow(_) => "TagBeyondWindow".to_string(),
|
||||
Why::BadLength(_) => "BadLength".to_string(),
|
||||
Why::GapTooBig(_) => "GapTooBig".to_string(),
|
||||
other => format!("{other:?}"),
|
||||
};
|
||||
*by_why.entry(k).or_default() += 1;
|
||||
}
|
||||
println!("\nwhy, by cause:");
|
||||
for (k, n) in &by_why {
|
||||
println!(" {k:20} x{n}");
|
||||
}
|
||||
|
||||
println!("\nevery occurrence (name is what the FALLBACK scan returned):");
|
||||
for (p, ei, ci, name, hex, why) in &rows {
|
||||
println!(" {p:28} entry {ei:4} child {ci:3} {name:24} {why:?}");
|
||||
println!(" 24 bytes before the magic: {hex}");
|
||||
}
|
||||
}
|
||||
27
crates/sylpheed-formats/examples/se_wave_dump.rs
Normal file
27
crates/sylpheed-formats/examples/se_wave_dump.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Dump the menu's three SE cues as decodable `RIFF`s, to prove `se_wave_riff`
|
||||
//! produces something ffmpeg actually accepts.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example se_wave_dump -- <outdir>
|
||||
//! ffmpeg -i <outdir>/move.riff move.wav
|
||||
use sylpheed_formats::media::{self, DirectorySource};
|
||||
|
||||
fn main() {
|
||||
let out = std::env::args().nth(1).unwrap_or_else(|| "/tmp".into());
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(&disc);
|
||||
for (name, off, pkts) in [("move", 0x1ec0usize, 4usize), ("back", 0x0ec0, 2), ("confirm", 0x5d6c0, 6)] {
|
||||
match media::se_wave_riff(&src, "Static.slb", off, pkts, 1, 48000) {
|
||||
Ok(riff) => {
|
||||
let p = format!("{out}/{name}.riff");
|
||||
std::fs::write(&p, &riff).unwrap();
|
||||
println!("{p}: {} bytes ({pkts} packets at {off:#x})", riff.len());
|
||||
}
|
||||
Err(e) => println!("{name}: ERROR {e}"),
|
||||
}
|
||||
}
|
||||
// The refusal path: a packet count the bank cannot satisfy.
|
||||
match media::se_wave_riff(&src, "Static.slb", 0x1ec0, 1 << 20, 1, 48000) {
|
||||
Ok(_) => println!("REFUSAL PATH FAILED — returned a short stream"),
|
||||
Err(e) => println!("refusal path ok: {e}"),
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,62 @@ pub fn sound_bank_riffs<S: DiscSource + ?Sized>(
|
||||
Ok(riffs_of(&bytes))
|
||||
}
|
||||
|
||||
/// One sound-effect wave out of a **delimiter-less** bank, as a decodable `RIFF`.
|
||||
///
|
||||
/// `Static.slb` — where the menu's cues live — has no `RIFF`, no `seek` chunk and
|
||||
/// no XACT container: it is a packed run of whole 2048-byte XMA1 packets. So
|
||||
/// [`sound_bank_riffs`] finds nothing to split on, and a wave is defined *only*
|
||||
/// by `(offset, packet_count)`. Both come from the running game, not from the
|
||||
/// file: launch Canary with `--xma_param_probe=true`, trigger the sound, and the
|
||||
/// log prints the stream's packet count and first 32 bytes; searching those bytes
|
||||
/// in the bank gives the offset. ⚠️ The file order is **not** cue-id order, so the
|
||||
/// index cannot be counted out — see `docs/re/menu-audio-cues.md`.
|
||||
///
|
||||
/// The three cues a menu needs, all mono 48 kHz:
|
||||
///
|
||||
/// | event | offset | packets |
|
||||
/// |---|---|---|
|
||||
/// | d-pad move | `0x1ec0` | 4 |
|
||||
/// | Ⓑ back | `0x0ec0` | 2 |
|
||||
/// | Ⓐ confirm | `0x5d6c0` | 6 |
|
||||
///
|
||||
/// Returns an error rather than a short stream if the bank does not actually
|
||||
/// hold `packet_count` whole packets at `offset` — a truncated XMA stream decodes
|
||||
/// to plausible-sounding garbage, which is the failure worth refusing.
|
||||
pub fn se_wave_riff<S: DiscSource + ?Sized>(
|
||||
source: &S,
|
||||
bank: &str,
|
||||
offset: usize,
|
||||
packet_count: usize,
|
||||
channels: u8,
|
||||
rate: u32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let len = packet_count * crate::slb::XMA1_PACKET;
|
||||
// Read only the packets asked for, not the whole bank. That is not just an
|
||||
// efficiency point: `Static.slb` is 8.97 MB and is the ONE entry in
|
||||
// `sound.pak` whose declared extent runs past the end of the extracted
|
||||
// segments (by 616 768 B), so reading it whole fails outright on a disc
|
||||
// extraction that is short at the tail. Every cue we need sits in the first
|
||||
// few hundred KB. See `docs/re/menu-audio-cues.md`.
|
||||
let toc = source.read_file("dat/sound.pak")?;
|
||||
let entries = PakArchive::parse_toc(&toc).map_err(|e| e.to_string())?;
|
||||
let hash = crate::hash::name_hash(bank);
|
||||
let idx = entries
|
||||
.binary_search_by_key(&hash, |e| e.name_hash)
|
||||
.map_err(|_| format!("{bank}: not present in sound.pak"))?;
|
||||
let e = &entries[idx];
|
||||
if offset + len > e.comp_size as usize {
|
||||
return Err(format!(
|
||||
"{bank}: {packet_count} packets at {offset:#x} need {len} bytes, \
|
||||
but the bank declares only {} bytes",
|
||||
e.comp_size
|
||||
));
|
||||
}
|
||||
let packets =
|
||||
source.read_segment_range("dat/sound", e.offset as u64 + offset as u64, len)?;
|
||||
Ok(crate::slb::xma1_wave_riff(&packets, channels, rate))
|
||||
}
|
||||
|
||||
/// The XMA `RIFF`s of a continuous byte region of the voice stream, as returned
|
||||
/// by [`resolve_movie_voice_region`].
|
||||
pub fn voice_region_riffs<S: DiscSource + ?Sized>(
|
||||
|
||||
@@ -62,7 +62,7 @@ pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
|
||||
for (idx, &(off, kind)) in offs.iter().enumerate() {
|
||||
let next = offs.get(idx + 1).map(|&(o, _)| o).unwrap_or(bytes.len());
|
||||
children.push(RatcChild {
|
||||
name: name_before(bytes, off),
|
||||
name: opt_name(bytes, off).unwrap_or_else(|| name_before(bytes, off)),
|
||||
kind: kind.to_string(),
|
||||
offset: off,
|
||||
size: next.saturating_sub(off),
|
||||
@@ -71,9 +71,51 @@ pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
|
||||
Some(children)
|
||||
}
|
||||
|
||||
/// The name a child's own `opt ` block states, if it has one.
|
||||
///
|
||||
/// The real format is explicit. Immediately before each child sits
|
||||
///
|
||||
/// ```text
|
||||
/// "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||||
/// ```
|
||||
///
|
||||
/// -- the same `opt ` block `ui_layout`'s focus link already reads. Prefer it,
|
||||
/// because [`name_before`] is a heuristic and those 3 trailing bytes are
|
||||
/// sometimes printable, in which case the heuristic reads THEM as the name.
|
||||
/// Measured disc-wide: of 18 002 RATC children, 17 942 carry an `opt ` block,
|
||||
/// 17 918 of which agree with the scan and **24 do not** -- every one of the 24
|
||||
/// a 3-byte tail (`8AX` x22, `'OX` x2) beating a real name. On the title screens
|
||||
/// that cost the whole background: `pteff05.t32` came out as `8AX`, its element
|
||||
/// then resolved to no sprite, and `compose` silently dropped it. See
|
||||
/// `docs/re/structures/ratc-child-names.md`.
|
||||
fn opt_name(bytes: &[u8], off: usize) -> Option<String> {
|
||||
let lo = off.saturating_sub(128);
|
||||
let win = &bytes[lo..off];
|
||||
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
|
||||
let len = u32::from_be_bytes(bytes.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
|
||||
if len == 0 || len > 64 || pos + 8 + len > off {
|
||||
return None;
|
||||
}
|
||||
// It must be THIS child's block: the name, its NUL and a short run to the
|
||||
// magic. Anything further away is a neighbour's block, so fall back.
|
||||
if off - (pos + 8 + len) > 8 {
|
||||
return None;
|
||||
}
|
||||
let s = String::from_utf8_lossy(&bytes[pos + 8..pos + 8 + len]).to_string();
|
||||
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
|
||||
}
|
||||
|
||||
/// The nearest name string preceding `off`: the *last* printable run (len ≥ 3)
|
||||
/// in the 96 bytes before the child magic. A few record-header bytes usually sit
|
||||
/// between the name and the magic, so an exact-adjacency scan isn't enough.
|
||||
///
|
||||
/// Fallback only -- [`opt_name`] is the stated name. 60 of the disc's 18 002
|
||||
/// children have no `opt ` block and still rely on this, and all 60 are
|
||||
/// accounted for: they are the ten frames of the disc's only `.tan` frame
|
||||
/// sequence, in six language copies of one `GP_READY_ROOM` bundle, where a
|
||||
/// single `opt ` block names the whole run. See
|
||||
/// `docs/re/structures/ratc-tan-frame-sequence.md` -- and note that this means
|
||||
/// `parse` OVER-reports there, listing frames as children.
|
||||
fn name_before(bytes: &[u8], off: usize) -> String {
|
||||
let start = off.saturating_sub(96);
|
||||
let window = &bytes[start..off];
|
||||
@@ -101,6 +143,49 @@ fn name_before(bytes: &[u8], off: usize) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The `opt ` block wins over a printable tail.
|
||||
///
|
||||
/// This is the `pteff05.t32` / `8AX` case, byte for byte: the name is stated
|
||||
/// with an explicit length, then a NUL, then three payload bytes that happen
|
||||
/// to spell `8AX` in ASCII. The old backwards printable-run scan returned
|
||||
/// `8AX` here, which is what dropped the background from every menu screen.
|
||||
#[test]
|
||||
fn opt_block_beats_a_printable_tail() {
|
||||
let mut b = RATC_MAGIC.to_vec();
|
||||
b.extend_from_slice(&[0u8; 28]);
|
||||
b.extend_from_slice(b"opt ");
|
||||
b.extend_from_slice(&11u32.to_be_bytes()); // len("pteff05.t32")
|
||||
b.extend_from_slice(b"pteff05.t32\0");
|
||||
b.extend_from_slice(b"8AX"); // payload, printable by accident
|
||||
let off = b.len();
|
||||
b.extend_from_slice(b"T8aD");
|
||||
b.extend_from_slice(&[0u8; 16]);
|
||||
|
||||
let kids = parse(&b).expect("parse");
|
||||
assert_eq!(kids.len(), 1);
|
||||
assert_eq!(kids[0].name, "pteff05.t32");
|
||||
assert_eq!(kids[0].offset, off);
|
||||
// And the heuristic on its own really would have said `8AX` -- so this
|
||||
// test fails for the right reason if the preference is ever reversed.
|
||||
assert_eq!(name_before(&b, off), "8AX");
|
||||
}
|
||||
|
||||
/// No `opt ` block: 60 of the disc's 18 002 children are like this, and they
|
||||
/// must keep working off the scan.
|
||||
#[test]
|
||||
fn falls_back_to_the_scan_without_an_opt_block() {
|
||||
let mut b = RATC_MAGIC.to_vec();
|
||||
b.extend_from_slice(&[0u8; 28]);
|
||||
b.extend_from_slice(b"plain.t32");
|
||||
b.extend_from_slice(&[0x0e, 0x10, 0xa4]);
|
||||
let off = b.len();
|
||||
b.extend_from_slice(b"T8aD");
|
||||
b.extend_from_slice(&[0u8; 16]);
|
||||
|
||||
let kids = parse(&b).expect("parse");
|
||||
assert_eq!(kids[0].name, "plain.t32");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_named_children() {
|
||||
let mut b = RATC_MAGIC.to_vec();
|
||||
|
||||
@@ -535,6 +535,28 @@ pub fn to_xma_riff_best(slb: &[u8]) -> Option<Vec<u8>> {
|
||||
(!data.is_empty()).then(|| build_riff(&synth_xma1_fmt(2, 2, 48000), data))
|
||||
}
|
||||
|
||||
/// Wrap a run of **raw XMA1 packets** as a standalone, decodable `RIFF/WAVE`.
|
||||
///
|
||||
/// For a bank with no internal delimiters — `Static.slb` is a packed run of whole
|
||||
/// 2048-byte packets with no `RIFF`, no `seek` and no `WAVE` — a wave is defined
|
||||
/// *only* by `(offset, packet count)`, both of which come from the running game
|
||||
/// (`--xma_param_probe`). There is nothing in the file to parse, so the header
|
||||
/// has to be synthesized, and that is the step worth doing exactly once, here,
|
||||
/// rather than in each consumer.
|
||||
///
|
||||
/// `packets` must be a whole number of [`XMA1_PACKET`] bytes; anything else is a
|
||||
/// short read and produces a stream the decoder will run off the end of.
|
||||
/// The `channel_mask` follows the same convention as the rest of this module:
|
||||
/// `1` for mono, `2` for stereo.
|
||||
///
|
||||
/// The three menu cues in `docs/re/menu-audio-cues.md` are
|
||||
/// `(0x1ec0, 4)` d-pad move, `(0x0ec0, 2)` Ⓑ back and `(0x5d6c0, 6)` Ⓐ confirm,
|
||||
/// all mono 48 kHz.
|
||||
pub fn xma1_wave_riff(packets: &[u8], channels: u8, rate: u32) -> Vec<u8> {
|
||||
let mask = if channels == 1 { 1 } else { 2 };
|
||||
build_riff(&synth_xma1_fmt(channels, mask, rate), packets)
|
||||
}
|
||||
|
||||
/// A minimal `fmt ` chunk carrying an XMA1 `XMAWAVEFORMAT` (one stream).
|
||||
fn synth_xma1_fmt(channels: u8, channel_mask: u16, rate: u32) -> Vec<u8> {
|
||||
let mut fmt = Vec::with_capacity(40);
|
||||
|
||||
@@ -322,6 +322,20 @@ pub struct UiBuild {
|
||||
pub elements: Vec<Element>,
|
||||
/// Sprite name → (offset, size) of its `T8aD` child within the bundle.
|
||||
pub sprites: HashMap<String, (usize, usize)>,
|
||||
/// Record name → (offset, size) of its nested `.rat` **leaf** within the
|
||||
/// bundle, e.g. `ptbtn01f.rat`.
|
||||
///
|
||||
/// Exposed because a leaf is where a focused button's extra elements live —
|
||||
/// `ptbtn0Nf.rat` declares the focus ring `ptbtneff01.t32` **and** the bright
|
||||
/// label, and the parent bundle declares no element for the `f` record at
|
||||
/// all. A consumer that walks only top-level elements cannot see either.
|
||||
///
|
||||
/// A leaf is itself readable by [`parse_build`]: its first 32 bytes have the
|
||||
/// same shape as a bundle header, so
|
||||
/// `parse_build(&bundle[off..off + size])` returns its elements with
|
||||
/// absolute design-space placements. See
|
||||
/// `docs/re/structures/ui-button-focus-record.md`.
|
||||
pub records: HashMap<String, (usize, usize)>,
|
||||
/// A guessed context from the sprite naming (e.g. `"tutorial"`), if any.
|
||||
pub context_hint: Option<String>,
|
||||
/// True when the declaration table was unusable and the build was recovered
|
||||
@@ -599,6 +613,7 @@ pub fn parse_build(bundle: &[u8]) -> Option<UiBuild> {
|
||||
design_h,
|
||||
elements,
|
||||
sprites,
|
||||
records,
|
||||
context_hint,
|
||||
from_fallback,
|
||||
placement_order,
|
||||
@@ -967,6 +982,13 @@ pub fn compose(
|
||||
continue;
|
||||
}
|
||||
let Some(sprite) = el.sprite.as_ref() else {
|
||||
// Report it, do not just skip it. This arm used to `continue`
|
||||
// silently while the two arms below recorded into `missing`, so when
|
||||
// a name-decoding defect left every menu screen's background
|
||||
// unresolved, `screen render` still said "sprites that did not
|
||||
// resolve: none". A diagnostic with a hole in it is worse than none.
|
||||
// See docs/re/structures/ratc-child-names.md.
|
||||
missing.push(format!("{} (element declares no resolvable sprite)", el.name));
|
||||
continue;
|
||||
};
|
||||
let Some(&(off, size)) = build.sprites.get(sprite) else {
|
||||
|
||||
@@ -120,3 +120,13 @@ turn ends — but sequential tool calls within a turn are fine.
|
||||
|
||||
The loop runs on a fixed interval set by the harness, so you do **not** need to
|
||||
arm the next wakeup yourself. Spend that attention on the write-up instead.
|
||||
|
||||
**And never stop it.** Do not call `ScheduleWakeup` at all — not to re-pace the
|
||||
loop, not to tidy up, and above all not with `stop`. Ending the loop ends the
|
||||
run: the container exits, and the next iteration never happens. If the cadence
|
||||
is genuinely wrong, say so in your reply and leave it to a human — the interval
|
||||
is set outside this prompt and is not yours to optimise.
|
||||
|
||||
This is not hypothetical. A run ended at 2026-08-29 04:0x with a clean exit 0
|
||||
while the display title read "Loop interval optimization", leaving four files of
|
||||
work uncommitted in the tree.
|
||||
|
||||
@@ -41,6 +41,76 @@ authored version can be deleted.
|
||||
|
||||
## Already settled — the port can rely on these today
|
||||
|
||||
### ⬅ Answers to the port's five asks (2026-08-29)
|
||||
|
||||
* **1 — how to recognise the splash.** ❔ **No content rule exists; you are
|
||||
authoring this.** Design size fails (every extra composable bundle sampled is
|
||||
1280×720, same as every screen) and element count fails (the fragments run
|
||||
2…15 elements, the splash halves have 3 and 7 — the ranges overlap).
|
||||
✅ **But `GP_TITLE` needs no rule.** There, `--all` adds exactly **four**
|
||||
bundles and all four are real screens — no fragments at all — and the `--all`
|
||||
index equals the pak **entry** index 1:1 across all 16, so addressing by entry
|
||||
index does not mean something different from elsewhere.
|
||||
🔴 **And there are TWO splash screens; you have one.** Entries **11/14** are the
|
||||
developer logos (GAME ARTS / SETA / studio anima). Entries **10/13** are the
|
||||
**SQUARE ENIX publisher** wordmark — the *first* thing the boot shows — and you
|
||||
do not have them. The pairs are region twins (`™` on 10, `®` on 13). All four
|
||||
draw every element they declare.
|
||||
[`ui-splash-addressing.md`](../re/ui-splash-addressing.md) ·
|
||||
[render grid](../re/captures/title-builds/splash-both-halves-rendered.png)
|
||||
|
||||
* **2 — the ~0.4 s fade-out is (a)**, and it is bigger than the fade quad.
|
||||
Every element of a screen ends on **exactly one** untimed keyframe — so there
|
||||
is one unknown duration per screen, which rules out (b). That final block is
|
||||
where the screen plays out: the quad goes `a=255` (black) while the buttons,
|
||||
`ptmsg` and the glows go `a=0` and the two frames hold. (c) is refuted by a
|
||||
null test on the capture: a black quad alone keeps the button÷background
|
||||
brightness **ratio constant**, and measured through the fade it falls
|
||||
**6.50 → 1.94, a 3.4× monotonic drop**. So: write one authored constant
|
||||
(~0.4 s / ~24 units) and **play the group to its end on every element** — do
|
||||
not fade a black rectangle over a frozen screen.
|
||||
[`screen-transitions.md`](../re/screen-transitions.md)
|
||||
|
||||
* **3 — focus: your choice is fine, and it is not your bug.** ✅ The focused
|
||||
sprite **completely covers** the base — `f` alpha ≥ base alpha at **100.0 %** of
|
||||
base-visible pixels on three pairs across both languages, once aligned properly
|
||||
(the true offset is **(7,7)**, and at the centre alignment it reads a
|
||||
misleading 78–84 %). Compositing both ways differs by **RMSE 1.1 inside the
|
||||
button rectangle**, max 12/255 on ~25 px — unmeasurable at frame level.
|
||||
🔴 **What you are actually missing is the focus record's SECOND element.**
|
||||
`ptbtn0Nf.rat` declares **two** sprites — `ptbtneff01.t32` (a 42×46 **glowing
|
||||
ring**, focus only) then `ptbtn0Nf.t32` (the bright label) — where the base
|
||||
record declares one. That ring is the marker you say you draw nowhere. ⚠️ Note
|
||||
the small dot-in-circle at each underline's left end is *not* it: that is on
|
||||
every button all the time, part of the base art.
|
||||
[`structures/ui-button-focus-record.md`](../re/structures/ui-button-focus-record.md)
|
||||
|
||||
* **4 — rotation: not mine to decide alone.** Raised with the human; see
|
||||
MISSION. What I can say without a decision: the two sub-questions are not
|
||||
equally open. Rotation is about the **declared pivot** — that anchor is
|
||||
*measured*, not assumed: the title's two `ptloop` sweeps scale 600 %/800 %
|
||||
vertically, where the pivot term is worth 450 and 630 px, and the GPU capture
|
||||
puts both quad centres at y **359.1**/**360.0** against the pivot formula's
|
||||
**360.0**; top-left anchoring predicts 810/990 and centre-as-position 270. So
|
||||
if you draw rotation, rotate about the declared pivot.
|
||||
⚠️ It changes nothing on your five screens **at rest** — they have zero
|
||||
top-level rotations, and the title's two nested ones sit entirely off-screen at
|
||||
rest. [`structures/ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||||
|
||||
* **5 — the capture is not gamma-neutral, and RMSE against it has a floor.**
|
||||
Measured on flat patches (16×16, both images `std < 8`):
|
||||
`capture ≈ 255·(render/255)^γ` with **γ ≈ 1.49** (main menu), **1.49**
|
||||
(`EXTRAS`), **1.34** (title). The chain says this is a ramp **the game
|
||||
installed**, not a capture-path artefact: canary's swap-path gamma stage is a
|
||||
pure 256-entry LUT that defaults to identity, and the game is measured calling
|
||||
`VdGetCurrentDisplayGamma` once at video init. ⚠️ **Reach: the flat patches are
|
||||
almost all dark (render ~0–60), so nothing here constrains midtones or
|
||||
highlights** — which is where γ 1.4 does its visible work. So: yes, there is a
|
||||
floor; a γ ≈ 1.4 darkening gets closer and is **authored**, best applied where
|
||||
it was measured rather than extrapolated. Do not chase RMSE below it.
|
||||
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||||
|
||||
|
||||
* **`GP_TITLE.pak` is eight screens, each shipped twice — English and Japanese.**
|
||||
Build 4 is the English title art and 7 its Japanese twin; **2/3 are the
|
||||
`PRESS Ⓐ BUTTON` plate, a build of their own** composited over the title and
|
||||
@@ -261,7 +331,37 @@ authored version can be deleted.
|
||||
setting rather than bake in.
|
||||
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||||
|
||||
* 🟡 **`screen render` silently drops one full-screen element per screen — and
|
||||
* ✅ **FIXED 2026-08-29 — the dropped background was a name-decoding defect, and
|
||||
`screen render` now draws it.** The reference composites for all five screens
|
||||
changed; regenerate anything you diffed against before that date. Cause: a
|
||||
RATC child's name is stated by an **`opt ` block** immediately before it
|
||||
(`"opt " | BE32 len | name | NUL | 3 bytes | magic`), and our parser instead
|
||||
guessed it from the last printable run of bytes. For this one child the 3
|
||||
trailing bytes are `38 41 58` = `"8AX"`, which beat the real name
|
||||
`pteff05.t32`. **`8AX` was never a name** — earlier text on this page treating
|
||||
it as one was wrong. Disc-wide: 18 002 children, 17 918 already agreed with the
|
||||
`opt ` reading and **24 did not**, every one the same 3-byte-tail failure.
|
||||
Effect on your five screens: mean brightness unmoved, high-frequency detail
|
||||
**×1.15…×1.30** — the same art at twice the resolution, which is exactly what
|
||||
[8AX](../re/structures/ui-8ax-fullres-background.md) said the game draws.
|
||||
⚠️ Both backgrounds are now drawn (`ptbase` upscaled, then the full-res one over
|
||||
it): correct, but wasted fill. Draw only the full-res one, taking its *timing*
|
||||
from `ptbase`'s element, which carries the keyframes.
|
||||
[`structures/ratc-child-names.md`](../re/structures/ratc-child-names.md)
|
||||
✅ **And the fix has no remaining hole.** 60 of the disc's 18 002 RATC children
|
||||
still have no `opt ` block; all 60 are now accounted for and **none is on your
|
||||
screens**. They are the ten frames of the disc's only `.tan` **frame sequence**
|
||||
(`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle), where
|
||||
a single `opt ` block names the whole run — so a name-resolution miss is not
|
||||
hiding anything else the way `8AX` was. ⚠️ Two notes if you ever read outside
|
||||
`GP_TITLE`: `ratc::parse` **over-reports** there, listing a `.tan`'s frames as
|
||||
anonymous children; and a RATC bundle names exactly six kinds of resource —
|
||||
`.t32` (14 756), `.rat` (3 311), `.prm` (367), `.tbm` (224), `.sbo` (54),
|
||||
`.tan` (6).
|
||||
[`structures/ratc-tan-frame-sequence.md`](../re/structures/ratc-tan-frame-sequence.md)
|
||||
<details><summary>the original entry, kept because its reasoning still stands</summary>
|
||||
|
||||
🟡 **`screen render` silently drops one full-screen element per screen — and
|
||||
you must NOT simply draw it.** Auditing what the composer omits on your five
|
||||
screens: everything is accounted for (`kind & 0x4` ghost instances, `.prm`
|
||||
primitives, `loop*` animations) except **`pteff04.t32`** on the title and
|
||||
@@ -287,9 +387,11 @@ authored version can be deleted.
|
||||
full-screen layer over an identical one costs fill and hides later changes; and
|
||||
note `ptbase`'s element is the one carrying the keyframes, so you need its
|
||||
timing with `8AX`'s pixels.
|
||||
⚠️ It does not show whether `ptbase` is *also* drawn underneath — `8AX` is
|
||||
⚠️ It does not show whether `ptbase` is *also* drawn underneath — the full-res
|
||||
background is
|
||||
~86 % opaque and would hide it either way.
|
||||
[`structures/ui-8ax-fullres-background.md`](../re/structures/ui-8ax-fullres-background.md)
|
||||
</details>
|
||||
|
||||
* ✅ **Paint order: your exposure is two element pairs, on one screen.** We use
|
||||
an order *measured from the running game* where one exists and a derived order
|
||||
|
||||
@@ -208,6 +208,28 @@ settled and only multi-keyframe absolute timing is open; `rest()` differs from
|
||||
its alternative on **one** element across all five screens, and the current
|
||||
answer there is the defensible one.
|
||||
|
||||
## 🔵 Needs a human decision — rotation (raised 2026-08-29)
|
||||
|
||||
The port agent asks whether it should **render** `rotation_deg` (decoded at
|
||||
keyframe `+12`) when `sylpheed-cli screen render` deliberately does not. Its own
|
||||
framing is the reason this is not mine to settle: if the port rotates and the
|
||||
reference renderer does not, then `verify-screen` reports a large title diff that
|
||||
means *"the port is right"* — a silently inverted signal.
|
||||
|
||||
The RE half is answered and is in HANDOFF: rotation is about the **declared
|
||||
pivot** (measured against a GPU capture, not assumed), and it changes nothing on
|
||||
the five screens **at rest**.
|
||||
|
||||
What needs a decision is which way the divergence gets closed:
|
||||
|
||||
* teach `ui_layout::blit` a rotating path, so the two renderers stay comparable
|
||||
and the diff keeps meaning "someone is wrong" — costs work in the reference
|
||||
renderer, which is otherwise not on the port's critical path; or
|
||||
* let the port render rotation and mark the title as a known-divergent screen in
|
||||
`verify-screen`, accepting a check that no longer guards the title.
|
||||
|
||||
Recorded rather than chosen, per "do not improvise around a blocker".
|
||||
|
||||
## Known unknowns — say so, do not fill them in
|
||||
|
||||
Some of these may turn out to be undecodable. That is a valid, useful answer, and
|
||||
|
||||
@@ -21,7 +21,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
||||
| IDXD nameless field keys | ✅/❌ | [idxd-unnamed-keys](structures/idxd-unnamed-keys.md) + [`tools/re-capture/idxd_unnamed_keys.py`](../../tools/re-capture/idxd_unnamed_keys.py) | Census of every field entry whose `name_off` is `0xFFFFFFFF`, disc-wide: **7 750 objects, 2 757 039 field entries, 0 parse failures**, `tag_hash` reproducing **1 271 462/1 271 462** named keys. **7 094 distinct keys are never named — and 7 052 of them are not hashes at all**, but author-assigned element ids (equal to the field's own index in 1 404 924 of 1 485 577 cases; `tag_hash("BGM_001")` is `0xC662435B` while the key valued `BGM_001.slb` is `0x000003E9`). ⚠️ **The "504 hash-keyed nameless fields" figure is 504 ENTRIES, not 504 names** — 42 distinct keys × 6 language copies × 2 records. All 42 are **ISL script-symbol hashes** in `<lang>\script\ID.tbl` (GP_READY_ROOM.pak), the link map built by `PrepareScript`'s "isl script prescanning"; 41 of 42 appear as little-endian call targets inside the `.isb` bytecode, forming a coherent launcher/helper call graph. The hash's own algebra pins the **trailing digits of 30 of the 42 names** (deltas of exactly `+0x01000001` across `stage01..09`, `stage10..16`, `challenge01..06`; `+0x01010000` across `tutorial0101..0601`). ❌ **No name was cracked, and the negative is quantified**: seven attacks up to a 3.5×10⁸ composition space found nothing above the noise floor; exhaustive preimage search recovers `"Stage01"` from its own hash but returns nothing for the real targets at ≤6 characters, and at 7 characters one target already has **1 176** preimages — a 24-bit modulus cannot name an 8+ character identifier uniquely |
|
||||
| XPR2 texture + cubemap | 🟡/✅ | `sylpheed-formats/src/texture.rs` + [colour check](xpr2-colour-check.md) | de-tile + A8R8G8B8 and DXT1. **Channel order ✅ confirmed against the running game**: the Delta Saber's decoded atlas is orange-dominant (median saturated hue 23.3°, *zero* cool pixels) and the game renders the same hull at 9.3° — a red↔blue swap would sit at ≈200°. Exact fidelity (gamma/sRGB curve, premultiplied alpha, per-channel scale) is 🟡 untested, since a hue comparison cannot see it; cubemap face ordering ❔. **2026-08-29, for the UI path only:** our composite is brighter than the emulator's frame by a gamma of **≈1.34–1.49** across three screens ([tone curve](structures/ui-render-tone-curve.md)) — 🟡 measured, not decoded, constrained only over render values ~0–60, and possibly canary's own `kernel_display_gamma_type = 2` (BT.709) output stage rather than the game's |
|
||||
| T8aD 2D texture | ✅ | `sylpheed-formats/src/t8ad.rs` | **100 % of the disc decodes** (19 216/19 216, measured). The "~15 % deferred variants" were a wrong model, not a variant: a surface is a list of **arbitrary sub-rectangles**, each with a 16-byte header of `dst X, dst Y, width, height`, not a 256×256 grid — `0x1c` is the **rectangle count**. Uncovered area stays transparent. **Colours ✅ CONFIRMED** ([k8888](structures/texture-color-k8888.md)) |
|
||||
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **10 144 of 10 148 references resolve**. The 4 that do not are one dangling asset — `pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, and `pmbase.t32` is **on the disc nowhere** |
|
||||
| RATC bundle | ✅ | `sylpheed-formats/src/ratc.rs` | **✅ 2026-08-29: a child's NAME is stated by an `opt ` block** (`"opt " | BE32 len | name | NUL | 3 bytes | magic`), not by the printable bytes before it. We had been guessing it from the last printable run, which is right 17 918 times and **wrong 24**, each time because the 3-byte tail is itself printable — `8AX` (×22) and `'OX` (×2). `8AX` is **not a name**; it hid `pteff05.t32`/`pteff04.t32`, the full-resolution background of all five menu screens, which `compose` then dropped with no diagnostic ([ratc-child-names](structures/ratc-child-names.md)). ✅ 60 of 18 002 children carry no `opt ` block, and all 60 are explained: they are the ten frames of the disc's only `.tan` **frame sequence** (`pb_f15_eg_anm.tan`, six language copies of one `GP_READY_ROOM` bundle, 6 × 10 = 60), where one `opt ` block names the whole run — so `parse` over-reports frames as children there. A census of all **18 718** `opt ` names shows a bundle names exactly six kinds of resource: `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6 ([tan-frame-sequence](structures/ratc-tan-frame-sequence.md)). Child listing confirmed. **"One level deep" is not a limitation — there is nothing deeper**: 2 859 bundles hold 18 002 children at depth 1 and **0 at depth 2**, with no parse failures. Nested RATC blobs are **leaf records that reference siblings by name** (`opt `, the sprite name): 3 311 leaves, all embedding sibling names, **✅ 10 148 of 10 148 references resolve, as of 2026-08-29.** The 4 that did not were `pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`'s four language builds, recorded as "`pmbase.t32` is on the disc nowhere". It was on the disc all along, as the child the printable-run scan named `8AX` — the `opt ` name fix above resolves it in all four builds (3 686 767 B each). A dangling reference that closes itself when an unrelated decode lands is the corroboration that decode wanted |
|
||||
| LSTA sprite list | ✅ | `sylpheed-formats/src/lsta.rs` | A display list of inline elements: **T8aD sprites and `PRMD` primitives**. The `count` at `0x04` is **exact and counts both** — `count == T8aD + PRMD` for **64/64** lists on the disc, which retires the old "a few entries disagree" note (it compared sprites against a total including primitives). **All 1 281 sprite frames decode** after the T8aD rectangle-list fix |
|
||||
| IXUD subtitle + caption text | ✅ | `sylpheed-formats/src/ixud.rs` + `movie_subtitle.rs` ([container](structures/idxd-container.md) · [movie link](movie-subtitle-link.md)) | **The IXUD record/field table is decoded and wired in (2026-08-26)** — `IxudObject` mirrors `IdxdObject`; uniform 16-byte records, 12-byte fields, every offset in **chars**, and the word at `0x08` is record 0's hash, not a schema id. Verified disc-wide: **1104/1104** objects, **1476/1476** records, **628 165/628 165** named fields reproducing their `ixud_hash` (`tests/ixud_records_disc.rs`). **Caption text: 537 → 8800 lines, which is 8800 of 8800 distinct keys.** Two steps — generalising the key parser from `MSG_DEMO_*` to all **eight** families (`ACRO ADAN ADPL BIRD DEMO RHIN TCAF` use `MSG_<FAM>_<id>_<page>_<line>`, `VOICE` alone inserts a family letter) took 537 → 8074; switching from **token adjacency to record fields** took it to 8800. ⚠️ An earlier "1.3 % of the game's text" figure of mine counted *occurrences across blocks* — the honest denominator is **8800 distinct keys**, so the real starting point was 6.1 %. The `DEMO` control shows why the field route matters: token adjacency finds 537 lines there, fields find **541** — it was dropping lines in the one family it was written for. | timed cues. **The movie↔subtitle↔voice link is solved — statically**, and as of 2026-08-25 read from the IDXD **record table** rather than scraped from the string pool: **104 cutscene slots binding 101 distinct movies**, 99 slots / 96 movies with a subtitle, 99 / 96 with a voice track, 22 / 22 with a telop. ⚠️ The previous counts (94 / 83 / 21) were the numbers of **distinct pool strings** — a repeat reference contributes no token, so 13 later `VOICE_D_450..454` references read as "no binding". **All 18 hokyu movies are bound**, not five. 93 of the 94 distinct subtitle members resolve; `SUBTITLE_S12B.tbl` resolves in none of the six languages — a dangling reference on the disc. The ~104 **script ids are no longer ❔**: they are literal positional field keys in `BASE_INFO`, each naming its record, and all 104 resolve. `movie_manifest::parse` now reads the record table; CSV regenerated by `examples/movie_map_csv.rs` |
|
||||
| Fonts (ttf/otf/ttc) | ✅ | `sylpheed-formats/src/font.rs` | standard OpenType, parsed via ttf-parser |
|
||||
|
||||
@@ -838,3 +838,45 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
||||
"measured" covers several kinds of evidence, and the page that recorded it
|
||||
usually says which — `ui-screen-runtime.md` said "child slots" in as many
|
||||
words. Read that line before building an argument on top of it.
|
||||
|
||||
* **A heuristic that is right 99.9 % of the time still has a shape to its
|
||||
failures — find it before trusting the field.** RATC child names were read by
|
||||
scanning backwards for the last printable run of bytes. That agrees with the
|
||||
format's own `opt ` declaration on 17 918 of 17 942 children, which is the kind
|
||||
of agreement that stops people looking. The 24 exceptions were not random: all
|
||||
24 are the *same* case, a 3-byte binary tail that happens to be printable ASCII
|
||||
(`8AX`), and one of them was the full-resolution background of every menu
|
||||
screen we care about. Ask what the format *states* before settling for what a
|
||||
scan *infers*, especially when the stated version is already decoded elsewhere
|
||||
in the same file — `opt ` was being read for button focus links the whole time.
|
||||
|
||||
* **A `continue` that silently skips is a defect even when the skip is correct.**
|
||||
`compose` drops an element whose sprite does not resolve. Two arms above it
|
||||
record the name into `missing` first; the `el.sprite.is_none()` arm does not.
|
||||
So a screen lost its background and `screen render` still reported "sprites
|
||||
that did not resolve: none" — the diagnostic was structurally unable to see it.
|
||||
When adding an early-out to a loop that already reports what it discards, make
|
||||
it report through the same channel, or it becomes a place findings go to die.
|
||||
|
||||
* **"It has no name" can mean "it is not a thing that gets named."** Sixty RATC
|
||||
children had no `opt ` name block and the open question was whether the block
|
||||
was absent or merely outside our search window. It was neither: the sixty are
|
||||
*frames*, ten each of six copies of one `.tan` animation, and one `opt ` block
|
||||
names the whole run. The give-away was in the data before any hypothesis was —
|
||||
the distances back to the nearest tag were an exact arithmetic progression
|
||||
(`213 + n·60600`), i.e. ten different records finding the *same* tag. When a
|
||||
negative result's measurements come out evenly spaced, the thing you are
|
||||
counting is probably not the thing the format counts.
|
||||
|
||||
* **This container OOM-kills `slb_leading_segment_disc` under default test
|
||||
parallelism.** It dies with `signal: 9, SIGKILL` and no assertion — eight
|
||||
threads each holding a slice of a ~1.1 GB bank. It is not a regression and not
|
||||
a flake, and it reproduces when run alone. `-- --test-threads=1` passes 8/8 in
|
||||
20 s. Before believing a SIGKILL in this repo, re-run the suite serially;
|
||||
before believing a *pass*, check nothing else heavy was sharing the box.
|
||||
⚠️ And budget for `mesh_consistency_disc`: it takes **22 minutes** (1 318 s
|
||||
measured, serial) and produces no output while it runs, so `build-reborn test`
|
||||
looks hung for a third of an hour. It is not. Two runs were killed for looking
|
||||
stuck before it was timed. If the change under test is not in the mesh path,
|
||||
`build-reborn t -p <crate>` scopes around it — note that `build-reborn test`
|
||||
itself passes `--workspace` and will ignore a `-p`.
|
||||
|
||||
@@ -605,3 +605,15 @@ neighbourhood, not just the line.
|
||||
them regardless. The no-overlap measurement was correct; the inference from it
|
||||
was not. What survives: a capture of this screen can only cross-check the order
|
||||
*within* each half. [`ui-prm-primitives.md`](structures/ui-prm-primitives.md)
|
||||
|
||||
* "`8AX` is the name a `T8aD` is registered under" → **it is not a name at all.**
|
||||
It is three bytes of the preceding record's payload (`38 41 58`) that happen to
|
||||
be printable ASCII, which our backwards printable-run scan preferred over the
|
||||
name the format actually states in its `opt ` block. The claim sat in
|
||||
`HANDOFF.md` and `ui-8ax-fullres-background.md` as though `8AX` were a real
|
||||
identifier, and cost every menu screen its full-resolution background.
|
||||
[`ratc-child-names.md`](structures/ratc-child-names.md)
|
||||
* "`pmbase.t32` is on the disc nowhere" (the one dangling asset behind
|
||||
`10 144 of 10 148 references resolve`) → **withdrawn; it is on the disc.** It is
|
||||
the `GP_STAGE_CLEAR` child the same scan named `8AX`. With the name decoded the
|
||||
count is **10 148 of 10 148**. [`ratc-child-names.md`](structures/ratc-child-names.md)
|
||||
|
||||
53
docs/re/captures/ORACLE-CAPTURES.md
Normal file
53
docs/re/captures/ORACLE-CAPTURES.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# The oracle frames — what to verify a render against
|
||||
|
||||
**These are framebuffer captures of the real game running under Xenia Canary.**
|
||||
They are the reference. `sylpheed-cli screen render` is **not** — Reborn is a GUI
|
||||
explorer and extraction CLI built to check that our *decoding* is right, and it
|
||||
may very well be wrong. Where a render and a capture disagree, the capture wins,
|
||||
and the render is the thing to go and fix.
|
||||
|
||||
⚠️ Two renderers agreeing proves nothing: they share our assumptions. This corpus
|
||||
has been bitten by exactly that three times — the dropped `pteff05` background,
|
||||
the scale-0 rect, and `rest()`. Each was invisible to any render-vs-render diff
|
||||
and visible immediately against a capture.
|
||||
|
||||
## The frames
|
||||
|
||||
All are **1279×675**, top-left aligned, cropped to the game surface by the
|
||||
screenshot tool (the guest renders 1280×720; the missing row/column is the crop,
|
||||
not a scale).
|
||||
|
||||
| screen | capture |
|
||||
|---|---|
|
||||
| publisher splash (SQUARE ENIX) | [`title-builds/live-splash-publisher.png`](title-builds/live-splash-publisher.png) |
|
||||
| developer splash (GAME ARTS / SETA / anima) | [`title-builds/live-splash-developer.png`](title-builds/live-splash-developer.png) |
|
||||
| title, **without** the `PRESS Ⓐ` plate | [`title-builds/live-title-build4-no-plate.png`](title-builds/live-title-build4-no-plate.png) |
|
||||
| title, **with** the plate | [`title-builds/live-title-press-a.png`](title-builds/live-title-press-a.png) |
|
||||
| main menu | [`title-builds/live-main-menu.png`](title-builds/live-main-menu.png) · [`main-menu-oracle.png`](main-menu-oracle.png) |
|
||||
| main menu, **`OPTIONS` focused** | [`title-builds/live-main-menu-options-focused.png`](title-builds/live-main-menu-options-focused.png) |
|
||||
| `EXTRAS` | [`title-builds/live-extras.png`](title-builds/live-extras.png) |
|
||||
| title (alternate) | [`title-screen-oracle.png`](title-screen-oracle.png) |
|
||||
| a screen transition, 13 frames | [`transitions/transition-filmstrip.png`](transitions/transition-filmstrip.png) + [`transition-luminance.csv`](transitions/transition-luminance.csv) |
|
||||
|
||||
The **focused** pair is the useful one for button states: the same screen with a
|
||||
different button highlighted, so the difference isolates what focus changes.
|
||||
|
||||
## ⚠️ Before you compute an RMSE against one
|
||||
|
||||
* **They are not gamma-neutral.** `capture ≈ 255·(render/255)^γ` with γ ≈ 1.34–1.49,
|
||||
and that is a ramp **the game installed**, not a capture-path artefact. So RMSE
|
||||
against these has a floor and chasing it below that floor is chasing the ramp.
|
||||
[`../structures/ui-render-tone-curve.md`](../structures/ui-render-tone-curve.md)
|
||||
* **Geometry is sound**: cross-correlating a render against `live-main-menu.png`
|
||||
over ±6 px puts the best alignment at exactly (0,0), correlation 0.9466. So a
|
||||
positional disagreement is real, not a crop artefact.
|
||||
* **A capture is one moment.** Several of these screens are still animating; the
|
||||
title's two `ptloop` sweeps move continuously. Compare settled poses, or
|
||||
compare regions you know are at rest.
|
||||
|
||||
## What is NOT here
|
||||
|
||||
No capture of the interactive title reached mid-run without a pad press — three
|
||||
runs across two locales and two launch paths never reached it in ~35 minutes.
|
||||
See [`../capture-harness-status.md`](../capture-harness-status.md). And no
|
||||
`GP_READY_ROOM` capture; S1 ruled it out of scope.
|
||||
BIN
docs/re/captures/title-builds/splash-both-halves-rendered.png
Normal file
BIN
docs/re/captures/title-builds/splash-both-halves-rendered.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/re/captures/ui-layout/button-base-focus-ring-sprites.png
Normal file
BIN
docs/re/captures/ui-layout/button-base-focus-ring-sprites.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/re/captures/ui-layout/focus-ring-oracle-vs-sprite.png
Normal file
BIN
docs/re/captures/ui-layout/focus-ring-oracle-vs-sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
52
docs/re/data/ratc-child-name-audit.txt
Normal file
52
docs/re/data/ratc-child-name-audit.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
# RATC child names: the printable-run scan vs the `opt ` block, all 33 dat/*.pak
|
||||
# 2026-08-29, crates/sylpheed-formats/examples/ratc_child_names.rs
|
||||
#
|
||||
# RUN 1 was taken BEFORE the fix, when ratc::parse still named children by
|
||||
# scanning for the last printable run. It is the evidence for the defect.
|
||||
|
||||
RATC children scanned : 18002
|
||||
with an `opt ` block: 17942
|
||||
scanned name AGREES : 17918
|
||||
scanned name DIFFERS : 24
|
||||
|
||||
distinct disagreements (scanned -> opt), with counts:
|
||||
'OX -> po_keys_win1.t32 x2
|
||||
8AX -> pbbg.t32 x12
|
||||
8AX -> pmbase.t32 x4
|
||||
8AX -> pteff04.t32 x2
|
||||
8AX -> pteff05.t32 x4
|
||||
|
||||
first 20 occurrences:
|
||||
GP_OPTIONS.pak entry 20 'OX -> po_keys_win1.t32
|
||||
GP_OPTIONS.pak entry 22 'OX -> po_keys_win1.t32
|
||||
GP_READY_ROOM.pak entry 132 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 141 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 211 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 219 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 233 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 234 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 264 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 265 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 967 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 999 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 1097 8AX -> pbbg.t32
|
||||
GP_READY_ROOM.pak entry 1099 8AX -> pbbg.t32
|
||||
GP_STAGE_CLEAR.pak entry 2 8AX -> pmbase.t32
|
||||
GP_STAGE_CLEAR.pak entry 4 8AX -> pmbase.t32
|
||||
GP_STAGE_CLEAR.pak entry 7 8AX -> pmbase.t32
|
||||
GP_STAGE_CLEAR.pak entry 8 8AX -> pmbase.t32
|
||||
GP_TITLE.pak entry 4 8AX -> pteff04.t32
|
||||
GP_TITLE.pak entry 5 8AX -> pteff05.t32
|
||||
|
||||
# RUN 2, same command AFTER the fix. ratc::parse now prefers the `opt ` name, so
|
||||
# the two readings must agree everywhere -- which is the check that the fix is
|
||||
# complete rather than partial.
|
||||
|
||||
RATC children scanned : 18002
|
||||
with an `opt ` block: 17942
|
||||
scanned name AGREES : 17942
|
||||
scanned name DIFFERS : 0
|
||||
|
||||
distinct disagreements (scanned -> opt), with counts:
|
||||
|
||||
first 20 occurrences:
|
||||
26
docs/re/data/ratc-name-fix-render-effect.txt
Normal file
26
docs/re/data/ratc-name-fix-render-effect.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
# Effect on the five port screens of naming RATC children from the `opt `
|
||||
# block. 2026-08-29. `sylpheed-cli screen render --build N GP_TITLE.pak`,
|
||||
# before vs after the ratc.rs change. The newly-resolved element is the
|
||||
# full-resolution background (pteff04.t32 on the title, pteff05.t32 on the
|
||||
# menus), which the old name `8AX` hid.
|
||||
|
||||
build screen mean brightness high-frequency detail
|
||||
4 title 72.77 -> 72.81 8.722 -> 10.062 x1.15 pixels changed >2: 30.0%
|
||||
5 main menu (JP) 42.73 -> 42.74 3.978 -> 5.051 x1.27 pixels changed >2: 20.0%
|
||||
6 EXTRAS (JP) 44.08 -> 44.09 3.953 -> 5.025 x1.27 pixels changed >2: 19.4%
|
||||
8 main menu 41.57 -> 41.58 3.830 -> 4.970 x1.30 pixels changed >2: 21.0%
|
||||
9 EXTRAS 42.99 -> 43.00 3.867 -> 5.000 x1.29 pixels changed >2: 20.2%
|
||||
|
||||
# Brightness unmoved, detail up ~a quarter = the same artwork at twice the
|
||||
# resolution replacing a 2x upscale. New CONTENT would move the mean.
|
||||
|
||||
# Covering check: the background is opaque and full-screen and paints 4th
|
||||
# of 16 on the menu, so it could hide what follows. Standard deviation
|
||||
# inside each element's resting rect (build 8), before vs after:
|
||||
ptbtn01 sd 59.98 -> 60.03
|
||||
ptbtn03 sd 63.40 -> 63.45
|
||||
ptbtn05 sd 66.54 -> 66.58
|
||||
ptframe1 sd 51.16 -> 51.20
|
||||
ptmsg sd 56.11 -> 56.12
|
||||
loop-area sd 50.99 -> 51.04
|
||||
# Nothing is covered.
|
||||
58
docs/re/data/ratc-tan-frame-sequence.txt
Normal file
58
docs/re/data/ratc-tan-frame-sequence.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
# The 60 RATC children with no `opt ` block — probe output
|
||||
|
||||
```
|
||||
$ cargo run -p sylpheed-formats --example ratc_optless_children -- $SYLPHEED_DISC/dat/*.pak
|
||||
|
||||
RATC children scanned : 18002
|
||||
with NO accepted `opt ` block: 60
|
||||
... of which are child #0 : 0
|
||||
|
||||
why, by cause:
|
||||
TagBeyondWindow x60
|
||||
|
||||
every occurrence (name is what the FALLBACK scan returned):
|
||||
GP_READY_ROOM.pak entry 26 child 1 TagBeyondWindow(213)
|
||||
24 bytes before the magic: 00 00 00 07 00 00 00 28 00 00 00 08 00 00 00 28 00 00 00 09 00 00 00 28
|
||||
GP_READY_ROOM.pak entry 26 child 2 TagBeyondWindow(60813)
|
||||
24 bytes before the magic: 0a eb 61 00 08 eb 61 00 06 eb 61 00 05 eb 61 00 03 eb 61 00 01 eb 61 00
|
||||
... (the remaining 54 rows are the same five bundles' children 1..10;
|
||||
every distance is 213 + n*60600, i.e. the SAME tag)
|
||||
```
|
||||
|
||||
## Reading bundle 26 directly — the ten are frames of one `.tan`
|
||||
|
||||
```
|
||||
entry 26: 15 children, payload 706609 bytes
|
||||
0 @0x00000444 T8aD size 1933 opt@-40 name='pbf15_energie_generator2.t32'
|
||||
1 @0x00000bd1 T8aD size 60600 opt@-213 name='pb_f15_eg_anm.tan'
|
||||
2 @0x0000f889 T8aD size 60600 opt@-60813 name='pb_f15_eg_anm.tan'
|
||||
3 @0x0001e541 T8aD size 60600 opt@-121413 name='pb_f15_eg_anm.tan'
|
||||
4 @0x0002d1f9 T8aD size 60600 opt@-182013 name='pb_f15_eg_anm.tan'
|
||||
5 @0x0003beb1 T8aD size 60600 opt@-242613 name='pb_f15_eg_anm.tan'
|
||||
6 @0x0004ab69 T8aD size 60600 opt@-303213 name='pb_f15_eg_anm.tan'
|
||||
7 @0x00059821 T8aD size 60600 opt@-363813 name='pb_f15_eg_anm.tan'
|
||||
8 @0x000684d9 T8aD size 60600 opt@-424413 name='pb_f15_eg_anm.tan'
|
||||
9 @0x00077191 T8aD size 60600 opt@-485013 name='pb_f15_eg_anm.tan'
|
||||
10 @0x00085e49 T8aD size 60632 opt@-545613 name='pb_f15_eg_anm.tan'
|
||||
11 @0x00094b21 T8aD size 17523 opt@-32 name='pbf15_pd_inside2.t32'
|
||||
12 @0x00098f94 T8aD size 75068 opt@-35 name='pbenergie_generator.t32'
|
||||
13 @0x000ab4d0 T8aD size 4697 opt@-28 name='pbf15_eg_eff.t32'
|
||||
14 @0x000ac729 RATC size 264 opt@-25 name='pb_s15_eg.rat'
|
||||
```
|
||||
|
||||
## Disc-wide: every `opt ` name, by extension
|
||||
|
||||
```
|
||||
`opt ` blocks disc-wide: 18718
|
||||
|
||||
by extension:
|
||||
.t32 x14756
|
||||
.rat x3311
|
||||
.prm x367
|
||||
.tbm x224
|
||||
.sbo x54
|
||||
.tan x6
|
||||
|
||||
.tan resources with >=1 child: 6
|
||||
GP_READY_ROOM.pak pb_f15_eg_anm.tan frames= 10 sizes=[60600, 60632] x6 bundles
|
||||
```
|
||||
17
docs/re/data/se-wave-riff-decode.txt
Normal file
17
docs/re/data/se-wave-riff-decode.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
# se_wave_riff — the three menu cues, decoded end to end
|
||||
|
||||
$ cargo run -p sylpheed-formats --example se_wave_dump -- /tmp/se
|
||||
|
||||
/tmp/se/move.riff: 8252 bytes (4 packets at 0x1ec0)
|
||||
/tmp/se/back.riff: 4156 bytes (2 packets at 0xec0)
|
||||
/tmp/se/confirm.riff: 12348 bytes (6 packets at 0x5d6c0)
|
||||
refusal path ok: Static.slb: 1048576 packets at 0x1ec0 need 2147483648 bytes, but the bank declares only 8970240 bytes
|
||||
|
||||
$ ffmpeg -i <cue>.riff <cue>.wav # then measure the PCM
|
||||
|
||||
move 48000 Hz mono 0.533 s rms 2084.7 peak 29813 non-quiet 47.5%
|
||||
back 48000 Hz mono 0.344 s rms 2984.7 peak 16973 non-quiet 95.1%
|
||||
confirm 48000 Hz mono 1.016 s rms 4327.0 peak 32767 non-quiet 92.5%
|
||||
Non-silent, plausible envelopes, durations consistent with a UI blip.
|
||||
The refusal path is exercised in the same run: an impossible packet count is
|
||||
rejected rather than returning a short stream.
|
||||
@@ -115,3 +115,56 @@ screen's build-in is a separate, longer thing.
|
||||
quad);
|
||||
* the ~0.4 s fade-out and the black hold are **authored from this page** — the
|
||||
disc does not carry them.
|
||||
|
||||
## ✅ Which quantity the ~0.4 s is — measured 2026-08-29
|
||||
|
||||
Asked by the port: is 0.4 s **(a)** the ramp from the hold to the exit pose, i.e.
|
||||
exactly the missing duration of that untimed keyframe, **(b)** several keyframes'
|
||||
worth, or **(c)** something the game does independently of the group?
|
||||
|
||||
**It is (a)** — and it is bigger than the fade quad. Two facts.
|
||||
|
||||
**1. There is exactly one untimed keyframe, and every element has it.** Reading
|
||||
`screen info --build 5 --geometry` for the main menu, all 16 elements end on a
|
||||
single timeless block; none has two. So there is one unknown duration per screen,
|
||||
not a chain of them — which rules out (b) outright. And that final block is not
|
||||
idle: it is where the screen *plays out*.
|
||||
|
||||
| elements | final untimed block | what it does |
|
||||
|---|---|---|
|
||||
| `pteff00.prm` (the fade quad) | `a = 255` | goes **black** |
|
||||
| `pteff10`, `pteff12`, `ptbtn01…05`, `ptmsg` | `a = 0` | **fade out** |
|
||||
| `ptframe1`, `ptframe2` | `a = 255` | hold, and get covered |
|
||||
| `ptbase`, `pteff05`, `ptloop*`, `pteff02.prm` | single keyframe | hold |
|
||||
|
||||
**2. The capture shows the content fading, not just a black quad arriving.** This
|
||||
has a null hypothesis that discriminates: under (c) — the game blackens the frame
|
||||
independently — every region is scaled by the same `1 − α`, so the **ratio**
|
||||
between a button region and a background region is *constant* through the
|
||||
fade-out. Under (a) it must fall, because the buttons ramp to `a = 0` while the
|
||||
background elements hold at 255 and are only dimmed.
|
||||
|
||||
Measured on [`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png),
|
||||
button column ÷ upper-right background art, frame by frame through the fade-out:
|
||||
|
||||
```
|
||||
frame 0 1 2 3 4 5
|
||||
ratio 6.495 5.574 3.105 2.125 1.935 (black)
|
||||
```
|
||||
|
||||
**A 3.4× monotonic fall.** Constant is refuted. The buttons really are fading
|
||||
independently of the overall dim, exactly as their declared final block says.
|
||||
(The incoming screen runs it in reverse, 2.22 → 3.47 over frames 7–12.)
|
||||
|
||||
⚠️ **Reach.** The filmstrip is downsampled and the "button" region unavoidably
|
||||
contains some background, so the ratio is a direction, not a clean alpha
|
||||
measurement. It refutes the constant-ratio null decisively; it does not by itself
|
||||
pin the 0.4 s to ±0.05 s. And it is measured on **one** transition pair.
|
||||
|
||||
### For the port
|
||||
|
||||
Write **one** authored constant — the duration of the final untimed keyframe,
|
||||
~0.4 s / ~24 units — and **play the group to its end on every element**. Do not
|
||||
model the exit as a black rectangle fading over a frozen screen: the buttons and
|
||||
labels ramp to transparent at the same time, and that difference is visible.
|
||||
|
||||
|
||||
141
docs/re/structures/ratc-child-names.md
Normal file
141
docs/re/structures/ratc-child-names.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# A RATC child's name is stated by an `opt ` block, not by the bytes before it
|
||||
|
||||
**Status:** ✅ `DECODED`, with a disc-wide check. This fixes a decoder defect that
|
||||
silently dropped the **full-resolution background from all five menu screens**.
|
||||
|
||||
## The field
|
||||
|
||||
Immediately before nearly every child of a `RATC` bundle — 17 942 of the disc's
|
||||
18 002 — sits an `opt ` block:
|
||||
|
||||
```text
|
||||
"opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
||||
```
|
||||
|
||||
Two children of the main menu bundle (`GP_TITLE.pak` entry 8), raw:
|
||||
|
||||
```text
|
||||
opt 00 00 00 0a p t b a s e . t 3 2 00 0e 10 a4 T8aD
|
||||
opt 00 00 00 0b p t e f f 0 5 . t 3 2 00 38 41 58 T8aD
|
||||
^^^^^^^^
|
||||
"8AX"
|
||||
```
|
||||
|
||||
This is the **same `opt ` block** `ui_layout::opt_link` already decodes for a
|
||||
button's focus link. Nothing new had to be discovered to read it — only noticed.
|
||||
|
||||
## The defect it fixes
|
||||
|
||||
`ratc::parse` named each child with `name_before`: the last printable run in the
|
||||
96 bytes before the child's magic. That is a heuristic, and it is *usually* right
|
||||
— `ptbase.t32`'s three trailing bytes are `0e 10 a4`, not printable, so the scan
|
||||
walks back to the real name.
|
||||
|
||||
But `pteff05.t32`'s trailing three bytes are `38 41 58`, which is `"8AX"` in
|
||||
ASCII. The scan takes those, the child is registered under the name `8AX`, the
|
||||
element that declares `pteff05.t32` matches nothing in the sprite table, and
|
||||
`compose` drops it through
|
||||
|
||||
```rust
|
||||
let Some(sprite) = el.sprite.as_ref() else { continue };
|
||||
```
|
||||
|
||||
— *before* the arm that records a `missing` sprite. So the screen's background
|
||||
vanished with **no diagnostic at all**: `screen render` reported "all resolved".
|
||||
|
||||
⚠️ `8AX` was carried in `docs/` as though it were a name the game uses — the old
|
||||
text read "the `T8aD` behind its `opt ` link is registered under the name `8AX`".
|
||||
It is not a name. It is three bytes of the preceding record's payload.
|
||||
|
||||
## The disc-wide check
|
||||
|
||||
[`examples/ratc_child_names.rs`](../../../crates/sylpheed-formats/examples/ratc_child_names.rs)
|
||||
compares the two readings for every RATC child in all 33 `dat/*.pak`
|
||||
([data](../data/ratc-child-name-audit.txt)):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| RATC children scanned | **18 002** |
|
||||
| carrying an `opt ` block | 17 942 |
|
||||
| scanned name **agrees** with it | **17 918** |
|
||||
| scanned name **differs** | **24** |
|
||||
|
||||
Every one of the 24 is the same failure: a 3-byte printable tail beating a real
|
||||
name.
|
||||
|
||||
| scanned | actual | count | where |
|
||||
|---|---|---|---|
|
||||
| `8AX` | `pbbg.t32` | 12 | `GP_READY_ROOM` |
|
||||
| `8AX` | `pteff05.t32` | 4 | `GP_TITLE` 5, 6, 8, 9 — the menus |
|
||||
| `8AX` | `pmbase.t32` | 4 | `GP_STAGE_CLEAR` |
|
||||
| `8AX` | `pteff04.t32` | 2 | `GP_TITLE` 4, 7 — the title |
|
||||
| `'OX` | `po_keys_win1.t32` | 2 | `GP_OPTIONS` |
|
||||
|
||||
⚠️ **What I checked about the 24, precisely.** That the recovered name is the one
|
||||
the bundle actually wants is verified for the ten `GP_TITLE` and `GP_STAGE_CLEAR`
|
||||
cases: on the title screens `pteff04.t32`/`pteff05.t32` are *declared elements*
|
||||
that previously resolved to nothing and now resolve, and `pmbase.t32` is the
|
||||
target of `GP_STAGE_CLEAR`'s long-standing dangling reference (below). For the 12
|
||||
`GP_READY_ROOM` (`pbbg.t32`) and 2 `GP_OPTIONS` (`po_keys_win1.t32`) cases I
|
||||
checked only that no element in those bundles is left unresolved afterwards —
|
||||
which is consistent with, not proof of, the same story. None of the 14 is on the
|
||||
five menu screens.
|
||||
|
||||
**The control is the 17 918 the heuristic already got right**: the `opt ` reading
|
||||
reproduces every one of them. A reading that fixed the 24 but disturbed the rest
|
||||
would be a different rule, not this one.
|
||||
|
||||
✅ **Reach — closed 2026-08-29.** 60 children (0.3 %) have **no** `opt ` block
|
||||
within 128 bytes and fall back to the scan. **They are not children.** They are
|
||||
the ten frames of the disc's only `.tan` resource, `pb_f15_eg_anm.tan`, in the
|
||||
six language copies of one `GP_READY_ROOM` bundle — 6 × 10 = 60, the whole
|
||||
population with nothing left over. One `opt ` block names the whole run, which is
|
||||
why nine of the ten find no block of their own. None is on the five menu screens.
|
||||
[`ratc-tan-frame-sequence.md`](ratc-tan-frame-sequence.md)
|
||||
|
||||
## What it changes in the composite
|
||||
|
||||
Resolving the name makes the element resolve, so `compose` now draws it. On all
|
||||
five port screens ([before/after](../data/ratc-name-fix-render-effect.txt)):
|
||||
|
||||
| build | screen | mean brightness | high-frequency detail |
|
||||
|---|---|---|---|
|
||||
| 4 | title | 72.77 → 72.81 | **×1.15** |
|
||||
| 5 | main menu (JP) | 42.73 → 42.74 | **×1.27** |
|
||||
| 6 | `EXTRAS` (JP) | 44.08 → 44.09 | **×1.27** |
|
||||
| 8 | main menu | 41.57 → 41.58 | **×1.30** |
|
||||
| 9 | `EXTRAS` | 42.99 → 43.00 | **×1.29** |
|
||||
|
||||
The brightness is unmoved and the detail is up by a quarter — which is exactly
|
||||
the signature of *the same artwork at twice the resolution* replacing a 2×
|
||||
upscale, and not of new content appearing. That it *should* be the full-res art
|
||||
was settled separately and against the running game, in
|
||||
[`ui-8ax-fullres-background.md`](ui-8ax-fullres-background.md); this page only
|
||||
supplies the name that lets the renderer find it.
|
||||
|
||||
✅ **Nothing is covered.** The background is opaque and full-screen, and on the
|
||||
menu it paints 4th of 16, so the worry is real. Measured at each element's
|
||||
resting rect, before vs after: `ptbtn01` sd 59.98 → 60.03, `ptbtn03` 63.40 →
|
||||
63.45, `ptbtn05` 66.54 → 66.58, `ptframe1` 51.16 → 51.20, `ptmsg` 56.11 → 56.12.
|
||||
Everything survives; only the two `loop*` elements paint beneath it, and those
|
||||
are excluded from the default composite anyway.
|
||||
|
||||
⚠️ **Both backgrounds are now drawn** — `ptbase.t32` upscaled 2×, then the
|
||||
full-res one opaquely over it. Correct output, wasted fill. The port should draw
|
||||
only the full-res one, and take its *timing* from `ptbase`'s element, which is
|
||||
the one carrying the keyframes.
|
||||
|
||||
## What is NOT decoded
|
||||
|
||||
❔ **No declaration word points at the child.** Before reading the bytes I tested
|
||||
whether the 60-byte element declaration indexes the T8aD child table. Its unread
|
||||
words are `+28`, `+36`, `+44` and `+56`; the control is the resolved elements,
|
||||
whose child index is known. On the main menu, of 13 controls the words reproduce
|
||||
the child index **1, 0, 0 and 1** times — and both 1s are the trivial index-0
|
||||
case. There is no pointer; the association is by name, and the name is the `opt `
|
||||
string. [`decl_word_probe.rs`](../../../crates/sylpheed-formats/examples/decl_word_probe.rs)
|
||||
|
||||
✅ Incidental, from the same probe: **`+44` is a button ordinal.** It is `1…5` on
|
||||
exactly the five `ptbtn0N.rat` elements of the main menu, in order, and `−1` on
|
||||
every other element. Not needed for anything open, and recorded rather than
|
||||
chased.
|
||||
120
docs/re/structures/ratc-tan-frame-sequence.md
Normal file
120
docs/re/structures/ratc-tan-frame-sequence.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# `.tan` — one name over a run of frames, and the 60 "nameless" children it explains
|
||||
|
||||
**Status:** ✅ `DECODED`, with a disc-wide check. This closes the 🟡 reach caveat
|
||||
left open by [`ratc-child-names.md`](ratc-child-names.md): *"60 children have no
|
||||
`opt ` block within 128 bytes and still fall back to the scan — whether they
|
||||
genuinely lack the block or sit past the search window is not established."*
|
||||
|
||||
**Neither.** They are not children. They are the **ten frames of a single `.tan`
|
||||
resource**, and the one `opt ` block that names the whole run sits up to 545 KB
|
||||
behind the last of them.
|
||||
|
||||
## What was measured
|
||||
|
||||
[`examples/ratc_optless_children.rs`](../../../crates/sylpheed-formats/examples/ratc_optless_children.rs)
|
||||
re-runs `ratc::parse`'s own guards over every child on the disc and reports, for
|
||||
each rejection, *which* guard fired and whether a tag exists further back
|
||||
([data](../data/ratc-tan-frame-sequence.txt)):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| RATC children scanned | 18 002 |
|
||||
| with no accepted `opt ` block | **60** |
|
||||
| of those, rejected by **length**, **gap** or **charset** | **0** |
|
||||
| of those, rejected because the only tag is **beyond the 128-byte window** | **60** |
|
||||
| of those, that are child #0 of their bundle | **0** |
|
||||
| archives involved | **1** — `GP_READY_ROOM.pak` |
|
||||
| bundles involved | **6** — entries 26, 30, 159, 160, 1029, 1050, each exactly 706 609 B |
|
||||
| children involved | **1…10 of each**, never 0 and never 11+ |
|
||||
|
||||
The distances are the tell. Within one bundle they are
|
||||
|
||||
```text
|
||||
213, 60 813, 121 413, 182 013, 242 613, 303 213, 363 813, 424 413, 485 013, 545 613
|
||||
```
|
||||
|
||||
— an exact arithmetic progression, step **60 600**. Ten different children all
|
||||
find the **same** `opt ` tag, because there is only one. Nine of them do not have
|
||||
a block that is merely far away; they have no block.
|
||||
|
||||
## What they are
|
||||
|
||||
Reading bundle 26 directly, without the Rust parser, the 15 "children" resolve:
|
||||
|
||||
| # | offset | kind | size | `opt ` at | name |
|
||||
|---|---|---|---|---|---|
|
||||
| 0 | `0x000444` | T8aD | 1 933 | −40 | `pbf15_energie_generator2.t32` |
|
||||
| **1…9** | `0x000bd1` … | T8aD | **60 600** each | −213 … −485 013 | **`pb_f15_eg_anm.tan`** |
|
||||
| **10** | `0x085e49` | T8aD | **60 632** | −545 613 | **`pb_f15_eg_anm.tan`** |
|
||||
| 11 | `0x094b21` | T8aD | 17 523 | −32 | `pbf15_pd_inside2.t32` |
|
||||
| 12 | `0x098f94` | T8aD | 75 068 | −35 | `pbenergie_generator.t32` |
|
||||
| 13 | `0x0ab4d0` | T8aD | 4 697 | −28 | `pbf15_eg_eff.t32` |
|
||||
| 14 | `0x0ac729` | RATC | 264 | −25 | `pb_s15_eg.rat` |
|
||||
|
||||
So the format is doing something perfectly ordinary that our scan had no concept
|
||||
of: **`.tan` is a frame sequence.** One `opt ` block declares the resource, and
|
||||
its payload is a run of equal-size `T8aD` blocks, one per frame. The name is on
|
||||
the disc and always was. What was missing was the idea that one name can cover
|
||||
more than one block.
|
||||
|
||||
`anm` in `pb_f15_eg_anm` is the authors' own abbreviation, and it agrees.
|
||||
|
||||
## The disc-wide check
|
||||
|
||||
Every `opt ` block in every RATC bundle in all 33 `dat/*.pak`, by the extension
|
||||
it names
|
||||
([`tools/re-capture/ratc_opt_name_census.py`](../../../tools/re-capture/ratc_opt_name_census.py)):
|
||||
|
||||
| extension | count | what it is |
|
||||
|---|---|---|
|
||||
| `.t32` | 14 756 | a `T8aD` sprite |
|
||||
| `.rat` | 3 311 | a nested RATC leaf |
|
||||
| `.prm` | 367 | a primitive |
|
||||
| `.tbm` | 224 | — |
|
||||
| `.sbo` | 54 | — |
|
||||
| **`.tan`** | **6** | **a frame sequence** |
|
||||
| | **18 718** | |
|
||||
|
||||
A RATC bundle names exactly six kinds of resource, and **`.tan` occurs six times
|
||||
on the whole disc** — all of them `pb_f15_eg_anm.tan`, one per language copy of
|
||||
the same bundle, each holding **10 frames**.
|
||||
|
||||
**6 × 10 = 60.** That is the entire population of opt-less children, with nothing
|
||||
left over. The negative is closed, not narrowed.
|
||||
|
||||
## ⚠️ What this says about `ratc::parse`
|
||||
|
||||
The child list **over-reports**. `parse` finds children by scanning for the four
|
||||
child magics, so a `.tan`'s ten frames are listed as ten anonymous children of the
|
||||
bundle rather than as one named resource with ten frames. The disc's "18 002
|
||||
children" is therefore 18 002 *magic-delimited blocks*, of which 60 are frames.
|
||||
|
||||
**Not changed here**, deliberately: nothing in the menu milestone reads a `.tan`,
|
||||
and a rewrite of the child model is a bigger change than the one fact it would
|
||||
buy. Recorded so that a later consumer of `.tan` knows the shape it needs.
|
||||
|
||||
## ❔ Not established
|
||||
|
||||
* **The frame timing.** Ten frames of the same size is a sequence; nothing here
|
||||
shows the rate, whether it loops, or whether the frames are equal-duration. No
|
||||
field was looked for.
|
||||
* **The pixel layout of a 60 600-byte frame.** They decode as `T8aD` like any
|
||||
other sprite as far as the magic goes; their dimensions were not read.
|
||||
* **What `.tbm` and `.sbo` are.** They surfaced from the same census and are
|
||||
recorded above as counts only.
|
||||
* **The two `opt ` totals do not reconcile exactly** and are not forced to.
|
||||
This census counts **18 718** blocks; the Rust audit in
|
||||
[`ratc-child-names.md`](ratc-child-names.md) counts **17 942** children *with* a
|
||||
block. They apply different guards — the Rust one additionally requires the
|
||||
named thing to be one of the four child magics and to follow within 8 bytes,
|
||||
which `.prm` / `.tbm` / `.sbo` (645 blocks) never satisfy. That accounts for
|
||||
most of the 776 difference but not all of it, and the remainder was not chased.
|
||||
Each number is reported as what its own script measured.
|
||||
|
||||
## Scope
|
||||
|
||||
`GP_READY_ROOM.pak` is **out of scope** for the menu milestone ([S1 is a
|
||||
no-go](../ready-room-probe.md)), and `.tan` occurs in no other archive. **None of
|
||||
the five menu screens contains a `.tan`**, so nothing the port draws changes.
|
||||
This closes a caveat on a decode the port *does* depend on, rather than adding a
|
||||
capability.
|
||||
@@ -1,8 +1,26 @@
|
||||
# 🟡 `pteff04` / `pteff05` are dropped — the texture is registered as `8AX`
|
||||
# ✅ `pteff04` / `pteff05` — the full-resolution background, once dropped as `8AX`
|
||||
|
||||
**Status:** 🟡 a real name-resolution gap, **currently harmless to look at**, and
|
||||
the obvious fix would make the render *worse*. Found by auditing what
|
||||
`screen render` silently omits on the port's five screens.
|
||||
**Status:** ✅ **RESOLVED 2026-08-29.** This page's two questions are both closed
|
||||
and it is kept for the evidence, not as an open item.
|
||||
|
||||
* *Which of the two backgrounds does the game draw?* — the **full-resolution**
|
||||
one. Measured against a capture; the section below stands unchanged.
|
||||
* *Why did ours drop it?* — because `8AX` **is not a name.** It is three bytes of
|
||||
the preceding record's payload (`38 41 58`) that happen to be printable ASCII,
|
||||
and our backwards printable-run scan preferred them to the name the format
|
||||
states in its `opt ` block. Decoded, with a disc-wide check, in
|
||||
[`ratc-child-names.md`](ratc-child-names.md); `ratc::parse` now reads the
|
||||
stated name and `screen render` draws the background on all five screens.
|
||||
|
||||
⚠️ **Read the rest of this page with that correction in mind.** It was written
|
||||
while `8AX` was believed to be a name the game uses, and says so in several
|
||||
places — "the texture is registered as `8AX`", "the `T8aD` behind its `opt ` link
|
||||
is registered under the name `8AX`". The `opt ` link was never the problem; the
|
||||
`opt ` block was the answer, sitting unread three bytes away.
|
||||
|
||||
<sub>Original status line: 🟡 a real name-resolution gap, currently harmless to
|
||||
look at, and the obvious fix would make the render *worse*. Found by auditing
|
||||
what `screen render` silently omits on the port's five screens.</sub>
|
||||
|
||||
## What `screen render` drops, and why
|
||||
|
||||
|
||||
203
docs/re/structures/ui-button-focus-record.md
Normal file
203
docs/re/structures/ui-button-focus-record.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# ✅ A focused button is a **two-element record**, and the second element is the ring
|
||||
|
||||
**Status:** 🟡 `DECODED for GP_TITLE`, disc-wide check **not yet run**; the
|
||||
sprite contents and the ring are ✅ `MEASURED` against a live capture. Answers the
|
||||
port's *"focus: drawn over the base element, or instead of it?"*
|
||||
|
||||
⚠️ The element-count field at `+20` is read here on **`GP_TITLE`'s ten button
|
||||
records only** (five buttons × two language bundles). A disc-wide check —
|
||||
"does `+20` equal the number of resource names the leaf embeds, for every `.rat`
|
||||
leaf on the disc?" — is written and was still running when this was committed.
|
||||
Until it lands, treat `+20` as a **strong local reading, not a decoded field**:
|
||||
the *observable* claim below (the focus record carries a second sprite,
|
||||
`ptbtneff01.t32`, and the port must draw it) rests on the embedded names and the
|
||||
capture, not on that word.
|
||||
|
||||
**Short answer: it does not matter, and that is not the bug.** The focused sprite
|
||||
covers the base completely, so over-vs-instead is worth at most 12/255 on ~25
|
||||
pixels. What *is* missing is the focus record's **second element** — a glowing
|
||||
ring that only exists when focused.
|
||||
|
||||
## The record
|
||||
|
||||
A button's base and focused variants are both `.rat` leaves, and they are not the
|
||||
same shape:
|
||||
|
||||
```text
|
||||
ptbtn02.rat 164 B strings: RATC, ptbtn02.t32, opt ptbtn02f.rat
|
||||
ptbtn02f.rat 312 B strings: RATC, ptbtneff01.t32, ptbtn02f.t32, opt ptbtn02b.rat
|
||||
```
|
||||
|
||||
The header says so explicitly. A `.rat` leaf begins:
|
||||
|
||||
```text
|
||||
+0 "RATC"
|
||||
+16 u32 flags 0x00008110 (base) 0x00008112 (focused)
|
||||
+20 u32 ELEMENT COUNT 1 2
|
||||
+32 first element name
|
||||
```
|
||||
|
||||
So the focused record declares **two** elements — `ptbtneff01.t32` first, then
|
||||
`ptbtn0Nf.t32` — where the base declares one. Both `f` records of both language
|
||||
bundles read the same way, and the flag word differs only in bit `0x02`
|
||||
alongside the count.
|
||||
|
||||
⚠️ The `opt ` link on a `.rat` leaf is **not** simply "my focused variant". The
|
||||
chain runs `ptbtn01.rat → ptbtn01f.rat → ptbtn02.rat → ptbtn02f.rat →
|
||||
ptbtn02b.rat`, i.e. it threads base and focused records together in order. The
|
||||
corpus calls it the focus link and for a *base* record that reading works; do not
|
||||
generalise it to the `f` records.
|
||||
|
||||
## The three sprites
|
||||
|
||||
Decoded with `sylpheed-cli pak textures`, shown over a checkerboard in
|
||||
[`button-base-focus-ring-sprites.png`](../captures/ui-layout/button-base-focus-ring-sprites.png):
|
||||
|
||||
| sprite | size | what it is |
|
||||
|---|---|---|
|
||||
| `ptbtn02.t32` | 117×43 | the **dim** label, its underline, and a small dot-in-circle at the underline's left end |
|
||||
| `ptbtn02f.t32` | 130×56 | the **same label, bright and glowing** — a complete replacement, 13 px larger in each axis |
|
||||
| `ptbtneff01.t32` | 42×46 | a **glowing ring**, focus only |
|
||||
|
||||
The small dot-in-circle is on **every** button all the time; the large ring is
|
||||
the focus marker. They are different things and sit side by side on the focused
|
||||
row — visible in
|
||||
[`live-main-menu-options-focused.png`](../captures/title-builds/live-main-menu-options-focused.png)
|
||||
against
|
||||
[`live-main-menu.png`](../captures/title-builds/live-main-menu.png).
|
||||
|
||||
## Over or instead? Measured, and the answer is "unobservable"
|
||||
|
||||
Aligning base and focused by normalised cross-correlation of their alpha masks —
|
||||
the true offset is **(7,7)**, not the (6,6) that centring predicts —
|
||||
|
||||
| pair | base-visible px | `f` alpha ≥ base alpha there |
|
||||
|---|---|---|
|
||||
| `ptbtn02` / `f` (bundle `b58a0fe6`) | 1 444 | **100.0 %** |
|
||||
| `ptbtn01` / `f` (bundle `b58a0fe6`) | 1 898 | **100.0 %** |
|
||||
| `ptbtn02` / `f` (bundle `a715f485`) | 3 720 | **100.0 %** |
|
||||
|
||||
⚠️ **At the centre alignment it reads 78–84 %, and that number is an artefact.**
|
||||
A 1 px shift on strokes this thin manufactures a fifth of a sprite's worth of
|
||||
"the focused art is thinner here". Solve the alignment before trusting a coverage
|
||||
figure.
|
||||
|
||||
Coverage is not the same as hiding, though, because `f` is not fully opaque
|
||||
everywhere. Compositing both ways over the menu's own background colour:
|
||||
|
||||
| pair | max channel difference | px > 8/255 | RMSE over the button rect |
|
||||
|---|---|---|---|
|
||||
| `ptbtn02` | 12.5 | 23 | 1.09 |
|
||||
| `ptbtn01` | 12.2 | 27 | 1.08 |
|
||||
| `ptbtn02` (JP) | 12.2 | 27 | 1.05 |
|
||||
|
||||
So the two hypotheses differ by **~1.1 RMSE inside the button rectangle**, on a
|
||||
couple of dozen pixels — below the ≈ γ 1.4 tone gap
|
||||
([tone curve](ui-render-tone-curve.md)) and far below any frame-level RMSE this
|
||||
corpus can resolve. **Either choice is defensible; neither is measurable.**
|
||||
Replacing is the cheaper one and is what the file's structure suggests, since
|
||||
`ptbtn0Nf.t32` is a whole label rather than an overlay.
|
||||
|
||||
## ✅ Where the ring is placed — DECODED 2026-08-29, no authoring needed
|
||||
|
||||
This page previously said the per-element placement inside a `.rat` leaf was not
|
||||
decoded and that a consumer should eyeball it off a capture. **That was wrong by
|
||||
omission**: a leaf needs no new reader. Its first 32 bytes have the same shape as
|
||||
a bundle header — `"RATC"`, `0x3c` declaration-entry size at `+4`, element count
|
||||
at `+20`, design size `1280x720` at `+24`/`+28` — so `ui_layout::parse_build`
|
||||
reads it **unchanged**.
|
||||
|
||||
**The control is the base record**, whose position is known independently: the
|
||||
parent screen reports `ptbtn01.rat` resting at `(542,162)`, and parsing the leaf
|
||||
on its own returns `ptbtn01.t32` at `(542,162)`. It reproduces all five.
|
||||
|
||||
Positions are **absolute design-space top-left**, not offsets
|
||||
([`examples/rat_leaf_placement.rs`](../../../crates/sylpheed-formats/examples/rat_leaf_placement.rs)):
|
||||
|
||||
| button | base | ring `ptbtneff01.t32` | Δ | label `ptbtn0Nf.t32` | Δ |
|
||||
|---|---|---|---|---|---|
|
||||
| `ptbtn01` | (542,162) | **(500,156)** | (−42,−6) | (535,155) | (−7,−7) |
|
||||
| `ptbtn02` | (542,242) | **(500,236)** | (−42,−6) | (535,235) | (−7,−7) |
|
||||
| `ptbtn03` | (542,322) | **(500,316)** | (−42,−6) | (535,315) | (−7,−7) |
|
||||
| `ptbtn04` | (542,402) | **(500,396)** | (−42,−6) | (535,395) | (−7,−7) |
|
||||
| `ptbtn05` | (542,482) | **(500,476)** | (−42,−6) | (535,475) | (−7,−7) |
|
||||
|
||||
The offset is **uniform**: `(−42,−6)` for the ring and `(−7,−7)` for the label on
|
||||
every button, and identical in the Japanese bundle (pak entry 8).
|
||||
|
||||
⚠️ **Which placement wins — and an earlier version of this page was misleading.**
|
||||
It said "the parent is what `compose` honours; treat the leaf's as the source only
|
||||
for elements the parent does not declare". That is right for a **base** record and
|
||||
wrong for an **`f`** record, because *the parent declares no element for
|
||||
`ptbtn0Nf.rat` at all* — checked, zero of build 5's 16 elements name it. So the
|
||||
`f` record's placement can only come from its own leaf, for **both** its elements,
|
||||
the bright label included.
|
||||
|
||||
The `(−7,−7)` on the label is real and load-bearing: `ptbtn0Nf.t32` is 13 px
|
||||
larger in each axis than the base, and −7 keeps the two **concentric**
|
||||
(`535 + 96/2 = 583` against `542 + 83/2 = 583.5`). Drawing the `f` label at the
|
||||
base element's position would push it 7 px down-right.
|
||||
|
||||
✅ **Checked against the oracle.** Differencing the `OPTIONS`-focused capture
|
||||
against the unfocused one, the changed region is x **505…703**, y **397…446**.
|
||||
The leaf predicts ink starting inside the ring's box at x ≥ 500 (measured 505, a
|
||||
5 px art inset) and the label's right edge near 707 (measured 703); the
|
||||
parent-position reading predicts 714. Both the right edge and the bottom edge
|
||||
favour the leaf by ~7 px. ⚠️ Ink-inset reasoning is soft — the decisive argument
|
||||
is the structural one above, that there is no parent element to inherit from.
|
||||
|
||||
The one thing that *is* duplicated is the **base** record: `ptbtn04`'s parent
|
||||
element rests at y **401** while its own leaf says **402** (and in the JP bundle
|
||||
the leaf says 401 against a parent 401). For a base record the parent is what
|
||||
`compose` honours.
|
||||
|
||||
## ✅ The ring SPINS — and the oracle confirms the game draws the rotation
|
||||
|
||||
The ring's two keyframes differ in exactly one field:
|
||||
|
||||
```text
|
||||
ptbtneff01.t32 kf0 t=120 pos=(500,156) scale=100%,100% a=255 rot=0
|
||||
kf1 t=None pos=(500,156) scale=100%,100% a=255 rot=360
|
||||
```
|
||||
|
||||
Position, scale, alpha and tint are all constant; only `rotation_deg` ramps,
|
||||
**0 → 360**. That is a spin in place — the same shape as the `GP_BUNK` example
|
||||
already recorded in [`ui-keyframe-rotation.md`](ui-keyframe-rotation.md).
|
||||
|
||||
✅ **And it is actually rendered.** In the oracle's `OPTIONS`-focused frame the
|
||||
ring's bright head sits in a completely different angular position from the
|
||||
sprite's own — the game caught it mid-spin:
|
||||
[`focus-ring-oracle-vs-sprite.png`](../captures/ui-layout/focus-ring-oracle-vs-sprite.png)
|
||||
(left: the oracle at the ring's declared box; right: the sprite, unrotated).
|
||||
|
||||
🔴 **Do not quote an angle from this.** A brightest-region centroid puts the
|
||||
displacement near 250°, but the **control refuses it**: rotating the sprite by a
|
||||
known 30/90/180/270° and re-measuring gives errors up to **19.8°**, so the
|
||||
estimator is not trustworthy at that precision. What survives is the part the
|
||||
error bar cannot touch — a ≤20° error cannot manufacture a ~250° displacement, so
|
||||
**the ring is drawn substantially rotated**, and the exact angle is one frame of a
|
||||
continuous spin and not a stable quantity anyway.
|
||||
|
||||
⚠️ **This raises rotation's priority for a port.** It is not a title-only concern
|
||||
that sits off-screen at rest: the main menu's focus marker spins, so a renderer
|
||||
that ignores `rotation_deg` draws a static ring with its highlight in the wrong
|
||||
place, on the screen the player looks at most.
|
||||
|
||||
🟡 The spin's **period** is not established. `t=120` is the first keyframe's time,
|
||||
and what the untimed second one means for a *leaf* — as opposed to a screen, where
|
||||
it is the ~0.4 s exit ramp — was not tested.
|
||||
|
||||
## ⚠️ `screen render --focus` is blind to the ring, and so was this page
|
||||
|
||||
`el.focused` is name-based on **top-level** elements, and a screen's buttons are
|
||||
`.rat` records whose focused twin is not itself a top-level element — so
|
||||
rendering build 5 with and without `--focus` produces an identical image. The
|
||||
reference renderer has the same blind spot the port reported, for the same
|
||||
reason: neither walks into the leaf. Fixing it is a renderer change, not a
|
||||
format question; the format is decoded above.
|
||||
|
||||
## ❔ Not established
|
||||
* **`ptbtn02b.t32`** — a third variant, `b`, exists for button 02 only, same size
|
||||
as the base. Not seen on any capture. Not chased.
|
||||
* Whether a **non-title** archive uses the same two-element convention. Checked
|
||||
on `GP_TITLE` only.
|
||||
@@ -330,8 +330,14 @@ parser limitation. It is not — **there is nothing deeper on the disc**:
|
||||
- The children that are themselves RATC (the `.rat` layout records) are **leaf
|
||||
records**: they carry no child list and instead **reference their siblings by
|
||||
name** — the sprite they place and, via `opt `, their focused variant.
|
||||
**3 311** such leaves, every one embedding sibling names, and **10 144 of
|
||||
**3 311** such leaves, every one embedding sibling names, and **10 148 of
|
||||
10 148** references resolve to a sibling of the same bundle.
|
||||
⚠️ It read **10 144 of 10 148** until 2026-08-29. The 4 misses were
|
||||
`pmbase.rat` → `pmbase.t32` in `GP_STAGE_CLEAR.pak`, written up as an asset
|
||||
that is "on the disc nowhere". It was on the disc: it is the child our name
|
||||
scan called `8AX`. See [ratc-child-names](ratc-child-names.md) — the same
|
||||
defect that hid the menu backgrounds. Nothing about the reference was wrong;
|
||||
the thing it pointed at had the wrong name in our index.
|
||||
|
||||
That is the same by-name convention used one level up, where a screen's config
|
||||
names `.prt` components, and one level up again, where the movie table names
|
||||
|
||||
70
docs/re/ui-splash-addressing.md
Normal file
70
docs/re/ui-splash-addressing.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# ❔ How to address the developer-logo splash — no content rule, but `GP_TITLE` needs none
|
||||
|
||||
**Status:** ❔ **undecodable as a general predicate, with reach** — and ✅
|
||||
**exact for `GP_TITLE`**, which is the only archive in scope. Answers the port's
|
||||
*"I need a predicate, not an index."*
|
||||
|
||||
## The ask
|
||||
|
||||
The splash has no `.rat` layout child, so `is_build` rejects it and it is
|
||||
reachable only through `--all` / `is_composable`, which disc-wide also admits
|
||||
~1 786 non-screen bundles. The port wanted a rule that admits the splash and not
|
||||
those.
|
||||
|
||||
## ✅ In `GP_TITLE` the problem does not arise
|
||||
|
||||
```
|
||||
$ sylpheed-cli screen list GP_TITLE.pak 12 builds — entries 0-9, 12, 15
|
||||
$ sylpheed-cli screen list --all GP_TITLE.pak 16 bundles — entries 0-15
|
||||
```
|
||||
|
||||
`--all` adds exactly **four** bundles: entries **10, 11, 13, 14**. All four are
|
||||
real screens, and **not one fragment appears**. So within this archive
|
||||
`is_composable` *is* the predicate — it is exact, with no filtering needed.
|
||||
|
||||
⚠️ And the renumbering worry is moot here: in `GP_TITLE` the `--all` index equals
|
||||
the pak entry index **1:1 across all 16**. That is an accident of this archive
|
||||
having 16 composable bundles at entries 0…15, not a general property — but it
|
||||
means the port can address these by entry index without the index meaning
|
||||
something different from elsewhere.
|
||||
|
||||
## ✅ And there are TWO splash screens, not one
|
||||
|
||||
Rendered with `screen render --all --build <n> --primitives`
|
||||
([grid](captures/title-builds/splash-both-halves-rendered.png)):
|
||||
|
||||
| entry | elements | what it draws |
|
||||
|---|---|---|
|
||||
| **10** | 3 | **SQUARE ENIX** publisher wordmark, `™` |
|
||||
| **11** | 7 | **GAME ARTS** / **SETA** / **studio anima** developer logos |
|
||||
| **13** | 3 | SQUARE ENIX, `®` — the twin of 10 |
|
||||
| **14** | 7 | the twin of 11 |
|
||||
|
||||
⚠️ **The port has 11/14 and is missing 10/13** — the publisher half, which is the
|
||||
*first* thing the boot sequence shows. The two halves differ only by the
|
||||
trademark glyph (`™` on 10, `®` on 13), which is the region/language split this
|
||||
archive uses everywhere else.
|
||||
|
||||
All four draw every element they declare (3/3 and 7/7), so nothing is silently
|
||||
dropped.
|
||||
|
||||
## ❔ The general rule: looked for, not found
|
||||
|
||||
Two candidate predicates, both dead:
|
||||
|
||||
* **Design size.** Every extra composable bundle sampled is **1280×720** — the
|
||||
same as every screen. It does not separate anything.
|
||||
* **Element count.** The "two-element fragment" story holds in
|
||||
`GP_MISSION_SELECT` (10 extras, all 2 elements) and **fails elsewhere**:
|
||||
`GP_OPTIONS`'s extras have 5 and 15 elements, `GP_SAVE_LOAD`'s have 4, 5, 6, 9
|
||||
and 13. The splash's own halves have 3 and 7. The ranges overlap, so no
|
||||
threshold separates them.
|
||||
|
||||
Reach: three archives sampled beyond `GP_TITLE`, chosen because they have
|
||||
non-build composables. Not a disc-wide sweep. A rule may exist in a field not
|
||||
looked at — the `.prm` fade quad is one candidate
|
||||
([transitions](screen-transitions.md) shows overlays lack it), untested here.
|
||||
|
||||
**So: the port is authoring this.** Locating the splash by entry index is the
|
||||
honest description, and `name_source` should say it was located by index and not
|
||||
by a rule.
|
||||
75
tools/re-capture/ratc_opt_name_census.py
Normal file
75
tools/re-capture/ratc_opt_name_census.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Every `opt ` name in every RATC bundle on the disc, by extension -- and the
|
||||
`.tan` frame sequences among them.
|
||||
|
||||
Written to close the reach caveat in docs/re/structures/ratc-child-names.md:
|
||||
60 of 18 002 RATC children carry no `opt ` block of their own. They are not
|
||||
children. They are the ten frames of the disc's only `.tan` resource, and one
|
||||
`opt ` block names the whole run.
|
||||
|
||||
python3 tools/re-capture/ratc_opt_name_census.py
|
||||
|
||||
Reads $SYLPHEED_DISC/dat/*.pak directly (IPFB TOC + Z1/zlib entries), so it does
|
||||
not depend on the Rust parser it is checking. Takes a few minutes.
|
||||
"""
|
||||
|
||||
import struct, zlib, glob, os, collections, bisect
|
||||
MAG = (b"T8aD", b"RATC", b"ttcf", b"\x89PNG")
|
||||
ext = collections.Counter()
|
||||
tan_sites = []
|
||||
opt_total = 0
|
||||
DISC = os.environ.get("SYLPHEED_DISC", "/work/sylph_extract")
|
||||
for pakpath in sorted(glob.glob(f"{DISC}/dat/*.pak")):
|
||||
base = pakpath[:-4]
|
||||
pak = open(pakpath, "rb").read()
|
||||
if pak[:4] != b"IPFB": continue
|
||||
n = struct.unpack_from(">I", pak, 4)[0]
|
||||
toc = [struct.unpack_from(">III", pak, 0x10 + 12*i) for i in range(n)]
|
||||
segs = sorted(glob.glob(base + ".p[0-9][0-9]"))
|
||||
if not segs: continue
|
||||
data = b"".join(open(s, "rb").read() for s in segs)
|
||||
for ei, (h, off, cs) in enumerate(toc):
|
||||
raw = data[off:off+cs]
|
||||
try:
|
||||
b = zlib.decompress(raw[10:]) if raw[:2] == b"Z1" else raw
|
||||
except Exception:
|
||||
continue
|
||||
if b[:4] != b"RATC": continue
|
||||
names = []
|
||||
p = b.find(b"opt ")
|
||||
while p >= 0:
|
||||
ln = struct.unpack_from(">I", b, p+4)[0] if p+8 <= len(b) else 0
|
||||
if 0 < ln <= 64 and p+8+ln <= len(b):
|
||||
nm = b[p+8:p+8+ln].decode('latin1', 'replace')
|
||||
if nm and all(32 < ord(c) < 127 for c in nm):
|
||||
names.append((p, nm)); opt_total += 1
|
||||
ext[os.path.splitext(nm)[1].lower()] += 1
|
||||
p = b.find(b"opt ", p+4)
|
||||
offs, i = [], 4
|
||||
while i + 4 <= len(b):
|
||||
if b[i:i+4] in MAG:
|
||||
offs.append(i); i += 4
|
||||
else: i += 1
|
||||
if not names or not offs: continue
|
||||
npos = [p for p, _ in names]
|
||||
# each child -> index of the nearest preceding opt
|
||||
owner = collections.defaultdict(list)
|
||||
for k, o in enumerate(offs):
|
||||
j = bisect.bisect_left(npos, o) - 1
|
||||
if j >= 0: owner[j].append(k)
|
||||
for j, (p, nm) in enumerate(names):
|
||||
if not nm.lower().endswith(".tan"): continue
|
||||
ks = owner.get(j, [])
|
||||
if not ks: continue
|
||||
sizes = sorted({(offs[k+1] if k+1 < len(offs) else len(b)) - offs[k] for k in ks})
|
||||
tan_sites.append((os.path.basename(pakpath), ei, nm, len(ks), sizes))
|
||||
print(f"`opt ` blocks disc-wide: {opt_total}")
|
||||
print("\nby extension:")
|
||||
for e, c in ext.most_common(25):
|
||||
print(f" {e or '(none)':10} x{c}")
|
||||
print(f"\n.tan resources with >=1 child: {len(tan_sites)}")
|
||||
seen = collections.Counter()
|
||||
for t in tan_sites:
|
||||
seen[(t[0], t[2], t[3], tuple(t[4]))] += 1
|
||||
for (pk, nm, fr, sz), c in sorted(seen.items()):
|
||||
print(f" {pk:24} {nm:30} frames={fr:3} sizes={list(sz)} x{c} bundles")
|
||||
Reference in New Issue
Block a user