Compare commits
143 Commits
auto/re-is
...
auto/re-ra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26b5afeaa3 | ||
|
|
3491d30066 | ||
|
|
7eeae3006a | ||
|
|
d110cf38c7 | ||
|
|
b21c8e4118 | ||
|
|
9ca1eb50fd | ||
|
|
9a0ca0d71f | ||
|
|
f817dd5939 | ||
|
|
56cc7acfc3 | ||
|
|
49f109deb1 | ||
|
|
7a4e4333f4 | ||
|
|
2532c056be | ||
|
|
7988f52ce7 | ||
|
|
7156591654 | ||
|
|
2a086051b3 | ||
|
|
baed44a9ea | ||
|
|
7fb6bdfad8 | ||
|
|
1f2b469f7e | ||
|
|
458585d136 | ||
|
|
4012d5b555 | ||
|
|
515456c59a | ||
|
|
93e9b185ea | ||
|
|
0265da31a1 | ||
|
|
60285a6ead | ||
|
|
fb6376a548 | ||
|
|
0efd692b4c | ||
|
|
145046f88f | ||
|
|
6d9214fd07 | ||
|
|
714a26769d | ||
|
|
489ea12759 | ||
|
|
5f701db588 | ||
|
|
c463f98164 | ||
|
|
921a3cfc44 | ||
|
|
421c61a517 | ||
|
|
c6ce000002 | ||
|
|
93b0a6b20f | ||
|
|
3490fba9e3 | ||
|
|
aaaa08b164 | ||
|
|
7373035868 | ||
|
|
f7f9b555f6 | ||
|
|
ba47bdebe8 | ||
|
|
c3cf3c2e81 | ||
|
|
46006406a4 | ||
|
|
a3946e5005 | ||
|
|
e9924ff9e8 | ||
|
|
cc4e5e04a7 | ||
|
|
ee73de50be | ||
|
|
2b4ec20f00 | ||
|
|
7d07eb945f | ||
|
|
8783adda14 | ||
|
|
f10edf1e79 | ||
|
|
0f1c0f4e14 | ||
|
|
6d246c7a97 | ||
|
|
b973d8cde3 | ||
|
|
c2c69b02be | ||
|
|
b947fa8e8e | ||
|
|
6f23f4d113 | ||
|
|
531840b417 | ||
|
|
f11fde51e5 | ||
|
|
67fa1a1b0b | ||
|
|
130ad8d1b3 | ||
|
|
5115b4894a | ||
|
|
0ecb20bdbc | ||
|
|
6930d301c1 | ||
|
|
e10a6d6f2c | ||
|
|
5d02d5cd60 | ||
|
|
e0d02dce71 | ||
|
|
c10802536f | ||
|
|
2a0afc1405 | ||
|
|
e249b2e2ff | ||
|
|
cdb5a99def | ||
|
|
d217c07db2 | ||
|
|
5414db34bf | ||
|
|
4bc970688f | ||
|
|
ed4e5c7b37 | ||
|
|
1b95001feb | ||
|
|
c176fa53bc | ||
|
|
83293c3f81 | ||
|
|
3717293fa9 | ||
|
|
00ce0f33d0 | ||
|
|
a048910497 | ||
|
|
5f3d8f86a4 | ||
|
|
113c25aac1 | ||
|
|
9f39e8ee72 | ||
|
|
b302558d26 | ||
|
|
cc72f8b5a8 | ||
|
|
4830c63402 | ||
|
|
4380305921 | ||
|
|
86e52b01d0 | ||
|
|
a98e96b0f0 | ||
|
|
52275ddd53 | ||
|
|
6e92ae4597 | ||
|
|
c8c7677f6d | ||
|
|
8b4965f0a5 | ||
|
|
2be5ba7934 | ||
|
|
2b0e66b6ca | ||
|
|
b1b2576769 | ||
|
|
0d7199bc22 | ||
|
|
aea529e7dc | ||
|
|
dfcaac221e | ||
|
|
bc6354dce0 | ||
|
|
2c4005795b | ||
|
|
f5e7426cfa | ||
|
|
c760cdc509 | ||
|
|
c8e8dc0427 | ||
|
|
935f7ecab8 | ||
|
|
1f822f8b30 | ||
|
|
e81dcad632 | ||
|
|
e64965b3f9 | ||
|
|
b3c8632006 | ||
|
|
7fef19b3a0 | ||
|
|
4e745c8177 | ||
|
|
bebdb18a02 | ||
|
|
88b3ce9af5 | ||
|
|
27e8c51978 | ||
|
|
8b6dbcfead | ||
|
|
69291a315f | ||
|
|
4734797e50 | ||
|
|
ecbd70f69a | ||
|
|
b5a193839c | ||
|
|
5cdff5e515 | ||
|
|
77cd58202b | ||
|
|
306a8a5661 | ||
|
|
fb70511242 | ||
|
|
47e4e43310 | ||
|
|
1054d43676 | ||
|
|
e1dcc689bc | ||
|
|
03ece95c06 | ||
|
|
1fce3f9c71 | ||
|
|
8124b85fc7 | ||
|
|
245b73243b | ||
|
|
0cbef2023f | ||
|
|
50625b5a9e | ||
|
|
e5c6c27e6a | ||
|
|
a7d5a3bd12 | ||
|
|
1f4524f9fe | ||
|
|
3b67a451c2 | ||
|
|
fa3818c58c | ||
|
|
1ba16da9a4 | ||
|
|
e9c06e2e87 | ||
|
|
eb75b62f82 | ||
|
|
e1e4f7bc1d | ||
|
|
8af6a3f5d5 |
@@ -523,7 +523,7 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
||||
println!();
|
||||
println!("geometry — decoded sprite size vs the declared pivot, and every keyframe");
|
||||
println!(
|
||||
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=fade-alpha",
|
||||
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=alpha r=rot°",
|
||||
"#", "sprite", "decoded", "pivot*2", "same"
|
||||
);
|
||||
for el in &b.elements {
|
||||
@@ -543,8 +543,13 @@ fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let rot = if f.rotation_deg != 0 {
|
||||
format!(" r={}", f.rotation_deg)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!(
|
||||
"{}: {},{} {}%,{}% a={}",
|
||||
"{}: {},{} {}%,{}% a={}{rot}",
|
||||
f.time.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
|
||||
f.x,
|
||||
f.y,
|
||||
|
||||
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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
crates/sylpheed-formats/examples/paint_order_audit.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
//! Does the DERIVED paint order reproduce the ones measured from the game?
|
||||
//!
|
||||
//! `compose` uses a measured order for the three builds that have one and falls
|
||||
//! back to `derived_paint_order` (a sort on each sprite's layer key) everywhere
|
||||
//! else. The doc comment claims the derived order "reproduces both measured
|
||||
//! orders up to ties" — this checks that claim against all three, and says what
|
||||
//! the ties actually cost.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example paint_order_audit -- <GP_TITLE.pak>
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn measured(names: &[&str]) -> Option<(&'static str, Vec<usize>)> {
|
||||
const TITLE: [&str; 24] = [
|
||||
"ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32", "ptlogo2.t32", "ptlogo1.t32",
|
||||
"ptlogo2.t32", "pteff01.t32", "ptlogo_tm.t32", "pteff00.prm", "ptbase2.t32",
|
||||
"pteff04.t32", "ptloop01.rat", "ptloop02.rat", "pteff02.prm",
|
||||
"ptlogo_back2eff1.t32", "ptlogo_back2eff2.t32", "ptlogo_back2eff3.t32",
|
||||
"ptlogo_back2eff4.t32", "ptlogo_back2eff5.t32", "ptlogo_back2.t32",
|
||||
"ptlogo_back2eff.t32", "ptcopyright.t32", "ptlogoall_eff.t32",
|
||||
"ptlogoall_eff2.t32",
|
||||
];
|
||||
const SPLASH: [&str; 7] = [
|
||||
"palogo_eff0.prm", "palogo_gamearts.t32", "palogo_gamearts_eff.t32",
|
||||
"palogo_seta.t32", "palogo_seta_eff.t32", "palogo_anima.t32",
|
||||
"palogo_anima_eff.t32",
|
||||
];
|
||||
const MENU: [&str; 16] = [
|
||||
"pteff00.prm", "ptbase.t32", "pteff05.t32", "ptloop01.rat",
|
||||
"ptloop02.rat", "pteff02.prm", "ptframe1.t32", "ptframe2.t32",
|
||||
"pteff10.t32", "pteff12.t32", "ptbtn01.rat", "ptbtn02.rat",
|
||||
"ptbtn03.rat", "ptbtn04.rat", "ptbtn05.rat", "ptmsg.t32",
|
||||
];
|
||||
if names == TITLE {
|
||||
return Some(("title", vec![9,11,12,10,13,6,20,19,14,15,18,16,17,0,2,4,7,1,3,5,22,23,21,8]));
|
||||
}
|
||||
if names == SPLASH { return Some(("splash", vec![0,2,4,6,1,3,5])); }
|
||||
if names == MENU {
|
||||
return Some(("main menu", vec![1,3,4,2,5,8,9,6,7,15,10,11,12,13,14,0]));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: paint_order_audit <pak>");
|
||||
let ar = pak::PakArchive::open(&path).expect("open pak");
|
||||
let mut checked = 0;
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).collect();
|
||||
// Every build: how exposed is it to tie-breaking? A tie between
|
||||
// OVERLAPPING elements is where a derived order can go visibly wrong.
|
||||
let keys_all: Vec<u32> = build.elements.iter()
|
||||
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
||||
.collect();
|
||||
let mut tie_pairs = 0;
|
||||
for a in 0..keys_all.len() {
|
||||
for b in (a + 1)..keys_all.len() {
|
||||
if keys_all[a] == keys_all[b] && keys_all[a] != u32::MAX { tie_pairs += 1; }
|
||||
}
|
||||
}
|
||||
// Of the tied pairs, how many OVERLAP? Only those can paint visibly
|
||||
// differently under an arbitrary tie-break. Rect from the declared
|
||||
// pivot (= half the sprite for a .t32) at the resting placement.
|
||||
let rect = |e: &ui_layout::Element| -> Option<(i32,i32,i32,i32)> {
|
||||
let kf = e.rest()?;
|
||||
let (w, h) = ((e.pivot_x * 2) as i32, (e.pivot_y * 2) as i32);
|
||||
if w == 0 || h == 0 { return None; }
|
||||
Some((kf.x, kf.y, w, h))
|
||||
};
|
||||
let mut tie_overlap = 0;
|
||||
for a in 0..keys_all.len() {
|
||||
for b in (a + 1)..keys_all.len() {
|
||||
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
||||
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
||||
else { continue };
|
||||
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
||||
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
||||
if ox > 0 && oy > 0 { tie_overlap += 1; }
|
||||
}
|
||||
}
|
||||
let Some((label, want)) = measured(&names) else {
|
||||
println!("entry {i:2} (no measured order) {} elements, {tie_pairs} tied pairs, \
|
||||
{tie_overlap} of them OVERLAPPING", build.elements.len());
|
||||
// Name them: these are the only pairs whose order can show.
|
||||
for a in 0..keys_all.len() {
|
||||
for b in (a + 1)..keys_all.len() {
|
||||
if keys_all[a] != keys_all[b] || keys_all[a] == u32::MAX { continue; }
|
||||
let (Some(ra), Some(rb)) = (rect(&build.elements[a]), rect(&build.elements[b]))
|
||||
else { continue };
|
||||
let ox = (ra.0 + ra.2).min(rb.0 + rb.2) - ra.0.max(rb.0);
|
||||
let oy = (ra.1 + ra.3).min(rb.1 + rb.3) - ra.1.max(rb.1);
|
||||
if ox > 0 && oy > 0 {
|
||||
println!(" overlapping tie: [{a}] {} x [{b}] {} key {} rect {:?} / {:?} overlap {}x{}",
|
||||
build.elements[a].name, build.elements[b].name, keys_all[a], ra, rb, ox, oy);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
};
|
||||
checked += 1;
|
||||
let got = ui_layout::derived_paint_order(&build, &bytes);
|
||||
let keys: Vec<u32> = build.elements.iter()
|
||||
.map(|e| ui_layout::sprite_layer_key(&build, &bytes, e).unwrap_or(u32::MAX))
|
||||
.collect();
|
||||
let exact = got == want;
|
||||
// How many adjacent pairs in the MEASURED order does derived get wrong,
|
||||
// and of those, how many are between elements sharing a layer key (a
|
||||
// tie the sort cannot resolve) versus a genuine key-order conflict?
|
||||
let pos_got: Vec<usize> = {
|
||||
let mut p = vec![0; got.len()];
|
||||
for (r, &e) in got.iter().enumerate() { p[e] = r; }
|
||||
p
|
||||
};
|
||||
let (mut inv, mut tied) = (0, 0);
|
||||
for a in 0..want.len() {
|
||||
for b in (a + 1)..want.len() {
|
||||
let (x, y) = (want[a], want[b]);
|
||||
if pos_got[x] > pos_got[y] {
|
||||
inv += 1;
|
||||
if keys[x] == keys[y] { tied += 1; }
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("entry {i:2} {label:10} {} elements", want.len());
|
||||
println!(" derived == measured : {}", if exact { "YES" } else { "NO" });
|
||||
println!(" inverted pairs : {inv} (of which same-layer-key ties: {tied})");
|
||||
if !exact {
|
||||
println!(" measured: {want:?}");
|
||||
println!(" derived : {got:?}");
|
||||
println!(" keys : {keys:?}");
|
||||
}
|
||||
}
|
||||
println!("\n{checked} build(s) with a measured order were checked");
|
||||
}
|
||||
52
crates/sylpheed-formats/examples/plateauless_suppression.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Does DRAWING NOTHING beat guessing, for an element with no held pose?
|
||||
//!
|
||||
//! A keyframe group is entry → hold → exit, and the exit ends invisible (on the
|
||||
//! five port screens the final keyframe is invisible for 21/24, 8/16, 12/18, 2/3
|
||||
//! and 6/7 elements). So the screen "as seen" is the HOLD — which is why
|
||||
//! `rest_plateau` is the primary rule. An element with **no** plateau has no
|
||||
//! hold, and `rest()` currently falls back to guessing an endpoint of a movement.
|
||||
//!
|
||||
//! This renders each screen twice — as-is, and with every plateau-less element
|
||||
//! suppressed via `compose`'s `visible` mask — and correlates both against the
|
||||
//! live capture. If suppression wins, the fallback should draw nothing.
|
||||
//! Writes both composites as raw RGBA (`<out>/entryNN_{asis,suppressed}.raw`,
|
||||
//! 1280x720) so the correlation is done outside — this crate has no image
|
||||
//! decoder and the comparison is not worth a dependency.
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let pak_path = std::env::args().nth(1).expect("usage: <GP_TITLE.pak> <outdir> <entry>...");
|
||||
let ar = pak::PakArchive::open(&pak_path).expect("open");
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
let outdir = std::env::args().nth(2).expect("outdir");
|
||||
std::fs::create_dir_all(&outdir).ok();
|
||||
for spec in std::env::args().skip(3) {
|
||||
let idx: usize = spec.parse().unwrap();
|
||||
let bytes = ar.read(&entries[idx]).expect("read");
|
||||
let Some(build) = ui_layout::parse_build(&bytes) else { continue };
|
||||
// plateau-less = rest() had to guess: no two adjacent keyframes share a pose
|
||||
// Default: suppress plateau-less elements. With SUPPRESS_SUBSTR set,
|
||||
// suppress every element whose NAME contains it instead — used to test
|
||||
// the entry→hold→exit model's prediction that the splash glows are all
|
||||
// finished by the moment the logos are up.
|
||||
let by_name = std::env::var("SUPPRESS_SUBSTR").ok();
|
||||
let mask: Vec<bool> = build.elements.iter().map(|e| {
|
||||
if let Some(sub) = &by_name {
|
||||
return !e.name.to_lowercase().contains(sub.as_str());
|
||||
}
|
||||
let k = &e.keyframes;
|
||||
(0..k.len().saturating_sub(1)).any(|i| {
|
||||
k[i].fade == k[i+1].fade && k[i].scale_x == k[i+1].scale_x
|
||||
&& k[i].scale_y == k[i+1].scale_y && k[i].x == k[i+1].x && k[i].y == k[i+1].y
|
||||
})
|
||||
}).collect();
|
||||
let suppressed = mask.iter().filter(|m| !**m).count();
|
||||
let opts = ui_layout::ComposeOptions::default();
|
||||
let a = ui_layout::compose(&build, &bytes, opts, None);
|
||||
let b = ui_layout::compose(&build, &bytes, opts, Some(&mask));
|
||||
std::fs::write(format!("{outdir}/entry{idx:02}_asis.raw"), &a.rgba).unwrap();
|
||||
std::fs::write(format!("{outdir}/entry{idx:02}_suppressed.raw"), &b.rgba).unwrap();
|
||||
println!("entry {idx:2} {}x{} elements {:2} plateau-less suppressed {suppressed}",
|
||||
a.width, a.height, build.elements.len());
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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}"),
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,10 @@ pub mod movie_manifest;
|
||||
|
||||
pub mod movie_voice;
|
||||
|
||||
/// Assembling media whose bytes are not one archive entry — segment-spanning
|
||||
/// reads, multi-sub-wave banks, and the continuous cutscene-voice stream.
|
||||
pub mod media;
|
||||
|
||||
pub mod game_data;
|
||||
|
||||
pub mod localization;
|
||||
|
||||
361
crates/sylpheed-formats/src/media.rs
Normal file
@@ -0,0 +1,361 @@
|
||||
//! Assembling media that does **not** sit in one place on the disc.
|
||||
//!
|
||||
//! Most assets are one archive entry and are read with [`crate::pak`] alone.
|
||||
//! Audio is not, and this module owns every case where the bytes of one playable
|
||||
//! thing have to be gathered from somewhere other than a single entry:
|
||||
//!
|
||||
//! * **An entry spans segment files.** A `.pak` TOC offset addresses the
|
||||
//! *concatenated* `.p00….pNN` stream, so one entry routinely straddles two
|
||||
//! files on disc. [`DiscSource::read_segment_range`] is the seam for that.
|
||||
//! * **A bank holds several sub-waves.** A `.slb` is an XACT bank; its sub-waves
|
||||
//! are either alternate takes or sequential segments of one line, and only
|
||||
//! concatenating them all and clamping to the known length gets both right.
|
||||
//! * **A cutscene voice is not in its own bank.** The movie voices are one
|
||||
//! continuous XMA stream chunked into `VOICE_*.slb` TOC entries whose
|
||||
//! boundaries do **not** match the cutscene cues. A cue routinely spans two
|
||||
//! chunks, so *a `.slb` need not hold the track its name claims*.
|
||||
//! [`resolve_movie_voice_region`] resolves a movie to a byte region of the
|
||||
//! stream instead, which is the only reading that produces the right audio.
|
||||
//!
|
||||
//! ## Why this lives in `sylpheed-formats` and not in a viewer
|
||||
//!
|
||||
//! It used to live in the Bevy viewer, which meant the one piece of logic most
|
||||
//! likely to be re-derived incorrectly was in the crate least likely to be
|
||||
//! reused. Anything that reads the disc — the viewer, a CLI, an asset exporter
|
||||
//! for a port — needs the same answers, and there must be one implementation of
|
||||
//! them.
|
||||
//!
|
||||
//! ## What deliberately stays out
|
||||
//!
|
||||
//! Decoding. This module returns **XMA `RIFF`s**, not PCM: turning XMA into
|
||||
//! samples means shelling out to FFmpeg, which is a native-only dependency and
|
||||
//! a policy decision for the consumer. The seam is "here are the bytes that
|
||||
//! belong together" — everything up to that point is disc knowledge, everything
|
||||
//! after it is a codec choice.
|
||||
|
||||
use crate::pak::PakArchive;
|
||||
use crate::slb::VoiceLang;
|
||||
|
||||
/// Where disc bytes come from. Implemented over an extracted directory, an ISO,
|
||||
/// or anything else that can serve the same three questions.
|
||||
///
|
||||
/// It is a trait rather than a concrete type because the callers differ in ways
|
||||
/// this module should not know about: a viewer reads from whichever source the
|
||||
/// user opened, a headless exporter reads from a fixed extract, and a test reads
|
||||
/// from a fixture.
|
||||
pub trait DiscSource {
|
||||
/// Read a whole file by disc-relative path, e.g. `dat/sound.pak`.
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, String>;
|
||||
|
||||
/// Open an IPFB archive by disc-relative path, with its `.pNN` segments.
|
||||
fn open_pak(&self, path: &str) -> Result<PakArchive, String>;
|
||||
|
||||
/// Read `len` bytes at `offset` into the concatenated `<stem>.p00….pNN`
|
||||
/// stream, where `stem` is a disc-relative path without extension
|
||||
/// (`dat/sound`). The range may cross a segment boundary; that is the point.
|
||||
fn read_segment_range(&self, stem: &str, offset: u64, len: usize) -> Result<Vec<u8>, String>;
|
||||
}
|
||||
|
||||
/// Read one `sound.pak` bank by name-hash, taking only its byte range from the
|
||||
/// segments rather than inflating the 1.07 GB archive.
|
||||
pub fn read_sound_bank<S: DiscSource + ?Sized>(
|
||||
source: &S,
|
||||
name_hash: u32,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let toc = source.read_file("dat/sound.pak")?;
|
||||
let entries = PakArchive::parse_toc(&toc).map_err(|e| e.to_string())?;
|
||||
let idx = entries
|
||||
.binary_search_by_key(&name_hash, |e| e.name_hash)
|
||||
.map_err(|_| "not present in sound.pak".to_string())?;
|
||||
let e = &entries[idx];
|
||||
source.read_segment_range("dat/sound", e.offset as u64, e.comp_size as usize)
|
||||
}
|
||||
|
||||
/// The XMA `RIFF`s of one named bank, in the order they must be concatenated.
|
||||
///
|
||||
/// Every sub-wave is returned, not just the first. The two bank shapes need
|
||||
/// this for opposite reasons: a **segment** bank (`VOICE_RT07A` = 24 s + 14 s +
|
||||
/// 11 s ≈ the 50 s movie) is only complete when all of them are joined, and an
|
||||
/// **alternate-take** bank (`VOICE_S00A`, whose sub-wave 0 already spans the
|
||||
/// whole movie) is trimmed by the caller's length clamp. Taking sub-wave 0 alone
|
||||
/// dropped two thirds of the dialogue on segment banks — that was a real bug.
|
||||
pub fn sound_bank_riffs<S: DiscSource + ?Sized>(
|
||||
source: &S,
|
||||
clip_name: &str,
|
||||
) -> Result<Vec<Vec<u8>>, String> {
|
||||
let bytes = read_sound_bank(source, crate::hash::name_hash(clip_name))?;
|
||||
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>(
|
||||
source: &S,
|
||||
start: u64,
|
||||
end: u64,
|
||||
) -> Result<Vec<Vec<u8>>, String> {
|
||||
let bytes = source.read_segment_range("dat/sound", start, (end - start) as usize)?;
|
||||
Ok(riffs_of(&bytes))
|
||||
}
|
||||
|
||||
/// Sub-wave `RIFF`s of a bank's bytes, with the single-stream fallback.
|
||||
///
|
||||
/// Some banks — the data-before-header `\etc\` radio clips — defeat the
|
||||
/// multi-sub-wave scanner, and the robust single-stream reader handles them. An
|
||||
/// empty result here means genuinely undecodable, not "scanner confused".
|
||||
fn riffs_of(bytes: &[u8]) -> Vec<Vec<u8>> {
|
||||
let riffs = crate::slb::to_xma_riffs(bytes);
|
||||
if riffs.is_empty() {
|
||||
crate::slb::to_xma_riff_best(bytes).into_iter().collect()
|
||||
} else {
|
||||
riffs
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a movie's voice bank **name** through the manifest in `tables.pak`.
|
||||
///
|
||||
/// Only the manifest's DIRECT bindings are trusted. Extending this to unbound
|
||||
/// resupply movies by shared demo line was tried and verified WRONG — it played
|
||||
/// the wrong recording — so an unbound movie stays unvoiced rather than play a
|
||||
/// guess. `None` therefore means "this cutscene has no voice-over", which is a
|
||||
/// real answer for most `hokyu_*` movies.
|
||||
pub fn resolve_movie_voice_clip<S: DiscSource + ?Sized>(
|
||||
source: &S,
|
||||
movie: &str,
|
||||
lang: VoiceLang,
|
||||
) -> Option<String> {
|
||||
let pak = source.open_pak("dat/tables.pak").ok()?;
|
||||
let manifest = find_manifest(&pak)?;
|
||||
let sounds = pak
|
||||
.read_by_name(&format!("{}\\sounds.tbl", lang.code_pub()))?
|
||||
.ok()?;
|
||||
crate::movie_manifest::resolve_voice_entry(&manifest, &sounds, movie, lang)
|
||||
}
|
||||
|
||||
/// The manifest has no stable name, so it is found by shape among the entries.
|
||||
fn find_manifest(pak: &PakArchive) -> Option<Vec<u8>> {
|
||||
pak.entries().iter().find_map(|e| {
|
||||
pak.read(e)
|
||||
.ok()
|
||||
.filter(|b| crate::movie_manifest::is_manifest(b))
|
||||
})
|
||||
}
|
||||
|
||||
/// Voice token for a hokyu (resupply) cutscene the manifest leaves unbound.
|
||||
///
|
||||
/// Only 5 of the 18 hokyu movies carry an explicit `VOICETRACK`; the rest reuse
|
||||
/// those recordings. The selector is the cutscene's **demo id** (from its
|
||||
/// subtitle track), NOT the ship category: `hokyu_LS_s02A` and `hokyu_LS_s11A`
|
||||
/// are both LS/carrier but use demos 600 vs 601, whose lines differ. So the map
|
||||
/// is derived from the 5 bound hokyu — each of which has both a subtitle demo id
|
||||
/// and a `VOICETRACK` — and the target movie's demo id is looked up in it.
|
||||
pub fn hokyu_voice_token<S: DiscSource + ?Sized>(
|
||||
source: &S,
|
||||
movie: &str,
|
||||
lang: VoiceLang,
|
||||
manifest: &[u8],
|
||||
) -> Option<String> {
|
||||
use crate::movie_subtitle as ms;
|
||||
if !movie.starts_with("hokyu_") {
|
||||
return None;
|
||||
}
|
||||
let lang_pak = source
|
||||
.open_pak(&format!("dat/movie/{}.pak", lang.code_pub()))
|
||||
.ok()?;
|
||||
let want = ms::track_voice_cues(&lang_pak, movie).first().map(|&(d, _)| d)?;
|
||||
crate::movie_manifest::parse(manifest)
|
||||
.into_iter()
|
||||
.find_map(|e| {
|
||||
let tok = e.voice_token.filter(|_| e.movie.starts_with("hokyu_"))?;
|
||||
ms::track_voice_cues(&lang_pak, &e.movie)
|
||||
.iter()
|
||||
.any(|&(d, _)| d == want)
|
||||
.then_some(tok)
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve a movie's cutscene voice to a continuous `[start, end)` byte region
|
||||
/// of the voice stream — **the reading that produces the right audio**.
|
||||
///
|
||||
/// The chain is movie → cue token (manifest) → sound id (master registry) →
|
||||
/// region (scan the stream for two trailers). Each cue ends at an inline
|
||||
/// `(sound_id, 0x11, …)` trailer, so cue *N* is the bytes between trailer *N-1*
|
||||
/// and trailer *N*.
|
||||
///
|
||||
/// Returns `None` for movies whose voice is not a `\Movie\` bank — the hokyu
|
||||
/// `\etc\` clips — which the caller then resolves the per-clip way via
|
||||
/// [`resolve_movie_voice_clip`].
|
||||
pub fn resolve_movie_voice_region<S: DiscSource + ?Sized>(
|
||||
source: &S,
|
||||
movie: &str,
|
||||
lang: VoiceLang,
|
||||
) -> Option<(u64, u64)> {
|
||||
use crate::{hash::name_hash, movie_manifest, movie_voice};
|
||||
let code = lang.code_pub();
|
||||
let tpak = source.open_pak("dat/tables.pak").ok()?;
|
||||
let manifest = find_manifest(&tpak)?;
|
||||
let token = movie_manifest::voice_token(&manifest, movie)
|
||||
.or_else(|| hokyu_voice_token(source, movie, lang, &manifest))?;
|
||||
|
||||
// token → sound id, via the large per-language IDXD entry carrying the
|
||||
// `<lang>\Movie\VOICE_*.slb` paths. Located by content, like the manifest.
|
||||
let marker = format!("{code}\\Movie\\VOICE_ADV.slb");
|
||||
let registry = tpak.entries().iter().find_map(|e| {
|
||||
tpak.read(e)
|
||||
.ok()
|
||||
.filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes()))
|
||||
})?;
|
||||
let id = *movie_voice::registry_voice_ids(®istry).get(&token)?;
|
||||
|
||||
// Physical anchor: the TOC offset of this token's own `.slb` chunk. That is a
|
||||
// start point NEAR the cue's trailers, not the cue itself — the cue may sit
|
||||
// before or after it, which is the whole reason a region is needed. The
|
||||
// token's subdirectory varies by kind.
|
||||
let stoc = source.read_file("dat/sound.pak").ok()?;
|
||||
let entries = PakArchive::parse_toc(&stoc).ok()?;
|
||||
let anchor = ["Movie", "etc", "Voice"].iter().find_map(|dir| {
|
||||
let h = name_hash(&format!("{code}\\{dir}\\{token}.slb"));
|
||||
entries
|
||||
.binary_search_by_key(&h, |e| e.name_hash)
|
||||
.ok()
|
||||
.map(|i| entries[i].offset as u64)
|
||||
})?;
|
||||
|
||||
// Scan both directions from the anchor. The window must span the largest
|
||||
// bank (ADV ≈ 3.6 MB) or the predecessor trailer falls outside it.
|
||||
let win_start = anchor.saturating_sub(2 * 1024 * 1024) & !3;
|
||||
let window = source
|
||||
.read_segment_range("dat/sound", win_start, 8 * 1024 * 1024)
|
||||
.ok()?;
|
||||
let end_local = movie_voice::find_descriptor(&window, id)?;
|
||||
let end = win_start + end_local as u64;
|
||||
|
||||
// Start = the predecessor trailer. Prefer the exact `id-1`; where the id
|
||||
// sequence has a gap (VOICE_D_453 → 454) fall back to the nearest trailer
|
||||
// below — but only within one bank (~1.5 MB), else this is the first cue in
|
||||
// its block and the audio starts at the anchor itself.
|
||||
let start = movie_voice::find_descriptor(&window, id.wrapping_sub(1))
|
||||
.or_else(|| movie_voice::find_descriptor_before(&window, end_local))
|
||||
.map(|o| win_start + o as u64)
|
||||
.filter(|&s| s < end && end - s < 1_500_000)
|
||||
.unwrap_or(anchor);
|
||||
Some((start, end))
|
||||
}
|
||||
|
||||
/// A [`DiscSource`] over an **extracted** disc directory.
|
||||
///
|
||||
/// Provided here rather than left to each caller because every headless
|
||||
/// consumer — the CLI, the disc tests, an asset exporter for a port — wants
|
||||
/// exactly this and would otherwise re-derive the segment-spanning read, which
|
||||
/// is the part that is easy to get subtly wrong.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct DirectorySource {
|
||||
root: std::path::PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl DirectorySource {
|
||||
pub fn new(root: impl Into<std::path::PathBuf>) -> Self {
|
||||
Self { root: root.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl DiscSource for DirectorySource {
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, String> {
|
||||
std::fs::read(self.root.join(path)).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
fn open_pak(&self, path: &str) -> Result<PakArchive, String> {
|
||||
PakArchive::open(self.root.join(path)).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
/// Walks `<stem>.p00`, `.p01`, … skipping whole segments until the offset is
|
||||
/// inside one, then reads across as many as the length needs. A range that
|
||||
/// straddles a boundary is the normal case, not an edge case.
|
||||
fn read_segment_range(&self, stem: &str, offset: u64, len: usize) -> Result<Vec<u8>, String> {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
let mut out = Vec::with_capacity(len);
|
||||
let (mut skip, mut need) = (offset, len);
|
||||
for i in 0..100u32 {
|
||||
if need == 0 {
|
||||
break;
|
||||
}
|
||||
let path = self.root.join(format!("{stem}.p{i:02}"));
|
||||
let Ok(meta) = std::fs::metadata(&path) else { break };
|
||||
let seg_len = meta.len();
|
||||
if skip >= seg_len {
|
||||
skip -= seg_len;
|
||||
continue;
|
||||
}
|
||||
let mut f = std::fs::File::open(&path).map_err(|e| e.to_string())?;
|
||||
f.seek(SeekFrom::Start(skip)).map_err(|e| e.to_string())?;
|
||||
let take = need.min((seg_len - skip) as usize);
|
||||
let start = out.len();
|
||||
out.resize(start + take, 0);
|
||||
f.read_exact(&mut out[start..]).map_err(|e| e.to_string())?;
|
||||
need -= take;
|
||||
skip = 0;
|
||||
}
|
||||
if need != 0 {
|
||||
return Err(format!("segment range short by {need} bytes"));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,10 @@ use crate::pak::PakArchive;
|
||||
|
||||
/// Subtitle language. `pak_code` selects `dat/movie/<code>.pak`; `game_code`
|
||||
/// selects `dat/GP_MAIN_GAME_<code>.pak` (the caption text).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum SubLang {
|
||||
/// Default only because the disc's own default is English.
|
||||
#[default]
|
||||
English,
|
||||
Japanese,
|
||||
German,
|
||||
|
||||
@@ -225,16 +225,34 @@ impl PakArchive {
|
||||
}
|
||||
|
||||
/// The raw stored bytes for an entry (still `"Z1"`-wrapped / compressed).
|
||||
///
|
||||
/// One entry on the retail disc declares more bytes than the segments hold:
|
||||
/// `sound.pak`'s `Static.slb` (the SFX bank) claims 8 970 240 bytes at the
|
||||
/// highest offset in the archive, 616 768 past the end of `sound.p04`. It is
|
||||
/// not corruption and it is not our extraction — `sound.p04` is byte-for-byte
|
||||
/// the size the ISO's own directory record gives, and a sweep of **every**
|
||||
/// `.pak` on the disc finds this one entry and no other. So the last entry's
|
||||
/// `comp_size` is an allocation size, not a stored size.
|
||||
///
|
||||
/// A short read is therefore allowed **only** for the highest-offset entry,
|
||||
/// which is the shape the evidence supports. Any other overrun is still an
|
||||
/// error: that would be real damage, and clamping it would hide the damage
|
||||
/// behind a half-decoded asset.
|
||||
pub fn stored_bytes(&self, entry: &PakEntry) -> Result<&[u8], PakError> {
|
||||
let start = entry.offset as usize;
|
||||
let end = start + entry.comp_size as usize;
|
||||
self.data
|
||||
.get(start..end)
|
||||
.ok_or(PakError::OffsetOutOfRange {
|
||||
offset: entry.offset,
|
||||
size: entry.comp_size,
|
||||
data_len: self.data.len(),
|
||||
})
|
||||
if let Some(b) = self.data.get(start..end) {
|
||||
return Ok(b);
|
||||
}
|
||||
let is_tail = self.entries.iter().all(|e| e.offset <= entry.offset);
|
||||
if is_tail && start < self.data.len() {
|
||||
return Ok(&self.data[start..]);
|
||||
}
|
||||
Err(PakError::OffsetOutOfRange {
|
||||
offset: entry.offset,
|
||||
size: entry.comp_size,
|
||||
data_len: self.data.len(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Decompress an entry to its raw payload bytes. Handles the `"Z1"` container
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -29,13 +29,25 @@ pub const XMA1_PACKET: usize = 2048;
|
||||
|
||||
/// Voice language for cutscene audio. Only English and Japanese voice exist on
|
||||
/// the disc (subtitles cover more languages, voice does not).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum VoiceLang {
|
||||
/// The default only because the disc's own default audio track is English;
|
||||
/// nothing else about the code should assume it.
|
||||
#[default]
|
||||
English,
|
||||
Japanese,
|
||||
}
|
||||
|
||||
impl VoiceLang {
|
||||
pub const ALL: [VoiceLang; 2] = [VoiceLang::English, VoiceLang::Japanese];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
VoiceLang::English => "English",
|
||||
VoiceLang::Japanese => "Japanese",
|
||||
}
|
||||
}
|
||||
|
||||
fn code(self) -> &'static str {
|
||||
match self {
|
||||
VoiceLang::English => "eng",
|
||||
@@ -67,14 +79,122 @@ pub struct VoiceClip {
|
||||
pub display: String,
|
||||
}
|
||||
|
||||
/// Enumerate the voice/dialog clips named in a decompressed `sounds.tbl` (the
|
||||
/// IDXD in `tables.pak`). Extracts every `<lang>\{Voice,etc,Movie,Briefing}\…`
|
||||
/// path ending in `.slb` for `lang`, parsed into `(name, speaker, display)`.
|
||||
pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
||||
/// What kind of audio a `sounds.tbl` entry names.
|
||||
///
|
||||
/// The split is the on-disc path shape, not a guess: the 36 language-independent
|
||||
/// banks sit at the table root (`BGM_###.slb`, `JNGL_00#.slb`, `Static.slb`),
|
||||
/// while everything else is under `<lang>\<dir>\`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum AudioCategory {
|
||||
/// `BGM_###.slb` — 32 music tracks, language-independent.
|
||||
Music,
|
||||
/// `JNGL_00#.slb` — 3 short jingles (mission clear / fail stings).
|
||||
Jingle,
|
||||
/// `Static.slb` — the sound-effect bank, one 9 MB multi-wave bank.
|
||||
Sfx,
|
||||
/// `<lang>\Voice\` — in-mission radio chatter, by speaker.
|
||||
Radio,
|
||||
/// `<lang>\etc\` — the other spoken lines (cutscene dialogue, system).
|
||||
Dialogue,
|
||||
/// `<lang>\Movie\VOICE_<movie>.slb` — a cutscene's continuous voice track.
|
||||
MovieVoice,
|
||||
/// `<lang>\Briefing\BR<NN>_<MM>.slb` — mission briefing lines.
|
||||
Briefing,
|
||||
/// A `.slb` whose path matched no known shape.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl AudioCategory {
|
||||
pub const ALL: [AudioCategory; 8] = [
|
||||
AudioCategory::Music,
|
||||
AudioCategory::Jingle,
|
||||
AudioCategory::Sfx,
|
||||
AudioCategory::Radio,
|
||||
AudioCategory::Dialogue,
|
||||
AudioCategory::MovieVoice,
|
||||
AudioCategory::Briefing,
|
||||
AudioCategory::Other,
|
||||
];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
AudioCategory::Music => "Music",
|
||||
AudioCategory::Jingle => "Jingles",
|
||||
AudioCategory::Sfx => "Sound effects",
|
||||
AudioCategory::Radio => "Radio",
|
||||
AudioCategory::Dialogue => "Dialogue",
|
||||
AudioCategory::MovieVoice => "Movie voice",
|
||||
AudioCategory::Briefing => "Briefing",
|
||||
AudioCategory::Other => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
/// True for the categories that are spoken lines — the set
|
||||
/// [`list_voice_clips`] returns.
|
||||
pub fn is_voice(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
AudioCategory::Radio
|
||||
| AudioCategory::Dialogue
|
||||
| AudioCategory::MovieVoice
|
||||
| AudioCategory::Briefing
|
||||
)
|
||||
}
|
||||
|
||||
/// True when the bank is language-independent, so it appears whichever
|
||||
/// `<lang>\sounds.tbl` is read.
|
||||
pub fn is_shared(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
AudioCategory::Music | AudioCategory::Jingle | AudioCategory::Sfx
|
||||
)
|
||||
}
|
||||
|
||||
fn classify(name: &str) -> AudioCategory {
|
||||
let leaf = name.rsplit('\\').next().unwrap_or(name);
|
||||
if !name.contains('\\') {
|
||||
return if leaf.starts_with("BGM_") {
|
||||
AudioCategory::Music
|
||||
} else if leaf.starts_with("JNGL_") {
|
||||
AudioCategory::Jingle
|
||||
} else if leaf.eq_ignore_ascii_case("Static.slb") {
|
||||
AudioCategory::Sfx
|
||||
} else {
|
||||
AudioCategory::Other
|
||||
};
|
||||
}
|
||||
match name.rsplit('\\').nth(1) {
|
||||
Some("Voice") => AudioCategory::Radio,
|
||||
Some("etc") => AudioCategory::Dialogue,
|
||||
Some("Movie") => AudioCategory::MovieVoice,
|
||||
Some("Briefing") => AudioCategory::Briefing,
|
||||
_ => AudioCategory::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One playable bank named in `sounds.tbl`, with the category its path implies.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AudioEntry {
|
||||
pub clip: VoiceClip,
|
||||
pub category: AudioCategory,
|
||||
}
|
||||
|
||||
/// Enumerate **every** `.slb` bank named in a decompressed `sounds.tbl` (the
|
||||
/// IDXD in `tables.pak`): the language-independent music/jingle/SFX banks at
|
||||
/// the table root, plus every `<lang>\…` spoken line.
|
||||
///
|
||||
/// Measured on the retail disc: `eng\sounds.tbl` names 4 418 banks (36 shared +
|
||||
/// 2 382 Radio + 1 821 Dialogue + 101 Briefing + 78 Movie voice) and
|
||||
/// `jpn\sounds.tbl` names 5 136 (the same 36 shared + 5 100 Japanese lines).
|
||||
/// Every one of the 36 shared names resolves to a `sound.pak` TOC entry under
|
||||
/// [`crate::hash::name_hash`], which is the check that they are real banks and
|
||||
/// not stale table text.
|
||||
pub fn list_audio_entries(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<AudioEntry> {
|
||||
let prefix = format!("{}\\", lang.code());
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
let mut out = Vec::new();
|
||||
// Scan for printable-ASCII runs; keep those that look like a voice path.
|
||||
// Scan for printable-ASCII runs; keep those that name a `.slb`.
|
||||
let mut i = 0;
|
||||
while i < sounds_tbl.len() {
|
||||
let start = i;
|
||||
@@ -83,15 +203,14 @@ pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
||||
}
|
||||
if i - start >= 6 {
|
||||
if let Ok(s) = std::str::from_utf8(&sounds_tbl[start..i]) {
|
||||
// Every spoken-line category, so the standalone player covers them
|
||||
// all: in-mission radio (`\Voice\`, `\etc\`) and bound movie voices
|
||||
// (`\Movie\`) all carry `VOICE_`; mission-briefing lines live in
|
||||
// `\Briefing\` as `BR<NN>_<MM>.slb` (no `VOICE` in the name).
|
||||
let is_voice = s.contains("VOICE") || s.contains("\\Briefing\\");
|
||||
if s.starts_with(&prefix) && s.ends_with(".slb") && is_voice {
|
||||
if seen.insert(s.to_string()) {
|
||||
out.push(parse_voice_clip(s));
|
||||
}
|
||||
// Take this language's entries plus the root (shared) banks; a
|
||||
// path under the OTHER language would be a table artefact.
|
||||
let mine = s.starts_with(&prefix) || !s.contains('\\');
|
||||
if mine && s.ends_with(".slb") && seen.insert(s.to_string()) {
|
||||
out.push(AudioEntry {
|
||||
category: AudioCategory::classify(s),
|
||||
clip: parse_voice_clip(s),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,6 +219,21 @@ pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Enumerate just the spoken-line clips — [`list_audio_entries`] restricted to
|
||||
/// [`AudioCategory::is_voice`].
|
||||
///
|
||||
/// In-mission radio (`\Voice\`, `\etc\`) and bound movie voices (`\Movie\`) all
|
||||
/// carry `VOICE_`; mission-briefing lines live in `\Briefing\` as
|
||||
/// `BR<NN>_<MM>.slb` and carry no `VOICE` at all, which is why the category —
|
||||
/// i.e. the directory — decides this and not the filename.
|
||||
pub fn list_voice_clips(sounds_tbl: &[u8], lang: VoiceLang) -> Vec<VoiceClip> {
|
||||
list_audio_entries(sounds_tbl, lang)
|
||||
.into_iter()
|
||||
.filter(|e| e.category.is_voice())
|
||||
.map(|e| e.clip)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_voice_clip(name: &str) -> VoiceClip {
|
||||
// `<lang>\<cat>\VOICE_<SPK>_<NNN>.slb` or `..\VOICE_<movie>.slb`.
|
||||
let stem = name
|
||||
@@ -401,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);
|
||||
@@ -525,6 +681,56 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_audio_entries_categorises_root_banks_and_keeps_them_language_shared() {
|
||||
// The three root banks carry no language component, so BOTH sounds.tbl
|
||||
// files name them; a language filter that only accepted `<lang>\` would
|
||||
// silently drop all the music, which is what it used to do.
|
||||
let mut tbl = Vec::new();
|
||||
for s in [
|
||||
"BGM_001.slb",
|
||||
"JNGL_002.slb",
|
||||
"Static.slb",
|
||||
"eng\\Voice\\VOICE_ADAN_010.slb",
|
||||
"eng\\etc\\VOICE_D_450.slb",
|
||||
"eng\\Movie\\VOICE_S13A.slb",
|
||||
"eng\\Briefing\\BR01_01.slb",
|
||||
] {
|
||||
tbl.extend_from_slice(s.as_bytes());
|
||||
tbl.push(0);
|
||||
}
|
||||
let by = |lang| {
|
||||
list_audio_entries(&tbl, lang)
|
||||
.into_iter()
|
||||
.map(|e| (e.clip.name, e.category))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let eng = by(VoiceLang::English);
|
||||
let want = [
|
||||
("BGM_001.slb", AudioCategory::Music),
|
||||
("JNGL_002.slb", AudioCategory::Jingle),
|
||||
("Static.slb", AudioCategory::Sfx),
|
||||
("eng\\Voice\\VOICE_ADAN_010.slb", AudioCategory::Radio),
|
||||
("eng\\etc\\VOICE_D_450.slb", AudioCategory::Dialogue),
|
||||
("eng\\Movie\\VOICE_S13A.slb", AudioCategory::MovieVoice),
|
||||
("eng\\Briefing\\BR01_01.slb", AudioCategory::Briefing),
|
||||
];
|
||||
assert_eq!(eng.len(), want.len());
|
||||
for (n, c) in want {
|
||||
assert!(
|
||||
eng.iter().any(|(en, ec)| en == n && *ec == c),
|
||||
"{n} not categorised as {c:?}"
|
||||
);
|
||||
}
|
||||
// Reading the Japanese table yields the shared banks and none of the
|
||||
// English lines.
|
||||
let jpn = by(VoiceLang::Japanese);
|
||||
assert_eq!(jpn.len(), 3, "only the shared banks: {jpn:?}");
|
||||
assert!(jpn.iter().all(|(_, c)| c.is_shared()));
|
||||
// And the voice view is exactly the non-shared half.
|
||||
assert_eq!(list_voice_clips(&tbl, VoiceLang::English).len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_riff_from_headerless() {
|
||||
let mut slb = vec![0u8; HEADERLESS_DATA_OFFSET];
|
||||
|
||||
@@ -38,6 +38,18 @@ pub struct T8adImage {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgba: Vec<u8>,
|
||||
/// The header word at `+0x04`. A flag word; **bit `0x02`** is a candidate
|
||||
/// blend selector — it separates the title's effect sprites from its ordinary
|
||||
/// ones exactly, and disc-wide it toggles independently of the rest of the
|
||||
/// word in 27.1 % of 19 216 sprites. See
|
||||
/// `docs/re/structures/ui-paint-order-key.md`.
|
||||
///
|
||||
/// ⚠️ **Additive was tested and REFUTED.** Blending bit-`0x02` sprites
|
||||
/// additively moved every measure against the title capture the wrong way:
|
||||
/// whole-frame mean diff +0.55 → +1.04, swoosh-band mean +1.83 → +3.98, band
|
||||
/// edge-correlation 0.6971 → 0.5578. The bit is real and independent, but it
|
||||
/// does not select an additive blend. Carried, not acted on.
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
/// Whether `bytes` starts with the T8aD magic.
|
||||
@@ -108,6 +120,7 @@ pub fn parse(bytes: &[u8]) -> Option<T8adImage> {
|
||||
}
|
||||
}
|
||||
Some(T8adImage {
|
||||
flags: be32(bytes, 4),
|
||||
width: width as u32,
|
||||
height: height as u32,
|
||||
rgba,
|
||||
|
||||
@@ -59,9 +59,9 @@ const DESIGN_H: u32 = 720;
|
||||
///
|
||||
/// ```text
|
||||
/// +0 u32 ARGB fade colour — alpha ramps 0x00 → 0x80 → 0xd5 … over the group
|
||||
/// +4 u32 0
|
||||
/// +8 u32 0
|
||||
/// +12 u32 0
|
||||
/// +4 i32 ⚠️ NOT always 0 — see below
|
||||
/// +8 i32 ⚠️ NOT always 0 — see below
|
||||
/// +12 i32 ⚠️ NOT always 0 — see below
|
||||
/// +16 u32 scale X, percent
|
||||
/// +20 u32 scale Y, percent
|
||||
/// +24 u32 tint (0xffff_ffff on every frame seen)
|
||||
@@ -69,10 +69,39 @@ const DESIGN_H: u32 = 720;
|
||||
/// +32 i32 Y ← signed
|
||||
/// +36 u32 time
|
||||
/// ```
|
||||
///
|
||||
/// ## `+12` is the screen-plane ROTATION, in degrees (2026-08-28)
|
||||
///
|
||||
/// ✅ **Measured against the framebuffer, not against our own renderer.** The
|
||||
/// title's two light sweeps are the nested leaf records `ptloop01.rat` /
|
||||
/// `ptloop02.rat`, and their keyframe blocks read `+12` = `30` and `-45`. A
|
||||
/// `log_ui_draws` capture of the live title submits those two quads rotated by
|
||||
/// **+30.26°** and **-45.28°** — magnitude *and* sign, on two different values.
|
||||
/// Positive is clockwise in screen space (Y down).
|
||||
///
|
||||
/// `+4` and `+8` are 🟡 still unexplained: signed, non-zero in ~4.7 % / 4.6 %
|
||||
/// of blocks disc-wide, dominated by `±180` and `±90`. Plausibly rotation about
|
||||
/// the other two axes, but nothing observed turns on them.
|
||||
///
|
||||
/// ⚠️ **`rotation_deg` is decoded but NOT rendered.** [`crate::ui_layout`]'s
|
||||
/// blitter draws axis-aligned quads only, so `screen render` still paints a
|
||||
/// rotated element upright. See `docs/re/ui-title-build-map.md`.
|
||||
///
|
||||
/// ⚠️ The earlier note here — *"every element of `GP_TITLE` build 4 has all
|
||||
/// three at zero"* — was **wrong about reach, not about the bytes**: build 4's
|
||||
/// top-level elements do read zero, but the rotated quads come from its two
|
||||
/// **nested** `.rat` leaf records, which the census never opened.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Keyframe {
|
||||
/// The fade colour, ARGB. Its alpha is what ramps an element in.
|
||||
pub fade: u32,
|
||||
/// Screen-plane rotation in **degrees**, clockwise-positive (`+12`).
|
||||
/// Confirmed against a GPU capture; see the type's docs. Not rendered.
|
||||
pub rotation_deg: i32,
|
||||
/// `+4` / `+8` — signed, meaning unexplained. Carried rather than dropped
|
||||
/// so a consumer can see them instead of assuming they are zero.
|
||||
pub unknown_4: i32,
|
||||
pub unknown_8: i32,
|
||||
/// Scale in percent (100 = 1:1).
|
||||
pub scale_x: u32,
|
||||
pub scale_y: u32,
|
||||
@@ -154,6 +183,15 @@ impl Element {
|
||||
/// Falls back to the longest-dwell rule when no two adjacent keyframes
|
||||
/// agree — a group that ramps through every frame and never holds.
|
||||
pub fn rest(&self) -> Option<&Keyframe> {
|
||||
// `lastall`: the LAST keyframe for every element, bypassing the plateau
|
||||
// rule entirely. This is what the shifted time reading predicts — under
|
||||
// it the final pose is reached at a definite time and nothing follows,
|
||||
// so "rest" needs no heuristic. Testing it against the captures is an
|
||||
// independent check on that reading, from static composites rather than
|
||||
// from animation timing.
|
||||
if std::env::var("SYLPHEED_REST_RULE").as_deref() == Ok("lastall") {
|
||||
return self.keyframes.last();
|
||||
}
|
||||
if let Some(k) = self.rest_plateau() {
|
||||
return Some(k);
|
||||
}
|
||||
@@ -161,6 +199,26 @@ impl Element {
|
||||
0 => None,
|
||||
1 => self.keyframes.first(),
|
||||
n => {
|
||||
// ⚠️ EXPERIMENT GATE, default off. Both published alternatives to
|
||||
// the longest-dwell fallback died by argument rather than by
|
||||
// measurement, and `compose` can score a rule against the live
|
||||
// captures — so they are reachable here to be tested.
|
||||
// SYLPHEED_REST_RULE=last -> the final keyframe
|
||||
// SYLPHEED_REST_RULE=maxalpha -> the most opaque keyframe
|
||||
match std::env::var("SYLPHEED_REST_RULE").as_deref() {
|
||||
Ok("last") => return self.keyframes.last(),
|
||||
Ok("maxalpha") => {
|
||||
let mut best = (0usize, 0u32);
|
||||
for (k, f) in self.keyframes.iter().enumerate() {
|
||||
let a = (f.fade >> 24) & 0xff;
|
||||
if a >= best.1 {
|
||||
best = (k, a);
|
||||
}
|
||||
}
|
||||
return self.keyframes.get(best.0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut best = (0usize, 0u32);
|
||||
for k in 0..n - 1 {
|
||||
let (Some(t0), Some(t1)) =
|
||||
@@ -227,7 +285,24 @@ impl Element {
|
||||
any = Some(i);
|
||||
any_len = len;
|
||||
}
|
||||
if j != n - 1 && len >= best_len {
|
||||
// A trailing run is normally the EXIT and is excluded (see the
|
||||
// doc comment) — but not always, and the tell is its ALPHA.
|
||||
//
|
||||
// An exit fades the element out, so its final keyframe is
|
||||
// transparent: `pgptitle.rat`'s trailing run is `0x00ffffff`, and
|
||||
// taking it erases the word PAUSE. An element with **no exit**
|
||||
// ends on its hold, which is whatever it looks like on screen —
|
||||
// opaque: `ptframe1`/`ptframe2` on the title main menu end on a
|
||||
// run of three at `0xffffffff`, and excluding it dropped the
|
||||
// bright circuit bracket the capture plainly shows.
|
||||
//
|
||||
// So a trailing run is the hold exactly when it is **visible**.
|
||||
// (The port agent's report proposed "the final untimed keyframe
|
||||
// has the same pose as the last timed one"; that is true of
|
||||
// `pgptitle` too and would erase PAUSE, so it is the alpha and
|
||||
// not the pose-equality that separates the two.)
|
||||
let trailing_is_the_hold = j == n - 1 && (self.keyframes[i].fade >> 24) != 0;
|
||||
if (j != n - 1 || trailing_is_the_hold) && len >= best_len {
|
||||
best = Some(i);
|
||||
best_len = len;
|
||||
}
|
||||
@@ -247,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
|
||||
@@ -404,6 +493,8 @@ fn mark_focused_states(elements: &mut [Element]) {
|
||||
/// Read the placement region that follows the declaration table, filling in each
|
||||
/// element's keyframe group.
|
||||
fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
||||
// Experiment gate, default off; see the `time` field below.
|
||||
let shift_times = std::env::var("SYLPHEED_KF_TIME_SHIFT").as_deref() == Ok("1");
|
||||
let count = elements.len();
|
||||
let mut order = Vec::with_capacity(count);
|
||||
let mut pos = DECL_TABLE_AT + count * DECL_ENTRY;
|
||||
@@ -430,13 +521,25 @@ fn parse_placements(bundle: &[u8], elements: &mut [Element]) -> Vec<usize> {
|
||||
}
|
||||
group.push(Keyframe {
|
||||
fade: be32(bundle, blk),
|
||||
rotation_deg: be32(bundle, blk + 12) as i32,
|
||||
unknown_4: be32(bundle, blk + 4) as i32,
|
||||
unknown_8: be32(bundle, blk + 8) as i32,
|
||||
scale_x: be32(bundle, blk + 16),
|
||||
scale_y: be32(bundle, blk + 20),
|
||||
tint: be32(bundle, blk + 24),
|
||||
x: be32(bundle, blk + 28) as i32,
|
||||
y: be32(bundle, blk + 32) as i32,
|
||||
// Only a block wholly inside the group carries a time.
|
||||
time: (blk + 40 <= group_end).then(|| be32(bundle, blk + 36)),
|
||||
//
|
||||
// ⚠️ Which block a time word BELONGS TO is under test — see
|
||||
// `docs/re/ui-keyframe-time-unit.md`. Set `SYLPHEED_KF_TIME_SHIFT=1`
|
||||
// to read `W[k-1]` as block `k`'s time ("the word is the time the
|
||||
// NEXT pose is reached") instead of `W[k]`. Default is unchanged.
|
||||
time: if shift_times {
|
||||
(k >= 1).then(|| be32(bundle, blk - KEYFRAME + 36))
|
||||
} else {
|
||||
(blk + 40 <= group_end).then(|| be32(bundle, blk + 36))
|
||||
},
|
||||
});
|
||||
}
|
||||
elements[idx].keyframes = group;
|
||||
@@ -510,6 +613,7 @@ pub fn parse_build(bundle: &[u8]) -> Option<UiBuild> {
|
||||
design_h,
|
||||
elements,
|
||||
sprites,
|
||||
records,
|
||||
context_hint,
|
||||
from_fallback,
|
||||
placement_order,
|
||||
@@ -539,6 +643,9 @@ fn fallback_elements(bundle: &[u8], records: &HashMap<String, (usize, usize)>) -
|
||||
pivot_y: be32(rec, 0x54),
|
||||
keyframes: vec![Keyframe {
|
||||
fade: 0xffff_ffff,
|
||||
rotation_deg: 0,
|
||||
unknown_4: 0,
|
||||
unknown_8: 0,
|
||||
scale_x: 100,
|
||||
scale_y: 100,
|
||||
tint,
|
||||
@@ -811,8 +918,16 @@ pub fn compose(
|
||||
// Measured paint order when one exists for this build, declaration order
|
||||
// otherwise — see `measured_paint_order`.
|
||||
// Measured order when this build is one of the two read off the running
|
||||
// game; otherwise the order DERIVED from the sprites' layer keys, which
|
||||
// reproduces both measured orders up to ties.
|
||||
// game; otherwise the order DERIVED from the sprites' layer keys.
|
||||
//
|
||||
// ✅ Checked 2026-08-29 (`examples/paint_order_audit.rs`), because the
|
||||
// previous wording here — "reproduces both measured orders up to ties" —
|
||||
// was unmeasured and stale by one: there are THREE measured orders. The
|
||||
// derived order reproduces the main menu and the developer splash EXACTLY
|
||||
// (0 inverted pairs each) and differs on the title by 8 pairs, all of them
|
||||
// same-layer-key ties, two being total occlusions. Of the port's five
|
||||
// screens only `EXTRAS` rests on a derived order with ties: 15 tied pairs,
|
||||
// 2 overlapping. See docs/re/structures/ui-paint-order-derived-check.md.
|
||||
let order: Vec<usize> =
|
||||
measured_paint_order(build).unwrap_or_else(|| derived_paint_order(build, bundle));
|
||||
for &ei in &order {
|
||||
@@ -867,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 {
|
||||
@@ -919,8 +1041,11 @@ fn fill_quad(
|
||||
if sw == 0 || sh == 0 {
|
||||
return false;
|
||||
}
|
||||
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
|
||||
let sy_pct = if kf.scale_y == 0 { 100 } else { kf.scale_y };
|
||||
// `scale = 0` is COLLAPSED TO NOTHING, not "unset" — see `blit`.
|
||||
let (sx_pct, sy_pct) = (kf.scale_x, kf.scale_y);
|
||||
if sx_pct == 0 || sy_pct == 0 {
|
||||
return false;
|
||||
}
|
||||
let dw = (sw * sx_pct / 100).max(1);
|
||||
let dh = (sh * sy_pct / 100).max(1);
|
||||
let ox = kf.x - (pivot_x as i32 * (sx_pct as i32 - 100)) / 100;
|
||||
@@ -977,8 +1102,16 @@ fn blit(
|
||||
if sw == 0 || sh == 0 {
|
||||
return;
|
||||
}
|
||||
let sx_pct = if kf.scale_x == 0 { 100 } else { kf.scale_x };
|
||||
let sy_pct = if kf.scale_y == 0 { 100 } else { kf.scale_y };
|
||||
// `scale = 0` means COLLAPSED TO NOTHING, not "unset". This used to coerce
|
||||
// 0 → 100 %, which drew a fully-collapsed element at full size. The disc
|
||||
// settles it: of 15 493 elements with a keyframe group, **2 166 have at
|
||||
// least one zero-scale keyframe and not one has zero on every keyframe** —
|
||||
// and 1 762 of them grow back out of it (`ptlogo_eff3.t32` runs 0 % → 200 %).
|
||||
// An "unset" marker that no element ever uses throughout is not a marker.
|
||||
let (sx_pct, sy_pct) = (kf.scale_x, kf.scale_y);
|
||||
if sx_pct == 0 || sy_pct == 0 {
|
||||
return;
|
||||
}
|
||||
let dw = (sw * sx_pct / 100).max(1);
|
||||
let dh = (sh * sy_pct / 100).max(1);
|
||||
// Keep the pivot point fixed as the element scales.
|
||||
@@ -1032,6 +1165,10 @@ fn blit(
|
||||
continue;
|
||||
}
|
||||
let di = ((ty as u32 * cw + tx as u32) * 4) as usize;
|
||||
// Straight alpha-over. `T8aD +0x04` bit 0x02 was tested as an
|
||||
// ADDITIVE selector and REFUTED — it moved every metric against the
|
||||
// title capture the wrong way (see the doc comment on
|
||||
// `T8adImage::flags`), so the bit is carried but not acted on.
|
||||
for (k, sc) in [sr, sg, sb].into_iter().enumerate() {
|
||||
let dc = canvas[di + k] as u32;
|
||||
canvas[di + k] = ((sc * sa + dc * (255 - sa)) / 255) as u8;
|
||||
@@ -1084,6 +1221,9 @@ mod tests {
|
||||
fn kf(x: i32, y: i32, time: u32) -> Keyframe {
|
||||
Keyframe {
|
||||
fade: 0xffff_ffff,
|
||||
rotation_deg: 0,
|
||||
unknown_4: 0,
|
||||
unknown_8: 0,
|
||||
scale_x: 100,
|
||||
scale_y: 100,
|
||||
tint: 0xffff_ffff,
|
||||
@@ -1197,12 +1337,16 @@ mod tests {
|
||||
// leaves the top-left quadrant bare. See
|
||||
// `docs/re/structures/ui-rat-layout.md`.
|
||||
let img = t8ad::T8adImage {
|
||||
flags: 0,
|
||||
width: 640,
|
||||
height: 360,
|
||||
rgba: vec![255u8; 640 * 360 * 4],
|
||||
};
|
||||
let k = Keyframe {
|
||||
fade: 0xffff_ffff,
|
||||
rotation_deg: 0,
|
||||
unknown_4: 0,
|
||||
unknown_8: 0,
|
||||
scale_x: 200,
|
||||
scale_y: 200,
|
||||
tint: 0xffff_ffff,
|
||||
@@ -1230,6 +1374,7 @@ mod tests {
|
||||
// `ptcopyright.t32` is 694x20 at (293,655), and the capture's glyph run
|
||||
// starts at x = 295 — inside that rect, not offset by a pivot.
|
||||
let img = t8ad::T8adImage {
|
||||
flags: 0,
|
||||
width: 694,
|
||||
height: 20,
|
||||
rgba: vec![255u8; 694 * 20 * 4],
|
||||
|
||||
88
crates/sylpheed-formats/tests/media_disc.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Real-disc tests for media assembly. Skipped without `SYLPHEED_DISC`.
|
||||
//!
|
||||
//! This logic used to live in the Bevy viewer, where it had no test at all. It
|
||||
//! is the trickiest reading on the disc — a cutscene's voice is a byte region of
|
||||
//! a continuous stream, not the bank its name points at — so it gets pinned here
|
||||
//! before anything else is built on top of it.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
||||
use sylpheed_formats::slb::VoiceLang;
|
||||
|
||||
fn disc() -> Option<DirectorySource> {
|
||||
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
|
||||
p.join("dat").is_dir().then(|| DirectorySource::new(p))
|
||||
}
|
||||
|
||||
/// A segment-spanning read returns the same bytes as slicing the whole archive.
|
||||
///
|
||||
/// The control that matters: `sound.pak`'s data is five segments, so a TOC
|
||||
/// offset late in the archive addresses a position no single file has. If the
|
||||
/// walk were off by a segment this would return plausible-looking wrong bytes
|
||||
/// rather than fail, which is exactly why it is asserted against the archive's
|
||||
/// own read rather than against a length.
|
||||
#[test]
|
||||
fn segment_range_matches_the_archive_read() {
|
||||
let Some(src) = disc() else {
|
||||
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||
return;
|
||||
};
|
||||
let name = "BGM_020.slb";
|
||||
let hash = sylpheed_formats::hash::name_hash(name);
|
||||
let via_range = media::read_sound_bank(&src, hash).expect("segment range read");
|
||||
|
||||
let toc = src.read_file("dat/sound.pak").unwrap();
|
||||
let entries = sylpheed_formats::PakArchive::parse_toc(&toc).unwrap();
|
||||
let e = entries
|
||||
.iter()
|
||||
.find(|e| e.name_hash == hash)
|
||||
.expect("BGM_020 in the TOC");
|
||||
assert_eq!(via_range.len(), e.comp_size as usize);
|
||||
|
||||
// And it decodes, which a misaligned read would not do.
|
||||
let riffs = media::sound_bank_riffs(&src, name).expect("riffs");
|
||||
assert!(!riffs.is_empty(), "no sub-waves recovered");
|
||||
}
|
||||
|
||||
/// A movie's voice resolves to a byte region, and the region is sane.
|
||||
///
|
||||
/// `RT01A` is one of the cutscenes whose voice spans more than one `.slb`
|
||||
/// chunk — the case that motivated regions over per-bank reads in the first
|
||||
/// place. The assertions are deliberately about *shape* (ordered, non-empty,
|
||||
/// smaller than one bank) rather than exact offsets, because the offsets are
|
||||
/// disc facts we have no independent oracle for here; a regression that
|
||||
/// reversed or emptied the region would still be caught.
|
||||
#[test]
|
||||
fn movie_voice_resolves_to_a_region_that_decodes() {
|
||||
let Some(src) = disc() else {
|
||||
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||
return;
|
||||
};
|
||||
let (start, end) = media::resolve_movie_voice_region(&src, "RT01A", VoiceLang::English)
|
||||
.expect("RT01A has a bound voice track");
|
||||
assert!(start < end, "region is inverted: {start}..{end}");
|
||||
assert!(end - start > 4096, "region is implausibly small");
|
||||
assert!(end - start < 1_500_000, "region spans more than one bank");
|
||||
|
||||
let riffs = media::voice_region_riffs(&src, start, end).expect("region riffs");
|
||||
assert!(!riffs.is_empty(), "region decoded to no audio");
|
||||
}
|
||||
|
||||
/// An unbound movie stays unvoiced rather than borrowing a neighbour's clip.
|
||||
///
|
||||
/// This is a *negative* the corpus paid for: extending resolution to unbound
|
||||
/// resupply movies by shared demo line played the WRONG recording. The guard
|
||||
/// keeps that door shut.
|
||||
#[test]
|
||||
fn manifest_binding_is_the_only_route() {
|
||||
let Some(src) = disc() else {
|
||||
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||
return;
|
||||
};
|
||||
// A movie the manifest does not bind must resolve to nothing, not to a guess.
|
||||
assert_eq!(
|
||||
media::resolve_movie_voice_clip(&src, "no_such_movie_xyz", VoiceLang::English),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -167,3 +167,62 @@ fn manifest_slot_and_movie_counts() {
|
||||
// Every id names a real record, so nothing dangles.
|
||||
assert!(entries.iter().all(|e| !e.slot.is_empty() && !e.movie.is_empty()));
|
||||
}
|
||||
|
||||
/// Everything the Cutscenes browser shows, asserted against the disc.
|
||||
///
|
||||
/// The window's value is that it answers "which cutscenes exist, which mission
|
||||
/// is each one in, and can I read it without playing it" — so the test checks
|
||||
/// exactly those three, including the **negative**: five manifest-bound movies
|
||||
/// have no `.wmv`, and nine more have no English transcript. A browser that
|
||||
/// quietly omitted them would look complete and be wrong, so the counts are
|
||||
/// pinned here rather than left to the eye.
|
||||
#[test]
|
||||
fn cutscene_catalog_binds_movies_and_transcripts() {
|
||||
let Some(root) = disc_root() else {
|
||||
eprintln!("SKIP: set SYLPHEED_DISC");
|
||||
return;
|
||||
};
|
||||
use sylpheed_formats::movie_subtitle as ms;
|
||||
let (manifest, _) = load_manifest_and_sounds(&root);
|
||||
let rows = movie_manifest::parse(&manifest);
|
||||
assert_eq!(rows.len(), 104, "manifest slots");
|
||||
|
||||
let movies: std::collections::BTreeSet<&str> =
|
||||
rows.iter().map(|r| r.movie.as_str()).collect();
|
||||
assert_eq!(movies.len(), 101, "distinct movies");
|
||||
assert_eq!(rows.iter().filter(|r| r.subtitle.is_some()).count(), 99);
|
||||
assert_eq!(rows.iter().filter(|r| r.voice_token.is_some()).count(), 99);
|
||||
assert_eq!(rows.iter().filter(|r| r.telop.is_some()).count(), 22);
|
||||
|
||||
// Bound-but-absent: the four boot logos and one encoder test clip are named
|
||||
// by the manifest and are not on the disc. The browser marks these in red
|
||||
// rather than offering a Play button that would fail.
|
||||
let on_disc: std::collections::BTreeSet<String> = std::fs::read_dir(root.join("dat/movie"))
|
||||
.unwrap()
|
||||
.filter_map(|e| {
|
||||
let p = e.ok()?.path();
|
||||
(p.extension()?.to_str()? == "wmv")
|
||||
.then(|| p.file_stem()?.to_str().map(|s| s.to_ascii_lowercase()))?
|
||||
})
|
||||
.collect();
|
||||
let mut absent: Vec<&str> = movies
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|m| !on_disc.contains(&m.to_ascii_lowercase()))
|
||||
.collect();
|
||||
absent.sort_unstable();
|
||||
assert_eq!(
|
||||
absent,
|
||||
["SYLPH_HD720p_8M-CBR_2ch", "logo1", "logo2", "logo3", "logo4"],
|
||||
"manifest-bound movies with no .wmv on the disc"
|
||||
);
|
||||
|
||||
// Transcripts, which is what makes a cutscene readable without playback.
|
||||
let lang_pak = PakArchive::open(root.join("dat/movie/eng.pak")).unwrap();
|
||||
let text_pak = PakArchive::open(root.join("dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let resolved = movies
|
||||
.iter()
|
||||
.filter(|m| !ms::load(m, &lang_pak, &text_pak).is_empty())
|
||||
.count();
|
||||
assert_eq!(resolved, 92, "movies with an English transcript");
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ use bevy::prelude::*;
|
||||
use bevy_egui::{egui, EguiContexts};
|
||||
|
||||
use crate::iso_loader::{
|
||||
AudioPreview, FileInfo, FileSelected, GameCategory, GameData, ImageRgba, IsoState, ModelPreview,
|
||||
MovieSubtitles, MovieVoice, PakContent, PakView, RequestAudio, RequestGameData, RequestOpenDir,
|
||||
RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose,
|
||||
RequestShipCatalog, RequestShipRender, RequestSubtitles, RequestVoiceLibrary, SaveBrowser,
|
||||
ScreenBrowser, ShipBrowser, SkyboxPreview, TextPreview, TexturePreview,
|
||||
VideoPreview, VoiceLibrary, IsoLoaderSystemSet,
|
||||
AudioLibrary, AudioPreview, CutsceneBrowser, FileInfo, FileSelected, GameCategory, GameData,
|
||||
ImageRgba, IsoLoaderSystemSet, IsoState, ModelPreview, MovieSubtitles, MovieVoice, PakContent,
|
||||
PakView, RequestAudio, RequestAudioLibrary, RequestCutscenes, RequestGameData, RequestOpenDir,
|
||||
RequestOpenIso, RequestSaveOpen, RequestScreenCatalog, RequestScreenCompose, RequestShipCatalog,
|
||||
RequestShipRender, RequestSubtitles, SaveBrowser, ScreenBrowser, ShipBrowser, SkyboxPreview,
|
||||
TextPreview, TexturePreview, VideoPreview,
|
||||
};
|
||||
use crate::ViewerState;
|
||||
use sylpheed_formats::SubLang;
|
||||
@@ -37,6 +37,7 @@ impl Plugin for ViewerUiPlugin {
|
||||
app.add_systems(Update, draw_ships_ui.after(IsoLoaderSystemSet));
|
||||
app.add_systems(Update, draw_screens_ui.after(IsoLoaderSystemSet));
|
||||
app.add_systems(Update, draw_save_ui.after(IsoLoaderSystemSet));
|
||||
app.add_systems(Update, draw_cutscenes_ui.after(IsoLoaderSystemSet));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,11 +149,12 @@ struct UiEvents<'w> {
|
||||
file_selected: EventWriter<'w, FileSelected>,
|
||||
subtitles: EventWriter<'w, RequestSubtitles>,
|
||||
audio: EventWriter<'w, RequestAudio>,
|
||||
voice_lib: EventWriter<'w, RequestVoiceLibrary>,
|
||||
audio_lib: EventWriter<'w, RequestAudioLibrary>,
|
||||
game_data: EventWriter<'w, RequestGameData>,
|
||||
ships: EventWriter<'w, RequestShipCatalog>,
|
||||
screens: EventWriter<'w, RequestScreenCatalog>,
|
||||
save: EventWriter<'w, RequestSaveOpen>,
|
||||
cutscenes: EventWriter<'w, RequestCutscenes>,
|
||||
}
|
||||
|
||||
fn draw_viewer_ui(
|
||||
@@ -170,7 +172,7 @@ fn draw_viewer_ui(
|
||||
mut subtitles: ResMut<MovieSubtitles>,
|
||||
mut movie_voice: ResMut<MovieVoice>,
|
||||
mut audio: ResMut<AudioPreview>,
|
||||
mut voice_lib: ResMut<VoiceLibrary>,
|
||||
mut audio_lib: ResMut<AudioLibrary>,
|
||||
mut events: UiEvents,
|
||||
) {
|
||||
let ctx = contexts.ctx_mut();
|
||||
@@ -202,11 +204,11 @@ fn draw_viewer_ui(
|
||||
});
|
||||
|
||||
ui.menu_button("View", |ui| {
|
||||
if ui.button("🎙 Voice Lines…").clicked() {
|
||||
voice_lib.open = true;
|
||||
if !voice_lib.loaded && !voice_lib.loading {
|
||||
voice_lib.loading = true;
|
||||
events.voice_lib.send_default();
|
||||
if ui.button("🔊 Audio Library…").clicked() {
|
||||
audio_lib.open = true;
|
||||
if !audio_lib.loaded && !audio_lib.loading {
|
||||
audio_lib.loading = true;
|
||||
events.audio_lib.send_default();
|
||||
}
|
||||
ui.close_menu();
|
||||
}
|
||||
@@ -228,6 +230,12 @@ fn draw_viewer_ui(
|
||||
events.screens.send_default();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("🎬 Cutscenes…").clicked() {
|
||||
// The manifest binds every cutscene slot to its movie,
|
||||
// subtitle track, voice token and telop overlay.
|
||||
events.cutscenes.send_default();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui.button("💾 Save File…").clicked() {
|
||||
// A save is not on the disc — it lives in the emulator's
|
||||
// content tree, so this opens a file dialog.
|
||||
@@ -248,98 +256,134 @@ fn draw_viewer_ui(
|
||||
});
|
||||
|
||||
// ── Standalone voice-line browser (floating window) ───────────────────
|
||||
if voice_lib.open {
|
||||
if audio_lib.open {
|
||||
let mut open = true;
|
||||
egui::Window::new("🎙 Voice Lines")
|
||||
.default_width(360.0)
|
||||
.default_height(480.0)
|
||||
egui::Window::new("🔊 Audio Library")
|
||||
.default_width(400.0)
|
||||
.default_height(520.0)
|
||||
.open(&mut open)
|
||||
.show(ctx, |ui| {
|
||||
if voice_lib.loading {
|
||||
if audio_lib.loading {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.label("Reading sounds.tbl…");
|
||||
});
|
||||
ctx.request_repaint();
|
||||
} else if !voice_lib.loaded {
|
||||
} else if !audio_lib.loaded {
|
||||
ui.label("Open a game source first.");
|
||||
} else {
|
||||
use sylpheed_formats::slb::VoiceLang;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Voice:");
|
||||
// The table name IS the selector: there is no language
|
||||
// field inside sounds.tbl, so switching re-reads the
|
||||
// other file. Music/jingles/SFX are shared and stay.
|
||||
for lang in VoiceLang::ALL {
|
||||
if ui
|
||||
.selectable_label(audio_lib.lang == lang, lang.label())
|
||||
.clicked()
|
||||
&& audio_lib.lang != lang
|
||||
{
|
||||
audio_lib.lang = lang;
|
||||
audio_lib.loaded = false;
|
||||
audio_lib.entries.clear();
|
||||
audio_lib.reload = true;
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Filter:");
|
||||
ui.text_edit_singleline(&mut voice_lib.filter);
|
||||
ui.text_edit_singleline(&mut audio_lib.filter);
|
||||
});
|
||||
let f = voice_lib.filter.to_lowercase();
|
||||
// Group the thousands of entries as directory → speaker so the
|
||||
// list is navigable (e.g. browse `Voice` by character to find a
|
||||
// cutscene's radio line). `name` is `<lang>\<dir>\<file>.slb`.
|
||||
let f = audio_lib.filter.to_lowercase();
|
||||
// Group category → speaker. The category comes from the path
|
||||
// shape, so the root banks (music/jingles/SFX) get real
|
||||
// headings instead of the "?" a directory split gave them.
|
||||
use std::collections::BTreeMap;
|
||||
let mut groups: BTreeMap<&str, BTreeMap<&str, Vec<&sylpheed_formats::slb::VoiceClip>>> =
|
||||
BTreeMap::new();
|
||||
for c in &voice_lib.clips {
|
||||
if !f.is_empty() && !c.name.to_lowercase().contains(&f) {
|
||||
type Group<'a> = BTreeMap<&'a str, Vec<&'a sylpheed_formats::slb::AudioEntry>>;
|
||||
let mut groups: BTreeMap<
|
||||
sylpheed_formats::slb::AudioCategory,
|
||||
Group<'_>,
|
||||
> = BTreeMap::new();
|
||||
for e in &audio_lib.entries {
|
||||
if !f.is_empty() && !e.clip.name.to_lowercase().contains(&f) {
|
||||
continue;
|
||||
}
|
||||
let dir = c.name.rsplit('\\').nth(1).unwrap_or("?");
|
||||
groups
|
||||
.entry(dir)
|
||||
.entry(e.category)
|
||||
.or_default()
|
||||
.entry(c.speaker.as_str())
|
||||
.entry(e.clip.speaker.as_str())
|
||||
.or_default()
|
||||
.push(c);
|
||||
.push(e);
|
||||
}
|
||||
let shown: usize = groups.values().flat_map(|s| s.values()).map(Vec::len).sum();
|
||||
ui.label(
|
||||
egui::RichText::new(format!("{shown} / {} clips", voice_lib.clips.len()))
|
||||
.weak()
|
||||
.small(),
|
||||
egui::RichText::new(format!(
|
||||
"{shown} / {} banks",
|
||||
audio_lib.entries.len()
|
||||
))
|
||||
.weak()
|
||||
.small(),
|
||||
);
|
||||
ui.separator();
|
||||
let filtering = !f.is_empty();
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
for (dir, speakers) in &groups {
|
||||
let dtotal: usize = speakers.values().map(Vec::len).sum();
|
||||
egui::CollapsingHeader::new(format!("📁 {dir} ({dtotal})"))
|
||||
.id_salt(("vdir", *dir))
|
||||
.default_open(filtering)
|
||||
.show(ui, |ui| {
|
||||
for (speaker, clips) in speakers {
|
||||
for (cat, speakers) in &groups {
|
||||
let ctotal: usize = speakers.values().map(Vec::len).sum();
|
||||
egui::CollapsingHeader::new(format!(
|
||||
"{} ({ctotal})",
|
||||
cat.label()
|
||||
))
|
||||
.id_salt(("acat", *cat))
|
||||
.default_open(filtering || ctotal <= 40)
|
||||
.show(ui, |ui| {
|
||||
for (speaker, entries) in speakers {
|
||||
// A single-bank group (Static.slb) would be a
|
||||
// pointless nested header.
|
||||
let flat = speakers.len() == 1 || entries.len() == 1;
|
||||
let mut row = |ui: &mut egui::Ui| {
|
||||
for e in entries {
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.button("▶")
|
||||
.on_hover_text(&e.clip.name)
|
||||
.clicked()
|
||||
{
|
||||
audio.generation =
|
||||
audio.generation.wrapping_add(1);
|
||||
audio.loading = true;
|
||||
audio.active = true;
|
||||
audio.error = None;
|
||||
audio.name = e.clip.display.clone();
|
||||
events.audio.send(RequestAudio {
|
||||
clip: e.clip.name.clone(),
|
||||
display: e.clip.display.clone(),
|
||||
movie: None,
|
||||
mono: e.category.is_voice(),
|
||||
generation: audio.generation,
|
||||
});
|
||||
}
|
||||
ui.label(&e.clip.display);
|
||||
});
|
||||
}
|
||||
};
|
||||
if flat {
|
||||
row(ui);
|
||||
} else {
|
||||
egui::CollapsingHeader::new(format!(
|
||||
"{speaker} ({})",
|
||||
clips.len()
|
||||
entries.len()
|
||||
))
|
||||
.id_salt(("vspk", *dir, *speaker))
|
||||
.default_open(filtering || clips.len() <= 6)
|
||||
.show(ui, |ui| {
|
||||
for c in clips {
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
.button("▶")
|
||||
.on_hover_text(&c.name)
|
||||
.clicked()
|
||||
{
|
||||
audio.generation =
|
||||
audio.generation.wrapping_add(1);
|
||||
audio.loading = true;
|
||||
audio.active = true;
|
||||
audio.name = c.display.clone();
|
||||
events.audio.send(RequestAudio {
|
||||
clip: c.name.clone(),
|
||||
display: c.display.clone(),
|
||||
movie: None,
|
||||
generation: audio.generation,
|
||||
});
|
||||
}
|
||||
ui.label(&c.display);
|
||||
});
|
||||
}
|
||||
});
|
||||
.id_salt(("aspk", *cat, *speaker))
|
||||
.default_open(filtering || entries.len() <= 6)
|
||||
.show(ui, &mut row);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
voice_lib.open = open;
|
||||
audio_lib.open = open;
|
||||
}
|
||||
|
||||
// ── Left panel: file browser ──────────────────────────────────────────
|
||||
@@ -481,11 +525,13 @@ fn draw_viewer_ui(
|
||||
audio.generation = audio.generation.wrapping_add(1);
|
||||
audio.loading = true;
|
||||
audio.active = true; // show the panel immediately (spinner)
|
||||
audio.error = None;
|
||||
audio.name = format!("VOICE_{movie}");
|
||||
events.audio.send(RequestAudio {
|
||||
clip: String::new(),
|
||||
display: format!("VOICE_{movie}"),
|
||||
movie: Some((movie.clone(), movie_voice.lang)),
|
||||
mono: true, // a cutscene voice track
|
||||
generation: audio.generation,
|
||||
});
|
||||
}
|
||||
@@ -1348,8 +1394,10 @@ fn draw_audio_player(ui: &mut egui::Ui, audio: &mut AudioPreview) {
|
||||
if audio.loading {
|
||||
ui.spinner();
|
||||
ui.label("decoding…");
|
||||
} else if let Some(err) = &audio.error {
|
||||
ui.colored_label(egui::Color32::from_rgb(224, 86, 122), format!("⚠ {err}"));
|
||||
} else {
|
||||
ui.label("voice track");
|
||||
ui.label("sound bank");
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.button("✖ Close").clicked() {
|
||||
@@ -1876,6 +1924,21 @@ fn draw_screens_ui(
|
||||
if !screens.filter.is_empty() && ui.small_button("✖").clicked() {
|
||||
screens.filter.clear();
|
||||
}
|
||||
ui.separator();
|
||||
// Widens the enumeration to everything `compose` can draw — the
|
||||
// same rule the pak browser's inline preview uses. Costs a
|
||||
// re-scan, so it is a deliberate click rather than the default.
|
||||
if ui
|
||||
.checkbox(&mut screens.include_fragments, "Fragments")
|
||||
.on_hover_text(
|
||||
"Also list RATC bundles with no .rat layout child: the developer-logo \
|
||||
splash, and ~1 786 two-element fragments (a button beside its glow). \
|
||||
These are what the PAK browser preview draws but this list omits.",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
screens.rescan = true;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
|
||||
@@ -1895,6 +1958,12 @@ fn draw_screens_ui(
|
||||
egui::CollapsingHeader::new(&pak.label)
|
||||
.default_open(pak.builds.len() == 1)
|
||||
.show(ui, |ui| {
|
||||
if pak.truncated {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(224, 168, 86),
|
||||
"⚠ list truncated (decode budget)",
|
||||
);
|
||||
}
|
||||
for (bi, (entry, size)) in pak.builds.iter().enumerate() {
|
||||
let selected = screens.selected == Some(pi)
|
||||
&& screens.build == bi;
|
||||
@@ -1922,6 +1991,32 @@ fn draw_screens_ui(
|
||||
{
|
||||
recompose = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut screens.black_backdrop, "Black backdrop")
|
||||
.on_hover_text(
|
||||
"Composite over black instead of the default dim slate. The slate \
|
||||
stands in for the PRMD dim quad plus the live 3D scene behind an \
|
||||
in-mission screen; black is what the game composites over on a \
|
||||
screen carrying its own background — and what a framebuffer \
|
||||
capture must be compared against.",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
recompose = true;
|
||||
}
|
||||
if ui
|
||||
.checkbox(&mut screens.show_primitives, "Primitives")
|
||||
.on_hover_text(
|
||||
"Draw the untextured .prm fade/dim/flash quads. Decoded, but their \
|
||||
paint order is unsolved: a primitive has no T8aD header and so no \
|
||||
layer key, and the derived order forces keyless elements last. \
|
||||
That is measurably wrong — expect an opaque quad to wipe some \
|
||||
screens (32 of GP_DIALOG's builds).",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
recompose = true;
|
||||
}
|
||||
if screens.composing {
|
||||
ui.spinner();
|
||||
ctx.request_repaint();
|
||||
@@ -2081,8 +2176,17 @@ fn draw_screens_ui(
|
||||
compose.send(RequestScreenCompose {
|
||||
pak: pak.path.clone(),
|
||||
build: screens.build,
|
||||
// The catalog stored (pak entry index, size) per build; the
|
||||
// entry index is what lets the worker read one entry.
|
||||
entry: pak
|
||||
.builds
|
||||
.get(screens.build)
|
||||
.map(|(e, _)| *e)
|
||||
.unwrap_or(0),
|
||||
focus: screens.show_focus,
|
||||
animated: screens.show_animated,
|
||||
black_backdrop: screens.black_backdrop,
|
||||
primitives: screens.show_primitives,
|
||||
hidden: screens.hidden.clone(),
|
||||
});
|
||||
}
|
||||
@@ -2295,3 +2399,236 @@ fn draw_save_ui(
|
||||
}
|
||||
saves.open &= open;
|
||||
}
|
||||
|
||||
// ── Cutscene browser (View ▸ Cutscenes) ──────────────────────────────────────
|
||||
|
||||
/// The cutscene catalog: every slot the manifest binds, with its movie,
|
||||
/// subtitle track, voice token and telop overlay — and a transcript pane that
|
||||
/// resolves the captions to text **without playing the video**.
|
||||
///
|
||||
/// Before this the manifest was invisible plumbing: it resolved a voice bank and
|
||||
/// was never rendered, so a cutscene could only be found by hunting `.wmv` in
|
||||
/// the ISO tree, where nothing says which mission a file belongs to.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn draw_cutscenes_ui(
|
||||
mut contexts: EguiContexts,
|
||||
mut cut: ResMut<CutsceneBrowser>,
|
||||
mut requests: EventReader<RequestCutscenes>,
|
||||
mut file_selected: EventWriter<FileSelected>,
|
||||
mut browser: ResMut<FileBrowserState>,
|
||||
) {
|
||||
if requests.read().next().is_some() {
|
||||
cut.open = true;
|
||||
}
|
||||
if !cut.open {
|
||||
return;
|
||||
}
|
||||
let ctx = contexts.ctx_mut().clone();
|
||||
let mut open = true;
|
||||
let mut pick: Option<usize> = None;
|
||||
let mut play: Option<String> = None;
|
||||
|
||||
egui::Window::new("🎬 Cutscenes")
|
||||
.default_width(880.0)
|
||||
.default_height(560.0)
|
||||
.open(&mut open)
|
||||
.show(&ctx, |ui| {
|
||||
if cut.loading {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.label("Reading the cutscene manifest…");
|
||||
});
|
||||
ctx.request_repaint();
|
||||
return;
|
||||
}
|
||||
if !cut.loaded {
|
||||
ui.label("Open a game source first (File ▸ Open…).");
|
||||
return;
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("🔍");
|
||||
ui.text_edit_singleline(&mut cut.filter);
|
||||
if !cut.filter.is_empty() && ui.small_button("✖").clicked() {
|
||||
cut.filter.clear();
|
||||
}
|
||||
ui.separator();
|
||||
ui.label("Subtitles:");
|
||||
let before = cut.lang;
|
||||
egui::ComboBox::from_id_salt("cutscene_lang")
|
||||
.selected_text(cut.lang.label())
|
||||
.show_ui(ui, |ui| {
|
||||
for l in sylpheed_formats::movie_subtitle::SubLang::ALL {
|
||||
ui.selectable_value(&mut cut.lang, l, l.label());
|
||||
}
|
||||
});
|
||||
if cut.lang != before && cut.selected.is_some() {
|
||||
cut.want_cues = true;
|
||||
}
|
||||
});
|
||||
let missing = cut.rows.iter().filter(|r| !r.present).count();
|
||||
ui.label(
|
||||
egui::RichText::new(format!(
|
||||
"{} slots · {} distinct movies{}",
|
||||
cut.rows.len(),
|
||||
cut.rows
|
||||
.iter()
|
||||
.map(|r| r.movie.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>()
|
||||
.len(),
|
||||
if missing > 0 {
|
||||
format!(" · ⚠ {missing} bound to a movie not on the disc")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
))
|
||||
.weak()
|
||||
.small(),
|
||||
);
|
||||
ui.separator();
|
||||
|
||||
let filter = cut.filter.to_lowercase();
|
||||
egui::SidePanel::left("cutscene_list")
|
||||
.resizable(true)
|
||||
.default_width(330.0)
|
||||
.show_inside(ui, |ui| {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
egui::Grid::new("cutscene_grid")
|
||||
.striped(true)
|
||||
.num_columns(2)
|
||||
.show(ui, |ui| {
|
||||
for (i, r) in cut.rows.iter().enumerate() {
|
||||
let hay = format!("{} {} {}", r.slot, r.movie, r.kind)
|
||||
.to_lowercase();
|
||||
if !filter.is_empty() && !hay.contains(&filter) {
|
||||
continue;
|
||||
}
|
||||
let label = match (r.mission, r.phase) {
|
||||
(Some(m), Some(p)) => format!("S{m:02} ph{p} {}", r.movie),
|
||||
(Some(m), None) => format!("S{m:02} {}", r.movie),
|
||||
_ => format!(" {}", r.movie),
|
||||
};
|
||||
let mut text = egui::RichText::new(label).monospace();
|
||||
if !r.present {
|
||||
text = text.color(egui::Color32::from_rgb(224, 86, 122));
|
||||
}
|
||||
if ui
|
||||
.selectable_label(cut.selected == Some(i), text)
|
||||
.on_hover_text(&r.slot)
|
||||
.clicked()
|
||||
{
|
||||
pick = Some(i);
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(r.kind).weak().small(),
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||
let Some(row) = cut.selected.and_then(|i| cut.rows.get(i)) else {
|
||||
ui.label("Pick a cutscene on the left.");
|
||||
return;
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading(&row.slot);
|
||||
if row.present {
|
||||
if ui.button("▶ Play").clicked() {
|
||||
play = Some(format!("dat/movie/{}.wmv", row.movie));
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(224, 86, 122),
|
||||
"⚠ not on the disc",
|
||||
);
|
||||
}
|
||||
});
|
||||
egui::Grid::new("cutscene_detail")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
let mut kv = |k: &str, v: String| {
|
||||
ui.label(egui::RichText::new(k).weak().small());
|
||||
ui.label(egui::RichText::new(v).monospace());
|
||||
ui.end_row();
|
||||
};
|
||||
kv("kind", row.kind.to_string());
|
||||
kv("movie", format!("dat/movie/{}.wmv", row.movie));
|
||||
if let (Some(m), Some(p)) = (row.mission, row.phase) {
|
||||
kv("mission", format!("S{m:02}, phase {p}"));
|
||||
} else if let Some(m) = row.mission {
|
||||
kv("mission", format!("S{m:02}"));
|
||||
}
|
||||
kv(
|
||||
"subtitle",
|
||||
row.subtitle.clone().unwrap_or_else(|| "—".into()),
|
||||
);
|
||||
kv(
|
||||
"voice track",
|
||||
row.voice_token.clone().unwrap_or_else(|| "—".into()),
|
||||
);
|
||||
// The .prt overlay is bound here but we have no parser,
|
||||
// so name the reference and say the content is not read.
|
||||
kv(
|
||||
"telop (.prt)",
|
||||
match &row.telop {
|
||||
Some(t) => format!("{t} (not decoded)"),
|
||||
None => "—".into(),
|
||||
},
|
||||
);
|
||||
});
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Transcript").strong());
|
||||
if cut.cues_loading {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.label("resolving captions…");
|
||||
});
|
||||
ctx.request_repaint();
|
||||
} else if cut.cues.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"no captions resolved for this movie in this language",
|
||||
)
|
||||
.weak(),
|
||||
);
|
||||
} else {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_salt("cue_scroll")
|
||||
.show(ui, |ui| {
|
||||
egui::Grid::new("cue_grid").num_columns(2).striped(true).show(
|
||||
ui,
|
||||
|ui| {
|
||||
for c in &cut.cues {
|
||||
ui.label(
|
||||
egui::RichText::new(fmt_time(c.start))
|
||||
.monospace()
|
||||
.weak()
|
||||
.small(),
|
||||
);
|
||||
ui.label(&c.text);
|
||||
ui.end_row();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(i) = pick {
|
||||
cut.selected = Some(i);
|
||||
cut.want_cues = true;
|
||||
}
|
||||
if let Some(path) = play {
|
||||
// Route through the normal file-open path, so the existing video player
|
||||
// handles it exactly as it would from the tree.
|
||||
browser.loading = true;
|
||||
browser.selected = browser.files.iter().position(|f| f.eq_ignore_ascii_case(&path));
|
||||
file_selected.send(FileSelected(path));
|
||||
}
|
||||
cut.open = open;
|
||||
}
|
||||
|
||||
82
docker/agent/bin/push-work
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
# Push the current topic branch to origin — the ONLY sanctioned way out of the
|
||||
# container.
|
||||
#
|
||||
# Why a wrapper instead of plain `git push`:
|
||||
#
|
||||
# * **`main` and shared branches are refused.** The agent commits to
|
||||
# `auto/<topic>`; a human merges. A token that can push anywhere is one
|
||||
# confused iteration away from rewriting the consolidated line.
|
||||
# * **Force-push is refused**, always. Nothing here needs it, and history
|
||||
# rewriting is the one mistake that cannot be undone by merging.
|
||||
# * It pushes the CURRENT branch only, by name, so a stray `--all` cannot
|
||||
# publish another agent's worktree branch mid-experiment.
|
||||
#
|
||||
# Credentials come from a file mounted read-only at ~/.git-credentials (see
|
||||
# `sylph-agent`). They are never printed, never logged, and never passed on a
|
||||
# command line.
|
||||
#
|
||||
# push-work push the current branch, and any annotated tags on it
|
||||
#
|
||||
# --follow-tags publishes ANNOTATED tags reachable from the pushed commits. That
|
||||
# is what makes a pinned decoder state durable: the port depends on commits of
|
||||
# ours by revision, and a commit reachable only from a topic branch is orphaned
|
||||
# by a squash-merge. Lightweight tags are deliberately not pushed.
|
||||
# push-work --dry-run say what it would do
|
||||
set -euo pipefail
|
||||
|
||||
DRY=0
|
||||
[ "${1:-}" = "--dry-run" ] && DRY=1
|
||||
|
||||
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || {
|
||||
echo "push-work: not inside a git repository" >&2; exit 1; }
|
||||
cd "$repo_root"
|
||||
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$branch" = "HEAD" ]; then
|
||||
echo "push-work: detached HEAD — check out a branch first" >&2; exit 1
|
||||
fi
|
||||
|
||||
case "$branch" in
|
||||
auto/*) ;;
|
||||
*)
|
||||
echo "push-work: refusing to push '$branch'." >&2
|
||||
echo " Only auto/* topic branches may leave the container; a human merges" >&2
|
||||
echo " them into main. Move your work: git switch -c auto/<topic>" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
|
||||
if [ ! -s "$HOME/.git-credentials" ]; then
|
||||
echo "push-work: no credentials mounted at ~/.git-credentials." >&2
|
||||
echo " The host must start the container with SYLPH_GIT_CREDENTIALS pointing" >&2
|
||||
echo " at a file containing one line:" >&2
|
||||
echo " https://<user>:<token>@git.mc02.dev" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# `store` reads the file we mounted; nothing is written back (it is read-only).
|
||||
# Applied to THIS COMMAND ONLY, via `-c`, never `git config --local`.
|
||||
#
|
||||
# Writing it to --local config persists it in the repository, and this repo is a
|
||||
# bind mount the host also uses -- so the host's git inherited
|
||||
# `store --file=/sylph-home/re/.git-credentials`, a path that exists only inside
|
||||
# the container, and every host push then failed with
|
||||
# `unable to get credential storage lock: No such file or directory`.
|
||||
#
|
||||
# A tool that configures a shared repository to suit itself breaks every other
|
||||
# user of that repository. Keep it to the invocation.
|
||||
CRED_HELPER="store --file=$HOME/.git-credentials"
|
||||
|
||||
ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD)
|
||||
echo "push-work: $branch — $ahead commit(s) to publish"
|
||||
|
||||
if [ "$DRY" = 1 ]; then
|
||||
echo "push-work: --dry-run, stopping here"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --force-with-lease is deliberately NOT offered. If this is rejected as
|
||||
# non-fast-forward, someone else moved the branch: fetch and merge, do not
|
||||
# overwrite.
|
||||
git -c "credential.helper=$CRED_HELPER" push --follow-tags --set-upstream origin "$branch"
|
||||
echo "push-work: pushed $branch"
|
||||
@@ -1,48 +1,132 @@
|
||||
Work the Project Sylpheed reverse-engineering backlog, one item at a time.
|
||||
Answer the open questions the Godot menu port is blocked on, one at a time.
|
||||
|
||||
Read `Syplheed-Reborn/docker/agent/AGENT.md` first — it has the container's
|
||||
tooling and, more importantly, the method rules this corpus is built on.
|
||||
## Your objective
|
||||
|
||||
`Syplheed-Reborn/docs/port/MISSION.md` — read it every iteration. It lists the
|
||||
open questions Q1…Q9 plus a gated probe S1, and the gate each one must pass.
|
||||
|
||||
**You do not build the port.** A separate agent does that, from what you produce.
|
||||
Your deliverable is decoded, verified, written-down answers with the evidence.
|
||||
If you find yourself designing an export schema or writing GDScript, you have
|
||||
crossed the line — go back to the question you were answering.
|
||||
|
||||
This is still reverse engineering. What changed is what earns attention: an item
|
||||
is worth doing when the menu port is blocked on it.
|
||||
|
||||
## Read these first, every iteration
|
||||
|
||||
Short on purpose, and the reason this prompt is short:
|
||||
|
||||
1. `docs/port/MISSION.md` — the open questions, their gates, what is out of scope.
|
||||
2. `docs/port/HANDOFF.md` — what the port agent has been told so far. **Update it
|
||||
when you answer something.** An answer not reachable from that page has not
|
||||
been delivered.
|
||||
3. `docs/re/REFUTED.md` — claims already tested and dead. Grep it for your nouns
|
||||
before designing anything.
|
||||
4. `docs/re/METHOD.md` — the traps this corpus has already paid for.
|
||||
5. `docs/re/INDEX.md` — what is already decoded. Re-deriving a ✅ row is not a
|
||||
finding; asking whether its values *resolve* is.
|
||||
6. `docker/agent/AGENT.md` — the container's tooling.
|
||||
|
||||
`docs/re/disc-atlas.html` maps how the assets reference each other — useful when
|
||||
you need to find what feeds what.
|
||||
|
||||
**These files are the memory.** A finding that lives only in your context is lost
|
||||
when the container dies.
|
||||
|
||||
## Each iteration
|
||||
|
||||
1. **Pick one item.** Take the next open entry from
|
||||
`Syplheed-Reborn/docs/re/BACKLOG.md`, preferring the one whose "first step"
|
||||
is cheapest and most decisive. If you are mid-item from a previous
|
||||
iteration, continue it rather than starting another.
|
||||
2. **Do the smallest experiment that could settle it**, and try to *refute*
|
||||
your hypothesis before you believe it.
|
||||
3. **Write the result down** in `docs/re/` under the ✅/🟡/❔ convention, with
|
||||
the evidence. A withdrawn or refuted result is a real result — record it,
|
||||
with the reasoning, rather than deleting it.
|
||||
4. **Commit** to a topic branch (below), one logical change per commit.
|
||||
5. **Say plainly what you did not settle**, and stop the iteration.
|
||||
1. **Pick one question** from MISSION.md, preferring the one that blocks the port
|
||||
earliest and whose first step is cheapest. If you are mid-question, continue it
|
||||
rather than starting another.
|
||||
2. **Do the smallest experiment that could settle it**, and try to *refute* your
|
||||
hypothesis before believing it. Run the known-positive through any new filter
|
||||
first; a filter that fails its own control is dead, not tuneable.
|
||||
3. **Classify the answer honestly.** Every answer is exactly one of:
|
||||
* **decoded** — the field, plus a disc-wide check;
|
||||
* **measured** — not on the disc in any form you found, but here is what the
|
||||
running game does, and here is the capture;
|
||||
* **undecodable, with reach** — you looked here, here and here, and this is
|
||||
why it is not there.
|
||||
|
||||
Never a fourth thing. *Measured* and *undecodable* mean the port agent will
|
||||
author that value by hand, and it must know it is authoring rather than
|
||||
transcribing. Labelling a guess as a decode puts it into the port wearing the
|
||||
badge of a measurement.
|
||||
4. **Write it down** in `docs/re/` under the ✅/🟡/❔ convention, with the evidence
|
||||
and the *reach* of any negative, then update the row in `docs/port/HANDOFF.md`.
|
||||
* Refuted something → a line in `REFUTED.md`.
|
||||
* Bitten by a general trap → a line in `METHOD.md`.
|
||||
* Closed a format → update its `INDEX.md` row.
|
||||
5. **Commit** to `auto/<topic>`, one logical change per commit.
|
||||
6. **Publish**: `push-work`. Every iteration that produced a commit.
|
||||
7. **Say plainly what you did not settle**, and stop.
|
||||
|
||||
## Hard rules
|
||||
|
||||
* **Never commit to `main`.** Work on `auto/<topic>` in whichever repo you are
|
||||
touching, branched from the current `main`. Create it if it does not exist.
|
||||
* **Never push.** No push credentials are mounted, and that is deliberate — a
|
||||
human reviews before anything leaves the box.
|
||||
* **One emulator at a time.** `run-canary` enforces this with a lockfile; do not
|
||||
work around it.
|
||||
* **Do not edit `main`'s history**, do not rebase shared branches, and do not
|
||||
delete branches.
|
||||
* **Measure the oracle; never infer it.** An iteration that reasons about the
|
||||
game without running it is a red flag unless it is a pure static-format task.
|
||||
* **Verify with an artifact**, not with "it compiles": `build-reborn test` (it
|
||||
wires up `SYLPHEED_DISC` — without it the disc tests silently self-skip and a
|
||||
green run means almost nothing), `sylpheed-cli mesh render`, `screen render`,
|
||||
`save info`, a screenshot.
|
||||
* **Do not build the port.** No Godot project, no GDScript, no asset pipeline, no
|
||||
export schema, no transcoding. Those belong to the port agent.
|
||||
* **Do not touch `crates/sylpheed-viewer`.** The Explorer is the human's tool for
|
||||
exploring and verifying the RE work; it keeps its static-data-only rule and the
|
||||
port does not depend on it.
|
||||
* **Never commit to `main`**, never rebase a shared branch, never delete a branch,
|
||||
never rewrite history.
|
||||
* **Do not touch another agent's worktree.** `git worktree list` first; branches
|
||||
marked `+` are checked out elsewhere.
|
||||
* **One emulator at a time** — `run-canary` enforces it with a lockfile.
|
||||
* **Measure the oracle; never infer it.** Most of the open questions are about
|
||||
*behaviour* — timing, transitions, what a button does, what a d-pad press does
|
||||
at the end of a list. Those cannot be answered from the file. An iteration that
|
||||
reasons about the game without running it is a red flag unless the question is
|
||||
a pure static-format one.
|
||||
* **Do not improvise around a blocker.** If a question needs a decision only the
|
||||
user can make, or the container cannot do it, write what you found, note it in
|
||||
MISSION.md, and move to the next question you can actually finish.
|
||||
|
||||
## When you are blocked
|
||||
## The S1 probe
|
||||
|
||||
If an item needs something the container cannot do — hardware Vulkan for a
|
||||
rendering question, a push, a decision only the user can make — **do not
|
||||
improvise around it**. Write what you found, note the blocker in `BACKLOG.md`,
|
||||
and move to the next item.
|
||||
One iteration, then **stop and write the go/no-go**. Do not start Ready Room work
|
||||
on your own authority — MISSION.md §S1 says why.
|
||||
|
||||
## Verifying
|
||||
|
||||
* `build-reborn test` wires up `SYLPHEED_DISC`; without it the disc tests
|
||||
self-skip and a green run means almost nothing.
|
||||
* Verify with an **artifact**, not with "it compiles": `sylpheed-cli screen
|
||||
info` / `screen render` / `mesh render` / `save info`, a capture, a screenshot.
|
||||
* Commit the reference data beside the finding, so the port can be built without
|
||||
a disc in the loop during development.
|
||||
* A regenerated artifact that comes out byte-identical is strong evidence a change
|
||||
was additive. When it does change, check that every diff line pairs exactly.
|
||||
|
||||
## Publishing
|
||||
|
||||
`push-work` pushes the current branch to origin. It refuses anything that is not
|
||||
`auto/*` and never force-pushes, so the consolidated line stays a human's
|
||||
decision. Run it **every iteration that produced a commit** — not at the end of
|
||||
some longer arc, which is exactly when a container dies.
|
||||
|
||||
If it reports no credentials, say so in your reply and continue working. Do not
|
||||
improvise another route out: no remote rewrite, no credential helper of your own,
|
||||
no alternate transport. A push that is blocked is a blocked push.
|
||||
|
||||
## Pacing
|
||||
|
||||
Self-pace. A useful iteration is one experiment plus its write-up, not a
|
||||
marathon; stopping with a clean commit and an honest "here is what is still
|
||||
open" is the goal every time.
|
||||
One experiment plus its write-up is a good iteration; a marathon is not. Stop with
|
||||
a clean commit, a push, and an honest list of what is still open.
|
||||
|
||||
An emulator session must fit inside ONE turn — a Stop hook kills xenia when the
|
||||
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.
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
# SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists
|
||||
# SYLPH_REMOTE=0 do NOT enable Remote Control (default: enabled for `loose`)
|
||||
# SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent)
|
||||
# SYLPH_GIT_CREDENTIALS file with `https://<user>:<token>@host` for push-work
|
||||
# (default: $HOME/.sylph-git-credentials)
|
||||
# SYLPH_LOOP_INTERVAL fixed loop cadence, e.g. 30m (default: 45m)
|
||||
# SYLPH_CPUS / SYLPH_MEM_GB override the computed half
|
||||
set -euo pipefail
|
||||
|
||||
@@ -125,6 +128,21 @@ docker_args() {
|
||||
echo " packaged SPIRV-Tools is too old. Running is unaffected." >&2
|
||||
fi
|
||||
|
||||
# ── git push ──
|
||||
# Read-only, and only ever used by `push-work`, which refuses anything but an
|
||||
# auto/* branch and never force-pushes. Without this the agent's work only
|
||||
# exists inside the container and dies with it.
|
||||
GITCRED="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}"
|
||||
if [ -f "$GITCRED" ]; then
|
||||
_out+=(-v "$GITCRED:/sylph-home/re/.git-credentials:ro")
|
||||
else
|
||||
echo "==> NOTE: no git credentials at $GITCRED — the agent cannot push," >&2
|
||||
echo " so its work will be lost if the container is destroyed. Create it" >&2
|
||||
echo " with a single line and chmod 600:" >&2
|
||||
echo " https://<user>:<token>@git.mc02.dev" >&2
|
||||
echo " or point SYLPH_GIT_CREDENTIALS elsewhere." >&2
|
||||
fi
|
||||
|
||||
[ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY")
|
||||
[ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN")
|
||||
[ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE")
|
||||
@@ -191,7 +209,12 @@ case "${1:-}" in
|
||||
TASK="Work the RE backlog in Syplheed-Reborn/docs/re/BACKLOG.md."
|
||||
fi
|
||||
fi
|
||||
INTERVAL="${SYLPH_LOOP_INTERVAL:-}" # empty = let the model self-pace
|
||||
# A FIXED interval by default, not self-pacing. Self-pacing requires the
|
||||
# agent to call ScheduleWakeup itself at the end of every turn, and the one
|
||||
# thing an agent deep in an experiment reliably forgets is the bookkeeping
|
||||
# after it. With an interval the harness owns the cadence and a forgotten
|
||||
# wakeup cannot end the run. Set SYLPH_LOOP_INTERVAL= (empty) to self-pace.
|
||||
INTERVAL="${SYLPH_LOOP_INTERVAL-45m}"
|
||||
declare -a ARGS; docker_args ARGS
|
||||
ARGS+=(-e SYLPH_AUTONOMOUS=1 -w "$PROJECT")
|
||||
echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g"
|
||||
@@ -206,8 +229,19 @@ case "${1:-}" in
|
||||
echo " ./sylph-agent attach chat with it locally (Ctrl-P Ctrl-Q to leave it running)"
|
||||
echo " ./sylph-agent stop stop it"
|
||||
echo
|
||||
echo " It commits to auto/* branches and cannot push — no git credentials"
|
||||
echo " are mounted, so review its work with: git -C '$PROJECT/Syplheed-Reborn' log --oneline auto/..."
|
||||
# Report what is actually true. This line used to claim unconditionally that
|
||||
# the agent could not push, which was written before credentials were
|
||||
# supported and then went stale — telling the operator their work was at risk
|
||||
# when it was not, which is the exact failure the credential mount fixes.
|
||||
if [ -f "${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}" ]; then
|
||||
echo " It commits to auto/* branches and publishes them with push-work,"
|
||||
echo " which refuses any other branch and never force-pushes. Review with:"
|
||||
echo " git -C '$PROJECT/Syplheed-Reborn' fetch origin && git log --oneline origin/auto/..."
|
||||
else
|
||||
echo " It commits to auto/* branches and CANNOT PUSH — no git credentials"
|
||||
echo " are mounted, so its work dies with the container. Review it with:"
|
||||
echo " git -C '$PROJECT/Syplheed-Reborn' log --oneline auto/..."
|
||||
fi
|
||||
;;
|
||||
|
||||
logs) shift; exec docker logs "$@" "$NAME" ;;
|
||||
|
||||
926
docs/port/HANDOFF.md
Normal file
@@ -0,0 +1,926 @@
|
||||
# Handoff — what the menu port needs, and where it stands
|
||||
|
||||
The single page the **port agent** reads. Everything here is produced by the
|
||||
container agent's reverse engineering; nothing here is a design decision about
|
||||
the port itself.
|
||||
|
||||
Keep it current. It is a summary with links into `docs/re/`, not a second copy of
|
||||
the findings — but an answer that is not reachable from this page has not been
|
||||
delivered.
|
||||
|
||||
## How to read an answer
|
||||
|
||||
Every row below is one of exactly three things, and the distinction is the point:
|
||||
|
||||
| | meaning | what the port should do |
|
||||
|---|---|---|
|
||||
| **decoded** | a field on the disc, with a disc-wide check | read it from the data |
|
||||
| **measured** | not on the disc in any form we found, but the running game does *this* | hardcode it, and cite this page |
|
||||
| **undecodable** | we looked in these places, it is not there, here is the reach of the negative | author it by hand, knowingly |
|
||||
|
||||
There is no fourth kind. If a row says *measured* or *undecodable*, the port is
|
||||
**authoring** that value, not transcribing it — and it should be kept somewhere a
|
||||
human can see it is a human decision, so that when it is later decoded the
|
||||
authored version can be deleted.
|
||||
|
||||
## Status
|
||||
|
||||
| | Question | State | Answer / link |
|
||||
|---|---|---|---|
|
||||
| Q1 | keyframe time unit + ramp shape | ✅ answered, 🟡 one gap | ramp is **linear**; **2 units per rendered frame**; **`1 unit = 1/60 s` — settled**, the idle title presents at 28.5 fps so the game is 30 Hz. 🟡 **The interpolation law is settled; the group TIMELINE for multi-keyframe elements is not** — `palogo_gamearts` is still at full alpha 9 frames after its declared `a=32`, and its declared 80-frame fade-in never draws — [`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md). ✅ **REPLICATED 2026-08-29 — for ANIMATION, read `+36` as the time the NEXT pose is reached.** Three elements across two screens: `palogo_gamearts` and `palogo_seta` hold full alpha for **83 frames** and `palogo_sqex` for **≥77**, where the current reading predicts **6–8** and the shifted one **80–102**. The elements that cannot discriminate (the `_eff` glows, on which the linear law was measured) fit both. ⚠️ Our decoder still defaults to the other reading (`SYLPHEED_KF_TIME_SHIFT=1` to flip) because it changes `rest()` on one element — but that is an unsound fallback guessing either way, so **static rendering is unaffected and animation timing should use the shift** |
|
||||
| Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`ui-title-build-map.md`](../re/ui-title-build-map.md) |
|
||||
| Q3 | paint order for the six screens | ✅ answered, ❔ tie-break | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md). ⚠️ **The key does not fully order a screen**: elements sharing a key are tied, and the tie-break is ❔ **undecodable from the bundle** — declaration table, `T8aD` header (exhaustive: every offset 0x00–0x7f at u8/u16/u32, both directions, **0** fields match the measured order against **64** for the control) and the RATC child order all give the same order the game does *not* use. Your exposure is **2 overlapping tied pairs on `EXTRAS`** — [`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md) |
|
||||
| Q4 | button → GamePart | ✅ answered | **measured** which screen all **5** buttons open — `NEW GAME` → `DIFFICULTY` → `SELECT DATA`, not a hang. The **GamePart id is still a name match**, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||||
| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
|
||||
| Q6 | boot sequence + what drives it | ✅ answered | sequence **measured** end to end; the driver is **code, not data** — four search spaces closed, so the port **authors** the sequence — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
|
||||
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
|
||||
| Q8 | menu audio bindings | ✅ answered | cue vocabulary + bank **decoded**; event binding is a **name match** (the authors' own event names). ✅ **You CAN have the SE audio** — ⚠️ an earlier version of this row said it was "undecodable from the disc"; that was **retracted** and the row was stale. Three cues are located in `Static.slb` and **decode to PCM**: d-pad move `0x1ec0` (4 packets), Ⓑ back `0x0ec0` (2), Ⓐ confirm `0x5d6c0` (6), all mono 48 kHz. The bank is a packed run of XMA waves with no delimiter, so a wave is only (offset, packet count) — and ⚠️ the file order is **not** cue-id order, so the index cannot be counted out — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |
|
||||
| Q9 | video binding + playback rules | ✅ answered | **decoded** from the movie manifest: `ADVERTISE_MOVIE`→`ADV.wmv` (boot intro *and* attract are one asset), `MS00A`→`S00A.wmv` is the new-game intro, `STAFF_ROLL`→the credits reel. ✅ **one Ⓐ skips a movie** (title at 57 s vs a 193 s baseline) — [`movie-binding.md`](../re/movie-binding.md) |
|
||||
| Q10 | music-bank sub-wave roles (intro+loop?) | ✅ answered | **two stems of one performance, played together** — sample-synchronous, equal duration, 32/32 banks. **Concatenating is wrong.** Not a seamless loop either — [`structures/bgm-two-stems.md`](../re/structures/bgm-two-stems.md) |
|
||||
| S1 | Ready Room go/no-go | ✅ **no-go** | it is 2D and enumerates fine (60 builds), but `GP_READY_ROOM.pak` holds **briefing/tactical-map** content, not the six-button Ready Room menu — [`ready-room-probe.md`](../re/ready-room-probe.md) |
|
||||
|
||||
## 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
|
||||
faded in a beat later; 5/8 are the five-button main menu; 6/9 are the `EXTRAS`
|
||||
submenu — the **only** submenu inside this archive. Builds 0/1 and 10/11 are a
|
||||
`DELTASABER / SYLPHEED A.I.` plate that was **never seen running**, in the boot
|
||||
path, any title-side screen, or the attract loop. ✅ measured against live
|
||||
captures for the four English screens the boot path shows;
|
||||
[`ui-title-build-map.md`](../re/ui-title-build-map.md).
|
||||
**Withdrawn:** the earlier "builds 6/8/9 are submenus" — 8 is the Japanese main
|
||||
menu. The other four main-menu buttons leave the archive, and where each one
|
||||
goes is **measured** — see the button-destination bullet below.
|
||||
* **Buttons are identifiable as data.** Element kind `0x3002` = button, `0x0` =
|
||||
decoration, `0x10` = primitive. ✅ decoded **for the title-side screens**.
|
||||
⚠️ `0x3002` is one member of a `0x3000` family with sub-bits, and it is not the
|
||||
only button kind on the disc: `GP_READY_ROOM`'s 902 bundles contain **zero**
|
||||
`0x3002` and use `0x3000` / `0x3004` / `0x300c` / `0x3008` instead. Nothing in
|
||||
this milestone changes — every screen in scope is `GP_TITLE` — but do not ship
|
||||
`kind == 0x3002` as a general button test.
|
||||
(The kind is the 4th `u32` of the 60-byte declaration entry, at `+40`.)
|
||||
* **The title's settled pose is `rest` — and always pass `--primitives`.**
|
||||
Against a plate-free capture of the real screen, `screen render --build 4
|
||||
--black` edge-correlates **0.9163 at (0,0)**, so the geometry of `rest` is the
|
||||
arrived pose; the timeline is not needed for the title.
|
||||
⚠️ **But without `--primitives` the whole frame is +13.14 too bright** (R +12.35,
|
||||
G +13.58, B +13.48); with them, **+0.55**. The missing element is
|
||||
**`pteff02.prm`**, the 25 % dim, and `--primitives` is off by default. **That is
|
||||
the "washed-out cyan slab"** — a dim that should be there and isn't, not a glow
|
||||
that shouldn't. Because the art is blue-dominant, the shortfall reads cyan.
|
||||
🔴 **A second, separate defect — and it is the "slab".** With the dim in place
|
||||
the residual is localised to one band (y ≈ 112–225): the game draws the logo's
|
||||
`Z` **swoosh thin with a pink/magenta edge**, our render draws it **thick and
|
||||
solid white**. Crop:
|
||||
[`title-swoosh-capture-vs-render.png`](../re/captures/title-builds/title-swoosh-capture-vs-render.png).
|
||||
The elements are `ptlogo_back2.t32`, `ptlogo_back2eff.t32` and
|
||||
`ptlogo_back2eff1…5`. 🟡 Those five are also the group with the **known
|
||||
unsolved paint-order tie-break** (key `0x8083`) — same screen, same elements —
|
||||
but a blend-order swap explains white-instead-of-pink poorly.
|
||||
🔴 **The pivot mismatch is NOT the cause — checked and refuted.** These elements'
|
||||
declared pivots really do belong to the *other language's* sprite
|
||||
(`ptlogo_back2`'s `(500,117)` is exactly half the Japanese 1000×234, not its own
|
||||
English 1118×262; 24 of 109 title elements are off by > 8 px). **But it cannot
|
||||
affect this render**: `blit` sizes a sprite from its **texture**, and applies the
|
||||
pivot only as `kf.x − pivot·(scale−100)/100` — and **all seven swoosh elements
|
||||
are scale `(100,100)` at every keyframe**, so the term is zero.
|
||||
✅ **That formula is now MEASURED, not just implemented (2026-08-28).** The
|
||||
title's two `ptloop` sweeps scale **600 %** and **800 %** vertically, where the
|
||||
pivot term is worth 450 and 630 px, and the GPU capture puts both quad centres
|
||||
at y **359.1** and **360.0** — against the formula's **360.0** for both.
|
||||
Top-left anchoring predicts 810 and 990; treating the position as the centre
|
||||
predicts 270. Horizontally the same term makes the group's `t` solved from
|
||||
**position** agree with `t` solved from **vertex alpha** to **0.33 / 0.65
|
||||
units**, versus ~8 units without it. So: **anchor scaling on the pivot, not on
|
||||
the top-left corner.**
|
||||
🟡 It does *not* prove interpolation is linear — both fields were inverted
|
||||
through the same linear map, so a shared easing curve would cancel. It does
|
||||
show position and alpha ride **one shared parameter**.
|
||||
⚠️ It *would* bite `ptlogo1`/`ptlogo2`, which scale 100 → 150 during the
|
||||
build-in. Not at rest, and not on the swoosh.
|
||||
🔴 **Ruled out for the COLOUR**: every swoosh keyframe's fade is `0x??ffffff`
|
||||
(white RGB, alpha only), no tint is non-white, and the texture decodes
|
||||
*blue*-leaning (175,174,198).
|
||||
🟡 **What is left is the BLEND.** Size and position are the texture's own and
|
||||
match; fade, tint and texture colour are all ruled out. Seven overlapping
|
||||
mostly-transparent sprites (`ptlogo_back2` 5.4 % opaque, its glow 10.3 %, the
|
||||
five `eff` segments 10–23 %, all white or warm) stacked with plain alpha-over
|
||||
saturate to opaque white — which is exactly what we draw, and would read as
|
||||
"thicker" against the game's thin coloured stroke.
|
||||
🟡 **And there is a candidate field for it.** The `T8aD` header word at
|
||||
**`+0x04`** splits the title's sprites exactly along effect-vs-normal:
|
||||
`pteff01`, `pteff03a`, `ptlogo_back2eff1…5`, `ptlogoall_eff`/`_eff2` are
|
||||
**`0x8832`**; `ptlogo1`/`2`, `ptlogo_tm`, `ptbase2`, `ptlogo_back2`,
|
||||
`ptcopyright` and `ptlogo_back2eff` are **`0x8830`**. One bit — **`0x02`**.
|
||||
Disc-wide it is a real independent flag: 19 216 sprites, 18 distinct values,
|
||||
bit `0x02` set in **27.1 %**, and it toggles against otherwise-identical words
|
||||
(`0x8830`/`0x8832`, `0x0830`/`0x0832`, `0x0810`/`0x0812`, `0x0030`/`0x0032`).
|
||||
⚠️ **Correlation only — untested.** Nothing yet shows it *means* additive; the
|
||||
test is to blend bit-`0x02` sprites additively and re-correlate the title
|
||||
against the capture. Note `ptlogo_back2eff` is `0x8830` despite its name, so
|
||||
this is a field and not a naming pattern. ❔ Not diagnosed — but two candidate meanings are now dead: **not** "the name contains `eff`" — and not even the one-way "bit ⇒ `eff` name", which held 10/10 on the title but fails on **2 657 of 4 995** bit-set sprites disc-wide (`P(eff|set) = 0.468`). What survives is a 3.3× association, and the counterexamples are rings, glows and lights — effect-like art without the naming convention and **not** "the element is transient" (`pteff03`/`pteff03a` carry the bit and persist to `t=250`). 🛑 **Parked** — four candidate meanings are now dead (additive blend; `eff` name, both directions; transient element; **premultiplied alpha**, refuted because flagged sprites violate `RGB ≤ A` *more* than unflagged, 55.5 % vs 33.7 %) and none produced a positive account. It blocks nothing — your screens composite at 0.947 correlation against a capture without it. ⚠️ Practical note for an asset pipeline: `T8aD` headers appear in **RATC child order** (18/18 verified on build 4 against decoded dimensions), which is the only sound way to attribute a header to a name when two sprites share a size — and two here do.
|
||||
⚠️ **The rotated draw is NOT the swoosh.** It was identified as the swoosh by
|
||||
elimination; that is **refuted** — its quads span y −209…925 in screen space,
|
||||
the swoosh is a band at y 126…360. ✅ **It is the two `ptloop` sweeps**
|
||||
(`ptloop01.rat` → `pteff03.t32`, `ptloop02.rat` → `pteff03a.t32`), confirmed by
|
||||
edge length: 400 × 1076 and 400 × 1444 against 399×180 at the two elements'
|
||||
**different** declared scales, 600 % (= 1080) and 800 % (= 1440). What stands
|
||||
from the old bullet: the game *does* submit rotated quads this compositor
|
||||
cannot draw, and vertex colours are white.
|
||||
✅ **SOLVED 2026-08-28 by draw capture — it is the GEOMETRY.** The game submits
|
||||
the swoosh as **two rotated parallelograms** (edges `(0.54,−0.56)` and
|
||||
`(0.44,0.79)`, ~45° and ~61°, extending to `y=±1.81` NDC).
|
||||
`ui_layout::blit` draws **axis-aligned rectangles only**, so it blits the sprite
|
||||
upright — right on average, right in position, wrong in shape, which is the
|
||||
measured signature exactly. **A port that blits upright rects will have the same
|
||||
defect.**
|
||||
🔴 Vertex colour is refuted with it: every colour in the capture is
|
||||
`<alpha>FFFFFF`, white RGB.
|
||||
✅ **CLOSED 2026-08-28 — the rotation IS on the disc, at keyframe `+12`.**
|
||||
It is a signed angle in **degrees**, clockwise-positive in screen space
|
||||
(Y down), and `ui_layout::Keyframe` now carries it as `rotation_deg`.
|
||||
Confirmed against the framebuffer, not against our own renderer: the two
|
||||
`ptloop` records declare `+12` = **30** and **−45**, and the GPU capture
|
||||
submits their quads at **+30.26°** and **−45.28°** — magnitude and sign, on two
|
||||
different values. Corroborated separately by shape: `GP_BUNK` entry `117ca14f`
|
||||
holds a group whose `+12` ramps **0 → 360** with position, scale and alpha all
|
||||
constant — a spin in place. Disc-wide `+12` is non-zero in **14.50 %** of
|
||||
83 862 keyframe blocks.
|
||||
⚠️ **Two things the port must know about it.**
|
||||
(1) **Rotation lives in BOTH the top-level table and nested `.rat` leaf
|
||||
records.** ⚠️ I told you one iteration ago that it looked nested-only; that was
|
||||
three archives' worth of pattern and **it is refuted** — `GP_DIALOG` and
|
||||
`GP_DEBRIEFING_PILOTLOG` rotate top-level elements. The actionable half stands:
|
||||
the *title's* rotations are nested, so a composer reading only the declaration
|
||||
table gets zero rotation on exactly the elements that move there.
|
||||
The clearest examples are top-level and show up in
|
||||
`screen info --build 0 --geometry dat/GP_DIALOG.pak`: `pceff03.t32` and
|
||||
`pceff04.t32` ramp `r=` **90 → 30 → 10 → 3 → 0** while their alpha ramps
|
||||
0 → 255 and they slide into place — a swing-in that settles upright; and build
|
||||
6's `pzeff02.t32` ramps **43 → 61 → 75 → 90** while scaling 112 % → 200 % and
|
||||
fading to 0 — a spin-out burst.
|
||||
(2) **`sylpheed-cli screen render` still does not rotate.** The field is
|
||||
decoded, not rendered; `ui_layout::blit` is axis-aligned only. So the reference
|
||||
renderer and the port will *both* draw these upright until a rotating blit
|
||||
exists — and per your own rule, the two of them agreeing about it means
|
||||
nothing.
|
||||
✅ **Checked 2026-08-29: this does NOT affect your five screens at rest.**
|
||||
Title, main menu, `EXTRAS` and both splash halves have **zero** top-level
|
||||
elements with a non-zero rotation. The only rotations on any of them are the
|
||||
title's two nested `ptloop` records (`r = 30` and `−45`), and at rest those sit
|
||||
at `x = 1521` and `x = −839` — a 399-wide sprite entirely off both edges of a
|
||||
1280 screen. So a static composite is unaffected; the caveat applies only if
|
||||
you animate the title's build-in, where the sweeps cross the screen rotated.
|
||||
🟡 The neighbouring words `+4` and `+8` are still unexplained: signed, non-zero
|
||||
in ~4.8 % / 4.6 % of blocks, almost entirely `±180`/`±90`. That distribution
|
||||
looks like a flip flag rather than a free angle, but nothing observed turns on
|
||||
them — **do not transcribe them as X/Y rotation.**
|
||||
[`ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md)
|
||||
⚠️ The earlier "pink versus white" reading compared two differently-shaped
|
||||
renderings and should be re-checked after geometry, not carried as a separate
|
||||
defect.
|
||||
❔ **Classified: undecodable from the disc, with reach.** Seven candidates
|
||||
eliminated — pivot (inert at scale 100 *and* no measured displacement), `fade`,
|
||||
`tint`, texture colour, additive blend via `+0x04` bit `0x02`, and
|
||||
capture-not-settled (the band is identical from t = 4.0 s to t = 21.5 s). The
|
||||
residual is stable and modest: band mean **+1.83**, edge-corr **0.70** vs ≈ 0.92
|
||||
frame-wide. A next attempt should use a **per-draw GPU capture** of the running
|
||||
guest — not another field — but ⚠️ that capture records prim/indices/shader
|
||||
hashes/**texture bindings**/**vertex attributes** and **no blend state**, so it
|
||||
can test a per-draw *vertex colour* today and would need a Canary change to dump
|
||||
`RB_BLENDCONTROL`. ✅ And the plate-free capture is sound; use it.
|
||||
🔴 **Refuted:** it is *not* that our dim covers the whole frame instead of
|
||||
sitting beneath the UI — the logo reads +2.36 against a background of −0.74, so
|
||||
the paint order is being honoured.
|
||||
|
||||
* **The title's motion, decoded and attributed.** After building in, the *title
|
||||
art* is essentially static — a 22 s capture measures the wordmark region at
|
||||
sd 0.06 and the bottom-right corner at sd 0.003. Two things do move:
|
||||
* ✅ **Two slow light sweeps in build 4.** `ptloop01.rat` (an `opt `-linked
|
||||
`RATC` at `0xbb5966`) sweeps `pteff03.t32` left→right over **450 units =
|
||||
7.5 s**; `ptloop02.rat` sweeps `pteff03a.t32` right→left over **570 units =
|
||||
9.5 s**. Ordinary 40-byte keyframe blocks from `+0x68`, three keyframes each.
|
||||
* ✅ **The `PRESS Ⓐ BUTTON` plate pulses**, and it is the loudest thing on
|
||||
screen — a capture's per-tile amplitude map puts **sd 7.65** in the band
|
||||
x ≈ 318–954, y ≈ 560–672 against **0.06** on the wordmark. It is
|
||||
`ptbtn00f.rat`, the plate's highlight variant (build **2**, not build 4),
|
||||
whose alpha ramps **`0x00 → 0x06 → 0x4a → 0x50` (hold) `→ 0x4a → 0x06 → 0x00`**
|
||||
over **eight** keyframes at `t = 6, 29, 35, 50, 58, 97, 105, ?` — a glow that
|
||||
fades in and back out, **closing on fully transparent**, so it is a complete
|
||||
cycle rather than a one-shot ramp.
|
||||
🟡 Its cycle **length** is not readable, and this is now observed rather than
|
||||
assumed: the eighth block's time slot literally contains the ASCII terminator
|
||||
`end `, so the record ends there and the value does not exist. Declared span is
|
||||
therefore **≥ 105 units = 1.75 s** against a measured **≈ 2.3 s** — which would
|
||||
need a final step of ≈ 33 units. That number is **fitted to the measurement, not
|
||||
read**; the port should take ≈ 2.3 s as measured.
|
||||
⚠️ An earlier version of this bullet said "the title screen loops at ≈ 2.2 s"
|
||||
and attributed it to `ptloop01/02`. Both halves were wrong: it is the **plate**,
|
||||
and it is a different build.
|
||||
* 🟡 **Our composite is brighter than the emulator's frame — measured, and you
|
||||
would be *authoring* if you apply it.** Alignment is exact (best offset
|
||||
dy=0 dx=0, correlation **0.9466**), so only the tone differs. Fitting on 16×16
|
||||
patches that are flat in **both** images: `capture ≈ 255·(render/255)^γ` with
|
||||
γ = **1.491** (main menu), **1.493** (`EXTRAS`), **1.338** (title).
|
||||
⚠️ **Narrow reach.** Those flat patches span only render values ~0–60, where a
|
||||
gamma and a plain scale are nearly indistinguishable — on both menus the errors
|
||||
are 0.28 vs 0.34 and 0.22 vs 0.28. Only the title separates them (1.08 vs
|
||||
9.02). Nothing here constrains midtones or highlights.
|
||||
🔴 The held-out control **failed to discriminate**: the splash's 2 918 flat
|
||||
patches are pure black (render 0–4), so every model scores ≈ 0.
|
||||
🔴 **My "it may be the emulator" caveat is withdrawn.** I said canary applies
|
||||
`kernel_display_gamma_type = 2` (BT.709) on output. It does not — that cvar is
|
||||
a value a **kStub getter reports to the guest** (`VdGetCurrentDisplayGamma`),
|
||||
which the game uses to build its own ramp; canary then applies **the guest's**
|
||||
ramp from the `DC_LUT` registers in the swap path (`apply_gamma_table.ps` /
|
||||
`apply_gamma_pwl.ps`). So there is no emulator post-process to subtract, and
|
||||
any gamma in a capture is one the game installed.
|
||||
✅ **Measured 2026-08-29: the game DOES query the display gamma.** Booted with
|
||||
Kernel logging on (`--log_mask=12 --log_level=3`, which changes nothing about
|
||||
the output), `VdGetCurrentDisplayGamma` is called once at video init, between
|
||||
`VdGetSystemCommandBuffer` and `VdSetDisplayMode` — the moment a ramp-builder
|
||||
would ask. Control in the same log: 359 `VdRetrainEDRAM` lines, so an absent
|
||||
call would have shown.
|
||||
🟡 **That it then writes the ramp is inferred, not observed** — but the chain
|
||||
is closed: canary's swap-path gamma stage is a **pure 256-entry LUT** with no
|
||||
other transfer (`apply_gamma_table.xesli`), and that LUT **defaults to
|
||||
identity** (`CommandProcessor::Initialize`, whose comment says the linear
|
||||
default is "what games set when starting with the sRGB return value"). Identity
|
||||
cannot produce the measured γ ≈ 1.4, so a non-identity ramp was written.
|
||||
⚠️ The weak joint is that this assumes our composite reproduces the *pre-ramp*
|
||||
framebuffer; what carries it is that a systematic ~1.4 across three screens is
|
||||
not the shape of a compositor bug. A fixed sRGB stage does not fit either
|
||||
direction (encode brightens; decode darkens far more).
|
||||
**Practical upshot for you is unchanged:** the darkening is the game's own
|
||||
display ramp, so it belongs in a port as a display profile, not baked in.
|
||||
⚠️ Note the ramp depends on the display type the game is *told*; canary
|
||||
hard-codes TV/BT.709, which on hardware is a console setting. **So this is a
|
||||
display profile, not a fixed property of the game** — reasonable to expose as a
|
||||
setting rather than bake in.
|
||||
[`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md)
|
||||
|
||||
* ✅ **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
|
||||
**`pteff05.t32`** on both menus. Those are `kind 0x0`, one keyframe, rest
|
||||
`a = 255`, pivot `(640,360)` — full-screen and opaque.
|
||||
**Cause:** the element declares `pteff05.t32`, but the `T8aD` behind its `opt `
|
||||
link is registered under the name **`8AX`**, so the sprite lookup misses and a
|
||||
silent `continue` drops it.
|
||||
✅ **It does not currently show,** because `ptbase.t32` (640×360, drawn at
|
||||
200 %) is *the same artwork at half resolution* — its 2× upscale differs from
|
||||
`8AX` by mean 2.05, and our background is pixel-identical to `8AX` in every
|
||||
patch sampled.
|
||||
✅ **SETTLED 2026-08-29 — the game draws the full-res `8AX`, so use it.**
|
||||
Previously parked as "needs a per-draw capture"; it did not. The two carry the
|
||||
same art at two resolutions, so what separates them is the detail `8AX` has
|
||||
that an upscale cannot. Correlating the capture's departure-from-upscale
|
||||
against the 8AX-only detail (both first mapped through the measured gamma):
|
||||
main menu **+0.0475** vs controls +0.0032 / −0.0075, title **+0.0634** vs
|
||||
+0.0095 / +0.0086 — **two screens, both 68 % of the theoretical ceiling, 7–15×
|
||||
their matched controls**.
|
||||
**So: resolve the name and draw `8AX` at 1:1.** Upscaling the 640×360 `ptbase`
|
||||
2× is *wrong*, not merely softer. ⚠️ Do not draw **both** — an opaque
|
||||
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 — 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
|
||||
(a sort on each sprite's layer key) elsewhere. Checked, rather than assumed:
|
||||
the derived order reproduces the measured one **exactly** on the main menu
|
||||
(0 inverted pairs) and the developer splash (0). On the **title** it differs by
|
||||
8 pairs — **all same-layer-key ties** — and two of those are total occlusions
|
||||
(`back2eff5` is 1133×280 and *fully contains* `back2eff3` and `back2eff4`;
|
||||
derived puts it on top, the game puts it underneath). The title is unaffected
|
||||
in practice because it has a measured order.
|
||||
Per screen: title **measured**, main menu **measured**, developer splash
|
||||
**measured**, publisher splash derived but with **0 ties** (fully determined),
|
||||
and **`EXTRAS` derived with 15 tied pairs of which only 2 overlap**.
|
||||
✅ **That narrows again to ONE, and the capture is consistent with it.** Of the
|
||||
two overlapping pairs, `ptloop01`×`ptloop02` are `loop*` animations that
|
||||
`compose` skips by default, so their tie is unreachable. The remaining pair is
|
||||
`ptframe3`×`ptframe4`, overlapping 102×132 px — and against `live-extras.png`
|
||||
that contested region correlates **+0.9622**, *better* than the whole frame
|
||||
(+0.9440) and inside the range of regions where order cannot matter (+0.8502 /
|
||||
+0.9903). 🟡 Consistent with, not proof — correlation cannot see a swap between
|
||||
locally similar art. **15 → 2 → 1 → consistent** is the whole paint-order risk
|
||||
on your five screens.
|
||||
[`structures/ui-paint-order-derived-check.md`](../re/structures/ui-paint-order-derived-check.md)
|
||||
|
||||
* ✅ **How good are the five screens, actually?** One page with the numbers:
|
||||
[`five-screens-acceptance.md`](../re/five-screens-acceptance.md). Rendered and
|
||||
correlated against the live captures — title **0.9500**, main menu **0.9460**,
|
||||
`EXTRAS` **0.9440**, publisher splash **0.9600**, developer splash **0.9643**,
|
||||
and every one aligns at **exactly dy=0 dx=0** over a ±2 px search, so placement
|
||||
and scale are right and the residual is tone and detail rather than geometry.
|
||||
Every undrawn element is accounted for (ghost instances, `.prm` primitives,
|
||||
`loop*` animations, and the `8AX` name mismatch whose art is on screen anyway) —
|
||||
the counts add up exactly, with nothing unexplained. The residual, ranked: tone
|
||||
(γ≈1.4, the game's own ramp), `8AX` resolution, one `EXTRAS` paint-order tie,
|
||||
and rotation-not-rendered which does not affect these five at rest.
|
||||
⚠️ Reach: static composites at rest against single frames — this says nothing
|
||||
about animation.
|
||||
|
||||
* ✅ **A static composite is only meaningful for a screen that SETTLES — the two
|
||||
splashes are animations.** Measured with its control: suppressing the `_eff`
|
||||
glows takes the **publisher splash 0.9604 → 0.9982** and the **developer splash
|
||||
0.9659 → 0.9980**, while the same edit makes the title −0.002, the main menu
|
||||
**−0.092** and `EXTRAS` **−0.107** worse. The draw log says why — on the
|
||||
developer splash the glows draw on frames 94–115 and the logos on 116–211, so
|
||||
at the captured moment every glow is already finished, including the two with
|
||||
plateaus that `rest_plateau` renders visible.
|
||||
So `rest_plateau` is right where a screen settles and over-draws where it does
|
||||
not. **There is no "resting pose" for the splashes** — they play through and
|
||||
leave, and a static composite of them is a picture of one arbitrary frame
|
||||
(≈0.998 for the frame these captures hold). **Play the timeline for the two
|
||||
splashes; composite statically for title / main menu / `EXTRAS`.**
|
||||
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||||
|
||||
* ❔ **A group's DURATION is in the data; its START TIME is not.** Testing whether
|
||||
the splash timeline *played* reproduces the capture: each element is on screen
|
||||
for its declared span to within 2 % (glows 44 units observed vs 45 declared;
|
||||
logos 192 vs 195). But every glow declares the same times `15,30,45` and every
|
||||
logo the same `15,30,190,194,206,210`, so on one clock they would overlap almost
|
||||
entirely — and they **do not overlap at all**. The glows run frames 94–115, the
|
||||
logos 116–211, strictly sequential.
|
||||
🔴 The obvious candidate is dead: each group header carries an undecoded
|
||||
**lead-in word**, and it is `0x00000000` for all seven elements. Not the
|
||||
keyframe times, not that word, not declaration order, not the RATC child order.
|
||||
✅ **Here is the sequence to author, measured** (1 frame = 1/30 s):
|
||||
publisher `palogo_sqex` **3.00 s+** (a floor — it is already at full alpha on
|
||||
the capture's first frame), a **0.10 s** gap with nothing drawn, then the
|
||||
developer **glows for 0.73 s**, then the developer **logos for 3.20 s**, the
|
||||
glow→logo switch being a single frame boundary with no overlap. Each phase is
|
||||
within 2 % of its element's declared span, so the *durations* come from the
|
||||
bundle and only the *ordering* is authored.
|
||||
⚠️ One capture, one run; and `palogo_anima`/`_eff` get 0 draws in all 214
|
||||
frames, so a third pair's phase is not in this measurement.
|
||||
[`data/splash-phase-timeline.txt`](../re/data/splash-phase-timeline.txt)
|
||||
⚠️ **So you must author the sequencing.** The observed order on the developer
|
||||
splash — both glows, then both logos — is *measured for one screen*, not a
|
||||
decoded rule.
|
||||
✅ **Verified against its own refutation:** across all 235 captured frames,
|
||||
**zero** contain both a glow and a logo; the switch is one clean boundary with
|
||||
two sprites either side.
|
||||
🔴 **And it goes further than start times — a bundle's declared elements are
|
||||
not what gets drawn.** ✅ **And the mechanism is now established, by
|
||||
elimination.** The competing story — "two compositions shown in sequence" —
|
||||
needs a bundle declaring the glows *without* the logos, and no such bundle
|
||||
exists: only four entries in `GP_TITLE` carry `palogo` elements, and each
|
||||
developer entry (11, 14) declares **all six** logos and glows. So whichever
|
||||
bundle was active, only a subset of its elements was drawn at a time.
|
||||
(An earlier texture-base test could not separate the two — it failed its own
|
||||
control, since the publisher splash is a different bundle and shares the base
|
||||
`0x11C30000`, a reused upload slot rather than an identity.) Entry 11 declares *three* logo/glow pairs; only two are ever drawn.
|
||||
`palogo_anima` gets **0 frames** while `palogo_gamearts` gets **95**, from
|
||||
byte-identical keyframe times. (Reach: the capture covers frames 1–214, so
|
||||
"never in the window".) **Compositing every element of a bundle does not
|
||||
reproduce what the game shows over time** — it is right for a static screen
|
||||
that settles, and it is not a timeline.
|
||||
[`structures/ui-group-start-time.md`](../re/structures/ui-group-start-time.md)
|
||||
|
||||
* ✅ **The splash's backdrop is an opaque black `.prm` from the bundle, painted
|
||||
first.** Measured: every frame opens with the clear and then an **untextured
|
||||
1280×720 quad, vertex colour `FF000000` in all 212 frames** — matching
|
||||
`palogo_eff0.prm`'s declaration (`kind 0x10`, pivot 640×360, one keyframe,
|
||||
`a=255`). That is why a splash render needs `--black` rather than the default
|
||||
backdrop. ⚠️ Not a general rule: the main menu's measured order puts one `.prm`
|
||||
at position 4 and the other **last** (the transition fade).
|
||||
✅ **The splash's recorded paint order `[0,2,4,6,1,3,5]` is a real depth
|
||||
order** — read off the runtime child array, and independently backed by the
|
||||
static layer key (glows `0xa100` sort before logos `0xa110`;
|
||||
`paint_order_audit` reports derived == measured, 0 ties). A same-day claim on
|
||||
this page that it was "a temporal sequence, not depth" was **withdrawn**; see
|
||||
the ❌ section in the linked page for why, if you read it before the fix.
|
||||
⚠️ Separately and still true — and it is an **activation** fact, not a
|
||||
paint-order one: the splash's glows and logos are never on screen together
|
||||
(glows f94–115, logos f116–211,
|
||||
[timeline](../re/data/splash-phase-timeline.txt)). Compositing all seven at
|
||||
once reproduces no frame of the real screen. Depth says *what covers what*;
|
||||
it does not say *what is up*.
|
||||
[`structures/ui-prm-primitives.md`](../re/structures/ui-prm-primitives.md)
|
||||
|
||||
* **Menu order is geometric.** Buttons sorted top-to-bottom by resting Y. This is
|
||||
✅ correct for a vertical menu and is **not** a decoded neighbour graph — the
|
||||
disc's real navigation structure is unknown, and `opt ` is *not* a focus link
|
||||
(measured and refuted, see
|
||||
[`ui-focus-and-effect-elements.md`](../re/structures/ui-focus-and-effect-elements.md)).
|
||||
* **Highlighted states pair by name** — `ptbtn01.rat` ↔ `ptbtn01f.rat`. 🟡 a
|
||||
naming convention that holds for all 54 real pairs, not a decoded field.
|
||||
* **`rest()` was wrong for elements with no exit animation — fixed 2026-08-28.**
|
||||
A trailing run of identical keyframes was always treated as the exit and
|
||||
excluded; on an element that has no exit it *is* the hold, and `rest()` fell
|
||||
back to the element's **first** keyframe — off-position and transparent. Six
|
||||
elements on the main menu were affected, including `ptframe1`/`ptframe2`, the
|
||||
bright circuit bracket around the menu, which both the port's composite **and**
|
||||
`sylpheed-cli screen render` were dropping.
|
||||
The rule now: **a trailing run is the hold exactly when it is visible** (alpha
|
||||
≠ 0). ⚠️ Not the pose-equality test the report proposed — `pgptitle.rat`'s
|
||||
trailing run also matches its last timed keyframe, and adopting that would erase
|
||||
the word PAUSE. Oracle correlation over the bracket region improved
|
||||
**0.9596 → 0.9748**; the PAUSE control is unchanged.
|
||||
✅ **Disc-wide**: over 2 859 bundles / 13 991 elements, `rest` moves for **30**
|
||||
(0.21 %) — **4 invisible → visible, 0 visible → invisible**. Tests green
|
||||
(131 passed).
|
||||
❔ **Back to the port agent:** on the English main menu exactly **two** elements
|
||||
satisfy your pose-equality condition (`ptframe1`/`ptframe2`), so your six span
|
||||
the whole export. If any of the other four have a **transparent** trailing run,
|
||||
this rule leaves them alone on purpose. **Which screens are they on, and does a
|
||||
capture show any of them drawn?** If so the alpha rule is incomplete.
|
||||
This also closes the old ❔ on `ptframe1`/`ptframe2` "resting at alpha 0 but the
|
||||
capture shows the frame plainly".
|
||||
* **The resting pose is the hold**, not the first, last or longest-dwell keyframe;
|
||||
a keyframe is the **start of a ramp**.
|
||||
[`ui-resting-pose.md`](../re/structures/ui-resting-pose.md). ✅
|
||||
* **That ramp is linear, and it runs at 2 keyframe time units per rendered
|
||||
frame.** Measured frame-by-frame off the running game's own draw stream: a
|
||||
declared 15-unit fade lands on `round(255·k/15)` for all seven of its samples,
|
||||
with `k` stepping 2, 4, 6, 8, 10, 12, 14 on seven consecutive submitted frames.
|
||||
**measured**, not decoded — the disc says `t=30`, it does not say what a `t` is.
|
||||
🟡 **But a multi-keyframe element's TIMELINE does not reproduce (2026-08-28).**
|
||||
The linear law and the 2-units-per-frame rate rest on the splash's `_eff`
|
||||
glows, and those are exact. Applying *their* calibration — with no free
|
||||
parameter left — to `palogo_gamearts` in the same bundle and the same frames:
|
||||
the logo is still at `a=255` nine frames after its declared `a=32`, its
|
||||
fade-out runs ~17 frames late, and its declared 80-frame fade-in is never
|
||||
drawn at all (and that is not culling — the same element is submitted down to
|
||||
`a=7` on the way out). Its declared fade-out also spends 12 of 16 units
|
||||
dropping only 23/255 of the alpha; the capture shows no such plateau.
|
||||
🔵 **Followed up, and the candidate is now strongly favoured — but not
|
||||
adopted.** That `+36` holds the *next* pose's time is supported by a
|
||||
calibration-free measurement: the observed full-alpha **hold : fade-out** ratio
|
||||
is 83 : 13 frames = **6.38**, the shifted reading predicts **8.00**, and the
|
||||
current reading predicts **0.25** — off by **26×**. With the glow's 2
|
||||
units/frame fixed and nothing else free, the current reading says the logo
|
||||
holds full alpha for **2.0 frames**; the game holds it for **83**. The shift
|
||||
also makes `rest()`'s plain dwell rule pick the visible hold instead of a
|
||||
fully transparent pose, and removes the decoder's "last block's time is
|
||||
unreadable" special case.
|
||||
~~🔴 **What stops it:** … `GP_TITLE` build 7 … twins should match in
|
||||
brightness …~~ **Withdrawn 2026-08-28.** That render difference is **not**
|
||||
evidence about the time association. It is one element — `ptlogo_eff3.t32`, a
|
||||
transient bloom that grows `0 % → 200 %` at full alpha while rotating, then
|
||||
collapses — and it has **no resting pose at all**. `rest()` falls through to
|
||||
its dwell fallback and returns whichever end of that movement the indexing
|
||||
lands on: the invisible frame as decoded, the 200 % peak shifted. An 896×389
|
||||
sprite at 200 % is larger than the screen, which is the whole 13.1 % and the
|
||||
whole luminance gap. I was comparing a heuristic, not a decode.
|
||||
🔴 **And that is a real defect you should know about:** `rest()`'s dwell rule
|
||||
is unsound *whenever it runs*. A dwell gap is time spent interpolating **from**
|
||||
pose k **to** pose k+1; neither is held unless they are equal — which is a
|
||||
plateau, and the plateau path has already returned by then. **So any element
|
||||
with no two adjacent identical poses has a guessed rest pose**, in our renderer
|
||||
and in anything built from it. Measured disc-wide: **2 305 of 15 493 elements
|
||||
(14.88 %)** — ⚠️ **corrected 2026-08-29 down from a published 3 807 (24.57 %)**,
|
||||
which counted 1 502 *single-keyframe* elements as guesses; those have one pose
|
||||
and it is unambiguously their rest. Of the real 2 305, **195 get a degenerate
|
||||
`scale = 0 %` pose**, and the two
|
||||
candidate rules agree only **50.2 %** of the time.
|
||||
✅ **This does not block you — your exposure is TWO elements, both on the
|
||||
splashes.** The fallback is reached only by an element that is plateau-less
|
||||
*and* multi-keyframe; a single-keyframe element short-circuits. Title, main
|
||||
menu and `EXTRAS` reach it **zero** times, which is why three different
|
||||
fallback rules render them to identical correlations. The publisher and
|
||||
developer splashes reach it once each — and there, `SYLPHEED_REST_RULE=last`
|
||||
(the final keyframe) scores **+0.9982** and **+0.9758** against the current
|
||||
rule's +0.9600 and +0.9643. ⚠️ Measured against single frames of a transient
|
||||
animation, so it fixes which pose matches *those* captures, not which is
|
||||
canonically at rest. Default unchanged — it is better on both screens where it
|
||||
fires and identical on the other three, but it would move 2 305 elements
|
||||
disc-wide on two measurements.
|
||||
✅ **And the picture is now coherent.** Applying the last keyframe to *every*
|
||||
element (not just the plateau-less ones) collapses all five screens — title
|
||||
0.9500→0.6819, main menu 0.9460→0.6416, `EXTRAS` 0.9440→0.5745, and both
|
||||
splashes render **blank**. The reason is the model: a group is **entry → hold →
|
||||
exit**, and the exit is the screen's *dismissal*. A displayed screen is sitting
|
||||
at the **hold**, not at its final pose — so `rest_plateau` is right, and the
|
||||
last keyframe is the *post-exit* state. It is right for a **transient** element
|
||||
precisely because a transient's settled state is "gone". The draw capture agrees
|
||||
independently: on the developer splash the `_eff` glows draw on frames 94–115
|
||||
and the logos on 116–211, so the glows are already over when the logos are up.
|
||||
**Three independent observables — animation timing, static composites and the
|
||||
per-frame draw log — all support: plateau where there is one, last keyframe
|
||||
where there is not.**
|
||||
📊 **Structurally, `last` is defensible for 2 293 of the 2 305 (99.5 %)**: 1 618
|
||||
end invisible (a transient, gone at rest), 675 end at maximum alpha (faded in
|
||||
and stopped — the final pose *is* settled), and only **12** end visible below
|
||||
full alpha, which are genuinely unclear. The dwell rule returns a mid-movement
|
||||
frame by construction. ⚠️ The 1 618 rest on an assumption worth seeing: that
|
||||
such an element's animation has finished by the time the screen settles — shown
|
||||
by the draw log for the two splash glows, unshown for the rest.
|
||||
📊 The disc-wide blast radius, for whoever decides: the rules **differ on 82.3 %**
|
||||
of those 2 305, so "either is fine" is not available — and the current rule
|
||||
returns a **zero-scale** (collapsed, pre-roll) pose for **195** of them against
|
||||
**43** under `last`, a 4.5× reduction in provably-degenerate results. That
|
||||
argues the same way as the captures, from the data's own structure. The single disagreement
|
||||
is `palogo_anima_eff.t32` on the developer splash, and the current answer is
|
||||
the defensible one there — see below. Still worth flagging plateau-less
|
||||
elements in an export rather than silently inheriting our guess; it is one pass
|
||||
over the keyframes.
|
||||
✅ **One concrete part of it is fixed (2026-08-28): `scale = 0` no longer
|
||||
renders at full size.** `blit`/`fill_quad` coerced `scale == 0` to 100 %, so an
|
||||
element collapsed to nothing was drawn full-size. The disc settles the reading:
|
||||
2 166 elements have a zero-scale keyframe, **not one is zero throughout**, and
|
||||
1 762 grow back out of zero — so 0 means collapsed, not "unset". **Renders are
|
||||
24/24 byte-identical** on `GP_TITLE` (all 16 builds), `GP_PAUSE_MENU` and
|
||||
`GP_OPTIONS`, so nothing you rely on moves; the 126 elements the old
|
||||
code actually painted are **all in `GP_READY_ROOM.pak`**, already a no-go.
|
||||
[`structures/ui-rat-layout.md`](../re/structures/ui-rat-layout.md)
|
||||
🔴 **"rest = last keyframe" is refuted** as the fix. The splash has three
|
||||
sibling glows with identical structure and times, differing in one byte
|
||||
(`a=212` vs `a=255` at `t=45`); that rule would make `anima_eff` alone
|
||||
invisible while its two siblings stay lit. The capture agrees weakly — box-mean
|
||||
ratios capture/render are gamearts 0.717, seta 0.723, **anima 0.772**, and a
|
||||
glow we drew that the game does not would put anima *below* its siblings.
|
||||
[`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)
|
||||
🟡 **The shift is still not adopted**, now for a different reason: it flips
|
||||
this element to the visibly wrong answer, so it and a decision about
|
||||
plateau-less elements have to land together, and neither half has a capture to
|
||||
verify against.
|
||||
**Default unchanged**, experiment reachable via `SYLPHEED_KF_TIME_SHIFT=1`.
|
||||
**What this means for you:** the interpolation *law* is settled (linear, 2
|
||||
units/frame); a multi-keyframe group's *timing* is not — do not expect a
|
||||
2-frame hold where the game holds 83.
|
||||
✅ **The seconds conversion is settled: `1 unit = 1/60 s`, a 30 Hz title.** The
|
||||
re-test this page used to name has been run — 300 submitted frames timed on the
|
||||
**idle** title (nothing loading) came out at **28.8 and 28.3 fps**, the same
|
||||
rate as the 27.6 fps measured during the loading splash. The competing 60 Hz
|
||||
reading is excluded: it needs the emulator at 47 % of real time while idling on
|
||||
a screen that costs ~5 draws per frame.
|
||||
A second, independent line agrees — the transition quad is declared black for
|
||||
12 units (0.20 s under this conversion) and a capture measured the pure-black
|
||||
plateau at 0.17–0.23 s
|
||||
([`screen-transitions.md`](../re/screen-transitions.md)).
|
||||
⚠️ Still **measured, not decoded**: no field on the disc says "sixtieths of a
|
||||
second".
|
||||
* **The paint order is derivable from the file.** Each `T8aD` sprite header
|
||||
carries a **`u16` layer key at `+0x0A`** (the upper half of the 32-bit word at
|
||||
`+0x08` is zero in all 21 184 sprites on the disc). Paint order is that key,
|
||||
**stable-sorted** so equal keys keep declaration order; elements with no sprite
|
||||
(`.prm` primitives, `.tbm`) have no key and take an implied one — the backdrop
|
||||
and dim quads sort early, the screen-transition fade (`pteff00.prm`,
|
||||
`pfeff00.prm`) sorts **last**. ✅ decoded. Checked against five paint orders read
|
||||
off the running game, one of them (`GP_SAVE_LOAD`'s slot-list header, 6
|
||||
instances) **exact and independent of the screens the rule was fitted to**, and
|
||||
against a fresh `EXTRAS` capture. It reorders 341 of the disc's 965 builds, so
|
||||
it is not a no-op dressed as a rule.
|
||||
🟡 **The one residual: ties.** Where two elements share a key the game
|
||||
sometimes paints them in an order nothing predicts — eight candidates refuted,
|
||||
including declaration order, RATC child order, keyframe times, resting X/Y and
|
||||
`kind`. Measured cost: on the three screens with ground truth it changes the
|
||||
blend of **one element on one screen** (a title glow). Take the stable sort and
|
||||
accept that.
|
||||
|
||||
* **Ⓑ returns to the title, and that title still works.** Ⓐ on the Ⓑ-returned
|
||||
title opens the main menu — measured, with Ⓐ on the boot title as the control in
|
||||
the same run. (Only the *attract*-returned title is inert, which is an emulator-
|
||||
harness curiosity, not a port concern.)
|
||||
* **Menu movement, measured off the running game.** ⬆⬇ move one item per press
|
||||
and **wrap at both ends** (5-item main menu and 3-item `EXTRAS` both). ⬅➡ do
|
||||
nothing. Ⓑ goes up one level **and restores focus to the item you came from**;
|
||||
Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. **Initial
|
||||
focus is not stable**: four boots of the same script gave `TUTORIAL`,
|
||||
`TUTORIAL`, `NEW GAME`, `NEW GAME`. Do not hardcode it; pick one and say you
|
||||
picked it. All **measured**, none of it on the disc.
|
||||
* **Each button's destination is measured; its GamePart id is not.**
|
||||
**`NEW GAME` → `DIFFICULTY`** (`EASY`/`NORMAL`/`HARD`/`BACK`, opening on
|
||||
`NORMAL`) **→ `SELECT DATA`** — it does *not* hang; the run then hits the
|
||||
already-documented `sub_823070B0` cache crash, which is not a menu problem.
|
||||
`LOAD GAME` → the save-slot list, `TUTORIAL` → the lesson list, `OPTIONS` →
|
||||
the settings menu, `EXTRAS` → `GP_TITLE` build 6, `EXTRAS ▸ MISSION SELECT` →
|
||||
the stage list. The
|
||||
GamePart ids (`3`, `25`, `8`, `5`, `7`) are the entries of the decoded id table
|
||||
whose **names match the screens seen**; that binding is authored, not measured.
|
||||
|
||||
* **A screen change is a fade through black.** Each screen carries a full-screen
|
||||
black `.prm` quad that paints last (`pteff00.prm` / `pfeff00.prm`) whose
|
||||
keyframe group *is* the transition: black at `T0`, clear by `T1`, clear until
|
||||
`T2`, then back to black on exit. ✅ decoded, with a disc-wide check — and in
|
||||
`GP_TITLE` exactly the six **screen** builds carry it while the six overlays do
|
||||
not. The fade-in length is `T1 − T0` and is read from the file (0.87 s for
|
||||
`EXTRAS`, 0.97 s main menu, 4.08 s title). ❔ **The fade-OUT length is not on
|
||||
the disc** — the last keyframe has no time slot; **measured ~0.4 s**, twice.
|
||||
The black hold measures 0.17–0.23 s. ⚠️ Do not time the fade-in off a capture's
|
||||
brightness: the incoming screen's own element animations dominate it and run
|
||||
much longer than the quad.
|
||||
|
||||
* **The movie manifest names every boot-side video by role.** `dat/tables.pak`
|
||||
entry `0x5b983a08`: `LOGO1`–`LOGO4` → `logo1.wmv`–`logo4.wmv` (**not on the
|
||||
disc** — this is why the splash is a screen), **`ADVERTISE_MOVIE` → `ADV.wmv`**,
|
||||
`STAFF_ROLL` → `SYLPH_HD720p_8M-CBR_2ch.wmv`, **`MS00A` → `S00A.wmv`** (the
|
||||
new-game intro, 93.9 s, with subtitle + `VOICE_S00A` + a text overlay),
|
||||
`MS01A` → `S01A.wmv`. ✅ decoded — and **`S00A.wmv` is now also measured**: matched off the running game at 0.96–1.000 with a strictly monotone playhead over 25 consecutive 0.5 s samples. It starts ~4.5 s after Ⓐ on the save slot.
|
||||
**The boot intro and the attract movie are the SAME asset** — there is no
|
||||
separate boot slot, and 15 of 19 captured attract frames match `ADV.wmv` with a
|
||||
monotonically advancing playhead ending at its full 137 s. One video, not two.
|
||||
✅ The attract movie **plays to its end**; nothing cuts it short.
|
||||
✅ **A movie is skippable with a single Ⓐ.** Measured: one tap ~45 s into the
|
||||
boot brought the title at ~57 s against a ~193 s no-input baseline over three
|
||||
boots, with Canary's own keystroke counter proving exactly one press was
|
||||
delivered — and the skipped-to title is **fully functional** (`PRESS Ⓐ` plate
|
||||
present, Ⓐ opens the main menu). What breaks the boot is *hammering*: 88
|
||||
presses left a permanent black screen. One press is fine.
|
||||
|
||||
* **The menu's sound events are named on the disc.** `tables.pak`'s `SOUNDS`
|
||||
record carries 322 `SE_*` cues, and the low block is the UI vocabulary — named
|
||||
after the *event*: `SE_UI_CURSOR` (2), `SE_UI_DECIDE` (3), `SE_UI_CANSEL` (4),
|
||||
`SE_UI_IMPOSI` (5, the error), `SE_UI_SUB_WIN_OPN`/`_CLS` (8/9),
|
||||
`SE_UI_SPLASH_IN`/`_OUT` (12/13). ✅ decoded, full list in
|
||||
[`data/se-ui-cues.txt`](../re/data/se-ui-cues.txt). They all live in **one**
|
||||
bank — `BANK_SE` is a single field naming `Static.slb`, and 0 of the 322 has
|
||||
its own `FILES` entry.
|
||||
🟡 **Which event fires which cue is a name match**, not a measurement — strong,
|
||||
because these are the authors' own event names, but nobody has watched the game
|
||||
emit cue 2 on a d-pad press. The port is authoring it.
|
||||
✅ **The SE audio IS extractable — by playing it.** (This corrects an earlier
|
||||
"cannot be extracted" on this page.) The disc carries no index: `Static.slb` has
|
||||
**0 `RIFF`/`seek`/`WAVE`** and there is **no XACT container anywhere** (0 ×
|
||||
`XGSF`/`SDBK`/`WBND` in 1.08 GB of `sound.pak`; no `XACT` string in the
|
||||
executable — those extensions are the authoring tool's). But Canary's
|
||||
`--xma_param_probe=true` logs every stream's head bytes, and searching them in
|
||||
`Static.slb` locates the wave exactly:
|
||||
**d-pad move → `0x1ec0` (4 pkts / 8 192 B, 0.533 s); Ⓐ confirm → `0x5d6c0`
|
||||
(6 pkts / 12 288 B, 1.016 s); Ⓑ back → `0x0ec0` (2 pkts / 4 096 B, 0.344 s)** —
|
||||
every cue the five screens need. Move and back reproduce with identical head
|
||||
bytes across **two independent boots**. Each matched at one offset only, and
|
||||
the first two are contiguous
|
||||
(`0x0ec0 + 4096 = 0x1ec0`) — the bank is a packed run of whole 2 048-byte
|
||||
packets with no delimiters, which is why nothing could be scanned for.
|
||||
❔ **⬅ and ➡ play nothing distinct** — no new stream on either, so leave them
|
||||
silent (the probe dedups, so this excludes a *distinct* invalid cue, not a
|
||||
quiet replay of an already-heard one).
|
||||
🟡 The Ⓐ press both confirms and opens a screen, so whether its wave is the cue
|
||||
named `SE_UI_DECIDE` or `SE_UI_SUB_WIN_OPN` is not separated.
|
||||
⚠️ The order is **not** cue-id order, so the index must be observed per cue, not
|
||||
counted. ✅ **The slices decode**: 0.533 s, 0.344 s and 1.016 s of mono 48 kHz
|
||||
audio with the attack-and-decay shape of UI blips, via
|
||||
`tools/re-capture/slb_extract_wave.py` — whose wrapper reproduces a known-good
|
||||
`BGM_001` decode to the same 173.808875 s, so it is verified, not assumed.
|
||||
|
||||
* **The boot sequence is not data-driven — the port authors it.** ❔ Four places
|
||||
were checked and the order is in none: `config.ini`'s `[SYSTEM]` is empty, the
|
||||
movie manifest carries assets not transitions, the requested GamePart id lives
|
||||
**only as a stack argument in flight** (no persistent field, no literal store),
|
||||
and the string `GP_ADVERTISE_DEMO` has **zero xrefs**. A transition is a call
|
||||
with an id argument, chosen by code.
|
||||
🟡 **But the states have names, and the transition uses them.** `sub_821C6458`,
|
||||
the title part's state function, calls **`sub_821CC860(…, "<NAME>", 0)`** with
|
||||
`TITLE_SCREEN`, `TITLE_MENU` and `LOADING` — the three states measured off the
|
||||
game, in the game's own words — and installs the result. So a transition is a
|
||||
call with a **name** argument, which is also why `GP_ADVERTISE_DEMO` has no
|
||||
xrefs: at this level the screen graph is name-keyed, not id-keyed.
|
||||
✅ The argument is now **decoded** at 46 of that function's 48 call sites (28
|
||||
distinct names). ⚠️ It is a **generic name-keyed lookup**, not a screen factory —
|
||||
its arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
|
||||
`GAMMA_RGB`. An earlier version of this page claimed `DIFFICULTY` and
|
||||
`EXTRA_MENU` as corroborated screen names; **neither is ever the argument**, and
|
||||
only `TUTORIAL_MENU` survives.
|
||||
✅ **And the state machine itself is decoded**: `state = this+136`, ten states
|
||||
dispatched through a jump table at `0x821C6498`, with **18 transitions** each a
|
||||
literal `li`/`stw`. States **0, 2, 8** install `TITLE_SCREEN`, `TITLE_MENU`,
|
||||
`LOADING`. `4 → 0` is the only edge back to the title, reached from `2 → 4` —
|
||||
which matches Ⓑ-returns-to-title as measured. Full graph in
|
||||
[`data/title-state-machine.txt`](../re/data/title-state-machine.txt).
|
||||
⚠️ **All of that is ONE PHASE.** `GamePart_Title` dispatches on an outer phase
|
||||
field at `this+132` (five values) before reaching any of it: phase 0 is the
|
||||
**developer splash** (`sub_821C5690`, the same function the corpus fingered
|
||||
independently), phase 4 is the title/menu machine. The ten states and
|
||||
eighteen edges above live inside phase 4 alone.
|
||||
✅ **State 4's edge conditions are an event code** — `sub_821C6458`'s third
|
||||
argument. State 4 is the input-waiting state (reached straight after the menu
|
||||
is installed) and handles 6 of 26 events: **`0` → title**, **`3`, `5`, `8`,
|
||||
`25` → `LOADING`**, `10` → state 5.
|
||||
🟡 What the event *numbers* mean is not decoded — button id, menu row, or
|
||||
message id — so the port still takes the button→destination map from
|
||||
measurement. The one-to-title / four-to-loading shape matches the five-item
|
||||
menu with Ⓑ, but that is a **count-match, not a mapping**. The sequence itself is fully measured:
|
||||
splash → `ADV.wmv` → title + `PRESS Ⓐ` → (idle ~8–10 s → `ADV.wmv` in full →
|
||||
title) → Ⓐ → main menu, with Ⓑ from the main menu returning to the title.
|
||||
|
||||
* **`config.ini` picks the language, and that is what picks the EN/JP build.**
|
||||
The disc's **only** config file (400 bytes, at the root; one `find` over the
|
||||
whole extract). Its `[LANGUAGE]` section maps the console's `XC_LANGUAGE_*`
|
||||
value to `eng`/`jpn`/`deu`/`fra`/`esp`/`ita`, defaulting to `eng` — that code
|
||||
selects `GP_TITLE`'s English or Japanese build and the `<lang>.pak` families.
|
||||
✅ decoded.
|
||||
❔ Its `[SYSTEM]` section — which the file's own comment says holds what the
|
||||
game and every game part share — is **empty**, so the boot *order* is not in
|
||||
disc-side configuration at all.
|
||||
* **Five GameParts are named but never registered.** Pulling every
|
||||
`RegisterToFactory<N, class silph::GamePart_X>` diagnostic string binds **24 of
|
||||
the 29 ids to a C++ class**
|
||||
([`data/gamepart-class-ids.txt`](../re/data/gamepart-class-ids.txt)). Ids `1`,
|
||||
`2`, `16`, `18`, `28` have no registration site — including
|
||||
**`GP_ADVERTISE_DEMO` (1)**, which agrees with the measurement that the attract
|
||||
loop is the *title* replaying `ADV.wmv` rather than a separate part. 🟡 an
|
||||
argument from a diagnostic string, not from the code.
|
||||
Also: **`3` and `4` are both `GamePart_SaveLoad`** — one part, two ids.
|
||||
|
||||
* **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the
|
||||
executable's own registration strings. ✅ This is the screen vocabulary; which
|
||||
button reaches which entry is Q4 and is *not* part of it.
|
||||
* **The logo splash is a screen, not a video, and it renders.** `logo1`–`logo4`
|
||||
are manifest-bound with no `.wmv` on the disc. ✅ The screen is **two** bundles
|
||||
in `GP_TITLE.pak`, each shipped twice: entries **10/13** the white
|
||||
`SQUARE ENIX` publisher logo, entries **11/14** `GAME ARTS`/`SETA`/`studio
|
||||
anima`. ⚠️ They are **invisible to the default `screen list`/`render`** —
|
||||
`is_build` rejects them for having no `.rat` child. Pass **`--all`**, which
|
||||
renumbers `--build`. So the first of the five screens does have a reference
|
||||
composite. ✅ **And a capture**: both halves edge-correlate to their renders at
|
||||
**0.91** and **0.98** at zero shift, each rejected by the other (0.03, −0.03).
|
||||
✅ **Its timing is DECODED, not just measured.** The splash **fades both ways**
|
||||
and the bundles carry the keyframes: `SQUARE ENIX` `[15 30 235 239 251 255]`,
|
||||
developer logos `[15 30 190 194 206 210]`, each with an `_eff` glow child on
|
||||
`[15 30 45]`. Under `1 unit = 1/60 s` that is a 0.25 s ramp in, a **3.42 s**
|
||||
/ **2.67 s** hold, and a **0.33 s** fade out — and a 10 fps capture measures
|
||||
≈ 3.5 s / ≈ 2.4 s holds and **≈ 0.3 s** fade-outs. The visible brightness
|
||||
overshoot on the way in is the `_eff` glow ramping after the logo, not a
|
||||
rendering artifact. Wall-clock: logos ≈ 0.4–4.7 s and ≈ 4.9–8.4 s, then
|
||||
`ADV.wmv` from ≈ 8.9 s. ⚠️ The cyan `SQUARE ENIX` at ≈ 9.5 s is the
|
||||
**movie's** opening, not a third splash screen.
|
||||
* **Sprites carry their own labels.** No font rendering or localisation is needed
|
||||
for this milestone. ✅ — and the localisation is *already baked in*: the
|
||||
Japanese screens are separate builds in the same pak, not a text swap.
|
||||
|
||||
## Facts the port will trip over
|
||||
|
||||
* **`ADV.wmv` is WMV3 video + WMA Pro audio**, 1280×720 at 30 fps, 137 s. Godot 4
|
||||
plays only Ogg Theora natively. How to handle that is the port's decision, not
|
||||
ours — but it is not optional.
|
||||
* **The disc holds 3.3 GB of video, and exactly two files are in scope:**
|
||||
`dat/movie/ADV.wmv` (the boot intro *and* the attract loop — one asset) and
|
||||
`dat/movie/S00A.wmv` (the new-game intro, 93.9 s). Named from the movie
|
||||
manifest, ✅ decoded.
|
||||
* **`Static.slb` over-declares its size** by 616 768 bytes — it is the
|
||||
highest-offset entry in `sound.pak` and its size field is an allocation size. A
|
||||
reader must allow a short read there and only there.
|
||||
* **Voice downmixes to mono, music does not.** The left-channel downmix is correct
|
||||
for spoken lines and discards half a music mix.
|
||||
* **A music bank is TWO STEMS THAT PLAY TOGETHER — do not concatenate.**
|
||||
✅ The "10 KB + 4.47 MB + 4.67 MB" reading was wrong: the 10 KB is the bank
|
||||
header, and a bank is exactly **two waves of identical duration** (32/32 banks
|
||||
on the disc). `BGM_001`'s two are **sample-synchronous** — transient
|
||||
correlation peaks at lag 0.00 s over ±5 s, and both stop at the same
|
||||
millisecond, 167.663 s. Wave 1 is quieter, far more L/R-decorrelated and has
|
||||
almost no bass, so it reads as a surround-rear pair or a second intensity
|
||||
layer — 🟡 which of those is unsettled, and `ChannelMask` is `0x0002` on both,
|
||||
so the file will not say. Today's 347 s concatenation plays the piece twice,
|
||||
the second time as a bass-less stem.
|
||||
❔ **And it is not a seamless loop**: `BGM_001` fades out at 167.663 s and is
|
||||
followed by 6.15 s of silence, with no loop-point field identified. A menu loop
|
||||
is authored.
|
||||
✅ **The menu's music is `BGM_103`.** The cue *table* cannot say — its BGM
|
||||
entries are numeric — but `GamePart_Title`'s `sub_821C5580` plays **cue 1103**,
|
||||
and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are
|
||||
byte-for-byte the two streams the XMA probe saw decoding at the main menu.
|
||||
Static code, disc census and runtime all agree. The port does **not** have to
|
||||
choose a track.
|
||||
* **`JNGL_001.slb` does not decode.** One bank in 9 519; its payload is not a whole
|
||||
number of XMA1 packets from any known data offset.
|
||||
|
||||
## What is still open
|
||||
|
||||
Every question above is answered, so this is the honest residue rather than a
|
||||
work queue. **None of it blocks the five screens.** (Q1's seconds conversion was
|
||||
here until 2026-08-28 and is now settled.)
|
||||
|
||||
| | what | why it is stuck |
|
||||
|---|---|---|
|
||||
| 🟡 | **cue NAME → event binding** (Q8) | event→**wave** is measured for move/confirm/back; that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still read off the authors' identifiers |
|
||||
| ❔ | **the other ~319 SE cues** (Q8) | located one at a time by triggering them; only the three the menu needs have been done |
|
||||
| 🟡 | **the paint-order tie-break** (Q3) | eight candidates refuted; costs one element's blend on one screen |
|
||||
| 🟡 | **GamePart ids behind the buttons** (Q4) | the *screens* are measured; the ids are a name match onto the executable's class names |
|
||||
| 🟡 | **the boot transitions in code** (Q6) | both levels decoded — phase at `this+132` (`entry→2`, `2→0`, `2→3`, `3→4`, `4→2`) and state at `this+136` inside phase 4. Phase 0 = splash (`LOGO`), phase 2 = title + `PRESS Ⓐ`, phase 4 = menu. Unknown: what the event *numbers* mean |
|
||||
| ❔ | **builds 0/1 and 10/11**, the `DELTASABER` plates (Q2) | never seen anywhere in the boot path, the title-side screens or the attract loop. A mission load is the remaining candidate and this container kills runs before one completes |
|
||||
|
||||
(An earlier version of this table called the audio items blocked on "an emulator
|
||||
whose audio path can be observed". That was wrong — this build already has
|
||||
`--xma_param_probe`, and using it settled both.)
|
||||
|
||||
⚠️ **A container caveat that bounds all of the above:** the emulator has twice
|
||||
been killed mid-run with no crash line in its own log (at ~50 s and ~145 s), on a
|
||||
box sitting at ~1 GB free with swap exhausted. Dynamic experiments here have to
|
||||
fit in roughly two minutes of guest time, which is why several of these residuals
|
||||
are unfinished rather than unattempted.
|
||||
|
||||
## Reference data
|
||||
|
||||
Committed alongside the findings, so the port can be built without a disc in the
|
||||
loop during development:
|
||||
|
||||
* `sylpheed-cli screen info --build <n> GP_TITLE.pak` — the element table, per
|
||||
build, with pivots, kinds, focus links, keyframes and resting poses.
|
||||
* `sylpheed-cli screen render` — the reference composite. When the port draws a
|
||||
screen, this is what it should be diffed against; where they disagree, one of
|
||||
them is wrong and the disagreement is worth reporting back.
|
||||
* `docs/re/captures/` — framebuffer captures of the real screens, for anything
|
||||
that has to be checked against the game rather than against our renderer.
|
||||
314
docs/port/MISSION.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Primary objective — answer everything the menu port needs
|
||||
|
||||
**Status:** active, set 2026-08-28. This replaces "work the RE backlog" as the
|
||||
agent's primary objective. It does not change what the agent *does* — it is still
|
||||
reverse engineering — it changes what earns attention: an item is worth doing
|
||||
when the menu port is blocked on it.
|
||||
|
||||
## Who builds what
|
||||
|
||||
**You do not build the port.** A separate agent will build it, from what you
|
||||
produce. Your deliverable is decoded, verified, written-down answers plus the
|
||||
reference data that proves them.
|
||||
|
||||
| | container agent (you) | port agent |
|
||||
|---|---|---|
|
||||
| decodes the disc | ✅ | ❌ — consumes your answers |
|
||||
| measures the running game | ✅ | ❌ — no emulator |
|
||||
| writes `docs/re/` and `docs/port/HANDOFF.md` | ✅ | reads them |
|
||||
| Godot project, asset pipeline, transcoding | ❌ | ✅ |
|
||||
|
||||
If you find yourself designing an export schema or writing GDScript, you have
|
||||
crossed the line. Stop and go back to the question you were answering.
|
||||
|
||||
`crates/sylpheed-viewer` is also **not yours to change** for this objective. The
|
||||
Explorer is the human's tool for exploring and verifying the RE work, it keeps its
|
||||
static-data-only rule, and the port does not depend on it.
|
||||
|
||||
## The target
|
||||
|
||||
Someone else has to build this, from your answers alone:
|
||||
|
||||
```
|
||||
developer logo splash → intro video → title / PRESS Ⓐ → main menu → submenus
|
||||
```
|
||||
|
||||
No gameplay, no 3D, no HUD, no missions. If an answer is not needed to put those
|
||||
five screens on a display and let a person move through them with a d-pad and Ⓐ,
|
||||
it is not in this objective.
|
||||
|
||||
## What is already answered
|
||||
|
||||
Do not re-derive these. They are in `docs/re/` and the
|
||||
[disc atlas](../re/disc-atlas.html):
|
||||
|
||||
* **The screen archive.** `GP_TITLE.pak` holds the whole title-side tree — build 4
|
||||
the title with animating wordmarks, build 5 the five-button main menu, builds
|
||||
6/8/9 submenus, and the developer splash as the `palogo` bundle in the same pak.
|
||||
* **Buttons are identifiable as data.** Element kind `0x3002` is a button, `0x0`
|
||||
decoration, `0x10` a primitive; buttons sort top-to-bottom by resting Y; each
|
||||
pairs with an `f`-suffixed highlighted variant.
|
||||
* **The screen vocabulary.** The GamePart id table, 29 entries at `.rdata
|
||||
0x820A1630`, confirmed by the executable's own factory-registration strings.
|
||||
* **The resting pose rule** — the hold, not the longest dwell — and that a
|
||||
keyframe is the *start of a ramp*.
|
||||
* **Screen composition**, pixel-accurate for the tutorial pause menu and the title
|
||||
main menu, via `sylpheed-cli screen render`.
|
||||
* **The logo splash is not a video.** `logo1`–`logo4` are manifest-bound with no
|
||||
`.wmv` on the disc; the splash is the RATC screen, which already renders.
|
||||
|
||||
## The open questions — these are the objective
|
||||
|
||||
Ordered by what blocks the port earliest. Each is done when its **gate** exists:
|
||||
a written `docs/re/` result with the evidence, and reference data committed
|
||||
alongside it.
|
||||
|
||||
| | Question | Gate |
|
||||
|---|---|---|
|
||||
| **Q1** | **What is a keyframe time?** Values run 16…269. 60 Hz frames would make the title intro ~4.5 s — plausible and untested. Also: is the ramp linear, or eased? | A measured answer against the running game, not an inference. Everything animated downstream depends on this number |
|
||||
| **Q2** | **Which build is which screen state?** Confirm build↔state for splash, title/PRESS Ⓐ, main menu and each submenu | A table, each row confirmed against a capture of the real screen |
|
||||
| **Q3** | **Paint order for these six screens.** Solved at runtime, unsolved from the file — the declaration table is provably not it | Either a rule derived from the bundle, or six measured orders and a clear statement that no file-side rule was found |
|
||||
| **Q4** | **What does each button do?** Labels are baked into the sprites; no decoded field says which GamePart a button opens | Button → GamePart id, from code or from driving the game. Say which |
|
||||
| **Q5** | **Navigation semantics.** Initial focus, wrap-around at the ends, whether left/right does anything, what B does on each screen | Observed behaviour, per screen |
|
||||
| **Q6** | **The boot sequence, and what drives it.** Order is observable; the *data or code* that sequences it is not decoded. Include the attract loop and what returns to the title | The sequence, plus whatever the game reads to decide it |
|
||||
| **Q7** | **Transitions.** What happens visually between screens — the `pteff00.prm` quads, a fade, a cut — and its timing | Described and timed against a capture |
|
||||
| **Q8** | **Menu audio.** Which BGM per screen; which cue on move / confirm / back / error. The cue table is complete; the event binding is not | Cue names bound to events, with how you established each |
|
||||
| **Q9** | **Video binding.** Which movie is the boot intro vs the new-game intro; whether playback is skippable and what ends it | Named movies plus the playback rules |
|
||||
| **Q10** | **What are a music bank's sub-waves?** `BGM_001.slb` is three sub-waves — 10 KB, 4.47 MB, 4.67 MB — and we currently **concatenate them blindly** into one 347 s track. Two near-equal halves could be intro + loop, or two variations, or two halves of one piece. A menu that loops its music needs to know which | The role of each sub-wave, established for at least the menu BGM. "Concatenate" is a decision, not a default — right now it is a default nobody chose |
|
||||
| **S1** | ~~**Ready Room probe.**~~ **DONE 2026-08-28 — [no-go](../re/ready-room-probe.md).** It is 2D and enumerates fine, but the pak is briefing/tactical-map content, not the Ready Room menu | ✅ go/no-go written |
|
||||
|
||||
## 🔴 Emulator-side questions are blocked — the title is not reachable here
|
||||
|
||||
**Status 2026-08-29, instrument-verified.** Two open items need a running menu:
|
||||
the gamma control behind [tone curve](../re/structures/ui-render-tone-curve.md),
|
||||
and separating `8AX` from `ptbase` in
|
||||
[8AX](../re/structures/ui-8ax-fullres-background.md). Both are blocked.
|
||||
|
||||
✅ **The negative is now solid.** A probe that restarts its capture stream every
|
||||
30 s and cross-checks itself against an independent grabber every 60 s
|
||||
(9 checks, 8 agreeing to within 2 %) ran **560 continuous seconds from launch at
|
||||
3.30 fps**: the interactive title's green Ⓐ plate never appeared, while the game
|
||||
rendered throughout. The final frame correlates 0.0145 / −0.0047 / 0.0102 with
|
||||
our title / main-menu / `EXTRAS` renders — attract-movie content.
|
||||
|
||||
❔ **Why is unknown.** `live-title-press-a.png` (753 glyph pixels) proves it was
|
||||
reachable from this container on 2026-08-28. Clearing the shader cache fixed a
|
||||
*black surface* but not this.
|
||||
|
||||
⚠️ Neither blocked item blocks the five menu screens. See
|
||||
[capture-harness-status](../re/capture-harness-status.md) for the full trail,
|
||||
including three earlier "the title never appears" claims that were withdrawn
|
||||
because the instrument was broken each time.
|
||||
|
||||
## 🟡 Needs one more run — a Japanese-locale capture
|
||||
|
||||
Recorded rather than worked around, per "do not improvise around a blocker".
|
||||
|
||||
Two questions have converged on needing **one capture we cannot take**: a running
|
||||
capture of `GP_TITLE` **build 7**, the Japanese title screen.
|
||||
|
||||
* Q1's keyframe-time association. The shifted reading is favoured 26× by a
|
||||
calibration-free measurement, and the only render it changes on the whole disc
|
||||
is build 7 ([`ui-keyframe-time-unit.md`](../re/ui-keyframe-time-unit.md)).
|
||||
* What `rest()` should return for a plateau-less element. The one element that
|
||||
discriminates, `ptlogo_eff3.t32`, is also in build 7
|
||||
([`structures/ui-resting-pose.md`](../re/structures/ui-resting-pose.md)).
|
||||
|
||||
**🔴 The "cannot" was wrong, and is withdrawn (2026-08-29).** It is true that
|
||||
`user_language` is only ever `DECLARE_int32` here, with no `DEFINE` and no entry
|
||||
in `xenia-canary.config.toml`, so there is no flag to pass — and passing an
|
||||
unknown one is specifically dangerous, because `run-canary`'s own header records
|
||||
that xenia calls `ShowSimpleMessageBox` from `ParseLaunchArguments` *before*
|
||||
logging starts. But the cvar is not the only route, and I stopped at the first
|
||||
one I checked.
|
||||
|
||||
**The language is persisted, and canary's own file is writable.**
|
||||
`kernel_state.cc` builds `XConfig` over `<storage_root>/xconfig.settings`, which
|
||||
exists here at `/sylph-home/re/.local/share/Xenia/xconfig.settings` (6 680 B).
|
||||
`user.language` is a **big-endian u32 at file offset `0x912`**, located by three
|
||||
independent landmarks rather than guessed:
|
||||
|
||||
| landmark | expected | found |
|
||||
|---|---|---|
|
||||
| `music_volume` (`User+449`) | `0.7f` | BE float at 2727 → `User` base `0x8e6` |
|
||||
| `language` (`User+44`) | `1` = `kEnglish`, the hard-coded default | **1** at `0x912` |
|
||||
| `country` (`User+64`) | United States | `103` at `0x926` |
|
||||
|
||||
`XLanguage::kJapanese = 2` (`xbox.h:307`). Writing 2 there and restoring
|
||||
afterwards is using canary's own persistence, not patching its code.
|
||||
|
||||
**🔴 DIAGNOSED 2026-08-29 — the harness, not the game.** `screenshot` costs
|
||||
**10.8 s while xenia is running** and **0.117 s once it is killed** (92×, at a
|
||||
1-minute load average of 1.80, so it is contention with the emulator). A
|
||||
two-grab polling loop therefore samples every **~41 s**, against a title screen
|
||||
this corpus documents as lasting *a few seconds*. Four runs — two locales, two
|
||||
launch paths, two gamma settings — were all blinking slower than the event.
|
||||
🔴 **…and that diagnosis is itself refuted (same day).** The faster probe was
|
||||
built — 3.98 s → **0.29 s** per sample, 13.7×, control-verified at 753/327 — and
|
||||
at 3.99 fps for **420 continuous seconds (1 674 samples)** the interactive title
|
||||
*still* never appeared. Sampling rate was a real defect and not the cause.
|
||||
✅ So "the interactive title does not appear mid-run without a pad press" is
|
||||
reinstated, now as a dense measurement. ⚠️ Its reach is a **mid-run** window; it
|
||||
says nothing about the boot title.
|
||||
🟡 Leading hypothesis, unconfirmed: the plate appears only in the **boot** title
|
||||
window and the attract loop's title has none — which is precisely what
|
||||
`title_states_capture.sh` was written to test. The experiment is to start the
|
||||
fast probe from t=0, not attach to a run already in progress.
|
||||
See [capture-harness-status](../re/capture-harness-status.md).
|
||||
|
||||
**🟡 Still not settled — two runs, and the reason moved again.**
|
||||
|
||||
*Run 1 (2026-08-28)* reported "title not seen" in 787 s. **That was a broken
|
||||
tool, not the game.** `wait_title.sh` still carried the single-pixel oracle that
|
||||
`is_title.py` was written to replace — a 1280×720 coordinate sampled against the
|
||||
1279×675 game surface, so it always reads the copyright line. Fixed; it now
|
||||
delegates to `is_title.py`.
|
||||
|
||||
*Run 2 (2026-08-29)*, with the working oracle and the profile flag the English
|
||||
captures use, **still did not reach the interactive title.** Not one frame in the
|
||||
run showed a single green-Ⓐ glyph pixel, and content correlation against either
|
||||
build-7 render never exceeded **0.22**. The game sat in the attract movie
|
||||
throughout, polling `XamInputGetKeystrokeEx` (601 calls) — i.e. alive and waiting
|
||||
for input, not hung.
|
||||
|
||||
**✅ The control was run (2026-08-29), and it removes the locale from the
|
||||
picture.** Same flags, same oracle, English locale: **75 samples over 734 s, every
|
||||
one glyph = 0.** The English boot does not present the interactive title either.
|
||||
Canary was alive throughout and polling `XamInputGetKeystrokeEx` (1 801 calls);
|
||||
the frame at 734 s has content but correlates only **0.15** with build 4's render
|
||||
and 0.05 with the main menu, i.e. it is an attract-movie frame, not title art.
|
||||
|
||||
So the Japanese run was **not** failing because of the locale — neither locale
|
||||
reaches the interactive title in ~12 minutes of no pad input. **🔴 Tried the proven path too, and I am stopping this line (2026-08-29).**
|
||||
Run 3 launched exactly the way `boot_menu.sh` does — `DISPLAY=:98`, `--apu=sdl`,
|
||||
`/dev/shm/xenia_*` cleared, the existing profile signed in — and drove
|
||||
`skip_intro.sh`, the detector that is documented to work. It classified **every
|
||||
one of 20 samples over 604 s as "movie"** and waited them all out, then timed
|
||||
out. A direct check at 604 s confirms it was right: zero green-glyph pixels,
|
||||
`screen_id` = other, warm mean (69,53,40), correlation **0.09** with build 7. The
|
||||
game really was playing attract movies for ten minutes.
|
||||
|
||||
So: **three runs, two locales, two launch paths, ~35 minutes of emulator time, no
|
||||
interactive title.** Either the attract loop is far longer than the 600–780 s
|
||||
windows tried, or something has regressed since `live-title-press-a.png` was
|
||||
captured (that one came from a pad-driven boot — commit `88b3ce9`, "booting to
|
||||
the main menu and walking it").
|
||||
|
||||
⚠️ **Not worth more loop iterations.** This capture does not block any of the
|
||||
five menu screens, and it has now cost five. It is written down so a later
|
||||
session with a reason to spend an hour on the boot path can pick it up; the
|
||||
locale mechanism, the fixed oracle and the launch recipe are all in place, so
|
||||
what is left is patience with the attract loop, not tooling.
|
||||
|
||||
The locale is restored to English; `set_console_language.py ja` flips it back in
|
||||
one command.
|
||||
|
||||
⚠️ Neither question blocks the five menu screens. Q1's *interpolation law* is
|
||||
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
|
||||
it is better than a guess, because the port agent will otherwise have to author
|
||||
the mapping by hand and needs to know it is authoring rather than transcribing.
|
||||
|
||||
For each question, the answer is one of:
|
||||
|
||||
* **decoded** — here is the field, here is the disc-wide check;
|
||||
* **measured** — not on the disc in any form we found, but here is what the
|
||||
running game does, and here is the capture;
|
||||
* **undecodable, with reach** — we looked here, here and here, and this is why it
|
||||
is not there.
|
||||
|
||||
Never a fourth thing. In particular: if Q4 ends as "read the labels off the sprite
|
||||
images by eye", say exactly that — it is then an authored mapping on the port
|
||||
side, not a disc fact, and mislabelling it would put a guess into the port wearing
|
||||
the badge of a measurement.
|
||||
|
||||
## The Ready Room probe (S1) — one iteration, then stop
|
||||
|
||||
`GP_READY_ROOM.pak` is the largest UI archive on the disc, 1 106 entries, and only
|
||||
**6 of its names resolve**. It is also ISL-scripted. That is either a week or a
|
||||
quarter, and one cheap test tells you which.
|
||||
|
||||
Our screen catalog enumerates bundles by **content**, not by name — `is_build` /
|
||||
`is_composable` read the bytes — so unrecoverable *paths* do not necessarily mean
|
||||
unrenderable *screens*.
|
||||
|
||||
```bash
|
||||
sylpheed-cli screen list "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak"
|
||||
sylpheed-cli screen render --build <n> "$SYLPHEED_DISC/dat/GP_READY_ROOM.pak" /tmp/rr.png
|
||||
```
|
||||
|
||||
Report how many builds it finds, whether any composite looks like a Ready Room,
|
||||
and **whether the room is 2D at all** or 3D with a UI overlay — if it is 3D the
|
||||
answer is no-go by definition, not "try harder".
|
||||
|
||||
**Then stop and write the go/no-go.** Do not start Ready Room work on your own
|
||||
authority.
|
||||
|
||||
## Publishing a decoder state the port pins
|
||||
|
||||
The port's exporter depends on `sylpheed-formats` **by revision**, so a commit of
|
||||
yours becomes part of its build. That creates an obligation that is easy to miss:
|
||||
|
||||
⚠️ **A commit reachable only from an `auto/*` branch is not safe to pin.** If the
|
||||
branch is later deleted, or — worse — **squash-merged**, the object is orphaned.
|
||||
Squash creates *new* commits, so `main` appears to contain the work while the
|
||||
pinned sha becomes unreachable, and the port stops building for anyone doing a
|
||||
fresh checkout. Silently, at their build, long after the moment of breakage.
|
||||
|
||||
**So when you land something the port needs, tag it:**
|
||||
|
||||
```bash
|
||||
git tag -a formats-pin-$(date +%F) -m "what the port gets from this state"
|
||||
push-work # --follow-tags publishes annotated tags with the branch
|
||||
```
|
||||
|
||||
A tag is a permanent ref. It survives branch deletion and squash-merge, it is
|
||||
self-documenting in the port's `Cargo.toml`, and it fails loudly at *fetch* if it
|
||||
ever goes missing rather than silently at build.
|
||||
|
||||
Then tell the port over the message channel that a new pin exists. It bumps
|
||||
deliberately, as its own commit — that is how it stays current without floating,
|
||||
which would only give it staleness it cannot see.
|
||||
|
||||
`formats-pin-2026-08-29` at `7eeae30` is the first, created because the port had
|
||||
already pinned a commit that lived on one topic branch and nowhere else.
|
||||
|
||||
## Handing it over
|
||||
|
||||
[`HANDOFF.md`](HANDOFF.md) is the single page the port agent reads. Keep it
|
||||
current as you answer questions: it is a summary with links into `docs/re/`, not a
|
||||
second copy of the findings. An answer that is not reachable from HANDOFF.md has
|
||||
not been delivered.
|
||||
|
||||
## Out of scope
|
||||
|
||||
3D, gameplay, HUD, missions, save/load, localisation beyond English, the Godot
|
||||
project itself, any asset pipeline, and any archive outside `GP_TITLE`,
|
||||
`tables.pak`, `sound.pak` and `dat/movie/` — except for the S1 probe.
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README](README.md).
|
||||
|
||||
Also durable, and worth reading before proposing anything:
|
||||
[`REFUTED.md`](REFUTED.md) — what has already been tested and died ·
|
||||
[`METHOD.md`](METHOD.md) — the traps this corpus has already paid for ·
|
||||
[`BACKLOG.md`](BACKLOG.md) — what is still open.
|
||||
|
||||
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
|
||||
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
|
||||
Promote to a prose `structures/…md` file when a format needs behavioural notes beyond layout.
|
||||
@@ -13,9 +18,10 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
||||
| IPFB `.pak` archive | ✅ | `sylpheed-formats/src/pak.rs` + `tests/pak_idxd_disc.rs` | header + 12-byte TOC, Z1/zlib payloads |
|
||||
| name-hash (TOC keys) | ✅ | `sylpheed-formats/src/hash.rs` | Barrett-reduction hash; recovers original paths |
|
||||
| IDXD object/table | ✅ | `sylpheed-formats/src/idxd.rs` + `tests/idxd_records_disc.rs` ([container](structures/idxd-container.md)) | **The binary record/index region in front of the string pool is DECODED** (2026-08-25), closing the parser's long-standing "not yet decoded" note. Uniform 16-byte records `{name_hash, name_off, field_begin, field_end}` sorted by hash and binary-searched, then a field count, 12-byte fields `{key, name_off, value_off}` sorted by key, a pool size, and the string pool; the trailing `pool_size == file_len - pool_base` identity makes the layout self-checking. Verified over the **whole disc** with **zero** failures: 7 750/7 750 objects, 190 782/190 782 records reproducing their stored `tag_hash`, 1 271 462/1 271 462 named fields reproducing their key — and `IXUD` is the same container with `ixud_hash`, UTF-16BE and all offsets in **chars** (1 104/1 104 objects, 628 165/628 165 fields). **Field names are stored on disc** — a field's middle word points at its own name — so nothing needs preimage search except the **504** field entries disc-wide that are hash-keyed with no name — which are only **42 distinct keys**, each in 12 places (the page for these was never written — the finding is in this row), and are provably unrecoverable from the hash alone; the other 1 485 073 nameless fields are *positional*, keyed by a literal integer (line slots, movie ids). ⚠️ **Two long-held beliefs WITHDRAWN**: the word at `0x08` is **not a schema hash**, it is record 0's `name_hash` (7 750/7 750) — the header has no type field at all, so an object's kind is known only from the caller that loads it; and the field's middle word is **not** an always-`0xFFFFFFFF` flags word. The first was caught by a test asserting that every movie id names a real record: `1005 -> STAGE10_PHASE01` failed because `tag_hash("STAGE10_PHASE01")` **is** `0x067025B9`, that table's supposed schema id. 🟡 the legacy value-before-key string-pool reader is now known to be an *approximation* of the real table, and every number derived from it is re-checkable but not yet re-checked |
|
||||
| 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 ❔ |
|
||||
| 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 |
|
||||
@@ -24,7 +30,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
||||
| Weapon fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/weapon-struct-runtime.md) · [DATA SHEET route](weapon-datasheet-runtime.md) | **Solved.** Canary maps guest RAM into `/dev/shm`, so the parsed `Weapon`/`Shell` objects are readable live; their layout is solved against disc ground truth (zero contradictions over 100+ records). All 126 weapons, exact numbers, no story progress needed — [4 393 values](captures/weapon-runtime-fields.csv). ⚠️ **CORRECTED 2026-08-26: the disc DOES carry a third of them.** Re-read through the [record table](structures/idxd-container.md), **1 514 of the 4 393** rows labelled `defaulted-on-disc` have a value on disc (2 879 genuinely absent) — measured here as an upper bound, since a field is counted when it appears in *any* record of the object; a per-record count gives ~1 448. The old reader could not name a record, so per-record fields read as absent. Spot-checked exactly: `wep_05`/`wep_60` `TriggerShotCount` = **4**, `wep_02` `Power` = **100.0**, `wep_60` `Power` = **1000.0** (refuting the recorded "C band ≈150…500" bracket), `wep_25` `MaximumRange` = **4000.0**, `wep_11/28/36/70` `LoadingCount` = **6/5/5/0**. The runtime capture's numbers all match the disc — what is withdrawn is the premise that it was reaching values the disc lacks. See [weapon-datasheet-runtime](weapon-datasheet-runtime.md) Supersedes the letter-bucket limit of the DATA SHEET route, which now serves as the independent cross-check |
|
||||
| Unit (craft/vessel) fields defaulted on disc ⚠️ | ✅/❌ | [runtime struct](structures/unit-struct-runtime.md) | The parsed `unit\UN_*.tbl` definition object, vtable `0x820af844`, ≥`0x380` bytes, one per unit — **discovered, not assumed** (`unit_discover.py`), and distinguished from the spawned-entity class `0x820af030` by being one-per-ID and byte-constant within a run. Across runs only pointer words move — `--crosscheck` proves **no reported field offset is run-dependent** (two words, `+0x2c8`/`+0x2d0`, are stage-dependent and remain unidentified). 27 fields ✅ (21 units, 7 runs); the `Maneuver` block is **schema declaration order, 4 bytes/field, base `0x9c` with a two-slot gap after `AA_Roll_Min`** (29 anchors, 0 conflicts), which also pins 5 fields *no* disc record ever values. Angles are **radians at runtime, degrees on disc**. **Re-derived independently 2026-08-13 from the loader's own key strings** (`sub_82341A20`; the field name for each store is a string in the image): **159 fields**, agreeing with this solver on **25 of 25 shared offsets**, verified at **406 values matching the disc and 0 disagreeing** over 11 live objects spanning UNIT and VESSEL — landed as `data/unit_definition_layout.txt` + `sylpheed_formats::unit_layout` + a no-emulator test, with **121 defaulted fields** read out ([live-unit-definitions](live-unit-definitions.md)). ⚠️ **CORRECTED 2026-08-26:** those fields are **on the disc**. The ~30-field player-craft table is on disc at exactly the values the runtime "recovered", spread across the `Generic` / `Shield` / `Mass` / `SE` records — which is why a reader that could not name a record saw them as absent. And **"18 of 23 vessel records are missing at least one of `Size_X/Y/Z/HP`" is false: 0 of 114** objects with a `Generic.Type` (43 Craft + 71 Vessel) miss any of the four. See [unit-struct-runtime](structures/unit-struct-runtime.md) Unlike weapons, unit definitions are instantiated **per stage**, so coverage (21/110) grows by visiting missions — ❌ **the sibling-default rules are WITHDRAWN (2026-08-25)** — `Size_Y` is on disc for **114/114** unit tables and **differs from `Size_X` in 90**; the old reader missed it exactly when the two were equal (a string-pool dedup artefact, cross-tab `seen+equal = 0` for all three pairs), so the rule was re-deriving the condition that hid the field. It also predicts wrong twice — `UN_e104_ADAN_Carrier.DefencePoint` and `UN_e011_ADAN_Attacker_B_HF_Wayne.FCSRange`. Read the record table instead — [values](captures/unit-runtime-fields.csv) |
|
||||
| Arsenal develop economy | ✅/❔ | [arsenal-develop-economy](arsenal-develop-economy.md) + [conditions](captures/arsenal-develop-conditions.csv) | The Arsenal reads `weapon.tbl` (item ids, in the 8-category display order) and `strings.tbl` (names, descriptions, and a **"Conditions to obtain"** block per item) out of `GP_HANGAR_ARSENAL.pak`. All **60** conditions are extracted: gates are stage completion, a predecessor item, or an **ace kill**; costs run 3 000–350 000 P and **20 items are free** once gated. `weapon.tbl`'s first record reproduces the in-game DATA SHEET exactly (Range D / Power E / Speed – / Weight 0.3 = Light / 4000 P) — later records are unreadable from the string pool alone because IDXD **dedupes repeated values**. Used to identify the save blob's index space, now **solved**: the blob follows **`strings.tbl`'s** order — the display order *plus* the cut items only the localisation file lists (`Adhesive Mine B2A`, `Ballista GSH`, …) — pinned by four hand-written probe saves (9 Stiletto, 21 Falcon, 39 Tomahawk, 48 Jamming System) and closing exactly at index 53. `weapon.tbl`'s id list is **not** the index space; that it is also 54 long is a coincidence, and the two agree only to index 32. The retail save's five unexplained owned entries are the cut items, shipped owned and never rendered |
|
||||
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly |
|
||||
| UI screen layout (`.rat`) | ✅/🟡 | [ui-rat-layout](structures/ui-rat-layout.md) | One pak per UI screen; each RATC = one (context × language) build; every `<name>.t32` sprite has a `<name>.rat` **layout record** (BE u32; 1280×720 design space; scale/tint/X/Y, keyframes for animated elements, `opt ` link to the focused state). **The tutorial PAUSE menu and the title main menu both rebuild pixel-accurately from the disc.** `loop1.rat` is decoded — it is a **looping sprite animation**, not a composition. ⚠️ **DEMOTED 2026-08-18** — the declaration table is *not* the paint order: a per-draw capture of the running title screen ([ui-title-paint-order-capture](ui-title-paint-order-capture.md)) paints element 13 first and elements 0/1 late, and the visible screen composites two bundles. The rest of the table's reading stands. Previously claimed: the **screen's draw list is the RATC bundle's own declaration table** (elements in back-to-front order, including the `eff*`/`deli*`/`msg` sprites that have no `.rat`, and excluding focused button variants reached via `opt `); its entry also carries a **parent element index** at `+32`. **A screen is fully reconstructible from its bundle**: the placement region right after the declaration table gives every element a keyframe group (header = element index + keyframe count, then 40-byte blocks of scale/tint/X/Y), including the `.rat`-less sprites — verified 11/11 on the tutorial pause bundle, with `pgp_ttrl_btn10`'s inline (546,288) matching its own record exactly. **✅ 2026-08-28: the keyframe block's `+12` is a screen-plane ROTATION in degrees**, clockwise-positive — confirmed against a GPU capture (declared 30 / −45 vs measured +30.26° / −45.28°) and non-zero in 14.50 % of 83 862 blocks disc-wide ([ui-keyframe-rotation](structures/ui-keyframe-rotation.md)). ⚠️ Every rotation on the disc is inside a **nested** `.rat` leaf record, and `ui_layout::blit` is axis-aligned, so the field is decoded but **not rendered**. 🟡 `+4`/`+8` remain unexplained |
|
||||
| UI screen paint order | ✅ | [runtime screen object](structures/ui-screen-runtime.md) + [title capture](ui-title-paint-order-capture.md) | **SOLVED**: the game's screen object keeps a second, reordered list of its elements — the child array at `+0x30` — and that is the paint order, not the declaration table. Read live from guest memory (found by the item vtable `0x820b30b4`) and checked against the draw capture: the seven nameable elements sit at child slots 0, 6, 7, 13, 16, 17, 22, strictly ascending, exactly as captured; it also settles the one pair no static field could order. 🟡 deriving that order from the bundle — what the port needs — is still open |
|
||||
| Scripted input / profile traps | ✅/🟡 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Why a scripted run appears unable to press Ⓐ: **F10 opens the emulator menu bar**, and any Xenia UI makes `XamInputGetKeystrokeEx` return SUCCESS with an empty keystroke *before* any driver is asked (Canary now logs `[RE-INPUT] … swallowed by IsUIActive`); the title needs a **signed-in profile** (hence `--create_profile_if_none`); and a **FIFO trace consumer that exits stalls the emulator**, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question |
|
||||
| Title-screen guest crash | ✅ | [title-crash-stl-tree](title-crash-stl-tree.md) | The guest throws **`std::out_of_range`** from its cache-manager flush (`sub_823070B0`, an STL map/set erase that builds `'invalid map/set<T> iterator'`); the access violation after it is only the throw **returning**, because this build does not unwind guest EH. Trigger found and controlled: an **incomplete on-disc cache** (`~/.local/share/Xenia/cache/aab216c3`) throws ~100 s into a boot, a complete one never does — 2 runs each way. ❌ `mem_watch`, the handoff's suspect #1, is **eliminated**: cold cache + `--mem_watch=false` throws anyway |
|
||||
@@ -94,9 +100,10 @@ files, which is how the same ground got covered twice.
|
||||
| [`idxd-legacy-reader-audit.md`](idxd-legacy-reader-audit.md) | The legacy IDXD string-pool reader vs the real field table — what the old numbers got wrong | 🟡 shape CONFIRMED by hand (`FCSRange`, `ShieldRatio`, hangar `Model`); disc-wide rates are single-source |
|
||||
| [`structures/idxd-container.md`](structures/idxd-container.md) | The IDXD/IXUD container — record/field table, and the two beliefs it withdraws | ✅ CONFIRMED disc-wide, 7 750/7 750 objects and 1 271 462/1 271 462 named fields, zero failures |
|
||||
| [`structures/hud-glyph-quad.md`](structures/hud-glyph-quad.md) | The HUD's glyph quad — vtable `0x820B2A64` | ✅ CONFIRMED for the object layout and the atlas size, read live off |
|
||||
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` leading-stream offset is `first_riff % 2048`, not the constant 1392 | ✅ CONFIRMED by decoding — 85 of 140 sampled banks yield more audio (median 70×), 54 identical controls. ⚠️ The *cause* is a segment-packing phase, not a header: `X = (cumulative .pNN start) mod 2048`. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
|
||||
| [`structures/slb-data-offset.md`](structures/slb-data-offset.md) | `.slb` data offset = `(cumulative start of the `.pNN` segment) mod 2048`; the leading bytes are the previous bank's audio, not a header | ✅ CONFIRMED — exact for 8 783/8 783 banks, 0 mismatches; the four values (1392/1468/1600/1728) are the running sums of the five segment sizes mod 2048, which supersedes the earlier `first_riff % 2048` scan heuristic. Decoding at the right offset yields more audio in 85 of 140 sampled banks (median 70×), 54 identical controls. Wave boundaries are exact — `seek` magic at `data_at + declared_size`, **7 620/7 620** — and `Channels` must be read from `RIFF+49` (2.12 % are stereo) |
|
||||
| [`structures/sound-pak-contents.md`](structures/sound-pak-contents.md) | Census of `sound.pak`, and the limit of the leading-region rule | ✅ CONFIRMED, 5 135/5 135 names hash into the TOC, **9 519/9 519** entries accounted for, and a full 4 114-bank manifest (408.3 min of audio) computed from PsuedoBytesPerSec without decoding; ⚠️ leading-region rule holds for 1 571/4 382 eng and 0/5 100 jpn |
|
||||
| [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files |
|
||||
| [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md) | A music bank is two stems of one performance, played together | ✅ decoded structure, 32/32 banks two waves of equal duration (4 explained by the leading-region straddle); stems measured sample-synchronous; 🟡 rear-pair vs intensity-layer unsettled; ❔ no bank names a screen |
|
||||
| [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways |
|
||||
| [`structures/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuation*rand held in the lingering queue node; the 14 player-facing events are voiced by the player set alone (13/15 Katana tables, 0/129 others) (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue |
|
||||
| [`structures/mission-scoring.md`](structures/mission-scoring.md) | Mission scoring and the S/A/B/C/D rank thresholds — 24 tables x 3 difficulties | ✅ CONFIRMED, one 22-field schema over 72 records; difficulty moves 10 of 22 fields and never the RankScore thresholds; ✅ all 24 named stage\StageParameter_S<NN>.tbl (+ _Tutorial shared by six, + _Test) |
|
||||
@@ -116,17 +123,19 @@ files, which is how the same ground got covered twice.
|
||||
| [`structures/unit-datasheet-static.md`](structures/unit-datasheet-static.md) | The static unit datasheet and AI flight model — `Generic`, `Maneuver`, `Effect` | ✅ CONFIRMED — 394 Generic + 114 Maneuver + 114 Effect records read; AA_/AV_ are one interleaved block (AV at X, AA at X+8), selection 🔴 BLOCKED for static RE (4 routes, all controlled) |
|
||||
| [`structures/weapon-datasheet-static.md`](structures/weapon-datasheet-static.md) | The static weapon datasheet — `Weapon`, `Shell`, `AssortMissileParam` | ✅ CONFIRMED — 131 Weapon + 131 Shell + 9 AssortMissileParam records read |
|
||||
| [`structures/isl-timers.md`](structures/isl-timers.md) | A ScriptPhase owns 32 stopwatches, and they count seconds | ✅ CONFIRMED — the advance is read from `sub_822710D0`, the unit from |
|
||||
| [`structures/isl-builtins.md`](structures/isl-builtins.md) | The 147 ISL built-ins — dispatch table, calling convention, and which one does what | ✅ for the table/ABI and ~135 handlers; ✅ 26 `set_unit_hp_pct`, 29 `set_unit_damage_taken_pct`, 101 `all_units_invulnerable`; 🟡 28 |
|
||||
| [`structures/isl-message-dialogue-link.md`](structures/isl-message-dialogue-link.md) | Mission scripts as dialogue — built-in 64 -> message id -> caption text | ✅ CONFIRMED total, 2 683/2 683 call sites across all 28 stages resolve, no residue |
|
||||
| [`structures/mission-objective-counter.md`](structures/mission-objective-counter.md) | `REMAINING OB` — the mission's own objective counter, in RAM | ✅ CONFIRMED for one Stage 02 run: a big-endian u32 whose value |
|
||||
| [`structures/movie-subtitles.md`](structures/movie-subtitles.md) | Movie subtitles & the movie ↔ mission ↔ text chain | — |
|
||||
| [`structures/asteroid-fields.md`](structures/asteroid-fields.md) | `AsteroidGroup_00N` — the asteroid-field tables a `Phase_N.AsteroidDefinition` names | ✅ 10 objects/pack, 384 group records, 3 fields each (`AsteroidModelName`, `AsteroidFrameName`, `EnumAsteroid` = **the frame record's `FrameCount`, 57/57**, and the frame holds **`index, quaternion(x,y,z,w), position(x,y,z)`** per asteroid — 37 518 items, unit norm 37 518/37 518); `Enum<Thing>` counts while `Enumerate<Thing>` names a table (54/60); the join resolves as `name_hash("stage\" + name)` 9/9; 🔑 a tenth `Frame_Alpha_S01_*` object nothing references — it miscounts AND its 7 frames have no placement records; 🔑 **S28.Phase_1 borrows `S14_p2_asteroid.tbl`** — the per-phase join shows 9 tables ↔ 9 volumes with one table shared |
|
||||
| [`structures/mcol-collision.md`](structures/mcol-collision.md) | `MCOL` — same container and map parameters as `REGN` | 🟡 OPENED — `POF0` at `data_size+16` 11/11, bbox pad words 11/11, `extent == max−min` 11/11, and bbox + cell-size distributions identical to `REGN` (2/6/3 and 2/9); everything past `0x40` ❔ |
|
||||
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a per-map spatial grid (and `MCOL` beside it) | ✅ CONFIRMED for the header, which self-checks on all 11 objects on |
|
||||
| [`structures/regn-map-grid.md`](structures/regn-map-grid.md) | `REGN` — a stage's tetrahedral navigation mesh (and `MCOL` beside it) | ✅ CONFIRMED — tet mesh + face adjacency + portal costs + uniform grid; decoded from the file's own `POF0` fixup table |
|
||||
| [`structures/savegame-format.md`](structures/savegame-format.md) | Save file (`savedata`) — container ✅ exact, 3 fields named ✅, rest ❔ (2026-08-11) | ✅ CONFIRMED for the container and the chunk layout — parsed off the |
|
||||
| [`structures/sound-slb.md`](structures/sound-slb.md) | Sound bank audio — `sound.pak` / `.slb` / XMA1 | — |
|
||||
| [`structures/stage-definition-table.md`](structures/stage-definition-table.md) | Stage definition table and the squadron (`UnitGroup`) roster | ✅ for the record vocabulary and the stage→table wiring; |
|
||||
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
|
||||
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
|
||||
| [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) | What a keyframe time is worth, and what shape the ramp has | ✅ CONFIRMED from the running game's own draw stream — the ramp is **linear** (a declared 15-unit fade lands on `round(255·k/15)` for all seven samples) and the animation clock advances **2 time units per submitted frame**. 🟡 the seconds conversion (`1 unit = 1/60 s`) rests on a measured 27.6 present-frames/second |
|
||||
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
|
||||
| [`structures/ui-focus-and-effect-elements.md`](structures/ui-focus-and-effect-elements.md) | `_eff` glow layers are not focused-state records | ✅ CONFIRMED by measurement over all 965 screen builds on the disc, |
|
||||
| [`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md) | The paint order comes from a layer key in the T8aD sprite header | ✅ CONFIRMED on both screens whose paint order has been measured — |
|
||||
@@ -141,6 +150,13 @@ files, which is how the same ground got covered twice.
|
||||
| [`title-crash-stl-tree.md`](title-crash-stl-tree.md) | The title-screen crash is an STL `map`/`set` erase on a bad iterator | ✅ CONFIRMED — the guest throws std::out_of_range from an STL |
|
||||
| [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously |
|
||||
| [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but |
|
||||
| [`menu-navigation-semantics.md`](menu-navigation-semantics.md) | The title menu — how it moves, and where each button goes | ✅ measured: wraps both ends, Ⓑ restores focus, ⬅➡ inert; 4 of 5 destinations driven. 🟡 GamePart id is a name match, ❔ `NEW GAME` untested |
|
||||
| [`screen-transitions.md`](screen-transitions.md) | Between two screens — a fade through black, and where its timing lives | ✅ the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; `GP_TITLE` = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc |
|
||||
| [`menu-audio-cues.md`](menu-audio-cues.md) | Menu audio — the event vocabulary is on the disc, the binding is not | ✅ `SE_UI_*` cue names/ids decoded and `BANK_SE`→`Static.slb` (0/322 in FILES); 🟡 event binding is a name match; ❔ `Static.slb` has no wave boundaries, so SE audio is not extractable |
|
||||
| [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md) | What the game reads at boot — `config.ini`, and which GameParts exist | ✅ `config.ini` selects the language (the disc's only config); ❔ its `[SYSTEM]` is empty so the boot order is not in config; 🟡 24/29 ids bind to a class, `GP_ADVERTISE_DEMO` is never registered |
|
||||
| [`movie-binding.md`](movie-binding.md) | Which movie plays where — boot intro, attract loop, new-game intro | ✅ decoded from the movie manifest (`ADVERTISE_MOVIE`→`ADV.wmv`, `MS00A`→`S00A.wmv`); attract identity confirmed independently by frame matching; 🟡 skippability unsettled |
|
||||
| [`ready-room-probe.md`](ready-room-probe.md) | S1 — the Ready Room probe: no-go, and not for the reason expected | ✅ it is 2D and enumerates (60 builds), but the pak is briefing/tactical-map content; and `kind == 0x3002` finds 0 buttons there |
|
||||
| [`ui-title-build-map.md`](ui-title-build-map.md) | Which `GP_TITLE` build is which screen state | ✅ CONFIRMED for title / `PRESS Ⓐ` / main menu / `EXTRAS` against live captures; the archive is 8 screens × EN/JP, and "6/8/9 are submenus" is withdrawn |
|
||||
| [`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md) | The title screen's paint order, measured from the guest's draw submissions | ✅ CONFIRMED — the order in which the running game paints the title |
|
||||
| [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad |
|
||||
| [`weapon-datasheet-runtime.md`](weapon-datasheet-runtime.md) | Weapon DATA SHEET — runtime capture (Route B) | 🟡 first dynamic capture, 2026-07-28. The Arsenal's Gallery Mode panel is a |
|
||||
|
||||
898
docs/re/METHOD.md
Normal file
@@ -0,0 +1,898 @@
|
||||
# Method traps already paid for
|
||||
|
||||
Each line cost an iteration at least once. They are general — they are not about
|
||||
Sylpheed, they are about how this kind of measurement goes wrong.
|
||||
|
||||
Like [`REFUTED.md`](REFUTED.md), this list had been living in the autonomous
|
||||
agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
|
||||
✅/🟡/❔ confidence convention itself.
|
||||
|
||||
## Controls
|
||||
|
||||
* **Every result needs a control. A control that fails kills the instrument.**
|
||||
* **Run the known-positive through a new filter FIRST.** Three filters have been
|
||||
killed by their own control. When one fails, **read the known-good's
|
||||
disassembly** before assuming a shape.
|
||||
* **A measured negative is a result** — but a negative is only as strong as the
|
||||
route you ran, so **state its reach**.
|
||||
* **A null result needs its cause shown to have happened.**
|
||||
* **A result with NO unknowns is suspicious.**
|
||||
* **Census the whole set; always run the other population as the control.**
|
||||
**Zero partials is stronger than a majority.**
|
||||
* **A 2×2 partition is the sharpest general tool** — both off-diagonals empty is
|
||||
a law.
|
||||
* **Re-derive a doc's own numbers as the control.**
|
||||
|
||||
## Inference
|
||||
|
||||
* **Never conclude from ONE sample.**
|
||||
* **A law proved on one population is a hypothesis on the next.**
|
||||
* **Finding one exception does not imply a family.**
|
||||
* **Consistency is not proof. A suggestive coincidence is a coincidence until
|
||||
measured.** **An analogy is not a measurement.**
|
||||
* **Same layout ≠ same instance.** **Same record-name set ≠ same object.**
|
||||
* **A marker is only proven by what it leaves out.**
|
||||
* **A high-confidence SCORE is not a high-confidence MECHANISM.**
|
||||
* **Knowing HOW MANY is not knowing WHICH.**
|
||||
* **Round numbers matching is weak evidence — unless you read the constant.**
|
||||
* **My own last-turn result is a hypothesis too.**
|
||||
* **A global partition can understate a per-owner one.**
|
||||
* **A residual is measured against a population — name it.**
|
||||
|
||||
## Searching and tooling
|
||||
|
||||
* **A search that returns thousands has no power; state the reach.**
|
||||
* **A substring match is not a hit.** **A regex miss looks like a null result —
|
||||
print one raw sample before believing a zero.**
|
||||
* **A derived table can be a cross product — measure its shape first.**
|
||||
* **After refuting an instrument, sweep everything that depended on it.**
|
||||
* **Before measuring how wrong a tool is, read what the tool actually does.**
|
||||
* **The instrument must pass its own control.**
|
||||
* **Classify a bulk before mining it. The residual is the prize.**
|
||||
* **Rank by similarity — the cliff is the finding.** But **read the values
|
||||
before trusting the rank.**
|
||||
* **Grep the nouns before designing the experiment — and believe it.**
|
||||
* **Grep gives you a file list — read *every* file on it.**
|
||||
* **The answer is often already in the doc that owns the subject — read it end
|
||||
to end.** A 🟡 often names its own route.
|
||||
* **Before re-trying a blocked idea, check whether the blocker's own doc already
|
||||
tried it.**
|
||||
* **Ship a regenerator with every artefact.** An artefact that moves by a pure
|
||||
reorder is a tool bug.
|
||||
* **Never print per-entry lines from a disc-wide sweep — aggregate.**
|
||||
|
||||
## Reading the data
|
||||
|
||||
* **Read what a loader NAMES, not where it stores.**
|
||||
* **A field the disc never values still gets named by the loader.**
|
||||
* **An indexed read beats a deduped-pool adjacency read.**
|
||||
* **Check the whole string set, not the one matching word.**
|
||||
* **A dict keyed by record name across a multi-entry pak is a lie.**
|
||||
* **A set-difference over names hides reuse — join per USER.**
|
||||
* **A self-index names records, not files.**
|
||||
* **Case-insensitive hashing means two spellings can be one entry.**
|
||||
* **An "unresolved" name may be the wrong kind, namespace or prefix — or part of
|
||||
a cut asset.**
|
||||
* **A garbled value may be a real string in another encoding.**
|
||||
* **Two of my own counts disagreeing is a grammar clue.**
|
||||
* **A game's own typo is a join key.**
|
||||
* **A bias constant in the code is a join key.**
|
||||
* **A prefix trap: enumerate maximal `[A-Za-z0-9_]` runs, not `startswith`.**
|
||||
* **Re-deriving a format is not a finding — asking whether its values *resolve*
|
||||
is.**
|
||||
|
||||
## Mechanics that have bitten
|
||||
|
||||
* **Never hand-convert a decimal VA — print `hex()`.**
|
||||
* **`grep -c` counts LINES** — use `grep -o | wc -l`.
|
||||
* **`Counter.most_common()` tie-breaks by insertion order — use `sorted()`.**
|
||||
* **Raw grep cannot see inside compressed pak entries.**
|
||||
* **Commit messages go in a file** (`git commit -F`); a literal `|` in a table
|
||||
cell needs escaping; `git log --all -- <path>` can hang.
|
||||
|
||||
## Runtime / emulator
|
||||
|
||||
* **Look at the PNG** — and check its dimensions.
|
||||
* **"Animating" is not "still in a mission".**
|
||||
* **Dedup entity enumerations by position value.**
|
||||
* **Do not diagnose timing or liveness under gdb.** `ps %cpu` is cumulative.
|
||||
* **Classify screens by whole-image statistics, not named pixels** — a named
|
||||
pixel is only valid while the image sits at a known place, and nothing errors
|
||||
when it moves.
|
||||
* **Do not poll faster than the guest updates** — it manufactures a clean curve
|
||||
out of noise.
|
||||
* **A probe that never performs the action will "prove" the action does not
|
||||
exist.**
|
||||
* **The container's Canary binary can be older than the Canary source tree, and
|
||||
the failure mode is a hang, not an error.** After a merge into `sylpheed-re`
|
||||
the prebuilt `xenia_canary` had no `log_ui_draws`, no `mem_watch`, no
|
||||
`create_profile_if_none` — and an unknown cvar makes xenia open an SDL message
|
||||
box before logging is up, which headless is an unexplained freeze. Check before
|
||||
trusting a harness flag: `nm -C <binary> | grep cvars::<flag>`, and
|
||||
`build-canary Release` if it is missing.
|
||||
* **A capture armed *at* a screen only ever sees the steady state.** Anything
|
||||
about how a screen is built or animated has to be armed *before* it exists.
|
||||
Re-arming every few seconds and keeping every log tiles the approach: each F10
|
||||
opens a new numbered file and closes the previous one complete.
|
||||
* **Measure animation in submitted frames, not in seconds.** `VdSwap` counts are
|
||||
the guest's own frames, so an emulator at 80 % of real time does not move them;
|
||||
a stopwatch reading does, silently and by an unknown factor.
|
||||
* **~~This game's menus drop d-pad presses shorter than ~0.3 s.~~ WITHDRAWN
|
||||
2026-08-28 — the menu WRAPS, and I had not measured that.** The claim came from
|
||||
reading a cursor that ended up "one item short"; once wrap-around at both ends
|
||||
was measured ([`menu-navigation-semantics.md`](menu-navigation-semantics.md)),
|
||||
every one of those press counts is exactly right — four presses at 0.12 s moved
|
||||
four steps *through the bottom*, which lands one above where a non-wrapping
|
||||
menu would put it. **No press was ever dropped.** The real lesson is the
|
||||
general one: *a step count is only readable once you know the topology*, and I
|
||||
invented a hardware-flakiness story rather than testing the ends of the list.
|
||||
Still true and worth keeping: screenshot after every step and read the cursor,
|
||||
rather than trusting arithmetic over the press count.
|
||||
* **A screenshot taken right after a transition can catch a screen mid-fade.**
|
||||
A grab 2.5 s after Ⓑ returned the game to the title showed the title art with
|
||||
no `PRESS Ⓐ BUTTON` plate; one second later the plate was there. That very
|
||||
nearly went into the corpus as "the returned title has no plate". Sample a
|
||||
changing screen several times before writing down what it does *not* contain.
|
||||
* **Do not identify a menu cursor by label brightness.** The obvious oracle —
|
||||
"the focused label is the brightest row" — fails on this game's menus, because
|
||||
the background art is brighter behind some rows than the highlight is. It
|
||||
confidently named the wrong item on a frame whose ring was plainly elsewhere.
|
||||
Detect the **focus ring** in the gutter left of the labels instead
|
||||
(`tools/re-capture/menu_focus.py`, 254 vs <82 — no threshold tuning needed),
|
||||
and look at the PNG before believing either.
|
||||
* **`screenshot` samples at 0.5 Hz — it cannot time an animation.** Measured:
|
||||
~2 s per grab (an `import` of the root plus an ImageMagick crop). A 0.4 s fade
|
||||
falls entirely between two samples, which is why a 40-frame burst across a
|
||||
screen change looked like an instant cut. For anything timed, record the
|
||||
display instead: `ffmpeg -f x11grab -framerate 30 -video_size <W>x<H> -i
|
||||
:98+<X>,<Y> -t <secs>`, then read per-frame statistics off the file. Take the
|
||||
geometry from `xwininfo -root -tree`, the same way `bin/screenshot` does.
|
||||
* **A screen's brightness curve is not its fade quad.** The incoming screen's own
|
||||
elements animate in *after* the transition quad has cleared, so mean luminance
|
||||
keeps rising long after the fade is over — 1.47 s against a declared 0.97 s on
|
||||
one screen. Time the fade from where the frame is *pure black*, and take the
|
||||
ramp itself from the keyframes.
|
||||
* **A `sound.pak` entry is not a bank.** Entry windows straddle bank boundaries
|
||||
(the leading-region effect), so a naive "walk the RIFFs in this entry" gives
|
||||
the tail of the previous bank, then one of your own, then the *next* bank's
|
||||
header. It looks like a bank with the wrong number of waves. The tell is a
|
||||
`BANK id=` that is not the entry's own number, and the fix is the `seek` packet
|
||||
counts — they pin each wave's join exactly, so a straddle can be realigned
|
||||
rather than thrown away. 4 of 32 BGM banks look like counterexamples until this
|
||||
is applied.
|
||||
* **Do not time a video whose start you did not see.** An attract cycle sampled
|
||||
from partway through measured "≈85 s", which was used to argue it was not the
|
||||
137 s `ADV.wmv`. It was `ADV.wmv`; the sampling began 39 s in. Identify a video
|
||||
by **content** — match frames against the candidates and check the matched
|
||||
playhead advances monotonically — before reasoning about its duration.
|
||||
* **A near-black frame has no signature.** In frame-matching, low-contrast frames
|
||||
drive every correlation toward zero and the ranking becomes noise; one control
|
||||
frame scored 0.000 and another tied the wrong movie at 0.98. Read a dark
|
||||
frame's runner-up as *no evidence*, not as evidence for the runner-up.
|
||||
* **A screen that never changes is not necessarily hung — it may be a menu.** A
|
||||
90-second screenshot loop that presses nothing will report a perfectly healthy
|
||||
`DIFFICULTY` menu as a standing hang, and that reading survived in this corpus
|
||||
for months. Before calling a static screen a hang, *look at it*, and press
|
||||
something.
|
||||
* **Never run `ps -ef` in this container.** Each of the three long-lived
|
||||
processes carries the whole loop prompt as its argv, so the listing is tens of
|
||||
kilobytes of the prompt you already have. Use `pgrep -x <name>` or
|
||||
`ps -o pid=,stat= -C <name>`.
|
||||
* **A file extension is not a format.** `sound.pak` is full of `.slb` banks and
|
||||
names a `.xgs` project, and neither is an XACT container: the whole 1.08 GB
|
||||
archive contains zero `XGSF`, `SDBK` or `WBND` magic, and the executable has no
|
||||
XACT string at all. The names came from the authoring tool and survived into the
|
||||
shipped data. Check for the magic before committing to a parser for the format
|
||||
the extension advertises.
|
||||
* **A log-polling probe must compare a COUNT, not test for presence.** Timing an
|
||||
operation by waiting for its "done" line in a log gives an instant false
|
||||
positive if any earlier run left one there. This produced a confident
|
||||
"39.5 fps" that was really 0.1 s of matching a stale line — and it disagreed
|
||||
with the true 28.5 fps by enough to have flipped a conclusion. Snapshot
|
||||
`log.count(marker)` before starting and wait for it to *increase*.
|
||||
* **This container kills long emulator runs.** Twice in one session the
|
||||
`xenia_canary` process vanished with **no crash line in its own log** — once
|
||||
~50 s into a boot, once ~145 s in at 1.15 GB RSS — while the box sat at
|
||||
~1 GB free with swap exhausted. Budget dynamic experiments to fit in ~2 minutes
|
||||
of guest time, take the measurement early, and never read a disappearance as a
|
||||
guest crash without checking the log tail for one.
|
||||
* **When a frame-match will not resolve, sample denser before doubting the
|
||||
hypothesis.** Identifying a movie from 5-second screenshots gave contrast-23
|
||||
frames, a playhead that would not join up, and a runner-up winning one frame —
|
||||
it looked like weak evidence for the wrong film. The same question sampled every
|
||||
0.5 s (`ffmpeg x11grab` at 10 fps) answered at **0.999 with a strictly monotone
|
||||
playhead**. Sparse sampling does not weaken a signal gracefully; it turns it
|
||||
into noise that looks like a different answer.
|
||||
* **Check what instrumentation the local build already has before declaring a
|
||||
question blocked on tooling.** Two Q8 residuals were written off as needing "an
|
||||
emulator whose audio path can be observed". The emulator in this container
|
||||
already had `xma_param_probe` — visible in the startup CONFIG DUMP the whole
|
||||
time — added by this project for exactly that purpose. Read the cvar dump, or
|
||||
`nm -C <binary> | grep cvars::`, before concluding you cannot measure something.
|
||||
* **"Strings a function references" is not "arguments a function passes".** A
|
||||
loose filter over the strings in a caller produced a tidy-looking screen
|
||||
vocabulary; recovering the actual register argument at each call site removed
|
||||
half of it, including two names that appeared to be corroborated by independent
|
||||
measurement. If the claim is about an argument, simulate the registers — the
|
||||
cheap proxy manufactures agreement.
|
||||
* **A backward scan for `addi rN, …` misses arguments staged through another
|
||||
register.** PPC code often builds a pointer in a scratch register and moves it
|
||||
into place later (`addi r27, r11, X` … `mr r5, r27`). A backward search for the
|
||||
destination register alone found 7 of 48 sites and missed the two known-good
|
||||
ones. Simulate forward over a window instead, tracking `lis`/`addi`/`mr`.
|
||||
* **Check a measurement against the WHOLE reference set, not the part you were
|
||||
reading.** Two observed BGM stream sizes were recorded as matching "no bank's
|
||||
declared waves", which spawned a wrong theory that the decoder gets a window
|
||||
rather than a whole wave. They match `BGM_103` exactly — the check had covered
|
||||
only the `BGM_0xx` rows of the census, because that is the block that had been
|
||||
on screen.
|
||||
* **`instructions.function` is unpopulated for most rows in `sylpheed.db`.** A
|
||||
query keyed on it returns *nothing* rather than erroring — a `bctr` search
|
||||
scoped that way reported "no jump tables" for a function with two known ones.
|
||||
Scope by `address between <start> and <end_address>` instead, and gate any such
|
||||
query on a function whose answer you already know.
|
||||
* **Before testing "state X behaves like this", check that your route actually
|
||||
reaches state X.** A test of "does Ⓐ work in phase 4 state 0" was run by
|
||||
pressing Ⓑ, on the strength of a decoded `4 → state 0` edge — but the same basic
|
||||
block also writes the *phase* field, sending it to phase 2. The test probed the
|
||||
wrong state and produced a confident refutation of a live hypothesis. When a
|
||||
transition sets more than one field, read the whole block, not the one store you
|
||||
were looking for.
|
||||
* **A default filter can hide a whole screen, and the corpus will record it as
|
||||
absent.** `screen list`/`render` filter on `is_build`, which needs a `.rat`
|
||||
child; the developer splash has none, so it never appeared — and a page ended up
|
||||
stating it "cannot be rendered at all". It renders fine under `--all`. When two
|
||||
pages disagree about whether something exists, run the tool both ways before
|
||||
believing either.
|
||||
* **A logo that appears twice may be two different things.** The boot shows
|
||||
`SQUARE ENIX` at ~2 s and again at ~10 s; the first is the splash screen, the
|
||||
second is the intro movie's own opening frame. Reading the capture alone gives
|
||||
"three logo screens". Cross-check a repeat against what is *supposed* to be
|
||||
playing at that moment before counting it as its own screen.
|
||||
* **Do not hard-code an expected element count into a structure scan.** Looking
|
||||
for a `.rat` record's keyframes, I required 29 strictly-increasing times because
|
||||
a header word read `30`. The records hold **three** keyframes in exactly the
|
||||
layout I was testing for, and the over-constrained filter reported the layout as
|
||||
refuted. Scan for the *shape* (here: `0x??ffffff` fade words at a constant
|
||||
stride) and let the count fall out.
|
||||
* **Read the target text before writing a patch pattern for it.** Two iterations
|
||||
running, a scripted edit to `HANDOFF.md` failed its assertion because the
|
||||
paragraph had been rewrapped since the pattern was written — and both times the
|
||||
finding landed in `docs/re/` while the page the port agent reads kept the stale
|
||||
version. The commit's file count is the tell: if it is one short, an edit was
|
||||
silently dropped.
|
||||
* **Two renderers agreeing is not evidence the field is right.** The port's
|
||||
composite and `sylpheed-cli screen render` matched on `main_menu` to 3/255 —
|
||||
and *both* omitted two elements the game draws, because both read one field
|
||||
through one decoder. Agreement between consumers of the same decode measures
|
||||
the decode's determinism, not its truth. Only a framebuffer capture is an
|
||||
independent check. (Reported by the port agent, 2026-08-28, after a capture
|
||||
crop found the missing bracket.)
|
||||
* **`./target/debug/<bin>` is STALE in this container — `CARGO_TARGET_DIR` is
|
||||
redirected.** The Dockerfile sets `CARGO_TARGET_DIR=/sylph-home/re/target-container`
|
||||
so a fresh `cargo build` writes there, while `./target/debug/sylpheed-cli`
|
||||
remains whatever was built on the host, hours old. A decoder fix was verified
|
||||
against the stale binary and produced byte-identical "before" and "after"
|
||||
renders — which reads exactly like "the change had no effect" rather than "you
|
||||
ran the old code". Run the binary cargo actually wrote, or `cargo run`.
|
||||
* **A pivot-equality test conflates rounding with real breakage.** Checking
|
||||
`pivot == texture/2` over `GP_TITLE` flagged 74 % as mismatched, which reads as
|
||||
a systemic decode failure. Split by magnitude it is 17 exact, 54 off by ≤ 1 px
|
||||
(odd dimensions rounding), and only **24 off by > 8 px** — and those 24 are the
|
||||
real finding, concentrated on one element family. Bucket a mismatch by size
|
||||
before reporting a rate.
|
||||
* **I have now broken the "read the target text first" rule three times.** Each
|
||||
time a scripted `HANDOFF.md` edit asserted on a paragraph that had been rewrapped
|
||||
by an earlier edit in the same session, and each time the finding landed in
|
||||
`docs/re/` while the port's page kept the stale text. Writing the rule down was
|
||||
not enough; the working fix is mechanical — `grep`/`sed -n` the target paragraph
|
||||
in the same command that patches it, and check the commit's file count.
|
||||
* **"The nearest header after the name" is not a name→header mapping.** Looking
|
||||
for each sprite's `T8aD` header by searching for its name and taking the next
|
||||
`T8aD` returned *the same header for every sprite* — all the names live together
|
||||
in the declaration table near the top of the bundle, so "next" is always the
|
||||
first one in the file. It failed its own control instantly: different sprites
|
||||
reported identical dimensions. Match on something the header itself carries
|
||||
(here width × height against the decoded PNGs) rather than on proximity.
|
||||
* **`cargo build` passing does not mean `cargo test` compiles.** Adding a field to
|
||||
`T8adImage` built the library fine in 1.48 s — and broke two *test-only* struct
|
||||
literals, so `cargo test` failed to compile with exit 101. A green build on the
|
||||
binary is not a gate for a struct change; run the tests before believing a
|
||||
data-structure edit is safe.
|
||||
* **Detach long background work with `setsid`, or a tool-call timeout kills it.**
|
||||
Three attempts to run the disc-gated test suite in the background died at exit
|
||||
143 with an empty log — the timeout's SIGTERM reached the whole process group.
|
||||
`setsid nohup cargo test … &` survives, and the run then completes across later
|
||||
tool calls. An empty log from a "background" job usually means it was killed,
|
||||
not that it produced nothing.
|
||||
* **Validate a recommendation before leaving it as advice.** Having concluded a
|
||||
question was undecodable from the disc, I named a per-draw GPU capture as the
|
||||
next route "because it reads the blend state". It does not — checking the
|
||||
capture's own source showed it records textures and vertex attributes and no
|
||||
blend registers. A named next step is a claim like any other.
|
||||
* **Name the element from the draw's own coordinates before building on it.** A
|
||||
skewed draw in a title capture was called "the swoosh" because the swoosh is the
|
||||
screen's only diagonal element — an identification by elimination, never checked
|
||||
against the draw's NDC extent. It was wrong: the quads span the full screen and
|
||||
beyond, the swoosh is a 234-pixel band. Five iterations of analysis pointed at
|
||||
the wrong element on the strength of it. Converting the coordinates takes one
|
||||
line and should come first.
|
||||
* **A census over a bundle's top-level table is not a census over the bundle.**
|
||||
The keyframe rotation field read "always zero on this screen" for several
|
||||
iterations because every scan walked `GP_TITLE` build 4's **declaration
|
||||
table**, and the rotated elements are **nested leaf records** reached through
|
||||
an `opt ` link. The bytes were right; the *reach* was wrong, and a negative
|
||||
stated without its reach reads like a fact about the disc. Say which region a
|
||||
negative covers, and check whether the thing you are looking for lives outside
|
||||
it.
|
||||
* **Do not assume 4-byte alignment when scanning raw bundle bytes.** A nested
|
||||
`RATC` blob starts wherever the parent's chunk stream leaves it — `ptloop01.rat`
|
||||
sits at `0xbb5966` — so its 40-byte keyframe blocks are odd-aligned. A scanner
|
||||
that filtered candidates on `%4 == 0` found **0/3** of its own control blocks
|
||||
and under-counted the corpus by **16 341** blocks, all of them nested. It cost
|
||||
nothing to catch, because the control was in the script.
|
||||
* **`pkill -f <pattern>` matches the shell running it.** `pkill -f kfscan.py`
|
||||
from a `bash -c` whose command line contains `kfscan.py` kills its own shell:
|
||||
the tool call returns exit 143/144 and the edit that was queued behind it never
|
||||
runs. The same trap makes `pgrep -f` self-report — a "still running? yes" that
|
||||
was the shell seeing itself, on a job that had already finished. Use
|
||||
`pgrep -x`, or match on a path the current command line does not contain.
|
||||
(This is the second `ps`/`pgrep` entry in this file; the first is about
|
||||
`ps -ef` dumping the loop prompt.)
|
||||
* **Grep a legend and you count the legend.** A sweep for elements with a
|
||||
rotation reported "1 element" in every build on the disc — the header line
|
||||
`a=alpha r=rot°` matched the ` r=` pattern. A uniform count across
|
||||
heterogeneous inputs is the tell. Make the pattern require the *value*
|
||||
(` r=-?[0-9]+`), and sanity-check that a known-negative build reports zero.
|
||||
* **A pattern over the archives you happened to check is not a negative.** I
|
||||
wrote "rotation appears to live only in nested leaf records", correctly scoped
|
||||
to the three paks I had swept — and the sweep refuted it on the fourth, within
|
||||
the hour, because the alphabet had not reached `GP_DIALOG` yet. Scoping the
|
||||
claim was what made it cheap to withdraw, but the better move is to let the
|
||||
sweep finish before writing the sentence at all: a negative that is still
|
||||
running is not a finding, it is a prediction.
|
||||
* **A field that is implemented is not a field that is measured.** The
|
||||
pivot-anchored scale term had been in `blit` for months and reasoned about in
|
||||
the handoff, and no capture had ever exercised it, because every element anyone
|
||||
had looked at was at 100 % scale where the term is identically zero. Look for
|
||||
the case that makes a term *large* — here 600 % and 800 % scale, worth 450 px —
|
||||
and check it there. A term you cannot distinguish from zero has not been
|
||||
verified by any amount of agreement.
|
||||
* **Printing a series' minimum and reading it as its range.** I summarised a
|
||||
captured alpha series as "constant α ≈ 33" and built a contradiction on it —
|
||||
the summary printed `min_alpha` and no maximum, and the series actually ramps
|
||||
34 → 255 → 33. The tell was there in the same table: the column beside it said
|
||||
*14 distinct colours*, which a constant series cannot have. When a summary
|
||||
statistic and a distinct-value count disagree, the summary is wrong.
|
||||
* **Calibrate on one element, test on another.** Fitting a declared ramp to a
|
||||
capture has two free parameters (rate and offset) and will "succeed" against
|
||||
almost anything — my first attempt scored RMS 128/255 and I nearly read the
|
||||
numbers rather than noticing the search could not reach the ramp at all. The
|
||||
version that means something: fix the clock from element A's ramp, check that
|
||||
fix against A's own next landmark, then apply it to element B in the same
|
||||
frames with **nothing left to tune**. That is what turned "the shapes look
|
||||
different" into "still at 255 nine frames after it should read 32".
|
||||
* **Two measurements can both be right and still disagree, when a heuristic sits
|
||||
between them.** A change to the keyframe time association is favoured 26× by an
|
||||
animation measurement and rejected by a static-render comparison. Both are
|
||||
sound: `rest()` picks a pose by *longest dwell*, which is a heuristic layered on
|
||||
top of the times, so moving the times moves its tie-breaks. The mistake would
|
||||
be to treat one as refuting the other and drop the loser. Name what each
|
||||
measurement actually constrains — here *timing* versus *pose selection* — before
|
||||
deciding they conflict.
|
||||
* **Before believing a render diff refutes a decode, find out which element moved.**
|
||||
I reported a 13 % render change as evidence against a keyframe-time hypothesis,
|
||||
on a brightness argument about language twins. One element accounted for all of
|
||||
it, and that element is a transient flash with **no resting pose** — so the
|
||||
difference was our own `rest()` heuristic guessing, and it would have guessed
|
||||
under either reading. A whole-image statistic (pixels changed, mean luminance,
|
||||
correlation) tells you *that* something moved, never *what*, and an argument
|
||||
built on it can be entirely about your own code. Localise the diff to an
|
||||
element and read its keyframes first.
|
||||
* **A fallback that only runs when its precondition fails is not a fallback.**
|
||||
`rest()` picks the longest-dwell keyframe when no plateau exists — but a dwell
|
||||
gap is time spent *moving between* two poses, and the only case where an
|
||||
endpoint is genuinely held is when the two poses are equal, which is exactly
|
||||
the plateau the first path already caught. So the fallback is guaranteed to be
|
||||
guessing every time it is reached. When a heuristic's justification is the same
|
||||
condition that routes around it, it has no justification.
|
||||
* **Siblings adjudicate a rule that a single element cannot.** Deciding what
|
||||
`rest()` should return for an element with no held pose looked like it needed a
|
||||
capture we do not have. It did not: the developer splash carries three glows
|
||||
with identical structure and identical times, differing in one alpha byte, and
|
||||
any rule that makes one of them behave completely differently from the other
|
||||
two is producing an artefact. Parallel constructions in the same bundle are a
|
||||
free control — look for them before concluding a question needs new measurement.
|
||||
* **Backgrounding with `&` in a compound command loses the working directory.**
|
||||
`cd X && cmd &` followed by more work in the same call left the shell reset, and
|
||||
a heredoc two lines later wrote nowhere — while the `echo` after it still
|
||||
printed "written". A success message after a failed redirect is not a success.
|
||||
Use `(setsid nohup … &)` and keep the file-writing in its own call.
|
||||
* **Read the whole function before describing what it does.** I wrote "a
|
||||
zero-scale pose paints a 1-pixel sliver" into a tool's docstring, from the
|
||||
`.max(1)` in the size arithmetic. Two lines above it, a guard coerced
|
||||
`scale == 0` to **100 %**, so the `.max(1)` never saw a zero and the element
|
||||
rendered at *full size* — a much larger bug than the one I described. I had
|
||||
read the lines I went looking for and stopped.
|
||||
* **Never diff a file a background job might still be writing.** A comparison
|
||||
loop over an output directory reported `GP_TITLE` build 6 as changed; the pixel
|
||||
diff was then all zeros, because `cmp` had read a half-written PNG. Compare
|
||||
only after the job signals done, or skip the newest file. A "difference" that
|
||||
vanishes on re-examination is usually a race, not a flake.
|
||||
* **A `screen list` BUILD index is not a pak ENTRY index.** `GP_TITLE` happens to
|
||||
map 1:1 (16 builds, entries 0–15), which is how the assumption survived;
|
||||
`GP_DIALOG` does not — its build 0 is **entry 2**. Indexing a pak directly with
|
||||
a build number silently reads a different bundle and reports a confident wrong
|
||||
answer: a census control asked for the two rotated elements of `GP_DIALOG`
|
||||
build 0 and got zero, from a bundle that genuinely has none. `screen list`
|
||||
prints the mapping; use it rather than assuming identity.
|
||||
* **"There is no flag" is not "there is no way".** I declared a Japanese-locale
|
||||
capture impossible in this container because `user_language` is `DECLARE`d and
|
||||
never `DEFINE`d, so no cvar exists to pass. That was true and not the question.
|
||||
The language is *persisted*: canary builds `XConfig` over
|
||||
`<storage_root>/xconfig.settings`, `SetDefaults()` only supplies the value when
|
||||
the file has none, and the file is writable. One grep further — for where the
|
||||
setting is **stored** rather than where it is **configured** — turned a
|
||||
"blocked, needs a human decision" into a two-line edit. When you conclude a
|
||||
capability is absent, check the persistence layer before writing it up.
|
||||
* **Locate a binary field from struct landmarks, not from a hard-coded offset.**
|
||||
`user.language` was found by scanning for `music_volume`'s `0.7f` (a known
|
||||
default at a known relative offset), then cross-checking `language == 1` and
|
||||
`country == 103` against the emulator's own `SetDefaults()`. Three agreeing
|
||||
landmarks, no offset assumed, and the check re-runs every invocation — so the
|
||||
tool fails loudly if the layout moves instead of silently patching a
|
||||
neighbouring field.
|
||||
* **A stale oracle reports the world, not itself.** A 787 s run concluded "the
|
||||
game never reached the title". It had, repeatedly: `wait_title.sh` was still
|
||||
sampling the single pixel (625,618) that `is_title.py` had already been written
|
||||
to replace — a 1280×720 coordinate against a 1279×675 game surface, always
|
||||
reading the copyright line. The replacement existed *in the same directory*,
|
||||
with a docstring naming the bug. Before believing a negative from a probe,
|
||||
check whether something in the toolkit already supersedes it, and run the probe
|
||||
against a known-positive capture first — `is_title.py` scores 753 on a real
|
||||
title frame and 327 on the main menu, which takes one command to confirm.
|
||||
* **Run the control before theorising about the difference.** A Japanese-locale
|
||||
run failed to reach the interactive title, and I wrote up the open question as
|
||||
"if English reaches it, the difference is the locale". English does not reach
|
||||
it either — 75 samples over 734 s, every one a miss. The locale had nothing to
|
||||
do with it, and two iterations framed the problem around a variable that was
|
||||
never implicated. The control cost one run and would have cost the same one
|
||||
iteration earlier.
|
||||
* **Log every sample, not just the hit.** The first probe printed nothing until
|
||||
it succeeded, so a 787 s failure was indistinguishable from a probe that never
|
||||
ran. Printing the oracle's value each tick turns "no title" into "75 samples,
|
||||
max glyph 0", which is a measurement — and it is what showed the black-screen
|
||||
and attract-movie phases were being sampled correctly all along.
|
||||
* **Know when to stop paying for a non-blocking answer.** A capture that would
|
||||
settle two 🟡 residuals — neither of which blocks the deliverable — absorbed
|
||||
five iterations. Each one produced a real finding (a broken oracle, a wrong
|
||||
"impossible", a locale red herring), so none was wasted, but the *question*
|
||||
never moved. The signal to stop is not "this is hard", it is "the thing this
|
||||
unblocks is not blocking anything". Write down the recipe you built and leave
|
||||
it for a session with a reason to spend the time.
|
||||
* **Audit what a renderer silently SKIPS, not only what it draws.** `compose` has
|
||||
half a dozen `continue` paths, and a sprite-name miss is one of them — an
|
||||
element vanishes with no error, which is exactly how the port agent's missing
|
||||
bracket happened. `screen render` already prints its omissions; reading that
|
||||
list for the five screens that matter took one command and turned up a
|
||||
full-screen opaque element dropped on three of them. Any composer with skip
|
||||
conditions should be asked to name what it skipped before it is trusted.
|
||||
* **An element's declared name is not necessarily its texture's name.** The
|
||||
`opt ` link on `pteff05.t32` leads to a `T8aD` registered as `8AX`, so a
|
||||
lookup by declared name misses. Before concluding a texture is absent, look at
|
||||
the bytes the link actually points at — the name in the declaration table and
|
||||
the name the sprite table keys on can differ.
|
||||
* **A fit whose residual is large for every model is a broken method, not a
|
||||
close call.** Comparing our composite to a capture pixel-wise gave mean abs
|
||||
errors of 10–14 for gamma, for a linear scale, for everything — and a
|
||||
non-monotonic transfer curve (render 96–127 mapping *brighter* than render
|
||||
128–159). The cause was edge misalignment: at correlation 0.947 a bright pixel
|
||||
in one image routinely lands on a dark one in the other. Restricting to patches
|
||||
that are flat in **both** images dropped the residual to 0.2–1.1. When every
|
||||
candidate model fits badly, stop choosing between them and look at what the
|
||||
comparison is actually measuring.
|
||||
* **Say when a control failed to discriminate, rather than reporting it as a
|
||||
pass.** The held-out screen for a tone-curve fit was the developer splash,
|
||||
whose flat regions are pure black — every model scored ≈ 0.00 error there. That
|
||||
is not corroboration; it is a test with no power, and reporting the 0.00 as
|
||||
agreement would have dressed an untested claim as a verified one.
|
||||
* **Time your probe against the thing you are probing for.** Four runs concluded
|
||||
"the game never reaches the title". `screenshot` costs **10.8 s while xenia is
|
||||
running** and **0.117 s once it is killed** — 92× — so a two-grab polling loop
|
||||
samples every ~41 s, against a title screen this corpus documents as lasting a
|
||||
few seconds. The harness was blinking slower than the event. Before believing a
|
||||
negative from a polling loop, measure its interval and compare it to the
|
||||
duration of what you are waiting for; and measure the probe's cost *under the
|
||||
same load as the run*, because idle timing here was off by two orders of
|
||||
magnitude.
|
||||
* **Do not change a display setting and a capture behaviour in the same run.**
|
||||
`kernel_display_gamma_type = 0` brightens the frame, and `skip_intro.sh`
|
||||
classifies movie-vs-static on an *absolute* rmse threshold — so the gamma
|
||||
change biased the very classifier the run depended on. Harness thresholds tuned
|
||||
on one output configuration are not portable to another.
|
||||
* **Fixing the instrument is how you test the explanation that blamed it.** I
|
||||
diagnosed four failed runs as "the probe samples slower than the event",
|
||||
wrote it up, and withdrew three earlier conclusions on that basis. Building the
|
||||
faster probe refuted it: at 100× the sampling density, over 420 unbroken
|
||||
seconds, the event still never occurred. The diagnosis was a real defect that
|
||||
happened not to be the cause — the most seductive kind, because fixing it feels
|
||||
like confirmation. A plausible mechanism that explains a failure is a
|
||||
hypothesis, and the fix is its experiment, not its proof.
|
||||
* **Ask whether the screen is drawing anything before explaining why a feature of
|
||||
it is missing.** Four iterations produced four explanations for "the title is
|
||||
not detected" — a stale oracle, the locale, the sampling rate, a one-shot boot
|
||||
window — and three of them were refuted in turn. Splitting a raw root grab into
|
||||
bands then showed the game surface at **0.08 % non-black, mean 0.07**: it was
|
||||
rendering black. The check costs one command and belongs at the *front* of that
|
||||
sequence, not after the fourth hypothesis. A detector reporting absence is
|
||||
ambiguous between "the feature is not there" and "nothing is there".
|
||||
* **A newly found fault does not retroactively explain older failures.** The
|
||||
black surface is real and reproducible *in the run where it was measured*.
|
||||
Earlier runs in the same session had measurable content (mean 33.1, mean 122.8,
|
||||
non-zero frame-to-frame rmse), so the failure mode changed over the session.
|
||||
Reaching back to re-explain earlier results with the newest cause would have
|
||||
been the fourth over-reach in a row.
|
||||
* **Validating a probe on static test images tests its ANALYSIS, not its
|
||||
ACQUISITION.** The fast probe's counter was controlled against committed
|
||||
captures and matched exactly (753 / 327), which proved the numpy expression
|
||||
right and the frame source untested. The source was the broken half: a
|
||||
long-lived x11grab stream degrades from 3.98 to 1.60 fps and then freezes,
|
||||
repeating one stale frame. The check that catches it is to read the same screen
|
||||
with an **independent grabber at the same moment** — `import` said 125.65 where
|
||||
the stream said 5.21. Do that during a run, not only at design time.
|
||||
* **A dense negative from one instrument is worth less than one cross-check.**
|
||||
"2 391 frames, zero hits" reads as overwhelming and collapses to nothing once
|
||||
the frames may all be the same frame. Sample count is not evidence of coverage
|
||||
unless the samples are known to be independent.
|
||||
* **A probe that cross-checks itself turns "no result" into a result.** Three
|
||||
successive "the title never appears" claims were withdrawn because the
|
||||
instrument was broken each time. The version that finally held prints its own
|
||||
reading beside an independent grabber's every 60 s — 9 checks, 8 agreeing to
|
||||
within 2 % — so the negative arrives with its own evidence that the frames were
|
||||
live. Building the cross-check into the tool costs a few lines and is what
|
||||
separates a measurement from a silence.
|
||||
* **A hedge in a code comment is an unmeasured claim.** `compose` said the
|
||||
derived paint order "reproduces both measured orders up to ties" — a sentence
|
||||
that sounds like a result and was neither measured nor kept current: there were
|
||||
three measured orders by then, not two. Measuring it took one example program
|
||||
and turned a hedge into a bounded number (exact on 4 of 5 bundles; the 5th off
|
||||
by 8 pairs, all ties). Grep your own comments for "up to", "roughly",
|
||||
"essentially" — each one is a claim nobody has checked.
|
||||
* **Count the cases that can actually bite, not the cases that match the
|
||||
pattern.** `EXTRAS` has 15 tied pairs in its derived paint order, which reads
|
||||
alarming. Only **2** of them overlap, and a tie between non-overlapping
|
||||
elements paints identically either way. Reporting 15 would have overstated the
|
||||
risk by 7×; the useful number is the one filtered by whether the difference can
|
||||
reach a pixel.
|
||||
* **An exhaustive field search needs a positive control, or "found nothing" is
|
||||
worthless.** Scanning a header for a field that reproduces a measured ordering
|
||||
returned zero hits — which could equally mean the field is absent or the scan
|
||||
is broken. Running the same scan against a *known* ordering (declaration order)
|
||||
returned **64** hits, proving the scan finds ordering fields when they exist.
|
||||
Only then is the zero a finding. The control costs four extra lines and turns a
|
||||
silence into a negative with reach.
|
||||
* **A stale row on the handoff page is worse than a missing one.** The port was
|
||||
told SE audio was "undecodable from the disc" while the linked doc had already
|
||||
retracted that and located three cues decoding to PCM. The finding landed in
|
||||
`docs/re/` and the page the port reads kept the superseded text — the fourth
|
||||
time in this corpus. Writing the rule down has not worked, so there is now a
|
||||
tool: `tools/re-capture/handoff_lint.py` flags every HANDOFF line that makes a
|
||||
strong negative claim and links a doc containing retraction language. It found
|
||||
this row, and one benign false positive, on its first run.
|
||||
* **Narrow a risk before reporting it, then narrow it again.** "15 tied pairs on
|
||||
`EXTRAS`" became 2 (only overlapping ties can paint differently), then 1 (one
|
||||
of those two is a `loop*` animation the compositor skips), then "consistent
|
||||
with the capture". Each step is a filter that costs one query and changes the
|
||||
number by more than an order of magnitude in total. A raw count is almost never
|
||||
the number a consumer needs.
|
||||
* **Nothing was checking that the docs' cited evidence exists.** A sweep of every
|
||||
relative link under `docs/` found **16 broken**, and two of them were the
|
||||
figures backing the UI layout decode's headline claim — the port's foundation,
|
||||
unreachable from its own page, because a path in `structures/` was written one
|
||||
directory too shallow. Eleven were wrong relative depth with the target present;
|
||||
five name files that do not exist. Evidence that cannot be opened is not
|
||||
evidence, and a link is exactly the kind of thing no one re-reads.
|
||||
`tools/re-capture/doc_link_check.py` now does it, and also flags targets that
|
||||
resolve to a **zero-byte** file — which looks correct in every listing.
|
||||
* **Repair in bulk only when the counts pair.** The fixer rewrote 11 links; the
|
||||
checker went from 1 038 resolving / 16 missing to 1 049 / 5. +11 and −11
|
||||
against 11 edits is the confirmation that the pass did what it said and touched
|
||||
nothing else. A bulk edit without that arithmetic is a hope.
|
||||
* **When two candidates carry the same content, compare their DIFFERENCE against
|
||||
the oracle, not themselves.** `8AX` (1280×720) and `ptbase` (640×360 at 200 %)
|
||||
are the same artwork, so no comparison of either against a capture can separate
|
||||
them — every such test had been read as "inconclusive, needs a per-draw
|
||||
capture". What separates them is `8AX − upscale(ptbase)`: the detail only one
|
||||
of them has. Correlating the capture's residual against *that* answered it
|
||||
statically, on two screens, with matched controls.
|
||||
* **A ratio that saturates at 1 under noise is not a discriminator.** A pixel-pair
|
||||
test cleanly separated upscales (0.00–0.72) from native (0.98) and put the
|
||||
capture at 1.01 — apparently decisive. Additive noise raises both terms of the
|
||||
ratio equally and drives *any* value toward 1, and fitting a noise term showed
|
||||
both hypotheses reproduce the observed numbers. Before believing a ratio, ask
|
||||
what it does as noise grows.
|
||||
* **Read what a cvar DOES before building an experiment around it.** I planned a
|
||||
run to decide whether a measured gamma was "canary's BT.709 output stage",
|
||||
by setting `kernel_display_gamma_type = 0` and re-fitting. The cvar is not an
|
||||
output stage: it is the value a `kStub` **getter returns to the guest**, which
|
||||
the game uses to build its own ramp, and canary applies *that* ramp in the swap
|
||||
path. The experiment could never have isolated a stage that does not exist —
|
||||
and it would have perturbed the capture harness as a side effect. One grep for
|
||||
the cvar's definition replaced a planned emulator run with a better one.
|
||||
* **An absence in a log is only evidence if the log would have shown it.** No
|
||||
gamma lines appear in any run log here — but kernel exports log at Debug and
|
||||
this harness masks Kernel logging entirely (`log_mask = 13`), so their absence
|
||||
is guaranteed regardless of what the game did. Check the logging configuration
|
||||
before reading silence as a result.
|
||||
* **Check what an experiment actually needs before filing it behind a blocker.**
|
||||
The gamma question sat parked behind "needs the emulator to reach a menu" for
|
||||
several iterations. It needed the emulator only to **boot** — video init, and
|
||||
the `VdGetCurrentDisplayGamma` call, happen in the first seconds, long before
|
||||
any title screen. A blocker that stops one experiment does not stop every
|
||||
experiment in the same area, and it is worth re-reading the parked list against
|
||||
what each item really requires rather than against the area it belongs to.
|
||||
* **A default value is evidence.** Whether the game writes a gamma ramp looked
|
||||
like it needed a GPU trace. It mostly did not: canary initialises the ramp
|
||||
table to **identity** and applies it through a shader that is a pure LUT lookup
|
||||
with no other transfer. An unwritten ramp is therefore a no-op, and any
|
||||
non-identity transfer in the output implies a write. Reading what a field holds
|
||||
when nobody has touched it turns "I cannot observe the write" into "the write
|
||||
must have happened" — cheaper than instrumenting, though it stays an inference
|
||||
and should be labelled one.
|
||||
* **Name the weak joint of an inference in the same breath as the conclusion.**
|
||||
The chain above assumes our composite reproduces the pre-ramp framebuffer,
|
||||
which is the one step that could be wrong. Writing that down beside the
|
||||
conclusion — rather than only the supporting facts — is what lets a later
|
||||
reader attack it instead of inheriting it.
|
||||
* **The startup config dump is the config FILE, not the effective flags.** A run
|
||||
passed `--log_mask=12 --log_level=3` and its dump printed `log_mask = 0,
|
||||
log_level = 2` — while Kernel Debug logging was demonstrably on, which is how
|
||||
that run's finding was obtained. So a dump can neither confirm nor refute a
|
||||
command-line override, and reading one as "my flag was ignored" is a mistake I
|
||||
nearly made with `trace_gpu_stream`. Verify a flag by its *effect*, not by the
|
||||
dump. (This does not undo the earlier `user_language` conclusion: absence of a
|
||||
*name* from the dump still shows the cvar is unregistered.)
|
||||
* **`kill -9` destroys anything that finalises on shutdown.** Canary starts a GPU
|
||||
trace at init but only closes it in `GraphicsSystem::Shutdown()`, so the hard
|
||||
kills this session used routinely could never have produced a trace. Before
|
||||
concluding a feature is broken, check whether the way you stop the program is
|
||||
what discards its output.
|
||||
* **Bound a risky experiment from inside, not by watching it.** A boot-time GPU
|
||||
trace on a disk at 95 % could have filled it between two tool calls. The runner
|
||||
carried its own watchdog that killed the emulator the moment the output passed
|
||||
a 2 GiB cap, so the experiment was safe regardless of how coarsely I polled.
|
||||
The watchdog never fired, which is the point — it cost nothing and removed the
|
||||
need to gamble on timing.
|
||||
* **A cvar existing does not mean the feature is compiled in.** `trace_gpu_stream`
|
||||
parses, is documented, sets state, and does nothing: the code that opens the
|
||||
trace sits behind `#if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1`, which
|
||||
`trace_writer.h` ties to `#ifdef NDEBUG` — off in release. Two runs and two
|
||||
wrong hypotheses before reading the `#if`. When a switch produces no effect,
|
||||
follow the code from the switch to the output and look for a compile-time gate
|
||||
before theorising about runtime causes.
|
||||
* **Test a compile-time gate against the binary, with a control.** A string that
|
||||
exists only inside the guarded block (`_stream.xtr`) settles it in one command:
|
||||
0 occurrences in the release binary, **1** in the debug binary. The debug build
|
||||
is the control that proves the test can find the string when it is there —
|
||||
without it, "0 occurrences" is just as consistent with a bad grep.
|
||||
* **`T8aD` headers sit in RATC child order — use that, not the size, to name
|
||||
one.** Attributing a sprite header to a sprite name by matching decoded
|
||||
dimensions fails whenever two sprites share a size, and `GP_TITLE` build 4 has
|
||||
such a pair (`ptlogo_back2eff` / `ptlogo_back2eff5`, both 1133×280) that differ
|
||||
in exactly the flag being studied. Header order matches child order — verified
|
||||
18/18 on that build against the dimensions as an independent check — so
|
||||
ordering resolves what size cannot.
|
||||
* **Identical summary statistics are not identical data.** Two same-sized sprites
|
||||
reported the same %opaque, %clear and mean alpha to one decimal, which reads as
|
||||
"the bundle stores this texture twice". Pixel-comparing them gives max abs diff
|
||||
**21**: two different renditions of one image. Three matching summaries over
|
||||
300 000 pixels is weak evidence of identity; the comparison costs one line.
|
||||
* **A pattern that is perfect on one screen can be near-chance on the disc.** The
|
||||
`0x02` bit matched `eff` names 10/10 on `GP_TITLE` build 4, which is exactly
|
||||
the sample size that makes a rule feel found. Disc-wide it holds 46.8 % of the
|
||||
time — the build's artists simply named their effect sprites consistently. An
|
||||
18-element bundle cannot distinguish a format rule from a local naming habit;
|
||||
before reporting an implication, run it over the corpus, not the example that
|
||||
suggested it.
|
||||
* **When an association survives a refuted implication, the counterexamples are
|
||||
the finding.** The bit is 3.3× enriched for `eff` names and the sprites that
|
||||
break the rule are `pv_loading_ring0`, `pv_loading_light0`–`3`,
|
||||
`px_bunk_line` — rings, glows, lights, thin lines. That the exceptions are all
|
||||
effect-like artwork *without* the naming convention says more about the field
|
||||
than the rule it broke did.
|
||||
* **Report a classifier's lift over its base rate, not its accuracy.** The
|
||||
`0x02` bit's association with bright-RGB/low-alpha art gives a best single
|
||||
threshold of 76.5 % accuracy — which sounds like a finding until you notice
|
||||
that always guessing the majority class scores 64.1 %. The lift is 12 points,
|
||||
the distributions overlap, and it is a tendency rather than a rule. An accuracy
|
||||
quoted without its base rate is not interpretable.
|
||||
* **Park a field after N failed hypotheses, and say what was eliminated.** Four
|
||||
candidate meanings for one flag bit were each refuted at a cost of roughly an
|
||||
iteration apiece, and the bit blocks nothing measurable. Stopping is the right
|
||||
call, but only if the negative space is written down — additive blend, name,
|
||||
lifetime, premultiplied alpha — so the next attempt starts where this one ended
|
||||
rather than at the beginning.
|
||||
* **Nothing was checking that a doc's figures match its committed data.** A
|
||||
number is written once from a run and then lives in prose that gets edited
|
||||
around it; the data file beside it is regenerated independently. All 19
|
||||
headline figures across four censuses do currently agree
|
||||
(`tools/re-capture/doc_figure_check.py`), which is worth knowing rather than
|
||||
assuming — but the checker had to be written **numerically**, because the first
|
||||
version grepped for the doc's formatting (`14 709` with a thin space, `33.7`
|
||||
rounded from `33.66`) and reported almost every figure as a mismatch. A
|
||||
consistency check that fails on formatting will train you to ignore it.
|
||||
* **A detached job you never check can outlive many iterations.** Two
|
||||
`setsid nohup cargo test … &` runs from earlier iterations were still alive
|
||||
**four hours** later, one child at 89 % CPU for 3 h 26 m, holding the load
|
||||
average at 14 on 12 cores. `setsid` was added precisely so a tool-call timeout
|
||||
could not kill them — which also means nothing kills them. Check that a
|
||||
backgrounded run actually exited before launching the next one, and prefer
|
||||
`ps -o etime=` over assuming.
|
||||
* **Know whether your verification gate can terminate.** `build-reborn test`
|
||||
includes a disc test that decodes 166 `.xpr` files (1.4 GB) and had not
|
||||
finished after 3 h 26 m of CPU. Every "green run" from it is partial unless the
|
||||
suite is stated to have terminated — so report the suite count and elapsed
|
||||
state rather than the word "green", which is what I should have been doing all
|
||||
session.
|
||||
* **"Cannot finish" and "takes an hour" are different claims — measure before
|
||||
choosing.** I wrote that the verification gate *cannot terminate*, from having
|
||||
watched it run 3 h 26 m. Timing the work directly: 19 of 166 containers exceed
|
||||
25 s, `Stage_S02` completes in **144 s**, and one full pass is ~45–60 minutes.
|
||||
Nothing hangs. The 3 h 26 m was that hour of work at a load average of 9–14,
|
||||
inflated by my own duplicate runs. A slow thing observed under contention looks
|
||||
like a stuck thing, and the correction matters: an hour-scale gate can be run
|
||||
deliberately, a hung one cannot be run at all.
|
||||
* **Separate the question with evidence from the question tangled with it.** The
|
||||
keyframe-time reading was held back for several iterations by one objection:
|
||||
adopting it changes `rest()` on a single element. But `rest()` reaches the times
|
||||
only through a fallback that is unsound whenever it runs, and the shift does not
|
||||
fix that fallback either — so the objection was never about the times. Timing
|
||||
had three discriminating measurements; pose-selection had a heuristic guessing.
|
||||
When a conclusion is blocked, check whether the blocker is actually evidence
|
||||
about the same thing.
|
||||
* **A predicate over adjacent PAIRS silently misclassifies a one-element list.**
|
||||
"Has a plateau" was implemented as *any two adjacent keyframes share a pose* —
|
||||
which is false for a single-keyframe element, so 1 502 static elements were
|
||||
counted as having a *guessed* rest pose and the published defect rate was 65 %
|
||||
too high. The error only surfaced when acting on it: suppressing those elements
|
||||
dropped the title's correlation by 0.27, because they include the backgrounds.
|
||||
Whenever a rule quantifies over pairs, ask what it says about a list of one.
|
||||
* **Acting on a claim is a better test of it than re-reading it.** The
|
||||
single-keyframe flaw survived a disc-wide census, a write-up and a handoff row.
|
||||
It died the moment the rule was used to change a rendering, because the result
|
||||
was visibly worse. If a measurement implies an action, take the action on
|
||||
something you can score.
|
||||
* **Score a rule where it can actually differ, or you will measure nothing.**
|
||||
Three rest-pose rules rendered builds 4, 5 and 6 to *identical* correlations —
|
||||
not because they agree, but because the code they change is unreachable on
|
||||
those screens. The signal was on the two splashes, the only builds whose
|
||||
elements reach the fallback at all. Identical results across variants is a
|
||||
clue that the variant is not being exercised, not evidence that the choice does
|
||||
not matter.
|
||||
* **An argument from symmetry is a prediction, not a refutation.** I killed the
|
||||
last-keyframe rule because it treats one of three sibling glows differently,
|
||||
which felt like an artefact. Measured, it is the better rule on both screens
|
||||
where it applies. Aesthetic expectations about how authored data "should" look
|
||||
are worth stating as hypotheses and worth nothing as verdicts.
|
||||
* **A blank render is a correlation of NaN, not a low score — notice which.**
|
||||
Applying "rest = last keyframe" to every element made both splashes render with
|
||||
zero variance, so the correlation was undefined rather than poor. Reading the
|
||||
NaN as "the tool failed" would have hidden the result; it *was* the result, and
|
||||
the strongest form of it. Check for degenerate output before treating a missing
|
||||
number as a broken measurement.
|
||||
* **When a model predicts something and the measurement refuses, the model is
|
||||
usually incomplete rather than wrong.** The shifted time reading says when each
|
||||
pose is reached, which looked like it implied the final pose is the resting
|
||||
one. It does not: it says nothing about whether the group is *played to
|
||||
completion* while the screen is still up. The exit is the dismissal, so a
|
||||
displayed screen never reaches its last keyframe. Both claims survive; what was
|
||||
wrong was the step between them that I supplied.
|
||||
* **The control is what turns a helpful edit into a finding.** Suppressing the
|
||||
`_eff` glows raised both splashes from ≈0.96 to ≈0.998 — on its own that reads
|
||||
as "we over-draw glows". Running the same edit on the three screens that settle
|
||||
made them **worse** (−0.002, −0.092, −0.107), which is what makes the result
|
||||
mean something specific: the over-draw is confined to transient screens, where
|
||||
a plateau mid-animation is not evidence the element is on screen at rest. An
|
||||
edit that improves one set of cases is only interesting once you have shown it
|
||||
damages the cases where it should.
|
||||
* **When a model reproduces durations but not positions, the missing piece is an
|
||||
origin, not a rate.** Playing the splash timeline matched every element's
|
||||
on-screen *length* to within 2 % while placing the elements in the wrong part
|
||||
of the run. That pattern names the gap precisely — the rate and the shape are
|
||||
right, so what is absent is a per-group start — and it pointed straight at the
|
||||
one undecoded word in the group header. (Which was zero, but the diagnosis was
|
||||
still what made the next step obvious rather than a search.)
|
||||
* **Two elements with identical data and different outcomes is the strongest
|
||||
possible evidence that the decision is elsewhere.** `palogo_anima` and
|
||||
`palogo_gamearts` carry byte-identical keyframe times; one is drawn for 95
|
||||
frames and the other not at all. No amount of re-reading the placement data can
|
||||
explain that, and the pair is worth more than a survey — when a hypothesis says
|
||||
"the data decides", look for two records that agree and behave differently.
|
||||
* **A shared resource address does not identify the resource's owner.** The draw
|
||||
log's `tex[base=…]` looked like it would say whether two animation phases came
|
||||
from the same bundle. All three splash phases report `0x11C30000` — including
|
||||
the publisher splash, which is certainly a *different* bundle. The address is a
|
||||
reused upload slot. The control was free and sitting in the same table; without
|
||||
it the shared base would have read as proof of a shared bundle.
|
||||
* **State the mechanism as a separate claim from the observation.** "Declared
|
||||
elements are not what gets drawn" is measured and holds. "Because one bundle
|
||||
activates its elements selectively" was an explanation I attached to it, and it
|
||||
is not established. Bundling the two let the weaker claim inherit the stronger
|
||||
one's evidence.
|
||||
* **Ask what the competing hypothesis would REQUIRE on the disc.** Two
|
||||
explanations for the splash's disjoint phases looked separable only by a
|
||||
capture I could not take, and the capture-side test failed its control. The
|
||||
alternative needed a bundle declaring the glows without the logos — a
|
||||
four-entry enumeration showed none exists, and the question closed statically.
|
||||
A hypothesis that predicts an artefact can be killed by looking for the
|
||||
artefact, which is often far cheaper than measuring the behaviour.
|
||||
* **Check what a "measured" value was measured *from* before reasoning about
|
||||
its limits.** Two things that never co-occur have no observable relative order
|
||||
— true, and a real limit on any order read from a *draw capture*. I applied it
|
||||
to the splash's paint order and withdrew it hours later: that vector is a read
|
||||
of the runtime **child array**, which has a definite order whether or not its
|
||||
children are ever drawn together, and the two halves in question also carry
|
||||
distinct static layer keys. Both the provenance and an independent static field
|
||||
said the order was real. The general trap is not the co-occurrence rule, it is
|
||||
applying a source-specific limit to a value from a different source: the word
|
||||
"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`.
|
||||
|
||||
* **Never measure a file another process is still writing.** `ffprobe` on a
|
||||
half-written transcode reported 33 s against a 137 s source — the shape of
|
||||
catastrophic truncation, with no error and no warning, and it nearly became a
|
||||
filed bug. The tell was the mtime and a climbing packet count, not anything in
|
||||
the tool's output. Before comparing an artifact you did not produce, check that
|
||||
whoever produces it has finished; across two agents sharing a repo this is a
|
||||
race, not an edge case.
|
||||
|
||||
* **A difference-signal RMS means nothing until the two are aligned.** Comparing a
|
||||
transcode against its source gave source RMS −25.3 dB and difference RMS
|
||||
−34.2 dB — only ~9 dB down, which looks like a failure. It is inconclusive: a
|
||||
one-sample offset makes the difference nearly as loud as the source. Any such
|
||||
test needs cross-correlation to align first and an agreed downmix, and only then
|
||||
is a pass mark like ">40 dB down" meaningful. Reporting the 9 dB as a result
|
||||
would have been a confident wrong number.
|
||||
619
docs/re/REFUTED.md
Normal file
@@ -0,0 +1,619 @@
|
||||
# Refuted — claims that were tested and died
|
||||
|
||||
**Read this before proposing a hypothesis.** Every line below was believed at
|
||||
some point, measured, and found false. Reviving one costs a whole iteration and
|
||||
produces nothing.
|
||||
|
||||
This file exists because the list had been living in the autonomous agent's
|
||||
*loop prompt* — the only copy, lost the moment the container was. A refuted
|
||||
result is a real result; it belongs in the corpus like any other.
|
||||
|
||||
**Format:** each entry is the claim as it was believed. Where the true answer is
|
||||
known it follows after `→`. Grouped by subject so a grep for your noun finds the
|
||||
neighbourhood, not just the line.
|
||||
|
||||
---
|
||||
|
||||
## Offsets, structs and the progress singleton
|
||||
|
||||
* `position = instance − 0x12c` → refuted.
|
||||
* `+0x29d0` → refuted.
|
||||
* "an offset intersection can find a struct's consumer" → **only for LARGE or
|
||||
unusual offsets.** Small ones have no power (`+184`: 301/351/115 hits).
|
||||
* "a `+1956` store means a progress write" → writes go through the COPY, not
|
||||
direct stores. 9 direct stores, none of them a progress write.
|
||||
* "the singleton-global filter can find progress writers" → it fails its own
|
||||
control.
|
||||
* "the progress copy destination is an `r1`-relative stack local" → it is a
|
||||
**frame register**. The `r1` assumption returned 0 for all 21 candidates
|
||||
*including the known-good* — the filter was killed by its own control.
|
||||
* "word B's writer also stores the Time/Points record" → it does not.
|
||||
* "the debriefing records the metric with the clear bit" → 44 calls, exactly two
|
||||
strings (`DEBRIEFING`, `BASE_INFO`), no `Time`, no `Points`.
|
||||
* "`0x820AF030` holds live state" → all 384 words constant; it is a
|
||||
spawned-entity record, not live state.
|
||||
|
||||
## Screens, classes and RTTI
|
||||
|
||||
* "the rotated draw in the title's capture is the `Z` swoosh
|
||||
(`ptlogo_back2*`)" → **mine, and refuted.** Its quads span y −209…925 and
|
||||
−292…1012 in screen space; the swoosh is a band at y 126…360.
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* "the keyframe words at `+4`/`+8`/`+12` are always zero" → they are non-zero in
|
||||
**4.76 %, 4.62 % and 14.50 % of 83 862** blocks disc-wide, reading as
|
||||
**degrees** (±180, ±90, 120, 360). The original note was a sample artefact.
|
||||
(Superseded figures: an earlier count of 72 287 blocks missed every nested
|
||||
record — see the alignment entry below.)
|
||||
* ~~"those angle fields are where the title's rotated quads come from" → **no** —
|
||||
every `GP_TITLE` build 4 element has all three at zero.~~ → **that refutation
|
||||
was itself wrong, and is withdrawn (2026-08-28).** `+12` *is* exactly where
|
||||
they come from. Build 4's **top-level** elements do all read zero; the rotated
|
||||
quads belong to its two **nested** leaf records, `ptloop01.rat` (`+12` = 30)
|
||||
and `ptloop02.rat` (`+12` = −45), which the census never opened. Measured
|
||||
off the GPU: **+30.26°** and **−45.28°**.
|
||||
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||
* "the rotated draw's element cannot be named from the capture" → refuted; its
|
||||
quads' **edge lengths** name it. 400 × 1076 and 400 × 1444 match `pteff03`
|
||||
399×180 at 600 % and `pteff03a` 399×180 at 800 % — two different heights, both
|
||||
landing. [`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||
* "a keyframe-block scanner may assume 4-byte alignment" → refuted; it found
|
||||
**0/3** of its own control blocks and under-counted the corpus by 16 341
|
||||
blocks. Nested `RATC` blobs start at odd offsets (`0xbb5966`).
|
||||
* "keyframe rotation lives only in **nested** `.rat` leaf records" → **mine, and
|
||||
refuted within the hour by my own sweep.** It held for the three archives I had
|
||||
checked (`GP_TITLE`, `GP_BUNK`, `GP_CHALLENGE`) and failed on the next:
|
||||
`GP_DIALOG` and `GP_DEBRIEFING_PILOTLOG` rotate **top-level** elements, and
|
||||
those are the clearest examples on the disc.
|
||||
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||
* "the pivot-anchored scale formula `kf.x − pivot·(scale−100)/100` is our
|
||||
renderer's reading, not a measurement" → **now measured.** At the `ptloop`
|
||||
pair's 600 %/800 % scale it predicts both quad centres at y = 360.0 against a
|
||||
captured 359.1/360.0, where top-left anchoring predicts 810/990.
|
||||
[`ui-keyframe-rotation.md`](structures/ui-keyframe-rotation.md)
|
||||
* "the game passes a pink per-vertex colour for the title swoosh" → **refuted by
|
||||
draw capture.** Every vertex colour in the capture is `<alpha>FFFFFF`, white RGB.
|
||||
* "the swoosh discrepancy is undecodable" → **solved**: the game submits it as two
|
||||
**rotated parallelograms**; `ui_layout::blit` only does axis-aligned rectangles.
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* "the `--log_ui_draws` per-draw capture reads the guest's blend state" →
|
||||
**mine, and wrong.** It records primitive type, index count, index-buffer
|
||||
address, VS/PS ucode hashes, texture bindings and vertex attribute 0 — no
|
||||
`RB_BLENDCONTROL`. It can test vertex colour as-is; blend state needs a Canary
|
||||
change. [`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* "the plate-free title capture (t ≈ 4.0 s) may be too early to be settled" →
|
||||
**mine, and refuted.** The swoosh band correlates 0.7342 at t = 4.0 s and
|
||||
0.7353 at t = 21.5 s — identical to 0.001 over 17.5 s.
|
||||
* "`T8aD +0x04` bit `0x02` selects an additive blend" → **mine, and refuted.**
|
||||
Blending those sprites additively worsens every measure against the capture.
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* "the title logo elements' wrong pivots (off by up to 59 px, authored against the
|
||||
other language's sprite) explain the swoosh rendering too thick" → **mine, and
|
||||
refuted.** `blit` sizes from the texture and applies the pivot only as
|
||||
`pivot·(scale−100)/100`; all seven swoosh elements are scale `(100,100)` at every
|
||||
keyframe, so the term is zero. The mismatch is real but inert here — it would
|
||||
bite `ptlogo1`/`ptlogo2`, which scale to 150 during the build-in.
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* "the title screen loops at ≈ 2.2 s" → **mine, and doubly wrong.** The title
|
||||
*art* is near-static (wordmark sd 0.06); the 2.3 s pulse is the
|
||||
**`PRESS Ⓐ BUTTON` plate**, which is a *different build* (2, not 4). Localised
|
||||
by a per-tile amplitude map and decoded in `ptbtn00f.rat`.
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* ~~"a `.rat` leaf record's keyframes use the build's 40-byte layout" → they do
|
||||
not~~ — **withdrawn.** They do. The scan that "found nothing" required 29
|
||||
increasing times; the records hold **three**. Decoded in
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md).
|
||||
* "the title's measured ≈ 2.2 s oscillation is the `ptloop01`/`ptloop02`
|
||||
elements" → **mine, and refuted by decoding them.** Their sweeps run 7.5 s and
|
||||
9.5 s; a 22 s capture showed 8 peaks, not 3. The period's source is unidentified.
|
||||
* "the developer splash cannot be rendered by `screen render` at all" → it can,
|
||||
with **`--all`**. It is only invisible to the *default* listing, which filters on
|
||||
`is_build`. [`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
* "the four `GP_TITLE` entries that are not screen builds are unidentified" → they
|
||||
are the **splash**: 10/13 the `SQUARE ENIX` logo, 11/14 the developer logos.
|
||||
* ~~"the boot title is phase 2 and the attract title is phase 4 state 0, and only
|
||||
phase 2 handles Ⓐ" → refuted~~ — **the REFUTATION is withdrawn.** The test
|
||||
assumed Ⓑ lands in phase 4 state 0; the event-0 block actually sets
|
||||
**phase = 2** and state = 0 together, so it probed phase 2. The hypothesis is
|
||||
untested, not dead.
|
||||
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||
* "any title after the first one refuses input" → too broad. The **Ⓑ-returned
|
||||
title accepts Ⓐ**; only the **attract**-returned title is inert.
|
||||
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md)
|
||||
* "`sub_821C6458` is `GamePart_Title`'s state machine" → **mine, imprecise.** It
|
||||
is the machine for **phase 4** of a five-way outer dispatch at `this+132`;
|
||||
phase 0 is the splash. The ten states and eighteen edges are phase 4 only.
|
||||
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||
* "`sub_821CC860` is the game's by-name screen factory" → **mine, and wrong.** Its
|
||||
decoded arguments include `BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`,
|
||||
`GAMMA_RGB` — it is a **generic name-keyed lookup**, mostly config.
|
||||
* "`DIFFICULTY` and `EXTRA_MENU` are corroborated screen names" → **mine, and
|
||||
wrong.** Neither appears in `r5` at any of the 48 call sites; they were strings
|
||||
merely referenced by the same functions. Only `TUTORIAL_MENU` survives.
|
||||
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||
* "the `{func, func, ptr}` triples at `0x820a3b48` are a GamePart state table" →
|
||||
**static-initialiser records trailing the `RegisterToFactory` strings.** The
|
||||
bytes before them are the tail of a diagnostic string and the data column is
|
||||
zero-filled descriptors.
|
||||
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||
* "the boot sequence is driven by a table the game reads" → it is **not
|
||||
data-driven**; four search spaces closed, transitions are calls with an id
|
||||
argument. [`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||
* "`config.ini` is a GamePart settings table, so the boot order is in it" → its
|
||||
`[SYSTEM]` section is **empty**; the only populated section is `[LANGUAGE]`.
|
||||
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md)
|
||||
* "the attract loop is `GP_ADVERTISE_DEMO` (GamePart 1)" → 🟡 id 1 has **no
|
||||
registration site** in the shipped build; the attract is the title replaying
|
||||
`ADV.wmv`.
|
||||
* "the 29 id-table names are 29 distinct GameParts" → 24 register, and `3`/`4`
|
||||
are the **same class** (`GamePart_SaveLoad`).
|
||||
* "Ⓐ on `NEW GAME` leads to a standing black-screen hang" → it opens
|
||||
**`DIFFICULTY`**, then **`SELECT DATA`**. What looked like a hang was a menu
|
||||
waiting for input that nobody pressed; the crash that follows is the already
|
||||
documented `sub_823070B0` cache throw.
|
||||
[`menu-navigation-semantics.md`](menu-navigation-semantics.md)
|
||||
* "tapping Ⓐ during the boot movie breaks the title" → **one** tap skips the
|
||||
movie cleanly and the title works normally. It is *hammering* (88 presses) that
|
||||
breaks it. [`movie-binding.md`](movie-binding.md)
|
||||
* "the attract movie runs ~85 s, so it is not `ADV.wmv` (137 s)" → **mine, and
|
||||
wrong.** Sampling began 39 s into the movie, so what was timed was its tail.
|
||||
The attract movie **is** `ADV.wmv`, played in full.
|
||||
[`movie-binding.md`](movie-binding.md)
|
||||
* "the boot intro and the attract movie are different videos" → one asset, one
|
||||
manifest slot (`ADVERTISE_MOVIE`).
|
||||
* "the new-game intro is unidentified" → `MS00A` → `S00A.wmv`, decoded from the
|
||||
movie manifest.
|
||||
* "`GP_READY_ROOM.pak` holds the Ready Room screen" → its 317 distinct element
|
||||
names contain **none** of the six visible labels; it is the briefing /
|
||||
tactical-map content behind the `BRIEFINGS` item.
|
||||
[`ready-room-probe.md`](ready-room-probe.md)
|
||||
* "the Ready Room might be 3D with a UI overlay" → it is **2D**; the pak carries
|
||||
zero 3D containers.
|
||||
* "element kind `0x3002` is *the* button kind" → title-side only. All 902
|
||||
`GP_READY_ROOM` bundles have **zero** `0x3002`; that pak uses `0x3000`,
|
||||
`0x3004`, `0x300c`, `0x3008`. `0x3002` is one member of a `0x3000` family.
|
||||
* "the transition between menu screens is a cut" → it is a **fade through
|
||||
black**; a 0.5 Hz screenshot burst simply samples too slowly to see it.
|
||||
[`screen-transitions.md`](screen-transitions.md)
|
||||
* "the main menu's initial focus is fixed" → three boots of one script gave
|
||||
`TUTORIAL`, `TUTORIAL`, `NEW GAME`.
|
||||
* "the title menus drop d-pad presses shorter than ~0.3 s" → **refuted by my
|
||||
own data.** The menu **wraps at both ends**; every press registered, and the
|
||||
"missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md).
|
||||
* "the main menu opens with `NEW GAME` focused" → it opens on **`TUTORIAL`**,
|
||||
2/2 boots (🟡 a third recorded run implies `NEW GAME`, so this is reproducible,
|
||||
not invariant).
|
||||
* "`GP_TITLE` builds 6/8/9 are three submenus" → **8 is the JAPANESE main
|
||||
menu**, and 6/9 are the English and Japanese `EXTRAS` submenu. `GP_TITLE`
|
||||
holds eight screens shipped twice (EN/JP), and exactly one submenu. See
|
||||
[`ui-title-build-map.md`](ui-title-build-map.md).
|
||||
* "the `PRESS Ⓐ BUTTON` plate is a state of the title build" → it is **its own
|
||||
build** (2/3), composited over build 4 and faded in a beat later.
|
||||
* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150
|
||||
`ANON_`, 0 `rtti_present`, 0 base classes.
|
||||
* "the sibling vtable methods name the class" → they cannot.
|
||||
* "`xrefs` can name the callers of a vtable method" → no.
|
||||
* "the `ind_call` refutation voids existing corpus claims" → damage bounded,
|
||||
4/4 caller claims verify. But **`xrefs.ind_call` is a CROSS PRODUCT** — always
|
||||
filter `kind='call'`.
|
||||
* "`BASE_INFO` marks the 5-slot screen family" → it discriminates
|
||||
screen-config from table-read, 9/9 vs 10/10.
|
||||
* "a high key count means a rich screen" → `sub_82297550` / `sub_822A2F00`'s 27
|
||||
"keys" are coordinate pairs, i.e. a layout table.
|
||||
* "`EX_` = the CHALLENGE-mission debriefing" → `EX_` is **EXTRA**, mission-kind
|
||||
3.
|
||||
* "the `EX_` selection has not been shown" → it has: `[[obj+4]+184] == 3`.
|
||||
|
||||
## Stages, missions and the challenge set
|
||||
|
||||
* "the disc's stages are numbered 1..28" → S01–S16 story, S17 **cut**, S18–S23
|
||||
tutorials, S24–S29 challenge.
|
||||
* "S24–S29 are story missions" → they are the challenge missions.
|
||||
* "the challenge missions have their own maps" → they reuse
|
||||
`GP_MAIN_GAME_E.pak`'s stage records.
|
||||
* "the challenge `REQUIREMENT` values are 16,25,26,27,29" → the chain is
|
||||
16→24→25→26→27→28.
|
||||
* "the `Extra0n` family shares one leaderboard metric" → `RECORD_TYPE` is
|
||||
per-stage: 3 Time / 3 Points.
|
||||
* "stage = filled SHAB count + 1" → refuted.
|
||||
* "the first TRIGGER is always the point of no return" → refuted.
|
||||
* "`EnumUnit_S14.tbl` might be missing" / "S14's 13 are a manifest omission" /
|
||||
"asteroids are exempt from the manifest" → S14's 13 are **dangling
|
||||
deployments**. NEEDS-HUMAN: fly S14.
|
||||
* "`StageMessageSet_S02.tbl` is not in the pak" → it is.
|
||||
* "`S28_p1` has an asteroid volume with no definition" → refuted.
|
||||
* "`test_s8p1_asteroid.tbl` is test-only" → refuted.
|
||||
* "the settings family has 28 or 29 objects" → 24.
|
||||
|
||||
## ISL / mission scripting
|
||||
|
||||
* "the bytecode is in the `.embsec_` sections" → refuted.
|
||||
* "only 31 built-ins take a unit" → refuted.
|
||||
* "`sub_8230C398` is the message pump" → refuted.
|
||||
* "`bus+8216` is the subscriber registry" → refuted.
|
||||
* "the ScriptPhase vtable is ≥200 slots" → 113.
|
||||
|
||||
## IDXD, paks and naming
|
||||
|
||||
* "IDXD record keys are `name_hash`" → record keys are **`tag_hash`**
|
||||
(case-SENSITIVE); `name_hash` is case-INSENSITIVE and used for pak keys.
|
||||
* "pak TOC order is stage order" / "TOC order is semantic order" → it is not.
|
||||
* "the executable holds the asset names" → the image names **no data value at
|
||||
all**; that route is powerless.
|
||||
* "the image might name a data VALUE" → powerless.
|
||||
* "the XPR2 manifest names hash to the DefTables tables" → refuted.
|
||||
* "the `DefTables` model names are unreachable" → reachable via the `Enumerate`
|
||||
declaration tables (1 413/1 425, 99.2 %).
|
||||
* "the `GP_MAIN_GAME_*` unnamed block is undiscovered data" → refuted.
|
||||
* "each `GP_MAIN_GAME_*` `Enumerate` object declares something" → refuted.
|
||||
* "`GP_HANGAR_ARSENAL` is missing data tables" → refuted.
|
||||
* "the `Enumeration` self-index can name objects" → a self-index names
|
||||
**records, not files**.
|
||||
* "a per-pak prefix might close the 2D blocker" → no.
|
||||
* "the `+` paths might name the 2D or `GP_READY_ROOM` keys" → the `+`-dictionary
|
||||
route is exhausted, 0 of 1 817.
|
||||
* "the `game:\` paths are unresolved" → refuted.
|
||||
* "a set-difference over file names can see reuse" → it cannot; **join per
|
||||
USER**. Per-pak copies are ×6.
|
||||
|
||||
|
||||
## Audio
|
||||
|
||||
* "which bank the menu plays is not on the disc" → the **cue table** cannot say
|
||||
(all BGM cues are numeric), but the **code** can: `sub_821C5580` plays cue
|
||||
**1103 = `BGM_103`**, and its two declared waves match the two streams the XMA
|
||||
probe saw byte-for-byte.
|
||||
* "the observed BGM stream sizes match no bank's declared waves, so the game hands
|
||||
the decoder a window" → **mine, and wrong** — I checked only the `BGM_0xx` rows.
|
||||
They are `BGM_103`'s two waves exactly; the game hands over the whole wave.
|
||||
|
||||
* "an individual SE cue's audio cannot be extracted" → **mine, and wrong.**
|
||||
`--xma_param_probe=true` logs each stream's head bytes; searching them in
|
||||
`Static.slb` locates the wave exactly. [`menu-audio-cues.md`](menu-audio-cues.md)
|
||||
* "`Static.slb` has no wave boundaries, so its layout is unknown" → it is a
|
||||
**packed run** of whole 2 048-byte XMA packets with no delimiters — the two
|
||||
located cues are contiguous. There is nothing to scan for, by design.
|
||||
|
||||
* "`Pj_Silph.xgs` holds the cue→wave index, so parse XACT" → **no XACT container
|
||||
exists on this disc**: 0 × `XGSF`/`SDBK`/`WBND` in all 1.08 GB of `sound.pak`,
|
||||
and no `XACT`/`.xgs` string in the executable. The extensions are the authoring
|
||||
tool's, not the format's. [`menu-audio-cues.md`](menu-audio-cues.md)
|
||||
|
||||
* "every sound cue resolves to its own `.slb` bank" → the 322 `SE_*` cues do not;
|
||||
**0 of 322** are in `FILES`, and `BANK_SE` puts them all in `Static.slb`.
|
||||
* "`Static.slb` can be split into waves like any other bank" → it holds **0
|
||||
`RIFF`, 0 `seek`, 0 `WAVE`** across all 8 353 472 readable bytes.
|
||||
[`menu-audio-cues.md`](menu-audio-cues.md)
|
||||
|
||||
* "`BGM_001.slb` is three sub-waves (10 KB + 4.47 MB + 4.67 MB)" → the 10 KB is
|
||||
the **bank header**; a bank is **two** waves.
|
||||
* "a music bank's two waves might be intro + loop, two variations, or two halves"
|
||||
→ they are **two stems of one performance, played together** — equal duration
|
||||
in 32/32 banks, and sample-synchronous.
|
||||
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md)
|
||||
* "`BGM_106`–`BGM_109` break the two-wave rule" → they are the leading-region
|
||||
straddle; realigned across entry boundaries they obey it.
|
||||
* "the cue table names which BGM belongs to which screen" → all 32 BGM cues are
|
||||
numeric (`BGM_001`…`BGM_109`).
|
||||
|
||||
## Units, weapons, effects and assets
|
||||
|
||||
* "`Generic` (394) is the unit datasheet" → refuted.
|
||||
* "a loadout's `Arm1` names an item" → it names a **hardpoint slot**
|
||||
(`Turret_NNN`), 59/59.
|
||||
* "`EnumUnit` and the unit datasheet share a vocabulary" → they do not.
|
||||
* "the roster is the `Generic.Model` set" → roster 40, `Generic.Model` 46,
|
||||
`GameResourceID` 480 — three vocabularies.
|
||||
* "every unit ID is `UN_<l>###_<FACTION>_<name>`" → the grammar is
|
||||
`UN_<letter>###_[<subkind>_]<FACTION>_<name>`.
|
||||
* "`_EXn` is the `Extra0n` index" → three different `EX` vocabularies exist.
|
||||
* "the only two `_EX5` names on the disc are the AA gun and the DeltaSaber" →
|
||||
refuted.
|
||||
* "running the tutorial will instantiate the `_Ttrl` weapons" → refuted.
|
||||
* "the disc has exactly three `EnumWeapon` tables" → four.
|
||||
* "the `wep_NN` package gaps are unshipped weapons" / "`wep_85` is the tip of a
|
||||
family" → `wep_85` is the **only** declared-but-unshipped asset (59/0/1/26).
|
||||
* "nothing is deployed without being declared" → refuted.
|
||||
* "effects are one namespace" → refuted.
|
||||
* "the 58 undeclared effect names are missing assets" → refuted.
|
||||
* "all five orphan effects are unshipped" → refuted.
|
||||
* "`eff_f0002` ships in `Base.xpr`" → refuted.
|
||||
* "`Base.xpr` holds more bound effects than `ptc_pack`" → refuted.
|
||||
* "the 34 unlocated are a scatter" → refuted.
|
||||
* "the 9 unlocated might be under another prefix" → refuted.
|
||||
* "`ptc_pack` has 532 names" → 727.
|
||||
* "the `_e`/`_f` law is effect-FIELD-specific" → it is the **faction law**,
|
||||
564/564.
|
||||
* "a disc-wide `.xpr` byte search can show an effect is ABSENT" → it cannot.
|
||||
* "`rot_n001` is on the disc" → refuted.
|
||||
* "`rou_f004`'s mesh is in `Stage_S28.xpr`" → it is in `DeltaSaber_A.xpr`.
|
||||
* "`parent` + `_all` + `_child` is the composite-model convention" → refuted.
|
||||
`_hangar` **is** real (59 of 166, 59/59 with a bare twin); `_all`/`_child` is
|
||||
not.
|
||||
* "`Motion_guard_start` has no damage variants" → refuted.
|
||||
* "`CoverArea` bits 2 and 3 are mutually exclusive" → refuted.
|
||||
* "the 27 unresolved `NamePlate` values are missing objects" → refuted.
|
||||
|
||||
## LOD, background and misc tables
|
||||
|
||||
* "`EnumLODSet_*` is a per-stage family" → `EnumLODSet_test.tbl` serves 17
|
||||
stages; 17+5+1 = 23.
|
||||
* "there are 8 orphan LOD tables" → 6.
|
||||
* "the orphans are stale copies of `_test`" → refuted.
|
||||
* "S25 is absent from the `DefTables` LOD families" → refuted.
|
||||
* "`BackGroundID` has no referent anywhere" → it is an **identity**.
|
||||
* "`BackGroundPackage == BG_<id>.xpr`" → refuted.
|
||||
* "`<X>ID` + `<X>Package` is a convention" → refuted.
|
||||
* "`Placement_*` / `RouteTest_*` are unattached" → refuted.
|
||||
* "the `AsteroidDefinition` join does not reproduce by hash" → it does.
|
||||
* "the 8-value frame is a new finding" → it was already in the corpus.
|
||||
|
||||
## Loaders, config and tuning
|
||||
|
||||
* "the config reader is XML" → INI.
|
||||
* "`sub_822F9498` is the unit-definition loader" → it is `PlayerParams`'s.
|
||||
* "`sub_822AE628` reads the main-game `Tweak`" → refuted.
|
||||
* "`sub_8230D1F8` is a rank table" → it is the stage-settings loader.
|
||||
* "`sub_82286BC8`'s key list is new" → refuted.
|
||||
* "`sub_825F2CF0` / `sub_825F2F88` read a post-processing table" → refuted.
|
||||
* "`Booster` is a new schema" / "`Booster` is the player craft's flight
|
||||
envelope" → refuted; nothing selects `Booster`.
|
||||
* "the `AnalogRevice`/`Tweak` block is unreachable" → reachable
|
||||
(`sub_821A6CF0`, base `0x820A1630`).
|
||||
* "a 0-xref string block has no reader" → refuted.
|
||||
* "the AI table was NEEDS-HUMAN" → refuted.
|
||||
* "the `PG*` HUD names are undocumented" → they are documented.
|
||||
* "a base-solver row identifies a FUNCTION" → it does not.
|
||||
* "a 64K-boundary base is low confidence" → **inverted**; it is high
|
||||
confidence.
|
||||
* "the 0x820B0000 cluster is a false positive" → refuted.
|
||||
* "a pointer to a function in the image implies a registry" → refuted.
|
||||
`.pdata` is not a registry.
|
||||
* "the 13 player-facing chatter tables are the WINGMAN tables" → refuted.
|
||||
* "the 8 undeclared chatter tables are tutorial chatter" → refuted.
|
||||
* "other datasheets ship a schema too" → refuted.
|
||||
|
||||
## Encoding and text
|
||||
|
||||
* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328.
|
||||
* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.
|
||||
* "the splash `_eff` glows hold a constant α ≈ 33, contradicting their declared
|
||||
255 plateau" → **mine, and refuted within the iteration.** They ramp 34 → 255
|
||||
in exact steps of 34. I had printed the series' minimum and read it as its
|
||||
range. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||
* "the declared keyframe timeline reproduces the captured splash" → **refuted for
|
||||
multi-keyframe elements.** `palogo_gamearts` is still at `a=255` nine frames
|
||||
after its declared `a=32`, and its declared 80-frame fade-in is never drawn.
|
||||
The `_eff` glows do reproduce, exactly — so this is about the group timeline,
|
||||
not about the interpolation law. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||
* "the `_eff` elements' agreement is the whole case for the keyframe-time shift,
|
||||
so it stays a shape argument" → superseded. The **hold duration** is
|
||||
calibration-free and decides it: observed 83 frames of full alpha against a
|
||||
predicted **2.0** as decoded and **80.0** shifted. The shift is nonetheless
|
||||
**not adopted** — it moves `GP_TITLE` build 7 by 13 % of pixels, away from its
|
||||
verified English twin's brightness. [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||
* "the `GP_TITLE` build 7 render difference is evidence against the keyframe-time
|
||||
shift" → **mine, and withdrawn.** It is one element, `ptlogo_eff3.t32`, a
|
||||
transient bloom with no resting pose; `rest()`'s dwell fallback returns a
|
||||
different endpoint of the same movement under each reading. The brightness
|
||||
comparison measured our heuristic, not the decode.
|
||||
[`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||
* "`rest()`'s longest-dwell fallback picks the pose the element rests at" →
|
||||
**refuted structurally.** A dwell gap is time spent interpolating *between*
|
||||
poses; an endpoint is only held when the two poses are equal, which is a
|
||||
plateau, which the earlier path already returned for. Every element that
|
||||
reaches the fallback has a guessed rest pose.
|
||||
* "`rest()` for a plateau-less element should be the **last keyframe**" → **mine,
|
||||
and refuted.** The developer splash's three sibling glows are structurally
|
||||
identical and differ by one byte (`a=212` vs `a=255` at `t=45`); that rule
|
||||
makes `palogo_anima_eff` alone invisible while `gamearts_eff` and `seta_eff`
|
||||
stay lit. Capture box-mean ratios (0.717 / 0.723 / **0.772**) go the other way
|
||||
too. [`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||
* "a keyframe `scale` of 0 means *unset*, so render at 100 %" → **refuted by a
|
||||
disc-wide control.** 2 166 elements have a zero-scale keyframe and **not one is
|
||||
zero on every keyframe**, while 1 762 grow back out of zero (`ptlogo_eff3.t32`
|
||||
runs 0 % → 200 %). Zero means collapsed; the renderer now draws nothing.
|
||||
[`structures/ui-rat-layout.md`](structures/ui-rat-layout.md)
|
||||
* "a Japanese-locale capture is impossible in this container, because canary has
|
||||
no `user_language` cvar" → **mine, and refuted the next iteration.** The cvar
|
||||
really is absent, but the language is persisted in
|
||||
`<storage_root>/xconfig.settings` (`user.language`, BE u32 at file offset
|
||||
`0x912`, located from three struct landmarks) and that file is writable. The
|
||||
capture is still not *taken* — a Japanese run never reached the title in 787 s
|
||||
— but it needs a longer run, not a rebuilt emulator.
|
||||
[`tools/re-capture/set_console_language.py`](../../tools/re-capture/set_console_language.py)
|
||||
* "the Japanese-locale run never reached the title in 787 s" → **the measurement
|
||||
was broken, not the run.** `wait_title.sh` carried the superseded single-pixel
|
||||
oracle. Re-run with `is_title.py`: the game still did not present the
|
||||
interactive title, but that is now a measured statement (zero green-glyph
|
||||
pixels, correlation ≤ 0.22 to either build-7 render) rather than an artefact.
|
||||
* "the Japanese-locale run fails to reach the interactive title *because of the
|
||||
locale*" → **refuted by the English control.** 75 samples over 734 s with the
|
||||
same flags and oracle, every one glyph = 0. Neither locale presents the
|
||||
interactive title without a pad press.
|
||||
* "neither locale reaches the interactive title without a pad press" /
|
||||
"the game sat in the attract loop for 604 s" → **withdrawn as causes.** Both
|
||||
rest on runs whose polling loop sampled every ~41 s, because `screenshot`
|
||||
costs 10.8 s while the emulator runs (0.117 s idle, 92×). A title lasting a few
|
||||
seconds would be missed. The observations stand; the conclusions drawn from
|
||||
them do not. [`capture-harness-status.md`](capture-harness-status.md)
|
||||
* "the boot harness fails because its polling loop samples every ~41 s, slower
|
||||
than the title screen lasts" → **mine, and refuted by my own fix.** The
|
||||
sampling defect was real (3.98 s → 0.29 s per sample, 13.7×, control-verified
|
||||
at 753/327), but a probe running at 3.99 fps for **420 continuous seconds —
|
||||
1 674 samples — still saw zero green-Ⓐ pixels.** Sampling rate was not the
|
||||
cause. [`capture-harness-status.md`](capture-harness-status.md)
|
||||
* ~~"neither locale reaches the interactive title without a pad press"~~ →
|
||||
withdrawn last iteration for want of evidence, now **reinstated as a
|
||||
measurement**: 1 674 dense samples over 420 s, English, zero glyph frames.
|
||||
⚠️ Reach: a mid-run window only; it says nothing about the boot title.
|
||||
* "the PRESS Ⓐ plate appears only in the **boot** title window, which mid-run
|
||||
sampling could never catch" → **mine, and refuted.** The fast probe was
|
||||
attached at t=0: **2 391 frames over 600 s at 3.98 fps from launch**, max glyph
|
||||
0. The plate did not appear at any point in the first ten minutes.
|
||||
[`capture-harness-status.md`](capture-harness-status.md)
|
||||
* "2 391 frames over 600 s from t=0, max glyph 0, therefore the title never
|
||||
appears in the first ten minutes" → **withdrawn: the instrument stalls.** A
|
||||
single long-lived x11grab stream degrades 3.98 → 1.60 fps and then freezes,
|
||||
repeating one stale frame; cross-checked, it read surface mean 5.21 where
|
||||
`import` read 125.65 at the same moment. A dense negative from a frozen stream
|
||||
is not a negative. [`capture-harness-status.md`](capture-harness-status.md)
|
||||
* "the `T8aD` layer key fully determines a screen's paint order" → **refuted, and
|
||||
the remainder is undecodable.** Elements sharing a key are tied; on the title
|
||||
the game paints the five tied `ptlogo_back2eff` glows `1,2,5,3,4` while the
|
||||
declaration table, the RATC child order and **every** field in the `T8aD`
|
||||
header (exhaustive 0x00–0x7f, u8/u16/u32, both directions — 0 matches against
|
||||
64 for the declaration-order control) all give `1,2,3,4,5`.
|
||||
[`ui-paint-order-derived-check.md`](structures/ui-paint-order-derived-check.md)
|
||||
* "SE audio is undecodable from the disc — no XACT container exists anywhere"
|
||||
(as it stood on the **handoff page**) → **stale**: `menu-audio-cues.md` had
|
||||
already retracted it and located three cues in `Static.slb` that decode to PCM.
|
||||
The retraction never reached the row the port agent reads. Handoff row fixed;
|
||||
`tools/re-capture/handoff_lint.py` now checks for this class.
|
||||
* "which of `8AX` and `ptbase` the game draws needs a per-draw capture recording
|
||||
texture base addresses" → **mine, and refuted — it is settled statically.** The
|
||||
two carry the same art at two resolutions, so neither compares usefully against
|
||||
a capture; their *difference* does. Correlating the capture's
|
||||
departure-from-upscale against the 8AX-only detail gives +0.0475 (main menu)
|
||||
and +0.0634 (title), both 68 % of ceiling against matched controls of ≤0.0095.
|
||||
The game draws the full-res `8AX`.
|
||||
[`ui-8ax-fullres-background.md`](structures/ui-8ax-fullres-background.md)
|
||||
* "the pixel-pair ratio shows the capture is native, not an upscale" → **mine,
|
||||
and withdrawn as evidence.** Upscales give 0.00–0.72, native 0.98, capture 1.01
|
||||
— but additive noise pushes any such ratio toward 1, and both "native + noise"
|
||||
and "bilinear upscale + noise" fit the observed values. The conclusion happens
|
||||
to be right; this test does not establish it.
|
||||
* "the render-vs-capture gamma may be canary's own BT.709 output transform, since
|
||||
`kernel_display_gamma_type = 2`" → **mine, and refuted from the source.** That
|
||||
cvar is the value a `kStub` getter (`VdGetCurrentDisplayGamma`) hands the
|
||||
**guest**; the game builds its own ramp from it and canary applies the guest's
|
||||
`DC_LUT` ramp in the swap path. No emulator-side gamma post-process exists to
|
||||
subtract. 🟡 Whether this game installs a ramp at all is still unestablished.
|
||||
[`ui-render-tone-curve.md`](structures/ui-render-tone-curve.md)
|
||||
* "the GPU trace produced nothing because either the CLI flag did not reach the
|
||||
cvar or `BeginTracing()` failed silently" → **both wrong.** The trace writer is
|
||||
**compiled out**: `trace_writer.h` gates it on `#ifdef NDEBUG`, so a release
|
||||
build has no writer at all. Confirmed with a control — the format string
|
||||
`_stream.xtr` appears **once** in the Debug binary and **zero** times in the
|
||||
Release binary `run-canary` actually uses.
|
||||
[`capture-harness-status.md`](capture-harness-status.md)
|
||||
* "the `T8aD` `+0x04` bit `0x02` means the sprite's name contains `eff`" →
|
||||
**refuted, now on evidence.** `ptlogo_back2eff` is an `eff` name with the bit
|
||||
clear; the attribution is confirmed by header-order pairing (18/18 on build 4)
|
||||
rather than by a size match, which cannot separate it from the same-sized
|
||||
`ptlogo_back2eff5`. All 10 bit-set sprites *are* `eff` names, so the
|
||||
implication runs one way only.
|
||||
* "the bit `0x02` marks a transient element" → **refuted.** `pteff03` and
|
||||
`pteff03a` carry the bit and run to `t=250`, ramping to `a=255` and holding.
|
||||
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
|
||||
* "bit `0x02` set ⇒ the sprite's name contains `eff`" (the one-way reading that
|
||||
survived the biconditional's refutation) → **mine, and refuted disc-wide the
|
||||
next iteration.** True 10/10 on `GP_TITLE` build 4; over 14 709 sprites it
|
||||
fails on **2 657 of 4 995** bit-set ones. `P(eff|set) = 0.468` against
|
||||
`P(eff|clear) = 0.144` — an association, not an implication.
|
||||
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
|
||||
* "`T8aD +0x04` bit `0x02` selects premultiplied alpha" → **refuted.**
|
||||
Premultiplied requires `RGB ≤ A` everywhere; over 170 decoded textures the
|
||||
flagged group violates it on a median **52.5 %** of pixels against **30.2 %**
|
||||
unflagged — both far from premultiplied, and the flagged group *further*.
|
||||
[`ui-paint-order-key.md`](structures/ui-paint-order-key.md)
|
||||
* "`build-reborn test` cannot terminate" → **mine, and too strong; corrected the
|
||||
next iteration.** It is heavy, not hung: 19 of 166 `.xpr` containers exceed
|
||||
25 s, `Stage_S02` completes in **144 s** with `rc = 0`, and one full pass is
|
||||
~45–60 minutes. The 3 h 26 m observed was that work at a load average of 9–14,
|
||||
inflated by my own two duplicate runs.
|
||||
[`test-suite-runtime.md`](test-suite-runtime.md)
|
||||
* "the case for the keyframe-time shift rests on a single element" → **no longer
|
||||
true.** Three elements across two screens discriminate and all favour it:
|
||||
`palogo_gamearts` and `palogo_seta` hold full alpha 83 frames, `palogo_sqex`
|
||||
≥77, against 6–8 predicted by the current reading and 80–102 by the shifted
|
||||
one. The `_eff` glows fit both and argue against neither.
|
||||
[`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md)
|
||||
* "an element with no held pose should be drawn as NOTHING rather than at a
|
||||
guessed endpoint" → **mine, and refuted.** Suppressing every plateau-less
|
||||
element and re-correlating against the live captures: title +0.9500 → +0.6839,
|
||||
main menu +0.9460 → +0.9037, `EXTRAS` +0.9440 → +0.9094. Worse on all three.
|
||||
* "`rest()` guesses for 24.57 % of elements (3 807 of 15 493)" → **mine, and
|
||||
overstated by 65 %.** The plateau test marks a **single-keyframe** element as
|
||||
plateau-less because it has no adjacent pair — but its one pose is
|
||||
unambiguously its rest. 1 502 of the 3 807 are those; the genuinely ambiguous
|
||||
population is **2 305 (14.88 %)**.
|
||||
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||
* "`rest()` for a plateau-less element should be the last keyframe → refuted by
|
||||
the sibling argument" → **that refutation is itself refuted, this time by
|
||||
measurement.** Rendering under the rule and correlating against the live
|
||||
captures: publisher splash +0.9600 → **+0.9982**, developer splash +0.9643 →
|
||||
**+0.9758**. Making `palogo_anima_eff` invisible *improves* the match; the
|
||||
sibling symmetry was my expectation, not evidence.
|
||||
* "the port's exposure to the rest-guessing defect is 14 elements" → **two.** The
|
||||
fallback needs an element to be plateau-less **and** multi-keyframe; title,
|
||||
main menu and `EXTRAS` reach it **zero** times, the two splashes once each.
|
||||
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||
* "the shifted time reading implies rest = the last keyframe, so the plateau rule
|
||||
can be dropped" → **mine, and refuted by measurement.** Applying it to every
|
||||
element collapses all five screens (title 0.9500→0.6819, main menu
|
||||
0.9460→0.6416, `EXTRAS` 0.9440→0.5745) and renders both splashes **blank**. A
|
||||
group is entry → hold → exit and the exit is the screen's *dismissal*: a
|
||||
displayed screen sits at the hold, not at its final pose.
|
||||
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||
* "`rest_plateau` renders elements the game has already finished with" (as a
|
||||
general claim) → **narrowed by its control.** It holds on **transient** screens
|
||||
only: suppressing the finished glows takes the two splashes from 0.9604/0.9659
|
||||
to **0.9982/0.9980**, while the same edit costs the title 0.002, the main menu
|
||||
**0.092** and `EXTRAS` **0.107**. A plateau mid-animation is evidence the
|
||||
element is held at that point in the timeline, not that it is on screen once the
|
||||
screen has settled — and where a screen does settle, the rule is right.
|
||||
[`ui-resting-pose.md`](structures/ui-resting-pose.md)
|
||||
* "the group header's undecoded lead-in word carries a per-element start offset"
|
||||
→ **refuted immediately.** It is `0x00000000` for all seven elements of the
|
||||
developer splash — glows and logos alike — while those two families are
|
||||
observed to run sequentially (frames 94–115 and 116–211) despite declaring
|
||||
overlapping times. [`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||
* "the splash elements share one clock origin" → **refuted.** Fitting a single
|
||||
origin needs f₀ ≈ 93.5 for `palogo_gamearts_eff` and f₀ ≈ 103 for
|
||||
`palogo_gamearts`, ~19 units apart, and aligning one throws the other off by
|
||||
~9 frames at both ends. Durations match (97.8 %, 98.5 %); starts do not.
|
||||
* "the glows and logos might overlap and my size-grouping merged them" → **tested
|
||||
and refuted.** Across all 235 captured frames, **zero** contain both a glow and
|
||||
a logo; f110–115 draw two glows and f116 onward two logos, with no transition
|
||||
frame. The sequencing is real.
|
||||
* "a bundle's declared elements are what the screen shows" → **refuted.** Entry 11
|
||||
declares three logo/glow pairs and only two are ever drawn — `palogo_anima`
|
||||
gets 0 frames against `palogo_gamearts`'s 95, from byte-identical keyframe
|
||||
times. (Reach: within the capture's frames 1–214.)
|
||||
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||
* "the glow and logo phases are one bundle with elements selectively activated"
|
||||
→ **mine, and withdrawn as unestablished.** The alternative — two compositions
|
||||
shown in sequence — fits equally. The texture-base test fails its control: the
|
||||
publisher splash is a different bundle and shares the base `0x11C30000`, so
|
||||
that address is a reused upload slot, not a bundle identity. What survives is
|
||||
that declared elements ≠ drawn elements.
|
||||
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||
* "two compositions shown in sequence" (as the alternative to selective
|
||||
activation) → **refuted statically.** It requires a bundle declaring the glows
|
||||
without the logos; no such bundle exists. Only four `GP_TITLE` entries carry
|
||||
`palogo` elements, and both developer entries declare **all six** logos and
|
||||
glows — so whichever was active, a subset of its elements was drawn at a time.
|
||||
Selective activation is reinstated on evidence.
|
||||
[`ui-group-start-time.md`](structures/ui-group-start-time.md)
|
||||
* ~~"the splash's `measured_paint_order` records a front-to-back depth order" →
|
||||
mis-typed; between its glow and logo halves it records only the temporal order
|
||||
they were seen in."~~ → **that refutation is itself refuted (same day).** The
|
||||
vector is a read of the runtime **child array** (`ui-screen-runtime.md`:
|
||||
"paint order (child slots)"), not of the draw capture, so co-occurrence does
|
||||
not bear on it; and the halves carry *distinct* T8aD layer keys
|
||||
(`0xa100` < `0xa110`, `paint_order_audit`: 0 same-key ties), so the file orders
|
||||
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)
|
||||
583
docs/re/boot-config-and-gamepart-registry.md
Normal file
@@ -0,0 +1,583 @@
|
||||
# What the game reads at boot — `config.ini`, and which GameParts actually exist
|
||||
|
||||
**Status:** ✅ `CONFIRMED` and **decoded** for the language selection. ❔ a bounded
|
||||
**negative** for the boot *order*. 🟡 for what the registration strings imply
|
||||
about the attract loop.
|
||||
|
||||
Contributes to [MISSION Q6](../port/MISSION.md), and firms up Q4's ids.
|
||||
|
||||
## ✅ `config.ini` — the disc's only config, and it selects the language
|
||||
|
||||
`config.ini` sits at the **disc root**, is 400 bytes, and is the **only** `.ini`,
|
||||
`.cfg` or `.txt` anywhere on the disc. Its own header comment (Shift-JIS) names
|
||||
it:
|
||||
|
||||
> `アプリケーション/ゲームパート初期設定テーブル`
|
||||
> — *"Application / GamePart initial settings table"*
|
||||
>
|
||||
> `SYSTEM セクションには、ゲーム及び各ゲームパートで共通にアクセスする情報を記述する`
|
||||
> — *"the SYSTEM section describes information accessed in common by the game and
|
||||
> each game part"*
|
||||
|
||||
```ini
|
||||
[SYSTEM]
|
||||
|
||||
[LANGUAGE]
|
||||
= eng ; default
|
||||
#0x01 = eng ; XC_LANGUAGE_ENGLISH
|
||||
#0x02 = jpn ; XC_LANGUAGE_JAPANESE
|
||||
#0x03 = deu ; XC_LANGUAGE_GERMAN
|
||||
#0x04 = fra ; XC_LANGUAGE_FRENCH
|
||||
#0x05 = esp ; XC_LANGUAGE_SPANISH
|
||||
#0x06 = ita ; XC_LANGUAGE_ITALIAN
|
||||
```
|
||||
|
||||
**This is the mechanism behind the EN/JP screen pairs.** The console's
|
||||
`XC_LANGUAGE_*` setting selects a three-letter code, and that code is what picks
|
||||
`GP_TITLE`'s English or Japanese build ([`ui-title-build-map.md`](ui-title-build-map.md))
|
||||
and the `<lang>.pak` families. Default is `eng`, keyed by the empty line — an
|
||||
unlisted language falls back to English. ✅ decoded. The executable does read the
|
||||
file: the string `config.ini` is at `.rdata 0x82062b44`.
|
||||
|
||||
## ❔ But the boot ORDER is not in it — and this is the whole search space
|
||||
|
||||
`[SYSTEM]` — the section the file's own comment says holds what the game and every
|
||||
game part share — is **empty**. So the file the game itself calls the *GamePart
|
||||
initial settings table* says nothing about which part runs first or what follows
|
||||
what.
|
||||
|
||||
**Reach of the negative:** this is the only config file on the disc (one `find`
|
||||
over the whole extract). The boot order is therefore not in disc-side
|
||||
configuration at all; it is in code, or in a table inside the executable that has
|
||||
not been located.
|
||||
|
||||
## 🟡 `GP_ADVERTISE_DEMO` is never registered — the attract loop is not its own part
|
||||
|
||||
The executable carries one diagnostic string per GamePart registration site
|
||||
(`silph::GamePartTask::RegisterToFactory<N, class silph::GamePart_X>::RegisterToFactory is failed!`)
|
||||
— the same evidence the corpus used to pin the id table
|
||||
([`challenge-mission-gate.md`](challenge-mission-gate.md) §3). Extracting all of
|
||||
them gives **24 of the 29 ids bound to a C++ class**; full list in
|
||||
[`data/gamepart-class-ids.txt`](data/gamepart-class-ids.txt).
|
||||
|
||||
**The five ids with no registration site are `1`, `2`, `16`, `18`, `28`** — in the
|
||||
id table's naming, `GP_ADVERTISE_DEMO`, `GP_SELECT_STORAGE`, `GP_DEMO`,
|
||||
`GP_SELECTOR`, `GP_TEST`.
|
||||
|
||||
That `GP_ADVERTISE_DEMO` (1) is among them matters for Q6, and it agrees with
|
||||
what was measured: the attract loop is **the title screen replaying `ADV.wmv`**
|
||||
([`movie-binding.md`](movie-binding.md)), not a transition into a separate
|
||||
advertise part. The id table names a part the shipped build never registers.
|
||||
|
||||
🟡 **not ✅**, because this is an argument from a *diagnostic string*: no error
|
||||
message for id 1 implies no registration site for id 1. That is how the corpus
|
||||
already reads these strings, and it is sound, but it is not the code.
|
||||
|
||||
### Two bonuses for Q4
|
||||
|
||||
* **`3` and `4` are the same class** — `GamePart_SaveLoad` is registered twice.
|
||||
The id table's separate `GP_LOAD` / `GP_SAVE` names are two ids on one part.
|
||||
* The ids behind the menu buttons now match the executable's own **class** names,
|
||||
not just a list of table names: `GamePart_Options` = 8, `GamePart_Tutorial` =
|
||||
25, `GamePart_Extras` = 5, `GamePart_MissionSelect` = 7,
|
||||
`GamePart_MovieTheater` = 6, `GamePart_Title` = 0. Still a **name match** —
|
||||
screen title ↔ class name — but anchored one level closer to the code.
|
||||
|
||||
## What is still missing for Q6
|
||||
|
||||
The *transitions*. Nothing found so far says "title, then movie, then title" — the
|
||||
manifest gives the boot-side **assets** in play order
|
||||
([`movie-binding.md`](movie-binding.md)), `config.ini` gives the **language**, and
|
||||
the registry gives **which parts exist**. What decides to advance is in
|
||||
`GamePart_Title`'s own code, and reading it is a static PPC job that has not been
|
||||
started.
|
||||
|
||||
## ❔ The transitions are code, not data — the reach of that negative
|
||||
|
||||
Q6's remaining half asked what the game *reads* to decide the boot order. The
|
||||
answer is: **nothing. It is not data-driven.** Four independent places were
|
||||
checked, and the sequence is in none of them.
|
||||
|
||||
| looked in | result |
|
||||
|---|---|
|
||||
| **disc configuration** | `config.ini` is the only config file on the disc, its `[SYSTEM]` section is empty (above) |
|
||||
| **the movie manifest** | carries the boot-side *assets* in play order, and no transitions — [`movie-binding.md`](movie-binding.md) |
|
||||
| **a persistent part-id field** | already refuted: the requested GamePart id exists **only as a stack argument in flight**, with no literal store anywhere — [`challenge-mission-gate.md`](challenge-mission-gate.md) §5 |
|
||||
| **the id table's attract entry** | the string `GP_ADVERTISE_DEMO` at `0x820a1fe8` has **zero xrefs of any kind**; nothing in the code reads it |
|
||||
|
||||
So a transition is a **call with an id argument**, chosen by code. There is no
|
||||
table to read and nothing to poke.
|
||||
|
||||
**Where that code is, as far as it was traced.** The `RegisterToFactory<0, class
|
||||
silph::GamePart_Title>` diagnostic string at `0x820a3d60` is referenced from
|
||||
exactly one place, `sub_8280E148` — the registration site — which also takes the
|
||||
address of **`sub_821C7D98`** (`addi`), the position a factory template puts its
|
||||
creator. 🟡 That identification is by position and convention, not proven;
|
||||
`sub_821C7D98` itself has **0 `.rdata` references**, consistent with a small
|
||||
`new`+ctor thunk rather than the state machine. The substantial function in the
|
||||
same class neighbourhood is `sub_821C6458` (4 460 bytes, has EH, 15 `.rdata`
|
||||
refs), and **reading it has not been attempted**.
|
||||
|
||||
### What this means for the port
|
||||
|
||||
The boot sequence is **authored**, not transcribed — and that is fine, because the
|
||||
sequence itself is measured end to end:
|
||||
|
||||
```
|
||||
developer splash (a RATC screen, not a video)
|
||||
→ ADV.wmv
|
||||
→ title + PRESS Ⓐ ──idle ~8–10 s──> fade to black → ADV.wmv in full → title
|
||||
→ Ⓐ → main menu
|
||||
```
|
||||
|
||||
with the fade-through-black timings in [`screen-transitions.md`](screen-transitions.md)
|
||||
and Ⓑ from the main menu returning to the title.
|
||||
|
||||
|
||||
## 🟡 `sub_821C6458` read — the title's states are NAMED in the executable
|
||||
|
||||
The page above left this function as the named next step: "the substantial
|
||||
function in `GamePart_Title`'s neighbourhood, and **reading it has not been
|
||||
attempted**". It has now been looked at — not disassembled line by line, but
|
||||
characterised, which is enough to sharpen Q6.
|
||||
|
||||
**It is the title part's screen-state function.** 4 460 bytes, has EH, and called
|
||||
from **exactly one** place — `sub_821C7850`, which sits in the same neighbourhood
|
||||
as the creator the registration site points at (`sub_821C7D98`). It makes 33
|
||||
distinct calls.
|
||||
|
||||
**What it names.** Its `.rdata` string references are:
|
||||
|
||||
| string | at |
|
||||
|---|---|
|
||||
| **`TITLE_SCREEN`** | `0x820a3d3c` |
|
||||
| **`TITLE_MENU`** | `0x820a3d30` |
|
||||
| **`LOADING`** | `0x820a214c` |
|
||||
| `BASE_INFO` (×2) | `0x820a20ec` |
|
||||
|
||||
Those are the states measured off the running game, in the game's own words: the
|
||||
title carrying `PRESS Ⓐ BUTTON`, the five-button menu, and a loading state. And
|
||||
`BASE_INFO` is this corpus's own marker for a **screen-config lookup** rather than
|
||||
a table read ([`REFUTED.md`](REFUTED.md), "`BASE_INFO` discriminates
|
||||
screen-config from table-read, 9/9 vs 10/10").
|
||||
|
||||
### The sharper negative
|
||||
|
||||
So the title part **does ask for configuration keyed by `TITLE_SCREEN` and
|
||||
`TITLE_MENU`** — and [`config.ini`](#-configini--the-discs-only-config-and-it-selects-the-language),
|
||||
the disc's only config file, contains **no such sections**. Its only sections are
|
||||
an empty `[SYSTEM]` and `[LANGUAGE]`. The lookups find nothing, and the values are
|
||||
whatever the code defaults to.
|
||||
|
||||
That is a better answer than "the order is not in config": the game *asks the
|
||||
question*, the shipped disc *does not answer it*, and the defaults live in code.
|
||||
For the port it means the state **names** are transcribable even though their
|
||||
contents are not.
|
||||
|
||||
🟡 **Not ✅.** This is a characterisation from string references and call counts,
|
||||
not a read of the control flow. Two self-references (`0x821c6498`, `0x821c6b7c`)
|
||||
inside the function look like jump tables — a switch, which is what a state
|
||||
machine compiles to — but that was **not confirmed**, and nothing here shows which
|
||||
state leads to which.
|
||||
|
||||
### 🔴 Refuted on the way, because it looked like a find
|
||||
|
||||
The words at `0x820a3b48`… resolve as neat `{func, func, ptr}` triples and read
|
||||
convincingly as a state/handler table. **They are not.** The bytes immediately
|
||||
before them are the tail of
|
||||
`…SaveLoad>::RegisterToFactory is failed!`, and the `0x8210c1xx` targets are
|
||||
zero-filled descriptors — static-initialiser records trailing the registration
|
||||
strings, not a dispatch table. A plausible-looking array of function pointers next
|
||||
to relevant strings is not evidence of anything until its neighbours are read.
|
||||
|
||||
|
||||
## 🟡 The title's transition is a screen lookup BY NAME — `sub_821CC860`
|
||||
|
||||
Reading the code around each of the three state names shows the **same four
|
||||
instructions** at all three sites:
|
||||
|
||||
```
|
||||
lwz r29, 20(r30) ; an object off the part
|
||||
lwz r3, 4(...) ; ...
|
||||
bl 0x822F2328
|
||||
lis r11, 0x820A
|
||||
li r6, 0
|
||||
lwz r4, 24(r30)
|
||||
addi r5, r11, 15676 ; "TITLE_SCREEN" (15664 = "TITLE_MENU",
|
||||
lwz r3, 88(r30) ; 8524 = "LOADING")
|
||||
bl 0x821CC860 ; <- lookup(this+88, this+24, NAME, 0)
|
||||
mr r4, r3
|
||||
bl 0x82187B78 ; <- install the result
|
||||
```
|
||||
|
||||
So a title-side transition is **`sub_821CC860(…, "<NAME>", 0)` followed by
|
||||
`sub_82187B78(result)`** — a *string-keyed* screen lookup, then an install. Not a
|
||||
numeric id, not a table index. That also fits `GP_ADVERTISE_DEMO` having zero
|
||||
xrefs: at this level the screen graph is keyed by **name**, not by GamePart id.
|
||||
|
||||
### The candidate name vocabulary
|
||||
|
||||
`sub_821CC860` is called from **28 distinct functions**. Collecting the
|
||||
upper-case identifier strings those callers reference gives 35 names, and they
|
||||
split into two obvious families:
|
||||
|
||||
* **screen/state names** — `TITLE_SCREEN`, `TITLE_MENU`, `LOADING`,
|
||||
**`DIFFICULTY`**, `EXTRA_MENU`, `TUTORIAL_MENU`, `STANDARD_MENU`, `DEBRIEFING`,
|
||||
`CHALLENGE`, `MISSIONS`, `LIVE_BOARD`, `LOCAL_BOARD`, `EXTRA_MENU`;
|
||||
* **config keys** — `TEXT_FONT`, `TEXT_HEIGHT`, `LINE_SPACE`, `GAMMA_RGB`,
|
||||
`GAMMA_WB`, `TEXT_SPEED_PER_LETTER`, `EXIT_VALUE`, `INPUT_DIR`,
|
||||
`MENU_ENABLE_SKIP`/`_DISABLE_SKIP`, `PAUSE_SE`, `JINGLE`, …
|
||||
|
||||
plus `BASE_INFO` in **19 of the 28** callers, which is this corpus's existing
|
||||
marker for a screen-config function.
|
||||
|
||||
**Three of the screen names are independently corroborated by measurement**:
|
||||
`DIFFICULTY` is exactly the screen `NEW GAME` opens, `EXTRA_MENU` matches the
|
||||
`EXTRAS` submenu and `TUTORIAL_MENU` the lesson list — all three measured off the
|
||||
running game in [`menu-navigation-semantics.md`](menu-navigation-semantics.md)
|
||||
before this function was ever looked at.
|
||||
|
||||
⚠️ **Why this is 🟡 and not ✅.** The 35 strings are what those callers
|
||||
*reference*, **not** proven arguments to `sub_821CC860` — the list plainly mixes
|
||||
screen names with config keys, so it is a **candidate vocabulary**, not a decoded
|
||||
one. Confirming it means checking, per call site, which string actually lands in
|
||||
`r5`. That was not done.
|
||||
|
||||
### What is still unread
|
||||
|
||||
Which state leads to which. The three lookups sit in different branches of one
|
||||
function and at least one (`TITLE_MENU`) is guarded by a `cmplwi`/`bne`, but the
|
||||
branch structure was not traced, so the *order* still comes from measurement, not
|
||||
from the code.
|
||||
|
||||
|
||||
## ✅ The argument is now decoded — and it refutes my own corroboration
|
||||
|
||||
The section above listed a **candidate** vocabulary from "upper-case strings the
|
||||
callers reference", flagged 🟡 because those are not proven arguments, and named
|
||||
the check: *confirm per call site which string actually lands in `r5`.* That check
|
||||
has been run, and it fired.
|
||||
|
||||
**Method.** Forward register simulation over the 100 instructions before each of
|
||||
the **48** call sites of `sub_821CC860`, tracking `lis` / `addi` / `subi` / `mr`.
|
||||
Full table in [`data/name-lookup-callsites.txt`](data/name-lookup-callsites.txt).
|
||||
|
||||
**Gated on a control, and the first version failed it.** A backward scan for
|
||||
`addi r5, …` recovered only 7/48 and **missed both title sites I had read by
|
||||
eye** — because there the name goes into `r27` first and reaches `r5` via a later
|
||||
`mr`. The rewritten forward simulation reproduces all three known sites
|
||||
(`TITLE_SCREEN`, `TITLE_MENU`, `LOADING`) and then recovers **46 of 48**.
|
||||
|
||||
### 🔴 `DIFFICULTY` and `EXTRA_MENU` are *not* arguments
|
||||
|
||||
Last section I wrote that three names were "independently corroborated by
|
||||
measurement": `DIFFICULTY`, `EXTRA_MENU`, `TUTORIAL_MENU`. **Only
|
||||
`TUTORIAL_MENU` survives.** `DIFFICULTY` and `EXTRA_MENU` never appear in `r5` at
|
||||
any of the 48 sites — they are strings that merely live in the same functions.
|
||||
The corroboration I claimed was an artifact of the loose filter, and the 🟡 I put
|
||||
on it is exactly what caught it.
|
||||
|
||||
### The 28 names that ARE passed
|
||||
|
||||
```
|
||||
BASE BASE(x3) BG(x3) BLACK(x2) BUTTON DEBRIEFING DETAIL EXTRA FADE(x3)
|
||||
FILE(x5) GAME GAMMA_RGB GAMMA_TITLE INFO KEY LIVE_BOARD(x2) LOADING(x2)
|
||||
LOCAL_BOARD(x2) MENU(x2) MENU_DISABLE_SKIP MESSAGE NEW_ITEM(x2) PAD SOUND
|
||||
TITLE(x2) TITLE_MENU TITLE_SCREEN(x2) TUTORIAL_MENU WINDOW
|
||||
```
|
||||
|
||||
### 🔴 So `sub_821CC860` is not a screen factory
|
||||
|
||||
The previous section called it one. The real argument list says otherwise:
|
||||
`BG`, `BLACK`, `FADE`, `FILE`, `KEY`, `PAD`, `SOUND`, `WINDOW`, `GAMMA_RGB`,
|
||||
`MENU_DISABLE_SKIP` are **not screens**. It is a **generic name-keyed lookup** —
|
||||
a named-entry getter used for config and resources throughout the executable, of
|
||||
which the title part happens to call it with its three state names.
|
||||
|
||||
That weakens nothing about the title finding itself — `sub_821C6458` really does
|
||||
transition by calling this with `TITLE_SCREEN` / `TITLE_MENU` / `LOADING` — but it
|
||||
removes the inference that the 28 names are a screen vocabulary. They are a
|
||||
**lookup-key vocabulary**, mostly config.
|
||||
|
||||
|
||||
## ✅ The title's state machine — decoded
|
||||
|
||||
The last open piece of Q6 was *which state leads to which*. It is a plain
|
||||
`switch` and it is now read.
|
||||
|
||||
```
|
||||
821c6474 lwz r11, 136(r30) ; state = this+0x88
|
||||
821c6478 cmplwi cr6, r11, 0x9 ; ten states, 0..9
|
||||
821c647c bgt cr6, <default>
|
||||
821c6480 lis/addi r12, 0x821C6498 ; jump table
|
||||
821c648c lwzx r0, r12, r0
|
||||
821c6494 bctr
|
||||
```
|
||||
|
||||
⚠️ The table at `0x821C6498` disassembles as ten `lwz r16, N(r28)` instructions.
|
||||
**It is data.** That is the "self-reference" this page flagged two sections ago as
|
||||
*looking* like a jump table — it is one, and the disassembler was decoding its
|
||||
words as code.
|
||||
|
||||
**Three of the ten states install a named screen**: **0 → `TITLE_SCREEN`**,
|
||||
**2 → `TITLE_MENU`**, **8 → `LOADING`**.
|
||||
|
||||
**Eighteen transitions**, every one a literal `li rX, N ; stw rX, 136(r30)`:
|
||||
|
||||
```
|
||||
0 → 1, 2 2 → 4 4 → 0, 5, 8×4 6 → 7, 9, 2 8 → 2
|
||||
1 → 2, 2 3 → 4 5 → 6 7 → 9 9 → (none)
|
||||
```
|
||||
|
||||
Full table with addresses in
|
||||
[`data/title-state-machine.txt`](data/title-state-machine.txt).
|
||||
|
||||
### 🟡 It lines up with what was measured — read as corroboration, not proof
|
||||
|
||||
The boot reaches the title (state 0) and Ⓐ opens the main menu (state 2); the
|
||||
graph has `0 → 2` directly and `0 → 1 → 2`. Ⓑ at the main menu returns to the
|
||||
title, and `4 → 0` is the **only** edge back to state 0, reached from `2 → 4`.
|
||||
Entering a submenu goes through `LOADING` and comes back, and `4 → 8` (four
|
||||
separate sites) then `8 → 2` is exactly that shape.
|
||||
|
||||
Those readings are **mine, matching a graph to observed behaviour** — the
|
||||
conditions on the edges are not decoded, so nothing here proves which input picks
|
||||
which branch.
|
||||
|
||||
### What is still not decoded
|
||||
|
||||
* **the condition on each edge** — which input or event selects it;
|
||||
* **states 1, 3, 5, 6, 7, 9** install no named screen, so what they do is unknown;
|
||||
* **state 3 is never a destination** in this function, so something outside sets
|
||||
it.
|
||||
|
||||
|
||||
## ✅ The edge conditions, for the state that has them — an EVENT CODE
|
||||
|
||||
The previous section left "the condition on each edge" open. For the state that
|
||||
matters it is now read.
|
||||
|
||||
**`sub_821C6458`'s third argument is an event code.** State 4 — reached from
|
||||
`2 → 4`, i.e. immediately after the main menu is installed — is the
|
||||
**input-waiting state**, and it dispatches on that argument:
|
||||
|
||||
```
|
||||
821c6b5c cmplwi cr6, r27, 0x19 ; 26 events, 0..25
|
||||
821c6b60 bgt cr6, <default>
|
||||
821c6b68 addi r12, r12, 27516 ; second jump table, at 0x821c6b7c
|
||||
821c6b78 bctr
|
||||
```
|
||||
|
||||
**Six of the 26 are handled**; the other twenty fall through and change nothing.
|
||||
|
||||
| event | → state | |
|
||||
|---|---|---|
|
||||
| **0** | **0** | `TITLE_SCREEN` — back to the title |
|
||||
| 3 | 8 | `LOADING` |
|
||||
| 5 | 8 | `LOADING` |
|
||||
| 8 | 8 | `LOADING` |
|
||||
| 10 | 5 | — |
|
||||
| 25 | 8 | `LOADING` |
|
||||
|
||||
So last section's edges `4 → 0, 5, 8×4` are each now attributed to a specific
|
||||
event, and the shape of the state graph is complete for the input state.
|
||||
|
||||
### 🟡 A correspondence worth noticing, and not more than that
|
||||
|
||||
The main menu has five items, Ⓑ returns to the title, and this table has **one
|
||||
event to state 0, four to `LOADING`, one elsewhere**. It is tempting to read that
|
||||
as *Ⓑ = event 0, four items load an external archive, `EXTRAS` stays inside
|
||||
`GP_TITLE`* — which would match everything measured.
|
||||
|
||||
**That is a count-match, not a decode.** The event numbers are not named, nothing
|
||||
here shows event 3 is a particular menu row, and state 5 installs no named screen,
|
||||
so the `EXTRAS` half of the story has no support at all. Recorded as an
|
||||
observation so the next person sees the shape; it is **not** a button→event map.
|
||||
|
||||
### What is still not decoded
|
||||
|
||||
* **what the event numbers mean** — button id, menu-item index, or message id;
|
||||
* **conditions on edges out of the other states** (`0 → 1` vs `0 → 2`, `6 → 7/9/2`);
|
||||
* states 1, 3, 5, 6, 7, 9, which install no named screen.
|
||||
|
||||
|
||||
## 🔴 Correcting my own framing: `sub_821C6458` is ONE PHASE, not the whole part
|
||||
|
||||
Chasing where the event code comes from turned up the level above, and it revises
|
||||
what the previous three sections called "the title part's state machine".
|
||||
|
||||
`sub_821C6458` has exactly one caller, `sub_821C7850`, and that caller is itself a
|
||||
dispatcher — on a **second, outer state field**:
|
||||
|
||||
```
|
||||
821c786c lwz r11, 16(r30) ; only runs at all when this+16 == 3
|
||||
821c7874 bne cr6, <exit>
|
||||
821c787c lwz r11, 132(r30) ; phase = this+0x84
|
||||
821c7880 cmplwi cr6, r11, 0x4 ; FIVE phases, 0..4
|
||||
821c789c bctr ; table at 0x821C78A0
|
||||
```
|
||||
|
||||
| phase | handler | |
|
||||
|---|---|---|
|
||||
| 0 | `sub_821C5690` | **the splash** — independently identified as the splash mechanics in the `iterate3E` notes, before any of this |
|
||||
| 1 | inline at `0x821c790c` | |
|
||||
| 2 | `sub_821C5818` | |
|
||||
| 3 | `sub_821C5EC0` | |
|
||||
| **4** | **`sub_821C6458`** | the title/menu machine — `TITLE_SCREEN` / `TITLE_MENU` / `LOADING` |
|
||||
|
||||
So `GamePart_Title` has **two nested state fields**: a phase at `this+132` (five
|
||||
values) selecting which sub-machine runs, and the state at `this+136` (ten values)
|
||||
*inside phase 4*. Everything the previous sections decoded — the ten states, the
|
||||
eighteen edges, the event dispatch — is **phase 4 only**. Phases 0–3 are
|
||||
untouched, and one of them is the developer splash.
|
||||
|
||||
That phase 0 lands on the function the corpus had already fingered as the splash,
|
||||
from a completely different direction, is the useful check here.
|
||||
|
||||
### And the event code is forwarded, not created
|
||||
|
||||
All five phase handlers are called as `(this, r29, r28)` with the *same*
|
||||
arguments, and `r28` is `sub_821C7850`'s own third argument, passed through
|
||||
untouched. So the event vocabulary is defined **at least one level above** this
|
||||
function. Finding what `3`, `5`, `8`, `10`, `25` mean means going up again, and
|
||||
that was not done.
|
||||
|
||||
|
||||
## Going up one more level — three of four callers pass a constant event
|
||||
|
||||
`sub_821C7850` has **four** direct callers. Recovering the `r5` argument at each:
|
||||
|
||||
| caller | event passed |
|
||||
|---|---|
|
||||
| `sub_821C7CB8` | **0** (→ back to the title) |
|
||||
| `sub_821C7BA0` | **0** |
|
||||
| `sub_821C47A0` | **5** (→ `LOADING`) |
|
||||
| `sub_821C5580` | **not constant** — `lwz r5, 4(r27)`, read out of a structure |
|
||||
|
||||
So the vocabulary is still not enumerable from here: the interesting caller reads
|
||||
its event from a field. ❔ What `3`, `8`, `10` and `25` mean remains open.
|
||||
|
||||
**`sub_821C5580` is also where the outer gate is set:**
|
||||
|
||||
```
|
||||
821c5640 li r11, 3
|
||||
821c5644 stw r11, 16(r28) ; this+16 = 3 — the exact value sub_821C7850 tests
|
||||
821c5650 lwz r5, 4(r27) ; event from a struct field
|
||||
821c5658 bl 0x821C7850
|
||||
```
|
||||
|
||||
That answers the "what does `this+16 == 3` gate on" question from the previous
|
||||
section: this function arms it.
|
||||
|
||||
### 🎁 And it names the title's music
|
||||
|
||||
The same function, a few instructions earlier:
|
||||
|
||||
```
|
||||
821c560c li r5, 1103
|
||||
821c5610 li r4, 4
|
||||
821c561c lwz r3, 0(r29)
|
||||
821c5620 bl 0x8217ACF8 ; a sound-play call
|
||||
```
|
||||
|
||||
**1103 is a BGM cue id** — `BGM_103`. That closes an open residual from Q8/Q10
|
||||
("which bank is the menu's music is not on the disc"), and it checks out three
|
||||
ways; see [`structures/bgm-two-stems.md`](structures/bgm-two-stems.md).
|
||||
|
||||
|
||||
## The other phases, characterised — and phase 0 confirms the splash twice over
|
||||
|
||||
Same treatment for the phase handlers that were still unread. Strings each one
|
||||
references, and whether it carries a jump table of its own:
|
||||
|
||||
| phase | handler | size | strings referenced | own switch |
|
||||
|---|---|---|---|---|
|
||||
| 0 | `sub_821C5690` | 380 | **`LOGO`** | none |
|
||||
| 2 | `sub_821C5818` | 1 576 | `BASE_INFO`, **`BUTTON`**, **`TITLE_SCREEN`** | none |
|
||||
| 3 | `sub_821C5EC0` | 1 220 | *(none)* | one, `bctr` at `0x821c5ef8` |
|
||||
| 4 | `sub_821C6458` | 4 460 | `BASE_INFO`, `LOADING`, `TITLE_MENU`, `TITLE_SCREEN` | two (known) |
|
||||
|
||||
**Phase 0 references `LOGO`** — a second, independent confirmation that it is the
|
||||
developer splash. The `iterate3E` notes reached `sub_821C5690` from the guest side
|
||||
and named the splash's `LOGO` items; this reaches the same function from the
|
||||
registration site and finds the same string.
|
||||
|
||||
**Phase 2 draws the title *with* the `PRESS Ⓐ BUTTON` plate** — it references
|
||||
`TITLE_SCREEN` and `BUTTON`, and [`ui-title-build-map.md`](ui-title-build-map.md)
|
||||
established from the archive that the plate is its own build (2/3), composited
|
||||
over the title art.
|
||||
|
||||
### 🟡 A hypothesis for a puzzle this corpus has had open for months
|
||||
|
||||
**The title is installed from two different places**: phase 2, and phase 4's
|
||||
state 0. Same screen, different code.
|
||||
|
||||
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) has long
|
||||
recorded, and never explained, that *the title which ends the boot accepts Ⓐ while
|
||||
the title the attract loop returns to accepts nothing* — with the giveaway that a
|
||||
draw capture in each state is identical, 13 quads at the same rects. Two code
|
||||
paths installing the same screen is exactly the shape that would produce that.
|
||||
|
||||
**Candidate:** the boot title is **phase 2**; the attract-returned title is
|
||||
**phase 4, state 0**. Only phase 2 wires up the Ⓐ handling.
|
||||
|
||||
⚠️ **Tested, and dead.** Ⓑ from the main menu is the decoded edge `4 → 0`, so the
|
||||
Ⓑ-returned title *is* phase 4 state 0 — and **Ⓐ works there**, opening the main
|
||||
menu, with Ⓐ on the boot title as the control in the same run. Phase 4 state 0
|
||||
handles Ⓐ, so the two-code-paths explanation is refuted. See
|
||||
[`canary-scripted-input-traps.md`](canary-scripted-input-traps.md), which the
|
||||
test did narrow usefully: only the **attract**-returned title is inert, not every
|
||||
title after the first.
|
||||
|
||||
### ⚠️ One query in this section failed its own control
|
||||
|
||||
Counting `stw rX, 136(r30)` per phase returned **0 for phase 4**, which is known
|
||||
to have 18 — the operand text is `r11, 136(r30)` and the pattern did not allow the
|
||||
space. The per-phase store counts were discarded. The `bctr` half *does* pass its
|
||||
control (phase 4 shows both known tables) and is reported above.
|
||||
|
||||
|
||||
## ✅ The phase transitions — and they invalidate yesterday's test
|
||||
|
||||
Nine stores to the phase field `this+132` across the `GamePart_Title`
|
||||
neighbourhood, attributed to the handler each lives in (control: the same query
|
||||
shape finds 30 stores to `this+136`, ≥ the 18 known in phase 4):
|
||||
|
||||
| store | sets phase | lives in |
|
||||
|---|---|---|
|
||||
| `0x821c4fbc` | 2 | outside the handlers (entry/setup) |
|
||||
| `0x821c4fe0`, `0x821c4fec` | 4 | outside the handlers |
|
||||
| `0x821c5010` | *(not an immediate)* | outside the handlers |
|
||||
| `0x821c592c` | **0** | phase 2 |
|
||||
| `0x821c5b00` | **3** | phase 2 |
|
||||
| `0x821c6370` | **4** | phase 3 |
|
||||
| **`0x821c6e04`** | **2** | **phase 4, in the event-0 block** |
|
||||
| `0x821c7950` | 2 | the dispatcher |
|
||||
|
||||
```
|
||||
entry → 2 2 → 0 (splash) 2 → 3 3 → 4 4 → 2 (event 0)
|
||||
```
|
||||
|
||||
### 🔴 Withdrawing the refutation from the previous iteration
|
||||
|
||||
The event-0 block sets **both** fields, one instruction apart:
|
||||
|
||||
```
|
||||
821c6df8 li r11, 2
|
||||
821c6e00 stw r28, 136(r30) ; state = 0
|
||||
821c6e04 stw r11, 132(r30) ; phase = 2
|
||||
```
|
||||
|
||||
So **Ⓑ from the menu lands in phase 2**, not phase 4 state 0. The previous section
|
||||
tested "Ⓐ on the Ⓑ-returned title" believing it was probing phase 4 state 0 —
|
||||
**it was probing phase 2**, the same phase as the boot title.
|
||||
|
||||
The test therefore says nothing about phase 4 state 0, and the refutation it
|
||||
produced is **withdrawn**. Worse for me and better for the idea: Ⓐ working there
|
||||
is exactly what the hypothesis predicts, since phase 2 is the phase that
|
||||
references `BUTTON`.
|
||||
|
||||
**Status of the hypothesis: untested, and now consistent with two observations**
|
||||
(Ⓐ works on the boot title and on the Ⓑ-returned title, both phase 2). What it
|
||||
still needs is the **attract**-returned title's phase — the one thing no test so
|
||||
far has read.
|
||||
@@ -643,3 +643,58 @@ Two lessons, both cheap:
|
||||
The evidence recovered cleanly once the interference stopped — the interrupted
|
||||
batch's own run 3 had already reached the menu with the same 40 calls / 6
|
||||
`ResolvePath` signature.
|
||||
|
||||
|
||||
## 2026-08-28 — a candidate explanation, from the static side
|
||||
|
||||
This page's oldest open question is **why the attract-returned title accepts no
|
||||
input while the boot title does**, given that a draw capture in each state is
|
||||
identical (13 quads, same rects).
|
||||
|
||||
`GamePart_Title` turns out to dispatch on an outer **phase** field at `this+132`,
|
||||
and **the title screen is installed from two different phases** — phase 2
|
||||
(`sub_821C5818`, which references `TITLE_SCREEN` *and* `BUTTON`) and phase 4's
|
||||
state 0 (`sub_821C6458`). Same screen, different code. See
|
||||
[`boot-config-and-gamepart-registry.md`](boot-config-and-gamepart-registry.md).
|
||||
|
||||
**Candidate:** the boot title is phase 2; the attract-returned title is phase 4
|
||||
state 0; only one of them wires up Ⓐ.
|
||||
|
||||
### ⚠️ Tested the same day, and the TEST was invalid — see the withdrawal below
|
||||
|
||||
There was a cheaper test than reading `this+132`: the state graph says Ⓑ from the
|
||||
main menu goes `4 → 0`, so **the Ⓑ-returned title *is* phase 4 state 0**. If phase
|
||||
4 state 0 were the inert one, Ⓐ would not work there.
|
||||
|
||||
**It works.** Measured:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| *control* — Ⓐ on the boot title | → main menu ✅ |
|
||||
| Ⓐ on the **Ⓑ-returned** title | → **main menu** ✅ |
|
||||
|
||||
**🔴 Withdrawn.** Reading the event-0 block in full shows it sets **two** fields,
|
||||
one instruction apart: `stw r28, 136(r30)` (state = 0) *and* `stw r11, 132(r30)`
|
||||
with `r11 = 2` — **phase = 2**. So Ⓑ lands in **phase 2**, the same phase as the
|
||||
boot title, and this test never probed phase 4 state 0 at all.
|
||||
|
||||
The refutation is withdrawn. Ⓐ working on the Ⓑ-returned title is exactly what
|
||||
the hypothesis predicts — phase 2 is the phase that references `BUTTON`.
|
||||
|
||||
### ✅ What the test DID establish — the puzzle is narrower than this page says
|
||||
|
||||
(This part stands: it is a measurement, independent of which phase the returned
|
||||
title turns out to be.)
|
||||
|
||||
This page frames the problem as *the boot title accepts Ⓐ, a later title does
|
||||
not*. That is too broad. **The Ⓑ-returned title accepts Ⓐ perfectly well** — a
|
||||
case nobody had tried. Only the **attract-returned** title is inert.
|
||||
|
||||
So whatever makes the title stop responding is tied to the attract cycle
|
||||
specifically — the movie playing and coming back — not to "the title has been
|
||||
shown before". That is a much smaller haystack than the one this page has been
|
||||
pointing at.
|
||||
|
||||
⚠️ Also reproduced in passing: 4 s after Ⓑ the plate is **absent** (169 plate
|
||||
pixels), and it fades in after. A test that samples too early will read the
|
||||
returned title as plate-less, which this corpus has already been caught by once.
|
||||
|
||||
364
docs/re/capture-harness-status.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# 🔴 Why the boot harness stopped reaching the title — `screenshot` costs 10.8 s
|
||||
|
||||
**Status:** ✅ **diagnosed, with a control.** Four consecutive runs on
|
||||
2026-08-28/29 failed to reach the interactive title, across two locales, two
|
||||
launch paths and two display-gamma settings. The cause is none of those.
|
||||
|
||||
## The measurement
|
||||
|
||||
| condition | one `screenshot` call |
|
||||
|---|---|
|
||||
| while Xenia Canary is running | **10.8 s** |
|
||||
| immediately after killing it | **0.117 s** |
|
||||
|
||||
**92×.** The 1-minute load average at the slow measurement was 1.80, so this is
|
||||
contention with the emulator (both go through the same X server), not general
|
||||
system load.
|
||||
|
||||
## Why that breaks the harness
|
||||
|
||||
`skip_intro.sh` takes **two** grabs 0.6 s apart per iteration, plus an
|
||||
`is_title.py` numpy load. Its actual sample timestamps in the last run:
|
||||
|
||||
```
|
||||
57, 95, 177, 218, 238, 298, 333, 380, 426, 471, 515 → intervals
|
||||
38, 82, 41, 20, 30, 30, 35, 47, 46, 45, 44, 43, 21, 19 median 41 s
|
||||
```
|
||||
|
||||
A **41-second** sampling interval against a title screen that the corpus already
|
||||
documents as lasting *"a few seconds"* before auto-returning to the attract loop
|
||||
(`wait_title.sh`'s own header). The harness is not seeing a stuck game; it is
|
||||
blinking slower than the thing it is looking for.
|
||||
|
||||
That is also why runs at 16:43–18:05 the same day succeeded and later ones did
|
||||
not — nothing about the game changed.
|
||||
|
||||
## 🔴 What this retracts
|
||||
|
||||
Three earlier conclusions were built on these runs and are **withdrawn as
|
||||
causes**, though the observations stand:
|
||||
|
||||
* *"the Japanese-locale run never reaches the interactive title"* — it may well
|
||||
have appeared, unsampled.
|
||||
* *"neither locale reaches the interactive title without a pad press"* — the
|
||||
English control shared the same defect, so it controlled for locale but not for
|
||||
the sampling rate.
|
||||
* *"the game sat in the attract loop for 604 s"* — what was observed is that
|
||||
**every one of ~15 samples** landed on movie content, which at a 41 s interval
|
||||
is a much weaker statement than it reads as.
|
||||
|
||||
## ⚠️ A confound I introduced
|
||||
|
||||
Setting `kernel_display_gamma_type = 0` makes the frame substantially brighter
|
||||
(a mid-attract frame measured mean **122.8** against **52.5** and **82.8** on
|
||||
comparable phases at type 2). `skip_intro.sh` classifies movie-vs-static on an
|
||||
**absolute** rmse threshold of 1500 between two grabs, so a brighter output
|
||||
inflates that difference and biases every frame toward "movie". **The capture
|
||||
harness's tuning is coupled to the display settings** — changing gamma and
|
||||
capture behaviour in one run confounds both.
|
||||
|
||||
## 🔴 The fix works — and it REFUTES the diagnosis above
|
||||
|
||||
Built and measured (`tools/re-capture/fast_title_probe.py`): one long-lived
|
||||
`ffmpeg` x11grab stream, raw RGB frames, glyph counted in numpy. No per-sample
|
||||
process startup, no PNG encode, no `convert -crop`.
|
||||
|
||||
| probe | seconds per sample, emulator running |
|
||||
|---|---|
|
||||
| the wrapper `screenshot` | **3.98** |
|
||||
| `import -window root` → PPM | 1.20 |
|
||||
| **long-lived x11grab stream** | **0.29** |
|
||||
|
||||
**13.7× faster**, and the counter is control-verified against the committed
|
||||
frames — it returns **753** on `live-title-press-a.png` and **327** on
|
||||
`live-main-menu.png`, byte-identical to `is_title.py`.
|
||||
|
||||
Then it was pointed at a running game:
|
||||
|
||||
```
|
||||
332 frames in 85.3 s = 3.89 fps; max glyph 0
|
||||
1674 frames in 420.0 s = 3.99 fps; max glyph 0
|
||||
```
|
||||
|
||||
**1 674 consecutive samples over seven unbroken minutes, four per second, and the
|
||||
interactive title never appeared.** So the sampling rate was a real defect and
|
||||
*not* the cause. The hypothesis on this page — that the harness was blinking
|
||||
slower than the event — is **mine, and refuted by my own fix**.
|
||||
|
||||
## What that restores
|
||||
|
||||
Last iteration I withdrew three conclusions on the strength of that hypothesis.
|
||||
The withdrawal was right at the time (15 samples at 41 s intervals cannot support
|
||||
them) and is now **superseded by better evidence**: dense sampling says the
|
||||
interactive title genuinely does not appear in a mid-run window. Reinstated as a
|
||||
**measurement**, with its reach:
|
||||
|
||||
* ✅ over **420 continuous seconds**, English, `gamma_type = 2`, ~13 minutes into
|
||||
a run with no pad input, **zero** frames carried the green Ⓐ glyph.
|
||||
* ⚠️ Reach: this covers a **mid-run** window only. It says nothing about the
|
||||
first minutes of boot.
|
||||
|
||||
## 🟡 The leading hypothesis, not confirmed
|
||||
|
||||
The corpus already suspects the answer. `title_states_capture.sh` exists to test
|
||||
*"whether the interactive one draws `ptbtn00` (the PRESS Ⓐ plate) and the other
|
||||
does not"* — i.e. the title appears **twice**: once at the end of the boot
|
||||
sequence, and again from the attract loop, and only the first may carry the
|
||||
plate. If so, the plate's window is early and one-shot, and no amount of
|
||||
mid-run sampling will ever find it.
|
||||
|
||||
That is consistent with everything measured, and it is **not confirmed**. The
|
||||
test is to start the fast probe *before* the boot title — from t=0 rather than
|
||||
attaching to a run already in progress.
|
||||
|
||||
## The original fix note, kept
|
||||
|
||||
Make the probe cheap enough to sample faster than the title window: grab a small
|
||||
region rather than the full surface, drop the ImageMagick `convert` round trip,
|
||||
or keep the glyph test in one long-lived process instead of re-importing numpy
|
||||
per sample. None of that is done — this page is the diagnosis, and it is what
|
||||
every remaining emulator-side question is waiting on.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🔴 The boot-window hypothesis is refuted too — and the surface is BLACK
|
||||
|
||||
**2026-08-29, third explanation and third refutation.** The named experiment was
|
||||
to attach the fast probe at **t = 0** so the boot title could not be missed.
|
||||
Done, on the default config, English:
|
||||
|
||||
```
|
||||
2391 frames in 600.4 s = 3.98 fps; max glyph 0; hits 0
|
||||
```
|
||||
|
||||
Ten minutes, sampled four times a second **from launch**, and the green Ⓐ glyph
|
||||
never appeared. So "the plate only shows in an early boot window I keep missing"
|
||||
is **mine, and refuted**.
|
||||
|
||||
Then the thing I should have checked first. Splitting the raw root grab by band:
|
||||
|
||||
| band | non-black | mean |
|
||||
|---|---|---|
|
||||
| y 0–44 (the GTK menu bar) | **100 %** | 210.5 |
|
||||
| **y 45–719 (the game surface)** | **0.08 %** | **0.07** |
|
||||
|
||||
**The game is rendering black**, reproducibly across back-to-back samples, while
|
||||
the guest is alive and polling input (`XamInputGetKeystrokeEx` past 1 201 calls)
|
||||
and `MEM-WATCH` keeps reporting. The crop and every pixel oracle were correct;
|
||||
there was nothing on the surface to detect.
|
||||
|
||||
### ⚠️ What this does and does not explain
|
||||
|
||||
**It does not retroactively explain the earlier failures**, and saying so would
|
||||
be the fourth over-reach in a row. Those runs had *content*: run 2 sampled frames
|
||||
at mean 33.1 (warm), run 3's classifier measured real frame-to-frame rmse, and
|
||||
the `gamma_type = 0` run measured mean 122.8. A black surface is **this run's**
|
||||
symptom.
|
||||
|
||||
So the honest reading is that the failure mode **changed** over the session, and
|
||||
the black screen is a new and worse one.
|
||||
|
||||
### 🟡 Hypothesis for the regression, untested
|
||||
|
||||
Canary's shader/pipeline cache at `~/.local/share/Xenia/cache` is **47 MB** and
|
||||
was last written **23:49 on 2026-08-28** — during the failed runs. This session
|
||||
has `kill -9`'d the emulator repeatedly, which can leave a partially written
|
||||
cache. A corrupt pipeline cache is a plausible route to a guest that runs while
|
||||
nothing reaches the screen.
|
||||
|
||||
**The test is one line and one run:** move `cache*` aside and boot again. Not
|
||||
done — it needs a fresh 10-minute run, and this iteration had spent its emulator
|
||||
budget proving the negative above.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 The fast probe STALLS — and that invalidates its own dense negatives
|
||||
|
||||
**2026-08-29.** The instrument built last iteration is unfit for long runs, found
|
||||
by cross-checking it against an independent grabber *while both watched the same
|
||||
screen*.
|
||||
|
||||
A single long-lived `ffmpeg` x11grab stream degrades and then freezes:
|
||||
|
||||
```
|
||||
862 frames in 540.1 s = 1.60 fps (it starts at 3.98 fps)
|
||||
t=450s surface mean 5.21 nonblack 10.1%
|
||||
t=480s surface mean 5.21 nonblack 10.1%
|
||||
t=510s surface mean 5.21 nonblack 10.1%
|
||||
t=540s surface mean 5.21 nonblack 10.1%
|
||||
```
|
||||
|
||||
Four consecutive 30-second marks reporting an identical value. At that same
|
||||
moment an `import` grab of the same display read **surface mean 125.65**, and a
|
||||
*freshly started* ffmpeg stream read **122.43** — agreeing with `import` to 3 %.
|
||||
So the acquisition, not the analysis, was broken: the stream was replaying a
|
||||
stale frame while the screen was 24× brighter.
|
||||
|
||||
🔴 **Therefore the previous section's headline is withdrawn.** "2 391 frames over
|
||||
600 s from t=0, max glyph 0" cannot distinguish *the title never appeared* from
|
||||
*the stream froze early and repeated one frame 2 391 times*. Its 3.98 fps was
|
||||
measured over the first 20 s, before the degradation.
|
||||
|
||||
**Fixed** in `tools/re-capture/fast_title_probe.py`: the stream is torn down and
|
||||
restarted every 30 s. Startup costs ~0.3 s, cheap against the title's window, and
|
||||
it buys a guarantee the frames are live.
|
||||
|
||||
## ✅ Clearing the shader cache restored rendering
|
||||
|
||||
The cache hypothesis from the previous section was tested: `cache`, `cache0`,
|
||||
`cache1` and `cache_host` moved aside (to `/tmp/xenia-cache-aside`, not deleted),
|
||||
then a fresh boot.
|
||||
|
||||
The surface **renders again**, confirmed with the independent grabber:
|
||||
`import` reads surface mean **54.8** and **68.6** on successive samples with 100 %
|
||||
non-black warm content, against **0.07** and 0.08 % non-black in the black run.
|
||||
The probe's own summary agrees where it can be trusted — **773 of 862 frames had
|
||||
> 2 % non-black**.
|
||||
|
||||
🟡 **Supported, not proven.** One run each side, and the emulator was `kill -9`'d
|
||||
many times before the black run, so a corrupt pipeline cache is plausible but a
|
||||
single A/B does not exclude run-to-run variation. The old caches are kept in
|
||||
`/tmp/xenia-cache-aside` if anyone wants to reproduce the failure.
|
||||
|
||||
❔ **Still no title.** Even rendering, `max glyph 0` — but that number now comes
|
||||
from a stalling probe, so it establishes nothing either way.
|
||||
|
||||
---
|
||||
|
||||
## ✅ A negative I can finally stand behind — measured with a self-validating probe
|
||||
|
||||
**2026-08-29.** Three earlier "the title never appears" claims came from
|
||||
instruments that were later found broken (a stale pixel oracle, a 41 s sampling
|
||||
interval, a freezing stream). This run used
|
||||
[`tools/re-capture/title_probe_xchecked.py`](../../tools/re-capture/title_probe_xchecked.py),
|
||||
which restarts its stream every 30 s **and cross-checks itself against an
|
||||
independent `import` grab every 60 s**, printing both numbers.
|
||||
|
||||
```
|
||||
1851 frames in 560.2 s = 3.30 fps
|
||||
cross-checks 9, disagreements 1
|
||||
max glyph 0
|
||||
```
|
||||
|
||||
| t | stream | import | |
|
||||
|---|---|---|---|
|
||||
| 62 s | 6.05 | 0.07 | disagree — a fade, the boot logos are mid-transition |
|
||||
| 123 s | 7.40 | 7.49 | agree |
|
||||
| 183 s | 8.18 | 8.29 | agree |
|
||||
| 243 s | 0.23 | 0.10 | agree |
|
||||
| 311 s | 89.68 | 89.51 | agree |
|
||||
| 371 s | 80.97 | 81.58 | agree |
|
||||
| 426 s | 117.43 | 117.72 | agree |
|
||||
| 487 s | 77.71 | 76.25 | agree |
|
||||
| 546 s | 70.43 | 70.55 | agree |
|
||||
|
||||
Eight of nine agree to within 2 %, the fps held at 3.30 (no collapse to 1.60),
|
||||
and the surface plainly moved through dark and bright phases. **The frames were
|
||||
live and the negative is real:**
|
||||
|
||||
> ✅ **Measured — over 560 continuous seconds from launch, sampled 3.3 times a
|
||||
> second by a cross-validated probe, the interactive title's green Ⓐ plate never
|
||||
> appears, while the game renders throughout.**
|
||||
|
||||
The final frame correlates **0.0145** with our title render, **−0.0047** with the
|
||||
main menu and **0.0102** with `EXTRAS` — it is attract-movie content, not a UI
|
||||
screen at all.
|
||||
|
||||
## ❔ Why, still unknown
|
||||
|
||||
`live-title-press-a.png` is committed and carries 753 glyph pixels, so the title
|
||||
*was* reachable from this container on 2026-08-28. Nothing found so far explains
|
||||
the change. Clearing the shader cache fixed the *black surface* but not this.
|
||||
|
||||
## Where this leaves the emulator-side questions
|
||||
|
||||
The two open items that need a running menu — the gamma control
|
||||
([tone curve](structures/ui-render-tone-curve.md)) and separating `8AX` from
|
||||
`ptbase` ([8AX](structures/ui-8ax-fullres-background.md)) — remain blocked, now
|
||||
on a well-characterised and instrument-verified failure rather than on a
|
||||
suspicion. Neither blocks the five menu screens. **Returning to static work**;
|
||||
the probe is committed for whoever picks this up.
|
||||
|
||||
---
|
||||
|
||||
## ❔ The GPU trace route: attempted, produced nothing, characterised
|
||||
|
||||
**2026-08-29.** To turn the gamma-ramp *inference* into an observation, canary's
|
||||
`trace_gpu_stream` should work — it records gamma ramps as their own command type
|
||||
(`kGammaRamp`, index 11 in `TraceCommandType`). Two bounded runs produced **no
|
||||
trace file at all**: nothing under the prefix, no `.xtr` anywhere, no
|
||||
`scratch/gpu/`.
|
||||
|
||||
Bounded deliberately: the container's disk is at **95 % (50 GiB free)** and a
|
||||
boot-time trace of all GPU packets includes video decode, so the runner carried a
|
||||
watchdog killing the emulator the moment the trace passed a 2 GiB cap. It never
|
||||
fired — there was nothing to cap. Disk was unchanged at 95 % throughout.
|
||||
|
||||
What the attempt did establish:
|
||||
|
||||
* `BeginTracing()` is called at GPU init when the cvar is set
|
||||
(`graphics_system.cc:237`), but `EndTracing()` runs only from
|
||||
`GraphicsSystem::Shutdown()`. **A `kill -9` — which this session has used
|
||||
routinely — can never finalise a trace.** The second run was therefore stopped
|
||||
with `SIGTERM` and exited cleanly. Still no file, so that is not the whole
|
||||
story.
|
||||
* ~~Two candidates remain and were **not** separated: the CLI flag not reaching
|
||||
the cvar, or `BeginTracing()` failing silently.~~
|
||||
|
||||
### ✅ Explained — and it was neither candidate
|
||||
|
||||
**The trace writer is compiled out of the build in use.** Following the code:
|
||||
`BeginTracing()` only sets `trace_state_ = kStreaming` — *"Streaming starts on
|
||||
the next primary buffer execute"* — and the file is opened later, in
|
||||
`ExecutePrimaryBuffer`, inside
|
||||
|
||||
```cpp
|
||||
#if XE_ENABLE_TRACE_WRITER_INSTRUMENTATION == 1
|
||||
```
|
||||
|
||||
which `trace_writer.h` defines as:
|
||||
|
||||
```cpp
|
||||
#ifdef NDEBUG
|
||||
#define XE_ENABLE_TRACE_WRITER_INSTRUMENTATION 0 // release
|
||||
#else
|
||||
#define XE_ENABLE_TRACE_WRITER_INSTRUMENTATION 1 // debug
|
||||
#endif
|
||||
```
|
||||
|
||||
Confirmed in the binaries themselves, with a control. The format string
|
||||
`"{:08X}_stream.xtr"` exists only inside that guard:
|
||||
|
||||
| binary | `_stream.xtr` occurrences |
|
||||
|---|---|
|
||||
| `build/bin/Linux/Release/xenia_canary` | **0** |
|
||||
| `build/bin/Linux/Debug/xenia_canary` | **1** |
|
||||
| `/sylph-home/re/canary-build/.../Release/xenia_canary` — **the one `run-canary` uses** | **0** |
|
||||
|
||||
So `trace_gpu_stream` is a **no-op in this container's emulator**: the cvar
|
||||
parses, `BeginTracing` runs, and nothing can ever open a file. Neither the CLI
|
||||
flag nor `BeginTracing` was at fault, and neither was the `kill -9` — though that
|
||||
would have destroyed the trace too, had one existed.
|
||||
|
||||
🟡 **The route exists but is not cheap.** A Debug build with the writer compiled
|
||||
in is present at `build/bin/Linux/Debug/xenia_canary` (253 MB against Release's
|
||||
18 MB). Running it means a much slower boot and a trace of every GPU packet on a
|
||||
disk at 95 %. Recorded as available rather than attempted — the thing it would
|
||||
confirm (the `DC_LUT` write) is already a well-supported inference, so the cost
|
||||
is out of proportion to the gain.
|
||||
|
||||
## ⚠️ The config dump in a log is the FILE, not the effective command line
|
||||
|
||||
Nearly a wrong conclusion here. The dump printed at startup showed
|
||||
`trace_gpu_stream = false` after I had passed `--trace_gpu_stream=true`, which
|
||||
reads as "the flag was ignored". It is not evidence either way:
|
||||
|
||||
| | passed on the CLI | shown in the dump | actual behaviour |
|
||||
|---|---|---|---|
|
||||
| gamma run | `--log_mask=12 --log_level=3` | `log_mask = 0`, `log_level = 2` | **Kernel Debug logging demonstrably ON** |
|
||||
|
||||
The gamma run's flags plainly took effect — that run is where
|
||||
`VdGetCurrentDisplayGamma` was captured — while its dump showed the file's
|
||||
values. So the dump reflects the config file and cannot confirm or refute a
|
||||
command-line override.
|
||||
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/menu-nav/extras-wrap.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/re/captures/menu-nav/q4-destinations.png
Normal file
|
After Width: | Height: | Size: 634 KiB |
BIN
docs/re/captures/menu-nav/wrap-montage.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/re/captures/newgame-path/newgame-difficulty.png
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
docs/re/captures/newgame-path/newgame-selectdata-crash.png
Normal file
|
After Width: | Height: | Size: 435 KiB |
BIN
docs/re/captures/newgame-path/s00a-frame-at-capture-6s.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/re/captures/newgame-path/title-after-single-A-skip.png
Normal file
|
After Width: | Height: | Size: 994 KiB |
BIN
docs/re/captures/ready-room-probe/ready-room-pak-builds.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 143 KiB |
BIN
docs/re/captures/title-builds/live-extras.png
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
docs/re/captures/title-builds/live-main-menu-options-focused.png
Normal file
|
After Width: | Height: | Size: 645 KiB |
BIN
docs/re/captures/title-builds/live-main-menu.png
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
docs/re/captures/title-builds/live-splash-developer.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/re/captures/title-builds/live-splash-publisher.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/re/captures/title-builds/live-title-build4-no-plate.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/re/captures/title-builds/live-title-press-a.png
Normal file
|
After Width: | Height: | Size: 996 KiB |
BIN
docs/re/captures/title-builds/splash-both-halves-rendered.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/re/captures/title-builds/splash-entries-rendered.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/re/captures/title-builds/title-build-contact-sheet.png
Normal file
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 664 KiB |
@@ -0,0 +1,43 @@
|
||||
# every draw in SUBMISSION ORDER, undeduplicated, frames 1..3
|
||||
# tex dimensions identify the sprite; base is the guest address
|
||||
0 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
|
||||
vb=0x15036638 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [-0.50,-0.50,z=0.00000] [1279.50,-0.50,z=0.00000] [1279.50,719.50,z=0.00000]
|
||||
1 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
|
||||
vb=0x15036690 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||
2 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||
vb=0x150366F0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [0.70,1.58,z=0.00000,col=C3FFFFFF] [1.24,1.02,z=0.00000,col=C3FFFFFF] [0.40,-1.57,z=0.00000,col=C3FFFFFF] [-0.14,-1.02,z=0.00000,col=C3FFFFFF] [-1.29,1.02,z=0.00000,col=B6FFFFFF] [-0.85,1.81,z=0.00000,col=B6FFFFFF] [0.75,-1.02,z=0.00000,col=B6FFFFFF] [0.31,-1.81,z=0.00000,col=B6FFFFFF]
|
||||
3 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
|
||||
vb=0x150367B0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||
4 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||
vb=0x15036810 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.90,0.68,z=0.00000,col=FFFFFFFF] [0.87,0.68,z=0.00000,col=FFFFFFFF] [0.87,-0.10,z=0.00000,col=FFFFFFFF] [-0.90,-0.10,z=0.00000,col=FFFFFFFF] [-0.89,0.65,z=0.00000,col=FFFFFFFF] [0.86,0.65,z=0.00000,col=FFFFFFFF] [0.86,-0.08,z=0.00000,col=FFFFFFFF] [-0.89,-0.08,z=0.00000,col=FFFFFFFF]
|
||||
5 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||
vb=0x150368D0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.71,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.15,z=0.00000,col=FFFFFFFF] [-0.71,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.10,z=0.00000,col=FFFFFFFF] [0.76,-0.10,z=0.00000,col=FFFFFFFF]
|
||||
6 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
|
||||
vb=0x15036990 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.79,0.15,z=0.00000,col=FFFFFFFF] [0.76,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.14,z=0.00000,col=FFFFFFFF] [-0.79,-0.14,z=0.00000,col=FFFFFFFF]
|
||||
7 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||
vb=0x150369F0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.67,z=0.00000,col=FFFFFFFF] [-0.40,-0.67,z=0.00000,col=FFFFFFFF]
|
||||
8 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||
vb=0x15036AB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.42,-0.49,z=0.00000,col=31FFFFFF] [0.42,-0.49,z=0.00000,col=31FFFFFF] [0.42,-0.70,z=0.00000,col=31FFFFFF] [-0.42,-0.70,z=0.00000,col=31FFFFFF]
|
||||
9 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||
10 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||
--- frame 3 ---
|
||||
11 prim=8 indices=3 vs=0x0A6D1DD7767FDF27 ps=0x2E372EA28CC404B7
|
||||
vb=0x15076C38 stride=28 attrs=[57@0 38@12 ] fmt0=57 v: [-0.50,-0.50,z=0.00000] [1279.50,-0.50,z=0.00000] [1279.50,719.50,z=0.00000]
|
||||
12 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BA60000 1280x768 fmt=6]
|
||||
vb=0x15076C90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||
13 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||
vb=0x15076CF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [0.73,1.58,z=0.00000,col=C5FFFFFF] [1.27,1.02,z=0.00000,col=C5FFFFFF] [0.43,-1.57,z=0.00000,col=C5FFFFFF] [-0.11,-1.02,z=0.00000,col=C5FFFFFF] [-1.32,1.02,z=0.00000,col=B8FFFFFF] [-0.88,1.81,z=0.00000,col=B8FFFFFF] [0.71,-1.02,z=0.00000,col=B8FFFFFF] [0.27,-1.81,z=0.00000,col=B8FFFFFF]
|
||||
14 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B690000 1280x768 fmt=6]
|
||||
vb=0x15076DB0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,1.00,z=0.00000,col=FFFFFFFF] [1.00,-1.00,z=0.00000,col=FFFFFFFF] [-1.00,-1.00,z=0.00000,col=FFFFFFFF]
|
||||
15 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||
vb=0x15076E10 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.90,0.68,z=0.00000,col=FFFFFFFF] [0.87,0.68,z=0.00000,col=FFFFFFFF] [0.87,-0.10,z=0.00000,col=FFFFFFFF] [-0.90,-0.10,z=0.00000,col=FFFFFFFF] [-0.89,0.65,z=0.00000,col=FFFFFFFF] [0.86,0.65,z=0.00000,col=FFFFFFFF] [0.86,-0.08,z=0.00000,col=FFFFFFFF] [-0.89,-0.08,z=0.00000,col=FFFFFFFF]
|
||||
16 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||
vb=0x15076ED0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.71,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.47,z=0.00000,col=FFFFFFFF] [0.72,0.15,z=0.00000,col=FFFFFFFF] [-0.71,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.05,z=0.00000,col=FFFFFFFF] [0.82,-0.10,z=0.00000,col=FFFFFFFF] [0.76,-0.10,z=0.00000,col=FFFFFFFF]
|
||||
17 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0BE30000 1280x768 fmt=6]
|
||||
vb=0x15076F90 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.79,0.15,z=0.00000,col=FFFFFFFF] [0.76,0.15,z=0.00000,col=FFFFFFFF] [0.76,-0.14,z=0.00000,col=FFFFFFFF] [-0.79,-0.14,z=0.00000,col=FFFFFFFF]
|
||||
18 prim=13 indices=8 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x11C80000 1280x768 fmt=6]
|
||||
vb=0x15076FF0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.82,z=0.00000,col=FFFFFFFF] [0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.54,-0.87,z=0.00000,col=FFFFFFFF] [-0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.53,z=0.00000,col=FFFFFFFF] [0.40,-0.67,z=0.00000,col=FFFFFFFF] [-0.40,-0.67,z=0.00000,col=FFFFFFFF]
|
||||
19 prim=13 indices=4 vs=0xE0BAFB4F520FE441 ps=0xE59B2B3DA4AA9008 tex[base=0x0B2C0000 1280x768 fmt=6]
|
||||
vb=0x150770B0 stride=24 attrs=[57@0 6@12 37@16 ] fmt0=57 v: [-0.42,-0.49,z=0.00000,col=1EFFFFFF] [0.42,-0.49,z=0.00000,col=1EFFFFFF] [0.42,-0.70,z=0.00000,col=1EFFFFFF] [-0.42,-0.70,z=0.00000,col=1EFFFFFF]
|
||||
20 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||
21 prim=8 indices=3 vs=0x72CBCAA6A7984111 ps=0xE59B2B3DA4AA9008 tex[base=0x10000000 1x1 fmt=26]
|
||||
BIN
docs/re/captures/title-builds/title-swoosh-capture-vs-render.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
docs/re/captures/transitions/transition-filmstrip.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
421
docs/re/captures/transitions/transition-luminance.csv
Normal file
@@ -0,0 +1,421 @@
|
||||
frame,seconds,mean_luminance
|
||||
0,0.0000,26.045
|
||||
1,0.0333,26.054
|
||||
2,0.0667,26.051
|
||||
3,0.1000,26.054
|
||||
4,0.1333,26.049
|
||||
5,0.1667,26.054
|
||||
6,0.2000,26.052
|
||||
7,0.2333,26.053
|
||||
8,0.2667,26.046
|
||||
9,0.3000,26.053
|
||||
10,0.3333,26.049
|
||||
11,0.3667,26.052
|
||||
12,0.4000,26.047
|
||||
13,0.4333,26.050
|
||||
14,0.4667,26.048
|
||||
15,0.5000,26.049
|
||||
16,0.5333,26.047
|
||||
17,0.5667,26.049
|
||||
18,0.6000,26.048
|
||||
19,0.6333,26.048
|
||||
20,0.6667,26.045
|
||||
21,0.7000,26.048
|
||||
22,0.7333,26.049
|
||||
23,0.7667,26.048
|
||||
24,0.8000,26.046
|
||||
25,0.8333,26.048
|
||||
26,0.8667,26.048
|
||||
27,0.9000,26.049
|
||||
28,0.9333,26.047
|
||||
29,0.9667,26.048
|
||||
30,1.0000,26.048
|
||||
31,1.0333,26.048
|
||||
32,1.0667,26.047
|
||||
33,1.1000,26.048
|
||||
34,1.1333,26.047
|
||||
35,1.1667,26.047
|
||||
36,1.2000,26.046
|
||||
37,1.2333,26.046
|
||||
38,1.2667,26.046
|
||||
39,1.3000,26.047
|
||||
40,1.3333,26.046
|
||||
41,1.3667,26.047
|
||||
42,1.4000,26.047
|
||||
43,1.4333,26.048
|
||||
44,1.4667,26.047
|
||||
45,1.5000,26.049
|
||||
46,1.5333,26.050
|
||||
47,1.5667,26.052
|
||||
48,1.6000,26.051
|
||||
49,1.6333,26.052
|
||||
50,1.6667,26.052
|
||||
51,1.7000,26.052
|
||||
52,1.7333,26.052
|
||||
53,1.7667,26.055
|
||||
54,1.8000,26.056
|
||||
55,1.8333,26.056
|
||||
56,1.8667,26.055
|
||||
57,1.9000,26.058
|
||||
58,1.9333,26.059
|
||||
59,1.9667,26.059
|
||||
60,2.0000,26.057
|
||||
61,2.0333,26.062
|
||||
62,2.0667,26.022
|
||||
63,2.1000,22.032
|
||||
64,2.1333,21.744
|
||||
65,2.1667,17.909
|
||||
66,2.2000,13.996
|
||||
67,2.2333,13.966
|
||||
68,2.2667,11.113
|
||||
69,2.3000,8.072
|
||||
70,2.3333,7.862
|
||||
71,2.3667,6.270
|
||||
72,2.4000,3.375
|
||||
73,2.4333,1.771
|
||||
74,2.4667,0.020
|
||||
75,2.5000,0.020
|
||||
76,2.5333,0.020
|
||||
77,2.5667,0.020
|
||||
78,2.6000,0.020
|
||||
79,2.6333,0.020
|
||||
80,2.6667,0.020
|
||||
81,2.7000,5.985
|
||||
82,2.7333,5.991
|
||||
83,2.7667,5.990
|
||||
84,2.8000,5.991
|
||||
85,2.8333,5.986
|
||||
86,2.8667,7.432
|
||||
87,2.9000,9.300
|
||||
88,2.9333,9.283
|
||||
89,2.9667,9.484
|
||||
90,3.0000,11.294
|
||||
91,3.0333,11.263
|
||||
92,3.0667,11.606
|
||||
93,3.1000,12.608
|
||||
94,3.1333,12.807
|
||||
95,3.1667,12.790
|
||||
96,3.2000,14.517
|
||||
97,3.2333,18.497
|
||||
98,3.2667,26.935
|
||||
99,3.3000,27.008
|
||||
100,3.3333,27.010
|
||||
101,3.3667,27.039
|
||||
102,3.4000,27.036
|
||||
103,3.4333,27.083
|
||||
104,3.4667,27.239
|
||||
105,3.5000,27.294
|
||||
106,3.5333,27.293
|
||||
107,3.5667,27.294
|
||||
108,3.6000,27.289
|
||||
109,3.6333,27.291
|
||||
110,3.6667,27.290
|
||||
111,3.7000,27.291
|
||||
112,3.7333,27.289
|
||||
113,3.7667,27.292
|
||||
114,3.8000,27.291
|
||||
115,3.8333,27.292
|
||||
116,3.8667,27.291
|
||||
117,3.9000,27.294
|
||||
118,3.9333,27.291
|
||||
119,3.9667,27.293
|
||||
120,4.0000,27.290
|
||||
121,4.0333,27.292
|
||||
122,4.0667,27.291
|
||||
123,4.1000,27.292
|
||||
124,4.1333,27.290
|
||||
125,4.1667,27.293
|
||||
126,4.2000,27.293
|
||||
127,4.2333,27.293
|
||||
128,4.2667,27.292
|
||||
129,4.3000,27.297
|
||||
130,4.3333,27.296
|
||||
131,4.3667,27.297
|
||||
132,4.4000,27.296
|
||||
133,4.4333,27.299
|
||||
134,4.4667,27.300
|
||||
135,4.5000,27.302
|
||||
136,4.5333,27.301
|
||||
137,4.5667,27.304
|
||||
138,4.6000,27.304
|
||||
139,4.6333,27.304
|
||||
140,4.6667,27.302
|
||||
141,4.7000,27.306
|
||||
142,4.7333,27.308
|
||||
143,4.7667,27.309
|
||||
144,4.8000,27.309
|
||||
145,4.8333,27.312
|
||||
146,4.8667,27.311
|
||||
147,4.9000,27.311
|
||||
148,4.9333,27.310
|
||||
149,4.9667,27.317
|
||||
150,5.0000,27.319
|
||||
151,5.0333,27.321
|
||||
152,5.0667,27.322
|
||||
153,5.1000,27.327
|
||||
154,5.1333,27.327
|
||||
155,5.1667,27.328
|
||||
156,5.2000,27.327
|
||||
157,5.2333,27.333
|
||||
158,5.2667,27.334
|
||||
159,5.3000,27.335
|
||||
160,5.3333,27.334
|
||||
161,5.3667,27.344
|
||||
162,5.4000,27.347
|
||||
163,5.4333,27.350
|
||||
164,5.4667,27.351
|
||||
165,5.5000,27.359
|
||||
166,5.5333,27.359
|
||||
167,5.5667,27.360
|
||||
168,5.6000,27.358
|
||||
169,5.6333,27.365
|
||||
170,5.6667,27.366
|
||||
171,5.7000,27.369
|
||||
172,5.7333,27.367
|
||||
173,5.7667,27.383
|
||||
174,5.8000,27.386
|
||||
175,5.8333,27.387
|
||||
176,5.8667,27.386
|
||||
177,5.9000,27.399
|
||||
178,5.9333,27.400
|
||||
179,5.9667,27.402
|
||||
180,6.0000,27.400
|
||||
181,6.0333,27.413
|
||||
182,6.0667,27.414
|
||||
183,6.1000,27.415
|
||||
184,6.1333,27.413
|
||||
185,6.1667,27.432
|
||||
186,6.2000,27.434
|
||||
187,6.2333,27.438
|
||||
188,6.2667,27.436
|
||||
189,6.3000,27.449
|
||||
190,6.3333,27.448
|
||||
191,6.3667,27.451
|
||||
192,6.4000,27.449
|
||||
193,6.4333,27.466
|
||||
194,6.4667,27.471
|
||||
195,6.5000,27.472
|
||||
196,6.5333,27.470
|
||||
197,6.5667,27.480
|
||||
198,6.6000,27.478
|
||||
199,6.6333,27.480
|
||||
200,6.6667,27.476
|
||||
201,6.7000,27.509
|
||||
202,6.7333,27.510
|
||||
203,6.7667,27.516
|
||||
204,6.8000,27.514
|
||||
205,6.8333,27.519
|
||||
206,6.8667,27.519
|
||||
207,6.9000,27.518
|
||||
208,6.9333,27.516
|
||||
209,6.9667,27.538
|
||||
210,7.0000,27.537
|
||||
211,7.0333,27.539
|
||||
212,7.0667,27.538
|
||||
213,7.1000,27.558
|
||||
214,7.1333,27.556
|
||||
215,7.1667,27.558
|
||||
216,7.2000,27.554
|
||||
217,7.2333,27.577
|
||||
218,7.2667,27.576
|
||||
219,7.3000,27.577
|
||||
220,7.3333,27.572
|
||||
221,7.3667,27.596
|
||||
222,7.4000,27.598
|
||||
223,7.4333,27.603
|
||||
224,7.4667,27.597
|
||||
225,7.5000,27.610
|
||||
226,7.5333,27.611
|
||||
227,7.5667,27.613
|
||||
228,7.6000,27.607
|
||||
229,7.6333,27.621
|
||||
230,7.6667,27.621
|
||||
231,7.7000,27.622
|
||||
232,7.7333,27.617
|
||||
233,7.7667,27.624
|
||||
234,7.8000,27.626
|
||||
235,7.8333,27.630
|
||||
236,7.8667,27.622
|
||||
237,7.9000,27.640
|
||||
238,7.9333,27.637
|
||||
239,7.9667,27.639
|
||||
240,8.0000,27.633
|
||||
241,8.0333,27.642
|
||||
242,8.0667,27.639
|
||||
243,8.1000,27.642
|
||||
244,8.1333,27.637
|
||||
245,8.1667,27.658
|
||||
246,8.2000,27.653
|
||||
247,8.2333,27.655
|
||||
248,8.2667,27.649
|
||||
249,8.3000,27.645
|
||||
250,8.3333,27.648
|
||||
251,8.3667,26.704
|
||||
252,8.4000,23.887
|
||||
253,8.4333,23.723
|
||||
254,8.4667,16.471
|
||||
255,8.5000,13.119
|
||||
256,8.5333,13.102
|
||||
257,8.5667,12.118
|
||||
258,8.6000,10.955
|
||||
259,8.6333,10.956
|
||||
260,8.6667,5.365
|
||||
261,8.7000,3.422
|
||||
262,8.7333,3.416
|
||||
263,8.7667,2.820
|
||||
264,8.8000,0.020
|
||||
265,8.8333,0.020
|
||||
266,8.8667,0.020
|
||||
267,8.9000,0.020
|
||||
268,8.9333,0.020
|
||||
269,8.9667,3.267
|
||||
270,9.0000,5.012
|
||||
271,9.0333,5.449
|
||||
272,9.0667,7.339
|
||||
273,9.1000,9.208
|
||||
274,9.1333,10.415
|
||||
275,9.1667,10.418
|
||||
276,9.2000,10.462
|
||||
277,9.2333,10.459
|
||||
278,9.2667,10.569
|
||||
279,9.3000,10.701
|
||||
280,9.3333,10.868
|
||||
281,9.3667,12.924
|
||||
282,9.4000,14.688
|
||||
283,9.4333,14.756
|
||||
284,9.4667,17.184
|
||||
285,9.5000,19.441
|
||||
286,9.5333,19.438
|
||||
287,9.5667,19.492
|
||||
288,9.6000,19.492
|
||||
289,9.6333,19.492
|
||||
290,9.6667,19.508
|
||||
291,9.7000,23.405
|
||||
292,9.7333,23.417
|
||||
293,9.7667,23.414
|
||||
294,9.8000,23.416
|
||||
295,9.8333,23.409
|
||||
296,9.8667,23.467
|
||||
297,9.9000,24.927
|
||||
298,9.9333,24.929
|
||||
299,9.9667,24.926
|
||||
300,10.0000,25.788
|
||||
301,10.0333,25.785
|
||||
302,10.0667,25.789
|
||||
303,10.1000,25.893
|
||||
304,10.1333,25.908
|
||||
305,10.1667,25.905
|
||||
306,10.2000,25.905
|
||||
307,10.2333,25.976
|
||||
308,10.2667,25.986
|
||||
309,10.3000,25.980
|
||||
310,10.3333,25.981
|
||||
311,10.3667,25.976
|
||||
312,10.4000,25.984
|
||||
313,10.4333,26.020
|
||||
314,10.4667,26.022
|
||||
315,10.5000,26.016
|
||||
316,10.5333,26.021
|
||||
317,10.5667,26.018
|
||||
318,10.6000,26.020
|
||||
319,10.6333,26.016
|
||||
320,10.6667,26.021
|
||||
321,10.7000,26.021
|
||||
322,10.7333,26.021
|
||||
323,10.7667,26.019
|
||||
324,10.8000,26.022
|
||||
325,10.8333,26.022
|
||||
326,10.8667,26.022
|
||||
327,10.9000,26.020
|
||||
328,10.9333,26.025
|
||||
329,10.9667,26.025
|
||||
330,11.0000,26.025
|
||||
331,11.0333,26.023
|
||||
332,11.0667,26.029
|
||||
333,11.1000,26.027
|
||||
334,11.1333,26.028
|
||||
335,11.1667,26.024
|
||||
336,11.2000,26.024
|
||||
337,11.2333,26.024
|
||||
338,11.2667,26.024
|
||||
339,11.3000,26.029
|
||||
340,11.3333,26.029
|
||||
341,11.3667,26.027
|
||||
342,11.4000,26.029
|
||||
343,11.4333,26.024
|
||||
344,11.4667,26.027
|
||||
345,11.5000,26.027
|
||||
346,11.5333,26.027
|
||||
347,11.5667,26.025
|
||||
348,11.6000,26.031
|
||||
349,11.6333,26.030
|
||||
350,11.6667,26.033
|
||||
351,11.7000,26.028
|
||||
352,11.7333,26.032
|
||||
353,11.7667,26.032
|
||||
354,11.8000,26.032
|
||||
355,11.8333,26.029
|
||||
356,11.8667,26.034
|
||||
357,11.9000,26.039
|
||||
358,11.9333,26.041
|
||||
359,11.9667,26.041
|
||||
360,12.0000,26.040
|
||||
361,12.0333,26.046
|
||||
362,12.0667,26.047
|
||||
363,12.1000,26.046
|
||||
364,12.1333,26.050
|
||||
365,12.1667,26.051
|
||||
366,12.2000,26.051
|
||||
367,12.2333,26.051
|
||||
368,12.2667,26.051
|
||||
369,12.3000,26.051
|
||||
370,12.3333,26.051
|
||||
371,12.3667,26.051
|
||||
372,12.4000,26.050
|
||||
373,12.4333,26.055
|
||||
374,12.4667,26.056
|
||||
375,12.5000,26.056
|
||||
376,12.5333,26.055
|
||||
377,12.5667,26.056
|
||||
378,12.6000,26.056
|
||||
379,12.6333,26.056
|
||||
380,12.6667,26.055
|
||||
381,12.7000,26.056
|
||||
382,12.7333,26.056
|
||||
383,12.7667,26.056
|
||||
384,12.8000,26.055
|
||||
385,12.8333,26.055
|
||||
386,12.8667,26.056
|
||||
387,12.9000,26.056
|
||||
388,12.9333,26.056
|
||||
389,12.9667,26.056
|
||||
390,13.0000,26.055
|
||||
391,13.0333,26.071
|
||||
392,13.0667,26.072
|
||||
393,13.1000,26.074
|
||||
394,13.1333,26.082
|
||||
395,13.1667,26.083
|
||||
396,13.2000,26.083
|
||||
397,13.2333,26.083
|
||||
398,13.2667,26.083
|
||||
399,13.3000,26.110
|
||||
400,13.3333,26.111
|
||||
401,13.3667,26.114
|
||||
402,13.4000,26.113
|
||||
403,13.4333,26.118
|
||||
404,13.4667,26.119
|
||||
405,13.5000,26.121
|
||||
406,13.5333,26.119
|
||||
407,13.5667,26.140
|
||||
408,13.6000,26.140
|
||||
409,13.6333,26.144
|
||||
410,13.6667,26.142
|
||||
411,13.7000,26.155
|
||||
412,13.7333,26.157
|
||||
413,13.7667,26.158
|
||||
414,13.8000,26.157
|
||||
415,13.8333,26.165
|
||||
416,13.8667,26.165
|
||||
417,13.9000,26.168
|
||||
418,13.9333,26.165
|
||||
419,13.9667,26.165
|
||||
|
BIN
docs/re/captures/ui-layout/button-base-focus-ring-sprites.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/re/captures/ui-layout/extras-composited.png
Normal file
|
After Width: | Height: | Size: 439 KiB |
BIN
docs/re/captures/ui-layout/focus-ring-oracle-vs-sprite.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
2052
docs/re/captures/ui-timing/splash-build-draws.log
Normal file
790
docs/re/captures/ui-timing/splash-build-quads.csv
Normal file
@@ -0,0 +1,790 @@
|
||||
frame,draw,x,y,w,h,rot,col
|
||||
1,0,0,0,1280,720,,
|
||||
1,1,0,0,1280,720,,FF000000
|
||||
1,2,307,331,666,65,,FFFFFFFF
|
||||
2,5,0,0,1280,720,,
|
||||
2,6,0,0,1280,720,,FF000000
|
||||
2,7,307,331,666,65,,FFFFFFFF
|
||||
3,10,0,0,1280,720,,
|
||||
3,11,0,0,1280,720,,FF000000
|
||||
3,12,307,331,666,65,,FFFFFFFF
|
||||
4,15,0,0,1280,720,,
|
||||
4,16,0,0,1280,720,,FF000000
|
||||
4,17,307,331,666,65,,FFFFFFFF
|
||||
5,20,0,0,1280,720,,
|
||||
5,21,0,0,1280,720,,FF000000
|
||||
5,22,307,331,666,65,,FFFFFFFF
|
||||
6,25,0,0,1280,720,,
|
||||
6,26,0,0,1280,720,,FF000000
|
||||
6,27,307,331,666,65,,FFFFFFFF
|
||||
7,30,0,0,1280,720,,
|
||||
7,31,0,0,1280,720,,FF000000
|
||||
7,32,307,331,666,65,,FFFFFFFF
|
||||
8,35,0,0,1280,720,,
|
||||
8,36,0,0,1280,720,,FF000000
|
||||
8,37,307,331,666,65,,FFFFFFFF
|
||||
9,40,0,0,1280,720,,
|
||||
9,41,0,0,1280,720,,FF000000
|
||||
9,42,307,331,666,65,,FFFFFFFF
|
||||
10,45,0,0,1280,720,,
|
||||
10,46,0,0,1280,720,,FF000000
|
||||
10,47,307,331,666,65,,FFFFFFFF
|
||||
11,50,0,0,1280,720,,
|
||||
11,51,0,0,1280,720,,FF000000
|
||||
11,52,307,331,666,65,,FFFFFFFF
|
||||
12,55,0,0,1280,720,,
|
||||
12,56,0,0,1280,720,,FF000000
|
||||
12,57,307,331,666,65,,FFFFFFFF
|
||||
13,60,0,0,1280,720,,
|
||||
13,61,0,0,1280,720,,FF000000
|
||||
13,62,307,331,666,65,,FFFFFFFF
|
||||
14,65,0,0,1280,720,,
|
||||
14,66,0,0,1280,720,,FF000000
|
||||
14,67,307,331,666,65,,FFFFFFFF
|
||||
15,70,0,0,1280,720,,
|
||||
15,71,0,0,1280,720,,FF000000
|
||||
15,72,307,331,666,65,,FFFFFFFF
|
||||
16,75,0,0,1280,720,,
|
||||
16,76,0,0,1280,720,,FF000000
|
||||
16,77,307,331,666,65,,FFFFFFFF
|
||||
17,80,0,0,1280,720,,
|
||||
17,81,0,0,1280,720,,FF000000
|
||||
17,82,307,331,666,65,,FFFFFFFF
|
||||
18,85,0,0,1280,720,,
|
||||
18,86,0,0,1280,720,,FF000000
|
||||
18,87,307,331,666,65,,FFFFFFFF
|
||||
19,90,0,0,1280,720,,
|
||||
19,91,0,0,1280,720,,FF000000
|
||||
19,92,307,331,666,65,,FFFFFFFF
|
||||
20,95,0,0,1280,720,,
|
||||
20,96,0,0,1280,720,,FF000000
|
||||
20,97,307,331,666,65,,FFFFFFFF
|
||||
21,100,0,0,1280,720,,
|
||||
21,101,0,0,1280,720,,FF000000
|
||||
21,102,307,331,666,65,,FFFFFFFF
|
||||
22,105,0,0,1280,720,,
|
||||
22,106,0,0,1280,720,,FF000000
|
||||
22,107,307,331,666,65,,FFFFFFFF
|
||||
23,110,0,0,1280,720,,
|
||||
23,111,0,0,1280,720,,FF000000
|
||||
23,112,307,331,666,65,,FFFFFFFF
|
||||
24,115,0,0,1280,720,,
|
||||
24,116,0,0,1280,720,,FF000000
|
||||
24,117,307,331,666,65,,FFFFFFFF
|
||||
25,120,0,0,1280,720,,
|
||||
25,121,0,0,1280,720,,FF000000
|
||||
25,122,307,331,666,65,,FFFFFFFF
|
||||
26,125,0,0,1280,720,,
|
||||
26,126,0,0,1280,720,,FF000000
|
||||
26,127,307,331,666,65,,FFFFFFFF
|
||||
27,130,0,0,1280,720,,
|
||||
27,131,0,0,1280,720,,FF000000
|
||||
27,132,307,331,666,65,,FFFFFFFF
|
||||
28,135,0,0,1280,720,,
|
||||
28,136,0,0,1280,720,,FF000000
|
||||
28,137,307,331,666,65,,FFFFFFFF
|
||||
29,140,0,0,1280,720,,
|
||||
29,141,0,0,1280,720,,FF000000
|
||||
29,142,307,331,666,65,,FFFFFFFF
|
||||
30,145,0,0,1280,720,,
|
||||
30,146,0,0,1280,720,,FF000000
|
||||
30,147,307,331,666,65,,FFFFFFFF
|
||||
31,150,0,0,1280,720,,
|
||||
31,151,0,0,1280,720,,FF000000
|
||||
31,152,307,331,666,65,,FFFFFFFF
|
||||
32,155,0,0,1280,720,,
|
||||
32,156,0,0,1280,720,,FF000000
|
||||
32,157,307,331,666,65,,FFFFFFFF
|
||||
33,160,0,0,1280,720,,
|
||||
33,161,0,0,1280,720,,FF000000
|
||||
33,162,307,331,666,65,,FFFFFFFF
|
||||
34,165,0,0,1280,720,,
|
||||
34,166,0,0,1280,720,,FF000000
|
||||
34,167,307,331,666,65,,FFFFFFFF
|
||||
35,170,0,0,1280,720,,
|
||||
35,171,0,0,1280,720,,FF000000
|
||||
35,172,307,331,666,65,,FFFFFFFF
|
||||
36,175,0,0,1280,720,,
|
||||
36,176,0,0,1280,720,,FF000000
|
||||
36,177,307,331,666,65,,FFFFFFFF
|
||||
37,180,0,0,1280,720,,
|
||||
37,181,0,0,1280,720,,FF000000
|
||||
37,182,307,331,666,65,,FFFFFFFF
|
||||
38,185,0,0,1280,720,,
|
||||
38,186,0,0,1280,720,,FF000000
|
||||
38,187,307,331,666,65,,FFFFFFFF
|
||||
39,190,0,0,1280,720,,
|
||||
39,191,0,0,1280,720,,FF000000
|
||||
39,192,307,331,666,65,,FFFFFFFF
|
||||
40,195,0,0,1280,720,,
|
||||
40,196,0,0,1280,720,,FF000000
|
||||
40,197,307,331,666,65,,FFFFFFFF
|
||||
41,200,0,0,1280,720,,
|
||||
41,201,0,0,1280,720,,FF000000
|
||||
41,202,307,331,666,65,,FFFFFFFF
|
||||
42,205,0,0,1280,720,,
|
||||
42,206,0,0,1280,720,,FF000000
|
||||
42,207,307,331,666,65,,FFFFFFFF
|
||||
43,210,0,0,1280,720,,
|
||||
43,211,0,0,1280,720,,FF000000
|
||||
43,212,307,331,666,65,,FFFFFFFF
|
||||
44,215,0,0,1280,720,,
|
||||
44,216,0,0,1280,720,,FF000000
|
||||
44,217,307,331,666,65,,FFFFFFFF
|
||||
45,220,0,0,1280,720,,
|
||||
45,221,0,0,1280,720,,FF000000
|
||||
45,222,307,331,666,65,,FFFFFFFF
|
||||
47,225,0,0,1280,720,,
|
||||
47,226,0,0,1280,720,,FF000000
|
||||
47,227,307,331,666,65,,FFFFFFFF
|
||||
48,230,0,0,1280,720,,
|
||||
48,231,0,0,1280,720,,FF000000
|
||||
48,232,307,331,666,65,,FFFFFFFF
|
||||
49,235,0,0,1280,720,,
|
||||
49,236,0,0,1280,720,,FF000000
|
||||
49,237,307,331,666,65,,FFFFFFFF
|
||||
50,240,0,0,1280,720,,
|
||||
50,241,0,0,1280,720,,FF000000
|
||||
50,242,307,331,666,65,,FFFFFFFF
|
||||
51,245,0,0,1280,720,,
|
||||
51,246,0,0,1280,720,,FF000000
|
||||
51,247,307,331,666,65,,FFFFFFFF
|
||||
52,250,0,0,1280,720,,
|
||||
52,251,0,0,1280,720,,FF000000
|
||||
52,252,307,331,666,65,,FFFFFFFF
|
||||
53,255,0,0,1280,720,,
|
||||
53,256,0,0,1280,720,,FF000000
|
||||
53,257,307,331,666,65,,FFFFFFFF
|
||||
54,260,0,0,1280,720,,
|
||||
54,261,0,0,1280,720,,FF000000
|
||||
54,262,307,331,666,65,,FFFFFFFF
|
||||
55,265,0,0,1280,720,,
|
||||
55,266,0,0,1280,720,,FF000000
|
||||
55,267,307,331,666,65,,FFFFFFFF
|
||||
56,270,0,0,1280,720,,
|
||||
56,271,0,0,1280,720,,FF000000
|
||||
56,272,307,331,666,65,,FFFFFFFF
|
||||
57,275,0,0,1280,720,,
|
||||
57,276,0,0,1280,720,,FF000000
|
||||
57,277,307,331,666,65,,FFFFFFFF
|
||||
58,280,0,0,1280,720,,
|
||||
58,281,0,0,1280,720,,FF000000
|
||||
58,282,307,331,666,65,,FFFFFFFF
|
||||
59,285,0,0,1280,720,,
|
||||
59,286,0,0,1280,720,,FF000000
|
||||
59,287,307,331,666,65,,FFFFFFFF
|
||||
60,290,0,0,1280,720,,
|
||||
60,291,0,0,1280,720,,FF000000
|
||||
60,292,307,331,666,65,,FFFFFFFF
|
||||
61,295,0,0,1280,720,,
|
||||
61,296,0,0,1280,720,,FF000000
|
||||
61,297,307,331,666,65,,FFFFFFFF
|
||||
62,300,0,0,1280,720,,
|
||||
62,301,0,0,1280,720,,FF000000
|
||||
62,302,307,331,666,65,,FFFFFFFF
|
||||
63,305,0,0,1280,720,,
|
||||
63,306,0,0,1280,720,,FF000000
|
||||
63,307,307,331,666,65,,FFFFFFFF
|
||||
64,310,0,0,1280,720,,
|
||||
64,311,0,0,1280,720,,FF000000
|
||||
64,312,307,331,666,65,,FFFFFFFF
|
||||
65,315,0,0,1280,720,,
|
||||
65,316,0,0,1280,720,,FF000000
|
||||
65,317,307,331,666,65,,FFFFFFFF
|
||||
66,320,0,0,1280,720,,
|
||||
66,321,0,0,1280,720,,FF000000
|
||||
66,322,307,331,666,65,,FFFFFFFF
|
||||
67,325,0,0,1280,720,,
|
||||
67,326,0,0,1280,720,,FF000000
|
||||
67,327,307,331,666,65,,FFFFFFFF
|
||||
68,330,0,0,1280,720,,
|
||||
68,331,0,0,1280,720,,FF000000
|
||||
68,332,307,331,666,65,,FFFFFFFF
|
||||
69,335,0,0,1280,720,,
|
||||
69,336,0,0,1280,720,,FF000000
|
||||
69,337,307,331,666,65,,FFFFFFFF
|
||||
70,340,0,0,1280,720,,
|
||||
70,341,0,0,1280,720,,FF000000
|
||||
70,342,307,331,666,65,,FFFFFFFF
|
||||
71,345,0,0,1280,720,,
|
||||
71,346,0,0,1280,720,,FF000000
|
||||
71,347,307,331,666,65,,FFFFFFFF
|
||||
72,350,0,0,1280,720,,
|
||||
72,351,0,0,1280,720,,FF000000
|
||||
72,352,307,331,666,65,,FFFFFFFF
|
||||
73,355,0,0,1280,720,,
|
||||
73,356,0,0,1280,720,,FF000000
|
||||
73,357,307,331,666,65,,FFFFFFFF
|
||||
74,360,0,0,1280,720,,
|
||||
74,361,0,0,1280,720,,FF000000
|
||||
74,362,307,331,666,65,,FFFFFFFF
|
||||
75,365,0,0,1280,720,,
|
||||
75,366,0,0,1280,720,,FF000000
|
||||
75,367,307,331,666,65,,FFFFFFFF
|
||||
76,370,0,0,1280,720,,
|
||||
76,371,0,0,1280,720,,FF000000
|
||||
76,372,307,331,666,65,,FFFFFFFF
|
||||
77,375,0,0,1280,720,,
|
||||
77,376,0,0,1280,720,,FF000000
|
||||
77,377,307,331,666,65,,FFFFFFFF
|
||||
78,380,0,0,1280,720,,
|
||||
78,381,0,0,1280,720,,FF000000
|
||||
78,382,307,331,666,65,,FEFFFFFF
|
||||
79,385,0,0,1280,720,,
|
||||
79,386,0,0,1280,720,,FF000000
|
||||
79,387,307,331,666,65,,F9FFFFFF
|
||||
80,390,0,0,1280,720,,
|
||||
80,391,0,0,1280,720,,FF000000
|
||||
80,392,307,331,666,65,,E7FFFFFF
|
||||
81,395,0,0,1280,720,,
|
||||
81,396,0,0,1280,720,,FF000000
|
||||
81,397,307,331,666,65,,D7FFFFFF
|
||||
82,400,0,0,1280,720,,
|
||||
82,401,0,0,1280,720,,FF000000
|
||||
82,402,307,331,666,65,,B5FFFFFF
|
||||
83,405,0,0,1280,720,,
|
||||
83,406,0,0,1280,720,,FF000000
|
||||
83,407,307,331,666,65,,A5FFFFFF
|
||||
84,410,0,0,1280,720,,
|
||||
84,411,0,0,1280,720,,FF000000
|
||||
84,412,307,331,666,65,,83FFFFFF
|
||||
85,415,0,0,1280,720,,
|
||||
85,416,0,0,1280,720,,FF000000
|
||||
85,417,307,331,666,65,,73FFFFFF
|
||||
86,420,0,0,1280,720,,
|
||||
86,421,0,0,1280,720,,FF000000
|
||||
86,422,307,331,666,65,,51FFFFFF
|
||||
87,425,0,0,1280,720,,
|
||||
87,426,0,0,1280,720,,FF000000
|
||||
87,427,307,331,666,65,,41FFFFFF
|
||||
88,430,0,0,1280,720,,
|
||||
88,431,0,0,1280,720,,FF000000
|
||||
88,432,307,331,666,65,,1FFFFFFF
|
||||
89,435,0,0,1280,720,,
|
||||
89,436,0,0,1280,720,,FF000000
|
||||
89,437,307,331,666,65,,0FFFFFFF
|
||||
90,440,0,0,1280,720,,
|
||||
90,441,0,0,1280,720,,FF000000
|
||||
90,442,307,331,666,65,,07FFFFFF
|
||||
91,445,0,0,1280,720,,
|
||||
91,446,0,0,1280,720,,FF000000
|
||||
92,449,0,0,1280,720,,
|
||||
92,450,0,0,1280,720,,FF000000
|
||||
93,453,0,0,1280,720,,
|
||||
93,454,0,0,1280,720,,FF000000
|
||||
94,457,0,0,1280,720,,
|
||||
94,458,0,0,1280,720,,FF000000
|
||||
94,459,378,155,525,90,,22FFFFFF
|
||||
94,459,512,306,262,108,,22FFFFFF
|
||||
95,462,0,0,1280,720,,
|
||||
95,463,0,0,1280,720,,FF000000
|
||||
95,464,378,155,525,90,,44FFFFFF
|
||||
95,464,512,306,262,108,,44FFFFFF
|
||||
96,467,0,0,1280,720,,
|
||||
96,468,0,0,1280,720,,FF000000
|
||||
96,469,378,155,525,90,,66FFFFFF
|
||||
96,469,512,306,262,108,,66FFFFFF
|
||||
97,472,0,0,1280,720,,
|
||||
97,473,0,0,1280,720,,FF000000
|
||||
97,474,378,155,525,90,,88FFFFFF
|
||||
97,474,512,306,262,108,,88FFFFFF
|
||||
98,477,0,0,1280,720,,
|
||||
98,478,0,0,1280,720,,FF000000
|
||||
98,479,378,155,525,90,,AAFFFFFF
|
||||
98,479,512,306,262,108,,AAFFFFFF
|
||||
99,482,0,0,1280,720,,
|
||||
99,483,0,0,1280,720,,FF000000
|
||||
99,484,378,155,525,90,,CCFFFFFF
|
||||
99,484,512,306,262,108,,CCFFFFFF
|
||||
100,487,0,0,1280,720,,
|
||||
100,488,0,0,1280,720,,FF000000
|
||||
100,489,378,155,525,90,,EEFFFFFF
|
||||
100,489,512,306,262,108,,EEFFFFFF
|
||||
101,492,0,0,1280,720,,
|
||||
101,493,0,0,1280,720,,FF000000
|
||||
101,494,378,155,525,90,,FFFFFFFF
|
||||
101,494,512,306,262,108,,FFFFFFFF
|
||||
102,497,0,0,1280,720,,
|
||||
102,498,0,0,1280,720,,FF000000
|
||||
102,499,378,155,525,90,,FFFFFFFF
|
||||
102,499,512,306,262,108,,FFFFFFFF
|
||||
103,502,0,0,1280,720,,
|
||||
103,503,0,0,1280,720,,FF000000
|
||||
103,504,378,155,525,90,,FFFFFFFF
|
||||
103,504,512,306,262,108,,FFFFFFFF
|
||||
104,507,0,0,1280,720,,
|
||||
104,508,0,0,1280,720,,FF000000
|
||||
104,509,378,155,525,90,,FFFFFFFF
|
||||
104,509,512,306,262,108,,FFFFFFFF
|
||||
105,512,0,0,1280,720,,
|
||||
105,513,0,0,1280,720,,FF000000
|
||||
105,514,378,155,525,90,,FFFFFFFF
|
||||
105,514,512,306,262,108,,FFFFFFFF
|
||||
106,517,0,0,1280,720,,
|
||||
106,518,0,0,1280,720,,FF000000
|
||||
106,519,378,155,525,90,,FFFFFFFF
|
||||
106,519,512,306,262,108,,FFFFFFFF
|
||||
107,522,0,0,1280,720,,
|
||||
107,523,0,0,1280,720,,FF000000
|
||||
107,524,378,155,525,90,,FFFFFFFF
|
||||
107,524,512,306,262,108,,FFFFFFFF
|
||||
108,527,0,0,1280,720,,
|
||||
108,528,0,0,1280,720,,FF000000
|
||||
108,529,378,155,525,90,,FEFFFFFF
|
||||
108,529,512,306,262,108,,FEFFFFFF
|
||||
109,532,0,0,1280,720,,
|
||||
109,533,0,0,1280,720,,FF000000
|
||||
109,534,378,155,525,90,,DCFFFFFF
|
||||
109,534,512,306,262,108,,DCFFFFFF
|
||||
110,537,0,0,1280,720,,
|
||||
110,538,0,0,1280,720,,FF000000
|
||||
110,539,378,155,525,90,,BAFFFFFF
|
||||
110,539,512,306,262,108,,BAFFFFFF
|
||||
111,542,0,0,1280,720,,
|
||||
111,543,0,0,1280,720,,FF000000
|
||||
111,544,378,155,525,90,,98FFFFFF
|
||||
111,544,512,306,262,108,,98FFFFFF
|
||||
112,547,0,0,1280,720,,
|
||||
112,548,0,0,1280,720,,FF000000
|
||||
112,549,378,155,525,90,,76FFFFFF
|
||||
112,549,512,306,262,108,,76FFFFFF
|
||||
113,552,0,0,1280,720,,
|
||||
113,553,0,0,1280,720,,FF000000
|
||||
113,554,378,155,525,90,,54FFFFFF
|
||||
113,554,512,306,262,108,,54FFFFFF
|
||||
114,557,0,0,1280,720,,
|
||||
114,558,0,0,1280,720,,FF000000
|
||||
114,559,378,155,525,90,,43FFFFFF
|
||||
114,559,512,306,262,108,,43FFFFFF
|
||||
115,562,0,0,1280,720,,
|
||||
115,563,0,0,1280,720,,FF000000
|
||||
115,564,378,155,525,90,,21FFFFFF
|
||||
115,564,512,306,262,108,,21FFFFFF
|
||||
116,567,0,0,1280,720,,
|
||||
116,568,0,0,1280,720,,FF000000
|
||||
116,569,390,162,499,72,,FFFFFFFF
|
||||
116,569,518,317,243,86,,FFFFFFFF
|
||||
117,572,0,0,1280,720,,
|
||||
117,573,0,0,1280,720,,FF000000
|
||||
117,574,390,162,499,72,,FFFFFFFF
|
||||
117,574,518,317,243,86,,FFFFFFFF
|
||||
118,577,0,0,1280,720,,
|
||||
118,578,0,0,1280,720,,FF000000
|
||||
118,579,390,162,499,72,,FFFFFFFF
|
||||
118,579,518,317,243,86,,FFFFFFFF
|
||||
119,582,0,0,1280,720,,
|
||||
119,583,0,0,1280,720,,FF000000
|
||||
119,584,390,162,499,72,,FFFFFFFF
|
||||
119,584,518,317,243,86,,FFFFFFFF
|
||||
120,587,0,0,1280,720,,
|
||||
120,588,0,0,1280,720,,FF000000
|
||||
120,589,390,162,499,72,,FFFFFFFF
|
||||
120,589,518,317,243,86,,FFFFFFFF
|
||||
121,592,0,0,1280,720,,
|
||||
121,593,0,0,1280,720,,FF000000
|
||||
121,594,390,162,499,72,,FFFFFFFF
|
||||
121,594,518,317,243,86,,FFFFFFFF
|
||||
122,597,0,0,1280,720,,
|
||||
122,598,0,0,1280,720,,FF000000
|
||||
122,599,390,162,499,72,,FFFFFFFF
|
||||
122,599,518,317,243,86,,FFFFFFFF
|
||||
123,602,0,0,1280,720,,
|
||||
123,603,0,0,1280,720,,FF000000
|
||||
123,604,390,162,499,72,,FFFFFFFF
|
||||
123,604,518,317,243,86,,FFFFFFFF
|
||||
124,607,0,0,1280,720,,
|
||||
124,608,0,0,1280,720,,FF000000
|
||||
124,609,390,162,499,72,,FFFFFFFF
|
||||
124,609,518,317,243,86,,FFFFFFFF
|
||||
125,612,0,0,1280,720,,
|
||||
125,613,0,0,1280,720,,FF000000
|
||||
125,614,390,162,499,72,,FFFFFFFF
|
||||
125,614,518,317,243,86,,FFFFFFFF
|
||||
126,617,0,0,1280,720,,
|
||||
126,618,0,0,1280,720,,FF000000
|
||||
126,619,390,162,499,72,,FFFFFFFF
|
||||
126,619,518,317,243,86,,FFFFFFFF
|
||||
127,622,0,0,1280,720,,
|
||||
127,623,0,0,1280,720,,FF000000
|
||||
127,624,390,162,499,72,,FFFFFFFF
|
||||
127,624,518,317,243,86,,FFFFFFFF
|
||||
128,627,0,0,1280,720,,
|
||||
128,628,0,0,1280,720,,FF000000
|
||||
128,629,390,162,499,72,,FFFFFFFF
|
||||
128,629,518,317,243,86,,FFFFFFFF
|
||||
129,632,0,0,1280,720,,
|
||||
129,633,0,0,1280,720,,FF000000
|
||||
129,634,390,162,499,72,,FFFFFFFF
|
||||
129,634,518,317,243,86,,FFFFFFFF
|
||||
130,637,0,0,1280,720,,
|
||||
130,638,0,0,1280,720,,FF000000
|
||||
130,639,390,162,499,72,,FFFFFFFF
|
||||
130,639,518,317,243,86,,FFFFFFFF
|
||||
131,642,0,0,1280,720,,
|
||||
131,643,0,0,1280,720,,FF000000
|
||||
131,644,390,162,499,72,,FFFFFFFF
|
||||
131,644,518,317,243,86,,FFFFFFFF
|
||||
132,647,0,0,1280,720,,
|
||||
132,648,0,0,1280,720,,FF000000
|
||||
132,649,390,162,499,72,,FFFFFFFF
|
||||
132,649,518,317,243,86,,FFFFFFFF
|
||||
133,652,0,0,1280,720,,
|
||||
133,653,0,0,1280,720,,FF000000
|
||||
133,654,390,162,499,72,,FFFFFFFF
|
||||
133,654,518,317,243,86,,FFFFFFFF
|
||||
134,657,0,0,1280,720,,
|
||||
134,658,0,0,1280,720,,FF000000
|
||||
134,659,390,162,499,72,,FFFFFFFF
|
||||
134,659,518,317,243,86,,FFFFFFFF
|
||||
135,662,0,0,1280,720,,
|
||||
135,663,0,0,1280,720,,FF000000
|
||||
135,664,390,162,499,72,,FFFFFFFF
|
||||
135,664,518,317,243,86,,FFFFFFFF
|
||||
136,667,0,0,1280,720,,
|
||||
136,668,0,0,1280,720,,FF000000
|
||||
136,669,390,162,499,72,,FFFFFFFF
|
||||
136,669,518,317,243,86,,FFFFFFFF
|
||||
137,672,0,0,1280,720,,
|
||||
137,673,0,0,1280,720,,FF000000
|
||||
137,674,390,162,499,72,,FFFFFFFF
|
||||
137,674,518,317,243,86,,FFFFFFFF
|
||||
138,677,0,0,1280,720,,
|
||||
138,678,0,0,1280,720,,FF000000
|
||||
138,679,390,162,499,72,,FFFFFFFF
|
||||
138,679,518,317,243,86,,FFFFFFFF
|
||||
140,682,0,0,1280,720,,
|
||||
140,683,0,0,1280,720,,FF000000
|
||||
140,684,390,162,499,72,,FFFFFFFF
|
||||
140,684,518,317,243,86,,FFFFFFFF
|
||||
141,687,0,0,1280,720,,
|
||||
141,688,0,0,1280,720,,FF000000
|
||||
141,689,390,162,499,72,,FFFFFFFF
|
||||
141,689,518,317,243,86,,FFFFFFFF
|
||||
142,692,0,0,1280,720,,
|
||||
142,693,0,0,1280,720,,FF000000
|
||||
142,694,390,162,499,72,,FFFFFFFF
|
||||
142,694,518,317,243,86,,FFFFFFFF
|
||||
143,697,0,0,1280,720,,
|
||||
143,698,0,0,1280,720,,FF000000
|
||||
143,699,390,162,499,72,,FFFFFFFF
|
||||
143,699,518,317,243,86,,FFFFFFFF
|
||||
144,702,0,0,1280,720,,
|
||||
144,703,0,0,1280,720,,FF000000
|
||||
144,704,390,162,499,72,,FFFFFFFF
|
||||
144,704,518,317,243,86,,FFFFFFFF
|
||||
145,707,0,0,1280,720,,
|
||||
145,708,0,0,1280,720,,FF000000
|
||||
145,709,390,162,499,72,,FFFFFFFF
|
||||
145,709,518,317,243,86,,FFFFFFFF
|
||||
146,712,0,0,1280,720,,
|
||||
146,713,0,0,1280,720,,FF000000
|
||||
146,714,390,162,499,72,,FFFFFFFF
|
||||
146,714,518,317,243,86,,FFFFFFFF
|
||||
147,717,0,0,1280,720,,
|
||||
147,718,0,0,1280,720,,FF000000
|
||||
147,719,390,162,499,72,,FFFFFFFF
|
||||
147,719,518,317,243,86,,FFFFFFFF
|
||||
148,722,0,0,1280,720,,
|
||||
148,723,0,0,1280,720,,FF000000
|
||||
148,724,390,162,499,72,,FFFFFFFF
|
||||
148,724,518,317,243,86,,FFFFFFFF
|
||||
149,727,0,0,1280,720,,
|
||||
149,728,0,0,1280,720,,FF000000
|
||||
149,729,390,162,499,72,,FFFFFFFF
|
||||
149,729,518,317,243,86,,FFFFFFFF
|
||||
150,732,0,0,1280,720,,
|
||||
150,733,0,0,1280,720,,FF000000
|
||||
150,734,390,162,499,72,,FFFFFFFF
|
||||
150,734,518,317,243,86,,FFFFFFFF
|
||||
151,737,0,0,1280,720,,
|
||||
151,738,0,0,1280,720,,FF000000
|
||||
151,739,390,162,499,72,,FFFFFFFF
|
||||
151,739,518,317,243,86,,FFFFFFFF
|
||||
152,742,0,0,1280,720,,
|
||||
152,743,0,0,1280,720,,FF000000
|
||||
152,744,390,162,499,72,,FFFFFFFF
|
||||
152,744,518,317,243,86,,FFFFFFFF
|
||||
153,747,0,0,1280,720,,
|
||||
153,748,0,0,1280,720,,FF000000
|
||||
153,749,390,162,499,72,,FFFFFFFF
|
||||
153,749,518,317,243,86,,FFFFFFFF
|
||||
154,752,0,0,1280,720,,
|
||||
154,753,0,0,1280,720,,FF000000
|
||||
154,754,390,162,499,72,,FFFFFFFF
|
||||
154,754,518,317,243,86,,FFFFFFFF
|
||||
155,757,0,0,1280,720,,
|
||||
155,758,0,0,1280,720,,FF000000
|
||||
155,759,390,162,499,72,,FFFFFFFF
|
||||
155,759,518,317,243,86,,FFFFFFFF
|
||||
156,762,0,0,1280,720,,
|
||||
156,763,0,0,1280,720,,FF000000
|
||||
156,764,390,162,499,72,,FFFFFFFF
|
||||
156,764,518,317,243,86,,FFFFFFFF
|
||||
157,767,0,0,1280,720,,
|
||||
157,768,0,0,1280,720,,FF000000
|
||||
157,769,390,162,499,72,,FFFFFFFF
|
||||
157,769,518,317,243,86,,FFFFFFFF
|
||||
158,772,0,0,1280,720,,
|
||||
158,773,0,0,1280,720,,FF000000
|
||||
158,774,390,162,499,72,,FFFFFFFF
|
||||
158,774,518,317,243,86,,FFFFFFFF
|
||||
159,777,0,0,1280,720,,
|
||||
159,778,0,0,1280,720,,FF000000
|
||||
159,779,390,162,499,72,,FFFFFFFF
|
||||
159,779,518,317,243,86,,FFFFFFFF
|
||||
160,782,0,0,1280,720,,
|
||||
160,783,0,0,1280,720,,FF000000
|
||||
160,784,390,162,499,72,,FFFFFFFF
|
||||
160,784,518,317,243,86,,FFFFFFFF
|
||||
161,787,0,0,1280,720,,
|
||||
161,788,0,0,1280,720,,FF000000
|
||||
161,789,390,162,499,72,,FFFFFFFF
|
||||
161,789,518,317,243,86,,FFFFFFFF
|
||||
162,792,0,0,1280,720,,
|
||||
162,793,0,0,1280,720,,FF000000
|
||||
162,794,390,162,499,72,,FFFFFFFF
|
||||
162,794,518,317,243,86,,FFFFFFFF
|
||||
163,797,0,0,1280,720,,
|
||||
163,798,0,0,1280,720,,FF000000
|
||||
163,799,390,162,499,72,,FFFFFFFF
|
||||
163,799,518,317,243,86,,FFFFFFFF
|
||||
164,802,0,0,1280,720,,
|
||||
164,803,0,0,1280,720,,FF000000
|
||||
164,804,390,162,499,72,,FFFFFFFF
|
||||
164,804,518,317,243,86,,FFFFFFFF
|
||||
165,807,0,0,1280,720,,
|
||||
165,808,0,0,1280,720,,FF000000
|
||||
165,809,390,162,499,72,,FFFFFFFF
|
||||
165,809,518,317,243,86,,FFFFFFFF
|
||||
166,812,0,0,1280,720,,
|
||||
166,813,0,0,1280,720,,FF000000
|
||||
166,814,390,162,499,72,,FFFFFFFF
|
||||
166,814,518,317,243,86,,FFFFFFFF
|
||||
167,817,0,0,1280,720,,
|
||||
167,818,0,0,1280,720,,FF000000
|
||||
167,819,390,162,499,72,,FFFFFFFF
|
||||
167,819,518,317,243,86,,FFFFFFFF
|
||||
168,822,0,0,1280,720,,
|
||||
168,823,0,0,1280,720,,FF000000
|
||||
168,824,390,162,499,72,,FFFFFFFF
|
||||
168,824,518,317,243,86,,FFFFFFFF
|
||||
169,827,0,0,1280,720,,
|
||||
169,828,0,0,1280,720,,FF000000
|
||||
169,829,390,162,499,72,,FFFFFFFF
|
||||
169,829,518,317,243,86,,FFFFFFFF
|
||||
170,832,0,0,1280,720,,
|
||||
170,833,0,0,1280,720,,FF000000
|
||||
170,834,390,162,499,72,,FFFFFFFF
|
||||
170,834,518,317,243,86,,FFFFFFFF
|
||||
171,837,0,0,1280,720,,
|
||||
171,838,0,0,1280,720,,FF000000
|
||||
171,839,390,162,499,72,,FFFFFFFF
|
||||
171,839,518,317,243,86,,FFFFFFFF
|
||||
172,842,0,0,1280,720,,
|
||||
172,843,0,0,1280,720,,FF000000
|
||||
172,844,390,162,499,72,,FFFFFFFF
|
||||
172,844,518,317,243,86,,FFFFFFFF
|
||||
173,847,0,0,1280,720,,
|
||||
173,848,0,0,1280,720,,FF000000
|
||||
173,849,390,162,499,72,,FFFFFFFF
|
||||
173,849,518,317,243,86,,FFFFFFFF
|
||||
174,852,0,0,1280,720,,
|
||||
174,853,0,0,1280,720,,FF000000
|
||||
174,854,390,162,499,72,,FFFFFFFF
|
||||
174,854,518,317,243,86,,FFFFFFFF
|
||||
175,857,0,0,1280,720,,
|
||||
175,858,0,0,1280,720,,FF000000
|
||||
175,859,390,162,499,72,,FFFFFFFF
|
||||
175,859,518,317,243,86,,FFFFFFFF
|
||||
176,862,0,0,1280,720,,
|
||||
176,863,0,0,1280,720,,FF000000
|
||||
176,864,390,162,499,72,,FFFFFFFF
|
||||
176,864,518,317,243,86,,FFFFFFFF
|
||||
177,867,0,0,1280,720,,
|
||||
177,868,0,0,1280,720,,FF000000
|
||||
177,869,390,162,499,72,,FFFFFFFF
|
||||
177,869,518,317,243,86,,FFFFFFFF
|
||||
178,872,0,0,1280,720,,
|
||||
178,873,0,0,1280,720,,FF000000
|
||||
178,874,390,162,499,72,,FFFFFFFF
|
||||
178,874,518,317,243,86,,FFFFFFFF
|
||||
179,877,0,0,1280,720,,
|
||||
179,878,0,0,1280,720,,FF000000
|
||||
179,879,390,162,499,72,,FFFFFFFF
|
||||
179,879,518,317,243,86,,FFFFFFFF
|
||||
180,882,0,0,1280,720,,
|
||||
180,883,0,0,1280,720,,FF000000
|
||||
180,884,390,162,499,72,,FFFFFFFF
|
||||
180,884,518,317,243,86,,FFFFFFFF
|
||||
181,887,0,0,1280,720,,
|
||||
181,888,0,0,1280,720,,FF000000
|
||||
181,889,390,162,499,72,,FFFFFFFF
|
||||
181,889,518,317,243,86,,FFFFFFFF
|
||||
182,892,0,0,1280,720,,
|
||||
182,893,0,0,1280,720,,FF000000
|
||||
182,894,390,162,499,72,,FFFFFFFF
|
||||
182,894,518,317,243,86,,FFFFFFFF
|
||||
183,897,0,0,1280,720,,
|
||||
183,898,0,0,1280,720,,FF000000
|
||||
183,899,390,162,499,72,,FFFFFFFF
|
||||
183,899,518,317,243,86,,FFFFFFFF
|
||||
184,902,0,0,1280,720,,
|
||||
184,903,0,0,1280,720,,FF000000
|
||||
184,904,390,162,499,72,,FFFFFFFF
|
||||
184,904,518,317,243,86,,FFFFFFFF
|
||||
185,907,0,0,1280,720,,
|
||||
185,908,0,0,1280,720,,FF000000
|
||||
185,909,390,162,499,72,,FFFFFFFF
|
||||
185,909,518,317,243,86,,FFFFFFFF
|
||||
186,912,0,0,1280,720,,
|
||||
186,913,0,0,1280,720,,FF000000
|
||||
186,914,390,162,499,72,,FFFFFFFF
|
||||
186,914,518,317,243,86,,FFFFFFFF
|
||||
187,917,0,0,1280,720,,
|
||||
187,918,0,0,1280,720,,FF000000
|
||||
187,919,390,162,499,72,,FFFFFFFF
|
||||
187,919,518,317,243,86,,FFFFFFFF
|
||||
188,922,0,0,1280,720,,
|
||||
188,923,0,0,1280,720,,FF000000
|
||||
188,924,390,162,499,72,,FFFFFFFF
|
||||
188,924,518,317,243,86,,FFFFFFFF
|
||||
189,927,0,0,1280,720,,
|
||||
189,928,0,0,1280,720,,FF000000
|
||||
189,929,390,162,499,72,,FFFFFFFF
|
||||
189,929,518,317,243,86,,FFFFFFFF
|
||||
190,932,0,0,1280,720,,
|
||||
190,933,0,0,1280,720,,FF000000
|
||||
190,934,390,162,499,72,,FFFFFFFF
|
||||
190,934,518,317,243,86,,FFFFFFFF
|
||||
191,937,0,0,1280,720,,
|
||||
191,938,0,0,1280,720,,FF000000
|
||||
191,939,390,162,499,72,,FFFFFFFF
|
||||
191,939,518,317,243,86,,FFFFFFFF
|
||||
192,942,0,0,1280,720,,
|
||||
192,943,0,0,1280,720,,FF000000
|
||||
192,944,390,162,499,72,,FFFFFFFF
|
||||
192,944,518,317,243,86,,FFFFFFFF
|
||||
193,947,0,0,1280,720,,
|
||||
193,948,0,0,1280,720,,FF000000
|
||||
193,949,390,162,499,72,,FFFFFFFF
|
||||
193,949,518,317,243,86,,FFFFFFFF
|
||||
194,952,0,0,1280,720,,
|
||||
194,953,0,0,1280,720,,FF000000
|
||||
194,954,390,162,499,72,,FFFFFFFF
|
||||
194,954,518,317,243,86,,FFFFFFFF
|
||||
195,957,0,0,1280,720,,
|
||||
195,958,0,0,1280,720,,FF000000
|
||||
195,959,390,162,499,72,,FFFFFFFF
|
||||
195,959,518,317,243,86,,FFFFFFFF
|
||||
196,962,0,0,1280,720,,
|
||||
196,963,0,0,1280,720,,FF000000
|
||||
196,964,390,162,499,72,,FFFFFFFF
|
||||
196,964,518,317,243,86,,FFFFFFFF
|
||||
197,967,0,0,1280,720,,
|
||||
197,968,0,0,1280,720,,FF000000
|
||||
197,969,390,162,499,72,,FFFFFFFF
|
||||
197,969,518,317,243,86,,FFFFFFFF
|
||||
198,972,0,0,1280,720,,
|
||||
198,973,0,0,1280,720,,FF000000
|
||||
198,974,390,162,499,72,,FFFFFFFF
|
||||
198,974,518,317,243,86,,FFFFFFFF
|
||||
199,977,0,0,1280,720,,
|
||||
199,978,0,0,1280,720,,FF000000
|
||||
199,979,390,162,499,72,,FEFFFFFF
|
||||
199,979,518,317,243,86,,FEFFFFFF
|
||||
200,982,0,0,1280,720,,
|
||||
200,983,0,0,1280,720,,FF000000
|
||||
200,984,390,162,499,72,,F3FFFFFF
|
||||
200,984,518,317,243,86,,F3FFFFFF
|
||||
201,987,0,0,1280,720,,
|
||||
201,988,0,0,1280,720,,FF000000
|
||||
201,989,390,162,499,72,,EDFFFFFF
|
||||
201,989,518,317,243,86,,EDFFFFFF
|
||||
202,992,0,0,1280,720,,
|
||||
202,993,0,0,1280,720,,FF000000
|
||||
202,994,390,162,499,72,,D7FFFFFF
|
||||
202,994,518,317,243,86,,D7FFFFFF
|
||||
203,997,0,0,1280,720,,
|
||||
203,998,0,0,1280,720,,FF000000
|
||||
203,999,390,162,499,72,,B5FFFFFF
|
||||
203,999,518,317,243,86,,B5FFFFFF
|
||||
204,1002,0,0,1280,720,,
|
||||
204,1003,0,0,1280,720,,FF000000
|
||||
204,1004,390,162,499,72,,94FFFFFF
|
||||
204,1004,518,317,243,86,,94FFFFFF
|
||||
205,1007,0,0,1280,720,,
|
||||
205,1008,0,0,1280,720,,FF000000
|
||||
205,1009,390,162,499,72,,83FFFFFF
|
||||
205,1009,518,317,243,86,,83FFFFFF
|
||||
206,1012,0,0,1280,720,,
|
||||
206,1013,0,0,1280,720,,FF000000
|
||||
206,1014,390,162,499,72,,62FFFFFF
|
||||
206,1014,518,317,243,86,,62FFFFFF
|
||||
207,1017,0,0,1280,720,,
|
||||
207,1018,0,0,1280,720,,FF000000
|
||||
207,1019,390,162,499,72,,41FFFFFF
|
||||
207,1019,518,317,243,86,,41FFFFFF
|
||||
208,1022,0,0,1280,720,,
|
||||
208,1023,0,0,1280,720,,FF000000
|
||||
208,1024,390,162,499,72,,30FFFFFF
|
||||
208,1024,518,317,243,86,,30FFFFFF
|
||||
209,1027,0,0,1280,720,,
|
||||
209,1028,0,0,1280,720,,FF000000
|
||||
209,1029,390,162,499,72,,17FFFFFF
|
||||
209,1029,518,317,243,86,,17FFFFFF
|
||||
210,1032,0,0,1280,720,,
|
||||
210,1033,0,0,1280,720,,FF000000
|
||||
210,1034,390,162,499,72,,0FFFFFFF
|
||||
210,1034,518,317,243,86,,0FFFFFFF
|
||||
211,1037,0,0,1280,720,,
|
||||
211,1038,0,0,1280,720,,FF000000
|
||||
211,1039,390,162,499,72,,07FFFFFF
|
||||
211,1039,518,317,243,86,,07FFFFFF
|
||||
212,1042,0,0,1280,720,,
|
||||
212,1043,0,0,1280,720,,FF000000
|
||||
213,1046,0,0,1280,720,,
|
||||
213,1047,0,0,1280,720,,FF000000
|
||||
214,1050,0,0,1280,720,,
|
||||
214,1051,0,0,1280,720,,FF000000
|
||||
215,1054,0,0,1280,720,,
|
||||
216,1057,0,0,1280,720,,
|
||||
217,1060,0,0,1280,720,,
|
||||
218,1063,0,0,1280,720,,
|
||||
219,1066,0,0,1280,720,,
|
||||
220,1069,0,0,1280,720,,
|
||||
221,1072,0,0,1280,720,,
|
||||
221,1073,0,0,1280,720,,
|
||||
223,1076,0,0,1280,720,,
|
||||
223,1077,0,0,1280,720,,
|
||||
224,1080,0,0,1280,720,,
|
||||
224,1081,0,0,1280,720,,
|
||||
226,1084,0,0,1280,720,,
|
||||
226,1085,0,0,1280,720,,
|
||||
227,1088,0,0,1280,720,,
|
||||
227,1089,0,0,1280,720,,
|
||||
228,1092,0,0,1280,720,,
|
||||
228,1093,0,0,1280,720,,
|
||||
229,1096,0,0,1280,720,,
|
||||
229,1097,0,0,1280,720,,
|
||||
230,1100,0,0,1280,720,,
|
||||
230,1101,0,0,1280,720,,
|
||||
231,1104,0,0,1280,720,,
|
||||
231,1105,0,0,1280,720,,
|
||||
232,1108,0,0,1280,720,,
|
||||
232,1109,0,0,1280,720,,
|
||||
233,1112,0,0,1280,720,,
|
||||
233,1113,0,0,1280,720,,
|
||||
234,1116,0,0,1280,720,,
|
||||
234,1117,0,0,1280,720,,
|
||||
236,1120,0,0,1280,720,,
|
||||
236,1121,0,0,1280,720,,
|
||||
237,1124,0,0,1280,720,,
|
||||
237,1125,0,0,1280,720,,
|
||||
238,1128,0,0,1280,720,,
|
||||
238,1129,0,0,1280,720,,
|
||||
239,1132,0,0,1280,720,,
|
||||
239,1133,0,0,1280,720,,
|
||||
240,1136,0,0,1280,720,,
|
||||
240,1137,0,0,1280,720,,
|
||||
241,1140,0,0,1280,720,,
|
||||
241,1141,0,0,1280,720,,
|
||||
|
33
docs/re/data/attract-frame-match.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
Frame-signature match of captured frames against five candidate movies.
|
||||
32x18 normalised grayscale, movie frames cropped to the 675/720 the game
|
||||
surface shows, 1 fps sampling, best correlation over the whole movie.
|
||||
|
||||
CONTROL - frames captured during the BOOT movie, known to be ADV.wmv:
|
||||
p2.png -> ADV @116s 0.644 (runner-up S13A 0.438)
|
||||
p3.png -> ADV @121s 0.994 (runner-up S03A 0.650)
|
||||
p4.png -> S13A @ 35s 0.984 (runner-up ADV 0.926)
|
||||
p5.png -> ADV @129s 0.933 (runner-up S13A 0.809)
|
||||
p6.png -> ADV @134s 0.992 (runner-up S13A 0.499)
|
||||
p7.png -> S15B @ 88s 0.000 (runner-up S13A 0.000)
|
||||
p8.png -> S13A @ 65s 0.576 (runner-up ADV 0.534)
|
||||
|
||||
TEST - frames captured during the ATTRACT loop, 5 s apart:
|
||||
attract01.png -> ADV @ 39s 0.990 (runner-up S13A 0.487)
|
||||
attract02.png -> ADV @ 45s 0.961 (runner-up S13A 0.411)
|
||||
attract03.png -> S03A @ 37s 0.731 (runner-up ADV 0.422)
|
||||
attract04.png -> ADV @ 56s 0.955 (runner-up S03A 0.532)
|
||||
attract05.png -> ADV @ 62s 0.934 (runner-up S15B 0.245)
|
||||
attract06.png -> S15B @ 65s 0.885 (runner-up ADV 0.743)
|
||||
attract07.png -> ADV @ 75s 0.961 (runner-up S03A 0.355)
|
||||
attract08.png -> ADV @ 80s 0.938 (runner-up S03A 0.753)
|
||||
attract09.png -> ADV @ 85s 0.974 (runner-up S03A 0.484)
|
||||
attract10.png -> ADV @ 92s 0.892 (runner-up S13A 0.754)
|
||||
attract11.png -> S03A @ 24s 0.606 (runner-up ADV 0.588)
|
||||
attract12.png -> ADV @102s 0.994 (runner-up S03A 0.374)
|
||||
attract13.png -> ADV @108s 0.977 (runner-up S13A 0.496)
|
||||
attract14.png -> ADV @113s 0.836 (runner-up S06B 0.464)
|
||||
attract15.png -> ADV @119s 0.958 (runner-up S15B 0.424)
|
||||
attract16.png -> S13A @ 32s 0.996 (runner-up ADV 0.980)
|
||||
attract17.png -> ADV @131s 0.865 (runner-up S13A 0.769)
|
||||
attract18.png -> ADV @137s 0.993 (runner-up S13A 0.502)
|
||||
attract19.png -> S13A @ 65s 0.551 (runner-up ADV 0.534)
|
||||
35
docs/re/data/bgm-wave-census.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
bank waves per-wave (bytes, bytes/s, Hz, seconds)
|
||||
BGM_001.slb 2 4466688 25697 48000 173.82s | 4673536 26887 48000 173.82s EQUAL-LENGTH
|
||||
BGM_002.slb 2 4382720 26376 48000 166.16s | 4667392 28090 48000 166.16s EQUAL-LENGTH
|
||||
BGM_003.slb 2 3555328 26201 48000 135.69s | 3414016 25159 48000 135.70s EQUAL-LENGTH
|
||||
BGM_004.slb 2 6311936 32874 48000 192.00s | 5787648 30143 48000 192.01s EQUAL-LENGTH
|
||||
BGM_005.slb 2 4005888 28013 48000 143.00s | 3917824 27397 48000 143.00s EQUAL-LENGTH
|
||||
BGM_006.slb 2 3946496 26377 48000 149.62s | 3915776 26172 48000 149.62s EQUAL-LENGTH
|
||||
BGM_007.slb 2 7565312 29269 48000 258.48s | 8099840 31337 48000 258.48s EQUAL-LENGTH
|
||||
BGM_008.slb 2 3258368 29947 48000 108.80s | 3397632 31227 48000 108.80s EQUAL-LENGTH
|
||||
BGM_009.slb 2 3819520 25636 48000 148.99s | 3614720 24262 48000 148.99s EQUAL-LENGTH
|
||||
BGM_010.slb 2 4190208 28457 48000 147.25s | 4100096 27845 48000 147.25s EQUAL-LENGTH
|
||||
BGM_011.slb 2 4222976 25232 48000 167.37s | 4311040 25758 48000 167.37s EQUAL-LENGTH
|
||||
BGM_012.slb 2 5562368 31605 48000 176.00s | 5404672 30709 48000 176.00s EQUAL-LENGTH
|
||||
BGM_013.slb 2 6539264 29904 48000 218.68s | 6457344 29530 48000 218.67s EQUAL-LENGTH
|
||||
BGM_014.slb 2 6656000 31438 48000 211.72s | 6948864 32821 48000 211.72s EQUAL-LENGTH
|
||||
BGM_015.slb 2 8017920 29230 48000 274.30s | 7974912 29074 48000 274.30s EQUAL-LENGTH
|
||||
BGM_016.slb 2 3549184 28341 48000 125.23s | 2775040 22159 48000 125.23s EQUAL-LENGTH
|
||||
BGM_017.slb 2 5353472 30690 48000 174.44s | 5062656 29023 48000 174.44s EQUAL-LENGTH
|
||||
BGM_018.slb 2 3620864 26878 48000 134.71s | 2920448 21679 48000 134.71s EQUAL-LENGTH
|
||||
BGM_019.slb 2 6834176 30665 48000 222.87s | 6713344 30123 48000 222.86s EQUAL-LENGTH
|
||||
BGM_020.slb 2 4804608 29592 44100 162.36s | 4675584 28798 44100 162.36s EQUAL-LENGTH
|
||||
BGM_021.slb 2 7780352 28034 44100 277.53s | 7694336 27724 44100 277.53s EQUAL-LENGTH
|
||||
BGM_022.slb 2 5545984 28006 44100 198.03s | 5867520 29630 44100 198.03s EQUAL-LENGTH
|
||||
BGM_023.slb 2 4962304 28001 44100 177.22s | 5044224 28463 44100 177.22s EQUAL-LENGTH
|
||||
BGM_101.slb 2 4800512 27481 48000 174.68s | 2635776 15089 48000 174.68s EQUAL-LENGTH
|
||||
BGM_102.slb 2 1150976 30703 48000 37.49s | 1269760 33872 48000 37.49s EQUAL-LENGTH
|
||||
BGM_103.slb 2 3876864 44181 48000 87.75s | 3930112 44788 48000 87.75s EQUAL-LENGTH
|
||||
BGM_104.slb 2 2672640 31503 48000 84.84s | 2674688 31527 48000 84.84s EQUAL-LENGTH
|
||||
BGM_105.slb 2 3581952 35879 48000 99.83s | 3850240 38566 48000 99.84s EQUAL-LENGTH
|
||||
BGM_106.slb 2 4098048 45389 48000 90.29s | 2383872 36575 48000 65.18s
|
||||
BGM_107.slb 2 2418688 37109 48000 65.18s | 3696640 28676 48000 128.91s
|
||||
BGM_108.slb 2 3696640 28676 48000 128.91s | 2887680 35432 48000 81.50s
|
||||
BGM_109.slb 4 2932736 35985 48000 81.50s | 112640 13377 48000 8.42s | 225280 14228 48000 15.83s | 235520 14795 48000 15.92s
|
||||
|
||||
32 BGM banks; 28 are exactly two waves of equal duration
|
||||
10
docs/re/data/doc-link-audit.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
# tools/re-capture/doc_link_check.py -- 2026-08-29, after the repair pass
|
||||
|
||||
1050 link(s) resolve
|
||||
|
||||
5 MISSING target(s):
|
||||
docs/re/autopilot-knowledge-sources.md -> ../../MEMORY.md
|
||||
docs/re/challenge-mission-gate.md -> ../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h
|
||||
docs/re/entities-live-roster.md -> ../../MEMORY.md
|
||||
docs/re/mission-freeze-resume-spin.md -> canary-build-verified-env-confound.md
|
||||
docs/re/stage-drift-is-navigation-not-save.md -> structures/weapon-datasheet-runtime.md
|
||||
7
docs/re/data/eff-bit-alpha-test.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
matched 170 decoded textures to a T8aD flag word
|
||||
bit SET n= 61 mean %(RGB>A) 55.52 median 52.52
|
||||
bit clear n=109 mean %(RGB>A) 33.66 median 30.17
|
||||
|
||||
premultiplied would require ~0% for the flagged group -> REFUTED
|
||||
best single-threshold accuracy: 76.5% at %(RGB>A) > 83.5
|
||||
(base rate, always-guess-majority: 64.1%)
|
||||
24
docs/re/data/eff-bit-census.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
# tools/re-capture/eff_bit_census.py over dat/GP_*.pak -- 2026-08-29
|
||||
|
||||
sprites with a resolvable preceding name: 14709
|
||||
bit SET & name has 'eff' : 2338
|
||||
bit SET & name lacks 'eff': 2657 <-- counterexamples to 'set => eff'
|
||||
bit clear & name has 'eff' : 1399
|
||||
bit clear & name lacks 'eff': 8315
|
||||
|
||||
P(name has 'eff' | bit set) = 0.468
|
||||
P(name has 'eff' | bit clear) = 0.144
|
||||
|
||||
counterexamples (bit set, no 'eff'):
|
||||
('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126')
|
||||
('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27')
|
||||
('GP_BUNK.pak', 'px_top_extra.t32', '00008832', '220x126')
|
||||
('GP_BUNK.pak', 'px_bunk_line.t32', '00008832', '663x27')
|
||||
('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101')
|
||||
('GP_BUNK.pak', 'pv_loading_ring0.t32', '00000832', '142x142')
|
||||
('GP_BUNK.pak', 'pv_loading_light0.t32', '00000832', '29x25')
|
||||
('GP_BUNK.pak', 'pv_loading_light1.t32', '00000832', '30x29')
|
||||
('GP_BUNK.pak', 'pv_loading_light2.t32', '00000832', '29x30')
|
||||
('GP_BUNK.pak', 'pv_loading_light3.t32', '00000832', '25x29')
|
||||
('GP_BUNK.pak', 'pv_loading_ring1.t32', '00000832', '72x72')
|
||||
('GP_BUNK.pak', 'pv_loading_line.t32', '00000832', '181x101')
|
||||
2
docs/re/data/eightax-detail-test.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
main menu: corr +0.0475 controls +0.0032 (shift) -0.0075 (flip) ceiling 0.070 -> 68% of it
|
||||
title: corr +0.0634 controls +0.0095 (shift) +0.0086 (flip) ceiling 0.094 -> 68% of it
|
||||
29
docs/re/data/gamepart-class-ids.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
GamePart ids bound to a C++ class by the executable's own registration strings
|
||||
(24 strings, 24 distinct ids)
|
||||
|
||||
0 GamePart_Title
|
||||
3 GamePart_SaveLoad
|
||||
4 GamePart_SaveLoad
|
||||
5 GamePart_Extras
|
||||
6 GamePart_MovieTheater
|
||||
7 GamePart_MissionSelect
|
||||
8 GamePart_Options
|
||||
9 GamePart_Movie
|
||||
10 GamePart_Bunk
|
||||
11 GamePart_ReadyRoom
|
||||
12 GamePart_Hangar
|
||||
13 GamePart_Arsenal
|
||||
14 GamePart_PilotLog
|
||||
15 GamePart_System
|
||||
17 GamePart_MainGame
|
||||
19 GamePart_PauseMenu
|
||||
20 GamePart_StageClear
|
||||
21 GamePart_MissionLog
|
||||
22 GamePart_GameOver
|
||||
23 GamePart_Debriefing
|
||||
24 GamePart_Dialog
|
||||
25 GamePart_Tutorial
|
||||
26 GamePart_ChallengeMission
|
||||
27 GamePart_Leaderboard
|
||||
|
||||
ids 0..28 with NO registration string: [1, 2, 16, 18, 28]
|
||||
9
docs/re/data/gamma-call-evidence.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# canary --log_mask=12 --log_level=3 (Kernel logging ON), 2026-08-29
|
||||
# control: the same log carries 359 VdRetrainEDRAM and 358 VdGetSystemCommandBuffer lines,
|
||||
# so a missing call would have been visible.
|
||||
|
||||
F 820006F4 8284E27C 1B9 ( 441) VdGetCurrentDisplayGamma
|
||||
F 8200070C 8284E2BC 1BA ( 442) VdGetCurrentDisplayInformation
|
||||
F 82000710 8284E2CC 1D3 ( 467) VdSetDisplayMode
|
||||
d> F8000008 VdGetSystemCommandBuffer(701CF830, 701CF804)
|
||||
d> F8000008 VdGetCurrentDisplayGamma(701CE1F8(00000000), 701CE1F0(0))
|
||||
7549
docs/re/data/idxd-unnamed-field-keys.txt
Normal file
909
docs/re/data/isl-builtins-26-28-29-sites.txt
Normal file
@@ -0,0 +1,909 @@
|
||||
# Every call site of ISL built-ins 26, 28, 29 and 101 across all 28 stage scripts.
|
||||
#
|
||||
# Regenerate with:
|
||||
# python3 tools/re-capture/isl_builtin_sites.py 26,28,29,101 \
|
||||
# --ssb <dir with StageNN.ssb> --craft <unitgroup.py --all output>
|
||||
#
|
||||
# Columns: stage offset builtin slot8 slot4-name slot4-raw symtype before after
|
||||
# slot8 is the double operand; the handler multiplies it by 0.01 before sending it.
|
||||
#
|
||||
Stage01 0x0009C8 29 0 TCN001 0x1B type2 [35,118,12] [12,29,15]
|
||||
Stage01 0x000A44 29 0 TCN002 0x1E type2 [12,29,12] [15,12,30]
|
||||
Stage01 0x001A5C 29 100 TCN001 0x1B type2 [127,123,8] [35,11,58]
|
||||
Stage01 0x0028A8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage01 0x002CF8 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage01 0x003210 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage01 0x007370 28 10 TCN002 0x1E type2 [1,1,1] [29,30,64]
|
||||
Stage01 0x0073B0 29 0 TCN002 0x1E type2 [1,1,28] [30,64,64]
|
||||
Stage01 0x00EDAC 29 0 TCN001 0x1B type2 [35,118,12] [12,15,29]
|
||||
Stage01 0x00EE80 29 0 TCN002 0x1E type2 [29,12,15] [12,15,29]
|
||||
Stage01 0x00EF54 29 0 TCN003 0x20 type2 [29,12,15] [12,15,30]
|
||||
Stage01 0x00F094 28 100 ADT201 0x3 type2 [12,15,30] [12,15,30]
|
||||
Stage01 0x00F1D4 28 100 ADT202 0x6 type2 [12,15,30] [12,3,15]
|
||||
Stage01 0x00F348 28 100 ADS221 0x7 type2 [12,3,15] [12,3,15]
|
||||
Stage01 0x00F4BC 28 200 ADS222 0xB type2 [12,3,15] [12,15,28]
|
||||
Stage01 0x00F590 28 100 ADS223 0xE type2 [28,12,15] [12,15,12]
|
||||
Stage01 0x0100B0 29 100 TCN001 0x1B type2 [127,123,8] [35,103,1]
|
||||
Stage01 0x010398 28 10 TCN002 0x1E type2 [1,1,1] [29,30,11]
|
||||
Stage01 0x0103D8 29 0 TCN002 0x1E type2 [1,1,28] [30,11,58]
|
||||
Stage01 0x01060C 28 100 ADT203 0xA type2 [15,30,47] [103,69,63]
|
||||
Stage01 0x0108B4 28 100 ADT204 0xD type2 [58,15,30] [47,1,1]
|
||||
Stage01 0x010B40 28 100 ADS224 0x11 type2 [58,3,15] [1,11,58]
|
||||
Stage01 0x010CE8 28 100 ADS225 0x13 type2 [58,3,15] [1,11,58]
|
||||
Stage01 0x010DF0 28 100 ADT205 0x10 type2 [11,58,15] [30,47,20]
|
||||
Stage01 0x01115C 28 200 ADS227 0x17 type2 [57,3,15] [58,15,28]
|
||||
Stage01 0x011230 28 100 ADS226 0x15 type2 [28,58,15] [48,92,1]
|
||||
Stage01 0x0115CC 28 100 ADS228 0x19 type2 [58,3,15] [1,11,58]
|
||||
Stage01 0x011774 28 100 ADS229 0x1A type2 [58,3,15] [1,11,95]
|
||||
Stage01 0x01184C 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage01 0x011D58 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage01 0x0127AC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage02 0x000E6C 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
|
||||
Stage02 0x002D14 26 0 TCN131 0x57 type2 [14,77,11] [11,116,127]
|
||||
Stage02 0x002E0C 29 100 TCN001 0x4B type2 [127,123,8] [35,103,30]
|
||||
Stage02 0x003220 29 0 TCN002 0x4E type2 [1,1,1] [11,58,15]
|
||||
Stage02 0x004DA8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage02 0x005274 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage02 0x0059BC 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage02 0x0060A8 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage02 0x01586C 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
|
||||
Stage02 0x018544 29 100 TCN001 0x4B type2 [123,123,8] [35,103,30]
|
||||
Stage02 0x0186DC 29 0 TCN002 0x4E type2 [1,1,1] [11,58,3]
|
||||
Stage02 0x019204 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage02 0x0196D0 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage02 0x01A21C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage02 0x01E114 26 0 ADN207 0x2A type2 [79,7,70] [26,11,47]
|
||||
Stage02 0x01E154 26 0 TCN208 0x68 type2 [7,70,26] [11,47,7]
|
||||
Stage02 0x01E2C4 26 0 ADN207 0x2A type2 [47,7,70] [26,11,47]
|
||||
Stage02 0x01E304 26 0 TCN208 0x68 type2 [7,70,26] [11,47,20]
|
||||
Stage02 0x01E58C 26 0 ADN207 0x2A type2 [80,7,70] [26,11,47]
|
||||
Stage02 0x01E5CC 26 0 TCN208 0x68 type2 [7,70,26] [11,47,20]
|
||||
Stage02 0x01E854 26 0 ADN207 0x2A type2 [80,7,70] [26,11,47]
|
||||
Stage02 0x01E894 26 0 TCN208 0x68 type2 [7,70,26] [11,47,18]
|
||||
Stage02 0x025B14 29 0 TCN001 0x4B type2 [35,118,12] [12,15,12]
|
||||
Stage02 0x025FCC 26 60 TCN324 0x67 type2 [15,12,15] [12,15,26]
|
||||
Stage02 0x0260A0 26 80 TCN325 0x6A type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x026174 26 60 TCN326 0x6D type2 [26,12,15] [12,15,12]
|
||||
Stage02 0x0262DC 26 80 TCN328 0x74 type2 [15,12,15] [12,15,12]
|
||||
Stage02 0x026AD8 26 80 ADN322 0x1F type2 [15,12,15] [12,15,26]
|
||||
Stage02 0x026BAC 26 60 ADN323 0x25 type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x026C80 26 80 ADN324 0x2C type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x026D54 26 30 ADN325 0x32 type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x026E28 26 80 ADN326 0x38 type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x026EFC 26 30 ADN327 0x3D type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x026FD0 26 60 ADN328 0x42 type2 [26,12,15] [12,15,26]
|
||||
Stage02 0x0270A4 26 80 ADN329 0x45 type2 [26,12,15] [12,15,12]
|
||||
Stage02 0x02720C 26 80 ADN331 0x20 type2 [15,12,15] [12,15,26]
|
||||
Stage02 0x0272E0 26 60 ADN332 0x26 type2 [26,12,15] [47,47,47]
|
||||
Stage02 0x028B5C 29 100 TCN001 0x4B type2 [123,123,8] [35,103,30]
|
||||
Stage02 0x028E14 29 0 TCN002 0x4E type2 [1,1,1] [11,26,26]
|
||||
Stage02 0x028E64 26 0 ADN313 0x1E type2 [1,29,11] [26,11,57]
|
||||
Stage02 0x028EA4 26 0 TCN334 0x6B type2 [29,11,26] [11,57,30]
|
||||
Stage02 0x02907C 29 100 ADT301 0x31 type2 [30,15,19] [57,30,15]
|
||||
Stage02 0x029244 29 100 ADT302 0x37 type2 [30,15,19] [48,47,47]
|
||||
Stage02 0x029B84 29 100 ADT303 0x3C type2 [30,15,19] [1,64,70]
|
||||
Stage02 0x029ED0 29 100 ADT304 0x41 type2 [30,15,19] [57,30,15]
|
||||
Stage02 0x02A098 29 100 ADT305 0x44 type2 [30,15,19] [58,15,48]
|
||||
Stage02 0x02AAE8 29 100 ADT306 0x47 type2 [30,15,19] [57,30,15]
|
||||
Stage02 0x02ACB0 29 100 ADT307 0x48 type2 [30,15,19] [57,30,15]
|
||||
Stage02 0x02AE78 29 100 ADT308 0x49 type2 [30,15,19] [58,15,48]
|
||||
Stage02 0x02B530 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage02 0x02B9FC 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage02 0x02C000 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage02 0x02C524 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage02 0x02D018 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage02 0x02D2DC 28 150 TCN004 0x56 type2 [20,105,115] [11,26,64]
|
||||
Stage02 0x02D350 26 0 ADT301 0x31 type2 [115,28,11] [64,90,11]
|
||||
Stage02 0x02D4D4 26 0 ADT302 0x37 type2 [64,90,11] [64,90,11]
|
||||
Stage02 0x02D658 26 0 ADT303 0x3C type2 [64,90,11] [64,90,11]
|
||||
Stage02 0x02D7DC 26 0 ADT304 0x41 type2 [64,90,11] [64,90,11]
|
||||
Stage02 0x02D960 26 0 ADT305 0x44 type2 [64,90,11] [64,90,11]
|
||||
Stage02 0x02DAF0 26 0 ADT306 0x47 type2 [64,90,11] [64,90,11]
|
||||
Stage02 0x02DC74 26 0 ADT307 0x48 type2 [64,90,11] [64,90,11]
|
||||
Stage02 0x02DDF8 26 0 ADT308 0x49 type2 [64,90,11] [64,90,11]
|
||||
Stage03 0x0009F0 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
|
||||
Stage03 0x002514 26 0 TCN132 0x30 type2 [14,77,11] [11,116,127]
|
||||
Stage03 0x00260C 29 100 TCN001 0x21 type2 [127,123,8] [35,103,1]
|
||||
Stage03 0x002810 29 0 TCN002 0x24 type2 [30,91,47] [11,58,19]
|
||||
Stage03 0x0034E8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage03 0x0039B4 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage03 0x00424C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage03 0x004720 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage03 0x00ABB4 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
|
||||
Stage03 0x00B0CC 29 0 TCT207 0x42 type2 [15,19,30] [12,15,19]
|
||||
Stage03 0x00C720 29 100 TCN001 0x21 type2 [116,123,8] [35,11,58]
|
||||
Stage03 0x00DD7C 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage03 0x00E248 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage03 0x00E84C 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage03 0x00EFF8 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage03 0x00F844 29 0 TCT207 0x42 type2 [64,19,11] [5,11,20]
|
||||
Stage03 0x00FA58 29 0 TCT208 0x44 type2 [19,19,11] [5,11,20]
|
||||
Stage03 0x00FC6C 29 0 TCT209 0x46 type2 [19,19,11] [5,11,20]
|
||||
Stage03 0x00FE34 29 100 TCT207 0x42 type2 [11,20,20] [11,20,20]
|
||||
Stage03 0x00FF50 29 100 TCT207 0x42 type2 [11,20,20] [11,20,105]
|
||||
Stage03 0x01584C 29 0 TCN001 0x21 type2 [35,118,12] [12,15,12]
|
||||
Stage03 0x0164CC 28 500 ADN301 0x4 type2 [117,12,15] [48,48,48]
|
||||
Stage03 0x016FF8 29 100 TCN001 0x21 type2 [116,123,8] [35,11,58]
|
||||
Stage03 0x01713C 28 500 ADN304 0xB type2 [58,15,91] [58,15,30]
|
||||
Stage03 0x01727C 28 1000 ADT302 0x19 type2 [58,15,30] [91,103,4]
|
||||
Stage03 0x017C6C 28 500 ADN306 0x11 type2 [11,58,15] [91,69,47]
|
||||
Stage03 0x0182F4 28 500 ADN308 0x18 type2 [11,58,15] [91,69,47]
|
||||
Stage03 0x018A4C 28 2000 ADT303 0x1D type2 [15,30,91] [69,47,69]
|
||||
Stage03 0x0191FC 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage03 0x0196C8 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage03 0x019CCC 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage03 0x019ED4 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage03 0x01A744 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage03 0x01BC10 28 2000 ADT303 0x1D type2 [15,30,91] [69,47,69]
|
||||
Stage03 0x01C358 28 150 TCN309 0x41 type2 [9,115,69] [64,11,64]
|
||||
Stage04 0x000A38 29 0 TCN001 0x15 type2 [35,118,12] [12,15,12]
|
||||
Stage04 0x002228 29 100 TCN001 0x15 type2 [127,123,8] [35,11,69]
|
||||
Stage04 0x0026D0 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage04 0x002BCC 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage04 0x003390 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage04 0x003834 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage04 0x005ED0 29 0 TCN001 0x15 type2 [35,118,12] [12,15,12]
|
||||
Stage04 0x006260 28 500 ADN201 0x0 type2 [15,12,15] [12,30,15]
|
||||
Stage04 0x0063A0 28 1000 ADT202 0x9 type2 [12,30,15] [12,15,12]
|
||||
Stage04 0x0074CC 29 100 TCN001 0x15 type2 [127,8,123] [35,11,58]
|
||||
Stage04 0x007674 28 500 ADN206 0x4 type2 [58,15,3] [92,91,64]
|
||||
Stage04 0x007990 28 500 ADN207 0x6 type2 [58,3,15] [47,1,11]
|
||||
Stage04 0x007B8C 28 500 ADN208 0x8 type2 [58,15,3] [92,91,64]
|
||||
Stage04 0x007FAC 28 2000 ADT203 0xD type2 [30,47,30] [64,64,8]
|
||||
Stage04 0x008324 28 500 ADN209 0xC type2 [58,15,3] [92,91,1]
|
||||
Stage04 0x0084AC 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage04 0x0089A8 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage04 0x009030 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage04 0x0099DC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage04 0x009D40 28 150 TCN205 0x25 type2 [9,115,69] [64,11,20]
|
||||
Stage04 0x00A004 28 2000 ADT203 0xD type2 [15,30,47] [64,64,64]
|
||||
Stage05 0x000898 29 0 TCN001 0x13 type2 [35,118,12] [15,12,15]
|
||||
Stage05 0x00181C 29 100 TCN001 0x13 type2 [8,127,123] [35,119,11]
|
||||
Stage05 0x002E6C 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||
Stage05 0x003BAC 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage05 0x003F28 28 150 TCN003 0x19 type2 [9,115,70] [70,48,11]
|
||||
Stage05 0x007F00 29 0 TCN001 0x13 type2 [35,118,12] [15,12,15]
|
||||
Stage05 0x0081C0 26 80 TCN004 0x1E type2 [12,15,30] [12,15,30]
|
||||
Stage05 0x008300 28 150 ADT201 0x4 type2 [12,15,30] [12,19,15]
|
||||
Stage05 0x0084C8 28 150 ADT202 0x6 type2 [19,15,30] [12,3,15]
|
||||
Stage05 0x008FF8 29 100 TCN001 0x13 type2 [8,127,123] [35,119,11]
|
||||
Stage05 0x009734 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||
Stage05 0x00A228 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage05 0x00B1A0 28 150 TCN001 0x13 type2 [64,9,115] [28,28,64]
|
||||
Stage05 0x00B1E0 28 150 TCN002 0x16 type2 [9,115,28] [28,64,64]
|
||||
Stage05 0x00B220 28 150 TCN003 0x19 type2 [115,28,28] [64,64,11]
|
||||
Stage06 0x000924 29 0 TCN001 0x26 type2 [118,12,15] [12,15,12]
|
||||
Stage06 0x001460 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
|
||||
Stage06 0x00330C 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage06 0x003810 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage06 0x003E94 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage06 0x0040D4 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||
Stage06 0x009B50 29 0 TCN001 0x26 type2 [118,12,15] [12,15,12]
|
||||
Stage06 0x00BEC4 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
|
||||
Stage06 0x00C168 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage06 0x00C66C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage06 0x00CCF0 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage06 0x00D274 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage06 0x0119AC 29 0 TCN001 0x26 type2 [118,12,15] [12,15,29]
|
||||
Stage06 0x011A80 29 0 TCN002 0x29 type2 [29,12,15] [28,12,15]
|
||||
Stage06 0x011AC0 28 50 TCN002 0x29 type2 [12,15,29] [12,15,12]
|
||||
Stage06 0x011EFC 26 50 TCN306 0x3D type2 [12,15,3] [12,15,3]
|
||||
Stage06 0x013A7C 29 100 TCN001 0x26 type2 [8,127,123] [35,119,11]
|
||||
Stage06 0x013B60 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage06 0x01405C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage06 0x014724 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage06 0x014F64 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage06 0x01845C 26 0 TCN350 0x3A type2 [64,11,64] [1,1,1]
|
||||
Stage06 0x01A7B0 26 80 TCN002 0x29 type2 [69,11,9] [64,64,1]
|
||||
Stage06 0x01AAA0 26 50 TCN002 0x29 type2 [69,11,9] [11,64,1]
|
||||
Stage06 0x01ACEC 26 30 TCN002 0x29 type2 [69,11,9] [11,64,11]
|
||||
Stage07 0x000D3C 29 0 TCN001 0x1C type2 [12,12,12] [15,15,15]
|
||||
Stage07 0x0026A4 29 100 TCN001 0x1C type2 [8,127,123] [35,119,11]
|
||||
Stage07 0x002788 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage07 0x002BD0 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage07 0x00324C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage07 0x0034D4 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage07 0x003714 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||
Stage07 0x00AB0C 29 0 TCN001 0x1C type2 [12,12,12] [15,15,15]
|
||||
Stage07 0x00CCD4 29 100 TCN001 0x1C type2 [116,8,123] [35,119,11]
|
||||
Stage07 0x00CDB8 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage07 0x00D200 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage07 0x00D87C 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage07 0x00E4A8 101 - ? None typeNone [20,69,116] [100,93,124]
|
||||
Stage07 0x00ED78 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage08 0x001F44 29 0 TCN001 0x1E type2 [15,12,15] [15,15,15]
|
||||
Stage08 0x0021C4 28 0.1 TCN104 0x30 type2 [92,15,30] [15,28,15]
|
||||
Stage08 0x00225C 28 0.1 TCN105 0x35 type2 [30,28,15] [15,28,15]
|
||||
Stage08 0x0022F4 28 0.1 TCN106 0x36 type2 [15,28,15] [15,30,28]
|
||||
Stage08 0x0023F8 28 0.1 TCN012 0x29 type2 [28,15,30] [15,28,15]
|
||||
Stage08 0x002490 28 0.1 TCN113 0x31 type2 [30,28,15] [15,92,30]
|
||||
Stage08 0x003350 26 0 ADN126 0xF type2 [4,14,77] [11,26,11]
|
||||
Stage08 0x0033A0 26 0 ADN126b 0x3D type2 [77,26,11] [11,26,11]
|
||||
Stage08 0x0033F0 26 0 ADN126c 0x3F type2 [11,26,11] [11,116,8]
|
||||
Stage08 0x0034E8 29 100 TCN001 0x1E type2 [8,127,123] [35,119,11]
|
||||
Stage08 0x0036D0 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||
Stage08 0x004294 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||
Stage08 0x006C54 28 1000 ADS151a 0x45 type2 [15,47,30] [29,58,15]
|
||||
Stage08 0x006C94 29 40 ADS151a 0x45 type2 [47,30,28] [58,15,47]
|
||||
Stage08 0x006E28 28 1000 ADS151b 0x4B type2 [15,47,30] [29,64,8]
|
||||
Stage08 0x006E68 29 40 ADS151b 0x4B type2 [47,30,28] [64,8,1]
|
||||
Stage08 0x00BD3C 28 150 TCN001 0x1E type2 [105,9,115] [28,28,64]
|
||||
Stage08 0x00BD7C 28 150 TCN002 0x21 type2 [9,115,28] [28,64,11]
|
||||
Stage08 0x00BDBC 28 150 TCN003 0x24 type2 [115,28,28] [64,11,20]
|
||||
Stage08 0x00D918 29 0 TCN001 0x1E type2 [12,12,12] [15,15,15]
|
||||
Stage08 0x00E190 29 0 ADT201 0x11 type2 [15,92,15] [117,15,92]
|
||||
Stage08 0x00EE0C 29 100 TCN001 0x1E type2 [8,127,123] [35,119,11]
|
||||
Stage08 0x00EFF4 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||
Stage08 0x00FD4C 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage09 0x000974 29 0 TCN001 0x28 type2 [35,118,12] [12,15,12]
|
||||
Stage09 0x002958 29 100 TCN001 0x28 type2 [127,123,8] [35,11,58]
|
||||
Stage09 0x003610 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage09 0x003A20 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage09 0x003C68 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage09 0x00426C 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage09 0x004A44 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage09 0x00851C 29 0 TCN001 0x28 type2 [35,118,12] [12,15,12]
|
||||
Stage09 0x009E64 29 100 TCN001 0x28 type2 [116,123,8] [35,11,58]
|
||||
Stage09 0x00A1C0 28 1000 ADS251a 0x44 type2 [12,15,30] [29,12,15]
|
||||
Stage09 0x00A200 29 40 ADS251a 0x44 type2 [15,30,28] [12,15,30]
|
||||
Stage09 0x00A340 28 1000 ADS251b 0x46 type2 [12,15,30] [29,47,47]
|
||||
Stage09 0x00A380 29 40 ADS251b 0x46 type2 [15,30,28] [47,47,1]
|
||||
Stage09 0x00A6D0 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage09 0x00AAE0 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage09 0x00ACE8 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage09 0x00B2D0 101 - ? None typeNone [7,9,116] [100,124,93]
|
||||
Stage09 0x00C38C 28 150 TCN001 0x28 type2 [115,9,115] [28,11,64]
|
||||
Stage09 0x00C3CC 28 150 TCN002 0x2B type2 [9,115,28] [11,64,64]
|
||||
Stage09 0x00FDC4 29 0 TCN001 0x28 type2 [114,114,12] [12,15,12]
|
||||
Stage09 0x010684 28 500 ADT301 0x14 type2 [15,12,15] [12,15,12]
|
||||
Stage09 0x011790 29 100 TCN001 0x28 type2 [116,123,8] [35,11,58]
|
||||
Stage09 0x011E84 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage09 0x012294 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage09 0x012B60 101 - ? None typeNone [69,69,116] [100,124,93]
|
||||
Stage09 0x0131AC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage09 0x017BE4 28 500 ADT302 0x18 type2 [3,15,47] [64,1,1]
|
||||
Stage10 0x0006DC 29 0 TCN001 0xE type2 [118,75,12] [12,3,15]
|
||||
Stage10 0x000910 28 2000 ADT001 0x2 type2 [15,30,47] [12,15,47]
|
||||
Stage10 0x000A38 28 500 ADS002 0x1 type2 [12,15,47] [12,15,47]
|
||||
Stage10 0x000B60 28 500 ADS003 0x5 type2 [12,15,47] [1,1,1]
|
||||
Stage10 0x000F38 29 100 TCN001 0xE type2 [8,127,123] [35,119,11]
|
||||
Stage10 0x00101C 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage10 0x0013A8 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage10 0x001620 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage10 0x002394 28 500 ADS004 0x7 type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x002BA0 28 500 ADS005 0x9 type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x0033AC 28 500 ADS006 0xA type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x003BB8 28 500 ADS007 0xB type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x0043C4 28 500 ADS008 0xC type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x004BD0 28 500 ADS009 0xD type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x0053DC 28 500 ADS010 0x0 type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x005BE8 28 500 ADS011 0x4 type2 [64,128,47] [1,11,20]
|
||||
Stage10 0x006554 28 500 ADS013 0x8 type2 [64,128,47] [1,11,20]
|
||||
Stage11 0x000744 29 0 TCN001 0x18 type2 [35,118,12] [12,15,12]
|
||||
Stage11 0x001610 29 100 TCN001 0x18 type2 [8,127,123] [35,11,58]
|
||||
Stage11 0x0017D8 28 1000 ADS151a 0x27 type2 [15,47,30] [29,58,15]
|
||||
Stage11 0x001818 29 40 ADS151a 0x27 type2 [47,30,28] [58,15,47]
|
||||
Stage11 0x0019AC 28 1000 ADS151b 0x29 type2 [15,47,30] [29,1,1]
|
||||
Stage11 0x0019EC 29 40 ADS151b 0x29 type2 [47,30,28] [1,1,1]
|
||||
Stage11 0x001B0C 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage11 0x001F54 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage11 0x0025D8 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage11 0x002818 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage11 0x00B070 29 0 TCN001 0x18 type2 [35,118,12] [12,15,12]
|
||||
Stage11 0x00BFB8 29 100 TCN001 0x18 type2 [8,127,123] [35,1,1]
|
||||
Stage11 0x00C0D8 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage11 0x00C520 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage11 0x00CBA4 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage11 0x00CDE4 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage12 0x0012D0 29 0 TCN001 0x20 type2 [12,12,12] [75,8,11]
|
||||
Stage12 0x001564 29 100 TCN001 0x20 type2 [8,127,123] [35,119,1]
|
||||
Stage12 0x0033CC 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||
Stage12 0x00403C 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||
Stage13 0x0012C4 29 0 TCN001 0x16 type2 [12,12,12] [15,15,15]
|
||||
Stage13 0x001D74 26 50 TCN013 0x25 type2 [3,15,1] [3,15,1]
|
||||
Stage13 0x0027DC 28 150 ADN004 0x8 type2 [47,92,3] [15,3,28]
|
||||
Stage13 0x002914 28 150 ADN005 0xB type2 [28,15,3] [15,47,28]
|
||||
Stage13 0x002A00 28 150 ADN012 0x6 type2 [28,15,47] [15,47,47]
|
||||
Stage13 0x0031BC 29 100 TCN001 0x16 type2 [116,8,123] [35,119,11]
|
||||
Stage13 0x003348 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage13 0x004EC4 101 - ? None typeNone [20,69,116] [100,93,124]
|
||||
Stage13 0x0098A0 28 150 ADN002 0x3 type2 [20,9,57] [15,47,47]
|
||||
Stage13 0x00A68C 28 500 ADS051a 0x34 type2 [64,11,58] [15,47,92]
|
||||
Stage13 0x00A894 28 500 ADS051b 0x36 type2 [92,30,58] [15,47,92]
|
||||
Stage13 0x00AA9C 28 500 ADS051c 0x37 type2 [92,30,58] [15,47,92]
|
||||
Stage13 0x00BA00 28 150 ADN015 0xE type2 [64,11,57] [15,92,64]
|
||||
Stage13 0x00BE08 28 150 ADN017 0x12 type2 [64,11,57] [15,47,92]
|
||||
Stage14 0x001230 29 0 TCN001 0x18 type2 [12,12,12] [15,15,92]
|
||||
Stage14 0x001394 26 80 TCN003 0x24 type2 [15,15,92] [25,15,30]
|
||||
Stage14 0x001548 28 500 ADT102 0x3 type2 [15,30,15] [30,47,92]
|
||||
Stage14 0x001714 28 500 ADT103 0x6 type2 [47,92,15] [30,47,92]
|
||||
Stage14 0x0018E0 28 500 ADT104 0x8 type2 [47,92,15] [30,47,92]
|
||||
Stage14 0x00242C 29 100 TCN001 0x18 type2 [8,127,123] [35,119,11]
|
||||
Stage14 0x002510 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage14 0x002960 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage14 0x002D84 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage14 0x003784 28 500 ADT105 0xA type2 [64,58,15] [30,47,47]
|
||||
Stage14 0x003E90 28 500 ADT108 0x10 type2 [64,58,15] [30,47,1]
|
||||
Stage14 0x004548 28 500 ADT114 0xB type2 [64,58,15] [30,47,47]
|
||||
Stage14 0x007334 29 0 TCN201 0x29 type2 [12,12,12] [15,30,47]
|
||||
Stage14 0x00748C 28 500 ADT201 0x5 type2 [15,30,47] [15,15,15]
|
||||
Stage14 0x007F18 29 100 TCN201 0x29 type2 [8,127,123] [35,119,11]
|
||||
Stage14 0x007FFC 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage14 0x0083C4 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage14 0x008644 101 - ? None typeNone [11,20,116] [100,93,124]
|
||||
Stage15 0x0014F8 29 0 TCN001 0x3A type2 [12,12,12] [15,15,15]
|
||||
Stage15 0x002390 29 0 TCN008 0x59 type2 [15,47,30] [1,15,48]
|
||||
Stage15 0x002C68 28 150 ADN101 0x1 type2 [15,48,92] [15,28,15]
|
||||
Stage15 0x002D00 28 150 ADN102 0x2 type2 [92,28,15] [15,3,1]
|
||||
Stage15 0x002FC8 28 150 ADN103 0x5 type2 [15,48,92] [15,3,15]
|
||||
Stage15 0x0032C0 28 150 ADN104 0xB type2 [48,47,92] [15,3,15]
|
||||
Stage15 0x003564 28 150 ADN106 0x18 type2 [15,48,92] [15,28,15]
|
||||
Stage15 0x0035FC 28 150 ADN109 0x2C type2 [92,28,15] [15,15,47]
|
||||
Stage15 0x003F50 29 100 TCN001 0x3A type2 [116,8,123] [35,119,11]
|
||||
Stage15 0x0040DC 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage15 0x00563C 101 - ? None typeNone [20,69,116] [100,93,124]
|
||||
Stage15 0x00DE90 29 0 TCN001 0x3A type2 [12,12,12] [15,15,15]
|
||||
Stage15 0x00E970 29 0 TCN008 0x59 type2 [47,92,30] [15,48,92]
|
||||
Stage15 0x00F198 28 150 ADT201 0x28 type2 [47,92,30] [15,1,30]
|
||||
Stage15 0x00F2C0 28 150 ADT202 0x30 type2 [15,1,30] [15,1,28]
|
||||
Stage15 0x00F37C 28 150 ADN203 0xF type2 [28,15,1] [15,28,15]
|
||||
Stage15 0x00F414 28 150 ADN204 0x15 type2 [1,28,15] [15,28,15]
|
||||
Stage15 0x00F4AC 28 150 ADN205 0x1C type2 [15,28,15] [15,28,15]
|
||||
Stage15 0x00F544 28 150 ADN206 0x21 type2 [15,28,15] [15,28,15]
|
||||
Stage15 0x00F5DC 28 150 ADN207 0x27 type2 [15,28,15] [15,28,15]
|
||||
Stage15 0x00F674 28 150 ADN208 0x2F type2 [15,28,15] [15,28,15]
|
||||
Stage15 0x00F70C 28 150 ADN209 0x36 type2 [15,28,15] [15,15,28]
|
||||
Stage15 0x00F7FC 28 500 ADN210 0x4 type2 [28,15,15] [47,92,1]
|
||||
Stage15 0x00F980 28 500 ADN211 0x9 type2 [92,1,15] [47,92,1]
|
||||
Stage15 0x00FB04 28 500 ADN212 0x10 type2 [92,1,15] [47,47,92]
|
||||
Stage15 0x00FCDC 28 500 ADN213 0x16 type2 [92,1,15] [47,47,92]
|
||||
Stage15 0x00FEB4 28 500 ADN214 0x1D type2 [92,1,15] [47,47,92]
|
||||
Stage15 0x0102DC 29 100 TCN001 0x3A type2 [8,127,123] [35,119,11]
|
||||
Stage15 0x0104C4 101 - ? None typeNone [20,9,116] [100,93,124]
|
||||
Stage15 0x0110CC 101 - ? None typeNone [20,20,116] [100,93,124]
|
||||
Stage15 0x013368 28 500 ADN215 0x22 type2 [20,58,15] [47,92,1]
|
||||
Stage15 0x01358C 28 500 ADN216 0x2A type2 [20,58,15] [47,92,11]
|
||||
Stage15 0x01378C 28 500 ADN217 0x32 type2 [20,58,15] [47,47,92]
|
||||
Stage15 0x0139E0 28 500 ADN218 0x37 type2 [20,58,15] [47,47,92]
|
||||
Stage15 0x013C34 28 500 ADN219 0x39 type2 [20,58,15] [47,47,92]
|
||||
Stage15 0x013E88 28 500 ADN220 0xA type2 [20,58,15] [47,92,11]
|
||||
Stage15 0x014088 28 500 ADN221 0x11 type2 [20,58,15] [47,92,11]
|
||||
Stage15 0x014924 28 150 ADN222 0x17 type2 [64,11,57] [15,64,64]
|
||||
Stage15 0x014D28 28 500 ADN223 0x1E type2 [11,58,15] [47,47,92]
|
||||
Stage15 0x015170 28 150 ADN224 0x23 type2 [64,11,57] [15,64,64]
|
||||
Stage15 0x01553C 28 500 ADN225 0x2B type2 [11,58,15] [47,92,64]
|
||||
Stage15 0x0158E8 28 500 ADN226 0x33 type2 [11,58,15] [47,92,58]
|
||||
Stage15 0x015A84 28 500 ADN227 0x38 type2 [92,58,15] [47,92,64]
|
||||
Stage16 0x0004F4 29 100 TCN001 0x1 type2 [8,127,123] [35,119,11]
|
||||
Stage16 0x0005D8 101 - ? None typeNone [11,95,116] [100,93,124]
|
||||
Stage16 0x000A28 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage16 0x000E4C 101 - ? None typeNone [140,140,116] [100,93,124]
|
||||
Stage18 0x000488 28 0 TCN001 0xB type2 [35,8,12] [52,1,11]
|
||||
Stage18 0x0009D4 26 0 ADT101 0x0 type2 [1,11,18] [13,0,97]
|
||||
Stage18 0x000D2C 26 0 ADT102 0x2 type2 [1,11,18] [13,0,11]
|
||||
Stage18 0x000E30 26 0 ADT103 0x4 type2 [0,11,18] [13,0,11]
|
||||
Stage18 0x000F34 26 0 ADT104 0x5 type2 [0,11,18] [13,0,11]
|
||||
Stage18 0x0013E4 26 0 ADT105 0x6 type2 [1,11,18] [13,0,11]
|
||||
Stage18 0x0014E8 26 0 ADT106 0x7 type2 [0,11,18] [13,0,11]
|
||||
Stage18 0x0015EC 26 0 ADT107 0x8 type2 [0,11,18] [13,0,11]
|
||||
Stage18 0x001AB0 26 0 ADT108 0x9 type2 [1,11,18] [13,0,97]
|
||||
Stage18 0x001E2C 28 100 TCN001 0xB type2 [4,1,11] [12,3,15]
|
||||
Stage18 0x0029FC 26 100 TCN001 0xB type2 [6,11,20] []
|
||||
Stage19 0x0003D4 28 0 TCN001 0x7 type2 [35,8,12] [52,1,11]
|
||||
Stage19 0x000CD4 28 100 TCN001 0x7 type2 [11,104,4] [1,11,69]
|
||||
Stage19 0x000F0C 26 0 ADS101 0x0 type2 [16,0,12] [26,84,4]
|
||||
Stage19 0x000F4C 26 0 ADS102 0x1 type2 [0,12,26] [84,4,97]
|
||||
Stage19 0x001010 28 0 TCN001 0x7 type2 [4,97,4] [12,3,15]
|
||||
Stage19 0x0012F8 26 0 ADS104 0x2 type2 [4,16,0] [84,4,97]
|
||||
Stage19 0x001394 28 100 TCN001 0x7 type2 [84,4,97] [12,3,15]
|
||||
Stage19 0x0019F4 26 100 TCN001 0x7 type2 [6,11,20] []
|
||||
Stage20 0x0026C0 26 100 TCN001 0xB type2 [6,11,20] []
|
||||
Stage21 0x0002A8 28 0 TCN001 0x2 type2 [35,8,12] [26,52,1]
|
||||
Stage21 0x0002E8 26 40 TCN001 0x2 type2 [8,12,28] [52,1,12]
|
||||
Stage21 0x000B58 28 100 TCN001 0x2 type2 [0,84,4] [97,4,12]
|
||||
Stage21 0x0012F4 26 100 TCN001 0x2 type2 [6,11,20] []
|
||||
Stage22 0x00098C 26 0 ADT101 0x0 type2 [1,11,18] [0,11,69]
|
||||
Stage22 0x000AD0 26 0 ADT102 0x2 type2 [69,11,18] [0,11,69]
|
||||
Stage22 0x001384 26 0 ADS103 0x1 type2 [11,69,0] [97,1,11]
|
||||
Stage22 0x001470 26 0 ADT104 0x3 type2 [1,11,18] [0,11,69]
|
||||
Stage22 0x001868 26 0 ADT106 0x5 type2 [1,11,18] [0,11,69]
|
||||
Stage22 0x001F1C 26 0 ADT107 0x6 type2 [1,11,18] [0,11,69]
|
||||
Stage22 0x002130 26 100 TCN001 0x7 type2 [6,11,20] []
|
||||
Stage23 0x0002A8 28 0 TCN001 0x3 type2 [35,8,12] [52,1,11]
|
||||
Stage23 0x000898 26 0 ADS105 0x2 type2 [4,16,0] [84,4,97]
|
||||
Stage23 0x000B98 28 100 TCN001 0x3 type2 [11,104,4] [1,11,69]
|
||||
Stage23 0x0012AC 26 100 TCN001 0x3 type2 [6,11,20] []
|
||||
Stage24 0x001274 29 0 ADT108 0x1E type2 [12,30,117] [12,12,12]
|
||||
Stage24 0x001EE8 29 100 TCN001 0x2C type2 [127,123,8] [35,119,1]
|
||||
Stage24 0x0029B0 29 100 ADT108 0x1E type2 [15,47,64] [20,64,11]
|
||||
Stage24 0x0045F8 29 100 ADT109 0x22 type2 [15,48,47] [64,1,7]
|
||||
Stage24 0x004A44 29 0 ADT109 0x22 type2 [58,30,117] [64,7,58]
|
||||
Stage24 0x0051B8 29 100 ADT103 0x8 type2 [30,15,47] [64,64,1]
|
||||
Stage24 0x0056B8 29 0 ADT103 0x8 type2 [58,30,117] [64,20,4]
|
||||
Stage24 0x007094 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage24 0x007304 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage24 0x007710 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage24 0x007DBC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage25 0x001790 28 100 ADN355 0x21 type2 [15,12,15] [12,15,28]
|
||||
Stage25 0x001864 28 100 ADN350 0xF type2 [28,12,15] [12,15,28]
|
||||
Stage25 0x001938 28 100 ADN351 0x13 type2 [28,12,15] [12,15,28]
|
||||
Stage25 0x001A0C 28 100 ADN352 0x17 type2 [28,12,15] [12,15,28]
|
||||
Stage25 0x001AE0 28 100 ADN353 0x1B type2 [28,12,15] [12,15,28]
|
||||
Stage25 0x001BB4 28 100 ADN354 0x1E type2 [28,12,15] [12,15,28]
|
||||
Stage25 0x001C88 28 200 ADN310 0x1 type2 [28,12,15] [12,15,117]
|
||||
Stage25 0x004218 29 100 TCN001 0x27 type2 [127,123,8] [35,119,1]
|
||||
Stage25 0x00440C 28 300 ADN312 0x5 type2 [11,57,16] [1,11,57]
|
||||
Stage25 0x0044F8 28 300 ADN313 0x9 type2 [11,57,16] [1,11,58]
|
||||
Stage25 0x004600 28 600 ADN314 0xD type2 [11,58,15] [47,47,1]
|
||||
Stage25 0x004964 28 600 ADN315 0x11 type2 [11,58,15] [47,47,1]
|
||||
Stage25 0x005348 28 100 ADN350 0xF type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x005814 28 200 ADN356 0x23 type2 [5,57,15] [145,47,47]
|
||||
Stage25 0x005BAC 28 200 ADN356 0x23 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00709C 28 100 ADN351 0x13 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x0074C8 28 200 ADN357 0x24 type2 [5,57,15] [145,47,47]
|
||||
Stage25 0x007860 28 200 ADN357 0x24 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x0088E8 28 100 ADN352 0x17 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x008D14 28 200 ADN358 0x25 type2 [5,57,15] [145,47,47]
|
||||
Stage25 0x0090AC 28 200 ADN358 0x25 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00A59C 28 100 ADN353 0x1B type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00A9C8 28 200 ADN359 0x26 type2 [5,57,15] [145,47,47]
|
||||
Stage25 0x00AD60 28 200 ADN359 0x26 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00BC70 28 100 ADN354 0x1E type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00C09C 28 200 ADN360 0x14 type2 [5,57,15] [145,47,47]
|
||||
Stage25 0x00C434 28 200 ADN360 0x14 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00D924 28 100 ADN355 0x21 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00DD50 28 200 ADN361 0x18 type2 [5,57,15] [145,47,47]
|
||||
Stage25 0x00E0E8 28 200 ADN361 0x18 type2 [143,57,15] [145,47,47]
|
||||
Stage25 0x00ECD4 28 200 ADN310 0x1 type2 [143,57,15] [19,64,20]
|
||||
Stage25 0x00F390 28 300 ADN311 0x2 type2 [5,57,15] [19,1,64]
|
||||
Stage25 0x00F704 28 300 ADN311 0x2 type2 [143,57,15] [19,64,8]
|
||||
Stage25 0x010EFC 28 300 ADN312 0x5 type2 [9,143,57] [16,64,69]
|
||||
Stage25 0x0111A8 26 0 ADN320 0x3 type2 [69,20,18] [69,8,69]
|
||||
Stage25 0x011450 28 300 ADN320 0x3 type2 [9,128,15] [47,47,1]
|
||||
Stage25 0x011C0C 28 300 ADN320 0x3 type2 [143,128,15] [47,47,1]
|
||||
Stage25 0x011FFC 28 300 ADN320 0x3 type2 [143,128,15] [47,47,1]
|
||||
Stage25 0x012240 26 0 ADN321 0x7 type2 [69,20,18] [69,8,69]
|
||||
Stage25 0x0124E8 28 300 ADN321 0x7 type2 [9,128,15] [47,47,1]
|
||||
Stage25 0x012CA4 28 300 ADN321 0x7 type2 [143,128,15] [47,47,1]
|
||||
Stage25 0x013094 28 300 ADN321 0x7 type2 [143,128,15] [47,47,1]
|
||||
Stage25 0x0134C0 28 300 ADN328 0x20 type2 [9,128,15] [47,1,69]
|
||||
Stage25 0x013BE8 28 300 ADN328 0x20 type2 [143,128,15] [47,1,69]
|
||||
Stage25 0x0142CC 28 300 ADN313 0x9 type2 [9,143,57] [16,64,69]
|
||||
Stage25 0x0146F4 28 300 ADN325 0x16 type2 [9,128,15] [47,1,69]
|
||||
Stage25 0x014E1C 28 300 ADN325 0x16 type2 [143,128,15] [47,1,18]
|
||||
Stage25 0x0151F4 28 300 ADN329 0x22 type2 [9,128,15] [47,1,69]
|
||||
Stage25 0x01591C 28 300 ADN329 0x22 type2 [143,128,15] [47,1,69]
|
||||
Stage25 0x016058 28 600 ADN314 0xD type2 [143,57,15] [47,47,145]
|
||||
Stage25 0x016B84 28 600 ADN315 0x11 type2 [143,57,15] [47,47,145]
|
||||
Stage25 0x017094 101 - ? None typeNone [64,95,116] [100,124,93]
|
||||
Stage25 0x01748C 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage25 0x017AE8 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage26 0x000EF0 29 0 TCN001 0x16 type2 [118,146,12] [15,12,15]
|
||||
Stage26 0x001614 29 50 TCT012 0x28 type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x0017F4 29 50 TCT013 0x2A type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x0019D4 29 50 TCT014 0x2C type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x001BB4 29 50 TCT015 0x2E type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x001D94 29 50 TCT016 0x30 type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x001F74 29 50 TCT017 0x32 type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x002154 29 50 TCT018 0x34 type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x002334 29 50 TCT019 0x36 type2 [15,3,30] [12,15,3]
|
||||
Stage26 0x003928 29 100 TCN001 0x16 type2 [127,123,8] [35,11,95]
|
||||
Stage26 0x003A00 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage26 0x003E10 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage26 0x004414 101 - ? None typeNone [6,11,116] [100,124,93]
|
||||
Stage26 0x004664 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage26 0x008DD0 29 20 ADN128 0x14 type2 [47,11,57] [15,3,46]
|
||||
Stage26 0x00AD0C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00AD4C 26 0 TCT012 0x28 type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00AF4C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00AF8C 26 0 TCT013 0x2A type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00B18C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00B1CC 26 0 TCT014 0x2C type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00B3CC 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00B40C 26 0 TCT015 0x2E type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00B60C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00B64C 26 0 TCT016 0x30 type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00B84C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00B88C 26 0 TCT017 0x32 type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00BA8C 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00BACC 26 0 TCT018 0x34 type2 [5,5,26] [4,64,11]
|
||||
Stage26 0x00BCCC 26 0 ADN128 0x14 type2 [18,5,5] [26,4,64]
|
||||
Stage26 0x00BD0C 26 0 TCT019 0x36 type2 [5,5,26] [4,64,11]
|
||||
Stage27 0x000AAC 29 0 TCN001 0x12 type2 [118,146,12] [15,12,15]
|
||||
Stage27 0x002088 29 100 TCN001 0x12 type2 [127,123,8] [35,11,95]
|
||||
Stage27 0x002160 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage27 0x002570 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage27 0x002BBC 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage27 0x00339C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage28 0x00101C 29 0 TCN001 0x13 type2 [118,146,12] [15,12,30]
|
||||
Stage28 0x0020EC 29 100 TCN001 0x13 type2 [127,123,8] [35,11,95]
|
||||
Stage28 0x0021C4 101 - ? None typeNone [11,95,116] [100,124,93]
|
||||
Stage28 0x00256C 101 - ? None typeNone [20,20,116] [100,124,93]
|
||||
Stage28 0x002958 101 - ? None typeNone [11,9,116] [100,124,93]
|
||||
Stage28 0x002E4C 29 0 ADT102 0x2 type2 [12,12,12] [29,29,136]
|
||||
Stage28 0x002E8C 29 0 ADT103 0x4 type2 [12,12,29] [29,136,13]
|
||||
Stage28 0x002ECC 29 0 ADT104 0x6 type2 [12,29,29] [136,13,1]
|
||||
Stage28 0x003BE8 29 0 ADT107 0xB type2 [12,12,12] [29,29,136]
|
||||
Stage28 0x003C28 29 0 ADT108 0xC type2 [12,12,29] [29,136,13]
|
||||
Stage28 0x003C68 29 0 ADT109 0xD type2 [12,29,29] [136,13,64]
|
||||
Stage28 0x004848 29 0 ADT113 0x7 type2 [12,12,12] [29,29,136]
|
||||
Stage28 0x004888 29 0 ADT114 0x8 type2 [12,12,29] [29,136,13]
|
||||
Stage28 0x0048C8 29 0 ADT115 0xA type2 [12,29,29] [136,13,1]
|
||||
Stage28 0x0050F8 26 0 ADT102 0x2 type2 [11,70,18] [13,70,136]
|
||||
Stage28 0x005330 26 0 ADT103 0x4 type2 [11,70,18] [13,70,11]
|
||||
Stage28 0x005520 26 0 ADT104 0x6 type2 [11,70,18] [13,11,70]
|
||||
Stage28 0x005638 26 0 ADT107 0xB type2 [11,70,18] [13,70,136]
|
||||
Stage28 0x005870 26 0 ADT108 0xC type2 [11,70,18] [13,70,11]
|
||||
Stage28 0x005A60 26 0 ADT109 0xD type2 [11,70,18] [13,11,70]
|
||||
Stage28 0x005B78 26 0 ADT113 0x7 type2 [11,70,18] [13,70,136]
|
||||
Stage28 0x005DB0 26 0 ADT114 0x8 type2 [11,70,18] [13,70,11]
|
||||
Stage28 0x005FA0 26 0 ADT115 0xA type2 [11,70,18] [13,11,64]
|
||||
Stage29 0x0083A0 29 100 TCN001 0x11 type2 [12,12,12] [15,29,15]
|
||||
Stage29 0x008438 29 100 TCN002 0x14 type2 [12,29,15] [15,29,15]
|
||||
Stage29 0x0084D0 29 50 TCN003 0x18 type2 [15,29,15] [15,92,15]
|
||||
Stage29 0x008740 29 75 TCN004 0x1F type2 [15,30,3] [15,3,29]
|
||||
Stage29 0x008878 29 50 TCN005 0x21 type2 [29,15,3] [15,30,19]
|
||||
Stage29 0x008AA4 29 75 TCT006 0x2C type2 [30,19,3] [15,3,3]
|
||||
Stage29 0x008C7C 29 50 TCN007 0x25 type2 [15,3,3] [15,3,3]
|
||||
Stage29 0x008E54 29 50 TCN110 0x17 type2 [15,3,3] [15,3,3]
|
||||
Stage29 0x00902C 29 50 TCN111 0x1D type2 [15,3,3] [15,3,3]
|
||||
Stage29 0x009204 29 50 TCN113 0x22 type2 [15,3,3] [15,30,29]
|
||||
Stage29 0x009308 29 0 TCN008 0x26 type2 [29,15,30] [92,15,48]
|
||||
Stage29 0x0094B4 29 50 TCN009 0x29 type2 [92,15,48] [15,92,29]
|
||||
Stage29 0x0095C0 29 50 TCN114 0x24 type2 [29,15,92] [1,15,92]
|
||||
Stage29 0x0096F0 29 50 TCN116 0x28 type2 [1,15,92] [1,15,48]
|
||||
Stage29 0x00984C 29 50 TCN119 0x2B type2 [1,15,48] [1,1,75]
|
||||
Stage29 0x00CCF0 29 100 TCN001 0x11 type2 [127,123,8] [35,119,11]
|
||||
Stage29 0x010580 101 - ? None typeNone [69,95,116] [100,93,124]
|
||||
Stage29 0x0109A8 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage29 0x011004 101 - ? None typeNone [11,20,116] [100,124,93]
|
||||
Stage29 0x01128C 101 - ? None typeNone [11,9,116] [100,93,124]
|
||||
Stage29 0x01158C 101 - ? None typeNone [6,11,116] [100,93,124]
|
||||
Stage29 0x011D74 29 50 TCN120 0x1E type2 [128,15,92] [11,20,11]
|
||||
Stage29 0x011FD0 29 50 TCN121 0x20 type2 [128,15,92] [11,69,57]
|
||||
Stage29 0x012D0C 28 200 ADN101ea1 0x0 type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x013294 28 200 ADN101ea2 0x1 type2 [1,128,15] [29,145,47]
|
||||
Stage29 0x0132D4 29 50 ADN101ea2 0x1 type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x01381C 28 200 ADN102ea1 0x2 type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x013DA4 28 200 ADN102ea2 0x3 type2 [1,128,15] [29,145,47]
|
||||
Stage29 0x013DE4 29 50 ADN102ea2 0x3 type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x01432C 28 200 ADN103ea1 0x4 type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x0148B4 28 200 ADN103ea2 0x6 type2 [1,128,15] [29,145,47]
|
||||
Stage29 0x0148F4 29 50 ADN103ea2 0x6 type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x014E3C 28 200 ADN104ea1 0x7 type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x0153C4 28 200 ADN104ea2 0xA type2 [1,128,15] [29,145,47]
|
||||
Stage29 0x015404 29 50 ADN104ea2 0xA type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x01594C 28 200 ADN121ea1 0x5 type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x015EB0 28 200 ADN121ea2 0x8 type2 [92,128,15] [29,145,47]
|
||||
Stage29 0x015EF0 29 50 ADN121ea2 0x8 type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x016338 28 200 ADN122ea1 0x9 type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x01689C 28 200 ADN122ea2 0xB type2 [92,128,15] [29,145,47]
|
||||
Stage29 0x0168DC 29 50 ADN122ea2 0xB type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x016D24 28 200 ADN123ea1 0xC type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x017288 28 200 ADN123ea2 0xD type2 [92,128,15] [29,145,47]
|
||||
Stage29 0x0172C8 29 50 ADN123ea2 0xD type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x017710 28 200 ADN124ea1 0xE type2 [145,128,15] [145,47,47]
|
||||
Stage29 0x017C74 28 200 ADN124ea2 0xF type2 [92,128,15] [29,145,47]
|
||||
Stage29 0x017CB4 29 50 ADN124ea2 0xF type2 [128,15,28] [145,47,47]
|
||||
Stage29 0x01C5FC 28 200 ADN101b 0x2F type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x01C71C 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01C83C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01C95C 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01CA7C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01CB9C 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01CCBC 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01CDDC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01CEFC 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D01C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D13C 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D25C 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D37C 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D49C 28 120 ADN107a 0x56 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D5C4 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D6E4 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D804 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01D924 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01DA44 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01DB64 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01DC84 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01DDA4 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01DEC4 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01DFE4 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E104 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E224 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E344 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E464 28 200 ADN104b 0x42 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E58C 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E6AC 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E7CC 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01E8EC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01EA0C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01EB2C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01EC4C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01ED6C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01EE8C 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01EFAC 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F0CC 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F1EC 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F30C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F42C 28 300 ADN104e 0x5C type2 [4,57,15] [1,57,15]
|
||||
Stage29 0x01F52C 28 200 ADN101c 0x32 type2 [1,57,15] [1,4,57]
|
||||
Stage29 0x01F64C 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F76C 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F88C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01F9AC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01FACC 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01FBEC 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01FD0C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01FE2C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x01FF4C 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02006C 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02018C 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0202AC 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0203CC 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0204F4 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020614 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020734 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020854 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020974 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020A94 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020BB4 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020CD4 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020DF4 28 200 ADN103b 0x39 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x020F14 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021034 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021154 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021274 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021394 28 300 ADN104e 0x5C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0214BC 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0215DC 28 300 ADN101e 0x40 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0216FC 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02181C 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02193C 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021A5C 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021B7C 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021C9C 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021DBC 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021EDC 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x021FFC 28 200 ADN101c 0x32 type2 [4,57,15] [1,57,15]
|
||||
Stage29 0x0220F4 28 200 ADN102b 0x33 type2 [1,57,15] [1,4,57]
|
||||
Stage29 0x022214 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022334 28 300 ADN104e 0x5C type2 [4,57,15] [1,57,15]
|
||||
Stage29 0x022434 28 300 ADN101e 0x40 type2 [1,57,15] [1,4,57]
|
||||
Stage29 0x022554 28 200 ADN101b 0x2F type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022674 28 300 ADN102e 0x4B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022794 28 120 ADN101a 0x2E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0228B4 28 200 ADN101c 0x32 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0229D4 28 300 ADN101d 0x37 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022AF4 28 120 ADN102a 0x30 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022C14 28 120 ADN103a 0x34 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022D34 28 120 ADN104a 0x3A type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022E54 28 200 ADN102b 0x33 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x022F74 28 300 ADN103e 0x54 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x023094 28 120 ADN105a 0x43 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0231B4 28 200 ADN102c 0x38 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0232D4 28 120 ADN106a 0x4D type2 [4,57,15] [1,4,11]
|
||||
Stage29 0x023764 28 200 ADN141a 0x3E type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x023EFC 28 200 ADN141b 0x48 type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x023F3C 29 50 ADN141b 0x48 type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x024420 28 200 ADN142a 0x49 type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x024BB8 28 200 ADN142b 0x52 type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x024BF8 29 50 ADN142b 0x52 type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x02517C 28 200 ADN143a 0x53 type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x025914 28 200 ADN143b 0x5A type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x025954 29 50 ADN143b 0x5A type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x025F78 28 200 ADN144a 0x5B type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x026710 28 200 ADN144b 0x61 type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x026750 29 50 ADN144b 0x61 type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x026CD4 28 200 ADN145a 0x62 type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x02746C 28 200 ADN145b 0x65 type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x0274AC 29 50 ADN145b 0x65 type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x02783C 28 200 ADN121b 0x35 type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x02795C 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x027A7C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x027B9C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x027CBC 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x027DDC 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x027EFC 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02801C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02813C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02825C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02837C 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02849C 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0285BC 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0286DC 28 120 ADN127a 0x60 type2 [4,57,15] [1,4,64]
|
||||
Stage29 0x0288A4 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x0289C4 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x028AE4 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x028C04 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x028D24 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x028E44 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x028F64 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029084 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0291A4 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0292C4 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x0293E4 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029504 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029624 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029744 28 200 ADN124b 0x50 type2 [4,57,15] [1,4,64]
|
||||
Stage29 0x0299AC 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x029ACC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029BEC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029D0C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029E2C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x029F4C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A06C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A18C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A2AC 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A3CC 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A4EC 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A60C 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A72C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02A84C 28 300 ADN124e 0x63 type2 [4,57,15] [1,64,64]
|
||||
Stage29 0x02AA8C 28 200 ADN121c 0x3B type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x02ABAC 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02ACCC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02ADEC 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02AF0C 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B02C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B14C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B26C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B38C 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B4AC 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B5CC 28 300 ADN121d 0x44 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B6EC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B80C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02B92C 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,64]
|
||||
Stage29 0x02BB94 28 300 ADN121d 0x44 type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x02BCB4 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02BDD4 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02BEF4 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C014 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C134 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C254 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C374 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C494 28 200 ADN123b 0x46 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C5B4 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C6D4 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C7F4 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02C914 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02CA34 28 300 ADN124e 0x63 type2 [4,57,15] [1,4,64]
|
||||
Stage29 0x02CC9C 28 200 ADN121b 0x35 type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x02CDBC 28 300 ADN121e 0x4E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02CEDC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02CFFC 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D11C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D23C 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D35C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D47C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D59C 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D6BC 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D7DC 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02D8FC 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02DA1C 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02DB3C 28 300 ADN124e 0x63 type2 [4,57,15] [1,64,64]
|
||||
Stage29 0x02DD7C 28 300 ADN121e 0x4E type2 [64,57,15] [1,4,57]
|
||||
Stage29 0x02DE9C 28 200 ADN121b 0x35 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02DFBC 28 300 ADN122e 0x57 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E0DC 28 120 ADN121a 0x31 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E1FC 28 200 ADN121c 0x3B type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E31C 28 300 ADN121d 0x44 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E43C 28 120 ADN122a 0x36 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E55C 28 120 ADN123a 0x3D type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E67C 28 120 ADN124a 0x47 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E79C 28 200 ADN122b 0x3C type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E8BC 28 300 ADN123e 0x5E type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02E9DC 28 120 ADN125a 0x51 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02EAFC 28 200 ADN122c 0x45 type2 [4,57,15] [1,4,57]
|
||||
Stage29 0x02EC1C 28 120 ADN126a 0x59 type2 [4,57,15] [1,4,64]
|
||||
Stage29 0x02F1EC 28 200 ADN146a 0x66 type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x02F960 28 200 ADN146b 0x67 type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x02F9A0 29 50 ADN146b 0x67 type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x02FF24 28 200 ADN147a 0x68 type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x030698 28 200 ADN147b 0x69 type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x0306D8 29 50 ADN147b 0x69 type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x030C5C 28 200 ADN148a 0x6A type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x0313D0 28 200 ADN148b 0x6B type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x031410 29 50 ADN148b 0x6B type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x031994 28 200 ADN149a 0x6C type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x032108 28 200 ADN149b 0x6D type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x032148 29 50 ADN149b 0x6D type2 [16,16,28] [145,47,47]
|
||||
Stage29 0x0326CC 28 200 ADN150a 0x3F type2 [16,16,16] [145,47,47]
|
||||
Stage29 0x032E40 28 200 ADN150b 0x4A type2 [16,16,16] [29,145,47]
|
||||
Stage29 0x032E80 29 50 ADN150b 0x4A type2 [16,16,28] [145,47,47]
|
||||
|
||||
# builtin 26: 97 sites, 7 distinct values
|
||||
# values: 0 x69, 80 x10, 100 x6, 60 x5, 30 x3, 50 x3, 40 x1
|
||||
# units: 66 distinct, top: ADN128 x8, TCN001 x7, ADN207 x4, TCN208 x4, TCN002 x3, ADT102 x3, ADT104 x3, ADT107 x3, ADT101 x2, ADT103 x2
|
||||
# craft cross-tab (0 unresolved):
|
||||
# UN_e201_ADAN_ISCMissile 17 0x17
|
||||
# UN_n001_TTRL_Box 16 0x16
|
||||
# UN_f202_TCAF_Cargo 9 0x9
|
||||
# UN_e106_ADAN_Destroyer 8 0x4, 80x2, 30x1, 60x1
|
||||
# UN_f106_TCAF_Destroyer 8 0x4, 60x2, 80x1, 50x1
|
||||
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 6 100x5, 40x1
|
||||
# UN_f105_TCAF_Cruiser 5 0x3, 80x1, 50x1
|
||||
# UN_e015_ADAN_Puppy_2 5 0x5
|
||||
# UN_e015_ADAN_Puppy 5 0x5
|
||||
# UN_e108_ADAN_ASFrigate 4 80x2, 60x1, 30x1
|
||||
# UN_f001_TCAF_DeltaSaber_T 3 80x1, 50x1, 30x1
|
||||
# UN_mn500_ADAN_FloatingMine 3 0x3
|
||||
# UN_e105_ADAN_Cruiser 2 80x1, 60x1
|
||||
# UN_e011_ADAN_Attacker_B 2 0x2
|
||||
# UN_f101_TCAF_Acropolis 1 80x1
|
||||
# UN_f002_TCAF_DeltaSaber_W 1 80x1
|
||||
# UN_n001_TTRL_Box_move 1 0x1
|
||||
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl2 1 100x1
|
||||
# UN_f001_TCAF_DeltaSaber_T_Ttrl 1 100x1
|
||||
|
||||
# builtin 28: 410 sites, 13 distinct values
|
||||
# values: 200 x116, 120 x76, 300 x74, 500 x48, 150 x37, 100 x29, 1000 x8, 2000 x5, 0.1 x5, 0 x5, 600 x4, 10 x2, 50 x1
|
||||
# units: 190 distinct, top: TCN001 x13, ADN101b x7, ADN101a x7, ADN102a x7, ADN102b x7, ADN103a x7, ADN104a x7, ADN101e x7, ADN102e x7, ADN121b x7
|
||||
# craft cross-tab (0 unresolved):
|
||||
# UN_e106_ADAN_Destroyer 105 120x76, 150x15, 100x12, 200x2
|
||||
# UN_e104_ADAN_Carrier 55 300x54, 150x1
|
||||
# UN_e105_ADAN_CruiserEX 52 200x52
|
||||
# UN_e102_ADAN_BattleshipEX 26 200x26
|
||||
# UN_e001_ADAN_Elan 24 200x18, 300x6
|
||||
# UN_e010_ADAN_Attacker_S 21 200x18, 100x3
|
||||
# UN_e004_ADAN_ElanPlus_N 19 500x19
|
||||
# UN_e009_ADAN_Phantom 16 500x16
|
||||
# UN_f001_TCAF_DeltaSaber_T_Player_Ttrl1 10 0x5, 100x5
|
||||
# UN_e007_ADAN_Turret 9 100x9
|
||||
# UN_e002_ADAN_Elan_N 9 500x9
|
||||
# UN_e105_ADAN_Cruiser 8 150x8
|
||||
# UN_e101_ADAN_SDBattleshipEX 8 300x8
|
||||
# UN_f001_TCAF_DeltaSaber_T 7 150x4, 10x2, 50x1
|
||||
# UN_e011_ADAN_Attacker_B 6 300x6
|
||||
# UN_f002_TCAF_DeltaSaber_W 5 150x5
|
||||
# UN_e003_ADAN_ElanPlus_Margras 4 2000x4
|
||||
# UN_e108_ADAN_ASFrigateEX 4 600x4
|
||||
# UN_f106_TCAF_Destroyer_Inv 3 0.1x2, 150x1
|
||||
# UN_e001_ADAN_Elan_GR_Violeta 3 1000x3
|
||||
# UN_e001_ADAN_Elan_GR 3 1000x3
|
||||
# UN_e101_ADAN_SDBattleship 3 150x3
|
||||
# UN_f101_TCAF_Acropolis 2 150x1, 0.1x1
|
||||
# UN_e013_ADAN_ElanPlus_Taskent 2 1000x2
|
||||
# UN_f002_TCAF_DeltaSaber_W_Player 2 150x2
|
||||
# UN_e011_ADAN_Attacker_B_HF 2 500x2
|
||||
# UN_e102_ADAN_Battleship 2 150x2
|
||||
# UN_f106_TCAF_Destroyer 1 150x1
|
||||
# UN_f001_TCAF_DeltaSaber_T_Player 1 150x1
|
||||
# UN_f105_TCAF_Cruiser 1 0.1x1
|
||||
# UN_f102_TCAF_LightCarrier_Inv 1 0.1x1
|
||||
# UN_e005_ADAN_ElanTypeQ_Margras 1 2000x1
|
||||
# UN_e108_ADAN_ASFrigate 1 150x1
|
||||
# UN_e011_ADAN_Attacker_B_HF_Wayne 1 500x1
|
||||
# UN_e006_ADAN_Vindicator_Margras 1 500x1
|
||||
|
||||
# builtin 29: 164 sites, 6 distinct values
|
||||
# values: 0 x64, 100 x53, 50 x38, 40 x6, 75 x2, 20 x1
|
||||
# units: 70 distinct, top: TCN001 x71, TCN002 x10, TCT207 x4, TCN008 x3, ADT108 x3, ADT109 x3, ADT103 x3, TCN003 x2, ADS151a x2, ADS151b x2
|
||||
# craft cross-tab (0 unresolved):
|
||||
# UN_f002_TCAF_DeltaSaber_W_Player 41 100x23, 0x18
|
||||
# UN_f001_TCAF_DeltaSaber_T 40 0x25, 100x15
|
||||
# UN_f002_TCAF_DeltaSaber_W 38 100x21, 0x16, 50x1
|
||||
# UN_f001_TCAF_DeltaSaber_T_Player 30 0x15, 100x15
|
||||
# UN_e010_ADAN_Attacker_S 18 50x18
|
||||
# UN_f202_TCAF_Cargo 14 50x8, 0x4, 100x2
|
||||
# UN_e201_ADAN_ISCMissile 9 100x8, 20x1
|
||||
# UN_n001_TTRL_Box 9 0x9
|
||||
# UN_e104_ADAN_Carrier 6 0x3, 100x3
|
||||
# UN_f003_TCAF_ArrowHead 5 50x5
|
||||
# UN_e001_ADAN_Elan_GR_Violeta 3 40x3
|
||||
# UN_e001_ADAN_Elan_GR 3 40x3
|
||||
# UN_e006_ADAN_Vindicator_MargrasF 3 0x3
|
||||
# UN_f004_TCAF_DeltaSaber_A_Player 2 0x1, 100x1
|
||||
# UN_f001_TCAF_DeltaSaber_T_EX5_el 2 0x1, 100x1
|
||||
# UN_f106_TCAF_Destroyer 2 50x2
|
||||
# UN_f105_TCAF_Cruiser 2 50x2
|
||||
# UN_be005_ADAN_SpaceFortress 1 0x1
|
||||
# UN_f101_TCAF_Acropolis 1 75x1
|
||||
# UN_f104_TCAF_Battleship 1 75x1
|
||||
# UN_f102_TCAF_LightCarrier 1 50x1
|
||||
# UN_e004_ADAN_ElanPlus_NF 1 50x1
|
||||
|
||||
# builtin 101: 133 sites, 1 distinct values
|
||||
# values: - x133
|
||||
# units: 1 distinct, top: ? x133
|
||||
# craft cross-tab (133 unresolved):
|
||||
51
docs/re/data/kf-angle-census.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
# tools/re-capture/kf_rotation_census.py over dat/GP_*.pak — 2026-08-28
|
||||
# CONTROL must read 3/3; see the tool docstring for why.
|
||||
|
||||
CONTROL ptloop01: 3/3 blocks found, +12 = {30} (want 30)
|
||||
CONTROL ptloop02: 3/3 blocks found, +12 = {-45} (want -45)
|
||||
|
||||
blocks scanned disc-wide: 83862
|
||||
+4: non-zero in 3990 ( 4.76 %)
|
||||
+8: non-zero in 3878 ( 4.62 %)
|
||||
+12: non-zero in 12164 (14.50 %)
|
||||
|
||||
value histogram (non-zero), top 25:
|
||||
+4 = 180 x3880
|
||||
+8 = 180 x3102
|
||||
+12 = 90 x1824
|
||||
+12 = -90 x1176
|
||||
+12 = 360 x1173
|
||||
+12 = 120 x492
|
||||
+12 = 180 x474
|
||||
+12 = -58 x402
|
||||
+12 = 53 x396
|
||||
+12 = -125 x378
|
||||
+12 = -120 x366
|
||||
+12 = -66 x294
|
||||
+12 = 114 x240
|
||||
+8 = 90 x201
|
||||
+12 = 129 x200
|
||||
+12 = -43 x199
|
||||
+12 = 115 x186
|
||||
+12 = -360 x180
|
||||
+8 = -180 x156
|
||||
+12 = 58 x156
|
||||
+8 = 178 x144
|
||||
+12 = -33 x138
|
||||
+12 = 124 x138
|
||||
+12 = -133 x126
|
||||
+12 = 130 x126
|
||||
|
||||
examples:
|
||||
+12 ('GP_BUNK', '117ca14f', '0x73961b', 360)
|
||||
+12 ('GP_BUNK', '117ca14f', '0x7398ac', 360)
|
||||
+12 ('GP_BUNK', '117ca14f', '0x739a93', 360)
|
||||
+12 ('GP_BUNK', '117ca14f', '0x739d24', 360)
|
||||
+4 ('GP_BUNK', '117ca14f', '0x73ae5d', -180)
|
||||
+4 ('GP_BUNK', '117ca14f', '0x73ae85', -180)
|
||||
+4 ('GP_BUNK', '1ff0bcb0', '0x733d68', -180)
|
||||
+4 ('GP_BUNK', '1ff0bcb0', '0x733d90', -180)
|
||||
+8 ('GP_DEBRIEFING_PILOTLOG', '18ab1d66', '0x6f8', 90)
|
||||
+8 ('GP_DEBRIEFING_PILOTLOG', '18ab1d66', '0x770', 90)
|
||||
+8 ('GP_DEBRIEFING_PILOTLOG', '1c8a0129', '0x6f8', 90)
|
||||
+8 ('GP_DEBRIEFING_PILOTLOG', '1c8a0129', '0x770', 90)
|
||||
97
docs/re/data/movie-audio-layouts.csv
Normal file
@@ -0,0 +1,97 @@
|
||||
ADV.wmv,wmapro,48000,6,5.1
|
||||
RT01A.wmv,wmav2,48000,2,unknown
|
||||
RT01B.wmv,wmav2,48000,2,unknown
|
||||
RT01C_1.wmv,wmav2,48000,2,unknown
|
||||
RT01C_2.wmv,wmav2,48000,2,unknown
|
||||
RT02A.wmv,wmav2,48000,2,unknown
|
||||
RT02B.wmv,wmav2,48000,2,unknown
|
||||
RT02C.wmv,wmav2,48000,2,unknown
|
||||
RT02D_1.wmv,wmav2,48000,2,unknown
|
||||
RT02D_2.wmv,wmav2,48000,2,unknown
|
||||
RT03A.wmv,wmav2,48000,2,unknown
|
||||
RT03B.wmv,wmav2,48000,2,unknown
|
||||
RT03C.wmv,wmav2,48000,2,unknown
|
||||
RT03D.wmv,wmav2,48000,2,unknown
|
||||
RT04A.wmv,wmav2,48000,2,unknown
|
||||
RT04B.wmv,wmav2,48000,2,unknown
|
||||
RT05A.wmv,wmav2,48000,2,unknown
|
||||
RT05B.wmv,wmav2,48000,2,unknown
|
||||
RT05C.wmv,wmav2,48000,2,unknown
|
||||
RT06A.wmv,wmav2,48000,2,unknown
|
||||
RT06B.wmv,wmav2,48000,2,unknown
|
||||
RT06C.wmv,wmav2,48000,2,unknown
|
||||
RT06D.wmv,wmav2,48000,2,unknown
|
||||
RT07A.wmv,wmav2,48000,2,unknown
|
||||
RT07B.wmv,wmav2,48000,2,unknown
|
||||
RT07C.wmv,wmav2,48000,2,unknown
|
||||
RT08A.wmv,wmav2,48000,2,unknown
|
||||
RT08B.wmv,wmav2,48000,2,unknown
|
||||
RT08C.wmv,wmav2,48000,2,unknown
|
||||
RT09A.wmv,wmav2,48000,2,unknown
|
||||
RT09B.wmv,wmav2,48000,2,unknown
|
||||
RT09C.wmv,wmav2,48000,2,unknown
|
||||
RT09D.wmv,wmav2,48000,2,unknown
|
||||
RT10A.wmv,wmav2,48000,2,unknown
|
||||
RT10B.wmv,wmav2,48000,2,unknown
|
||||
RT11A.wmv,wmav2,48000,2,unknown
|
||||
RT11B.wmv,wmav2,48000,2,unknown
|
||||
RT11C.wmv,wmav2,48000,2,unknown
|
||||
RT12A.wmv,wmav2,48000,2,unknown
|
||||
RT12B_1.wmv,wmav2,48000,2,unknown
|
||||
RT12B_2.wmv,wmav2,48000,2,unknown
|
||||
RT13A.wmv,wmav2,48000,2,unknown
|
||||
RT13B_1.wmv,wmav2,48000,2,unknown
|
||||
RT13B_2.wmv,wmav2,48000,2,unknown
|
||||
RT14A.wmv,wmav2,48000,2,unknown
|
||||
RT14B.wmv,wmav2,48000,2,unknown
|
||||
RT14C.wmv,wmav2,48000,2,unknown
|
||||
RT15A.wmv,wmav2,48000,2,unknown
|
||||
RT15B.wmv,wmav2,48000,2,unknown
|
||||
RT15C.wmv,wmav2,48000,2,unknown
|
||||
RT16C.wmv,wmav2,48000,2,unknown
|
||||
S00A.wmv,wmapro,48000,6,5.1
|
||||
S01A.wmv,wmapro,48000,6,5.1
|
||||
S02A.wmv,wmapro,48000,6,5.1
|
||||
S02B.wmv,wmapro,48000,6,5.1
|
||||
S02C.wmv,wmapro,48000,6,5.1
|
||||
S03A.wmv,wmapro,48000,6,5.1
|
||||
S04A.wmv,wmapro,48000,6,5.1
|
||||
S04B.wmv,wmapro,48000,6,5.1
|
||||
S05A.wmv,wmapro,48000,6,5.1
|
||||
S06A.wmv,wmapro,48000,6,5.1
|
||||
S06B.wmv,wmapro,48000,6,5.1
|
||||
S07A.wmv,wmapro,48000,6,5.1
|
||||
S07B.wmv,wmapro,48000,6,5.1
|
||||
S09B.wmv,wmapro,48000,6,5.1
|
||||
S10B.wmv,wmapro,48000,6,5.1
|
||||
S11A.wmv,wmapro,48000,6,5.1
|
||||
S11C.wmv,wmapro,48000,6,5.1
|
||||
S12A.wmv,wmapro,48000,6,5.1
|
||||
S12B.wmv,wmapro,48000,6,5.1
|
||||
S12C.wmv,wmapro,48000,6,5.1
|
||||
S13A.wmv,wmapro,48000,6,5.1
|
||||
S13B.wmv,wmapro,48000,6,5.1
|
||||
S14A.wmv,wmapro,48000,6,5.1
|
||||
S15A.wmv,wmapro,48000,6,5.1
|
||||
S15B.wmv,wmapro,48000,6,5.1
|
||||
S15C.wmv,wmapro,48000,6,5.1
|
||||
S16A.wmv,wmapro,48000,6,5.1
|
||||
hokyu_DS_s02A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_DS_s07A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_DS_s07H.wmv,wmav2,48000,2,unknown
|
||||
hokyu_DS_s08A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_DS_s13A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_DS_s14H.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s02A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s02H.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s03A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s03H.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s06A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s06H.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s09A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s09H.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s11A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s14A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s15A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s24A.wmv,wmav2,48000,2,unknown
|
||||
hokyu_LS_s27A.wmv,wmav2,48000,2,unknown
|
||||
|
57
docs/re/data/name-lookup-callsites.txt
Normal file
@@ -0,0 +1,57 @@
|
||||
The r5 (name) argument at every call site of sub_821CC860.
|
||||
Recovered by forward register simulation over the 100 instructions before
|
||||
each call, tracking lis/addi/subi/mr. Gated on three sites read by eye:
|
||||
0x821c6524 TITLE_SCREEN, 0x821c67c8 TITLE_MENU, 0x821c73c8 LOADING -- all three
|
||||
reproduce, so the remaining rows are trusted.
|
||||
|
||||
call site caller r5 name
|
||||
0x82185f14 sub_82185E80 0x820a2160 EXTRA
|
||||
0x821860b4 sub_82186050 0x820a214c LOADING
|
||||
0x82186350 sub_82186270 0x820a2140 NEW_ITEM
|
||||
0x821863f8 sub_82186270 0x820a2140 NEW_ITEM
|
||||
0x8218c094 sub_8218BFE0 0x820a2384 MENU
|
||||
0x8219403c sub_82193648 - (not recovered)
|
||||
0x82196b44 sub_82196A50 0x820a2168 FILE
|
||||
0x82198540 sub_82198440 0x820a2570 BASE
|
||||
0x821988e4 sub_82198808 0x820a270c MESSAGE
|
||||
0x82198a5c sub_82198808 0x820a28ec MENU_DISABLE_SKIP
|
||||
0x8219c3b4 sub_8219C328 0x820a2acc INFO
|
||||
0x821a0b4c sub_821A0AD0 0x820a255c FADE
|
||||
0x821a0b98 sub_821A0AD0 0x820a2ae8 BG
|
||||
0x821a0bdc sub_821A0AD0 0x820a2adc DEBRIEFING
|
||||
0x821a1028 sub_821A0F80 0x820a2ab4 LOCAL_BOARD
|
||||
0x821a113c sub_821A0F80 0x820a255c FADE
|
||||
0x821a1188 sub_821A0F80 0x820a2ae8 BG
|
||||
0x821a129c sub_821A0F80 0x820a2554 TITLE
|
||||
0x821a12e8 sub_821A0F80 0x820a2ad4 BLACK
|
||||
0x821a13b4 sub_821A0F80 0x820a2ab4 LOCAL_BOARD
|
||||
0x821a1a6c sub_821A19B0 0x820a2ac0 LIVE_BOARD
|
||||
0x821a1bd4 sub_821A19B0 0x820a255c FADE
|
||||
0x821a1c20 sub_821A19B0 0x820a2ae8 BG
|
||||
0x821a1d34 sub_821A19B0 0x820a2554 TITLE
|
||||
0x821a1d80 sub_821A19B0 0x820a2ad4 BLACK
|
||||
0x821a1e4c sub_821A19B0 0x820a2ac0 LIVE_BOARD
|
||||
0x821a2b48 sub_821A2A80 0x820a2a74 DETAIL
|
||||
0x821b368c sub_821B34E8 0x820a2570 BASE
|
||||
0x821b3780 sub_821B34E8 0x820a32ac WINDOW
|
||||
0x821b9210 sub_821B9118 0x820a2168 FILE
|
||||
0x821bc60c sub_821BC568 - (not recovered)
|
||||
0x821bca58 sub_821BC978 0x820a2384 MENU
|
||||
0x821be188 sub_821BDF88 0x820a2570 BASE
|
||||
0x821be1ec sub_821BDF88 0x820a38ac TUTORIAL_MENU
|
||||
0x821c1d30 sub_821C1B58 0x820a2168 FILE
|
||||
0x821c2f04 sub_821C2EA0 0x820a2168 FILE
|
||||
0x821c5c88 sub_821C5818 0x820a3d3c TITLE_SCREEN
|
||||
0x821c5d70 sub_821C5818 0x820a339c BUTTON
|
||||
0x821c6524 sub_821C6458 0x820a3d3c TITLE_SCREEN
|
||||
0x821c67c8 sub_821C6458 0x820a3d30 TITLE_MENU
|
||||
0x821c73c8 sub_821C6458 0x820a214c LOADING
|
||||
0x821c9488 sub_821C93C8 0x820a2168 FILE
|
||||
0x822b2e04 sub_822B2D80 0x820ab24c KEY
|
||||
0x822b581c sub_822B5790 0x820ab2a4 GAMMA_TITLE
|
||||
0x822b5914 sub_822B5790 0x820ab298 GAMMA_RGB
|
||||
0x822b71ac sub_822B7128 0x820a99dc GAME
|
||||
0x822b8700 sub_822B8670 0x820a3124 PAD
|
||||
0x822b9ebc sub_822B9E38 0x820a1784 SOUND
|
||||
|
||||
46/48 recovered.
|
||||
23
docs/re/data/order-crosscheck-reach.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# How much of each recorded paint order its draw capture actually cross-checked.
|
||||
# 2026-08-29, tools/re-capture/order_crosscheck_reach.py
|
||||
#
|
||||
# The three measured_paint_order vectors are reads of the runtime CHILD ARRAY
|
||||
# (ui-screen-runtime.md: "paint order (child slots)"). These captures are the
|
||||
# cross-check, not the source. Quads = indices/4: a single draw can carry
|
||||
# several (the menu's draw 9 has indices=24 = 6 quads), so counting draws
|
||||
# under-reads what is on screen.
|
||||
|
||||
== docs/re/captures/title-screen-draw-order.log
|
||||
frame 1: 8 draws, 12 quads, 5 textures
|
||||
frame 2: 8 draws, 12 quads, 5 textures
|
||||
frame 3: 8 draws, 12 quads, 5 textures
|
||||
textures in EVERY captured frame: 5 of 5
|
||||
== docs/re/captures/title-draw-order-with-alpha.log
|
||||
frame 1: 8 draws, 12 quads, 5 textures
|
||||
frame 2: 8 draws, 12 quads, 5 textures
|
||||
textures in EVERY captured frame: 5 of 5
|
||||
== docs/re/captures/main-menu-draw-order.log
|
||||
frame 0: 9 draws, 16 quads, 5 textures
|
||||
frame 3: 7 draws, 9 quads, 4 textures
|
||||
textures in EVERY captured frame: 4 of 5
|
||||
absent from some frame: ['11C30000']
|
||||
35
docs/re/data/paint-order-audit.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# cargo run -p sylpheed-formats --example paint_order_audit -- dat/GP_TITLE.pak
|
||||
# 2026-08-29
|
||||
|
||||
1682 | let decl = &decls[0];
|
||||
290 | let mut flush = |base: u32,
|
||||
entry 0 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||
entry 1 (no measured order) 7 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||
entry 2 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||
entry 3 (no measured order) 1 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||
entry 4 title 24 elements
|
||||
derived == measured : NO
|
||||
inverted pairs : 8 (of which same-layer-key ties: 8)
|
||||
measured: [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8]
|
||||
derived : [9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 16, 17, 18, 0, 1, 2, 3, 4, 5, 7, 22, 23, 21, 8]
|
||||
keys : [32928, 32928, 32928, 32928, 32928, 32928, 32832, 32928, 4294967295, 32768, 4294967295, 32784, 32784, 4294967295, 32899, 32899, 32899, 32899, 32899, 32898, 32897, 33024, 32936, 32937]
|
||||
entry 5 main menu 16 elements
|
||||
derived == measured : YES
|
||||
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||
entry 6 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
|
||||
entry 7 (no measured order) 30 elements, 37 tied pairs, 16 of them OVERLAPPING
|
||||
entry 8 main menu 16 elements
|
||||
derived == measured : YES
|
||||
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||
entry 9 (no measured order) 18 elements, 15 tied pairs, 2 of them OVERLAPPING
|
||||
entry 10 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||
entry 11 splash 7 elements
|
||||
derived == measured : YES
|
||||
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||
entry 12 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||
entry 13 (no measured order) 3 elements, 0 tied pairs, 0 of them OVERLAPPING
|
||||
entry 14 splash 7 elements
|
||||
derived == measured : YES
|
||||
inverted pairs : 0 (of which same-layer-key ties: 0)
|
||||
entry 15 (no measured order) 10 elements, 2 tied pairs, 1 of them OVERLAPPING
|
||||
5 build(s) with a measured order were checked
|
||||
25
docs/re/data/plateau-census.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
CONTROL GP_TITLE build 7: fallback elements [8, 13, 14] (want [8, 13, 14])
|
||||
index 8 is 'ptlogo_eff3.t32' (want ptlogo_eff3.t32)
|
||||
|
||||
elements with a keyframe group, disc-wide: 15493
|
||||
no plateau -> rest pose is GUESSED: 3807 (24.57 %)
|
||||
|
||||
most common guessed elements:
|
||||
1002 pb_name_eff.t32
|
||||
90 gbp301i.t32
|
||||
84 pb_warp_eff.t32
|
||||
78 pbb_destroyer.t32
|
||||
70 gbp303i.t32
|
||||
60 pb_w_line.t32
|
||||
54 pbmwindow_eff.t32
|
||||
54 pbfriendly.t32
|
||||
32 pzeff00.prm
|
||||
32 pzeff02.t32
|
||||
32 px_mission_area.rat
|
||||
30 pbr_fighter.t32
|
||||
|
||||
--- the 3807 plateau-less elements, by what each candidate rule returns
|
||||
current rule (longest dwell) returns an INVISIBLE pose : 1711 ( 44.9 %)
|
||||
current rule returns a ZERO-SCALE (degenerate) pose : 195 ( 5.1 %)
|
||||
'rest = last keyframe' returns an INVISIBLE pose : 1618 ( 42.5 %)
|
||||
the two rules agree : 1911 ( 50.2 %)
|
||||
16
docs/re/data/plateauless-endstate.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
# tools/re-capture/plateauless_endstate.py over dat/GP_*.pak -- 2026-08-29
|
||||
|
||||
plateau-less, multi-keyframe, ending VISIBLE: 687
|
||||
alpha never decreases (a fade/slide IN that stops): 472 (68.7 %)
|
||||
ends at its maximum alpha but dips on the way : 203 (29.5 %)
|
||||
ends visible but NOT at maximum alpha : 12 (1.7 %)
|
||||
|
||||
examples of the monotone kind (alpha sequence, then scale pairs):
|
||||
GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||
GP_BUNK.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||
GP_DEBRIEFING_PILOTLOG.pak pjex_eff.rat [0, 255] [(100, 100), (100, 100)]
|
||||
GP_MAIN_GAME_D2D.pak pghud_speed_cut.t32 [0, 255] [(100, 100), (100, 100)]
|
||||
GP_MAIN_GAME_D2D.pak pgmsg_restart_sub.rat [255, 255] [(106, 106), (100, 100)]
|
||||
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
@@ -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
@@ -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
|
||||
```
|
||||
8
docs/re/data/rest-rule-blast-radius.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
# tools/re-capture/rest_rule_blast_radius.py over dat/GP_*.pak -- 2026-08-29
|
||||
|
||||
genuinely ambiguous elements (plateau-less, 2+ keyframes): 2305
|
||||
the two rules AGREE on : 409 (17.7 %)
|
||||
they DIFFER on : 1896 (82.3 %)
|
||||
|
||||
rule dwell : returns an INVISIBLE pose 1711 ( 74.2 %), zero-scale 195 ( 8.5 %)
|
||||
rule last : returns an INVISIBLE pose 1618 ( 70.2 %), zero-scale 43 ( 1.9 %)
|
||||
7
docs/re/data/rotation-top-level-examples.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# `sylpheed-cli screen info --geometry`, rows with a non-zero rotation (r=).
|
||||
# GP_DIALOG builds 0 and 6; GP_DEBRIEFING_PILOTLOG build 10. 2026-08-28
|
||||
|
||||
4 pceff03.t32 247x281 38x38 NO 8: 630,348 100%,100% a=0 r=90 12: 550,273 100%,100% a=128 r=30 14: 510,235 100%,100% a=192 r=10 16: 490,217 100%,100% a=224 r=3 52: 470,198 100%,100% a=255 58: 470,198 100%,100% a=255 -: 470,198 100%,100% a=0
|
||||
5 pceff04.t32 257x311 478x586 NO 8: 413,77 100%,100% a=0 r=90 12: 483,167 100%,100% a=128 r=30 14: 518,212 100%,100% a=192 r=10 16: 536,234 100%,100% a=224 r=3 52: 553,247 100%,100% a=255 58: 553,247 100%,100% a=255 -: 553,247 100%,100% a=0
|
||||
1 pzeff02.t32 529x519 528x524 NO 197: 376,58 0%,0% a=0 217: 376,58 112%,112% a=255 r=43 232: 376,58 145%,145% a=64 r=61 237: 376,58 173%,173% a=32 r=75 -: 376,58 200%,200% a=0 r=90
|
||||
5 pjeff24a.t32 382x140 610x622 NO -: 335,49 210%,210% a=53 r=90
|
||||
32
docs/re/data/rotation-toplevel-census.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
CONTROL GP_TITLE build 4 (nested rotations only): 0 top-level rotated (want 0)
|
||||
CONTROL GP_DIALOG build 0: 2 top-level rotated (want 2)
|
||||
[4] pceff03.t32 r = [90, 30, 10, 3, 0, 0, 0]
|
||||
[5] pceff04.t32 r = [90, 30, 10, 3, 0, 0, 0]
|
||||
|
||||
top-level elements with a keyframe group, disc-wide: 15493
|
||||
carrying a non-zero rotation: 2152 (13.89 %)
|
||||
|
||||
by archive:
|
||||
1868 GP_READY_ROOM.pak
|
||||
40 GP_DIALOG.pak
|
||||
37 GP_MAIN_GAME_D2D.pak
|
||||
37 GP_MAIN_GAME_E2D.pak
|
||||
37 GP_MAIN_GAME_F2D.pak
|
||||
37 GP_MAIN_GAME_I2D.pak
|
||||
37 GP_MAIN_GAME_J2D.pak
|
||||
37 GP_MAIN_GAME_S2D.pak
|
||||
10 GP_GAMEOVER.pak
|
||||
8 GP_DEBRIEFING_PILOTLOG.pak
|
||||
4 GP_TITLE.pak
|
||||
|
||||
most common rotated element names:
|
||||
444 pbb_destroyer.t32
|
||||
402 pbr_destroyer.t32
|
||||
276 pbr_fighter.t32
|
||||
174 pbb_fighter.t32
|
||||
168 pbb_aircraft.t32
|
||||
84 pb_warp_eff.t32
|
||||
48 pbtab.t32
|
||||
35 gbp012i.t32
|
||||
32 pzeff02.t32
|
||||
30 gbp001i.t32
|
||||
39
docs/re/data/se-cue-runtime-offsets.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
SE cue waves located inside Static.slb by playing them.
|
||||
|
||||
Method: run Canary with --xma_param_probe=true, drive the main menu, and read the
|
||||
per-stream XMA-PARAM lines. Each line carries the stream's first 32 bytes; those
|
||||
bytes are then searched for in Static.slb's 8 353 472 readable bytes.
|
||||
|
||||
event packets bytes channels/rate offset in Static.slb
|
||||
------------------------- ------- ------ -------------- --------------------
|
||||
d-pad move (cursor) 4 8192 mono 48000 Hz 0x1ec0
|
||||
B / back (cancel) 2 4096 mono 48000 Hz 0x0ec0
|
||||
A / confirm (title->menu) 6 12288 mono 48000 Hz 0x5d6c0
|
||||
|
||||
Each head matched at exactly ONE offset. The first two are contiguous:
|
||||
0x0ec0 + 4096 = 0x1ec0.
|
||||
|
||||
Simultaneously at the main menu, two STEREO 48 kHz streams were decoding:
|
||||
1893 packets / 3 876 864 B and 1919 packets / 3 930 112 B. See
|
||||
structures/bgm-two-stems.md -- this is the runtime observation that the two
|
||||
stems of a music bank play at the same time.
|
||||
|
||||
DECODED 2026-08-28 with tools/re-capture/slb_extract_wave.py + ffmpeg's xma1:
|
||||
|
||||
event offset packets decoded rms peak envelope shape
|
||||
---------------- -------- ------- --------- ----- ----- ------------------------
|
||||
d-pad move 0x1ec0 4 0.533 s 2085 29813 sharp attack, monotonic decay
|
||||
B / back 0x0ec0 2 0.344 s 2985 16973 attack, peak in frame 2
|
||||
(pre-input cue) 0x5d6c0 6 1.016 s 4327 32767 peaks in frame 3, long decay
|
||||
|
||||
CONTROL: the same synthesized RIFF wrapper applied to BGM_001.slb's first wave
|
||||
(offset 14336, stereo) decodes to 173.808875 s -- identical to the duration
|
||||
obtained from that bank's own on-disc RIFF header. The wrapper is correct.
|
||||
|
||||
SECOND RUN 2026-08-28 -- independent boot, same probe:
|
||||
* d-pad move and B reproduced with IDENTICAL head bytes and sizes (2 of 2 runs).
|
||||
* The 12288-byte wave fires on the A that advances title -> main menu, together
|
||||
with the two stereo BGM stems (the menu's music starting). Counting from the
|
||||
title rather than the menu is what attributed it.
|
||||
* Left and Right fired NO new stream -- no distinct invalid cue. (Dedup caveat:
|
||||
a press replaying an already-heard wave would also log nothing.)
|
||||
326
docs/re/data/se-ui-cues.txt
Normal file
@@ -0,0 +1,326 @@
|
||||
Every SE cue on the disc, from tables.pak SOUNDS. 322 cues.
|
||||
BANK_SE names the single bank they all live in: Static.slb
|
||||
0 of the 322 has its own entry in FILES (5 135 bank paths).
|
||||
|
||||
1 SE_UI_START
|
||||
2 SE_UI_CURSOR
|
||||
3 SE_UI_DECIDE
|
||||
4 SE_UI_CANSEL
|
||||
5 SE_UI_IMPOSI
|
||||
6 SE_UI_WAIT
|
||||
7 SE_UI_NEXT
|
||||
8 SE_UI_SUB_WIN_OPN
|
||||
9 SE_UI_SUB_WIN_CLS
|
||||
10 SE_UI_ALART_WIN
|
||||
11 SE_UI_PAUSE
|
||||
12 SE_UI_SPLASH_IN
|
||||
13 SE_UI_SPLASH_OUT
|
||||
14 SE_UI_LOAD_CMP
|
||||
15 SE_TUT_BOX_CLEAR
|
||||
16 SE_TUT_UNIT_IN
|
||||
17 SE_UI_WEAPON_PLAN
|
||||
18 SE_UI_WEAPON_CREATE
|
||||
21 SE_HUD_MIS_LOCK
|
||||
22 SE_HUD_LOCK_ALART
|
||||
23 SE_HUD_WP_EMPTY
|
||||
24 SE_HUD_WP_CHARGED
|
||||
25 SE_HUD_DMG_ALARM
|
||||
26 SE_HUD_OH_ALART
|
||||
27 SE_HUD_OVER_HEAT
|
||||
28 SE_HUD_CALL
|
||||
29 SE_HUD_EMERG_CALL
|
||||
30 SE_HUD_MES_END
|
||||
31 SE_HUD_MES_SHAT
|
||||
32 SE_HUD_MIS_SEARCH
|
||||
33 SE_HUD_INVALID
|
||||
34 SE_HUD_MW_CHG
|
||||
35 SE_HUD_TRG_CHG
|
||||
36 SE_HUD_MOD_CHG
|
||||
37 SE_HUD_RADIO_MENU
|
||||
38 SE_HUD_RADIO_ORDER
|
||||
39 SE_HUD_RADIO_CANSEL
|
||||
40 SE_HUD_TM_USE
|
||||
41 SE_HUD_TM_INP
|
||||
42 SE_HUD_TM_COM
|
||||
43 SE_HUD_TM_OK
|
||||
44 SE_HUD_TM_ERROR
|
||||
45 SE_HUD_RADER_MARKER
|
||||
46 SE_HUD_GUIDE
|
||||
47 SE_HUD_SUB_TARGET
|
||||
48 SE_HUD_SP_CHARGE
|
||||
49 SE_HUD_SP_LV_UP
|
||||
50 SE_BR_PASS
|
||||
51 SE_BR_SHAT
|
||||
52 SE_BR_PROGRESS
|
||||
53 SE_BR_DECO_1
|
||||
54 SE_BR_FRAME
|
||||
55 SE_BR_DECO_2
|
||||
56 SE_BR_HELP
|
||||
57 SE_BR_DISP
|
||||
58 SE_BR_COWIN
|
||||
59 SE_BR_WAIT
|
||||
60 SE_BR_NEXT
|
||||
61 SE_BR_FRIENDRY
|
||||
62 SE_BR_ENEMY
|
||||
63 SE_BR_MESS
|
||||
64 SE_BR_POINT
|
||||
65 SE_BR_MARK
|
||||
66 SE_BR_OUT
|
||||
67 SE_BR_MAIN_OP
|
||||
68 SE_BR_MAIN_CLS
|
||||
69 SE_BR_JUMP_OUT
|
||||
70 SE_BR_JUMP_IN
|
||||
71 SE_BR_PROMETEUS
|
||||
72 SE_BR_PLANET_BROKEN
|
||||
73 SE_HUD_SP_RETURN
|
||||
74 SE_HUD_SP_ATACK
|
||||
75 SE_HUD_SP_SLOW
|
||||
76 SE_HUD_AAM_SITE_OPN
|
||||
77 SE_HUD_AAM_SITE_CLS
|
||||
78 SE_HUD_ASM_SITE_OPN
|
||||
79 SE_HUD_ASM_SITE_CLS
|
||||
81 SE_UI_MISSION_START
|
||||
82 SE_UI_MISSION_UPDATE
|
||||
83 SE_UI_MISSION_END
|
||||
101 SE_SW_nbe01_1
|
||||
102 SE_SW_nbe02_1
|
||||
103 SE_SW_nbe03_1
|
||||
104 SE_SW_nla01_1
|
||||
105 SE_SW_nvu01_1
|
||||
106 SE_SW_nvu02_1
|
||||
107 SE_SW_nvu03_1
|
||||
108 SE_SW_nvu04_1
|
||||
109 SE_SW_nsh01_1
|
||||
110 SE_SW_nsh02_1
|
||||
111 SE_SW_nsh03_1
|
||||
112 SE_SW_nsp01_1
|
||||
113 SE_SW_nsp02_1
|
||||
114 SE_SW_nvu01_2
|
||||
115 SE_SW_nvu01_3
|
||||
116 SE_SW_nvu02_2
|
||||
117 SE_SW_nvu02_3
|
||||
118 SE_SW_nvu02_4
|
||||
119 SE_SW_nvu03_2
|
||||
120 SE_SW_nvu03_3
|
||||
121 SE_SW_nvu04_2
|
||||
122 SE_SW_nla02_1
|
||||
123 SE_SW_SPIT_1
|
||||
201 SE_MW_wvu01_1
|
||||
202 SE_MW_wvu02_1
|
||||
203 SE_MW_wvu03_1
|
||||
204 SE_MW_wvu04_1
|
||||
205 SE_MW_wbe01_1
|
||||
206 SE_MW_wbe02_1
|
||||
207 SE_MW_whb01_1
|
||||
208 SE_MW_whb02_1
|
||||
209 SE_MW_wla01_1
|
||||
210 SE_MW_wla02_1
|
||||
211 SE_MW_wla03_1
|
||||
212 SE_MW_wla04_1
|
||||
213 SE_MW_wAA01_1
|
||||
214 SE_MW_wAA02_1
|
||||
215 SE_MW_wAA03_1
|
||||
216 SE_MW_wAA04_1
|
||||
217 SE_MW_wAA04_2
|
||||
218 SE_MW_wAA05_1
|
||||
219 SE_MW_wAA07_1
|
||||
220 SE_MW_wAA0x_2
|
||||
221 SE_MW_wro01_1
|
||||
222 SE_MW_wro02_1
|
||||
223 SE_MW_wro03_1
|
||||
224 SE_MW_wro04_1
|
||||
225 SE_MW_wAS01_1
|
||||
226 SE_MW_wAS02_1
|
||||
227 SE_MW_wAS03_1
|
||||
228 SE_MW_wAS04_1
|
||||
229 SE_MW_wAS05_1
|
||||
230 SE_MW_wAS0x_2
|
||||
231 SE_MW_wAS0x_3
|
||||
232 SE_MW_wto01_1
|
||||
233 SE_MW_wto01_2
|
||||
234 SE_MW_wto02_1
|
||||
235 SE_MW_wto02_2
|
||||
236 SE_MW_wto03_1
|
||||
237 SE_MW_wto03_2
|
||||
238 SE_MW_wto04_1
|
||||
239 SE_MW_wto04_2
|
||||
240 SE_MW_wbo01_1
|
||||
241 SE_MW_wbo02_1
|
||||
242 SE_MW_wbo03_1
|
||||
243 SE_MW_wbo04_1
|
||||
244 SE_MW_wbo05_1
|
||||
245 SE_MW_wbo06_1
|
||||
246 SE_MW_wbo07_1
|
||||
247 SE_MW_wbo08_1
|
||||
248 SE_MW_wbo09_1
|
||||
249 SE_MW_wKE01_1
|
||||
250 SE_MW_wKE02_1
|
||||
251 SE_MW_wKE03_1
|
||||
252 SE_MW_wgu01_1
|
||||
253 SE_MW_wgu02_1
|
||||
254 SE_MW_wgu03_1
|
||||
255 SE_MW_wgu04_1
|
||||
256 SE_MW_wgu05_1
|
||||
257 SE_MW_wgu05_2
|
||||
258 SE_MW_wgu06_1
|
||||
259 SE_MW_wgu07_1
|
||||
260 SE_MW_wsp01_1
|
||||
261 SE_MW_wsp01_2
|
||||
262 SE_MW_wsp01_3
|
||||
263 SE_MW_L3GP_1
|
||||
264 SE_MW_THOR_1
|
||||
301 SE_SPE_mcm01_1
|
||||
302 SE_SPE_mcm02_1
|
||||
304 SE_SPE_mpr01_1
|
||||
351 SE_SWP_BEAM_1
|
||||
352 SE_SWP_RAILGUN_1
|
||||
353 SE_SWP_PCANNON_1
|
||||
354 SE_SWP_LASER_1
|
||||
355 SE_SWP_BVULCAN_1
|
||||
356 SE_SWP_VULCAN_1
|
||||
357 SE_SWP_AAM_1
|
||||
358 SE_SWP_ASM_1
|
||||
359 SE_SWP_BEAM_2
|
||||
401 SE_EXT_OVER_HEAT
|
||||
402 SE_EXT_OH_RECOVER
|
||||
403 SE_EXT_EMPTY
|
||||
404 SE_EXT_EJECT_H
|
||||
405 SE_EXT_EJECT_M
|
||||
406 SE_EXT_EJECT_L
|
||||
407 SE_EXT_EJECT_FAE
|
||||
408 SE_EXT_MGT_OPN
|
||||
409 SE_EXT_MGT_CLS
|
||||
410 SE_EXT_PHOTON_CHG
|
||||
411 SE_EXT_GRAVITY_CHG
|
||||
412 SE_EXT_RECHARGE
|
||||
413 SE_EXT_RECHG_CMP
|
||||
414 SE_EXT_DOUBLER
|
||||
415 SE_EXT_SPIT_CHG
|
||||
501 SE_DS_ENGINE
|
||||
502 SE_DS_POWER_UP
|
||||
503 SE_DS_AFTER_BURNER
|
||||
504 SE_DS_REVERSE
|
||||
505 SE_DS_COCKPIT
|
||||
506 SE_DS_POWER_DOWN
|
||||
507 SE_DS_AB_END
|
||||
508 SE_DS_SIDE
|
||||
509 SE_DS_ROLL
|
||||
511 SE_ENG_ERAN
|
||||
512 SE_ENG_ERAN_Q
|
||||
513 SE_ENG_VINDI
|
||||
514 SE_ENG_DELTA
|
||||
515 SE_ENG_UNE
|
||||
516 SE_ENG_ADAN
|
||||
517 SE_ENG_SUPPLY
|
||||
518 SE_ENG_ATK_S
|
||||
519 SE_ENG_ATK_L
|
||||
551 SE_ENG_ACRO
|
||||
552 SE_ENG_JUN
|
||||
553 SE_ENG_KUCHI
|
||||
554 SE_ENG_OOGATA
|
||||
555 SE_ENG_YUSOU
|
||||
556 SE_ENG_BS
|
||||
557 SE_ENG_AAF
|
||||
558 SE_ENG_ASF
|
||||
559 SE_ENG_CARRIER
|
||||
560 SE_ENG_MISSILE
|
||||
561 SE_ENG_SIDE_S
|
||||
562 SE_ENG_SIDE_L
|
||||
601 SE_EXP_CRAFT
|
||||
602 SE_EXP_EX_CRAFT
|
||||
603 SE_EXP_ATK
|
||||
604 SE_EXP_OUT
|
||||
605 SE_EXP_BS_PART_L
|
||||
606 SE_EXP_BS_PART_M
|
||||
607 SE_EXP_BS_PART_S
|
||||
608 SE_EXP_BS_BREAK_LL
|
||||
609 SE_EXP_BS_BREAK_L
|
||||
610 SE_EXP_BS_BREAK_M
|
||||
611 SE_EXP_BS_BREAK_S
|
||||
612 SE_EXP_BS_BREAK_SS
|
||||
613 SE_EXP_BS_OMEN
|
||||
614 SE_EXP_BS_BOMB
|
||||
615 SE_EXP_MISSILE
|
||||
616 SE_EXP_TCAF_CRUISER
|
||||
617 SE_EXP_MINE
|
||||
651 SE_IMP_BM_DMG
|
||||
652 SE_IMP_BM
|
||||
653 SE_IMP_LS_DMG
|
||||
654 SE_IMP_LS
|
||||
655 SE_IMP_VULCAN
|
||||
656 SE_IMP_SHOTGUN
|
||||
657 SE_IMP_FIRE
|
||||
658 SE_IMP_MISSILE
|
||||
659 SE_IMP_ROCKET
|
||||
660 SE_IMP_SMOKE
|
||||
661 SE_IMP_TORPEDO
|
||||
662 SE_IMP_BOMB
|
||||
663 SE_IMP_KINETIC
|
||||
681 SE_IMP_BULLET
|
||||
683 SE_IMP_BS_BEAM
|
||||
684 SE_IMP_BS_RGUN
|
||||
685 SE_IMP_BS_PCANNON
|
||||
686 SE_IMP_BS_LASER
|
||||
687 SE_IMP_BS_BVULCAN
|
||||
688 SE_IMP_BS_VULCAN
|
||||
689 SE_IMP_BS_AAM
|
||||
690 SE_IMP_BS_ASM
|
||||
691 SE_IMP_GRV
|
||||
701 SE_SHIELD_DOWN
|
||||
702 SE_SHIELD_RECHARGE
|
||||
703 SE_SHIELD_DOWN_BS
|
||||
751 SE_BOSS_CRY
|
||||
752 SE_BOSS_CLOSE
|
||||
753 SE_BOSS_OPEN
|
||||
754 SE_BOSS_STUMBLE
|
||||
755 SE_BOSS_MOMENTS
|
||||
756 SE_BOSS_ATK_A
|
||||
758 SE_BOSS_ATK_C
|
||||
759 SE_BOSS_LASER
|
||||
760 SE_BOSS_HOMING
|
||||
761 SE_BOSS_CHARGE
|
||||
762 SE_BOSS_CORE_CHARGE
|
||||
763 SE_BOSS_SPREAD
|
||||
764 SE_CORE_COLLAPSE
|
||||
781 SE_PROME_AMB_1
|
||||
782 SE_PROME_AMB_2
|
||||
783 SE_PROME_AMB_3
|
||||
784 SE_PROME_FIRE
|
||||
785 SE_ENV_MINE
|
||||
786 SE_HNG_ARM_MV
|
||||
787 SE_HNG_MOUNT
|
||||
788 SE_HNG_UN_MOUNT
|
||||
789 SE_HNG_ARM_UP
|
||||
790 SE_HNG_ARM_DWN
|
||||
791 SE_HNG_MOUNT_2
|
||||
792 SE_HNG_UN_MOUNT_2
|
||||
793 SE_HNG_MOUNT_3
|
||||
794 SE_HNG_UN_MOUNT_3
|
||||
795 SE_HNG_MOUNT_4
|
||||
796 SE_HNG_UN_MOUNT_4
|
||||
807 SE_ETC_JMP_OP
|
||||
808 SE_ETC_JMP_IN
|
||||
809 SE_ETC_JMP_OUT
|
||||
810 SE_ENV_BS_M
|
||||
811 SE_ENV_BS_S
|
||||
812 SE_ENV_BS_L
|
||||
813 SE_ENV_BS_LL
|
||||
814 SE_ENV_CARRIER
|
||||
815 SE_ETC_HP_UNE
|
||||
816 SE_ETC_HP_ADAN
|
||||
817 SE_ETC_HP_ACRO
|
||||
818 SE_ENV_WIND
|
||||
819 SE_ENV_WIND_TURN
|
||||
820 SE_ETC_TURRET_L
|
||||
821 SE_ETC_TURRET_M
|
||||
822 SE_ETC_TURRET_S
|
||||
823 SE_PASS_DS
|
||||
824 SE_PASS_ADAN
|
||||
825 SE_PASS_ERAN
|
||||
826 SE_PASS_PLUS
|
||||
827 SE_PASS_TYPEQ
|
||||
828 SE_PASS_VINDI
|
||||
829 SE_ETC_COLLISION
|
||||
830 SE_ETC_SHIELD_DMG
|
||||
900 SE_LFE_BOMB
|
||||
901 SE_LFE_ENG
|
||||
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.
|
||||
22
docs/re/data/slow-xpr-files.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
# .xpr files whose `mesh info` decode exceeds 25 s (debug build), of 166 scanned
|
||||
# 2026-08-29. Stage_S02 measured at 144 s to completion, rc=0 -- heavy, not hung.
|
||||
|
||||
Hangar.xpr
|
||||
Stage_S02.xpr
|
||||
Stage_S03.xpr
|
||||
Stage_S04.xpr
|
||||
Stage_S06.xpr
|
||||
Stage_S07.xpr
|
||||
Stage_S08.xpr
|
||||
Stage_S09.xpr
|
||||
Stage_S11.xpr
|
||||
Stage_S12.xpr
|
||||
Stage_S13.xpr
|
||||
Stage_S15.xpr
|
||||
Stage_S24.xpr
|
||||
Stage_S25.xpr
|
||||
Stage_S26.xpr
|
||||
Stage_S27.xpr
|
||||
Stage_S28.xpr
|
||||
Stage_S29.xpr
|
||||
ptc_pack.xpr
|
||||
11
docs/re/data/splash-phase-timeline.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
# Measured splash phase timeline -- from docs/re/captures/ui-timing/splash-build-draws.log
|
||||
# 2026-08-29. Frames are VdSwap counts; 1 frame = 1/30 s (2 units/frame, 1 unit = 1/60 s).
|
||||
#
|
||||
# phase frames n seconds start (rel. capture)
|
||||
publisher: SQUARE ENIX logo 1-90 90 3.00+ 0.00 <- floor: already at a=255 on f1
|
||||
(gap, nothing drawn) 91-93 3 0.10 3.00
|
||||
developer: both glows 94-115 22 0.73 3.10
|
||||
developer: both logos 116-211 96 3.20 3.83
|
||||
#
|
||||
# palogo_anima / palogo_anima_eff: 0 draws in the whole 214-frame window.
|
||||
# The glow->logo switch is a single frame boundary (f115 -> f116), no overlap.
|
||||
20
docs/re/data/splash-ramp-check.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
CONTROL — the eff glow's fade-in must be linear at a constant step
|
||||
alphas [34, 68, 102, 136, 170, 204, 238] steps [34, 34, 34, 34, 34, 34]
|
||||
=> 2.000 units/frame; t(f) = 2*f - 171
|
||||
check: declared hold ends t=45 -> predicted frame 108.0; observed last full-alpha frame 107
|
||||
|
||||
LOGO — palogo_gamearts, checked against that calibration
|
||||
observed: first drawn frame 116 at alpha 255; full alpha through 198; fade-out 199..211
|
||||
declared a= 0 at t= 15 -> frame 93.0 observed alpha NOT DRAWN
|
||||
declared a= 0 at t= 30 -> frame 100.5 observed alpha NOT DRAWN
|
||||
declared a=255 at t=190 -> frame 180.5 observed alpha 255
|
||||
declared a=255 at t=194 -> frame 182.5 observed alpha 255
|
||||
declared a=232 at t=206 -> frame 188.5 observed alpha 255
|
||||
declared a= 32 at t=210 -> frame 190.5 observed alpha 255
|
||||
|
||||
SHAPE, independent of any calibration:
|
||||
declared fade-out spans t=194..210 (16 units); of that,
|
||||
12/16 units drop only 23/255 of the alpha (a near-flat leg),
|
||||
4/16 units drop 200/255 (a cliff).
|
||||
captured fade-out: [255, 254, 243, 237, 215, 181, 148, 131, 98, 65, 48, 23, 15, 7]
|
||||
per-frame drops: [1, 11, 6, 22, 34, 33, 17, 33, 33, 17, 25, 8, 8] -> no near-flat leg
|
||||
97
docs/re/data/title-state-machine.txt
Normal file
@@ -0,0 +1,97 @@
|
||||
GamePart_Title has TWO nested state fields.
|
||||
|
||||
OUTER -- sub_821C7850, the part's dispatcher
|
||||
821c786c lwz r11, 16(r30) ; only runs when this+16 == 3
|
||||
821c7870 cmpwi cr6, r11, 3
|
||||
821c7874 bne cr6, <exit>
|
||||
821c787c lwz r11, 132(r30) ; phase = this+0x84
|
||||
821c7880 cmplwi cr6, r11, 0x4 ; five phases, 0..4
|
||||
821c7888 lis/addi r12, 0x821C78A0 ; jump table
|
||||
821c789c bctr
|
||||
|
||||
phase 0 -> sub_821C5690 (the splash -- independently identified as the
|
||||
splash mechanics in the iterate3E notes)
|
||||
phase 1 -> inline block at 0x821c790c
|
||||
phase 2 -> sub_821C5818
|
||||
phase 3 -> sub_821C5EC0
|
||||
phase 4 -> sub_821C6458 (the title/menu machine, below)
|
||||
|
||||
All four call sites forward the SAME three arguments: (this, r29, r28),
|
||||
where r28 is this function's own third argument -- an EVENT CODE passed
|
||||
through unchanged from ITS caller.
|
||||
|
||||
INNER -- sub_821C6458, the title/menu machine (phase 4 only)
|
||||
|
||||
DISPATCH
|
||||
821c6474 lwz r11, 136(r30) ; state = this+0x88
|
||||
821c6478 cmplwi cr6, r11, 0x9 ; 10 states, 0..9
|
||||
821c647c bgt cr6, 0x821C75B8 ; out of range -> default
|
||||
821c6480 lis r12, 0x821C
|
||||
821c6484 addi r12, r12, 25752 ; jump table at 0x821C6498
|
||||
821c6488 slwi r0, r11, 2
|
||||
821c648c lwzx r0, r12, r0
|
||||
821c6490 mtctr r0
|
||||
821c6494 bctr
|
||||
|
||||
NOTE: a disassembler decodes 0x821C6498..0x821C64BC as `lwz r16, N(r28)`
|
||||
instructions. They are the jump TABLE's ten words, not code.
|
||||
|
||||
STATES
|
||||
case 0 -> 0x821c64c0 installs "TITLE_SCREEN"
|
||||
case 1 -> 0x821c65d0
|
||||
case 2 -> 0x821c66e4 installs "TITLE_MENU"
|
||||
case 3 -> 0x821c6b18
|
||||
case 4 -> 0x821c6b5c nested switch, table at 0x821c6b7c (live: 0,3,5,8)
|
||||
case 5 -> 0x821c6fd4
|
||||
case 6 -> 0x821c7028
|
||||
case 7 -> 0x821c72dc
|
||||
case 8 -> 0x821c733c installs "LOADING"
|
||||
case 9 -> 0x821c7558
|
||||
|
||||
TRANSITIONS -- every `li rX,N ; stw rX,136(r30)` in the function
|
||||
from to at
|
||||
0 1 0x821c6598
|
||||
0 2 0x821c65c8
|
||||
1 2 0x821c66a4
|
||||
1 2 0x821c66dc
|
||||
2 4 0x821c6afc
|
||||
3 4 0x821c6b54
|
||||
4 0 0x821c6e00
|
||||
4 5 0x821c6e70
|
||||
4 8 0x821c6ed0
|
||||
4 8 0x821c6f20
|
||||
4 8 0x821c6f7c
|
||||
4 8 0x821c6fcc
|
||||
5 6 0x821c7020
|
||||
6 7 0x821c725c
|
||||
6 9 0x821c72a8
|
||||
6 2 0x821c72d4
|
||||
7 9 0x821c7334
|
||||
8 2 0x821c7548
|
||||
(state 9 stores nothing -- terminal within this function)
|
||||
|
||||
THE CONDITION ON STATE 4's EDGES -- decoded
|
||||
sub_821C6458(this, ?, r5) takes an EVENT CODE in its third argument.
|
||||
State 4 -- the input-waiting state -- dispatches on it:
|
||||
|
||||
821c6b5c cmplwi cr6, r27, 0x19 ; 26 events, 0..25
|
||||
821c6b60 bgt cr6, <default>
|
||||
821c6b68 addi r12, r12, 27516 ; table at 0x821c6b7c
|
||||
821c6b78 bctr
|
||||
|
||||
Only 6 of the 26 are handled; the other 20 fall through with no state change.
|
||||
|
||||
event 0 -> block 0x821c6be4 -> state 0 (TITLE_SCREEN -- back to the title)
|
||||
event 3 -> block 0x821c6e78 -> state 8 (LOADING)
|
||||
event 5 -> block 0x821c6f84 -> state 8 (LOADING)
|
||||
event 8 -> block 0x821c6f34 -> state 8 (LOADING)
|
||||
event 10 -> block 0x821c6e28 -> state 5
|
||||
event 25 -> block 0x821c6ed8 -> state 8 (LOADING)
|
||||
|
||||
WHAT IS NOT DECODED
|
||||
* what the event NUMBERS mean -- they are forwarded unchanged from
|
||||
sub_821C7850's caller, so the vocabulary is defined at least one level up -- button id, menu-item id or message id;
|
||||
* the conditions on edges out of states other than 4;
|
||||
* what states 1, 3, 5, 6, 7 and 9 do (they install no named screen);
|
||||
* state 3 is never a destination here, so something outside this function
|
||||
sets it.
|
||||
24
docs/re/data/zero-scale-census.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
# tools/re-capture/zero_scale_census.py -- 2026-08-28
|
||||
|
||||
elements with a keyframe group: 15493
|
||||
at least one keyframe at scale 0: 2166
|
||||
EVERY keyframe at scale 0 (would vanish): 0
|
||||
zero is a transient the element grows out of: 1762
|
||||
rest poses with scale 0%%: 195
|
||||
... of which alpha > 0, so blit paints a 1-pixel sliver: 126
|
||||
... alpha == 0, harmless: 69
|
||||
|
||||
by archive:
|
||||
126 GP_READY_ROOM.pak
|
||||
|
||||
examples (pak, entry, element idx, name, sx, sy, alpha):
|
||||
('GP_READY_ROOM.pak', 0, 2, 'pbacropolis.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 0, 6, 'pbFriendly.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 0, 10, 'pbFriendly.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 0, 14, 'pbFriendly.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 7, 2, 'pbacropolis.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 7, 6, 'pbFriendly.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 7, 10, 'pbFriendly.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 7, 14, 'pbFriendly.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 74, 2, 'pbr_enemy.t32', 0, 100, 255)
|
||||
('GP_READY_ROOM.pak', 76, 2, 'pbr_enemy.t32', 0, 100, 255)
|
||||
921
docs/re/disc-atlas.html
Normal file
@@ -0,0 +1,921 @@
|
||||
<title>Sylpheed Disc Atlas</title>
|
||||
|
||||
<style>
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
Palette. Three of these hex values are literal constants in the codebase:
|
||||
--ground is ComposeOptions::default().backdrop = [14,14,20], the colour the
|
||||
UI compositor paints behind every reassembled screen; --warn and --refuted
|
||||
are the viewer's own egui warning/error colours, rgb(224,168,86) and
|
||||
rgb(224,86,122). --confirm is the cyan glow measured off the intro video.
|
||||
Neutrals are biased blue to sit with the ground rather than against it.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
--ground: #0e0e14;
|
||||
--confirm: #3fb6c6;
|
||||
--warn: #b8792c;
|
||||
--refuted: #c23f61;
|
||||
--accent: #c23f61;
|
||||
|
||||
--bg: #eceef3;
|
||||
--surface: #f6f7fa;
|
||||
--surface-2: #e2e5ee;
|
||||
--text: #16171f;
|
||||
--text-muted: #5c6076;
|
||||
--text-faint: #878ca3;
|
||||
--rule: #c9cddb;
|
||||
--rule-soft: #dbdfe9;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--confirm: #4fd0e0;
|
||||
--warn: #e0a856;
|
||||
--refuted: #e0567a;
|
||||
--accent: #e0567a;
|
||||
|
||||
--bg: #0e0e14;
|
||||
--surface: #16171f;
|
||||
--surface-2: #1e2029;
|
||||
--text: #e8e9ef;
|
||||
--text-muted: #9296ab;
|
||||
--text-faint: #6d7186;
|
||||
--rule: #2b2e3a;
|
||||
--rule-soft: #22242e;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--confirm: #4fd0e0;
|
||||
--warn: #e0a856;
|
||||
--refuted: #e0567a;
|
||||
--accent: #e0567a;
|
||||
|
||||
--bg: #0e0e14;
|
||||
--surface: #16171f;
|
||||
--surface-2: #1e2029;
|
||||
--text: #e8e9ef;
|
||||
--text-muted: #9296ab;
|
||||
--text-faint: #6d7186;
|
||||
--rule: #2b2e3a;
|
||||
--rule-soft: #22242e;
|
||||
}
|
||||
|
||||
/* Type roles: mono for every label and datum (this subject is hex and paths),
|
||||
serif for prose so the two never blur into each other. */
|
||||
:root {
|
||||
--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo,
|
||||
Consolas, "Liberation Mono", monospace;
|
||||
--serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
|
||||
Georgia, serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--serif);
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px 120px;
|
||||
}
|
||||
|
||||
/* Prose stays near 66ch; panels break out to the full width. */
|
||||
.col {
|
||||
max-width: 66ch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1em;
|
||||
}
|
||||
|
||||
/* ── Masthead ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
header.mast {
|
||||
border-bottom: 1px solid var(--rule);
|
||||
padding: 72px 0 34px;
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--mono);
|
||||
font-size: clamp(30px, 5.4vw, 52px);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.04;
|
||||
margin: 0 0 22px;
|
||||
text-wrap: balance;
|
||||
}
|
||||
h1 .dim { color: var(--text-faint); font-weight: 400; }
|
||||
|
||||
.standfirst {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-muted);
|
||||
max-width: 60ch;
|
||||
margin: 0;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
/* ── Stat strip ───────────────────────────────────────────────────────────── */
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--rule-soft);
|
||||
border: 1px solid var(--rule-soft);
|
||||
margin: 40px 0 0;
|
||||
}
|
||||
.stat { background: var(--bg); padding: 16px 18px; }
|
||||
.stat .n {
|
||||
font-family: var(--mono);
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
display: block;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.stat .l {
|
||||
font-family: var(--mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.11em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
margin-top: 7px;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ── Sections ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
section { margin: 78px 0 0; }
|
||||
|
||||
h2 {
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text);
|
||||
margin: 0 0 6px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
}
|
||||
h2 .idx { color: var(--accent); font-weight: 400; }
|
||||
h2 .rest { color: var(--text-faint); font-weight: 400; letter-spacing: 0.1em; }
|
||||
|
||||
h3 {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
margin: 42px 0 4px;
|
||||
}
|
||||
|
||||
p { margin: 0; }
|
||||
.col > p + p { margin-top: 0; }
|
||||
|
||||
a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
|
||||
a:focus-visible, button:focus-visible { outline: 2px solid var(--confirm); outline-offset: 3px; }
|
||||
|
||||
code, .m {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.86em;
|
||||
background: var(--surface-2);
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
strong { font-weight: 700; }
|
||||
em { font-style: italic; color: var(--text); }
|
||||
|
||||
/* ── Confidence chips ─────────────────────────────────────────────────────── */
|
||||
|
||||
.chip {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.c-ok { color: var(--confirm); }
|
||||
.c-part { color: var(--warn); }
|
||||
.c-no { color: var(--refuted); }
|
||||
.c-none { color: var(--text-faint); }
|
||||
|
||||
/* ── Panels (full-width breakouts) ────────────────────────────────────────── */
|
||||
|
||||
.panel {
|
||||
margin: 30px 0 0;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--surface);
|
||||
}
|
||||
.panel-head {
|
||||
font-family: var(--mono);
|
||||
font-size: 10.5px;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid var(--rule-soft);
|
||||
}
|
||||
.panel-body { padding: 4px 0; overflow-x: auto; }
|
||||
|
||||
/* The schematic is a screen, not a page element: it keeps the game's own
|
||||
ground in both themes, the way an instrument panel does. */
|
||||
.screen {
|
||||
background: var(--ground);
|
||||
border: 1px solid var(--rule);
|
||||
margin: 30px 0 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.screen svg { display: block; min-width: 760px; width: 100%; height: auto; }
|
||||
|
||||
figcaption {
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.65;
|
||||
color: var(--text-faint);
|
||||
margin-top: 12px;
|
||||
max-width: 76ch;
|
||||
}
|
||||
figure { margin: 0; }
|
||||
|
||||
/* ── Tables ───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.tbl-scroll { overflow-x: auto; margin: 26px 0 0; }
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
min-width: 620px;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-faint);
|
||||
font-weight: 700;
|
||||
padding: 0 14px 9px 0;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
white-space: nowrap;
|
||||
}
|
||||
td {
|
||||
padding: 8px 14px 8px 0;
|
||||
border-bottom: 1px solid var(--rule-soft);
|
||||
vertical-align: top;
|
||||
}
|
||||
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; padding-right: 22px; }
|
||||
tbody tr:hover { background: var(--surface); }
|
||||
td .note { color: var(--text-muted); font-family: var(--serif); font-size: 13.5px; }
|
||||
|
||||
/* ── Mermaid ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.dia {
|
||||
margin: 24px 0 0;
|
||||
padding: 18px 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--rule-soft);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.dia pre.mermaid { margin: 0; text-align: left; }
|
||||
|
||||
/* ── Callout ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
.note-box {
|
||||
border-left: 2px solid var(--accent);
|
||||
padding: 2px 0 2px 18px;
|
||||
margin: 26px 0 0;
|
||||
color: var(--text-muted);
|
||||
max-width: 64ch;
|
||||
}
|
||||
.note-box .lbl {
|
||||
font-family: var(--mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
ul.plain { margin: 0; padding-left: 1.15em; display: flex; flex-direction: column; gap: 0.55em; }
|
||||
ul.plain li::marker { color: var(--text-faint); }
|
||||
|
||||
/* ── Chain (numbered hops — the numbering is the hop order, which is real) ─── */
|
||||
|
||||
.hops { display: flex; flex-direction: column; gap: 0; margin: 24px 0 0; }
|
||||
.hop {
|
||||
display: grid;
|
||||
grid-template-columns: 34px 1fr;
|
||||
gap: 14px;
|
||||
padding: 13px 0;
|
||||
border-bottom: 1px solid var(--rule-soft);
|
||||
align-items: baseline;
|
||||
}
|
||||
.hop:first-child { border-top: 1px solid var(--rule); }
|
||||
.hop .n {
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.hop .b { font-family: var(--mono); font-size: 12.5px; line-height: 1.7; }
|
||||
.hop .b .d { color: var(--text-muted); font-family: var(--serif); font-size: 14px; display: block; margin-top: 3px; }
|
||||
|
||||
footer {
|
||||
margin-top: 96px;
|
||||
padding-top: 26px;
|
||||
border-top: 1px solid var(--rule);
|
||||
font-family: var(--mono);
|
||||
font-size: 11.5px;
|
||||
color: var(--text-faint);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { animation: none !important; transition: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<header class="mast">
|
||||
<p class="eyebrow">Project Sylpheed · Arc of Deception — reverse-engineering reference</p>
|
||||
<h1>Sylpheed Disc Atlas</h1>
|
||||
<p class="standfirst">
|
||||
What is on the disc, and how every piece of it points at every other piece.
|
||||
Four layers deep: the media, the archives, the container formats, and the
|
||||
domain chains a port has to walk.
|
||||
</p>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat"><span class="n">41</span><span class="l">IPFB archives</span></div>
|
||||
<div class="stat"><span class="n">26 443</span><span class="l">archive entries</span></div>
|
||||
<div class="stat"><span class="n">166</span><span class="l">.xpr packages</span></div>
|
||||
<div class="stat"><span class="n">97</span><span class="l">.wmv cutscenes</span></div>
|
||||
<div class="stat"><span class="n">9 519</span><span class="l">sound banks</span></div>
|
||||
<div class="stat"><span class="n">3</span><span class="l">hash functions</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="col">
|
||||
<p>
|
||||
Nothing on this disc is found by path at runtime. Every lookup is a
|
||||
<em>hash</em>, and the whole disc is joined together by three of them with
|
||||
different rules. Read that first, or the rest of the map reads as a pile of
|
||||
unrelated tables.
|
||||
</p>
|
||||
<p>
|
||||
Counts here were measured on the retail extract for this page, not copied
|
||||
forward. Where a claim is contested or partial, the chip says so — and the
|
||||
things that <em>do not</em> resolve get their own section, because a port
|
||||
has to survive them.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">01</span> <span>The join algebra</span> <span class="rest">three hashes, three rules</span></h2>
|
||||
|
||||
<div class="col">
|
||||
<p>
|
||||
These are not interchangeable, and mixing them up is the single most
|
||||
productive mistake this project has made. They differ in what they hash,
|
||||
and crucially in <strong>case sensitivity</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tbl-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Hash</th><th>Keys</th><th>Case</th><th>Verified</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>name_hash</strong></td>
|
||||
<td>IPFB archive TOC entries — a file path inside a <span class="m">.pak</span></td>
|
||||
<td>insensitive</td>
|
||||
<td class="num">recovers paths</td>
|
||||
<td><span class="chip c-ok">confirmed</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>tag_hash</strong></td>
|
||||
<td>IDXD record names and field keys</td>
|
||||
<td><em>sensitive</em></td>
|
||||
<td class="num">1 271 462 / 1 271 462</td>
|
||||
<td><span class="chip c-ok">confirmed</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>ixud_hash</strong></td>
|
||||
<td>IXUD localised-text record and field keys</td>
|
||||
<td><em>sensitive</em></td>
|
||||
<td class="num">628 165 / 628 165</td>
|
||||
<td><span class="chip c-ok">confirmed</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="note-box">
|
||||
<span class="lbl">The trap that cost the most</span>
|
||||
A 24-bit modulus cannot uniquely name an identifier of eight characters or
|
||||
more. Exhaustive preimage search recovers <span class="m">"Stage01"</span>
|
||||
from its own hash, but at seven characters one real target already has
|
||||
<strong>1 176</strong> preimages. Forty-two field keys on this disc are
|
||||
hash-only with no stored name, and they are <em>provably</em> unrecoverable
|
||||
— not merely unrecovered. Everything else carries its name inline: an IDXD
|
||||
field's middle word points at its own name string, which is why the
|
||||
containers are self-describing and the hash almost never has to be inverted.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">02</span> <span>The four layers</span> <span class="rest">disc → archive → container → domain</span></h2>
|
||||
|
||||
<div class="col">
|
||||
<p>
|
||||
Reading up from the bottom: a domain chain names an asset by string, that
|
||||
string hashes into an archive TOC, the TOC yields a compressed blob, and
|
||||
the blob's first four bytes say which container format it is. Every arrow
|
||||
in this diagram is a hash lookup or a magic-byte test — there are no
|
||||
directory scans.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<figure>
|
||||
<div class="screen">
|
||||
<svg viewBox="0 0 1000 640" role="img" aria-label="Four-layer schematic of the Project Sylpheed disc: media, archives, container formats and domain chains, joined by three hash functions.">
|
||||
<defs>
|
||||
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#4f5468"/>
|
||||
</marker>
|
||||
<style>
|
||||
.bl { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; letter-spacing: .16em; fill: #6d7186; text-transform: uppercase; }
|
||||
.bx { fill: #16171f; stroke: #2b2e3a; stroke-width: 1; }
|
||||
.bxa { fill: #16171f; stroke: #4fd0e0; stroke-width: 1; }
|
||||
.tt { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; fill: #e8e9ef; }
|
||||
.ts { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10px; fill: #6d7186; }
|
||||
.tn { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; fill: #4fd0e0; }
|
||||
.jn { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 10.5px; fill: #e0a856; letter-spacing: .06em; }
|
||||
.ln { stroke: #4f5468; stroke-width: 1; }
|
||||
.rl { stroke: #22242e; stroke-width: 1; }
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- band rules -->
|
||||
<line class="rl" x1="0" y1="118" x2="1000" y2="118"/>
|
||||
<line class="rl" x1="0" y1="268" x2="1000" y2="268"/>
|
||||
<line class="rl" x1="0" y1="418" x2="1000" y2="418"/>
|
||||
|
||||
<!-- ── BAND 1 · disc media ── -->
|
||||
<text class="bl" x="24" y="40">Disc</text>
|
||||
<g>
|
||||
<rect class="bx" x="130" y="30" width="160" height="58" rx="2"/>
|
||||
<text class="tt" x="145" y="54">default.xex</text>
|
||||
<text class="ts" x="145" y="70">executable + PE</text>
|
||||
|
||||
<rect class="bx" x="302" y="30" width="160" height="58" rx="2"/>
|
||||
<text class="tt" x="317" y="54">dat/</text>
|
||||
<text class="ts" x="317" y="70">37 archives</text>
|
||||
|
||||
<rect class="bx" x="474" y="30" width="160" height="58" rx="2"/>
|
||||
<text class="tt" x="489" y="54">hidden/</text>
|
||||
<text class="ts" x="489" y="70">DefTables · MiscBin</text>
|
||||
|
||||
<rect class="bx" x="646" y="30" width="160" height="58" rx="2"/>
|
||||
<text class="tt" x="661" y="54">dat/movie/</text>
|
||||
<text class="ts" x="661" y="70">97 wmv · 6 paks</text>
|
||||
|
||||
<rect class="bx" x="818" y="30" width="160" height="58" rx="2"/>
|
||||
<text class="tt" x="833" y="54">resource3d/</text>
|
||||
<text class="ts" x="833" y="70">166 xpr · 1.4 GB</text>
|
||||
</g>
|
||||
|
||||
<!-- join 1 -->
|
||||
<line class="ln" x1="382" y1="88" x2="382" y2="176" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="554" y1="88" x2="554" y2="176" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="726" y1="88" x2="726" y2="176" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="898" y1="88" x2="898" y2="220" marker-end="url(#ar)"/>
|
||||
<text class="jn" x="130" y="112">name_hash keys the TOC — case-insensitive</text>
|
||||
|
||||
<!-- ── BAND 2 · archives ── -->
|
||||
<text class="bl" x="24" y="190">Archive</text>
|
||||
<g>
|
||||
<rect class="bxa" x="130" y="176" width="560" height="72" rx="2"/>
|
||||
<text class="tt" x="146" y="200">IPFB · .pak index + .p00….pNN data segments</text>
|
||||
<text class="ts" x="146" y="218">12-byte TOC entry { name_hash, offset, comp_size } · Z1/zlib payloads</text>
|
||||
<text class="tn" x="146" y="236">41 archives · 26 443 entries</text>
|
||||
|
||||
<rect class="bx" x="706" y="176" width="272" height="72" rx="2"/>
|
||||
<text class="tt" x="722" y="200">raw files (not archived)</text>
|
||||
<text class="ts" x="722" y="218">.wmv video · .xpr resource packages</text>
|
||||
<text class="tn" x="722" y="236">263 files</text>
|
||||
</g>
|
||||
|
||||
<!-- join 2 -->
|
||||
<line class="ln" x1="200" y1="248" x2="200" y2="292" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="410" y1="248" x2="410" y2="292" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="620" y1="248" x2="620" y2="292" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="842" y1="248" x2="842" y2="292" marker-end="url(#ar)"/>
|
||||
<text class="jn" x="130" y="264">payload magic decides the format — nothing declares a type</text>
|
||||
|
||||
<!-- ── BAND 3 · containers ── -->
|
||||
<text class="bl" x="24" y="330">Container</text>
|
||||
<g>
|
||||
<rect class="bxa" x="130" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="140" y="316">IDXD</text><text class="ts" x="140" y="332">tables</text><text class="tn" x="140" y="347">6325</text>
|
||||
|
||||
<rect class="bxa" x="215" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="225" y="316">T8aD</text><text class="ts" x="225" y="332">2D tex</text><text class="tn" x="225" y="347">4525</text>
|
||||
|
||||
<rect class="bxa" x="300" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="310" y="316">RATC</text><text class="ts" x="310" y="332">UI bundle</text><text class="tn" x="310" y="347">2985</text>
|
||||
|
||||
<rect class="bxa" x="385" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="395" y="316">IXUD</text><text class="ts" x="395" y="332">text</text><text class="tn" x="395" y="347">1104</text>
|
||||
|
||||
<rect class="bxa" x="470" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="480" y="316">LSTA</text><text class="ts" x="480" y="332">sprites</text><text class="tn" x="480" y="347">64</text>
|
||||
|
||||
<rect class="bxa" x="555" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="565" y="316">SLB</text><text class="ts" x="565" y="332">XMA1</text><text class="tn" x="565" y="347">9519</text>
|
||||
|
||||
<rect class="bxa" x="640" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="650" y="316">XBG7</text><text class="ts" x="650" y="332">mesh</text><text class="tn" x="650" y="347">6294</text>
|
||||
|
||||
<rect class="bxa" x="725" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="735" y="316">XPR2</text><text class="ts" x="735" y="332">tex pkg</text><text class="tn" x="735" y="347">166</text>
|
||||
|
||||
<rect class="bxa" x="810" y="292" width="77" height="62" rx="2"/>
|
||||
<text class="tt" x="820" y="316">TTF</text><text class="ts" x="820" y="332">fonts</text><text class="tn" x="820" y="347">54</text>
|
||||
|
||||
<rect class="bx" x="895" y="292" width="77" height="62" rx="2" stroke="#e0567a"/>
|
||||
<text class="tt" x="905" y="316">ISB</text><text class="ts" x="905" y="332">PRT · BIN</text>
|
||||
<text class="ts" x="905" y="347" fill="#e0567a">no parser</text>
|
||||
</g>
|
||||
|
||||
<!-- join 3 -->
|
||||
<line class="ln" x1="200" y1="354" x2="200" y2="442" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="440" y1="354" x2="440" y2="442" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="680" y1="354" x2="680" y2="442" marker-end="url(#ar)"/>
|
||||
<line class="ln" x1="900" y1="354" x2="900" y2="442" marker-end="url(#ar)"/>
|
||||
<text class="jn" x="130" y="410">tag_hash / ixud_hash key records + fields — values are asset NAMES, hashed back up</text>
|
||||
|
||||
<!-- ── BAND 4 · domains ── -->
|
||||
<text class="bl" x="24" y="480">Domain</text>
|
||||
<g>
|
||||
<rect class="bx" x="130" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="141" y="466">Stage</text><text class="ts" x="141" y="482">28 missions</text><text class="ts" x="141" y="496">10 sub-tables</text>
|
||||
|
||||
<rect class="bx" x="252" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="263" y="466">Unit</text><text class="ts" x="263" y="482">159 fields</text><text class="ts" x="263" y="496">→ turret slot</text>
|
||||
|
||||
<rect class="bx" x="374" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="385" y="466">Arsenal</text><text class="ts" x="385" y="482">59 items</text><text class="ts" x="385" y="496">4-hop chain</text>
|
||||
|
||||
<rect class="bx" x="496" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="507" y="466">Audio</text><text class="ts" x="507" y="482">5798 cues</text><text class="ts" x="507" y="496">5 families</text>
|
||||
|
||||
<rect class="bx" x="618" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="629" y="466">Cutscene</text><text class="ts" x="629" y="482">104 slots</text><text class="ts" x="629" y="496">4 bindings</text>
|
||||
|
||||
<rect class="bx" x="740" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="751" y="466">UI screen</text><text class="ts" x="751" y="482">965 builds</text><text class="ts" x="751" y="496">decl + place</text>
|
||||
|
||||
<rect class="bx" x="862" y="442" width="112" height="66" rx="2"/>
|
||||
<text class="tt" x="873" y="466">Save</text><text class="ts" x="873" y="482">545 bytes</text><text class="ts" x="873" y="496">GDHA + zlib</text>
|
||||
</g>
|
||||
|
||||
<!-- the cross-domain edge: the save indexes the arsenal's string order -->
|
||||
<path class="ln" d="M 918 508 L 918 560 L 430 560 L 430 508" fill="none" stroke-dasharray="3 3" marker-end="url(#ar)"/>
|
||||
<text class="jn" x="560" y="578">the save's 54-byte blob is indexed by strings.tbl order — not weapon.tbl</text>
|
||||
|
||||
<text class="ts" x="130" y="614" fill="#4f5468">Counts measured on the retail extract. Container counts exclude sound.pak and entries over 8 MB.</text>
|
||||
</svg>
|
||||
</div>
|
||||
<figcaption>
|
||||
The three amber lines are the joins. Each is a hash lookup, and each is a
|
||||
place a port can silently take the wrong branch: an archive TOC miss
|
||||
returns nothing, and a field the disc never values reads as
|
||||
<span class="m">0.0</span> rather than as an error.
|
||||
</figcaption>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">03</span> <span>Archives</span> <span class="rest">where the mass actually sits</span></h2>
|
||||
|
||||
<div class="col">
|
||||
<p>
|
||||
Two archives hold most of the disc. <span class="m">sound.pak</span> is
|
||||
1.07 GB across five data segments — more than every other archive
|
||||
combined — and <span class="m">GP_HANGAR_ARSENAL.pak</span> is large for a
|
||||
menu because it is <em>stage-scoped</em>: 168 of its objects are 28
|
||||
missions × 6 languages, each a complete Hangar configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tbl-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Archive</th><th class="num">Entries</th><th class="num">Stored</th><th>What it is</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>sound.pak</td><td class="num">9 519</td><td class="num">1.07 GB</td><td><span class="note">every XMA1 bank — music, jingles, SFX, both voice languages</span></td></tr>
|
||||
<tr><td>GP_HANGAR_ARSENAL.pak</td><td class="num">1 538</td><td class="num">67.2 MB</td><td><span class="note">28 stages × 6 languages of Hangar config, plus item text</span></td></tr>
|
||||
<tr><td>GP_MAIN_GAME_{D,E,F,I,J,S}</td><td class="num">1 119 ea.</td><td class="num">4.6 MB ea.</td><td><span class="note">the mission data set — stages, units, routes, dialogue</span></td></tr>
|
||||
<tr><td>GP_READY_ROOM.pak</td><td class="num">1 106</td><td class="num">67.6 MB</td><td><span class="note">largest UI pak; ISL script bytecode + link map</span></td></tr>
|
||||
<tr><td>GP_MAIN_GAME_*2D <span class="chip c-no">unnamed</span></td><td class="num">711 ea.</td><td class="num">15.2 MB ea.</td><td><span class="note">six paks, 0 % of names recoverable — see §06</span></td></tr>
|
||||
<tr><td>DefTables.pak <span class="m">hidden/</span></td><td class="num">1 465</td><td class="num">—</td><td><span class="note">definition tables; 804 of 1 465 names resolved</span></td></tr>
|
||||
<tr><td>movie/{eng,jpn,deu,esp,fra,ita}</td><td class="num">117 ea.</td><td class="num">1.8 MB ea.</td><td><span class="note">subtitles, telop overlays and fonts — <em>not</em> voice</span></td></tr>
|
||||
<tr><td>tables.pak</td><td class="num">79</td><td class="num">330 KB</td><td><span class="note">the registries: sound cues, the cutscene manifest</span></td></tr>
|
||||
<tr><td>MiscBin.pak <span class="m">hidden/</span></td><td class="num">40</td><td class="num">—</td><td><span class="note">0 names resolved</span></td></tr>
|
||||
<tr><td>fonts.pak</td><td class="num">3</td><td class="num">2.4 MB</td><td><span class="note">the three shipped typefaces</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="note-box">
|
||||
<span class="lbl">One entry lies about its size</span>
|
||||
<span class="m">Static.slb</span>, the sound-effect bank, sits at the highest
|
||||
offset in <span class="m">sound.pak</span> and declares
|
||||
<strong>616 768 bytes more than the disc holds</strong>. This is not a bad
|
||||
extract — <span class="m">sound.p04</span> matches the ISO's own directory
|
||||
record — and a sweep of every archive finds this entry over-running and no
|
||||
other. The last entry's size field is an allocation size. A reader must
|
||||
allow a short read there, and <em>only</em> there.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">04</span> <span>Container formats</span> <span class="rest">what each blob turns into</span></h2>
|
||||
|
||||
<div class="tbl-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Magic</th><th>Content</th><th>State</th><th>Reach</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><strong>IDXD</strong></td><td><span class="note">self-describing record/field table — the game's whole data layer</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">7 750 / 7 750 objects</td></tr>
|
||||
<tr><td><strong>IXUD</strong></td><td><span class="note">the same container, UTF-16BE, offsets in <em>chars</em></span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">1 104 / 1 104 objects</td></tr>
|
||||
<tr><td><strong>T8aD</strong></td><td><span class="note">2D texture — a list of arbitrary sub-rectangles, not a tile grid</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">19 216 / 19 216</td></tr>
|
||||
<tr><td><strong>RATC</strong></td><td><span class="note">UI bundle; children are sprites, layout records and primitives</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">10 144 / 10 148 refs</td></tr>
|
||||
<tr><td><strong>LSTA</strong></td><td><span class="note">sprite display list — count covers T8aD <em>and</em> PRMD</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">64 / 64</td></tr>
|
||||
<tr><td><strong>XBG7</strong></td><td><span class="note">mesh; index pool then vertex pool, layout declared per sub-mesh</span></td><td><span class="chip c-part">99.25 %</span></td><td class="num">6 247 / 6 294</td></tr>
|
||||
<tr><td><strong>XPR2</strong></td><td><span class="note">texture package — de-tile, A8R8G8B8 and DXT1</span></td><td><span class="chip c-part">partial</span></td><td class="num">channel order confirmed</td></tr>
|
||||
<tr><td><strong>SLB</strong></td><td><span class="note">XACT bank of XMA1 sub-waves; two layouts, one headerless</span></td><td><span class="chip c-part">35/36 shared</span></td><td class="num"><span class="m">JNGL_001</span> fails</td></tr>
|
||||
<tr><td><strong>GDHA</strong></td><td><span class="note">save file — zlib payload, chunk stream, round-trips byte-identically</span></td><td><span class="chip c-part">~11 ❔ fields</span></td><td class="num">545 bytes</td></tr>
|
||||
<tr><td>TTF / OTF</td><td><span class="note">stock OpenType</span></td><td><span class="chip c-ok">confirmed</span></td><td class="num">54</td></tr>
|
||||
<tr><td><strong>ISB</strong></td><td><span class="note">ISL mission-script bytecode</span></td><td><span class="chip c-part">partial</span></td><td class="num">builtins documented</td></tr>
|
||||
<tr><td><strong>PRT</strong></td><td><span class="note">telop — on-screen text overlay for cutscenes</span></td><td><span class="chip c-none">no parser</span></td><td class="num">22 bound</td></tr>
|
||||
<tr><td><strong>BIN</strong></td><td><span class="note">collision meshes, <span class="m">CollisionSet_S<NN>.bin</span></span></td><td><span class="chip c-none">no parser</span></td><td class="num">documented only</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">05</span> <span>The domain chains</span> <span class="rest">what references what</span></h2>
|
||||
|
||||
<div class="col">
|
||||
<p>
|
||||
Each of these starts from something the player can see and ends at bytes on
|
||||
the disc. Where a chain has a hop that looks like it should be direct and
|
||||
is not, that hop is called out — those are where a naive port breaks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Mission</h3>
|
||||
<div class="col">
|
||||
<p>
|
||||
One IDXD object per stage in every language pak. Its
|
||||
<span class="m">StageResource</span> record is the hub: nineteen fields
|
||||
naming the 3D packages, the enemy roster, the routes, the collision set
|
||||
and the localised objective strings.
|
||||
</p>
|
||||
</div>
|
||||
<div class="dia">
|
||||
<pre class="mermaid">
|
||||
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px','clusterBkg':'#16171f','clusterBorder':'#2b2e3a'}}}%%
|
||||
flowchart LR
|
||||
A["GP_MAIN_GAME_<lang>.pak"] --> B["Stage_S<NN> record"]
|
||||
B --> C["StageResource"]
|
||||
C --> D["Stage_S<NN>.xpr<br/>BG_<place>.xpr"]
|
||||
C --> E["EnumUnit_S<NN>.tbl"]
|
||||
C --> F["UnitGroup_S<NN>.tbl<br/>squadron roster"]
|
||||
C --> G["Route_S<NN>.tbl<br/>FormationSet_S<NN>.tbl"]
|
||||
C --> H["CollisionSet_S<NN>.bin"]
|
||||
C --> I["EnumLocalString_S<NN>.tbl"]
|
||||
B --> J["Stage\script.tbl"]
|
||||
J --> K["StageNN.ssb<br/>ISL bytecode"]
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h3>Arsenal — the four-hop trap</h3>
|
||||
<div class="col">
|
||||
<p>
|
||||
The Hangar lists 59 weapons; the disc's <span class="m">Weapon</span> table
|
||||
has 131, and the two name sets overlap in <strong>zero</strong> values.
|
||||
That is not a mismatch to be reconciled — it is the wrong join. An arsenal
|
||||
item names a <em>hardpoint slot</em>, and the slot carries the weapon.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hops">
|
||||
<div class="hop"><span class="n">01</span><span class="b">loadout record <span style="color:var(--text-faint)">— flight position × pilot, e.g. Rhino2-Ellen</span><span class="d">Names <code>Arm1</code> / <code>Arm2</code> / <code>Arm3</code> / <code>Nose</code> — which are not items either.</span></span></div>
|
||||
<div class="hop"><span class="n">02</span><span class="b">per-slot allow-list record<span class="d">Its only <em>named</em> field is <code>Type</code>; the candidate items live in its positional, unnamed fields, in order.</span></span></div>
|
||||
<div class="hop"><span class="n">03</span><span class="b">arsenal item <span style="color:var(--text-faint)">→ .PlayerWeapon</span><span class="d">Resolves to <code>Turret_050</code> — a slot on the player craft's own unit table. 59 of 59 do; 0 of 59 name a weapon.</span></span></div>
|
||||
<div class="hop"><span class="n">04</span><span class="b">turret slot <span style="color:var(--text-faint)">→ .WeaponID</span><span class="d">And <em>this</em> is the <code>Weapon</code> record.</span></span></div>
|
||||
</div>
|
||||
|
||||
<h3>Audio</h3>
|
||||
<div class="col">
|
||||
<p>
|
||||
One IDXD object in <span class="m">tables.pak</span> is the whole cue
|
||||
index. Its <span class="m">SOUNDS</span> record is the only record found on
|
||||
the disc that is entirely named fields — 5 798 of them — and the names are
|
||||
the join key. Cue ids are partitioned by family with no overlap.
|
||||
</p>
|
||||
</div>
|
||||
<div class="dia">
|
||||
<pre class="mermaid">
|
||||
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
|
||||
flowchart LR
|
||||
M["script message id"] --> S["SOUNDS<br/>cue name → sound id"]
|
||||
S --> F["FILES<br/>5 135 bank paths"]
|
||||
F --> H{{"name_hash"}}
|
||||
H --> P["sound.pak<br/>9 519 entries"]
|
||||
P --> X["XMA1 sub-waves"]
|
||||
S -.-> C1["VOICE 1500–7331"]
|
||||
S -.-> C2["SE 1–901"]
|
||||
S -.-> C3["DEMO 8000–8408"]
|
||||
S -.-> C4["BR 8500–8600"]
|
||||
S -.-> C5["BGM 1001–1109"]
|
||||
</pre>
|
||||
</div>
|
||||
<div class="note-box">
|
||||
<span class="lbl">Two things the paths do not tell you</span>
|
||||
The 36 music, jingle and SFX banks sit at the table <em>root</em> with no
|
||||
language component, so both <span class="m">sounds.tbl</span> files name
|
||||
them. And a <span class="m">.slb</span> need not hold the track its name
|
||||
claims: the movie voices are one continuous stream chunked into TOC entries
|
||||
whose boundaries do not line up with the cues.
|
||||
</div>
|
||||
|
||||
<h3>Cutscene</h3>
|
||||
<div class="col">
|
||||
<p>
|
||||
One manifest record binds four assets per slot. Measured for this page:
|
||||
104 slots over 101 distinct movies, 99 with a subtitle track, 99 with a
|
||||
voice track, 22 with a telop overlay.
|
||||
</p>
|
||||
</div>
|
||||
<div class="dia">
|
||||
<pre class="mermaid">
|
||||
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#1e2029','primaryTextColor':'#e8e9ef','primaryBorderColor':'#4fd0e0','lineColor':'#8a8fa3','fontFamily':'ui-monospace, Menlo, monospace','fontSize':'13px'}}}%%
|
||||
flowchart LR
|
||||
MF["cutscene manifest<br/>tables.pak"] --> MV["MOVIE<br/>dat/movie/<name>.wmv"]
|
||||
MF --> TL["TELOP<br/><lang>.pak+*.prt"]
|
||||
MF --> SB["SUBTITLE<br/><lang>.pak+SUBTITLE_*.tbl"]
|
||||
MF --> VT["VOICETRACK<br/>cue name"]
|
||||
SB --> IX["IXUD caption keys"]
|
||||
IX --> TX["GP_MAIN_GAME_<lang><br/>8 800 text keys"]
|
||||
VT --> SC["sound cue → .slb region"]
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h3>UI screen</h3>
|
||||
<div class="col">
|
||||
<p>
|
||||
One archive per screen; each RATC bundle is one context × language build.
|
||||
A bundle's declaration table lists every element with its parent and pivot;
|
||||
the placement region right after it gives each element a keyframe group.
|
||||
Both the tutorial pause menu and the title main menu rebuild
|
||||
pixel-accurately from the disc alone.
|
||||
</p>
|
||||
<p>
|
||||
Two things are <em>not</em> what they look like. The resting position is
|
||||
neither the first nor the last keyframe — it is the plateau. And the
|
||||
declaration table is <strong>not</strong> the paint order: a per-draw
|
||||
capture of the running title screen paints element 13 first and elements
|
||||
0 and 1 late. The real order is a second, reordered child array the screen
|
||||
object keeps at runtime — deriving it from the bundle is still open.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Save</h3>
|
||||
<div class="col">
|
||||
<p>
|
||||
545 bytes: a <span class="m">GDHA</span> container, a zlib payload, then a
|
||||
chunk stream. The 54-byte develop blob has the alphabet <span class="m">0
|
||||
locked / 2 developable / 4 developed</span>, and is indexed by
|
||||
<span class="m">strings.tbl</span> order — the display order <em>plus</em>
|
||||
the cut items only the localisation file lists. <span class="m">weapon.tbl</span>'s
|
||||
id list is not the index space; that it is also 54 long is a coincidence,
|
||||
and the two agree only to index 32.
|
||||
</p>
|
||||
</div>
|
||||
<div class="note-box">
|
||||
<span class="lbl">The stale-summary trap</span>
|
||||
The in-game Details panel reads a <em>summary copy</em> held in the container
|
||||
header, not the payload. Edit the payload alone and the panel keeps showing
|
||||
the old values — which reads exactly like a failed parse.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">06</span> <span>References that go nowhere</span> <span class="rest">a port has to survive these</span></h2>
|
||||
|
||||
<div class="col">
|
||||
<p>
|
||||
These are not gaps in the reverse engineering. They are properties of the
|
||||
shipped disc, and each one was found by a check that expected the opposite.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tbl-scroll">
|
||||
<table>
|
||||
<thead><tr><th>Reference</th><th>Where</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span class="m">pmbase.t32</span></td><td><span class="note">named by <span class="m">pmbase.rat</span> in all four <span class="m">GP_STAGE_CLEAR</span> language builds</span></td><td><span class="chip c-no">nowhere on disc</span></td></tr>
|
||||
<tr><td><span class="m">SUBTITLE_S12B.tbl</span></td><td><span class="note">bound by the manifest, in all six languages</span></td><td><span class="chip c-no">resolves in none</span></td></tr>
|
||||
<tr><td><span class="m">logo1…logo4</span>, <span class="m">SYLPH_HD720p_8M-CBR_2ch</span></td><td><span class="note">manifest-bound movies</span></td><td><span class="chip c-no">no .wmv</span></td></tr>
|
||||
<tr><td><span class="m">dat\GP_TEST\</span></td><td><span class="note">a debug archive the script manifest points at</span></td><td><span class="chip c-no">not shipped</span></td></tr>
|
||||
<tr><td>6 × <span class="m">GP_MAIN_GAME_*2D</span></td><td><span class="note">711 entries each; eleven other paks are at 100 % by the same method</span></td><td><span class="chip c-no">0 % named</span></td></tr>
|
||||
<tr><td><span class="m">GP_READY_ROOM.pak</span></td><td><span class="note">the largest UI pak on the disc</span></td><td><span class="chip c-no">6 of 1 106 named</span></td></tr>
|
||||
<tr><td>42 ISL script-symbol keys</td><td><span class="note">hash-only field keys in <span class="m"><lang>\script\ID.tbl</span></span></td><td><span class="chip c-no">provably unrecoverable</span></td></tr>
|
||||
<tr><td>9 of 101 cutscenes</td><td><span class="note">no English transcript resolves</span></td><td><span class="chip c-part">unexplained</span></td></tr>
|
||||
<tr><td><span class="m">JNGL_001.slb</span></td><td><span class="note">headerless bank; payload is not a whole number of XMA1 packets</span></td><td><span class="chip c-part">does not decode</span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="note-box">
|
||||
<span class="lbl">Why the 2D paks matter more than they look</span>
|
||||
Six archives at <em>exactly</em> 0.0 %, all six, 711 entries each — while
|
||||
eleven menu paks hit 100 % in the same run. That control is what makes it a
|
||||
finding rather than a failed guess: the naming method works, and these
|
||||
archives are outside it. Whatever names their contents is not a path hashed
|
||||
the way every other archive's is.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════ -->
|
||||
<section>
|
||||
<h2><span class="idx">07</span> <span>Still open</span></h2>
|
||||
|
||||
<div class="col">
|
||||
<ul class="plain">
|
||||
<li><strong>UI paint order from the bundle.</strong> Solved at runtime, not statically — and static is what a port needs.</li>
|
||||
<li><strong>The <span class="m">.prt</span> telop format.</strong> 22 cutscenes bind one; no parser exists.</li>
|
||||
<li><strong>Collision meshes.</strong> <span class="m">CollisionSet_S<NN>.bin</span> is documented and unparsed.</li>
|
||||
<li><strong>47 XBG7 resources</strong> still miss — mostly pose/proxy composites and damage variants, not a threshold away.</li>
|
||||
<li><strong>~11 GHAD save fields</strong> unnamed, and difficulty-versus-stage undecided: three fields hold the value 2.</li>
|
||||
<li><strong>The naming of the 2D and Ready Room archives</strong>, which is the largest single block of unreachable content on the disc.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Counts measured on the retail extract (USA/Europe, En/Ja). Container-format
|
||||
counts exclude <span class="m">sound.pak</span> and entries over 8 MB.<br/>
|
||||
Confidence follows the corpus convention — confirmed · partial · refuted —
|
||||
and is per-claim, never per-document.
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
50
docs/re/doc-link-audit.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# ✅ Do the docs' cited artifacts exist? — 11 broken links repaired
|
||||
|
||||
**Status:** ✅ checked mechanically and fixed, with the count verified both ways.
|
||||
|
||||
The brief's rule is *"commit the reference data beside the finding, so the port
|
||||
can be built without a disc in the loop"*. An answer whose evidence is not
|
||||
reachable cannot be used. Nothing had ever checked that.
|
||||
|
||||
[`tools/re-capture/doc_link_check.py`](../../tools/re-capture/doc_link_check.py)
|
||||
walks every markdown file under `docs/`, resolves each relative link, and reports
|
||||
targets that do not exist — and, separately, targets that exist but are **empty**,
|
||||
which is the sneakier failure since a zero-byte file looks fine in any listing.
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| links resolving | 1 038 | **1 049** |
|
||||
| missing targets | **16** | 5 |
|
||||
| empty targets | 0 | 0 |
|
||||
|
||||
**+11 resolving, −11 missing — the two numbers pair exactly**, which is the check
|
||||
that the repair did what it claimed and nothing else.
|
||||
|
||||
## 🔴 Two of them were the evidence for the UI decode itself
|
||||
|
||||
`structures/ui-rat-layout.md` is the layout decode the port is built on. Its two
|
||||
figures — the ones backing *"the tutorial PAUSE menu rebuilds pixel-accurately
|
||||
from its sprites"* and *"the same method reproduces the main menu"* — were
|
||||
written as `captures/ui-layout/…` from a file in `structures/`, one directory
|
||||
too shallow. **The headline evidence for the decode was unreachable from its own
|
||||
document.**
|
||||
|
||||
## What was wrong, and what still is
|
||||
|
||||
Eleven links had the **wrong relative depth** while their targets existed — a
|
||||
missing or surplus `../`, or a missing `structures/`. Those are repaired; each
|
||||
was rewritten only when exactly one candidate path resolved, so nothing was
|
||||
guessed.
|
||||
|
||||
❔ **Five remain genuinely absent** and are left alone rather than invented:
|
||||
|
||||
| doc | target |
|
||||
|---|---|
|
||||
| `autopilot-knowledge-sources.md`, `entities-live-roster.md` | `../../MEMORY.md` — outside the repo |
|
||||
| `challenge-mission-gate.md` | a header in the separate `xenia-canary-native` tree |
|
||||
| `stage-drift-is-navigation-not-save.md` | `structures/weapon-datasheet-runtime.md` — never written |
|
||||
| `mission-freeze-resume-spin.md` | `canary-build-verified-env-confound.md` — never written |
|
||||
|
||||
None is port-relevant: they are mission, entity and emulator-side documents. Two
|
||||
name documents that do not exist, which is a different problem from a bad path
|
||||
and is not something to paper over with a link fix.
|
||||
@@ -79,7 +79,7 @@ looks stuck on the main menu while it is in fact three screens further on.
|
||||
|
||||
## Still open
|
||||
|
||||
* The **world-unit measurement** ([collisionset](collisionset.md)) — positions
|
||||
* The **world-unit measurement** ([collisionset](structures/collisionset.md)) — positions
|
||||
in world units against the HUD's own distance readout. This run reached the
|
||||
mission but the flight HUD was not yet up (green 0.03 %, vs 1.3–1.5 % in
|
||||
flight), so no distance readout was available to compare against.
|
||||
|
||||
63
docs/re/five-screens-acceptance.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# ✅ The five screens, end to end — what each one is actually worth
|
||||
|
||||
**Status:** ✅ measured, 2026-08-29. One page, because the answers for these five
|
||||
screens are otherwise spread across a dozen documents and none of them says how
|
||||
good the result *is*.
|
||||
|
||||
Rendered with `sylpheed-cli screen render` (adding `--all --black` for the two
|
||||
splash halves) and correlated against the committed live captures.
|
||||
|
||||
| screen | build | elements drawn | corr vs capture | alignment |
|
||||
|---|---|---|---|---|
|
||||
| title | 4 | 15 / 24 | **+0.9500** | dy=0 dx=0 |
|
||||
| main menu | 5 | 11 / 16 | **+0.9460** | dy=0 dx=0 |
|
||||
| `EXTRAS` | 6 | 13 / 18 | **+0.9440** | dy=0 dx=0 |
|
||||
| publisher splash | 10 | 2 / 3 | **+0.9600** | dy=0 dx=0 |
|
||||
| developer splash | 11 | 6 / 7 | **+0.9643** | dy=0 dx=0 |
|
||||
|
||||
Every one aligns at **exactly zero offset** — searched ±2 px in both axes — so
|
||||
placement and scale are right and the residual is tone and detail, not geometry.
|
||||
|
||||
## Every omission is accounted for
|
||||
|
||||
The counts are not slack; each undrawn element has a reason already documented:
|
||||
|
||||
| screen | not drawn | why |
|
||||
|---|---|---|
|
||||
| title | `ptlogo1`×2, `ptlogo2`×2 | `kind & 0x4` **ghost instances** — the draw capture shows one quad per wordmark |
|
||||
| | `pteff00.prm`, `pteff02.prm` | `.prm` primitives, off without `--primitives` |
|
||||
| | `ptloop01.rat`, `ptloop02.rat` | `loop*` animations, off without `--animated`; at rest both sit off-screen |
|
||||
| | `pteff04.t32` | the **`8AX` name mismatch** — the art *is* on screen, drawn via `ptbase2` ([8AX](structures/ui-8ax-fullres-background.md)) |
|
||||
| main menu, `EXTRAS` | 2 `.prm` + 2 `loop*` + `pteff05.t32` | same three reasons |
|
||||
| both splashes | `palogo_eff0.prm` | `.prm` primitive |
|
||||
|
||||
4 + 2 + 2 + 1 = 9 on the title; 2 + 2 + 1 = 5 on each menu; 1 on each splash.
|
||||
**Nothing is unexplained.**
|
||||
|
||||
## What the residual is, per screen
|
||||
|
||||
Ranked by what a port would notice:
|
||||
|
||||
1. **Tone.** The capture is darker than the composite by a gamma of ≈1.34–1.49
|
||||
([tone curve](structures/ui-render-tone-curve.md)) — the game's own display
|
||||
ramp, not an emulator artefact. This is the largest single contributor to the
|
||||
0.94–0.96 rather than higher.
|
||||
2. **`8AX` resolution.** The background is drawn from the 640×360 `ptbase`
|
||||
upscaled 2×, where the game uses the native 1280×720 `8AX`
|
||||
([measured](structures/ui-8ax-fullres-background.md), two screens at 68 % of
|
||||
ceiling).
|
||||
3. **Paint-order ties on `EXTRAS` only.** One drawable overlapping tie
|
||||
(`ptframe3`×`ptframe4`), and the capture is consistent with our order
|
||||
([check](structures/ui-paint-order-derived-check.md)). The other four screens
|
||||
use orders measured from the game.
|
||||
4. **Rotation is decoded but not rendered** — and does not affect any of these
|
||||
five at rest ([rotation](structures/ui-keyframe-rotation.md)).
|
||||
|
||||
## Reach
|
||||
|
||||
* These are **static composites at the resting pose** against **single frames**.
|
||||
Nothing here says the animation is right; Q1's interpolation law is settled but
|
||||
a multi-keyframe group's absolute timing is not
|
||||
([time unit](ui-keyframe-time-unit.md)).
|
||||
* Correlation on a whole frame is dominated by large areas. It is a sanity figure,
|
||||
not a per-element check — the per-element work is in the documents linked above.
|
||||
263
docs/re/menu-audio-cues.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Menu audio — the event vocabulary is on the disc; the binding is not
|
||||
|
||||
**Status:** ✅ `CONFIRMED` and **decoded** for the cue vocabulary and the bank
|
||||
that holds it. 🟡 the cue↔event binding is a **name match**, not a measurement.
|
||||
❔ two negatives with their reach: no cue names a screen's music, and the audio
|
||||
for an individual SE cue cannot yet be extracted.
|
||||
|
||||
Answers most of [MISSION Q8](../port/MISSION.md).
|
||||
|
||||
## ✅ The UI cue vocabulary — decoded
|
||||
|
||||
`dat/tables.pak`'s sound table has a `SOUNDS` record of 5 798 cues. 322 are `SE_*`
|
||||
and the **low block is the user-interface vocabulary**, named by the game's
|
||||
authors after the *events*, not after the sounds:
|
||||
|
||||
| id | cue | id | cue |
|
||||
|---|---|---|---|
|
||||
| 1 | `SE_UI_START` | 9 | `SE_UI_SUB_WIN_CLS` |
|
||||
| **2** | **`SE_UI_CURSOR`** | 10 | `SE_UI_ALART_WIN` |
|
||||
| **3** | **`SE_UI_DECIDE`** | 11 | `SE_UI_PAUSE` |
|
||||
| **4** | **`SE_UI_CANSEL`** *(sic)* | 12 | `SE_UI_SPLASH_IN` |
|
||||
| **5** | **`SE_UI_IMPOSI`** | 13 | `SE_UI_SPLASH_OUT` |
|
||||
| 6 | `SE_UI_WAIT` | 14 | `SE_UI_LOAD_CMP` |
|
||||
| 7 | `SE_UI_NEXT` | 17/18 | `SE_UI_WEAPON_PLAN` / `_CREATE` |
|
||||
| 8 | `SE_UI_SUB_WIN_OPN` | 81–83 | `SE_UI_MISSION_START` / `_UPDATE` / `_END` |
|
||||
|
||||
Full 322-cue list in [`data/se-ui-cues.txt`](data/se-ui-cues.txt). The remaining
|
||||
blocks are gameplay: `SE_HUD_*` (21–83), `SE_BR_*` briefing (50–72), `SE_SW_*` /
|
||||
`SE_MW_*` weapons, `SE_DS_*` engine, `SE_EXP_*` explosions, `SE_SHIELD_*`.
|
||||
|
||||
## ✅ They all live in one bank — decoded
|
||||
|
||||
The sound table's `BANK_SE` record is a **single field**: `Static.slb`. And the
|
||||
disc-wide check agrees — **0 of the 322 `SE_*` cues has an entry in `FILES`**, the
|
||||
5 135-path list where every voice, briefing and BGM bank is named. SE is the one
|
||||
family that is not one-cue-one-file.
|
||||
|
||||
`SETTINGS` names the XACT project too: `PATH` = `game:\dat\sound.pak+`,
|
||||
`PARAM` = `Pj_Silph.xgs`.
|
||||
|
||||
## 🟡 The binding: a name match, and I am calling it that
|
||||
|
||||
MISSION asks which cue fires on **move / confirm / back / error**. Read off the
|
||||
names:
|
||||
|
||||
| event | cue |
|
||||
|---|---|
|
||||
| cursor moves | `SE_UI_CURSOR` (2) |
|
||||
| Ⓐ confirm | `SE_UI_DECIDE` (3) |
|
||||
| Ⓑ back | `SE_UI_CANSEL` (4) |
|
||||
| invalid / error | `SE_UI_IMPOSI` (5) |
|
||||
| submenu opens / closes | `SE_UI_SUB_WIN_OPN` / `_CLS` (8/9) |
|
||||
| the splash | `SE_UI_SPLASH_IN` / `_OUT` (12/13) |
|
||||
|
||||
**Nobody has watched the game emit cue 2 on a d-pad press.** This is inference
|
||||
from identifiers, and it belongs in the same box as Q4's GamePart ids — with one
|
||||
honest difference worth stating: these are the *authors' own event names*, chosen
|
||||
to describe when the sound plays, not asset labels I am interpreting. It is a
|
||||
strong name match. It is still a name match, and the port is authoring it.
|
||||
|
||||
Measuring it needs the guest's cue-play call observed with its argument — the
|
||||
same instrumentation Q4's residual wants, and not something this container can do
|
||||
today (it runs `--mute=true` against an SDL dummy device, so there is no audio
|
||||
path to watch either).
|
||||
|
||||
## ✅ Which BGM per screen — the TABLE cannot say, but the CODE can
|
||||
|
||||
All 32 BGM cues are named `BGM_001`…`BGM_109`, and nothing in `SOUNDS`, `FILES` or
|
||||
the bank headers names a screen. That negative stands **for the tables**.
|
||||
|
||||
It does not stand for the executable. `GamePart_Title`'s phase handler
|
||||
`sub_821C5580` contains `li r5, 1103` feeding a sound call — **cue 1103 =
|
||||
`BGM_103`** — and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 bytes)
|
||||
are byte-for-byte the two streams the XMA probe saw decoding at the main menu.
|
||||
Three routes, one answer; see
|
||||
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md).
|
||||
|
||||
So the per-screen BGM binding is recoverable **per screen, from the code that
|
||||
plays it** — not from any table.
|
||||
|
||||
## ❔ And a new negative: an individual SE's audio is not extractable yet
|
||||
|
||||
`Static.slb` is **8 353 472 readable bytes** (declared 8 970 240 — exactly the
|
||||
616 768 over-declaration the corpus already records), and it contains
|
||||
**0 `RIFF`, 0 `seek`, 0 `WAVE`** — scanned over the whole buffer, so this is not
|
||||
an offset problem. Every other bank on the disc is delimited by `seek` magic at
|
||||
`data_at + declared_size` (7 620/7 620 in
|
||||
[`slb-data-offset.md`](structures/slb-data-offset.md)); `Static.slb` is outside
|
||||
that population. So the route that extracts every voice line and every BGM does
|
||||
**not** give you `SE_UI_CURSOR`'s audio: the cue is named, the bank is named, and
|
||||
the wave inside the bank is not locatable by any boundary marker.
|
||||
|
||||
**The named next step is `Pj_Silph.xgs`** — the XACT project file `SETTINGS`
|
||||
points at, which in XACT is exactly where cue→wave-index lives. It **is** in
|
||||
`sound.pak`: `name_hash("Pj_Silph.xgs")` resolves to TOC index 9454. ⚠️ Its entry
|
||||
is 533 bytes at a 2048-aligned offset and carries no `XGSF` magic — and its region
|
||||
has phase 1728, so those 533 bytes are very likely the *previous* bank's tail
|
||||
rather than its own (the leading-region effect, [`slb-data-offset.md`]
|
||||
(structures/slb-data-offset.md)). Locating its real bytes is the first job, not
|
||||
parsing XACT.
|
||||
|
||||
## For the port
|
||||
|
||||
* the cue **names and ids** are disc facts — use them as the event vocabulary;
|
||||
* which event fires which cue is **authored from the table above**;
|
||||
* which BGM plays on which screen is **authored** — nothing on the disc says;
|
||||
* and the UI **sound effects cannot be exported yet**. That is a gap in the
|
||||
assets, not in the naming.
|
||||
|
||||
## ❔ `Pj_Silph.xgs` is a dead end — retracting the lead, with the reach
|
||||
|
||||
The section above named `Pj_Silph.xgs` as "the named next step", on the reasoning
|
||||
that in XACT the cue→wave index lives in the project file. **That route is dead.**
|
||||
|
||||
| check | result |
|
||||
|---|---|
|
||||
| the entry's own bytes | 533 bytes, high entropy, **no `XGSF` magic** |
|
||||
| ±8 KB around the entry in the flat concatenation | no `XGSF`, `SDBK`, `WBND`, `RIFF` or `seek` |
|
||||
| **all of `sound.pak` — 1.08 GB across `.p00`–`.p04`** | **0 × `XGSF`, 0 × `SDBK`, 0 × `WBND`** |
|
||||
| the executable | **0** occurrences of `XGSF`/`XACT`; no string matching `xact`, `.xgs`, `wavebank` or `soundbank` |
|
||||
|
||||
**Control, run first:** the same magic scan over `BGM_001.slb`'s neighbourhood
|
||||
finds `RIFF` exactly where the bank structure says it should be. The scan works;
|
||||
the magic genuinely is not there.
|
||||
|
||||
So the `.xgs` / `.slb` names are **inherited from the authoring tool, not from the
|
||||
shipped format** — nothing on this disc is a parseable XACT container. There is no
|
||||
XACT project file to read, and writing an XACT parser would have been wasted work.
|
||||
|
||||
### Where that leaves the SE audio: undecodable, with reach
|
||||
|
||||
Locating an individual SE cue's wave inside `Static.slb` is **not possible from
|
||||
anything found on the disc**. Looked in:
|
||||
|
||||
* `Static.slb` itself — 8 353 472 readable bytes, **0 `RIFF` / 0 `seek` / 0 `WAVE`**,
|
||||
where every one of the other 7 620 banks is delimited by `seek` magic at
|
||||
`data_at + declared_size` ([`structures/slb-data-offset.md`](structures/slb-data-offset.md));
|
||||
* the XACT project file — absent, as above;
|
||||
* the sound table's five records — `SOUNDS` gives cue→id, `FILES` gives bank
|
||||
paths, `BANK_SE` gives the one bank name, `SETTINGS` gives a path and the
|
||||
(absent) project file, `STAGES` is empty. **No record carries an offset.**
|
||||
|
||||
**What does corroborate the binding:** `Static.slb` is 8 353 472 bytes, and at the
|
||||
bitrates the disc uses elsewhere (25 697–31 000 B/s) that is **269–325 seconds of
|
||||
audio — 0.84 to 1.01 s per cue over 322 cues.** Exactly the shape of a bank of
|
||||
short UI/HUD/weapon effects, which is what `BANK_SE` says it is. The bank is the
|
||||
right bank; only the index into it is missing.
|
||||
|
||||
**If it is ever picked up**, the route is the running game, not the disc: watch
|
||||
the guest hand a wave offset/length to the decoder when a cue fires. That is the
|
||||
same instrumentation the cue↔event binding needs, and the same one this container
|
||||
cannot run.
|
||||
|
||||
## ✅ Retracting "cannot be extracted" — the waves are locatable, by playing them
|
||||
|
||||
The section above concluded that an individual SE cue's audio "cannot be
|
||||
extracted". **That was too strong, and the tool to do it was already in this
|
||||
build.** Canary here carries a cvar `xma_param_probe`, added for exactly this
|
||||
purpose — its own comment says it logs each XMA stream's parameters and head
|
||||
bytes "so raw sound.pak entries can be matched to real decode params".
|
||||
|
||||
Run with `--xma_param_probe=true`, drive the menu, and each newly-decoded stream
|
||||
prints one line with its packet count, channel count, sample rate and **first 32
|
||||
bytes**. Searching those bytes in `Static.slb` finds the wave.
|
||||
|
||||
| event | packets | bytes | format | offset in `Static.slb` |
|
||||
|---|---|---|---|---|
|
||||
| **d-pad move** (cursor) | 4 | 8 192 | mono 48 kHz | **`0x1ec0`** |
|
||||
| **Ⓑ back** (cancel) | 2 | 4 096 | mono 48 kHz | **`0x0ec0`** |
|
||||
| **Ⓐ confirm** (title → main menu) | 6 | 12 288 | mono 48 kHz | **`0x5d6c0`** |
|
||||
|
||||
Each head matched at **exactly one** offset. Reference data in
|
||||
[`data/se-cue-runtime-offsets.txt`](data/se-cue-runtime-offsets.txt).
|
||||
|
||||
**Structurally this also cracks the bank's layout.** The first two are
|
||||
*contiguous* — `0x0ec0 + 4096 = 0x1ec0` — so `Static.slb` is a packed run of
|
||||
XMA waves, each a whole number of 2 048-byte packets, with no delimiter between
|
||||
them. That is why the `seek`-magic scan found nothing: there is nothing to find.
|
||||
A wave is defined only by (offset, packet count), and those live in the game's
|
||||
own tables, not in the bank.
|
||||
|
||||
⚠️ **The order is not cue-id order.** `Ⓑ`'s wave precedes the cursor's in the
|
||||
file, while the name match puts `SE_UI_CURSOR` at id 2 and `SE_UI_CANSEL` at 4.
|
||||
So the index cannot be derived by counting; it has to be observed per cue.
|
||||
|
||||
### What is now measured, and what is still a name match
|
||||
|
||||
* ✅ **measured** — a d-pad move and a Ⓑ press each play a *specific, different*
|
||||
wave, and both waves are located in `Static.slb`. The port can have the audio.
|
||||
* 🟡 **still a name match** — that the cursor's wave *is the cue called*
|
||||
`SE_UI_CURSOR`. The event→wave binding is measured; the event→*name* binding is
|
||||
still read off the authors' identifiers.
|
||||
* ✅ **decoded to PCM** — see below. The cues play.
|
||||
|
||||
### ✅ The waves decode — artifact, not assertion
|
||||
|
||||
[`tools/re-capture/slb_extract_wave.py`](../../tools/re-capture/slb_extract_wave.py)
|
||||
wraps a `(bank, offset, packet count, channels, rate)` slice in a synthesized
|
||||
XMA1 `RIFF` for ffmpeg's `xma1` decoder:
|
||||
|
||||
```
|
||||
slb_extract_wave.py Static.slb 0x1ec0 4 # -> Static_0x1ec0.riff -> 0.533 s
|
||||
```
|
||||
|
||||
| event | offset | packets | decodes to | RMS | peak | envelope |
|
||||
|---|---|---|---|---|---|---|
|
||||
| d-pad move | `0x1ec0` | 4 | **0.533 s** | 2 085 | 29 813 | sharp attack, monotonic decay |
|
||||
| Ⓑ back | `0x0ec0` | 2 | **0.344 s** | 2 985 | 16 973 | attack, peak in frame 2 |
|
||||
| **Ⓐ confirm** | `0x5d6c0` | 6 | **1.016 s** | 4 327 | 32 767 | peaks in frame 3, long decay |
|
||||
|
||||
All three are mono 48 kHz, audible from sample 0, and have the percussive
|
||||
attack-and-decay shape of UI blips — not silence, not noise.
|
||||
|
||||
**Control, run first:** the *same* synthesized wrapper applied to `BGM_001.slb`'s
|
||||
first wave decodes to **173.808875 s** — identical to the duration obtained from
|
||||
that bank's own on-disc `RIFF` header in
|
||||
[`structures/bgm-two-stems.md`](structures/bgm-two-stems.md). The wrapper is not
|
||||
approximately right; it reproduces a known-good decode exactly.
|
||||
|
||||
The bitrates are consistent too: 8 192 B / 0.533 s ≈ 15.4 kB/s, 4 096 / 0.344 ≈
|
||||
11.9 kB/s, 12 288 / 1.016 ≈ 12.1 kB/s — mono at roughly half the stereo BGM rate.
|
||||
|
||||
🟡 **The decoded audio is deliberately not committed.** Three commands regenerate
|
||||
it from the disc, and the corpus keeps measurements and tooling rather than
|
||||
extracted game audio. The offsets, packet counts and this tool are the
|
||||
deliverable.
|
||||
|
||||
## The three cues the milestone needs are bound — 2026-08-28, second run
|
||||
|
||||
A second, independent boot repeated the probe and pinned the one that was still
|
||||
unattributed.
|
||||
|
||||
| event | wave | packets / bytes | runs agreeing |
|
||||
|---|---|---|---|
|
||||
| **d-pad move** | `Static.slb+0x1ec0` | 4 / 8 192 | **2 of 2** — identical head bytes |
|
||||
| **Ⓑ back** | `Static.slb+0x0ec0` | 2 / 4 096 | **2 of 2** — identical head bytes |
|
||||
| **Ⓐ confirm** | `Static.slb+0x5d6c0` | 6 / 12 288 | newly bound this run |
|
||||
|
||||
The `0x5d6c0` wave was previously logged as "played before any input", because
|
||||
the first run only started counting at the main menu. Counting from the **title**
|
||||
instead shows it appears exactly on the Ⓐ that advances title → main menu, along
|
||||
with the two stereo BGM stems (the menu's music starting).
|
||||
|
||||
So move / confirm / back — everything the five-screen milestone needs — are
|
||||
located and decodable.
|
||||
|
||||
🟡 **What that Ⓐ measurement does not separate.** The same press both *confirms*
|
||||
and *opens a screen*, so this wave could be the cue the vocabulary calls
|
||||
`SE_UI_DECIDE` or the one it calls `SE_UI_SUB_WIN_OPN`. The port needs *a* sound
|
||||
on confirm and this is the one the game plays there; which name it carries is not
|
||||
established.
|
||||
|
||||
## ❔ ⬅ and ➡ play nothing distinct
|
||||
|
||||
[`menu-navigation-semantics.md`](menu-navigation-semantics.md) measured that
|
||||
left/right do not move the cursor. They also **fire no new XMA stream** — so
|
||||
there is no distinct "invalid" cue on the main menu, and `SE_UI_IMPOSI` was not
|
||||
observed. The port should leave left/right silent.
|
||||
|
||||
⚠️ **Reach of that negative:** the probe dedups on head bytes, so a press that
|
||||
replayed an *already-heard* wave would log nothing. At that point in the run the
|
||||
cursor, confirm and BGM waves had all been heard. So this excludes a **distinct**
|
||||
invalid cue; it cannot exclude left/right quietly replaying one of those three.
|
||||
136
docs/re/menu-navigation-semantics.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# The title menu — how it moves, and where each button goes
|
||||
|
||||
**Status:** ✅ `CONFIRMED` (**measured**, by driving the running game) for the
|
||||
movement rules and for four of the five main-menu destinations. 🟡 the GamePart
|
||||
*id* behind each destination is a **name match onto the decoded id table**, not a
|
||||
measurement. ❔ `NEW GAME` deliberately untested.
|
||||
|
||||
Answers [MISSION Q5](../port/MISSION.md) and most of Q4. Nothing here is on the
|
||||
disc in any form found so far — the port is **authoring** these rules from this
|
||||
page, not transcribing a field.
|
||||
|
||||
## Q5 — movement
|
||||
|
||||
Two boots via `tools/re-capture/boot_menu.sh`, cursor read off the focus ring
|
||||
with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py).
|
||||
|
||||
| | behaviour | evidence |
|
||||
|---|---|---|
|
||||
| **initial focus, main menu** | **`TUTORIAL`** — the *middle* item, not the top | 2/2 boots, the first frame after the menu appears |
|
||||
| **initial focus, `EXTRAS`** | `MISSION SELECT` — the top item | [`extras-wrap.png`](captures/menu-nav/extras-wrap.png) |
|
||||
| **up / down** | one item per press, no auto-repeat at the durations tried | |
|
||||
| **wrap at the top** | ⬆ from the first item goes to the **last** | [`wrap-montage.png`](captures/menu-nav/wrap-montage.png), panels 1→2 |
|
||||
| **wrap at the bottom** | ⬇ from the last item goes to the **first** | same, panels 3→4, and 4 presses from `EXTRAS` landing on `OPTIONS` — i.e. wrapping — is what makes the count come out |
|
||||
| **left / right** | **nothing**, on the main menu | cursor unmoved across one ⬅ and one ➡ |
|
||||
| **Ⓑ on a submenu** | returns to the parent **with focus restored to the item you entered from** — `LOAD GAME`→`LOAD GAME`, `TUTORIAL`→`TUTORIAL`, `OPTIONS`→`OPTIONS`, `EXTRAS`→`EXTRAS` | 4/4 |
|
||||
| **Ⓑ on the main menu** | goes to the **title**, which re-draws `PRESS Ⓐ BUTTON` after a beat | |
|
||||
| **Ⓑ on the title** | **nothing** | |
|
||||
|
||||
Wrap holds on both screens tested — the 5-item main menu and the 3-item `EXTRAS`
|
||||
submenu — so it is a menu rule, not a per-screen table.
|
||||
|
||||
**🟡 Initial focus is reproducible but not established as invariant.** Both of my
|
||||
boots opened on `TUTORIAL`, and both used `boot_menu.sh`. The run recorded in
|
||||
[`menu-state-in-memory.md`](menu-state-in-memory.md) reached `EXTRAS` with *four*
|
||||
downs from the main menu, which only works from `NEW GAME`. Either the harness
|
||||
path matters or something persists. **Do not hardcode `TUTORIAL` without
|
||||
re-testing it**; what is solid is that the menu does *not* open on the top item
|
||||
in this harness.
|
||||
|
||||
## Q4 — where each button goes
|
||||
|
||||
Measured by driving: focus the item, press Ⓐ, read the screen's own title.
|
||||
|
||||
| button | screen it opens | evidence | GamePart id |
|
||||
|---|---|---|---|
|
||||
| `NEW GAME` | ❔ **not tested** | — | — |
|
||||
| `LOAD GAME` | the save-slot list, `LOAD GAME` / `Current Storage` | [`q4-destinations.png`](captures/menu-nav/q4-destinations.png) left | 🟡 `3 GP_LOAD` |
|
||||
| `TUTORIAL` | the lesson list, `TUTORIAL`, Level 1 / Level 2 | same, middle | 🟡 `25 GP_TUTORIAL` |
|
||||
| `OPTIONS` | `OPTIONS` — GAME / CONTROL / SOUND / SCREEN SETTINGS / BACK | same, right | 🟡 `8 GP_OPTIONS` |
|
||||
| `EXTRAS` | **`GP_TITLE.pak` build 6** — MISSION SELECT / MOVIE THEATER / BACK | [`ui-title-build-map.md`](ui-title-build-map.md) | 🟡 `5 GP_EXTRAS` |
|
||||
| `EXTRAS ▸ MISSION SELECT` | the stage list + Wide Area Space Map | | 🟡 `7 GP_MISSION_SELECT` |
|
||||
| `EXTRAS ▸ MOVIE THEATER` | ❔ not tested | | 🟡 `6 GP_MOVIE_THEATER` |
|
||||
|
||||
**Say which, as the gate asks.** The *screen* each button opens is **measured** —
|
||||
I pressed the button and read the title off the framebuffer. The **GamePart id is
|
||||
not measured**: it is the entry of the decoded 29-id table at `.rdata 0x820A1630`
|
||||
([`challenge-mission-gate.md`](challenge-mission-gate.md) §3) whose *name* matches
|
||||
the screen I saw. The table is decoded; the *binding* of a button to an entry in
|
||||
it is a name match I made by eye. The port should treat these ids as authored.
|
||||
|
||||
Worth noting that the ids and the paks are not one-to-one: `GP_EXTRAS` is id 5
|
||||
with **no pak of its own** — its artwork is a build inside `GP_TITLE.pak`.
|
||||
|
||||
**❔ `NEW GAME` was deliberately not pressed.** Ⓐ on it leads to a standing
|
||||
black-screen hang that ends the run
|
||||
([`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md)),
|
||||
and this iteration needed the session. It is the one destination still unmeasured.
|
||||
|
||||
### The cheap way to finish this, and to make it a measurement
|
||||
|
||||
`0x828A690C` holds a **live screen id** — `1` title, `3` main menu, `4` extras —
|
||||
and `0x828F38AC` the cursor ([`menu-state-in-memory.md`](menu-state-in-memory.md)).
|
||||
Reading those while pressing Ⓐ turns "the screen said OPTIONS" into a measured
|
||||
transition, and works under `--gpu=null` with no screenshots at all. ⚠️ Note that
|
||||
those values are **not** GamePart ids — `GP_TITLE` is GamePart 0 and `GP_EXTRAS`
|
||||
is 5, but the word reads 1 and 4 — so it is a third enumeration and mapping it to
|
||||
the id table is itself unfinished work.
|
||||
|
||||
## ✅ `NEW GAME` — measured 2026-08-28, and it is not a hang
|
||||
|
||||
The one destination this page could not test has been driven. **Ⓐ on `NEW GAME`
|
||||
does not hang.** It opens two more menus first:
|
||||
|
||||
```
|
||||
NEW GAME -> DIFFICULTY -> SELECT DATA -> guest crash
|
||||
```
|
||||
|
||||
* **`DIFFICULTY`** — `EASY` / `NORMAL` / `HARD` / `BACK`, footer
|
||||
`Ⓐ : OK Ⓑ : Back`, opening focused on **`NORMAL`**
|
||||
([capture](captures/newgame-path/newgame-difficulty.png)). It sat unchanged for
|
||||
90 s with the emulator healthy — a menu waiting for input, which is exactly
|
||||
what "a standing hang" looks like to a screenshot loop that never presses
|
||||
anything.
|
||||
* Ⓐ on `NORMAL` → **`SELECT DATA`**, a save-slot picker headed
|
||||
`Current Storage: Dummy HDD`, prompting to pick a file for the auto-save.
|
||||
* Then the guest throws, and Xenia pauses with `PC: 0x82307128`
|
||||
([capture](captures/newgame-path/newgame-selectdata-crash.png)).
|
||||
|
||||
**That crash is already in the corpus and is not new**: `0x82307128` is inside
|
||||
`sub_823070B0`, the cache-manager STL erase documented in
|
||||
[`title-crash-stl-tree.md`](title-crash-stl-tree.md), whose trigger is an
|
||||
incomplete on-disc shader/code cache — not the menu path. The corpus also already
|
||||
holds `captures/select-data-crash.png`.
|
||||
|
||||
So Q4's last row closes as **measured**:
|
||||
|
||||
| button | screen it opens |
|
||||
|---|---|
|
||||
| `NEW GAME` | **`DIFFICULTY`** (then `SELECT DATA`) |
|
||||
|
||||
🟡 The GamePart ids for these two are unmeasured, like the rest — `24 GP_DIALOG`
|
||||
and `3 GP_LOAD` / `2 GP_SELECT_STORAGE` are name-match candidates and nothing
|
||||
more.
|
||||
|
||||
### Initial focus — a fourth data point, and it still varies
|
||||
|
||||
This boot opened the main menu on **`NEW GAME`**. Running tally across four
|
||||
boots of the same harness: `TUTORIAL`, `TUTORIAL`, `NEW GAME`, `NEW GAME`.
|
||||
Unchanged conclusion: **do not hardcode it.**
|
||||
|
||||
### The `NEW GAME` path completes — the `SELECT DATA` crash is state, not path
|
||||
|
||||
A second run of the same path, 2026-08-28, **did not crash**:
|
||||
|
||||
```
|
||||
NEW GAME → DIFFICULTY → (Ⓐ on NORMAL) → SELECT DATA → (Ⓐ on a slot) → a movie plays
|
||||
```
|
||||
|
||||
The previous run's throw at `PC 0x82307128` is therefore **not inherent to this
|
||||
menu path** — the same six presses got through it. That is consistent with the
|
||||
trigger `title-crash-stl-tree.md` already names (an incomplete on-disc cache) and
|
||||
with nothing about `NEW GAME`. 🟡 n = 1 either way; do not read it as "fixed".
|
||||
|
||||
Worth recording because the first observation could easily have hardened into
|
||||
"the new-game path crashes", which is what "A on NEW GAME hangs" had already
|
||||
become once.
|
||||
@@ -264,7 +264,7 @@ and the boot never happens.
|
||||
|
||||
A Stage 02 run froze after ~4 minutes of flight (screen still `flight`, not GAME
|
||||
OVER), and `gdb_bt.sh` took backtraces of all **79** threads
|
||||
([`captures/stage02-freeze-gdb-backtraces.txt`](../captures/stage02-freeze-gdb-backtraces.txt)).
|
||||
([`captures/stage02-freeze-gdb-backtraces.txt`](captures/stage02-freeze-gdb-backtraces.txt)).
|
||||
|
||||
**Every single one is in a wait.** Not one thread is executing guest code or
|
||||
sitting in a xenia loop:
|
||||
@@ -394,7 +394,7 @@ and the control are all in place; what is missing is one frozen sample.
|
||||
The fourth run froze **9 seconds** into the watcher's window, in flight
|
||||
(`freeze_watch.sh` confirmed the HUD was still on screen), and the probe built
|
||||
for exactly this moment reported **the healthy-run baseline and nothing else**
|
||||
([`captures/stage02-freeze-stuck-wait-probe.txt`](../captures/stage02-freeze-stuck-wait-probe.txt)):
|
||||
([`captures/stage02-freeze-stuck-wait-probe.txt`](captures/stage02-freeze-stuck-wait-probe.txt)):
|
||||
|
||||
```
|
||||
FROZEN IN FLIGHT at 9s
|
||||
|
||||