Merge pull request 're: land the F5/F6 title-clock corpus (docs/re, reference data, sylpheed-formats)' (#23) from auto/frame-blend-draw-path into main
Reviewed-on: #23
This commit is contained in:
@@ -194,6 +194,36 @@ enum ScreenCommands {
|
||||
/// `--build`**, which is why it is a flag and not the default.
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
/// Pose every element at this KEYFRAME TIME instead of at its resting
|
||||
/// pose (60 units = 1 second). The resting pose is each element's last
|
||||
/// *hold* keyframe, picked independently of every other element, so it is
|
||||
/// not the screen at any one moment: it omits anything still moving (the
|
||||
/// title's light sweeps hold off the right edge) and freezes a transient
|
||||
/// at its PEAK (the title's five two-frame flashes burn forever).
|
||||
/// ⚠️ This help used to end "Prefer `--settle`". That is WITHDRAWN and was
|
||||
/// never measured: scored against a live capture of the JP title, settle
|
||||
/// gives RMSE 40.210 and rest 41.690 — a margin of 1.48 against that
|
||||
/// instrument's own noise floor of 1.2, which is NOT decisive. `--settle`
|
||||
/// also has its own failure mode (25.5 % of elements are mid-ramp at their
|
||||
/// screen's settle instant). Neither is established as better; pick by what
|
||||
/// you are measuring. See `docs/re/structures/ui-resting-pose.md`.
|
||||
#[arg(long, conflicts_with = "settle")]
|
||||
at: Option<u32>,
|
||||
/// Pose every element at the instant the screen is SETTLED, derived from
|
||||
/// the disc: the midpoint of the longest interval containing no keyframe
|
||||
/// of any element. Prints the window it used, whose width is how much the
|
||||
/// midpoint is worth — a narrow one means the bundle never settles.
|
||||
/// ⚠️ That is **38 % of the screen builds this command renders** (185 of
|
||||
/// 491 carrying two or more keyframe times) and 39 % of the wider set
|
||||
/// `--all` admits (862 of 2 211), mostly `loop*` fragments. This help used
|
||||
/// to say "42 % of them" without saying of WHAT: 42 % was 731/1 758 over
|
||||
/// composable bundles, computed before the keyframe record-layout fix,
|
||||
/// which times a group's final pose and so admits ~450 bundles that
|
||||
/// previously had only one timed keyframe. ⚠️ NOT established as better
|
||||
/// than the resting pose — see the note on `--at`. See
|
||||
/// `docs/re/structures/ui-settle-time.md`.
|
||||
#[arg(long)]
|
||||
settle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -361,8 +391,10 @@ async fn main() -> Result<()> {
|
||||
black,
|
||||
all,
|
||||
primitives,
|
||||
at,
|
||||
settle,
|
||||
} => cmd_screen_render(
|
||||
&pak, &output, build, focus, animated, black, all, primitives,
|
||||
&pak, &output, build, focus, animated, black, all, primitives, at, settle,
|
||||
),
|
||||
},
|
||||
Commands::Save { cmd } => match cmd {
|
||||
@@ -596,12 +628,42 @@ fn cmd_screen_render(
|
||||
black: bool,
|
||||
all: bool,
|
||||
primitives: bool,
|
||||
at: Option<u32>,
|
||||
settle: bool,
|
||||
) -> Result<()> {
|
||||
use sylpheed_formats::ui_layout::{self, ComposeOptions};
|
||||
let builds = screen_builds(pak, all)?;
|
||||
let idx = pick_build(&builds, want)?;
|
||||
let bytes = &builds[idx].1;
|
||||
let b = ui_layout::parse_build(bytes).context("build did not parse")?;
|
||||
let at = if settle {
|
||||
match (b.settle_window(), b.settle_time()) {
|
||||
(Some((lo, hi)), Some(t)) => {
|
||||
// Report the width, not just the answer. A 4-unit window and a
|
||||
// 190-unit one give the same kind of number and mean entirely
|
||||
// different things.
|
||||
println!(
|
||||
"settle window [{lo}, {hi}] = {} units ({:.2} s) -> posing at t={t}{}",
|
||||
hi - lo,
|
||||
(hi - lo) as f64 / 60.0,
|
||||
if hi - lo < 30 {
|
||||
" ⚠️ narrow — this bundle may never settle"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
Some(t)
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"no settle window (fewer than two distinct keyframe times) — using rest()"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
at
|
||||
};
|
||||
let screen = ui_layout::compose(
|
||||
&b,
|
||||
bytes,
|
||||
@@ -614,6 +676,7 @@ fn cmd_screen_render(
|
||||
ComposeOptions::default().backdrop
|
||||
},
|
||||
include_primitives: primitives,
|
||||
at,
|
||||
},
|
||||
None,
|
||||
);
|
||||
@@ -639,14 +702,35 @@ fn cmd_screen_render(
|
||||
screen.missing
|
||||
);
|
||||
}
|
||||
let undrawn: Vec<&str> = b
|
||||
// 🔴 Report the INDEX, the KIND and WHY, not just the name. A kind-`0x4`
|
||||
// ghost instance carries its template's name, so a bare name list shows
|
||||
// `ptlogo1.t32` twice and reads as "the logo is missing" when what is
|
||||
// skipped is two motion-trail ghosts sitting at alpha 0 off-screen. That
|
||||
// misreading cost this project a wrong finding sent to another agent.
|
||||
let undrawn: Vec<String> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| !screen.drawn.contains(&e.index))
|
||||
.map(|e| e.name.as_str())
|
||||
.map(|e| {
|
||||
let why = if e.name.ends_with(".prm") {
|
||||
"untextured primitive, needs --primitives"
|
||||
} else if e.name.ends_with(".rat") {
|
||||
"animation, needs --animated"
|
||||
} else if e.kind == 0x4 {
|
||||
"kind 0x4 ghost instance"
|
||||
} else if e.rest().map(|k| k.fade >> 24) == Some(0) {
|
||||
"transparent at its pose"
|
||||
} else {
|
||||
"no reason established"
|
||||
};
|
||||
format!("[{}] {} ({why})", e.index, e.name)
|
||||
})
|
||||
.collect();
|
||||
if !undrawn.is_empty() {
|
||||
println!(" not drawn ({}): {undrawn:?}", undrawn.len());
|
||||
println!(" not drawn ({}):", undrawn.len());
|
||||
for u in &undrawn {
|
||||
println!(" {u}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -776,8 +860,16 @@ fn cmd_audio_info(file: &Path) -> Result<()> {
|
||||
" Bit depth : {}",
|
||||
opt(info.bits_per_sample.map(|b| format!("{b}-bit")))
|
||||
);
|
||||
if let Some(b) = info.avg_bytes_per_sec {
|
||||
println!(" Byte rate : {} B/s (declared)", b.to_string().yellow());
|
||||
}
|
||||
if let Some(d) = info.duration_secs {
|
||||
println!(" Duration : {d:.2} s");
|
||||
let how = if info.codec == sylpheed_formats::AudioCodec::Xma {
|
||||
" (from the declared byte rate, not decoded)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(" Duration : {d:.2} s{how}");
|
||||
}
|
||||
if let Some(p) = info.xma_packets {
|
||||
println!(" XMA packets: {} (2048 B each)", p.to_string().yellow());
|
||||
|
||||
@@ -60,7 +60,7 @@ fn main() {
|
||||
if rows.is_empty() {
|
||||
continue;
|
||||
}
|
||||
rows.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
rows.sort_by_key(|r| r.1);
|
||||
let ys: Vec<i32> = rows.iter().map(|r| r.1).collect();
|
||||
let gaps: Vec<i32> = ys.windows(2).map(|w| w[1] - w[0]).collect();
|
||||
if rows.len() == 4
|
||||
|
||||
@@ -33,7 +33,7 @@ fn main() {
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (_, &(o, s)) in &b.records {
|
||||
for &(o, s) in b.records.values() {
|
||||
records += 1;
|
||||
if o + 12 > by.len() || o + s > by.len() {
|
||||
continue;
|
||||
|
||||
92
crates/sylpheed-formats/examples/_all2.rs
Normal file
92
crates/sylpheed-formats/examples/_all2.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let i: usize = a.next().unwrap().parse().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
let by = ar.read(&ar.entries()[i]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
println!(
|
||||
"entry {i}: {} elements, {} records, {} sprites",
|
||||
b.elements.len(),
|
||||
b.records.len(),
|
||||
b.sprites.len()
|
||||
);
|
||||
let mut rk: Vec<&String> = b.records.keys().collect();
|
||||
rk.sort();
|
||||
println!(" records: {:?}", rk);
|
||||
for (rn, &(o, sz)) in &b.records {
|
||||
if o + sz > by.len() {
|
||||
continue;
|
||||
}
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + sz]) else {
|
||||
continue;
|
||||
};
|
||||
println!(" RECORD {rn}: {} elements", lb.elements.len());
|
||||
for le in &lb.elements {
|
||||
let lts: Vec<String> = le
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"t{}a{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" [{}] {:<22} kind=0x{:<6x} {}",
|
||||
le.index,
|
||||
le.name,
|
||||
le.kind,
|
||||
lts.join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
for e in &b.elements {
|
||||
let ts: Vec<String> = e
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"t{}a{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" [{}] {:<24} kind=0x{:<6x} {}",
|
||||
e.index,
|
||||
e.name,
|
||||
e.kind,
|
||||
ts.join(" ")
|
||||
);
|
||||
if let Some(&(o, s)) = b.records.get(&e.name) {
|
||||
if o + s <= by.len() {
|
||||
if let Some(lb) = ui_layout::parse_build(&by[o..o + s]) {
|
||||
for le in &lb.elements {
|
||||
let lts: Vec<String> = le
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"t{}a{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
" leaf {:<20} kind=0x{:<6x} {}",
|
||||
le.name,
|
||||
le.kind,
|
||||
lts.join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/sylpheed-formats/examples/_keys.rs
Normal file
27
crates/sylpheed-formats/examples/_keys.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
for t in a {
|
||||
let i: usize = t.parse().unwrap();
|
||||
let by = ar.read(&ar.entries()[i]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
println!("=== entry {i} ===");
|
||||
let order = ui_layout::derived_paint_order(&b, &by);
|
||||
for e in &b.elements {
|
||||
let k = ui_layout::sprite_layer_key(&b, &by, e);
|
||||
let pos = order.iter().position(|&x| x == e.index);
|
||||
println!(
|
||||
" [{}] {:<24} kind=0x{:<5x} sprite={:<24} key={:<12} paint#{:?}",
|
||||
e.index,
|
||||
e.name,
|
||||
e.kind,
|
||||
e.sprite.clone().unwrap_or_else(|| "<none>".into()),
|
||||
k.map(|v| format!("0x{v:08x}"))
|
||||
.unwrap_or_else(|| "NONE".into()),
|
||||
pos
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
crates/sylpheed-formats/examples/_pbafc.rs
Normal file
87
crates/sylpheed-formats/examples/_pbafc.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let ar = pak::PakArchive::open(format!("{root}/dat/GP_READY_ROOM.pak")).unwrap();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let Some(p) = b.elements.iter().find(|el| el.name == "pbafc.prm") else {
|
||||
continue;
|
||||
};
|
||||
println!("=== entry {i}: {} elements ===", b.elements.len());
|
||||
println!(
|
||||
" pbafc.prm pivot=({},{}) -> {}x{}",
|
||||
p.pivot_x,
|
||||
p.pivot_y,
|
||||
p.pivot_x * 2,
|
||||
p.pivot_y * 2
|
||||
);
|
||||
for k in &p.keyframes {
|
||||
println!(
|
||||
" t={:<5} fade={:08x} a={:<4} xy=({},{}) s={}/{}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade,
|
||||
k.fade >> 24,
|
||||
k.x,
|
||||
k.y,
|
||||
k.scale_x,
|
||||
k.scale_y
|
||||
);
|
||||
}
|
||||
// what does it cover, and is anything visible while it is opaque?
|
||||
let rest = p.rest().unwrap();
|
||||
let (px, py, pw, ph) = (
|
||||
rest.x,
|
||||
rest.y,
|
||||
(p.pivot_x * 2) as i32,
|
||||
(p.pivot_y * 2) as i32,
|
||||
);
|
||||
println!(" its rect at rest: ({px},{py}) {pw}x{ph}");
|
||||
let tmax = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let op: Vec<u32> = (0..=tmax)
|
||||
.filter(|&t| p.pose_at(t).map(|k| k.fade >> 24) == Some(255))
|
||||
.collect();
|
||||
println!(
|
||||
" opaque at {} instants (t={:?}..{:?}) of 0..{tmax}",
|
||||
op.len(),
|
||||
op.first(),
|
||||
op.last()
|
||||
);
|
||||
let mut cov = 0;
|
||||
let mut vis = 0;
|
||||
for o in &b.elements {
|
||||
if o.index == p.index {
|
||||
continue;
|
||||
}
|
||||
let Some(ok) = o.rest() else { continue };
|
||||
let (ow, oh) = ((o.pivot_x * 2) as i32, (o.pivot_y * 2) as i32);
|
||||
let overlap = (px + pw).min(ok.x + ow) - px.max(ok.x) > 0
|
||||
&& (py + ph).min(ok.y + oh) - py.max(ok.y) > 0;
|
||||
if !overlap {
|
||||
continue;
|
||||
}
|
||||
cov += 1;
|
||||
if op
|
||||
.iter()
|
||||
.any(|&t| o.pose_at(t).map(|k| k.fade >> 24).unwrap_or(0) > 0)
|
||||
{
|
||||
vis += 1;
|
||||
if vis <= 6 {
|
||||
println!(" covered AND visible while opaque: {}", o.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" elements its rect covers: {cov}; visible while it is opaque: {vis}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
38
crates/sylpheed-formats/examples/_rechdr.rs
Normal file
38
crates/sylpheed-formats/examples/_rechdr.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
for t in a {
|
||||
let i: usize = t.parse().unwrap();
|
||||
let by = ar.read(&ar.entries()[i]).unwrap();
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let top = u32::from_be_bytes(by[8..12].try_into().unwrap());
|
||||
println!("entry {i:2} TOP +08 = {top}");
|
||||
let mut ks: Vec<&String> = b.records.keys().collect();
|
||||
ks.sort();
|
||||
for rn in ks {
|
||||
let &(o, s) = b.records.get(rn).unwrap();
|
||||
if o + 16 > by.len() {
|
||||
continue;
|
||||
}
|
||||
let magic = &by[o..o + 4];
|
||||
let h4 = u32::from_be_bytes(by[o + 4..o + 8].try_into().unwrap());
|
||||
let h8 = u32::from_be_bytes(by[o + 8..o + 12].try_into().unwrap());
|
||||
let maxt = ui_layout::parse_build(&by[o..o + s])
|
||||
.map(|lb| {
|
||||
lb.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
println!(" {rn:<24} magic={:?} +04={:08x}({:.1}) +08={h8:<6} max keyframe t={maxt} ratio={:.4}",
|
||||
String::from_utf8_lossy(magic), h4, h4 as f64/65536.0,
|
||||
if maxt>0 {h8 as f64/maxt as f64} else {0.0});
|
||||
}
|
||||
}
|
||||
}
|
||||
41
crates/sylpheed-formats/examples/additive_census.rs
Normal file
41
crates/sylpheed-formats/examples/additive_census.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").unwrap());
|
||||
println!("# Which elements the reference renderer now draws ADDITIVE, per screen.");
|
||||
println!("# Source: T8aD +0x04 bit 0x02, docs/re/structures/ui-blend-mode-decoded.md.");
|
||||
println!("# Generated after ui_layout::blit gained an additive path (2026-09-01).");
|
||||
println!("# Before that change EVERY row below was drawn alpha-over by our renderer,");
|
||||
println!("# which is why `verify-screen` was structurally incapable on these screens.");
|
||||
for pak in ["GP_TITLE", "GP_OPTIONS"] {
|
||||
let Ok(ar) = PakArchive::open(root.join(format!("dat/{pak}.pak"))) else {
|
||||
continue;
|
||||
};
|
||||
let n = ar.entries().len();
|
||||
for e in 0..n {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut add: Vec<&String> = b
|
||||
.sprites
|
||||
.keys()
|
||||
.filter(|s| ui_layout::blend_additive_by_name(&b, &by, s) == Some(true))
|
||||
.collect();
|
||||
if add.is_empty() {
|
||||
continue;
|
||||
}
|
||||
add.sort();
|
||||
println!(
|
||||
"\n{pak} entry {e} -- {} of {} sprites additive",
|
||||
add.len(),
|
||||
b.sprites.len()
|
||||
);
|
||||
for s in add {
|
||||
println!(" {s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
crates/sylpheed-formats/examples/adv_region_extend.rs
Normal file
61
crates/sylpheed-formats/examples/adv_region_extend.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! Does `resolve_movie_voice_region` start LATE, and by exactly how much?
|
||||
//!
|
||||
//! The port agent's arithmetic: the running decoder's three `ADV` XMA contexts sum
|
||||
//! to **3 584 000** payload bytes, but the resolved voice region is **3 114 352** —
|
||||
//! 15 % too small to hold them. One of the two spans is not what the other thinks
|
||||
//! it is, and the disc side is this crate's.
|
||||
//!
|
||||
//! The gap is exact. `ctx0` declares **632** packets (1 294 336 B); the leading
|
||||
//! chunk the resolver yields has **394** (806 912 B). The difference is **238
|
||||
//! packets = 487 424 B**, a whole number of packets — which is what a start offset
|
||||
//! looks like, not corruption.
|
||||
//!
|
||||
//! So: walk the region start backwards and report where `to_xma_riffs` first
|
||||
//! reproduces the decoder's own three sizes. The probe's byte_sizes are the
|
||||
//! control — this is not free to fit, it either lands on them or it does not.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example adv_region_extend
|
||||
|
||||
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
||||
use sylpheed_formats::slb::{self, VoiceLang};
|
||||
|
||||
/// What the running decoder reported (docs/re/structures/voice-three-streams-are-concurrent.md).
|
||||
const WANT: [usize; 3] = [1_294_336, 1_118_208, 1_171_456];
|
||||
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
let (start, end) =
|
||||
media::resolve_movie_voice_region(&src, "ADV", VoiceLang::English).expect("region");
|
||||
println!("resolver says {start}..{end} ({} B)", end - start);
|
||||
println!(
|
||||
"decoder wants {:?} = {} B payload\n",
|
||||
WANT,
|
||||
WANT.iter().sum::<usize>()
|
||||
);
|
||||
|
||||
for back_packets in [0usize, 100, 200, 237, 238, 239, 300, 400] {
|
||||
let back = (back_packets * 2048) as u64;
|
||||
if back > start {
|
||||
continue;
|
||||
}
|
||||
let s = start - back;
|
||||
let Ok(bytes) = src.read_segment_range("dat/sound", s, (end - s) as usize) else {
|
||||
println!("-{back_packets:4} packets: unreadable");
|
||||
continue;
|
||||
};
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
let sizes: Vec<usize> = riffs.iter().map(|r| r.len() - 60).collect();
|
||||
let hit = sizes.len() == 3 && sizes.iter().zip(WANT.iter()).all(|(a, b)| a == b);
|
||||
println!(
|
||||
"-{back_packets:4} packets (start {s}): {} chunk(s) {:?}{}",
|
||||
riffs.len(),
|
||||
sizes,
|
||||
if hit {
|
||||
" <== MATCHES THE DECODER"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
44
crates/sylpheed-formats/examples/adv_voice_dump.rs
Normal file
44
crates/sylpheed-formats/examples/adv_voice_dump.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Dump `ADV`'s voice chunks as RIFF/XMA, so each can be decoded and identified.
|
||||
//!
|
||||
//! `intro-audio-decomposed.md` measured that the intro's output is the movie's own
|
||||
//! WMA Pro 5.1 track at 0.600 **plus** three streams occupying a front pair, a
|
||||
//! centre (with a silent partner) and a rear pair. What it could **not** say is
|
||||
//! *which* stream sits where — the assignment there is by position, not content.
|
||||
//! The port needs that to weight a positional downmix.
|
||||
//!
|
||||
//! This writes the chunks out so they can be decoded (ffmpeg has `xma2`) and
|
||||
//! correlated against the per-channel residuals.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example adv_voice_dump -- OUTDIR [MOVIE]
|
||||
|
||||
use sylpheed_formats::media::{self, DirectorySource, DiscSource};
|
||||
use sylpheed_formats::slb::{self, VoiceLang};
|
||||
|
||||
fn main() {
|
||||
let out = std::env::args().nth(1).expect("OUTDIR");
|
||||
let movie = std::env::args().nth(2).unwrap_or_else(|| "ADV".into());
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
std::fs::create_dir_all(&out).expect("outdir");
|
||||
|
||||
let (start, end) =
|
||||
media::resolve_movie_voice_region(&src, &movie, VoiceLang::English).expect("voice region");
|
||||
let bytes = src
|
||||
.read_segment_range("dat/sound", start, (end - start) as usize)
|
||||
.expect("region");
|
||||
println!("{movie}: region {start}..{end} = {} B", end - start);
|
||||
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
println!("{} RIFF chunk(s)", riffs.len());
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let p = format!("{out}/{movie}_{i}.xma");
|
||||
std::fs::write(&p, r).expect("write");
|
||||
// the probe reports `byte_size` = RIFF total - 60; print both so the
|
||||
// dump can be tied to a specific XMA context by its own number
|
||||
println!(
|
||||
" chunk {i}: {} B byte_size-equivalent {} -> {p}",
|
||||
r.len(),
|
||||
r.len() as i64 - 60
|
||||
);
|
||||
}
|
||||
}
|
||||
44
crates/sylpheed-formats/examples/bank_streams.rs
Normal file
44
crates/sylpheed-formats/examples/bank_streams.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! List a sound bank's streams and their declared rates.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example bank_streams -- <disc> BGM_102.slb …
|
||||
use sylpheed_formats::media::{self, DirectorySource};
|
||||
use sylpheed_formats::{hash::name_hash, slb};
|
||||
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let disc = a.next().expect("usage: bank_streams <disc> NAME.slb…");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
for name in a {
|
||||
let h = name_hash(&name);
|
||||
match media::read_sound_bank(&src, h) {
|
||||
Ok(bytes) => {
|
||||
let riffs = slb::to_xma_riffs(&bytes);
|
||||
println!(
|
||||
"{name} (hash {h:08x}, {} B on disc) header {:?} -> {} stream(s)",
|
||||
bytes.len(),
|
||||
slb::bank_header_len(&bytes),
|
||||
riffs.len()
|
||||
);
|
||||
for (i, r) in riffs.iter().enumerate() {
|
||||
let rate = if r.len() >= 0x28 {
|
||||
u32::from_le_bytes(r[0x20..0x24].try_into().unwrap())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let payload = r.len() - 60;
|
||||
println!(
|
||||
" stream {i}: payload {payload} B ({} packets) declared {rate} B/s \
|
||||
=> {:.3} s",
|
||||
payload / 2048,
|
||||
if rate > 0 {
|
||||
payload as f64 / rate as f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{name}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ fn main() {
|
||||
}
|
||||
let mut degen = 0usize;
|
||||
let (mut agree, mut counted) = (0usize, 0usize);
|
||||
for t in idx.chunks_exact(3) {
|
||||
for t in idx.as_chunks::<3>().0 {
|
||||
let (x, y, z) = (t[0] as usize, t[1] as usize, t[2] as usize);
|
||||
if x == y || y == z || x == z {
|
||||
degen += 1;
|
||||
|
||||
28
crates/sylpheed-formats/examples/bgm_dump.rs
Normal file
28
crates/sylpheed-formats/examples/bgm_dump.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! Dump one BGM bank's waves as RIFF/XMA so they can be decoded and compared
|
||||
//! against a capture of the running game.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example bgm_dump -- BGM_103.slb OUTDIR
|
||||
use sylpheed_formats::media::{self, DirectorySource};
|
||||
use sylpheed_formats::slb;
|
||||
|
||||
fn main() {
|
||||
let name = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "BGM_103.slb".into());
|
||||
let out = std::env::args().nth(2).expect("OUTDIR");
|
||||
let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
std::fs::create_dir_all(&out).expect("outdir");
|
||||
let h = sylpheed_formats::hash::name_hash(&name);
|
||||
let bytes = media::read_sound_bank(&src, h).expect("bank");
|
||||
println!("{name}: {} B", bytes.len());
|
||||
for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() {
|
||||
let p = format!("{out}/{}_{i}.xma", name.trim_end_matches(".slb"));
|
||||
std::fs::write(&p, r).expect("write");
|
||||
println!(
|
||||
" wave {i}: {} B (byte_size {}) -> {p}",
|
||||
r.len(),
|
||||
r.len() - 60
|
||||
);
|
||||
}
|
||||
}
|
||||
106
crates/sylpheed-formats/examples/black_backdrop_predicate.rs
Normal file
106
crates/sylpheed-formats/examples/black_backdrop_predicate.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
//! Does a screen declare its own OPAQUE BLACK backdrop? Disc-wide.
|
||||
//!
|
||||
//! `sylpheed-port` observed that the splash builds declare `palogo_eff0.prm` as a
|
||||
//! full-screen primitive at t=0 with `fade_argb 0xff000000` -- alpha 255 over RGB
|
||||
//! 000000 -- and turned it into a candidate predicate: a declared opaque-black
|
||||
//! backdrop separates STANDALONE screens from COMPOSITED ones. On their sixteen
|
||||
//! exported screens it splits 12 / 4, with all four exceptions independently known
|
||||
//! to be composited (the two `press_start` plates, and two loading builds that
|
||||
//! carry the `pgloading_*` set without its backdrop).
|
||||
//!
|
||||
//! That matters because the corpus previously told them "no content rule exists,
|
||||
//! take the entry index" -- correct for the question asked (recognise the splash),
|
||||
//! but this is a content rule for a different and useful question. They asked for
|
||||
//! it to be tested against an archive they do not have. This is that test.
|
||||
//!
|
||||
//! CONTROL: it must reproduce the 12/4 split on GP_TITLE's sixteen composable
|
||||
//! bundles before its disc-wide numbers mean anything.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example black_backdrop_predicate
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// A screen declares its own backdrop if some `.prm` primitive holds
|
||||
/// `fade == 0xff000000` at t = 0: full alpha over black.
|
||||
fn has_black_backdrop(b: &ui_layout::UiBuild) -> Option<String> {
|
||||
for el in &b.elements {
|
||||
if !el.name.ends_with(".prm") {
|
||||
continue;
|
||||
}
|
||||
if let Some(k) = el.keyframes.iter().find(|k| k.time == Some(0)) {
|
||||
if k.fade == 0xff00_0000 {
|
||||
return Some(el.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
|
||||
println!("== CONTROL: GP_TITLE's 16 composable bundles (port reports 12 with, 4 without)");
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let (mut y, mut n) = (0, 0);
|
||||
for e in 0..16usize {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
match has_black_backdrop(&b) {
|
||||
Some(nm) => {
|
||||
y += 1;
|
||||
println!(" entry {e:>2} YES {nm}")
|
||||
}
|
||||
None => {
|
||||
n += 1;
|
||||
println!(" entry {e:>2} no")
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" -> {y} with, {n} without\n");
|
||||
|
||||
println!("== DISC-WIDE, over every screen build");
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut tot, mut with) = (0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
let (mut t, mut w) = (0usize, 0usize);
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ui_layout::is_build(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
t += 1;
|
||||
if has_black_backdrop(&b).is_some() {
|
||||
w += 1
|
||||
}
|
||||
}
|
||||
if t > 0 {
|
||||
println!("{name:30} {w:4} / {t:<4} declare a black backdrop");
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
tot += t;
|
||||
with += w;
|
||||
}
|
||||
println!(
|
||||
"\n{with} of {tot} screen builds disc-wide declare an opaque-black backdrop \
|
||||
({:.1} %)",
|
||||
100.0 * with as f64 / tot as f64
|
||||
);
|
||||
println!("--- END ---");
|
||||
}
|
||||
107
crates/sylpheed-formats/examples/blend_api_check.rs
Normal file
107
crates/sylpheed-formats/examples/blend_api_check.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! Control for the new public accessor `ui_layout::sprite_blend_additive`.
|
||||
//!
|
||||
//! `blend_vs_t8ad_bit` established the field by reading the `T8aD` header inline.
|
||||
//! The exporter cannot do that — `Element` exposed nothing at `+0x04`, which is
|
||||
//! why a blend map keyed by SCREEN NAME had to be authored, and why the Japanese
|
||||
//! menus were being asserted-by-omission to blend differently from the English
|
||||
//! ones. This checks the accessor the exporter will actually call, against the
|
||||
//! same 35 oracle rows, so a later refactor cannot silently change the field.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example blend_api_check
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// (build entry, sprite, measured additive?) — from `data/blend-bit-vs-oracle.txt`,
|
||||
/// every label an `RB_BLENDCONTROL0` value read out of the guest command stream.
|
||||
const MEASURED: &[(usize, &str, bool)] = &[
|
||||
(4, "ptbase2.t32", false),
|
||||
(4, "ptlogo1.t32", false),
|
||||
(4, "ptlogo2.t32", false),
|
||||
(4, "ptlogo_tm.t32", false),
|
||||
(4, "ptcopyright.t32", false),
|
||||
(4, "ptlogo_back2.t32", false),
|
||||
(4, "ptlogo_back2eff.t32", false),
|
||||
(2, "ptbtn00.t32", false),
|
||||
(2, "ptbtn00f.t32", true),
|
||||
(5, "ptbase.t32", false),
|
||||
(5, "ptmsg.t32", false),
|
||||
(5, "ptbtn01f.t32", false),
|
||||
(5, "ptbtneff01.t32", false),
|
||||
(5, "pteff10.t32", true),
|
||||
(5, "pteff12.t32", true),
|
||||
(6, "pteff21.t32", true),
|
||||
(6, "pteff22.t32", true),
|
||||
(6, "pteff23.t32", true),
|
||||
(6, "ptframe3.t32", true),
|
||||
(6, "ptframe4.t32", true),
|
||||
(6, "pteff03.t32", true),
|
||||
(6, "pteff03a.t32", true),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let (mut ok, mut bad, mut missing) = (0, 0, 0);
|
||||
println!(
|
||||
"{:<7} {:<22} {:<10} {:<10} ",
|
||||
"entry", "sprite", "expected", "accessor"
|
||||
);
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let by = ar.read(&ar.entries()[e]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for &(oe, name, additive) in MEASURED {
|
||||
if oe != e {
|
||||
continue;
|
||||
}
|
||||
// Prefer the Element accessor; fall back to the by-name one for
|
||||
// focused variants, which are reached through `opt ` and carry no
|
||||
// top-level element of their own.
|
||||
let got = b
|
||||
.elements
|
||||
.iter()
|
||||
.find(|x| x.sprite.as_deref() == Some(name))
|
||||
.and_then(|el| ui_layout::sprite_blend_additive(&b, &by, el))
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, &by, name));
|
||||
if got.is_none() {
|
||||
println!("{e:<7} {name:<22} {additive:<10} {:<10} MISSING", "-");
|
||||
missing += 1;
|
||||
continue;
|
||||
}
|
||||
match got {
|
||||
Some(g) if g == additive => {
|
||||
ok += 1;
|
||||
println!("{e:<7} {name:<22} {additive:<10} {g:<10} OK");
|
||||
}
|
||||
other => {
|
||||
bad += 1;
|
||||
println!("{e:<7} {name:<22} {additive:<10} {other:?} MISMATCH");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n{ok} agree, {bad} mismatched, {missing} not found (of {})",
|
||||
MEASURED.len()
|
||||
);
|
||||
// The control that removes the test's own subject: the accessor must also
|
||||
// report a MIX. An accessor stuck at one value would pass every `false` row.
|
||||
let by = ar.read(&ar.entries()[6]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let add = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| ui_layout::sprite_blend_additive(&b, &by, e) == Some(true))
|
||||
.count();
|
||||
let over = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| ui_layout::sprite_blend_additive(&b, &by, e) == Some(false))
|
||||
.count();
|
||||
println!("control -- entry 6 must report BOTH values: additive={add} alpha-over={over}");
|
||||
assert!(add > 0 && over > 0, "accessor is not discriminating");
|
||||
assert_eq!(
|
||||
bad, 0,
|
||||
"the public accessor disagrees with the committed oracle"
|
||||
);
|
||||
println!("PASS");
|
||||
}
|
||||
61
crates/sylpheed-formats/examples/blend_prediction_splash.rs
Normal file
61
crates/sylpheed-formats/examples/blend_prediction_splash.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! A PREDICTION, written before the capture that tests it.
|
||||
//!
|
||||
//! `blend_vs_t8ad_bit` finds that `T8aD +0x04` bit `0x02` separates additive from
|
||||
//! alpha-over on all 35 elements whose blend has been measured off the GPU, and
|
||||
//! that no other bit of the 48-byte header does. That is a fit to three screens.
|
||||
//!
|
||||
//! The developer splash (`GP_TITLE` entries 10 and 13) has **never been captured**
|
||||
//! and is one of the five screens the port ships. This prints what the bit says
|
||||
//! its elements should be, so the capture can falsify it rather than confirm it.
|
||||
//!
|
||||
//! Takes an archive and a build list, so the prediction can be written for any
|
||||
//! screen -- including one in a DIFFERENT pak, which is the sharper test: the
|
||||
//! splash predicts alpha-over for both its elements and so can only fail, never
|
||||
//! discriminate, while a screen with a predicted MIX can do both.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example blend_prediction_splash
|
||||
//! cargo run -p sylpheed-formats --example blend_prediction_splash -- GP_OPTIONS 0 1 2
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = args
|
||||
.iter()
|
||||
.find(|a| a.parse::<usize>().is_err())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "GP_TITLE".to_string());
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
let builds: Vec<usize> = args.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
let builds = if builds.is_empty() {
|
||||
(0..ar.entries().len()).collect()
|
||||
} else {
|
||||
builds
|
||||
};
|
||||
for e in builds {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("=== {pak} entry {e} ===");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 8 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
let w = u32::from_be_bytes([s[4], s[5], s[6], s[7]]);
|
||||
println!(
|
||||
"{n:<26} +0x04 = {w:08X} bit 0x02 {} PREDICT {}",
|
||||
if w & 2 != 0 { "SET " } else { "clear" },
|
||||
if w & 2 != 0 { "ADDITIVE" } else { "alpha-over" }
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
298
crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs
Normal file
298
crates/sylpheed-formats/examples/blend_vs_t8ad_bit.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
//! Does `T8aD +0x04` bit `0x02` predict the blend the GAME uses?
|
||||
//!
|
||||
//! ⚠️ **`REFUTED.md` kills this claim**: *"`T8aD +0x04` bit `0x02` selects an
|
||||
//! additive blend" → mine, and refuted. Blending those sprites additively
|
||||
//! worsens every measure against the capture.* That refutation rests entirely on
|
||||
//! **our renderer** — it is a claim about our renderer, and the corpus's own rule
|
||||
//! says so. Since it was written, the blend has been measured off the GPU per
|
||||
//! draw on three screens (`structures/ui-blend-mode-measured.md`), so the claim
|
||||
//! can now be tested against the oracle instead of against a render.
|
||||
//!
|
||||
//! The labels below are **not** from a render. Every one is a
|
||||
//! `RB_BLENDCONTROL0` value read out of the guest command stream and attributed
|
||||
//! to an element by quad size:
|
||||
//! `data/ui-blend-mode-measured.txt`, `data/ui-blend-title-and-replication.txt`,
|
||||
//! `data/ui-blend-extras-complete.txt`.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example blend_vs_t8ad_bit
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// (build entry, sprite, measured additive?) — the oracle's verdicts, verbatim.
|
||||
const MEASURED: &[(usize, &str, bool)] = &[
|
||||
// --- GP_TITLE entry 4 + 2, the live title -------------------------------
|
||||
(4, "ptbase2.t32", false),
|
||||
(4, "ptlogo1.t32", false),
|
||||
(4, "ptlogo2.t32", false),
|
||||
(4, "ptlogo_tm.t32", false),
|
||||
(4, "ptcopyright.t32", false),
|
||||
(4, "ptlogo_back2.t32", false),
|
||||
(4, "ptlogo_back2eff.t32", false),
|
||||
(2, "ptbtn00.t32", false),
|
||||
(2, "ptbtn00f.t32", true),
|
||||
// --- entry 5, the main menu ---------------------------------------------
|
||||
(5, "ptbase.t32", false),
|
||||
(5, "ptmsg.t32", false),
|
||||
(5, "ptbtn01f.t32", false),
|
||||
(5, "ptbtneff01.t32", false),
|
||||
(5, "pteff10.t32", true),
|
||||
(5, "pteff12.t32", true),
|
||||
(5, "ptframe1.t32", true),
|
||||
(5, "ptframe2.t32", true),
|
||||
(5, "pteff03.t32", true), // the rotated sweep strips, via ptloop01/02
|
||||
(5, "pteff03a.t32", true),
|
||||
// --- entry 6, EXTRAS ------------------------------------------------------
|
||||
(6, "ptbase.t32", false),
|
||||
(6, "ptmsg2.t32", false),
|
||||
(6, "pttitle.t32", false),
|
||||
(6, "ptbtn11f.t32", false),
|
||||
(6, "ptbtn12.t32", false),
|
||||
(6, "ptbtn13.t32", false),
|
||||
(6, "ptbtneff02.t32", false),
|
||||
(6, "pteff10.t32", true),
|
||||
(6, "pteff20.t32", true),
|
||||
(6, "pteff21.t32", true),
|
||||
(6, "pteff22.t32", true),
|
||||
(6, "pteff23.t32", true),
|
||||
(6, "ptframe3.t32", true),
|
||||
(6, "ptframe4.t32", true),
|
||||
(6, "pteff03.t32", true),
|
||||
(6, "pteff03a.t32", true),
|
||||
];
|
||||
|
||||
fn main() {
|
||||
if std::env::args().any(|a| a == "decl") {
|
||||
decl_rivals();
|
||||
return;
|
||||
}
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let mut hdr: BTreeMap<(usize, String), u32> = BTreeMap::new();
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let by = ar.read(&ar.entries()[e]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for (n, &(off, size)) in &b.sprites {
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 8 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
hdr.insert((e, n.clone()), u32::from_be_bytes([s[4], s[5], s[6], s[7]]));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:<10} {:<22} {:<10} {:>10} measured blend",
|
||||
"entry", "sprite", "+0x04", "bit 0x02"
|
||||
);
|
||||
let (mut tp, mut tn, mut fp, mut fnn, mut missing) = (0, 0, 0, 0, 0);
|
||||
for &(e, n, additive) in MEASURED {
|
||||
let Some(&w) = hdr.get(&(e, n.to_string())) else {
|
||||
println!(
|
||||
"{e:<10} {n:<22} {:<10} {:>10} {}",
|
||||
"MISSING",
|
||||
"-",
|
||||
if additive { "ADDITIVE" } else { "alpha-over" }
|
||||
);
|
||||
missing += 1;
|
||||
continue;
|
||||
};
|
||||
let bit = w & 0x02 != 0;
|
||||
match (bit, additive) {
|
||||
(true, true) => tp += 1,
|
||||
(false, false) => tn += 1,
|
||||
(true, false) => fp += 1,
|
||||
(false, true) => fnn += 1,
|
||||
}
|
||||
println!(
|
||||
"{e:<10} {n:<22} {:08X} {:>10} {}{}",
|
||||
w,
|
||||
bit,
|
||||
if additive { "ADDITIVE" } else { "alpha-over" },
|
||||
if bit == additive {
|
||||
""
|
||||
} else {
|
||||
" <== DISAGREES"
|
||||
}
|
||||
);
|
||||
}
|
||||
println!("\nbit set & additive {tp}");
|
||||
println!("bit clear & alpha-over {tn}");
|
||||
println!("bit set & alpha-over {fp} <- false positives");
|
||||
println!("bit clear & additive {fnn} <- false negatives");
|
||||
println!("sprite not found {missing}");
|
||||
println!(
|
||||
"\n{}",
|
||||
if fp == 0 && fnn == 0 && missing == 0 {
|
||||
"PERFECT PARTITION on every element whose blend was measured."
|
||||
} else {
|
||||
"THE BIT DOES NOT PREDICT THE MEASURED BLEND."
|
||||
}
|
||||
);
|
||||
|
||||
// ── THE CONTROL THAT MATTERS ────────────────────────────────────────────
|
||||
// A perfect partition is worthless if half the header partitions equally
|
||||
// well: then the sample is too small to single out a field, and picking
|
||||
// `+0x04` bit 0x02 out of the tie is the same mistake as picking `+0x08`
|
||||
// 0x8050 was. So: how many OTHER bits of the first 12 header words separate
|
||||
// the same 35 elements without error?
|
||||
let mut rivals: Vec<String> = Vec::new();
|
||||
let mut words: BTreeMap<(usize, String), Vec<u32>> = BTreeMap::new();
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let by = ar.read(&ar.entries()[e]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for (n, &(off, size)) in &b.sprites {
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 48 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
words.insert(
|
||||
(e, n.clone()),
|
||||
(0..12)
|
||||
.map(|k| {
|
||||
u32::from_be_bytes([s[k * 4], s[k * 4 + 1], s[k * 4 + 2], s[k * 4 + 3]])
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for w in 0..12 {
|
||||
for bit in 0..32 {
|
||||
let mut ok = true;
|
||||
let mut set_seen = false;
|
||||
let mut clear_seen = false;
|
||||
for &(e, n, additive) in MEASURED {
|
||||
let Some(v) = words.get(&(e, n.to_string())) else {
|
||||
ok = false;
|
||||
break;
|
||||
};
|
||||
let on = (v[w] >> bit) & 1 == 1;
|
||||
if on {
|
||||
set_seen = true
|
||||
} else {
|
||||
clear_seen = true
|
||||
}
|
||||
if on != additive {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// A constant bit trivially "agrees" with nothing; require both sides.
|
||||
if ok && set_seen && clear_seen {
|
||||
rivals.push(format!("+0x{:02X} bit {bit} (0x{:X})", w * 4, 1u32 << bit));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\nRIVAL FIELDS — other bits of the first 12 header words that separate");
|
||||
println!("the same 35 elements with zero errors: {}", rivals.len());
|
||||
for r in &rivals {
|
||||
println!(" {r}");
|
||||
}
|
||||
if rivals.len() == 1 {
|
||||
println!(" -> the sample singles out ONE field. Nothing else in the header does it.");
|
||||
} else {
|
||||
println!(
|
||||
" -> the sample does NOT single out a field; {} candidates tie.",
|
||||
rivals.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── An integrity check the published decode did NOT do ──────────────────────
|
||||
// The rival sweep above covers the 48-byte T8aD header. It does NOT cover the
|
||||
// 60-byte DECLARATION entry, and the earlier declaration hunt was run with
|
||||
// labels taken from the port's RENDER -- which put pteff10, pteff12, pteff20 and
|
||||
// pteff21..23 on the alpha-over side, where the oracle says all six are
|
||||
// additive. So the declaration has never been swept with correct labels, and if
|
||||
// one of its words also partitions the 35 without error, "the field is the T8aD
|
||||
// bit" is underdetermined.
|
||||
//
|
||||
// Run as: cargo run -p sylpheed-formats --example blend_vs_t8ad_bit -- decl
|
||||
#[allow(dead_code)]
|
||||
fn decl_rivals() {
|
||||
use std::collections::BTreeMap;
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
const AT: usize = 0x20;
|
||||
const STRIDE: usize = 60;
|
||||
let mut decl: BTreeMap<(usize, String), Vec<u32>> = BTreeMap::new();
|
||||
for e in [2usize, 4, 5, 6] {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
if by.len() < 0x18 {
|
||||
continue;
|
||||
}
|
||||
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
||||
for i in 0..count {
|
||||
let at = AT + i * STRIDE;
|
||||
if at + STRIDE > by.len() {
|
||||
break;
|
||||
}
|
||||
let end = by[at..at + 12].iter().position(|&c| c == 0).unwrap_or(12);
|
||||
let name = String::from_utf8_lossy(&by[at..at + end]).to_string();
|
||||
decl.insert(
|
||||
(e, name),
|
||||
(0..15)
|
||||
.map(|k| {
|
||||
u32::from_be_bytes([
|
||||
by[at + k * 4],
|
||||
by[at + k * 4 + 1],
|
||||
by[at + k * 4 + 2],
|
||||
by[at + k * 4 + 3],
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut missing: Vec<String> = Vec::new();
|
||||
for &(e, n, _) in MEASURED {
|
||||
if !decl.contains_key(&(e, n.to_string())) {
|
||||
missing.push(format!("entry {e} {n}"));
|
||||
}
|
||||
}
|
||||
println!("\n=== DECLARATION-ENTRY RIVAL SWEEP ===");
|
||||
println!(
|
||||
"measured elements with NO declaration entry of their own: {} of {}",
|
||||
missing.len(),
|
||||
MEASURED.len()
|
||||
);
|
||||
for m in &missing {
|
||||
println!(" {m}");
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
println!(" -> no declaration field can select the blend for these, because they");
|
||||
println!(" have no declaration entry. The header is the only per-sprite home.");
|
||||
}
|
||||
let labelled: Vec<&(usize, &str, bool)> = MEASURED
|
||||
.iter()
|
||||
.filter(|(e, n, _)| decl.contains_key(&(*e, n.to_string())))
|
||||
.collect();
|
||||
let mut rivals = 0;
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for w in 0..15 {
|
||||
for bit in 0..32 {
|
||||
let (mut ok, mut s, mut c) = (true, false, false);
|
||||
for &&(e, n, additive) in &labelled {
|
||||
let on = (decl[&(e, n.to_string())][w] >> bit) & 1 == 1;
|
||||
if on {
|
||||
s = true
|
||||
} else {
|
||||
c = true
|
||||
}
|
||||
if on != additive {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ok && s && c {
|
||||
println!(" RIVAL: declaration +0x{:02X} bit {bit}", w * 4);
|
||||
rivals += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"declaration bits that separate the {} labellable elements: {rivals}",
|
||||
labelled.len()
|
||||
);
|
||||
}
|
||||
@@ -173,7 +173,7 @@ fn main() {
|
||||
let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) =
|
||||
(0usize, 0usize, 0usize, 0usize);
|
||||
let mut rows: Vec<(usize, String)> = Vec::new();
|
||||
for (_, (voff, ibs, vcount)) in per_buf.iter() {
|
||||
for (voff, ibs, vcount) in per_buf.values() {
|
||||
let batches = ibs.len();
|
||||
let total: u32 = ibs.iter().map(|i| i.icount).sum();
|
||||
let lo = ibs.iter().map(|i| i.ibase).min().unwrap() as i64 - base_delta;
|
||||
|
||||
@@ -34,7 +34,7 @@ fn main() {
|
||||
let text = std::fs::read_to_string(log).expect("log");
|
||||
for d in parse_capture(&text) {
|
||||
let k = d.ib.map(|i| (i.ibase, i.icount)).unwrap_or((0, 0));
|
||||
if d.ib.map_or(false, |i| i.head_len > 0)
|
||||
if d.ib.is_some_and(|i| i.head_len > 0)
|
||||
&& d.pos.len() >= 4
|
||||
&& seen.insert((log.clone(), d.vbase, k))
|
||||
{
|
||||
|
||||
@@ -183,7 +183,7 @@ fn main() {
|
||||
None => clusters.push((*s, 1)),
|
||||
}
|
||||
}
|
||||
clusters.sort_by(|x, y| y.1.cmp(&x.1));
|
||||
clusters.sort_by_key(|x| std::cmp::Reverse(x.1));
|
||||
let tops: Vec<String> = clusters
|
||||
.iter()
|
||||
.filter(|(_, n)| *n >= 3)
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use sylpheed_formats::mesh::Xbg7Model;
|
||||
|
||||
/// Every place one model name was seen: (container, verts, tris, span).
|
||||
type Sightings = BTreeMap<String, Vec<(String, usize, usize, [i64; 3])>>;
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||
let list = std::env::args().any(|a| a == "--list");
|
||||
@@ -23,7 +26,7 @@ fn main() {
|
||||
files.sort();
|
||||
|
||||
// name -> [(container, verts, tris, span)]
|
||||
let mut seen: BTreeMap<String, Vec<(String, usize, usize, [i64; 3])>> = BTreeMap::new();
|
||||
let mut seen: Sightings = BTreeMap::new();
|
||||
for f in &files {
|
||||
let Ok(bytes) = std::fs::read(f) else {
|
||||
continue;
|
||||
|
||||
65
crates/sylpheed-formats/examples/decl_entry_diff.rs
Normal file
65
crates/sylpheed-formats/examples/decl_entry_diff.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! Does a textured element's declaration carry anything that distinguishes the
|
||||
//! two FRAME elements from every other element on the main menu?
|
||||
//!
|
||||
//! `sylpheed-port` asks for the blend/alpha mode of `ptframe1`/`ptframe2`. Prior
|
||||
//! work is on `.prm` PRIMITIVES (`ui-prm-blend-mode.md`, undecodable with reach —
|
||||
//! no field, the declaration words are constant) and on a refuted `T8aD +0x04`
|
||||
//! bit. Neither covers a `.t32` element's own declaration entry, which is 60
|
||||
//! bytes and mostly unread.
|
||||
//!
|
||||
//! This dumps every declaration entry on the menu and reports, per 4-byte word,
|
||||
//! whether the two frames share a value that no other element has. A word that
|
||||
//! separates exactly those two is a candidate; one that does not, is not.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_entry_diff
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const AT: usize = 0x20;
|
||||
const N: usize = 60;
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let names: Vec<String> = b.elements.iter().map(|e| e.name.clone()).collect();
|
||||
let frames: Vec<usize> = names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| n.starts_with("ptframe"))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
println!("{} elements; frames at indices {:?}", names.len(), frames);
|
||||
|
||||
let word = |i: usize, w: usize| -> u32 {
|
||||
let o = AT + i * N + w * 4;
|
||||
u32::from_be_bytes([by[o], by[o + 1], by[o + 2], by[o + 3]])
|
||||
};
|
||||
println!("\nper-word: does a value separate EXACTLY the two frames?");
|
||||
for w in 0..N / 4 {
|
||||
let fv: Vec<u32> = frames.iter().map(|&i| word(i, w)).collect();
|
||||
let same_in_frames = fv.windows(2).all(|p| p[0] == p[1]);
|
||||
let others: Vec<u32> = (0..names.len())
|
||||
.filter(|i| !frames.contains(i))
|
||||
.map(|i| word(i, w))
|
||||
.collect();
|
||||
let unique = same_in_frames && !others.contains(&fv[0]);
|
||||
let distinct = {
|
||||
let mut v: Vec<u32> = (0..names.len()).map(|i| word(i, w)).collect();
|
||||
v.sort_unstable();
|
||||
v.dedup();
|
||||
v.len()
|
||||
};
|
||||
println!(
|
||||
" +0x{:02X} frames {:?} distinct values {distinct:2}{}",
|
||||
w * 4,
|
||||
fv.iter().map(|v| format!("{v:08X}")).collect::<Vec<_>>(),
|
||||
if unique {
|
||||
" <- SEPARATES THE FRAMES"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
45
crates/sylpheed-formats/examples/decl_flag_words.rs
Normal file
45
crates/sylpheed-formats/examples/decl_flag_words.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! Which elements carry which values at the low-cardinality declaration words?
|
||||
//!
|
||||
//! `decl_entry_diff` found nothing separating `ptframe1`/`ptframe2` except their
|
||||
//! NAME — +0x00 and +0x08 are the name string ("ptfr", ".t32"), so those two hits
|
||||
//! are a false positive of that test, not a field.
|
||||
//!
|
||||
//! The remaining candidates for a per-element mode flag are the words with few
|
||||
//! distinct values: +0x28 (3) and +0x2C (6). This prints who has what.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_flag_words
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const AT: usize = 0x20;
|
||||
const N: usize = 60;
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let w = |i: usize, off: usize| -> u32 {
|
||||
let o = AT + i * N + off;
|
||||
u32::from_be_bytes([by[o], by[o + 1], by[o + 2], by[o + 3]])
|
||||
};
|
||||
println!(
|
||||
"{:<22} {:>10} {:>10} {:>10} {:>10}",
|
||||
"element", "+0x28", "+0x2C", "+0x34", "kind"
|
||||
);
|
||||
for (i, e) in b.elements.iter().enumerate() {
|
||||
let mark = if e.name.starts_with("ptframe") {
|
||||
" <- FRAME"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
"{:<22} {:>10} {:>10} {:>10} {:>#10x}{mark}",
|
||||
e.name,
|
||||
w(i, 0x28),
|
||||
w(i, 0x2C) as i32,
|
||||
w(i, 0x34),
|
||||
e.kind
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,8 @@ fn main() {
|
||||
}
|
||||
println!("\n--- schema 0x{schema:08x} {name} ({n_obj} objects) ---");
|
||||
println!(
|
||||
"{:<30} {:>5} {:>5} {}",
|
||||
"KEY", "set", "dflt", "values seen (≤12) | owners defaulting"
|
||||
"{:<30} {:>5} {:>5} values seen (≤12) | owners defaulting",
|
||||
"KEY", "set", "dflt"
|
||||
);
|
||||
for (k, (n_set, n_def, vals, owners)) in defaulted {
|
||||
let vv: Vec<&str> = vals.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
82
crates/sylpheed-formats/examples/design_size_fallback.rs
Normal file
82
crates/sylpheed-formats/examples/design_size_fallback.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
//! How often is a screen's design size READ, and how often is it FABRICATED?
|
||||
//!
|
||||
//! `ui_layout.rs` scans the `.rat` records for a `(w,h)` at `+0x18`/`+0x1c` and,
|
||||
//! finding none, falls back to `(DESIGN_W, DESIGN_H)` = 1280x720. Its own comment
|
||||
//! says "every screen seen is 1280x720, **which is also the fallback**" -- which
|
||||
//! is precisely the problem: the fabricated value equals the expected one, so no
|
||||
//! output of the parser can distinguish a read design size from an invented one.
|
||||
//! The port sizes its screens off this number.
|
||||
//!
|
||||
//! This replicates the scan through the public RATC API and counts.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example design_size_fallback
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
|
||||
|
||||
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 root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut read, mut fell_back, mut nonstd) = (0usize, 0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
let (mut r, mut f) = (0usize, 0usize);
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ui_layout::is_build(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(kids) = ratc::parse(&by) else {
|
||||
continue;
|
||||
};
|
||||
// the same predicate ui_layout uses, over the same records
|
||||
// ⚠️ A first version took EVERY RATC child and failed its control:
|
||||
// it reported all 965 builds stating a non-1280x720 size, where
|
||||
// `screen list` prints 1280x720 for every one. `records` in
|
||||
// ui_layout is the `.rat` children only; a T8aD sprite header read
|
||||
// at +0x18 is garbage that passes the range test.
|
||||
let found = kids
|
||||
.iter()
|
||||
.filter(|k| k.kind == "RATC" || k.name.ends_with(".rat"))
|
||||
.find_map(|k| {
|
||||
let rec = &by[k.offset..(k.offset + k.size).min(by.len())];
|
||||
let (w, h) = (be32(rec, 0x18), be32(rec, 0x1c));
|
||||
(w > 0 && h > 0 && w <= 8192 && h <= 8192).then_some((w, h))
|
||||
});
|
||||
match found {
|
||||
Some((w, h)) => {
|
||||
r += 1;
|
||||
if (w, h) != (1280, 720) {
|
||||
nonstd += 1;
|
||||
println!(" {name} : a build states a NON-standard design size {w}x{h}");
|
||||
}
|
||||
}
|
||||
None => f += 1,
|
||||
}
|
||||
}
|
||||
if r + f > 0 {
|
||||
println!("{name:30} {r:5} read {f:5} FABRICATED");
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
read += r;
|
||||
fell_back += f;
|
||||
}
|
||||
println!("\n{read} builds state a design size, {fell_back} get the 1280x720 FALLBACK");
|
||||
println!("{nonstd} builds state something other than 1280x720");
|
||||
println!("--- END ---");
|
||||
}
|
||||
59
crates/sylpheed-formats/examples/dialog_button_rows.rs
Normal file
59
crates/sylpheed-formats/examples/dialog_button_rows.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Is `GP_DIALOG` entry 2 the `DLG_SELECT_DIFFICULTY` screen?
|
||||
//!
|
||||
//! The image lists `DLG_SELECT_DIFFICULTY` among the `DLG_*` names at
|
||||
//! `0x820A41BB`, so DIFFICULTY is a DIALOG, not a GamePart screen with its own
|
||||
//! pak — which is why a search for an 8-record `btn` build in a difficulty-named
|
||||
//! archive found nothing. `GP_DIALOG` entry 2 carries `pcbtn00`..`03`: four
|
||||
//! buttons, matching EASY / NORMAL / HARD / BACK.
|
||||
//!
|
||||
//! CONTROL: `GP_TITLE` entry 5's five buttons must come back at the rows the disc
|
||||
//! is independently known to place them (162/242/322/401/482, spacing 80).
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_button_rows
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn rows(ar: &PakArchive, entry: usize, what: &str) {
|
||||
let Ok(by) = ar.read(&ar.entries()[entry]) else {
|
||||
return;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
return;
|
||||
};
|
||||
let mut v: Vec<(i32, String)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| {
|
||||
let n = &e.name;
|
||||
(n.starts_with("pcbtn") || n.starts_with("ptbtn")) && !n.contains('f')
|
||||
})
|
||||
.map(|e| {
|
||||
(
|
||||
e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32),
|
||||
e.name.clone(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
v.sort_by_key(|r| r.0);
|
||||
println!("\n{what} (entry {entry}):");
|
||||
for (y, n) in &v {
|
||||
println!(" y {y:5} {n}");
|
||||
}
|
||||
if v.len() > 1 {
|
||||
let sp: Vec<i32> = v.windows(2).map(|w| w[1].0 - w[0].0).collect();
|
||||
println!(" spacing {sp:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let t = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
rows(
|
||||
&t,
|
||||
5,
|
||||
"CONTROL: GP_TITLE main menu (must be 162/242/322/401/482)",
|
||||
);
|
||||
let d = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
rows(&d, 2, "GP_DIALOG candidate for DLG_SELECT_DIFFICULTY");
|
||||
rows(&d, 3, "GP_DIALOG entry 3 (the pair)");
|
||||
}
|
||||
70
crates/sylpheed-formats/examples/dialog_pair_37.rs
Normal file
70
crates/sylpheed-formats/examples/dialog_pair_37.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Are the 37 equal-button-count `GP_DIALOG` pairs language pairs, or not?
|
||||
//!
|
||||
//! 26 of 65 adjacent pairs differ in BUTTON COUNT — two languages cannot, so those
|
||||
//! are unrelated dialogs. For the rest my language reading was left UNSUPPORTED
|
||||
//! rather than refuted, and both agents observed that nothing rewards closing it.
|
||||
//!
|
||||
//! A language pair must share its BUTTON NAMES and ROWS exactly (a locale changes
|
||||
//! glyphs, not layout) and differ only elsewhere. An unrelated pair will differ in
|
||||
//! button names or rows too.
|
||||
//!
|
||||
//! CONTROL: entries 2/3 (identical element sets) must come out as "buttons match".
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_37
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn sig(ar: &PakArchive, i: usize) -> Option<(Vec<(String, i32)>, usize)> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
let mut v: Vec<(String, i32)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| e.name.contains("btn"))
|
||||
.map(|e| {
|
||||
(
|
||||
e.name.clone(),
|
||||
e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
v.sort();
|
||||
let n = v.len();
|
||||
Some((v, n))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let (mut match_btn, mut differ_btn, mut ctrl) = (0, 0, false);
|
||||
let mut examples = 0;
|
||||
for k in (0..n).step_by(2) {
|
||||
let (Some((a, na)), Some((b, nb))) = (sig(&ar, k), sig(&ar, k + 1)) else {
|
||||
continue;
|
||||
};
|
||||
if na != nb {
|
||||
continue; // the 26 already settled
|
||||
}
|
||||
if a == b {
|
||||
match_btn += 1;
|
||||
if k == 2 {
|
||||
ctrl = true
|
||||
}
|
||||
} else {
|
||||
differ_btn += 1;
|
||||
if examples < 5 {
|
||||
println!(" entries {k:3}/{:<3} buttons DIFFER", k + 1);
|
||||
println!(" {:?}", a.iter().map(|x| &x.0).collect::<Vec<_>>());
|
||||
println!(" {:?}", b.iter().map(|x| &x.0).collect::<Vec<_>>());
|
||||
examples += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\nequal-button-count pairs whose button NAMES+ROWS match : {match_btn}");
|
||||
println!("equal-button-count pairs whose buttons DIFFER : {differ_btn}");
|
||||
println!(
|
||||
"control (entries 2/3 counted as matching): {}",
|
||||
if ctrl { "PASSED" } else { "FAILED" }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Do adjacent `GP_DIALOG` entries differ in BUTTON COUNT?
|
||||
//!
|
||||
//! I offered an untested reading for why 63 of 65 adjacent pairs have different
|
||||
//! element sets: dialog text baked into language-specific sprites, so EN/JP
|
||||
//! entries differ by construction. `sylpheed-port` refuted it with a count — two
|
||||
//! languages of one dialog cannot differ in how many buttons they have. This
|
||||
//! re-derives that with my own reader before I record the refutation.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_button_counts
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn btns(ar: &PakArchive, i: usize) -> Option<usize> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().filter(|e| e.name.contains("btn")).count())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let (mut diff, mut same, mut skip) = (0, 0, 0);
|
||||
let mut show = 0;
|
||||
for k in (0..n).step_by(2) {
|
||||
match (btns(&ar, k), btns(&ar, k + 1)) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a != b {
|
||||
diff += 1;
|
||||
if show < 6 {
|
||||
println!(" entries {k:3}/{:<3} button counts {a} vs {b}", k + 1);
|
||||
show += 1;
|
||||
}
|
||||
} else {
|
||||
same += 1
|
||||
}
|
||||
}
|
||||
_ => skip += 1,
|
||||
}
|
||||
}
|
||||
println!("\nadjacent pairs differing in BUTTON COUNT: {diff}");
|
||||
println!("adjacent pairs with equal button counts : {same}");
|
||||
println!("unreadable : {skip}");
|
||||
println!("\ntwo languages of one dialog cannot differ in button count.");
|
||||
}
|
||||
51
crates/sylpheed-formats/examples/dialog_pair_diffs.rs
Normal file
51
crates/sylpheed-formats/examples/dialog_pair_diffs.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
//! What differs between equal-button-count `GP_DIALOG` adjacent pairs?
|
||||
//!
|
||||
//! All 39 share button names and rows. That is consistent with a LANGUAGE PAIR and
|
||||
//! equally with TWO DIALOGS SHARING A BUTTON TEMPLATE (two yes/no boxes differing
|
||||
//! only in their message sprite). The difference is in what else differs: a
|
||||
//! language pair should differ in the SAME slots with locale-marked names.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_diffs
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn names(ar: &PakArchive, i: usize) -> Option<BTreeSet<String>> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|e| e.name.clone()).collect())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let mut shown = 0;
|
||||
for k in (0..n).step_by(2) {
|
||||
let (Some(a), Some(b)) = (names(&ar, k), names(&ar, k + 1)) else {
|
||||
continue;
|
||||
};
|
||||
if a == b {
|
||||
continue;
|
||||
}
|
||||
let oa: Vec<&String> = a.difference(&b).collect();
|
||||
let ob: Vec<&String> = b.difference(&a).collect();
|
||||
// only the equal-button-count ones
|
||||
let ba = a.iter().filter(|x| x.contains("btn")).count();
|
||||
let bb = b.iter().filter(|x| x.contains("btn")).count();
|
||||
if ba != bb {
|
||||
continue;
|
||||
}
|
||||
if shown < 6 {
|
||||
println!(
|
||||
"entries {k:3}/{:<3} shared {} only-in-{k}: {:?} only-in-{}: {:?}",
|
||||
k + 1,
|
||||
a.intersection(&b).count(),
|
||||
oa,
|
||||
k + 1,
|
||||
ob
|
||||
);
|
||||
shown += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
crates/sylpheed-formats/examples/dialog_pair_identity.rs
Normal file
58
crates/sylpheed-formats/examples/dialog_pair_identity.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Are `GP_DIALOG` entries 0/1 and 2/3 a LANGUAGE PAIR or a DUPLICATE?
|
||||
//!
|
||||
//! They are the only two adjacent pairs in that archive with identical element
|
||||
//! sets; every other adjacent pair is two unrelated dialogs. Left open as
|
||||
//! "untested" — identical element names are equally consistent with a language
|
||||
//! pair (same layout, different glyphs baked into the textures) and with a
|
||||
//! byte-for-byte duplicate.
|
||||
//!
|
||||
//! The bytes decide it: identical entries are a duplicate; entries that share
|
||||
//! every element name but differ in payload are a language pair.
|
||||
//!
|
||||
//! CONTROL: entries 10/11, known to be two DIFFERENT dialogs (stage 10 vs stage
|
||||
//! 02), must come out as differing — and by a lot. A comparator that cannot
|
||||
//! separate two unrelated dialogs cannot judge two similar ones.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pair_identity
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::pak::PakArchive;
|
||||
|
||||
fn cmp(ar: &PakArchive, a: usize, b: usize, what: &str) {
|
||||
let (Ok(x), Ok(y)) = (ar.read(&ar.entries()[a]), ar.read(&ar.entries()[b])) else {
|
||||
println!("{what}: unreadable");
|
||||
return;
|
||||
};
|
||||
let same_len = x.len() == y.len();
|
||||
let n = x.len().min(y.len());
|
||||
let diff = (0..n).filter(|&i| x[i] != y[i]).count();
|
||||
let first = (0..n).find(|&i| x[i] != y[i]);
|
||||
println!("{what}");
|
||||
println!(
|
||||
" sizes {} / {} ({})",
|
||||
x.len(),
|
||||
y.len(),
|
||||
if same_len { "equal" } else { "DIFFER" }
|
||||
);
|
||||
println!(
|
||||
" differing bytes over the common prefix: {diff} / {n} ({:.2}%)",
|
||||
100.0 * diff as f64 / n as f64
|
||||
);
|
||||
match first {
|
||||
None if same_len => println!(" => BYTE-IDENTICAL — a duplicate"),
|
||||
None => println!(" => one is a prefix of the other"),
|
||||
Some(o) => println!(" => first difference at offset 0x{o:X}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
cmp(
|
||||
&ar,
|
||||
10,
|
||||
11,
|
||||
"CONTROL: entries 10/11 — known two different dialogs",
|
||||
);
|
||||
cmp(&ar, 0, 1, "entries 0/1");
|
||||
cmp(&ar, 2, 3, "entries 2/3 — the DIFFICULTY build");
|
||||
}
|
||||
58
crates/sylpheed-formats/examples/dialog_pairing.rs
Normal file
58
crates/sylpheed-formats/examples/dialog_pairing.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Are `GP_DIALOG`'s 140 entries adjacent EN/JP pairs, one per dialog record?
|
||||
//!
|
||||
//! The dialog table has 70 records and the archive has 140 entries. If the
|
||||
//! pairing is adjacent — (0,1), (2,3), … — then dialog index = entry / 2, and the
|
||||
//! unbound id-to-entry join becomes an ordering question rather than a search.
|
||||
//!
|
||||
//! Test: for each pair, compare the SET of element names. GP_TITLE's EN/JP pairs
|
||||
//! share their sprite sets exactly except for the title art (4/7), so identical
|
||||
//! sets are the signature of a language pair.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pairing
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn names(ar: &PakArchive, i: usize) -> Option<BTreeSet<String>> {
|
||||
let by = ar.read(&ar.entries()[i]).ok()?;
|
||||
let b = ui_layout::parse_build(&by)?;
|
||||
Some(b.elements.iter().map(|e| e.name.clone()).collect())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let (mut adj_same, mut adj_diff, mut skipped) = (0, 0, 0);
|
||||
for k in (0..n).step_by(2) {
|
||||
match (names(&ar, k), names(&ar, k + 1)) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a == b {
|
||||
adj_same += 1;
|
||||
println!(" identical pair: entries {k}/{}", k + 1)
|
||||
} else {
|
||||
adj_diff += 1
|
||||
}
|
||||
}
|
||||
_ => skipped += 1,
|
||||
}
|
||||
}
|
||||
println!("ADJACENT pairing (2k, 2k+1): identical {adj_same} differing {adj_diff} unreadable {skipped}");
|
||||
// rival hypothesis: halves, (i, i+70)
|
||||
let (mut h_same, mut h_diff, mut h_skip) = (0, 0, 0);
|
||||
for k in 0..n / 2 {
|
||||
match (names(&ar, k), names(&ar, k + n / 2)) {
|
||||
(Some(a), Some(b)) => {
|
||||
if a == b {
|
||||
h_same += 1
|
||||
} else {
|
||||
h_diff += 1
|
||||
}
|
||||
}
|
||||
_ => h_skip += 1,
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"HALVES pairing (i, i+70): identical {h_same} differing {h_diff} unreadable {h_skip}"
|
||||
);
|
||||
}
|
||||
29
crates/sylpheed-formats/examples/dialog_pak_shape.rs
Normal file
29
crates/sylpheed-formats/examples/dialog_pak_shape.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
//! How is `GP_DIALOG.pak` organised? Testing whether dialog id maps positionally.
|
||||
//!
|
||||
//! The dialog table gives name -> id (70 records, `DLG_SELECT_DIFFICULTY` = 2000)
|
||||
//! and the disc gives a unique four-button build at GP_DIALOG entries 2/3. Nothing
|
||||
//! joins them. If the archive were laid out in table order, or in id order, the
|
||||
//! join would be positional — this checks.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example dialog_pak_shape
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_DIALOG.pak")).expect("GP_DIALOG");
|
||||
let n = ar.entries().len();
|
||||
let mut builds = 0;
|
||||
let mut with_btn = 0;
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if let Some(b) = ui_layout::parse_build(&by) {
|
||||
builds += 1;
|
||||
if b.elements.iter().any(|el| el.name.contains("btn")) {
|
||||
with_btn += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("entries {n}, parse as builds {builds}, of those with a btn element {with_btn}");
|
||||
println!("dialog table has 70 records; 70 x 2 (EN/JP) = 140");
|
||||
}
|
||||
@@ -5,7 +5,7 @@ fn short(s: &str) -> String {
|
||||
.trim_start_matches("UnitName_UN_")
|
||||
.into()
|
||||
}
|
||||
fn g<'a>(o: &'a IdxdObject, k: &str) -> String {
|
||||
fn g(o: &IdxdObject, k: &str) -> String {
|
||||
o.get_f32(k)
|
||||
.map(|v| {
|
||||
if v == v.trunc() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use sylpheed_formats::{game_data, localization::TextIndex, PakArchive};
|
||||
use sylpheed_formats::{localization::TextIndex, PakArchive};
|
||||
fn main() {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap();
|
||||
let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
|
||||
@@ -66,7 +66,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
let (mut shared, mut inconsistent) = (0usize, 0usize);
|
||||
for (_, list) in &seen {
|
||||
for list in seen.values() {
|
||||
if list.len() < 2 || !list.iter().all(|e| e.1 == list[0].1 && e.2 == list[0].2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
113
crates/sylpheed-formats/examples/element_records.rs
Normal file
113
crates/sylpheed-formats/examples/element_records.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Every record reachable from an element — leaf **and** `focus_link` — plus a
|
||||
//! disc-wide census of how many elements have more than one.
|
||||
//!
|
||||
//! ⚠️ WHY. I claimed `ptbtn00f`'s peak alpha of 80 was undeclared, having read
|
||||
//! `ptbtn00.rat` (the leaf, flat 255) and stopped. The pulse is in
|
||||
//! `ptbtn00f.rat`, the focus record. `focus_link` was already parsed and
|
||||
//! `ui_layout.rs` already documented it: the format was known and I did not
|
||||
//! consult it. An absence claim is only as good as its enumeration, so this
|
||||
//! enumerates rather than asking the reader to remember.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example element_records -- GP_TITLE ptbtn00
|
||||
//! cargo run -p sylpheed-formats --example element_records -- --census
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
if argv.iter().any(|a| a == "--census") {
|
||||
let (mut els, mut linked, mut screens_with) = (0usize, 0usize, 0usize);
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for ent in ar.entries() {
|
||||
let Ok(by) = ar.read(ent) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let n = b.elements.iter().filter(|e| e.focus_link.is_some()).count();
|
||||
els += b.elements.len();
|
||||
linked += n;
|
||||
if n > 0 {
|
||||
screens_with += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("elements disc-wide : {els}");
|
||||
println!(
|
||||
"with a focus_link record : {linked} ({:.1}%)",
|
||||
100.0 * linked as f64 / els as f64
|
||||
);
|
||||
println!("builds containing at least 1: {screens_with}");
|
||||
println!("\nEach of those carries a SECOND record whose keyframes are invisible");
|
||||
println!("to anyone who looks up the leaf by name and stops.");
|
||||
return;
|
||||
}
|
||||
|
||||
let pak = argv.first().cloned().unwrap_or_else(|| "GP_TITLE".into());
|
||||
let want = argv.get(1).cloned();
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
for (i, ent) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(ent) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if let Some(w) = &want {
|
||||
if !el.name.starts_with(w.as_str()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if el.focus_link.is_none() && want.is_none() {
|
||||
continue;
|
||||
}
|
||||
println!("entry {i}: {}", el.name);
|
||||
let stem = el.name.trim_end_matches(".rat");
|
||||
for (tag, rec) in [
|
||||
("leaf", format!("{stem}.rat")),
|
||||
("focus", el.focus_link.clone().unwrap_or_default()),
|
||||
] {
|
||||
if rec.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match b.records.get(rec.as_str()) {
|
||||
Some(&(lo, ls)) => {
|
||||
let bytes = &by[lo..(lo + ls).min(by.len())];
|
||||
let loop_u = ui_layout::loop_length_units(bytes);
|
||||
let kf: Vec<String> = ui_layout::parse_build(bytes)
|
||||
.map(|lb| {
|
||||
lb.elements
|
||||
.iter()
|
||||
.map(|e| {
|
||||
format!(
|
||||
"{} [{} keys, peak a{}]",
|
||||
e.name,
|
||||
e.keyframes.len(),
|
||||
e.keyframes
|
||||
.iter()
|
||||
.map(|k| k.fade >> 24)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
println!(" {tag:<6} {rec:<20} loop {:?} {}", loop_u, kf.join(", "));
|
||||
}
|
||||
None => println!(" {tag:<6} {rec:<20} (no such record)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/sylpheed-formats/examples/extras_button_order.rs
Normal file
48
crates/sylpheed-formats/examples/extras_button_order.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Is `ptbtn11` the TOP item of the `EXTRAS` screen?
|
||||
//!
|
||||
//! `sylpheed-port` authors `extras/initial_focus: ptbtn11` and states it is
|
||||
//! correct under the surviving reading — "a submenu resets to the item it opens
|
||||
//! on". The oracle shows EXTRAS opening on `MISSION SELECT`, the first of
|
||||
//! MISSION SELECT / MOVIE THEATER / BACK. So their value is right only if
|
||||
//! `ptbtn11` is that first item. This checks it against the disc.
|
||||
//!
|
||||
//! CONTROL: the same read on the MAIN MENU build, whose five buttons have a known
|
||||
//! top-to-bottom order (NEW GAME first). If the ordering rule cannot reproduce a
|
||||
//! known screen it cannot be trusted on an unknown one.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example extras_button_order
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn report(ar: &PakArchive, entry: usize, what: &str) {
|
||||
let Ok(by) = ar.read(&ar.entries()[entry]) else {
|
||||
return;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
return;
|
||||
};
|
||||
let mut rows: Vec<(i32, String, u32)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| e.name.starts_with("ptbtn") && !e.name.contains('f'))
|
||||
.map(|e| {
|
||||
let y = e.rest().map(|k| k.y).unwrap_or(e.pivot_y as i32);
|
||||
(y, e.name.clone(), e.kind)
|
||||
})
|
||||
.collect();
|
||||
rows.sort_by_key(|r| r.0);
|
||||
println!("\n{what} (entry {entry}) — buttons top to bottom:");
|
||||
for (y, n, k) in &rows {
|
||||
println!(" y {y:5} {n:14} kind 0x{k:04x}");
|
||||
}
|
||||
if let Some((_, first, _)) = rows.first() {
|
||||
println!(" => TOP item is {first}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
report(&ar, 5, "CONTROL: main menu (NEW GAME must be top)");
|
||||
report(&ar, 6, "EXTRAS");
|
||||
}
|
||||
69
crates/sylpheed-formats/examples/find_difficulty_build.rs
Normal file
69
crates/sylpheed-formats/examples/find_difficulty_build.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
//! Where does the `DIFFICULTY` screen live?
|
||||
//!
|
||||
//! `boot-config-and-gamepart-registry.md` records a count-match — "Ⓑ = event 0,
|
||||
//! four menu items load an external archive, EXTRAS stays inside GP_TITLE" —
|
||||
//! explicitly as an observation, not a decode. The disc can test half of it:
|
||||
//! OPTIONS, LOAD GAME and TUTORIAL have their own paks, and EXTRAS' two items
|
||||
//! have GP_MISSION_SELECT / GP_MOVIE_THEATER while EXTRAS itself is GP_TITLE
|
||||
//! entries 6/9. NEW GAME is the fourth, and there is no GP_DIFFICULTY.pak.
|
||||
//!
|
||||
//! So: which archive holds a build with EASY / NORMAL / HARD buttons?
|
||||
//!
|
||||
//! CONTROL: the same scan must find the EXTRAS build in GP_TITLE, whose location
|
||||
//! is independently known (entries 6/9, buttons ptbtn11/12/13).
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_difficulty_build
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let dat = root.join("dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dat)
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let mut found_extras = false;
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let btns: Vec<&String> = b
|
||||
.records
|
||||
.keys()
|
||||
.filter(|n| n.starts_with("ptbtn") || n.contains("btn"))
|
||||
.collect();
|
||||
if btns.len() != 8 {
|
||||
continue;
|
||||
}
|
||||
let name = p.file_name().unwrap().to_string_lossy();
|
||||
// CONTROL: the known MAIN MENU build (11 records, so this control is now vacuous) must show up.
|
||||
if name == "GP_TITLE.pak" && (i == 5 || i == 8) {
|
||||
found_extras = true;
|
||||
println!("CONTROL {name} entry {i}: {} button records — the known MAIN MENU build (11 records, so this control is now vacuous)",
|
||||
btns.len());
|
||||
}
|
||||
// any build outside GP_TITLE with a small button set is a candidate
|
||||
if name != "GP_TITLE.pak" {
|
||||
let mut names: Vec<String> = btns.iter().map(|s| (*s).clone()).collect();
|
||||
names.sort();
|
||||
println!(
|
||||
" {name:28} entry {i:3} {} buttons {:?}",
|
||||
btns.len(),
|
||||
&names[..names.len().min(8)]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\ncontrol {} — the known MAIN MENU build (11 records, so this control is now vacuous) was {}found",
|
||||
if found_extras { "PASSED" } else { "FAILED" },
|
||||
if found_extras { "" } else { "NOT " });
|
||||
}
|
||||
68
crates/sylpheed-formats/examples/find_difficulty_names.rs
Normal file
68
crates/sylpheed-formats/examples/find_difficulty_names.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Where is the `DIFFICULTY` screen? Search by NAME, not by structure.
|
||||
//!
|
||||
//! A previous pass searched every pak for a build with exactly 8 `btn`-named
|
||||
//! records, on the assumption that DIFFICULTY's four items (EASY / NORMAL / HARD
|
||||
//! / BACK) pair with `f` focus variants the way GP_TITLE's screens do. Nothing
|
||||
//! plausible turned up, and the assumption was mine — recorded as a negative
|
||||
//! narrower than "not found" (data/gp-title-holds-three-button-screens.txt).
|
||||
//!
|
||||
//! This drops the structural assumption and looks for the words instead, across
|
||||
//! every sprite AND record name in every build on the disc.
|
||||
//!
|
||||
//! CONTROL: the same scan must find `ptbtn11` in GP_TITLE — a name whose home is
|
||||
//! independently known — when asked for it. A name scan that finds nothing
|
||||
//! proves nothing unless it can find something.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_difficulty_names
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const WANTED: &[&str] = &["easy", "normal", "hard", "diff", "level", "rank"];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let mut control = false;
|
||||
let mut hits = 0usize;
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let pname = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let all: Vec<String> = b.sprites.keys().chain(b.records.keys()).cloned().collect();
|
||||
if pname == "GP_TITLE.pak" && all.iter().any(|n| n.contains("ptbtn11")) {
|
||||
control = true;
|
||||
}
|
||||
let m: Vec<&String> = all
|
||||
.iter()
|
||||
.filter(|n| {
|
||||
let l = n.to_lowercase();
|
||||
WANTED.iter().any(|w| l.contains(w))
|
||||
})
|
||||
.collect();
|
||||
if !m.is_empty() {
|
||||
hits += 1;
|
||||
let mut s: Vec<String> = m.iter().map(|x| (*x).clone()).collect();
|
||||
s.sort();
|
||||
s.dedup();
|
||||
println!(" {pname:28} entry {i:3} {:?}", &s[..s.len().min(6)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\ncontrol (found ptbtn11 in GP_TITLE): {}",
|
||||
if control { "PASSED" } else { "FAILED" }
|
||||
);
|
||||
println!("{hits} build(s) carried a difficulty-ish name");
|
||||
}
|
||||
125
crates/sylpheed-formats/examples/find_stream_by_size.rs
Normal file
125
crates/sylpheed-formats/examples/find_stream_by_size.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! Which cue owns an XMA stream of a given payload size?
|
||||
//!
|
||||
//! A boot with `--xma_param_probe` logs each decoded stream's `byte_size`. Three
|
||||
//! of the five on the take-2 `ADV` boot are that movie's own streams; two —
|
||||
//! 1 150 976 and 1 269 760 B — belong to something unidentified. The probe gives
|
||||
//! a size and nothing else, so the disc has to be asked which cue has a stream
|
||||
//! that long.
|
||||
//!
|
||||
//! Searches every inter-descriptor span of the continuous voice stream, and
|
||||
//! every `sound.pak` entry, for a stream whose payload matches.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example find_stream_by_size -- <disc> <bytes>…
|
||||
use sylpheed_formats::media::{DirectorySource, DiscSource};
|
||||
use sylpheed_formats::{slb, PakArchive};
|
||||
|
||||
const DESC_MARK: u32 = 0x11;
|
||||
const DESC_REPEAT: usize = 0x800;
|
||||
const ID_MAX: u32 = 0x1_0000;
|
||||
|
||||
fn all_descriptors(buf: &[u8]) -> Vec<(usize, u32)> {
|
||||
let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]);
|
||||
let mut out = Vec::new();
|
||||
if buf.len() < DESC_REPEAT + 8 {
|
||||
return out;
|
||||
}
|
||||
let end = buf.len() - (DESC_REPEAT + 4);
|
||||
let mut o = 0;
|
||||
while o <= end {
|
||||
let id = be(o);
|
||||
if (1..ID_MAX).contains(&id) && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id {
|
||||
out.push((o, id));
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let disc = args
|
||||
.next()
|
||||
.expect("usage: find_stream_by_size <disc> <bytes>…");
|
||||
let wanted: Vec<usize> = args.filter_map(|a| a.parse().ok()).collect();
|
||||
assert!(!wanted.is_empty(), "give at least one payload size");
|
||||
// A `to_xma_riffs` chunk is the payload plus a 60-byte RIFF wrapper.
|
||||
let want_riff: Vec<usize> = wanted.iter().map(|w| w + 60).collect();
|
||||
println!("looking for payloads {wanted:?} (riff sizes {want_riff:?})\n");
|
||||
let src = DirectorySource::new(std::path::PathBuf::from(&disc));
|
||||
|
||||
// --- 1. the continuous movie-voice stream, span by span
|
||||
let tpak = src.open_pak("dat/tables.pak").expect("tables.pak");
|
||||
let marker = "eng\\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()))
|
||||
})
|
||||
.expect("registry");
|
||||
let ids = sylpheed_formats::movie_voice::registry_voice_ids(®istry);
|
||||
let name_of: std::collections::HashMap<u32, String> =
|
||||
ids.iter().map(|(n, &i)| (i, n.clone())).collect();
|
||||
let win_start: u64 = 421_739_888 & !3;
|
||||
let buf = src
|
||||
.read_segment_range("dat/sound", win_start, 116_300_000)
|
||||
.expect("window");
|
||||
let descs = all_descriptors(&buf);
|
||||
println!("voice stream: {} descriptors", descs.len());
|
||||
let mut hits = 0;
|
||||
for w in descs.windows(2) {
|
||||
let (a, b) = (w[0].0, w[1].0);
|
||||
if b <= a || b - a < 4096 {
|
||||
continue;
|
||||
}
|
||||
for (i, r) in slb::to_xma_riffs(&buf[a..b]).iter().enumerate() {
|
||||
if want_riff.contains(&r.len()) {
|
||||
let name = name_of
|
||||
.get(&w[1].1)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("id{}", w[1].1));
|
||||
println!(
|
||||
" ✅ cue {name} (id {}) stream {i}: payload {} B",
|
||||
w[1].1,
|
||||
r.len() - 60
|
||||
);
|
||||
hits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" {hits} hit(s) in the voice stream\n");
|
||||
|
||||
// --- 2. every sound.pak entry
|
||||
let stoc = src.read_file("dat/sound.pak").expect("sound.pak toc");
|
||||
let entries = PakArchive::parse_toc(&stoc).expect("toc");
|
||||
println!("sound.pak: {} entries", entries.len());
|
||||
let mut phits = 0;
|
||||
let mut scanned = 0usize;
|
||||
for e in &entries {
|
||||
// Only entries big enough to hold the target.
|
||||
let need = wanted.iter().copied().min().unwrap_or(0) as u32;
|
||||
if e.comp_size < need {
|
||||
continue;
|
||||
}
|
||||
let Ok(bytes) = src.read_segment_range("dat/sound", e.offset as u64, e.comp_size as usize)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
scanned += 1;
|
||||
for (i, r) in slb::to_xma_riffs(&bytes).iter().enumerate() {
|
||||
if want_riff.contains(&r.len()) {
|
||||
println!(
|
||||
" ✅ sound.pak entry hash {:08x} offset {} size {} — stream {i}: payload {} B",
|
||||
e.name_hash,
|
||||
e.offset,
|
||||
e.comp_size,
|
||||
r.len() - 60
|
||||
);
|
||||
phits += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(" scanned {scanned} entries large enough; {phits} hit(s)");
|
||||
}
|
||||
97
crates/sylpheed-formats/examples/focus_alpha_census.rs
Normal file
97
crates/sylpheed-formats/examples/focus_alpha_census.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Which focus records have a VARYING alpha — disc-wide, not export-wide?
|
||||
//!
|
||||
//! `rest()` returns an element's last hold keyframe. For a constant-alpha
|
||||
//! element that is harmless. For one that pulses it returns the PEAK, which is
|
||||
//! the `ui-settle-time.md` pathology: the plate's `ptbtn00f` ramps 0→80→0 and
|
||||
//! `rest()` reports 80, its maximum.
|
||||
//!
|
||||
//! The port censused this over its own export (34 records, 2 varying) and
|
||||
//! concluded there is nothing to fix. That conclusion is only as wide as the
|
||||
//! export. This asks the same question of the whole disc.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut n_rec, mut n_elem, mut varying) = (0usize, 0usize, 0usize);
|
||||
let (mut at_peak, mut mid_ramp) = (0usize, 0usize);
|
||||
let mut by_pak: std::collections::BTreeMap<String, usize> = Default::default();
|
||||
let mut hits: Vec<String> = Vec::new();
|
||||
for p in &paks {
|
||||
let pn = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (ei, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (rn, &(o, s)) in &b.records {
|
||||
// A focus record is one whose name is another record's plus `f`.
|
||||
let Some(stem) = rn.strip_suffix("f.rat") else {
|
||||
continue;
|
||||
};
|
||||
if !b.records.contains_key(&format!("{stem}.rat")) {
|
||||
continue;
|
||||
}
|
||||
if o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
n_rec += 1;
|
||||
for el in &lb.elements {
|
||||
if el.keyframes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
n_elem += 1;
|
||||
let a: Vec<u32> = el.keyframes.iter().map(|k| k.fade >> 24).collect();
|
||||
let (lo, hi) = (*a.iter().min().unwrap(), *a.iter().max().unwrap());
|
||||
if lo == hi {
|
||||
continue;
|
||||
}
|
||||
varying += 1;
|
||||
let rest = el.rest().map(|k| k.fade >> 24).unwrap_or(0);
|
||||
if rest == hi {
|
||||
at_peak += 1
|
||||
} else {
|
||||
mid_ramp += 1
|
||||
}
|
||||
*by_pak.entry(pn.clone()).or_default() += 1;
|
||||
hits.push(format!(
|
||||
"{pn} [{ei}] {rn}::{} alpha {lo}..{hi} rest()={rest}{}",
|
||||
el.name,
|
||||
if rest == hi { " 🔴 == PEAK" } else { "" }
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("focus records disc-wide : {n_rec}");
|
||||
println!(" their timed elements : {n_elem}");
|
||||
println!(" with a VARYING alpha : {varying}");
|
||||
println!(" of which rest() == the PEAK : {at_peak} <- burns bright forever");
|
||||
println!(" of which rest() is MID-RAMP : {mid_ramp} <- neither extreme; looks plausible");
|
||||
println!("\nby pak:");
|
||||
for (k, v) in &by_pak {
|
||||
println!(" {k:<34} {v}")
|
||||
}
|
||||
println!("\nevery varying one:");
|
||||
hits.sort();
|
||||
hits.dedup();
|
||||
for h in &hits {
|
||||
println!(" {h}")
|
||||
}
|
||||
println!("\n({} distinct)", hits.len());
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! Reconcile two ink counts for one screen that were never counting the same pixels.
|
||||
//!
|
||||
//! The port agent double-witnessed the pixel-cost claim in Godot — a renderer
|
||||
//! sharing no code with `compose` — and got `GP_TITLE` entry 12 at **59 530 px**
|
||||
//! ink above threshold 0 and **48 368** above 1. This crate reported **49 771**.
|
||||
//! Neither is wrong; the question is which convention each was using, and on a
|
||||
//! mostly-dark frame the answer moves thousands of pixels.
|
||||
//!
|
||||
//! So: count the same composite every way, and print the family. Whichever row
|
||||
//! the port's numbers land in is the convention, and then the two renderers can be
|
||||
//! compared on purpose rather than by coincidence.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_ink_thresholds
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use ui_layout::ComposeOptions;
|
||||
|
||||
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
|
||||
idx.sort_by_key(|&i| {
|
||||
let el = &build.elements[i];
|
||||
(
|
||||
ui_layout::sprite_layer_key(build, bundle, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name))
|
||||
.unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
});
|
||||
idx
|
||||
}
|
||||
|
||||
fn counts(rgba: &[u8], t: u8) -> (usize, usize) {
|
||||
let rgb = rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|p| p[0] > t || p[1] > t || p[2] > t)
|
||||
.count();
|
||||
let alpha = rgba.as_chunks::<4>().0.iter().filter(|p| p[3] > t).count();
|
||||
(rgb, alpha)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
|
||||
for entry in [12usize, 15] {
|
||||
let by = ar.read(&ar.entries()[entry]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("parse");
|
||||
for (label, opts) in [
|
||||
(
|
||||
"primitives on (what the cost run used)",
|
||||
ComposeOptions {
|
||||
include_primitives: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"primitives+focus+animated",
|
||||
ComposeOptions {
|
||||
include_primitives: true,
|
||||
include_focus: true,
|
||||
include_animated: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
] {
|
||||
let with = ui_layout::derived_paint_order(&b, &by);
|
||||
let without = order_without_rule(&b, &by);
|
||||
let a = ui_layout::compose_with_order(&b, &by, opts, None, Some(&with));
|
||||
let c = ui_layout::compose_with_order(&b, &by, opts, None, Some(&without));
|
||||
println!(
|
||||
"\n== GP_TITLE entry {entry} — {label} ({}x{})",
|
||||
a.width, a.height
|
||||
);
|
||||
println!(
|
||||
" threshold | RGB>t with rule | A>t with rule | RGB>t WITHOUT | A>t WITHOUT"
|
||||
);
|
||||
for t in [0u8, 1, 2, 4, 8, 16] {
|
||||
let (r1, a1) = counts(&a.rgba, t);
|
||||
let (r0, a0) = counts(&c.rgba, t);
|
||||
println!(" >{t:<8} | {r1:>16} | {a1:>14} | {r0:>13} | {a0:>11}");
|
||||
}
|
||||
let changed = a
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(c.rgba.as_chunks::<4>().0.iter())
|
||||
.filter(|(x, y)| x != y)
|
||||
.count();
|
||||
println!(" exact-RGBA changed pixels between the two orders: {changed}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Of the forced instances the rule merely CONFIRMS, how many have a key READ
|
||||
//! FROM THE FILE, and how many an IMPLIED key that is itself a measurement?
|
||||
//!
|
||||
//! `forced_backdrop_necessity.rs` asked only whether an element had *a* key,
|
||||
//! collapsing `sprite_layer_key` (a `u16` read out of the `T8aD` header — decoded)
|
||||
//! with `implied_layer_key` (this crate's per-name table of positions **measured
|
||||
//! in the running game**). For counting whether the rule moves anything that is
|
||||
//! the right question. For describing what a confirmation is *made of*, it is not:
|
||||
//! "the file already settles it" and "another measurement already settles it" are
|
||||
//! different claims, and a reader who sees "own key" will take the first.
|
||||
//!
|
||||
//! Raised by the port agent 2026-08-30.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_key_source
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let (mut read, mut implied, mut none) = (0usize, 0usize, 0usize);
|
||||
println!("# archive entry element key_source key");
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if !ui_layout::forced_backdrop(&b, el) {
|
||||
continue;
|
||||
}
|
||||
let (src, key) = match ui_layout::sprite_layer_key(&b, &by, el) {
|
||||
Some(k) => {
|
||||
read += 1;
|
||||
("read_T8aD", Some(k))
|
||||
}
|
||||
None => match ui_layout::implied_layer_key(&el.name) {
|
||||
Some(k) => {
|
||||
implied += 1;
|
||||
("implied_MEASURED", Some(k))
|
||||
}
|
||||
None => {
|
||||
none += 1;
|
||||
("none", None)
|
||||
}
|
||||
},
|
||||
};
|
||||
println!(
|
||||
" {name} {i} {} {src} {}",
|
||||
el.name,
|
||||
key.map(|k| format!("0x{k:08X}")).unwrap_or("-".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n# forced instances by key source:");
|
||||
println!("# read from the T8aD header (decoded): {read}");
|
||||
println!("# implied — this crate's MEASURED name table: {implied}");
|
||||
println!("# none — only forced_backdrop can speak: {none}");
|
||||
}
|
||||
117
crates/sylpheed-formats/examples/forced_backdrop_necessity.rs
Normal file
117
crates/sylpheed-formats/examples/forced_backdrop_necessity.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
//! Which screens does `forced_backdrop` DECIDE, and which does it merely agree with?
|
||||
//!
|
||||
//! Every check this corpus has run on the rule measured its **stability** — that
|
||||
//! no verdict moved when something else changed. That is a different property
|
||||
//! from **necessity**: an element whose position is already fixed by a read or an
|
||||
//! implied key is confirmed by the rule, not decided by it.
|
||||
//!
|
||||
//! So: compute `derived_paint_order` with the rule, and again with the
|
||||
//! `forced_backdrop` fallback removed, and report every entry whose order moves.
|
||||
//! Where nothing moves, the rule is decorative on that screen; where it moves,
|
||||
//! the rule is the only thing holding the order up.
|
||||
//!
|
||||
//! Raised by the port agent 2026-08-30. Reach note in
|
||||
//! `docs/re/structures/ui-forced-backdrop.md`.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_necessity -- [pak...]
|
||||
//!
|
||||
//! 🔴 With no argument this used to default to `GP_TITLE` alone, so a bare run
|
||||
//! reported **6 instances, not 80** — a thirteenth of the census, printed in the
|
||||
//! same format and reading like the whole thing. The port agent hit it and nearly
|
||||
//! filed the discrepancy back at me. It now walks every `dat/*.pak` by default and
|
||||
//! says on stderr how many archives it opened, because "I ran your instrument" has
|
||||
//! to mean the same thing to both of us.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
|
||||
idx.sort_by_key(|&i| {
|
||||
let el = &build.elements[i];
|
||||
(
|
||||
ui_layout::sprite_layer_key(build, bundle, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name))
|
||||
.unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
});
|
||||
idx
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let (mut total_decides, mut total_agrees) = (0usize, 0usize);
|
||||
let mut paks: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
|
||||
if paks.is_empty() {
|
||||
let mut all: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
all.sort();
|
||||
paks = all;
|
||||
}
|
||||
eprintln!("# scanning {} archive(s)", paks.len());
|
||||
for path in &paks {
|
||||
let ar = PakArchive::open(path).expect("pak");
|
||||
println!("# {}", path.display());
|
||||
println!("# entry forced decides elements note");
|
||||
|
||||
let mut decides = Vec::new();
|
||||
let mut agrees = Vec::new();
|
||||
#[allow(unused)]
|
||||
let _ = (&decides, &agrees);
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let with = ui_layout::derived_paint_order(&b, &by);
|
||||
let without = order_without_rule(&b, &by);
|
||||
|
||||
// Which elements does the rule fire on, and of those, which have no key
|
||||
// of their own to fall back on?
|
||||
let mut forced = Vec::new();
|
||||
let mut keyless = Vec::new();
|
||||
for el in &b.elements {
|
||||
if !ui_layout::forced_backdrop(&b, el) {
|
||||
continue;
|
||||
}
|
||||
forced.push(el.name.clone());
|
||||
let own = ui_layout::sprite_layer_key(&b, &by, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name));
|
||||
if own.is_none() {
|
||||
keyless.push(el.name.clone());
|
||||
}
|
||||
}
|
||||
if forced.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let moved = with != without;
|
||||
if moved {
|
||||
decides.push(i);
|
||||
} else {
|
||||
agrees.push(i);
|
||||
}
|
||||
println!(
|
||||
" {i:>5} {:>6} {:>7} {:>8} forced=[{}] keyless=[{}]",
|
||||
forced.len(),
|
||||
if moved { "YES" } else { "no" },
|
||||
b.elements.len(),
|
||||
forced.join(","),
|
||||
keyless.join(","),
|
||||
);
|
||||
}
|
||||
println!("# rule DECIDES the order on entries {decides:?}");
|
||||
println!("# rule merely AGREES on entries {agrees:?}\n");
|
||||
total_decides += decides.len();
|
||||
total_agrees += agrees.len();
|
||||
}
|
||||
println!(
|
||||
"# TOTAL over {} archive(s): {total_decides} deciding entries, \
|
||||
{total_agrees} agreeing",
|
||||
paks.len()
|
||||
);
|
||||
}
|
||||
125
crates/sylpheed-formats/examples/forced_backdrop_pixel_cost.rs
Normal file
125
crates/sylpheed-formats/examples/forced_backdrop_pixel_cost.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//! What does `forced_backdrop` cost IN PIXELS on the screens it decides?
|
||||
//!
|
||||
//! `forced_backdrop_necessity.rs` answers "does the derived ORDER move", which is
|
||||
//! a property of the sort. The port agent then pointed out — correctly — that its
|
||||
//! re-run of that probe was **my code executed twice**, not a second witness, so
|
||||
//! the disc-wide 62 has one measurement behind it and only `GP_TITLE` has two.
|
||||
//!
|
||||
//! This does not fix that (it is still this crate), but it moves the question to a
|
||||
//! **different layer**: render each deciding build twice, once in the order
|
||||
//! `compose` derives and once with the `forced_backdrop` fallback removed, and
|
||||
//! count the pixels that differ. "The order moved" and "the picture moved" are not
|
||||
//! the same claim, and the second is the one anybody cares about — the tie-break
|
||||
//! work already found overlapping reorders that cost exactly zero pixels.
|
||||
//!
|
||||
//! Each entry carries its own CONTROL: the pixel count of the composite itself.
|
||||
//! If a build renders empty, its zero means the instrument saw nothing, not that
|
||||
//! the rule is free.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example forced_backdrop_pixel_cost -- [pak...]
|
||||
//!
|
||||
//! With no argument it walks **every `dat/*.pak`** — the necessity probe defaulted
|
||||
//! to `GP_TITLE`, which made a bare run report a thirteenth of the census and read
|
||||
//! like the whole thing.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use ui_layout::ComposeOptions;
|
||||
|
||||
fn order_without_rule(build: &ui_layout::UiBuild, bundle: &[u8]) -> Vec<usize> {
|
||||
let mut idx: Vec<usize> = (0..build.elements.len()).collect();
|
||||
idx.sort_by_key(|&i| {
|
||||
let el = &build.elements[i];
|
||||
(
|
||||
ui_layout::sprite_layer_key(build, bundle, el)
|
||||
.or_else(|| ui_layout::implied_layer_key(&el.name))
|
||||
.unwrap_or(u32::MAX),
|
||||
i,
|
||||
)
|
||||
});
|
||||
idx
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::env::args().skip(1).map(PathBuf::from).collect();
|
||||
if paks.is_empty() {
|
||||
let mut all: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
all.sort();
|
||||
paks = all;
|
||||
}
|
||||
eprintln!("# scanning {} archive(s)", paks.len());
|
||||
|
||||
let opts = ComposeOptions {
|
||||
include_primitives: true,
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
println!("# archive entry element changed_px total_px ink_px(control) pct");
|
||||
let (mut decided, mut zero_cost, mut blind) = (0usize, 0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let with = ui_layout::derived_paint_order(&b, &by);
|
||||
let without = order_without_rule(&b, &by);
|
||||
if with == without {
|
||||
continue;
|
||||
}
|
||||
let forced: Vec<&str> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| ui_layout::forced_backdrop(&b, el))
|
||||
.map(|el| el.name.as_str())
|
||||
.collect();
|
||||
|
||||
let a = ui_layout::compose_with_order(&b, &by, opts, None, Some(&with));
|
||||
let c = ui_layout::compose_with_order(&b, &by, opts, None, Some(&without));
|
||||
let n = a
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(c.rgba.as_chunks::<4>().0.iter())
|
||||
.filter(|(x, y)| x != y)
|
||||
.count();
|
||||
// Control: does this build put any ink down at all, against the bare
|
||||
// backdrop? A build that renders to nothing cannot show a reorder.
|
||||
let ink = a
|
||||
.rgba
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|p| p[..3] != [0, 0, 0])
|
||||
.count();
|
||||
let total = a.rgba.len() / 4;
|
||||
|
||||
decided += 1;
|
||||
if ink == 0 {
|
||||
blind += 1;
|
||||
} else if n == 0 {
|
||||
zero_cost += 1;
|
||||
}
|
||||
println!(
|
||||
" {name} {i} {} {n} {total} {ink} {:.2}%",
|
||||
forced.join(","),
|
||||
100.0 * n as f64 / total as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("\n# builds whose ORDER the rule decides: {decided}");
|
||||
println!("# of those, costing ZERO pixels: {zero_cost}");
|
||||
println!("# of those, BLIND (build renders no ink, control fails): {blind}");
|
||||
}
|
||||
67
crates/sylpheed-formats/examples/four_button_row_rivals.rs
Normal file
67
crates/sylpheed-formats/examples/four_button_row_rivals.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! Is `GP_DIALOG` 2/3 the ONLY build on the disc with four buttons at 259/329/399/469?
|
||||
//!
|
||||
//! Both agents recorded the same reach on the DIFFICULTY identification: entries
|
||||
//! 2/3 are picked out by button count and geometry, not by a binding from
|
||||
//! `DLG_SELECT_DIFFICULTY` to a pak entry, so "another four-button dialog with the
|
||||
//! same rows would be indistinguishable". This tests whether such a rival exists.
|
||||
//!
|
||||
//! CONTROL: the scan must find GP_DIALOG 2 and 3 themselves. A rival-search that
|
||||
//! cannot find the incumbent proves nothing.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example four_button_row_rivals
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const WANT: [i32; 4] = [259, 329, 399, 469];
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut incumbent, mut rivals) = (0, 0);
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let pname = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut ys: Vec<i32> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|el| el.name.contains("btn") && !el.name.contains('f'))
|
||||
.map(|el| el.rest().map(|k| k.y).unwrap_or(el.pivot_y as i32))
|
||||
.collect();
|
||||
ys.sort();
|
||||
ys.dedup();
|
||||
if ys.len() != 4 {
|
||||
continue;
|
||||
}
|
||||
let close = ys.iter().zip(WANT.iter()).all(|(a, b)| (a - b).abs() <= 6);
|
||||
if close {
|
||||
let is_inc = pname == "GP_DIALOG.pak" && (i == 2 || i == 3);
|
||||
if is_inc {
|
||||
incumbent += 1
|
||||
} else {
|
||||
rivals += 1
|
||||
}
|
||||
println!(
|
||||
" {}{pname:24} entry {i:4} rows {ys:?}",
|
||||
if is_inc { "INCUMBENT " } else { "RIVAL " }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\ncontrol: found {incumbent} incumbent build(s) (want 2) — {}",
|
||||
if incumbent == 2 { "PASSED" } else { "FAILED" }
|
||||
);
|
||||
println!("{rivals} rival build(s) elsewhere on the disc");
|
||||
}
|
||||
83
crates/sylpheed-formats/examples/frame_alpha_census.rs
Normal file
83
crates/sylpheed-formats/examples/frame_alpha_census.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! The main menu's sprites, by their ALPHA channel — is `ptframe1`/`ptframe2`'s
|
||||
//! "no fully-opaque pixel" a property of the artwork, and does any alpha value
|
||||
//! look like a scale the game expands (e.g. 0..128) rather than 0..255?
|
||||
//!
|
||||
//! The port measures both frames as rendering too DARK against the capture, with
|
||||
//! the shortfall correlating with the BACKGROUND. Two different causes predict
|
||||
//! that: a background-scaling blend selected in code, or an alpha that is too
|
||||
//! LOW in our decode. This example tests the second, which is on the disc.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example frame_alpha_census
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, t8ad, ui_layout};
|
||||
|
||||
fn census(name: &str, img: &t8ad::T8adImage) {
|
||||
let n = (img.width * img.height) as usize;
|
||||
let mut hist = [0usize; 256];
|
||||
for p in 0..n {
|
||||
hist[img.rgba[p * 4 + 3] as usize] += 1;
|
||||
}
|
||||
let zero = hist[0];
|
||||
let full = hist[255];
|
||||
let max = (0..256).rev().find(|&a| hist[a] > 0).unwrap_or(0);
|
||||
let nonzero = n - zero;
|
||||
// the top five alpha values that actually occur, by population
|
||||
let mut top: Vec<(usize, usize)> = (1..256)
|
||||
.map(|a| (hist[a], a))
|
||||
.filter(|&(c, _)| c > 0)
|
||||
.collect();
|
||||
top.sort_unstable_by_key(|a| std::cmp::Reverse(a.0));
|
||||
let top5: Vec<String> = top
|
||||
.iter()
|
||||
.take(5)
|
||||
.map(|&(c, a)| format!("{a}x{c}"))
|
||||
.collect();
|
||||
println!(
|
||||
"{name:<16} {}x{:<4} px={n:<8} a=0:{:5.1}% a=255:{:5.1}% max={max:<3} \
|
||||
partial(1..254)/nonzero={:5.1}% top:[{}]",
|
||||
img.width,
|
||||
img.height,
|
||||
100.0 * zero as f64 / n as f64,
|
||||
100.0 * full as f64 / n as f64,
|
||||
if nonzero > 0 {
|
||||
100.0 * (nonzero - full) as f64 / nonzero as f64
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
top5.join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = argv
|
||||
.iter()
|
||||
.find(|a| a.parse::<usize>().is_err())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "GP_TITLE".to_string());
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
// Builds default to the two the port ships and can be overridden, so the
|
||||
// same census serves the title (4) and the `PRESS (A)` plate (2).
|
||||
let args: Vec<usize> = argv.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
let builds: Vec<usize> = if args.is_empty() { vec![5, 6] } else { args };
|
||||
for build in builds {
|
||||
let Ok(by) = ar.read(&ar.entries()[build]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("=== {pak} build {build} ===");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
match t8ad::parse(s) {
|
||||
Some(img) => census(n, &img),
|
||||
None => println!("{n:<16} (not a T8aD / failed to parse)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
crates/sylpheed-formats/examples/frame_keyframe_unknowns.rs
Normal file
79
crates/sylpheed-formats/examples/frame_keyframe_unknowns.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! The keyframe record's two unexplained words (`+4`, `+8`) and the fade/tint —
|
||||
//! do any of them separate the four elements the port measures as rendering too
|
||||
//! dark (`ptframe1`/`2`, `ptframe3`/`4`) from the ones it measures as accurate?
|
||||
//!
|
||||
//! The T8aD header does not: no word and no bit of `+0x04`/`+0x08` puts the four
|
||||
//! frames on one side and `pteff10` (max alpha 130, wholly semi-transparent, and
|
||||
//! rendered nearly exact) on the other. The keyframe is the other place a
|
||||
//! per-element draw mode could live.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example frame_keyframe_unknowns
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
println!(
|
||||
"{:<22} {:>5} {:>3} {:>10} {:>10} {:>8} {:>8} {:>8}",
|
||||
"element", "build", "kf", "unknown_4", "unknown_8", "fade", "tint", "rot"
|
||||
);
|
||||
let mut frame_sets: Vec<(String, i32, i32, u32, u32, i32)> = Vec::new();
|
||||
let mut other_sets: Vec<(String, i32, i32, u32, u32, i32)> = Vec::new();
|
||||
for build in [5usize, 6] {
|
||||
let by = ar.read(&ar.entries()[build]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
for e in &b.elements {
|
||||
for (i, k) in e.keyframes.iter().enumerate() {
|
||||
println!(
|
||||
"{:<22} {build:>5} {i:>3} {:>10} {:>10} {:08X} {:08X} {:>8}",
|
||||
e.name, k.unknown_4, k.unknown_8, k.fade, k.tint, k.rotation_deg
|
||||
);
|
||||
let row = (
|
||||
e.name.clone(),
|
||||
k.unknown_4,
|
||||
k.unknown_8,
|
||||
k.fade,
|
||||
k.tint,
|
||||
k.rotation_deg,
|
||||
);
|
||||
if e.name.contains("frame") {
|
||||
frame_sets.push(row)
|
||||
} else {
|
||||
other_sets.push(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\nframe keyframes: {} other keyframes: {}",
|
||||
frame_sets.len(),
|
||||
other_sets.len()
|
||||
);
|
||||
for (label, get) in [
|
||||
("unknown_4", 0usize),
|
||||
("unknown_8", 1),
|
||||
("fade", 2),
|
||||
("tint", 3),
|
||||
("rotation", 4),
|
||||
] {
|
||||
let val = |r: &(String, i32, i32, u32, u32, i32)| -> i64 {
|
||||
match get {
|
||||
0 => r.1 as i64,
|
||||
1 => r.2 as i64,
|
||||
2 => r.3 as i64,
|
||||
3 => r.4 as i64,
|
||||
_ => r.5 as i64,
|
||||
}
|
||||
};
|
||||
let fv: std::collections::BTreeSet<i64> = frame_sets.iter().map(val).collect();
|
||||
let ov: std::collections::BTreeSet<i64> = other_sets.iter().map(val).collect();
|
||||
let only_frames: Vec<&i64> = fv.iter().filter(|v| !ov.contains(v)).collect();
|
||||
println!(
|
||||
"{label:<10} frames take {:?} others take {} distinct values; frame-only values: {:?}",
|
||||
fv,
|
||||
ov.len(),
|
||||
only_frames
|
||||
);
|
||||
}
|
||||
}
|
||||
96
crates/sylpheed-formats/examples/frame_vs_accurate_words.rs
Normal file
96
crates/sylpheed-formats/examples/frame_vs_accurate_words.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
//! Which T8aD header word, if any, separates the FOUR elements the port measures
|
||||
//! as rendering too dark (`ptframe1`/`2` on the main menu, `ptframe3`/`4` on
|
||||
//! `EXTRAS`) from the elements on the same two screens it measures as accurate?
|
||||
//!
|
||||
//! The control that matters: `pteff10` has max alpha 130 and no fully-opaque
|
||||
//! pixel — the same "wholly semi-transparent" property the port proposed as the
|
||||
//! reason the frames are special — and it renders nearly exact. So the separator
|
||||
//! must put `pteff10` on the ACCURATE side.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example frame_vs_accurate_words
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let mut rows: Vec<(String, usize, Vec<u32>)> = Vec::new();
|
||||
for build in [5usize, 6] {
|
||||
let by = ar.read(&ar.entries()[build]).expect("entry");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 48 || &s[0..4] != b"T8aD" {
|
||||
continue;
|
||||
}
|
||||
let ws: Vec<u32> = (0..12)
|
||||
.map(|k| u32::from_be_bytes([s[k * 4], s[k * 4 + 1], s[k * 4 + 2], s[k * 4 + 3]]))
|
||||
.collect();
|
||||
rows.push((n.clone(), build, ws));
|
||||
}
|
||||
}
|
||||
let is_frame = |n: &str| n.starts_with("ptframe");
|
||||
println!(
|
||||
"{:<16} {:>5} +0x04 +0x08 +0x1C +0x2C",
|
||||
"sprite", "build"
|
||||
);
|
||||
for (n, b, w) in &rows {
|
||||
println!(
|
||||
"{n:<16} {b:>5} {:08X} {:08X} {:08X} {:08X}{}",
|
||||
w[1],
|
||||
w[2],
|
||||
w[7],
|
||||
w[11],
|
||||
if is_frame(n) { " <- TOO DARK" } else { "" }
|
||||
);
|
||||
}
|
||||
println!("\nwords where every ptframe* agrees and NO other sprite takes that value:");
|
||||
let frames: Vec<&(String, usize, Vec<u32>)> =
|
||||
rows.iter().filter(|(n, _, _)| is_frame(n)).collect();
|
||||
let mut any = false;
|
||||
for k in 0..12 {
|
||||
let v = frames[0].2[k];
|
||||
if !frames.iter().all(|r| r.2[k] == v) {
|
||||
continue;
|
||||
}
|
||||
if rows.iter().any(|(n, _, w)| !is_frame(n) && w[k] == v) {
|
||||
continue;
|
||||
}
|
||||
println!(" word {k} (+0x{:02X}) = {v:08X}", k * 4);
|
||||
any = true;
|
||||
}
|
||||
if !any {
|
||||
println!(" NONE — no header word separates the four frames from the rest");
|
||||
}
|
||||
println!(
|
||||
"\nper-bit check on +0x04 and +0x08 (a bit that is 1 on all frames, 0 on all others):"
|
||||
);
|
||||
let mut anyb = false;
|
||||
for &k in &[1usize, 2] {
|
||||
for bit in 0..32 {
|
||||
let on = |v: u32| (v >> bit) & 1 == 1;
|
||||
if frames.iter().all(|r| on(r.2[k]))
|
||||
&& rows.iter().all(|(n, _, w)| is_frame(n) || !on(w[k]))
|
||||
{
|
||||
println!(" +0x{:02X} bit {bit} (0x{:X})", k * 4, 1u32 << bit);
|
||||
anyb = true;
|
||||
}
|
||||
if frames.iter().all(|r| !on(r.2[k]))
|
||||
&& rows.iter().all(|(n, _, w)| is_frame(n) || on(w[k]))
|
||||
{
|
||||
println!(
|
||||
" +0x{:02X} bit {bit} (0x{:X}) INVERTED",
|
||||
k * 4,
|
||||
1u32 << bit
|
||||
);
|
||||
anyb = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !anyb {
|
||||
println!(" NONE");
|
||||
}
|
||||
}
|
||||
32
crates/sylpheed-formats/examples/gp_title_buttons.rs
Normal file
32
crates/sylpheed-formats/examples/gp_title_buttons.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Every button record in `GP_TITLE.pak`, per entry.
|
||||
//!
|
||||
//! Testing half of the count-match in boot-config-and-gamepart-registry.md:
|
||||
//! "four menu items load an external archive, EXTRAS stays inside GP_TITLE".
|
||||
//! If DIFFICULTY (NEW GAME's destination, EASY/NORMAL/HARD/BACK) is also inside
|
||||
//! GP_TITLE, then NEW GAME loads nothing external and that reading is wrong.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example gp_title_buttons
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut btns: Vec<String> = b
|
||||
.records
|
||||
.keys()
|
||||
.filter(|n| n.starts_with("ptbtn"))
|
||||
.cloned()
|
||||
.collect();
|
||||
btns.sort();
|
||||
if btns.is_empty() {
|
||||
continue;
|
||||
}
|
||||
println!("entry {i:2} {:2} button records {:?}", btns.len(), btns);
|
||||
}
|
||||
}
|
||||
24
crates/sylpheed-formats/examples/gp_title_entry_names.rs
Normal file
24
crates/sylpheed-formats/examples/gp_title_entry_names.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").unwrap());
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).unwrap();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else {
|
||||
println!("{i:2} <unreadable>");
|
||||
continue;
|
||||
};
|
||||
let names: Vec<String> = ui_layout::parse_build(&by)
|
||||
.map(|b| b.sprites.keys().take(2).cloned().collect())
|
||||
.unwrap_or_default();
|
||||
let rec: Vec<String> = ui_layout::parse_build(&by)
|
||||
.map(|b| b.records.keys().take(2).cloned().collect())
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
"{i:2} {} B sprites {:?} records {:?}",
|
||||
by.len(),
|
||||
names,
|
||||
rec
|
||||
);
|
||||
}
|
||||
}
|
||||
57
crates/sylpheed-formats/examples/gp_title_pair_check.rs
Normal file
57
crates/sylpheed-formats/examples/gp_title_pair_check.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
//! Is `GP_TITLE.pak` really "8 screens shipped twice, EN/JP"?
|
||||
//!
|
||||
//! The Q2 headline says each screen appears twice. The entry dump raised a
|
||||
//! doubt: entry 11 shows `palogo_gamearts` and entry 14 shows `palogo_seta`,
|
||||
//! which are different studios, not a language pair. If the two halves of a
|
||||
//! "pair" declare different sprites, "shipped twice" is the wrong description of
|
||||
//! at least that pair.
|
||||
//!
|
||||
//! CONTROL: a pair known to be a real EN/JP pair must come out as matching. 2/3
|
||||
//! (the PRESS Ⓐ plate) is byte-identical in size and is the control.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example gp_title_pair_check
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn sprites(ar: &PakArchive, i: usize) -> BTreeSet<String> {
|
||||
let Ok(by) = ar.read(&ar.entries()[i]) else {
|
||||
return BTreeSet::new();
|
||||
};
|
||||
ui_layout::parse_build(&by)
|
||||
.map(|b| b.sprites.keys().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let pairs = [
|
||||
(0, 1, "loading plain"),
|
||||
(2, 3, "PRESS (A) plate [CONTROL]"),
|
||||
(4, 7, "title art"),
|
||||
(5, 8, "main menu"),
|
||||
(6, 9, "EXTRAS"),
|
||||
(10, 13, "publisher splash"),
|
||||
(11, 14, "developer splash"),
|
||||
(12, 15, "loading dressed"),
|
||||
];
|
||||
for (a, b, what) in pairs {
|
||||
let (sa, sb) = (sprites(&ar, a), sprites(&ar, b));
|
||||
let only_a: Vec<_> = sa.difference(&sb).cloned().collect();
|
||||
let only_b: Vec<_> = sb.difference(&sa).cloned().collect();
|
||||
let shared = sa.intersection(&sb).count();
|
||||
let verdict = if only_a.is_empty() && only_b.is_empty() {
|
||||
"IDENTICAL SET"
|
||||
} else {
|
||||
"DIFFERS"
|
||||
};
|
||||
println!("\n{a:2}/{b:<2} {what:26} {shared:3} shared {verdict}");
|
||||
if !only_a.is_empty() {
|
||||
println!(" only in {a}: {only_a:?}");
|
||||
}
|
||||
if !only_b.is_empty() {
|
||||
println!(" only in {b}: {only_b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ fn score(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> (usize, f32, usize)
|
||||
let mut degen = 0usize;
|
||||
let mut agree = 0usize;
|
||||
let mut counted = 0usize;
|
||||
for t in idx.chunks_exact(3) {
|
||||
for t in idx.as_chunks::<3>().0 {
|
||||
let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize);
|
||||
if a == b || b == c || a == c {
|
||||
degen += 1;
|
||||
|
||||
111
crates/sylpheed-formats/examples/inrange_fallback_count.rs
Normal file
111
crates/sylpheed-formats/examples/inrange_fallback_count.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! Do `ui_layout`'s two IN-RANGE fallbacks ever fire? Counted, disc-wide.
|
||||
//!
|
||||
//! An in-range fallback supplies a value that is legitimate, so no output can
|
||||
//! distinguish it from the real thing and inspection cannot settle it. The only
|
||||
//! question that has an answer is *how often does it fire*.
|
||||
//!
|
||||
//! ui_layout.rs:1681 kf.time.unwrap_or(0) -- 0 is a real keyframe time
|
||||
//! (pose 0's time IS 0), so a fabricated one is invisible.
|
||||
//! ui_layout.rs:1010 pose_at(t).map(|k| k.fade >> 24).unwrap_or(0) > 0
|
||||
//! -- alpha 0 is legitimate, and it makes "no pose here"
|
||||
//! read as "fully transparent", biasing an occlusion test
|
||||
//! toward NOT occluded.
|
||||
//!
|
||||
//! (ui_layout.rs:973's `unwrap_or(0)` is NOT counted: it is guarded two lines
|
||||
//! later by `if tmax == 0 { return false; }`, so 0 is handled, not assumed.)
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example inrange_fallback_count
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut kf, mut untimed, mut builds) = (0u64, 0u64, 0u64);
|
||||
let (mut queries, mut none_at) = (0u64, 0u64);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ui_layout::is_build(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
builds += 1;
|
||||
// (a) :1681 -- how many poses carry no time?
|
||||
for el in &b.elements {
|
||||
for k in &el.keyframes {
|
||||
kf += 1;
|
||||
if k.time.is_none() {
|
||||
untimed += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
// (b) :1010 -- ask every element for a pose at every time that any
|
||||
// element declares, which is the set the occlusion test draws from.
|
||||
let mut times: Vec<u32> = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.collect();
|
||||
times.sort_unstable();
|
||||
times.dedup();
|
||||
for el in &b.elements {
|
||||
for &t in × {
|
||||
queries += 1;
|
||||
if el.pose_at(t).is_none() {
|
||||
none_at += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
print!(".");
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
println!();
|
||||
println!("{builds} builds, {kf} keyframes");
|
||||
println!(":1681 untimed poses (the fallback would fabricate t=0): {untimed}");
|
||||
println!(":1010 pose_at queries {queries}, of which None (fallback reads a=0): {none_at}");
|
||||
// NEGATIVE CONTROL. Both counters above report 0, and a zero is the result
|
||||
// this corpus has learned to distrust most -- it reads clean rather than
|
||||
// suspicious. So prove the detector CAN see a hit: ask every element for a
|
||||
// pose at a time no build declares. If pose_at is total, `none_out` is 0 too
|
||||
// and the 0 above means nothing.
|
||||
let mut out_queries = 0u64;
|
||||
let mut none_out = 0u64;
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ui_layout::is_build(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
for &t in &[u32::MAX, 1_000_000u32] {
|
||||
out_queries += 1;
|
||||
if el.pose_at(t).is_none() {
|
||||
none_out += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("CONTROL pose_at at an undeclared time: {out_queries} queries, {none_out} None");
|
||||
println!(" (if this is 0 the detector is blind and the 0 above is meaningless)");
|
||||
println!("--- END ---");
|
||||
}
|
||||
@@ -4,14 +4,14 @@
|
||||
//!
|
||||
//! This is the diagnostic for the 2026-07-31 negative result (Stage_S02 capture,
|
||||
//! zero parts correlated). It separates three hypotheses:
|
||||
//! 1. LOD/variant vcount not covered by the correlator's variant list
|
||||
//! → the big draws DO map to named resources, just not to the `_m`/`_l`/`_d`
|
||||
//! set the correlator tries;
|
||||
//! 2. position validation over-rejects
|
||||
//! → the vcounts match the very parts we asked for (so the vcount key was
|
||||
//! fine and the rejection happened later);
|
||||
//! 3. a different draw path (instanced/batched/merged buffers)
|
||||
//! → the big draws match NO resource in the container at all.
|
||||
//! 1. LOD/variant vcount not covered by the correlator's variant list
|
||||
//! → the big draws DO map to named resources, just not to the `_m`/`_l`/`_d`
|
||||
//! set the correlator tries;
|
||||
//! 2. position validation over-rejects
|
||||
//! → the vcounts match the very parts we asked for (so the vcount key was
|
||||
//! fine and the rejection happened later);
|
||||
//! 3. a different draw path (instanced/batched/merged buffers)
|
||||
//! → the big draws match NO resource in the container at all.
|
||||
//!
|
||||
//! Usage:
|
||||
//! SYLPHEED_ISO=... cargo run --release --example invert_capture -- \
|
||||
@@ -171,7 +171,7 @@ fn main() {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
sizes.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
||||
sizes.sort_unstable_by_key(|a| std::cmp::Reverse(a.0));
|
||||
println!("\nlargest resources in {stage}.xpr → drawn in the capture?");
|
||||
for (v, name) in sizes.iter().take(top_n.min(sizes.len())) {
|
||||
let n = draw_count.get(v).copied().unwrap_or(0);
|
||||
|
||||
86
crates/sylpheed-formats/examples/kf_flip_test.rs
Normal file
86
crates/sylpheed-formats/examples/kf_flip_test.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Do `+4` / `+8` = 180 mean MIRROR?
|
||||
//!
|
||||
//! The disc-wide census shows `+4` and `+8` are dominated by 180 and ±90, while
|
||||
//! `+12` (the decoded screen-plane rotation) takes 157 distinct values including
|
||||
//! odd ones. That shape says flips rather than free rotation.
|
||||
//!
|
||||
//! Structural test, no renderer involved: if 180 means "mirror", then the same
|
||||
//! sprite should appear both with the field 0 and with it 180 **within one
|
||||
//! build** -- a mirrored pair. Free-rotation semantics predicts no such pairing.
|
||||
//!
|
||||
//! CONTROL: the same search run on `+12`, which is decoded as a real rotation
|
||||
//! and should NOT show a 0/180 pairing pattern of the same strength.
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("<disc>/dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dir)
|
||||
.expect("dir")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
// field -> (pak, entry, sprite) -> set of values seen
|
||||
let mut seen: [BTreeMap<(String, usize, String), BTreeSet<i32>>; 3] =
|
||||
[BTreeMap::new(), BTreeMap::new(), BTreeMap::new()];
|
||||
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let pn = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().to_vec().iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
let mut groups: Vec<(String, Vec<ui_layout::Keyframe>)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.map(|el| (el.name.clone(), el.keyframes.clone()))
|
||||
.collect();
|
||||
for el in &b.elements {
|
||||
if let Some(&(off, size)) = b.records.get(&el.name) {
|
||||
if let Some(lb) = ui_layout::parse_build(&bytes[off..off + size]) {
|
||||
for le in &lb.elements {
|
||||
groups.push((le.name.clone(), le.keyframes.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (nm, ks) in groups {
|
||||
for k in &ks {
|
||||
let key = (pn.clone(), i, nm.clone());
|
||||
seen[0].entry(key.clone()).or_default().insert(k.unknown_4);
|
||||
seen[1].entry(key.clone()).or_default().insert(k.unknown_8);
|
||||
seen[2].entry(key).or_default().insert(k.rotation_deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (idx, label) in [(0, "+4"), (1, "+8"), (2, "+12 (rotation, CONTROL)")] {
|
||||
let m = &seen[idx];
|
||||
let mut pair_0_180 = 0usize; // a sprite seen at BOTH 0 and 180 in one build
|
||||
let mut only_180 = 0usize;
|
||||
let mut multi = 0usize; // more than two distinct values
|
||||
for v in m.values() {
|
||||
if v.len() > 2 {
|
||||
multi += 1;
|
||||
}
|
||||
let has0 = v.contains(&0);
|
||||
let has180 = v.contains(&180) || v.contains(&-180);
|
||||
if has0 && has180 {
|
||||
pair_0_180 += 1;
|
||||
} else if has180 && !has0 {
|
||||
only_180 += 1;
|
||||
}
|
||||
}
|
||||
println!("{label}: {} sprite-instances", m.len());
|
||||
println!(" both 0 and ±180 in one build : {pair_0_180}");
|
||||
println!(" ±180 without any 0 : {only_180}");
|
||||
println!(" more than 2 distinct values : {multi}");
|
||||
}
|
||||
}
|
||||
37
crates/sylpheed-formats/examples/kf_timeline.rs
Normal file
37
crates/sylpheed-formats/examples/kf_timeline.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let bi: usize = a.next().unwrap().parse().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
let by = ar.read(&ar.entries()[bi]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
for t in a {
|
||||
let i: usize = t.parse().unwrap();
|
||||
let e = &b.elements[i];
|
||||
println!(
|
||||
"[{i}] {} kind=0x{:x} parent={:?} pivot=({},{}) kfs={}",
|
||||
e.name,
|
||||
e.kind,
|
||||
e.parent,
|
||||
e.pivot_x,
|
||||
e.pivot_y,
|
||||
e.keyframes.len()
|
||||
);
|
||||
for k in &e.keyframes {
|
||||
println!(
|
||||
" t={:<5} a={:<4} xy=({},{}) s={}/{} rot={} u4={} u8={} tint={:08x}",
|
||||
k.time.map(|v| v as i64).unwrap_or(-1),
|
||||
k.fade >> 24,
|
||||
k.x,
|
||||
k.y,
|
||||
k.scale_x,
|
||||
k.scale_y,
|
||||
k.rotation_deg,
|
||||
k.unknown_4,
|
||||
k.unknown_8,
|
||||
k.tint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
103
crates/sylpheed-formats/examples/kf_unknown_census.rs
Normal file
103
crates/sylpheed-formats/examples/kf_unknown_census.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! What are the keyframe block's `+4` and `+8`?
|
||||
//!
|
||||
//! `+12` is decoded as a screen-plane rotation in degrees. `+4` and `+8` sit
|
||||
//! immediately before it and are carried but unexplained; one standing 🟡
|
||||
//! reading is that the three together are rotations about three axes, "not tied
|
||||
//! to an observed rotation". This censuses them across every UI pak on the disc
|
||||
//! so the reading can be argued with rather than assumed.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let dir = std::env::args().nth(1).expect("usage: <disc>/dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dir)
|
||||
.expect("dir")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let mut h4: BTreeMap<i32, usize> = BTreeMap::new();
|
||||
let mut h8: BTreeMap<i32, usize> = BTreeMap::new();
|
||||
let mut h12: BTreeMap<i32, usize> = BTreeMap::new();
|
||||
let mut both_nz: Vec<String> = Vec::new();
|
||||
let (mut kfs, mut builds) = (0usize, 0usize);
|
||||
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().to_vec().iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
builds += 1;
|
||||
// parents and leaves alike
|
||||
let mut groups: Vec<(String, Vec<ui_layout::Keyframe>)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.map(|el| (el.name.clone(), el.keyframes.clone()))
|
||||
.collect();
|
||||
for el in &b.elements {
|
||||
if let Some(&(off, size)) = b.records.get(&el.name) {
|
||||
if let Some(lb) = ui_layout::parse_build(&bytes[off..off + size]) {
|
||||
for le in &lb.elements {
|
||||
groups
|
||||
.push((format!("{}->{}", el.name, le.name), le.keyframes.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (nm, ks) in groups {
|
||||
for k in &ks {
|
||||
kfs += 1;
|
||||
*h4.entry(k.unknown_4).or_default() += 1;
|
||||
*h8.entry(k.unknown_8).or_default() += 1;
|
||||
*h12.entry(k.rotation_deg).or_default() += 1;
|
||||
if k.unknown_4 != 0 || k.unknown_8 != 0 {
|
||||
both_nz.push(format!(
|
||||
"{name} e{i} {nm} +4={} +8={} +12={}",
|
||||
k.unknown_4, k.unknown_8, k.rotation_deg
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{builds} builds, {kfs} keyframes (parents + leaves)\n");
|
||||
for (nm, h) in [("+4", &h4), ("+8", &h8), ("+12 (rotation)", &h12)] {
|
||||
let nz: usize = h.iter().filter(|(k, _)| **k != 0).map(|(_, v)| *v).sum();
|
||||
println!(
|
||||
"{nm}: {} distinct values, {} non-zero keyframes ({:.4}%)",
|
||||
h.len(),
|
||||
nz,
|
||||
100.0 * nz as f64 / kfs as f64
|
||||
);
|
||||
let mut top: Vec<_> = h.iter().filter(|(k, _)| **k != 0).collect();
|
||||
top.sort_by_key(|(_, v)| std::cmp::Reverse(**v));
|
||||
for (k, v) in top.iter().take(6) {
|
||||
println!(" {k:>8} x{v}");
|
||||
}
|
||||
}
|
||||
println!("\nkeyframes with a non-zero +4 or +8: {}", both_nz.len());
|
||||
// Per-pak, so a reader can ask whether a pak they have a CAPTURE of is
|
||||
// among them -- which decides whether the field is testable at all.
|
||||
let mut per: BTreeMap<String, usize> = BTreeMap::new();
|
||||
for l in &both_nz {
|
||||
let pak = l.split_whitespace().next().unwrap_or("?").to_string();
|
||||
*per.entry(pak).or_default() += 1;
|
||||
}
|
||||
println!(" by pak:");
|
||||
for (k, v) in &per {
|
||||
println!(" {k:<28} {v}");
|
||||
}
|
||||
println!(" paks with NONE: (any UI pak not listed above)");
|
||||
if let Ok(f) = std::env::var("KF_SHOW") {
|
||||
println!("\n all lines for {f}:");
|
||||
for l in both_nz.iter().filter(|l| l.starts_with(&f)) {
|
||||
println!(" {l}");
|
||||
}
|
||||
}
|
||||
}
|
||||
61
crates/sylpheed-formats/examples/kind3002_names.rs
Normal file
61
crates/sylpheed-formats/examples/kind3002_names.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! What do 0x3002/0x3003 elements actually look like disc-wide?
|
||||
//! The port's menu-item rule keys on this class; this asks whether the class is
|
||||
//! uniformly menu-row-shaped or whether it contains other things too.
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut names: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for ent in ar.entries() {
|
||||
let Ok(by) = ar.read(ent) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if el.kind == 0x3002 || el.kind == 0x3003 {
|
||||
let stem = el.name.split('.').next().unwrap_or("");
|
||||
let cls: String = stem
|
||||
.trim_end_matches(|c: char| c.is_ascii_digit())
|
||||
.to_string();
|
||||
*names.entry(cls).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let total: usize = names.values().sum();
|
||||
println!(
|
||||
"0x3002/0x3003 elements disc-wide: {total}, {} name-stems",
|
||||
names.len()
|
||||
);
|
||||
let mut v: Vec<_> = names.into_iter().collect();
|
||||
v.sort_by_key(|(_, n)| std::cmp::Reverse(*n));
|
||||
let btn: usize = v
|
||||
.iter()
|
||||
.filter(|(k, _)| k.contains("btn"))
|
||||
.map(|(_, n)| n)
|
||||
.sum();
|
||||
println!(
|
||||
"stems containing \"btn\": {btn} of {total} ({:.1}%)",
|
||||
100.0 * btn as f64 / total as f64
|
||||
);
|
||||
println!("\ntop stems:");
|
||||
for (k, n) in v.iter().take(14) {
|
||||
println!(" {n:5} {k}");
|
||||
}
|
||||
println!("\nNON-btn stems (the ones a menu-item rule would also claim):");
|
||||
for (k, n) in v.iter().filter(|(k, _)| !k.contains("btn")).take(12) {
|
||||
println!(" {n:5} {k}");
|
||||
}
|
||||
}
|
||||
71
crates/sylpheed-formats/examples/kind_bit0_census.rs
Normal file
71
crates/sylpheed-formats/examples/kind_bit0_census.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! Disc-wide test of one bit: does `kind & 1` mean "this element has a parent"?
|
||||
//!
|
||||
//! The port is blocked on what `0x3003` is, having only `0x3002` in its rule.
|
||||
//! `0x3002` and `0x3003` differ in bit 0 alone, and the struct doc claims bit 0
|
||||
//! is "has a parent". That is a falsifiable claim over every element on the
|
||||
//! disc, so it is tested here rather than argued from two screens.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example kind_bit0_census
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut agree = 0usize;
|
||||
let mut disagree = 0usize;
|
||||
let mut kinds: BTreeMap<u32, usize> = BTreeMap::new();
|
||||
let mut kind_parent: BTreeMap<(u32, bool), usize> = BTreeMap::new();
|
||||
let mut examples: Vec<String> = Vec::new();
|
||||
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (i, ent) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(ent) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
let has_parent = el.parent.is_some();
|
||||
let bit0 = el.kind & 1 == 1;
|
||||
*kinds.entry(el.kind).or_default() += 1;
|
||||
*kind_parent.entry((el.kind, has_parent)).or_default() += 1;
|
||||
if bit0 == has_parent {
|
||||
agree += 1
|
||||
} else {
|
||||
disagree += 1;
|
||||
if examples.len() < 10 {
|
||||
examples.push(format!(
|
||||
"{} entry {i} {} kind={:#x} parent={:?}",
|
||||
p.file_name().unwrap().to_string_lossy(),
|
||||
el.name,
|
||||
el.kind,
|
||||
el.parent
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("kind&1 == has_parent : agree {agree} DISAGREE {disagree}");
|
||||
for e in &examples {
|
||||
println!(" counterexample: {e}");
|
||||
}
|
||||
println!("\nkind histogram (count, and how many of each have a parent):");
|
||||
for (k, n) in &kinds {
|
||||
let wp = kind_parent.get(&(*k, true)).copied().unwrap_or(0);
|
||||
println!(
|
||||
" {k:#06x} n={n:6} with parent {wp:6} without {:6}",
|
||||
n - wp
|
||||
);
|
||||
}
|
||||
}
|
||||
205
crates/sylpheed-formats/examples/kind_census_five_screens.rs
Normal file
205
crates/sylpheed-formats/examples/kind_census_five_screens.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
//! Refutation check on `sylpheed-port`'s kind census: *"Every sprite decoration
|
||||
//! on both screens is `0x0` — `ptframe1`…`ptframe4` included — and every button
|
||||
//! is `0x3002`."*
|
||||
//!
|
||||
//! Their exporter decodes the field independently; this reads it from the other
|
||||
//! side. The check is deliberately WIDER than their claim in two ways, because a
|
||||
//! census that only looks where the claim looks cannot fail:
|
||||
//!
|
||||
//! * it covers every element, not only `.t32` sprites, so a decoration with an
|
||||
//! unexpected kind cannot hide behind the word "sprite";
|
||||
//! * it covers every build of `GP_TITLE`, not the two screens they checked, so
|
||||
//! the claim's *reach* gets tested and not just its instances.
|
||||
//!
|
||||
//! It also cross-checks `kind` against the focus/nav index at `+0x2C`, which is
|
||||
//! `-1` on anything that cannot take the cursor. That turns a census into a
|
||||
//! decode: if the two fields agree everywhere, the bit that separates them is
|
||||
//! identified rather than guessed. Run over EVERY UI pak on the disc, not just
|
||||
//! `GP_TITLE`, so the claim is disc-wide.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example kind_census_five_screens
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn suffix(n: &str) -> &str {
|
||||
match n.rfind('.') {
|
||||
Some(i) => &n[i..],
|
||||
None => "(none)",
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let n = ar.entries().len();
|
||||
// kind -> suffix -> count, and the exceptions we care about by name
|
||||
let mut table: BTreeMap<u32, BTreeMap<String, usize>> = BTreeMap::new();
|
||||
let mut t32_nonzero: Vec<(usize, String, u32)> = Vec::new();
|
||||
let mut btn_nonstd: Vec<(usize, String, u32)> = Vec::new();
|
||||
let mut builds = 0usize;
|
||||
for e in 0..n {
|
||||
let by = match ar.read(&ar.entries()[e]) {
|
||||
Ok(b) => b,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let b = match ui_layout::parse_build(&by) {
|
||||
Some(b) => b,
|
||||
None => continue,
|
||||
};
|
||||
builds += 1;
|
||||
for el in &b.elements {
|
||||
*table
|
||||
.entry(el.kind)
|
||||
.or_default()
|
||||
.entry(suffix(&el.name).to_string())
|
||||
.or_default() += 1;
|
||||
if el.name.ends_with(".t32") && !el.name.contains("btn") && el.kind != 0 {
|
||||
t32_nonzero.push((e, el.name.clone(), el.kind));
|
||||
}
|
||||
if el.name.contains("btn") && el.kind != 0x3002 {
|
||||
btn_nonstd.push((e, el.name.clone(), el.kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("GP_TITLE: {builds} parseable builds of {n} entries\n");
|
||||
println!("{:<10} elements by file suffix", "kind");
|
||||
for (k, m) in &table {
|
||||
let s: Vec<String> = m.iter().map(|(sfx, c)| format!("{sfx}x{c}")).collect();
|
||||
println!("0x{k:<8X} {}", s.join(" "));
|
||||
}
|
||||
println!(
|
||||
"\nNON-BUTTON .t32 elements with kind != 0: {}",
|
||||
t32_nonzero.len()
|
||||
);
|
||||
for (e, nm, k) in t32_nonzero.iter().take(30) {
|
||||
println!(" entry {e:>2} {nm:<24} kind 0x{k:X}");
|
||||
}
|
||||
println!("\n*btn* elements with kind != 0x3002: {}", btn_nonstd.len());
|
||||
for (e, nm, k) in btn_nonstd.iter().take(30) {
|
||||
println!(" entry {e:>2} {nm:<24} kind 0x{k:X}");
|
||||
}
|
||||
|
||||
// Is `kind` a bitfield, and does bit 0x2 mean "focusable"? The focus/nav
|
||||
// index at +0x2C is -1 on everything that cannot take the cursor, so the two
|
||||
// fields cross-check each other. Printed rather than asserted: this is the
|
||||
// evidence for the reading, not the reading itself.
|
||||
println!("\nkind vs the focus index at +0x2C (-1 = not focusable), GP_TITLE:");
|
||||
// +0x2C is not in `Element`, so it is read straight out of the 60-byte
|
||||
// declaration entry: table at 0x20, 0x14 = count, entry stride 60.
|
||||
const AT: usize = 0x20;
|
||||
const STRIDE: usize = 60;
|
||||
let mut cross: BTreeMap<(u32, i32), usize> = BTreeMap::new();
|
||||
for e in 0..n {
|
||||
let by = match ar.read(&ar.entries()[e]) {
|
||||
Ok(b) => b,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ui_layout::parse_build(&by).is_none() {
|
||||
continue;
|
||||
}
|
||||
if by.len() < 0x18 {
|
||||
continue;
|
||||
}
|
||||
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
||||
for i in 0..count {
|
||||
let at = AT + i * STRIDE;
|
||||
if at + STRIDE > by.len() {
|
||||
break;
|
||||
}
|
||||
let kind =
|
||||
u32::from_be_bytes([by[at + 0x28], by[at + 0x29], by[at + 0x2A], by[at + 0x2B]]);
|
||||
let foc =
|
||||
i32::from_be_bytes([by[at + 0x2C], by[at + 0x2D], by[at + 0x2E], by[at + 0x2F]]);
|
||||
*cross
|
||||
.entry((kind, if foc < 0 { -1 } else { 1 }))
|
||||
.or_default() += 1;
|
||||
}
|
||||
}
|
||||
for ((k, f), c) in &cross {
|
||||
println!(
|
||||
" kind 0x{k:<6X} focus {:<10} {c:>4} elements",
|
||||
if *f < 0 { "= -1" } else { ">= 0" }
|
||||
);
|
||||
}
|
||||
|
||||
// ── the same test, every UI pak on the disc ──────────────────────────────
|
||||
let mut all: BTreeMap<(u32, i32), usize> = BTreeMap::new();
|
||||
let mut paks = 0usize;
|
||||
let mut violations: Vec<String> = Vec::new();
|
||||
let mut dir: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|d| d.ok())
|
||||
.map(|d| d.path())
|
||||
.collect();
|
||||
dir.sort();
|
||||
for path in dir {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
if !name.ends_with(".pak") {
|
||||
continue;
|
||||
}
|
||||
let ar = match PakArchive::open(&path) {
|
||||
Ok(a) => a,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let mut used = false;
|
||||
for i in 0..ar.entries().len() {
|
||||
let by = match ar.read(&ar.entries()[i]) {
|
||||
Ok(b) => b,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ui_layout::parse_build(&by).is_none() {
|
||||
continue;
|
||||
}
|
||||
if by.len() < 0x18 {
|
||||
continue;
|
||||
}
|
||||
used = true;
|
||||
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
||||
for e in 0..count {
|
||||
let at = AT + e * STRIDE;
|
||||
if at + STRIDE > by.len() {
|
||||
break;
|
||||
}
|
||||
let kind = u32::from_be_bytes([
|
||||
by[at + 0x28],
|
||||
by[at + 0x29],
|
||||
by[at + 0x2A],
|
||||
by[at + 0x2B],
|
||||
]);
|
||||
let foc = i32::from_be_bytes([
|
||||
by[at + 0x2C],
|
||||
by[at + 0x2D],
|
||||
by[at + 0x2E],
|
||||
by[at + 0x2F],
|
||||
]);
|
||||
let f = if foc < 0 { -1 } else { 1 };
|
||||
*all.entry((kind, f)).or_default() += 1;
|
||||
if ((kind & 0x2) != 0) != (f > 0) {
|
||||
violations.push(format!(
|
||||
"{name} entry {i} elem {e}: kind 0x{kind:X} focus {foc}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if used {
|
||||
paks += 1
|
||||
}
|
||||
}
|
||||
let total: usize = all.values().sum();
|
||||
println!("\nDISC-WIDE — {paks} UI paks, {total} declaration entries");
|
||||
println!("{:<12} {:>12} {:>12}", "kind", "focus = -1", "focus >= 0");
|
||||
let kinds: std::collections::BTreeSet<u32> = all.keys().map(|(k, _)| *k).collect();
|
||||
for k in kinds {
|
||||
println!(
|
||||
"0x{k:<10X} {:>12} {:>12}",
|
||||
all.get(&(k, -1)).copied().unwrap_or(0),
|
||||
all.get(&(k, 1)).copied().unwrap_or(0)
|
||||
);
|
||||
}
|
||||
println!("\nHYPOTHESIS: bit 0x2 of kind == (focus index >= 0)");
|
||||
println!("violations: {} of {total}", violations.len());
|
||||
for v in violations.iter().take(20) {
|
||||
println!(" {v}");
|
||||
}
|
||||
}
|
||||
71
crates/sylpheed-formats/examples/leaf_alpha_compose.rs
Normal file
71
crates/sylpheed-formats/examples/leaf_alpha_compose.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! How do a parent record's alpha ramp and its nested `.rat` leaf's compose?
|
||||
//!
|
||||
//! The port emits both but will not draw the leaf without knowing the rule --
|
||||
//! rightly, since drawing on a guess trades a visible 1.82 % error for an
|
||||
//! invisible wrong one. There is an oracle for this: the GPU draw capture
|
||||
//! records **vertex colours**, and on the title's `ptloop` draw they are
|
||||
//! `C3FFFFFF` and `B6FFFFFF` -- alpha **195** and **182**, not 255. So the game's
|
||||
//! composed alpha is observable, and a candidate rule either predicts those two
|
||||
//! numbers or does not.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example leaf_alpha_compose -- <GP_TITLE.pak>
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn alpha_of(kf: &ui_layout::Keyframe) -> u32 {
|
||||
// The keyframe block's +0 is an ARGB fade colour; alpha is its high byte.
|
||||
kf.fade >> 24
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("usage: <pak>");
|
||||
let ar = pak::PakArchive::open(&path).expect("open");
|
||||
let e = &ar.entries()[4]; // GP_TITLE entry 4 = the English title
|
||||
let bytes = ar.read(e).expect("read");
|
||||
let build = ui_layout::parse_build(&bytes).expect("build");
|
||||
|
||||
for name in ["ptloop01.rat", "ptloop02.rat"] {
|
||||
println!("\n=== {name} ===");
|
||||
if let Some(el) = build.elements.iter().find(|x| x.name == name) {
|
||||
println!(" PARENT keyframes (t, alpha, scale, rot, x,y):");
|
||||
for k in &el.keyframes {
|
||||
println!(
|
||||
" t={:<5} a={:<4} scale=({},{}) rot={:<5} ({},{})",
|
||||
k.time.map(|t| t as i64).unwrap_or(-1),
|
||||
alpha_of(k),
|
||||
k.scale_x,
|
||||
k.scale_y,
|
||||
k.rotation_deg,
|
||||
k.x,
|
||||
k.y
|
||||
);
|
||||
}
|
||||
}
|
||||
match build.records.get(name) {
|
||||
Some(&(off, size)) => {
|
||||
let leaf = &bytes[off..off + size];
|
||||
match ui_layout::parse_build(leaf) {
|
||||
Some(lb) => {
|
||||
for le in &lb.elements {
|
||||
println!(" LEAF element {:?}:", le.name);
|
||||
for k in &le.keyframes {
|
||||
println!(
|
||||
" t={:<5} a={:<4} scale=({},{}) rot={:<5} ({},{})",
|
||||
k.time.map(|t| t as i64).unwrap_or(-1),
|
||||
alpha_of(k),
|
||||
k.scale_x,
|
||||
k.scale_y,
|
||||
k.rotation_deg,
|
||||
k.x,
|
||||
k.y
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => println!(" leaf did not parse"),
|
||||
}
|
||||
}
|
||||
None => println!(" no leaf record"),
|
||||
}
|
||||
}
|
||||
println!("\nOBSERVED in the draw capture: quad A alpha 195 (0xC3), quad B alpha 182 (0xB6)");
|
||||
}
|
||||
68
crates/sylpheed-formats/examples/leaf_dump.rs
Normal file
68
crates/sylpheed-formats/examples/leaf_dump.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
//! Dump a record's parent keyframes and its nested `.rat` leaf's, for any entry.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example leaf_dump -- <pak> <entry> <name>
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn a(k: &ui_layout::Keyframe) -> u32 {
|
||||
k.fade >> 24
|
||||
}
|
||||
fn t(k: &ui_layout::Keyframe) -> i64 {
|
||||
k.time.map(|v| v as i64).unwrap_or(-1)
|
||||
}
|
||||
|
||||
fn show(tag: &str, els: &[ui_layout::Element]) {
|
||||
for e in els {
|
||||
println!(
|
||||
" {tag} {:?} pivot=({},{}) kind={:#x}",
|
||||
e.name, e.pivot_x, e.pivot_y, e.kind
|
||||
);
|
||||
for k in &e.keyframes {
|
||||
println!(
|
||||
" t={:<5} a={:<4} scale=({},{}) rot={:<5} pos=({},{}) tint={:#010x}",
|
||||
t(k),
|
||||
a(k),
|
||||
k.scale_x,
|
||||
k.scale_y,
|
||||
k.rotation_deg,
|
||||
k.x,
|
||||
k.y,
|
||||
k.tint
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut it = std::env::args().skip(1);
|
||||
let path = it.next().expect("pak");
|
||||
let entry: usize = it.next().expect("entry").parse().expect("entry");
|
||||
let want = it.next().expect("name");
|
||||
let ar = pak::PakArchive::open(&path).expect("open");
|
||||
let bytes = ar.read(&ar.entries()[entry]).expect("read");
|
||||
let b = ui_layout::parse_build(&bytes).expect("build");
|
||||
println!(
|
||||
"entry {entry}: {} elements, design {}x{}",
|
||||
b.elements.len(),
|
||||
b.design_w,
|
||||
b.design_h
|
||||
);
|
||||
let els: Vec<_> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| e.name.contains(&want))
|
||||
.cloned()
|
||||
.collect();
|
||||
show("PARENT", &els);
|
||||
for e in &els {
|
||||
match b.records.get(&e.name) {
|
||||
Some(&(off, size)) => {
|
||||
println!(" -- leaf of {:?}: {size} B at {off}", e.name);
|
||||
match ui_layout::parse_build(&bytes[off..off + size]) {
|
||||
Some(lb) => show(" LEAF", &lb.elements),
|
||||
None => println!(" leaf did not parse"),
|
||||
}
|
||||
}
|
||||
None => println!(" -- {:?} has NO leaf record", e.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
45
crates/sylpheed-formats/examples/leaf_keyframes.rs
Normal file
45
crates/sylpheed-formats/examples/leaf_keyframes.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! Dump the keyframes of any nested `.rat` leaf by name.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example leaf_keyframes -- GP_TITLE ptbtn00.rat 2 3 4
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = argv[0].clone();
|
||||
let rec = argv[1].clone();
|
||||
let builds: Vec<usize> = argv[2..].iter().filter_map(|a| a.parse().ok()).collect();
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
for e in builds {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let Some(&(lo, ls)) = b.records.get(rec.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
let leaf = &by[lo..(lo + ls).min(by.len())];
|
||||
println!("=== {pak} entry {e} — {rec} (leaf {ls} bytes) ===");
|
||||
if let Some(u) = ui_layout::loop_length_units(leaf) {
|
||||
println!(" declared loop: {u} units");
|
||||
}
|
||||
if let Some(lb) = ui_layout::parse_build(leaf) {
|
||||
for el in &lb.elements {
|
||||
println!(" {} — {} keyframes", el.name, el.keyframes.len());
|
||||
for (i, k) in el.keyframes.iter().enumerate() {
|
||||
println!(
|
||||
" kf{i:<2} t={:<5} x={:<6} y={:<6} fade={:08X} (alpha {:3})",
|
||||
k.time.map(|t| t.to_string()).unwrap_or_else(|| "-".into()),
|
||||
k.x,
|
||||
k.y,
|
||||
k.fade,
|
||||
k.fade >> 24
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
crates/sylpheed-formats/examples/leafdbg.rs
Normal file
33
crates/sylpheed-formats/examples/leafdbg.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let ar = pak::PakArchive::open(std::env::args().nth(1).unwrap()).unwrap();
|
||||
let by = ar.read(&ar.entries()[4]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
for n in ["ptloop01.rat", "ptloop02.rat"] {
|
||||
let el = b.elements.iter().find(|e| e.name == n).unwrap();
|
||||
println!(
|
||||
"{n}: kind={:#x} animated={} rec={:?}",
|
||||
el.kind,
|
||||
el.animated,
|
||||
b.records.get(n).map(|&(o, s)| (o, s))
|
||||
);
|
||||
if let Some(&(o, s)) = b.records.get(n) {
|
||||
match ui_layout::parse_build(&by[o..o + s]) {
|
||||
Some(lb) => {
|
||||
for le in &lb.elements {
|
||||
println!(
|
||||
" leaf {:?} sprite={:?} in_sprites={}",
|
||||
le.name,
|
||||
le.sprite,
|
||||
le.sprite
|
||||
.as_ref()
|
||||
.map(|x| b.sprites.contains_key(x))
|
||||
.unwrap_or(false)
|
||||
);
|
||||
}
|
||||
}
|
||||
None => println!(" leaf parse FAILED"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
crates/sylpheed-formats/examples/loo_band.rs
Normal file
36
crates/sylpheed-formats/examples/loo_band.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// Leave-one-out over the elements that touch a band, dumping raw RGBA each time.
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let bi: usize = a.next().unwrap().parse().unwrap();
|
||||
let dir = a.next().unwrap();
|
||||
let ar = pak::PakArchive::open(pk).unwrap();
|
||||
let by = ar.read(&ar.entries()[bi]).unwrap();
|
||||
let b = ui_layout::parse_build(&by).unwrap();
|
||||
let o = ui_layout::ComposeOptions {
|
||||
backdrop: [0, 0, 0, 255],
|
||||
..Default::default()
|
||||
};
|
||||
let n = b.elements.len();
|
||||
let base = ui_layout::compose(&b, &by, o, None);
|
||||
std::fs::write(format!("{dir}/base.raw"), &base.rgba).unwrap();
|
||||
println!("canvas {}x{} elements {n}", base.width, base.height);
|
||||
for i in 0..n {
|
||||
let e = &b.elements[i];
|
||||
let Some(kf) = e.rest() else { continue };
|
||||
// only bother with elements whose rest pose can touch the band
|
||||
let mut v = vec![true; n];
|
||||
v[i] = false;
|
||||
let c = ui_layout::compose(&b, &by, o, Some(&v));
|
||||
std::fs::write(format!("{dir}/wo-{i}.raw"), &c.rgba).unwrap();
|
||||
println!(
|
||||
"{i}\t{}\t{}\t({},{})\ta={}",
|
||||
e.name,
|
||||
e.sprite.clone().unwrap_or_default(),
|
||||
kf.x,
|
||||
kf.y,
|
||||
kf.fade >> 24
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Does the `+0x08` falsifier actually identify `+0x08`?
|
||||
//!
|
||||
//! `ui-record-loop-length.md` (mine) rests on: an animation cannot restart before
|
||||
//! its own last pose, so a wrong reading should produce violations, and none exist
|
||||
//! in 1 781 records. `sylpheed-port` re-ran it at the neighbouring offsets and
|
||||
//! reports the falsifier ACCEPTS `+0x04` too — meaning it does not discriminate,
|
||||
//! and the real evidence is the exactness statistic I called a formality.
|
||||
//!
|
||||
//! This checks that from my own reader before I correct the page.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example loop_length_offset_discriminates
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn be32(b: &[u8], o: usize) -> Option<u32> {
|
||||
(b.len() >= o + 4).then(|| u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let dat = root.join("dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dat)
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
// offset -> (records, violations where word < max_t, exact matches)
|
||||
let mut stat = [(0usize, 0usize, 0usize); 3];
|
||||
let offsets = [0x04usize, 0x08, 0x0c];
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (off, size) in b.records.values() {
|
||||
let rec = &by[*off..(*off + *size).min(by.len())];
|
||||
if rec.len() < 0x10 || &rec[0..4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
let Some(leaf) = ui_layout::parse_build(rec) else {
|
||||
continue;
|
||||
};
|
||||
let max_t = leaf
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max();
|
||||
let Some(max_t) = max_t else { continue };
|
||||
// sylpheed-port's reconciliation: max_t == 0 makes "does the word
|
||||
// equal the largest keyframe time?" vacuous, and those records were
|
||||
// silently in my denominator. Filter them and the counts must meet.
|
||||
if std::env::var("MEANINGFUL_ONLY").is_ok() && max_t == 0 {
|
||||
continue;
|
||||
}
|
||||
for (i, o) in offsets.iter().enumerate() {
|
||||
if let Some(w) = be32(rec, *o) {
|
||||
stat[i].0 += 1;
|
||||
if w < max_t {
|
||||
stat[i].1 += 1
|
||||
}
|
||||
if w == max_t {
|
||||
stat[i].2 += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:>8} {:>9} {:>12} {:>14}",
|
||||
"offset", "records", "violations", "exact == max_t"
|
||||
);
|
||||
for (i, o) in offsets.iter().enumerate() {
|
||||
let (n, v, x) = stat[i];
|
||||
println!(
|
||||
" +0x{o:02X} {n:>9} {v:>12} ({:>5.1}%) {x:>8} ({:>5.1}%)",
|
||||
100.0 * v as f64 / n.max(1) as f64,
|
||||
100.0 * x as f64 / n.max(1) as f64
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! What elements sit under the port's hot residual tiles on the main menu?
|
||||
//!
|
||||
//! `sylpheed-port` mapped the menu's edge residual at 64 px tiles and handed over
|
||||
//! coordinates without names — the element inventory is this side's. Hot tiles
|
||||
//! cluster at x 384–704, y 64–256, hottest at (512,128).
|
||||
//!
|
||||
//! ⚠️ Their tiles are in the frame they compare in; this prints DESIGN space, and
|
||||
//! the two differ by the capture offset (capture_y ≈ 64.8 + 0.992·design_y). Both
|
||||
//! readings are printed so the mapping is not assumed.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example main_menu_element_extents
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
println!(
|
||||
"{:<26} {:>6} {:>6} {:>7} {:>7} sprite",
|
||||
"element", "x", "y", "pivot_x", "pivot_y"
|
||||
);
|
||||
let mut rows: Vec<(i32, i32, String, String)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.map(|e| {
|
||||
let k = e.rest();
|
||||
(
|
||||
k.map(|k| k.x).unwrap_or(0),
|
||||
k.map(|k| k.y).unwrap_or(0),
|
||||
e.name.clone(),
|
||||
e.sprite.clone().unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
rows.sort_by_key(|r| (r.1, r.0));
|
||||
for (x, y, n, s) in &rows {
|
||||
// flag anything whose rest position lands in the hot band, read both ways
|
||||
let cap_y = 64.82 + 0.9919 * (*y as f64);
|
||||
let hot_design = (384..=704).contains(x) && (64..=256).contains(y);
|
||||
let hot_capture = (384..=704).contains(x) && (64.0..=256.0).contains(&cap_y);
|
||||
let mark = match (hot_design, hot_capture) {
|
||||
(true, true) => " <- HOT both readings",
|
||||
(true, false) => " <- hot in DESIGN space",
|
||||
(false, true) => " <- hot in CAPTURE space",
|
||||
_ => "",
|
||||
};
|
||||
println!(
|
||||
"{n:<26} {x:>6} {y:>6} {:>7} {:>7} {s}{mark}",
|
||||
b.elements
|
||||
.iter()
|
||||
.find(|e| &e.name == n)
|
||||
.map(|e| e.pivot_x)
|
||||
.unwrap_or(0),
|
||||
b.elements
|
||||
.iter()
|
||||
.find(|e| &e.name == n)
|
||||
.map(|e| e.pivot_y)
|
||||
.unwrap_or(0)
|
||||
);
|
||||
}
|
||||
}
|
||||
48
crates/sylpheed-formats/examples/name_from_hash.rs
Normal file
48
crates/sylpheed-formats/examples/name_from_hash.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Recover a `sound.pak` TOC name from its hash by generating candidates.
|
||||
//!
|
||||
//! The hash is a Barrett-reduction over the uppercased path, so it cannot be
|
||||
//! inverted — but the naming is regular enough to enumerate. Tries the shapes
|
||||
//! this disc actually uses for sound entries.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example name_from_hash -- <hex-hash>…
|
||||
use sylpheed_formats::hash::name_hash;
|
||||
|
||||
fn main() {
|
||||
let wanted: Vec<u32> = std::env::args()
|
||||
.skip(1)
|
||||
.filter_map(|a| u32::from_str_radix(a.trim_start_matches("0x"), 16).ok())
|
||||
.collect();
|
||||
assert!(!wanted.is_empty(), "give hashes in hex");
|
||||
let langs = ["eng", "jpn", ""];
|
||||
let dirs = [
|
||||
"", "Movie", "etc", "Voice", "Sound", "BGM", "bgm", "se", "SE",
|
||||
];
|
||||
let mut tried = 0usize;
|
||||
let check = |name: String, tried: &mut usize| {
|
||||
*tried += 1;
|
||||
let h = name_hash(&name);
|
||||
if wanted.contains(&h) {
|
||||
println!(" ✅ {h:08x} {name}");
|
||||
}
|
||||
};
|
||||
for l in langs {
|
||||
for d in dirs {
|
||||
let pre = match (l.is_empty(), d.is_empty()) {
|
||||
(true, true) => String::new(),
|
||||
(true, false) => format!("{d}\\"),
|
||||
(false, true) => format!("{l}\\"),
|
||||
(false, false) => format!("{l}\\{d}\\"),
|
||||
};
|
||||
for stem in ["BGM", "bgm", "JNGL", "jngl", "SE", "Static", "VOICE"] {
|
||||
for n in 0..1200u32 {
|
||||
check(format!("{pre}{stem}_{n:03}.slb"), &mut tried);
|
||||
check(format!("{pre}{stem}{n:03}.slb"), &mut tried);
|
||||
}
|
||||
}
|
||||
for bare in ["Static.slb", "static.slb", "SE.slb", "BGM.slb"] {
|
||||
check(format!("{pre}{bare}"), &mut tried);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("tried {tried} candidate names");
|
||||
}
|
||||
92
crates/sylpheed-formats/examples/ordinal_entry_map.rs
Normal file
92
crates/sylpheed-formats/examples/ordinal_entry_map.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! Where does `screen --build N`'s ORDINAL diverge from the pak ENTRY index?
|
||||
//!
|
||||
//! `screen render --build N` takes an ordinal into the filtered build list, not
|
||||
//! a pak entry. On `GP_TITLE` `[10]` is entry 12, which is how I rendered two
|
||||
//! loading screens while believing they were the splashes — and every downstream
|
||||
//! number validated. This enumerates the divergence across the disc so any
|
||||
//! `--build N` in `docs/` can be checked instead of trusted.
|
||||
//!
|
||||
//! Two lists, because `screen list --all` swaps the predicate (`is_composable`
|
||||
//! for `is_build`) and therefore RENUMBERS: `--build 4` and `--build 4 --all`
|
||||
//! are not necessarily the same object.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ordinal_entry_map
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
/// One decompression pass per entry, both predicates applied to it: reading the
|
||||
/// archive twice doubled the cost on `GP_READY_ROOM` (902 entries) for nothing.
|
||||
fn maps(ar: &PakArchive) -> (Vec<usize>, Vec<usize>) {
|
||||
let (mut d, mut a) = (Vec::new(), Vec::new());
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if ui_layout::is_build(&by) {
|
||||
d.push(i)
|
||||
}
|
||||
if ui_layout::is_composable(&by) {
|
||||
a.push(i)
|
||||
}
|
||||
}
|
||||
(d, a)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut clean, mut div, mut allshift) = (0usize, 0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
let (d, a) = maps(&ar);
|
||||
if d.is_empty() && a.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let bad = d.iter().enumerate().find(|(o, &e)| *o != e).map(|(o, _)| o);
|
||||
// does `--all` renumber? compare the entry each ordinal resolves to
|
||||
let shift = (0..d.len().min(a.len())).find(|&o| d[o] != a[o]);
|
||||
let tag = match bad {
|
||||
None => {
|
||||
clean += 1;
|
||||
format!("{:4} builds ordinal == entry throughout", d.len())
|
||||
}
|
||||
Some(o) => {
|
||||
div += 1;
|
||||
let t: Vec<String> = d
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(o)
|
||||
.take(5)
|
||||
.map(|(x, &y)| format!("[{x}]->{y}"))
|
||||
.collect();
|
||||
format!(
|
||||
"{:4} builds 🔴 diverges at ordinal {o}: {}",
|
||||
d.len(),
|
||||
t.join(" ")
|
||||
)
|
||||
}
|
||||
};
|
||||
let s = match shift {
|
||||
Some(o) => {
|
||||
allshift += 1;
|
||||
format!(
|
||||
" ⚠️ --all renumbers from [{o}]: entry {} -> {}",
|
||||
d[o], a[o]
|
||||
)
|
||||
}
|
||||
None if a.len() != d.len() => format!(" (--all appends {} more)", a.len() - d.len()),
|
||||
None => String::new(),
|
||||
};
|
||||
println!("{name:30} {tag}{s}");
|
||||
std::io::stdout().flush().ok();
|
||||
}
|
||||
println!("\n{clean} archives ordinal==entry, {div} diverge, {allshift} renumbered by --all");
|
||||
println!("--- END ---");
|
||||
}
|
||||
@@ -17,7 +17,7 @@ fn be16(b: &[u8], at: usize) -> u32 {
|
||||
/// folded to `max(na, 1-na)` so both authored windings read as ≈1.
|
||||
fn winding(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> f32 {
|
||||
let (mut agree, mut n) = (0usize, 0usize);
|
||||
for t in idx.chunks_exact(3) {
|
||||
for t in idx.as_chunks::<3>().0 {
|
||||
let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize);
|
||||
if a == b || b == c || a == c || a.max(b).max(c) >= pos.len() || a >= nrm.len() {
|
||||
continue;
|
||||
@@ -51,7 +51,9 @@ fn winding(idx: &[u32], pos: &[[f32; 3]], nrm: &[[f32; 3]]) -> f32 {
|
||||
}
|
||||
|
||||
fn degenerate(idx: &[u32]) -> usize {
|
||||
idx.chunks_exact(3)
|
||||
idx.as_chunks::<3>()
|
||||
.0
|
||||
.iter()
|
||||
.filter(|t| t[0] == t[1] || t[1] == t[2] || t[0] == t[2])
|
||||
.count()
|
||||
}
|
||||
|
||||
65
crates/sylpheed-formats/examples/palogo_eff_check.rs
Normal file
65
crates/sylpheed-formats/examples/palogo_eff_check.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! Are `palogo_gamearts_eff` / `palogo_seta_eff` dwell-FALLBACK cases, or PLATEAU
|
||||
//! cases? The port agent lists them among `GP_TITLE`'s four visible fallback
|
||||
//! fires; this census listed only `palogo_sqex_eff` and `palogo_anima_eff`.
|
||||
//!
|
||||
//! It matters because the two are different defects. A plateau is a pose the
|
||||
//! element genuinely HOLDS, and `rest_plateau()` returning it is correct. Only the
|
||||
//! fallback is the unsound path.
|
||||
//! cargo run -p sylpheed-formats --example palogo_eff_check
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if !el.name.starts_with("palogo") || !el.name.contains("eff") {
|
||||
continue;
|
||||
}
|
||||
// A single-keyframe element has no gap to maximise, so neither path
|
||||
// applies and `rest()` trivially returns the only pose. Excluding it
|
||||
// here matches the census, which filters `len < 2`.
|
||||
if el.keyframes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let plateau = el.keyframes.windows(2).position(|w| {
|
||||
w[0].x == w[1].x
|
||||
&& w[0].y == w[1].y
|
||||
&& w[0].scale_x == w[1].scale_x
|
||||
&& w[0].scale_y == w[1].scale_y
|
||||
&& w[0].fade == w[1].fade
|
||||
});
|
||||
let ks: Vec<String> = el
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"{}:a{} {},{} {}%",
|
||||
k.time.map(|v| v.to_string()).unwrap_or("-".into()),
|
||||
(k.fade >> 24) & 0xff,
|
||||
k.x,
|
||||
k.y,
|
||||
k.scale_x
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let r = el.rest();
|
||||
println!("e{i:<3} {:24} kf=[{}]", el.name, ks.join(" "));
|
||||
println!(
|
||||
" plateau at pair {:?} -> path: {} rest a={} t={:?}",
|
||||
plateau,
|
||||
if plateau.is_some() {
|
||||
"PLATEAU (sound: the pose is held)"
|
||||
} else {
|
||||
"DWELL FALLBACK (unsound)"
|
||||
},
|
||||
r.map(|k| (k.fade >> 24) & 0xff).unwrap_or(0),
|
||||
r.and_then(|k| k.time)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
crates/sylpheed-formats/examples/plateau_choice.rs
Normal file
133
crates/sylpheed-formats/examples/plateau_choice.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
//! When an element has MORE THAN ONE plateau, does `rest_plateau()` pick the
|
||||
//! wrong one — and is that the 21.9 % residual?
|
||||
//!
|
||||
//! `rest_vs_settle` found that among elements holding a pose ACROSS the screen's
|
||||
//! settle instant, `pose_at(settle)` and `rest()` still disagree 21.9 % of the
|
||||
//! time. I hypothesised that `rest_plateau()` picks the **longest** run (it does —
|
||||
//! `len >= any_len`), which need not be the run covering the settle instant.
|
||||
//!
|
||||
//! ⚠️ **Control**: on elements with exactly ONE plateau that covers the settle
|
||||
//! instant, the two MUST agree. If they do not, the hypothesis is not the
|
||||
//! explanation and something else is wrong.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example plateau_choice
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut one_cov, mut one_agree) = (0usize, 0usize); // control
|
||||
let (mut multi_cov, mut multi_agree, mut multi_wrongrun) = (0usize, 0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let Some((lo, hi)) = b.settle_window() else {
|
||||
continue;
|
||||
};
|
||||
if hi - lo < 10 {
|
||||
continue;
|
||||
}
|
||||
let st = lo + (hi - lo) / 2;
|
||||
for el in &b.elements {
|
||||
let k = &el.keyframes;
|
||||
if k.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let same = |a: &ui_layout::Keyframe, c: &ui_layout::Keyframe| {
|
||||
a.fade == c.fade
|
||||
&& a.scale_x == c.scale_x
|
||||
&& a.scale_y == c.scale_y
|
||||
&& a.tint == c.tint
|
||||
&& a.x == c.x
|
||||
&& a.y == c.y
|
||||
};
|
||||
// enumerate maximal runs of length >= 2, with their time spans
|
||||
let mut runs: Vec<(usize, usize)> = Vec::new();
|
||||
let mut i = 0usize;
|
||||
while i < k.len() {
|
||||
let mut j = i;
|
||||
while j + 1 < k.len() && same(&k[j], &k[j + 1]) {
|
||||
j += 1
|
||||
}
|
||||
if j - i + 1 >= 2 {
|
||||
runs.push((i, j))
|
||||
}
|
||||
i = j + 1;
|
||||
}
|
||||
if runs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let covers = |&(a, c): &(usize, usize)| match (k[a].time, k[c].time) {
|
||||
(Some(t0), Some(t1)) => t0 <= st && st <= t1,
|
||||
_ => false,
|
||||
};
|
||||
let covering: Vec<_> = runs.iter().filter(|r| covers(r)).collect();
|
||||
if covering.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (Some(r), Some(s)) = (el.rest(), el.pose_at(st)) else {
|
||||
continue;
|
||||
};
|
||||
let agree = r.fade == s.fade
|
||||
&& r.x == s.x
|
||||
&& r.y == s.y
|
||||
&& r.scale_x == s.scale_x
|
||||
&& r.scale_y == s.scale_y;
|
||||
if runs.len() == 1 {
|
||||
one_cov += 1;
|
||||
if agree {
|
||||
one_agree += 1
|
||||
}
|
||||
} else {
|
||||
multi_cov += 1;
|
||||
if agree {
|
||||
multi_agree += 1
|
||||
} else {
|
||||
// did rest() land on a run that does NOT cover settle?
|
||||
let on_covering = covering.iter().any(|&&(a, c)| {
|
||||
(a..=c).any(|idx| {
|
||||
let kk = &k[idx];
|
||||
kk.fade == r.fade
|
||||
&& kk.x == r.x
|
||||
&& kk.y == r.y
|
||||
&& kk.scale_x == r.scale_x
|
||||
&& kk.scale_y == r.scale_y
|
||||
})
|
||||
});
|
||||
if !on_covering {
|
||||
multi_wrongrun += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("CONTROL — exactly ONE plateau, and it covers the settle instant:");
|
||||
println!(
|
||||
" {one_cov} elements, rest() and pose_at(settle) agree on {one_agree} ({:.1} %)",
|
||||
100.0 * one_agree as f64 / one_cov.max(1) as f64
|
||||
);
|
||||
println!("\nTEST — MORE THAN ONE plateau, at least one covering the settle instant:");
|
||||
println!(
|
||||
" {multi_cov} elements, agree on {multi_agree} ({:.1} %)",
|
||||
100.0 * multi_agree as f64 / multi_cov.max(1) as f64
|
||||
);
|
||||
println!(
|
||||
" of the {} disagreements, rest() landed on a run that does NOT cover",
|
||||
multi_cov - multi_agree
|
||||
);
|
||||
println!(" the settle instant: {multi_wrongrun}");
|
||||
println!("\n--- END (if this line is missing, the run did not finish) ---");
|
||||
}
|
||||
@@ -23,11 +23,11 @@ fn main() {
|
||||
if tok == &key {
|
||||
let lo = i.saturating_sub(6);
|
||||
let hi = (i + 7).min(t.len());
|
||||
for j in lo..hi {
|
||||
for (j, tok_j) in t.iter().enumerate().skip(lo).take(hi - lo) {
|
||||
println!(
|
||||
" [{j}]{} {:?}",
|
||||
if j == i { " <-- key" } else { " " },
|
||||
t[j]
|
||||
tok_j
|
||||
);
|
||||
}
|
||||
println!();
|
||||
|
||||
77
crates/sylpheed-formats/examples/prm_alpha_census.rs
Normal file
77
crates/sylpheed-formats/examples/prm_alpha_census.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Does a primitive's alpha AT t=0 predict the layer it paints on?
|
||||
//!
|
||||
//! `implied_layer_key` is a measured per-name table. The four entries in it, read
|
||||
//! against their own keyframes, suggest a rule derived from the file instead:
|
||||
//!
|
||||
//! * `palogo_eff0.prm` measured FIRST (0x0000) -- alpha at t=0 = ?
|
||||
//! * `pfbase.tbm` measured FIRST (0x0000) -- alpha at t=0 = ?
|
||||
//! * `pteff02.prm` measured MIDDLE (0x8030) -- alpha at t=0 = ?
|
||||
//! * `pteff00.prm` measured LAST -- alpha at t=0 = ?
|
||||
//!
|
||||
//! ⚠️ The rule was invented AFTER seeing three of those answers, so it is fitted
|
||||
//! on them and only `pfbase.tbm` is out of sample. This prints all four plus a
|
||||
//! disc-wide census, so the fit and its reach are visible together.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
// name -> (count, set of t=0 alphas, set of "starts at max" flags)
|
||||
let mut byname: BTreeMap<String, (usize, BTreeMap<u32, usize>)> = BTreeMap::new();
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
// primitives and the keyless: anything with no layer key
|
||||
if ui_layout::sprite_layer_key(&b, &by, el).is_some() {
|
||||
continue;
|
||||
}
|
||||
let Some(k0) = el.keyframes.first() else {
|
||||
continue;
|
||||
};
|
||||
let a0 = k0.fade >> 24;
|
||||
let ent = byname.entry(el.name.clone()).or_default();
|
||||
ent.0 += 1;
|
||||
*ent.1.entry(a0).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:>28} {:>7} alpha at t=0 (count)",
|
||||
"keyless element", "n"
|
||||
);
|
||||
let known = [
|
||||
"palogo_eff0.prm",
|
||||
"pfbase.tbm",
|
||||
"pteff02.prm",
|
||||
"pteff00.prm",
|
||||
];
|
||||
for (n, (c, a)) in &byname {
|
||||
let tag = if known.contains(&n.as_str()) {
|
||||
" <- IN THE MEASURED TABLE"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if *c < 4 && tag.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let al: Vec<String> = a.iter().map(|(k, v)| format!("{k}x{v}")).collect();
|
||||
println!(" {n:>26} {c:>7} {}{tag}", al.join(" "));
|
||||
}
|
||||
}
|
||||
52
crates/sylpheed-formats/examples/prm_colour_census.rs
Normal file
52
crates/sylpheed-formats/examples/prm_colour_census.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! What COLOUR is a primitive, and does any of them only make sense additively?
|
||||
//!
|
||||
//! `ui-prm-primitives.md` leaves blend mode open, and `forced_backdrop` assumes
|
||||
//! straight alpha-over. A quad whose ARGB would tint the whole screen a colour no
|
||||
//! screen shows is evidence against alpha-over for that quad.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let mut m: BTreeMap<String, BTreeMap<String, usize>> = BTreeMap::new();
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if el.sprite.is_some() {
|
||||
continue;
|
||||
}
|
||||
for k in &el.keyframes {
|
||||
*m.entry(el.name.clone())
|
||||
.or_default()
|
||||
.entry(format!("{:08x}", k.fade))
|
||||
.or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{:>26} fade ARGB values (count)", "keyless element");
|
||||
for (n, v) in &m {
|
||||
let tot: usize = v.values().sum();
|
||||
if tot < 4 {
|
||||
continue;
|
||||
}
|
||||
let s: Vec<String> = v.iter().map(|(k, c)| format!("{k}x{c}")).collect();
|
||||
println!(" {n:>24} {}", s.join(" "));
|
||||
}
|
||||
}
|
||||
95
crates/sylpheed-formats/examples/prm_forced_first.rs
Normal file
95
crates/sylpheed-formats/examples/prm_forced_first.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
//! Which keyless primitives have their paint position FORCED by occlusion?
|
||||
//!
|
||||
//! An opaque full-screen quad must sort below every element visible at any
|
||||
//! instant it is opaque. Where that set is *every* other element, its position is
|
||||
//! forced to first — derived from the file, not analogised from a neighbour.
|
||||
//!
|
||||
//! Controls, both measured in the running game and both reproduced here:
|
||||
//! * `palogo_eff0.prm` is measured painting FIRST — and comes out forced first.
|
||||
//! * `pteff00.prm` is measured painting LAST — and is forced below only a
|
||||
//! handful, so the constraint permits it on top.
|
||||
//!
|
||||
//! ⚠️ Assumes straight alpha-over blending. Blend mode is ❔ in
|
||||
//! `ui-prm-primitives.md`; an additive quad at alpha 255 would not occlude.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut forced, mut partial, mut free) = (0usize, 0usize, 0usize);
|
||||
let mut names: std::collections::BTreeMap<String, (usize, usize)> = Default::default();
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let tmax = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if tmax == 0 {
|
||||
continue;
|
||||
}
|
||||
for el in &b.elements {
|
||||
if ui_layout::sprite_layer_key(&b, &by, el).is_some() {
|
||||
continue;
|
||||
}
|
||||
// full-screen only: a quad that does not cover cannot occlude
|
||||
if el.pivot_x * 2 < 1280 || el.pivot_y * 2 < 720 {
|
||||
continue;
|
||||
}
|
||||
let op: Vec<u32> = (0..=tmax)
|
||||
.filter(|&t| el.pose_at(t).map(|k| k.fade >> 24) == Some(255))
|
||||
.collect();
|
||||
if op.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let others: Vec<&ui_layout::Element> =
|
||||
b.elements.iter().filter(|o| o.index != el.index).collect();
|
||||
if others.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let below = others
|
||||
.iter()
|
||||
.filter(|o| {
|
||||
op.iter()
|
||||
.any(|&t| o.pose_at(t).map(|k| k.fade >> 24).unwrap_or(0) > 0)
|
||||
})
|
||||
.count();
|
||||
let ent = names.entry(el.name.clone()).or_default();
|
||||
ent.1 += 1;
|
||||
if below == others.len() {
|
||||
forced += 1;
|
||||
ent.0 += 1
|
||||
} else if below > 0 {
|
||||
partial += 1
|
||||
} else {
|
||||
free += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("keyless FULL-SCREEN primitives with an opaque interval:");
|
||||
println!(" position FORCED FIRST (below every other element) : {forced}");
|
||||
println!(" forced below SOME but not all : {partial}");
|
||||
println!(" occludes nothing : {free}");
|
||||
println!("\nby name — instances forced first / total:");
|
||||
for (n, (f, t)) in &names {
|
||||
println!(" {n:>24} {f:>4} / {t}");
|
||||
}
|
||||
}
|
||||
138
crates/sylpheed-formats/examples/prm_occlusion_check.rs
Normal file
138
crates/sylpheed-formats/examples/prm_occlusion_check.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
//! An OPAQUE full-screen primitive cannot paint on top of elements that are
|
||||
//! visible at the same time — the screen would be blank.
|
||||
//!
|
||||
//! That is a constraint read off the file, not a preference. For each keyless
|
||||
//! primitive this computes the interval over which it is opaque, and the interval
|
||||
//! over which any OTHER element is visible, and reports the overlap.
|
||||
//!
|
||||
//! The falsifier: `pteff00.prm` is MEASURED painting last on the title and the
|
||||
//! main menu. If any of its instances is opaque while content is up, the
|
||||
//! constraint is wrong and this whole line is dead.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
/// Interval(s) where alpha >= `thr`, sampled at every half unit.
|
||||
fn opaque_span(el: &ui_layout::Element, thr: u32, tmax: u32) -> Vec<(f64, f64)> {
|
||||
let mut out = Vec::new();
|
||||
let mut cur: Option<f64> = None;
|
||||
let mut t = 0.0;
|
||||
while t <= tmax as f64 {
|
||||
let a = el.pose_at(t as u32).map(|k| k.fade >> 24).unwrap_or(0);
|
||||
if a >= thr {
|
||||
if cur.is_none() {
|
||||
cur = Some(t)
|
||||
}
|
||||
} else if let Some(s) = cur.take() {
|
||||
out.push((s, t));
|
||||
}
|
||||
t += 0.5;
|
||||
}
|
||||
if let Some(s) = cur {
|
||||
out.push((s, tmax as f64))
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let want = [
|
||||
"pteff00.prm",
|
||||
"pgloading_eff00.prm",
|
||||
"palogo_eff0.prm",
|
||||
"pfbase.tbm",
|
||||
"pteff02.prm",
|
||||
"pzeff00.prm",
|
||||
"pceff00.prm",
|
||||
"pdeff00.prm",
|
||||
];
|
||||
println!(
|
||||
"{:>22} {:>5} {:>16} {:>18} overlap",
|
||||
"primitive", "entry", "opaque while", "content visible"
|
||||
);
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (ei, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let tmax = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if tmax == 0 {
|
||||
continue;
|
||||
}
|
||||
for el in &b.elements {
|
||||
if !want.contains(&el.name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if ui_layout::sprite_layer_key(&b, &by, el).is_some() {
|
||||
continue;
|
||||
}
|
||||
let op = opaque_span(el, 255, tmax);
|
||||
if op.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// when is any OTHER element visible?
|
||||
let mut cmin = f64::MAX;
|
||||
let mut cmax = f64::MIN;
|
||||
for o in &b.elements {
|
||||
if o.index == el.index {
|
||||
continue;
|
||||
}
|
||||
for (s, t) in opaque_span(o, 1, tmax) {
|
||||
cmin = cmin.min(s);
|
||||
cmax = cmax.max(t)
|
||||
}
|
||||
}
|
||||
if cmin > cmax {
|
||||
continue;
|
||||
}
|
||||
// overlap of the primitive's opaque span with the content span
|
||||
let ov: f64 = op
|
||||
.iter()
|
||||
.map(|&(s, t)| (t.min(cmax) - s.max(cmin)).max(0.0))
|
||||
.sum();
|
||||
let opd: String = op
|
||||
.iter()
|
||||
.map(|&(s, t)| format!("{s:.0}-{t:.0}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let flag = if ov > 2.0 {
|
||||
" 🔴 CANNOT BE ON TOP"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
println!(
|
||||
"{:>22} {:>5} {:>16} {:>18} {ov:6.1}{flag}",
|
||||
el.name,
|
||||
format!(
|
||||
"{}:{}",
|
||||
p.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.trim_end_matches(".pak")
|
||||
.trim_start_matches("GP_"),
|
||||
ei
|
||||
),
|
||||
opd,
|
||||
format!("{cmin:.0}-{cmax:.0}")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
crates/sylpheed-formats/examples/prm_span_sensitivity.rs
Normal file
144
crates/sylpheed-formats/examples/prm_span_sensitivity.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
//! Does `forced_backdrop`'s verdict depend on how the screen's timeline ENDS?
|
||||
//!
|
||||
//! The rule quantifies over "every instant the primitive is opaque" and "every
|
||||
//! element visible then", so both halves depend on where the timeline stops and
|
||||
//! on what an element does after its own last keyframe. The port asked, and it is
|
||||
//! the right question: a verdict that flips with the convention is not a decode.
|
||||
//!
|
||||
//! Four conventions, all applied to the same disc:
|
||||
//! A span = max keyframe time over all elements; elements HOLD their last pose
|
||||
//! (what `forced_backdrop` does, and what the port implements)
|
||||
//! B span = the primitive's OWN last keyframe time; elements hold
|
||||
//! C span = the bundle header `+0x08` (the declared length); elements hold
|
||||
//! D span = max keyframe time; an element is GONE after its own last keyframe
|
||||
//!
|
||||
//! D is the one worth the most: it is the assumption the port flagged as "doing
|
||||
//! real work", and it strictly shrinks the visible set, so it can only turn
|
||||
//! `forced` into `not forced`.
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn last_t(el: &ui_layout::Element) -> u32 {
|
||||
el.keyframes
|
||||
.iter()
|
||||
.filter_map(|k| k.time)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn forced(b: &ui_layout::UiBuild, el: &ui_layout::Element, tmax: u32, hold: bool) -> Option<bool> {
|
||||
if el.sprite.is_some() {
|
||||
return None;
|
||||
}
|
||||
if (el.pivot_x * 2) < b.design_w || (el.pivot_y * 2) < b.design_h {
|
||||
return None;
|
||||
}
|
||||
if tmax == 0 {
|
||||
return None;
|
||||
}
|
||||
let alpha = |e: &ui_layout::Element, t: u32| -> u32 {
|
||||
if !hold && t > last_t(e) {
|
||||
return 0;
|
||||
}
|
||||
e.pose_at(t).map(|k| k.fade >> 24).unwrap_or(0)
|
||||
};
|
||||
let op: Vec<u32> = (0..=tmax).filter(|&t| alpha(el, t) == 255).collect();
|
||||
if op.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let others: Vec<&ui_layout::Element> =
|
||||
b.elements.iter().filter(|o| o.index != el.index).collect();
|
||||
if others.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let below = others
|
||||
.iter()
|
||||
.filter(|o| op.iter().any(|&t| alpha(o, t) > 0))
|
||||
.count();
|
||||
Some(below == others.len())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut n, mut a_true) = (0usize, 0usize);
|
||||
let mut flips = [0usize; 3];
|
||||
let mut examples: Vec<String> = Vec::new();
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for (ei, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let tall = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let hdr = if by.len() >= 12 {
|
||||
u32::from_be_bytes(by[8..12].try_into().unwrap())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for el in &b.elements {
|
||||
let Some(va) = forced(&b, el, tall, true) else {
|
||||
continue;
|
||||
};
|
||||
n += 1;
|
||||
if va {
|
||||
a_true += 1
|
||||
}
|
||||
for (k, vb) in [
|
||||
forced(&b, el, last_t(el), true),
|
||||
forced(&b, el, hdr, true),
|
||||
forced(&b, el, tall, false),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
if vb != Some(va) {
|
||||
flips[k] += 1;
|
||||
if k == 2 && examples.len() < 6 {
|
||||
examples.push(format!(
|
||||
"{}:{} {} A={va} D={vb:?}",
|
||||
p.file_name().unwrap().to_string_lossy(),
|
||||
ei,
|
||||
el.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("keyless full-screen primitives with an opaque interval: {n}");
|
||||
println!(" convention A (span = all elements' max, hold) -> forced first: {a_true}\n");
|
||||
println!(" verdicts that CHANGE under:");
|
||||
println!(
|
||||
" B span = the primitive's own last keyframe : {}",
|
||||
flips[0]
|
||||
);
|
||||
println!(
|
||||
" C span = the header's declared length +0x08 : {}",
|
||||
flips[1]
|
||||
);
|
||||
println!(
|
||||
" D elements GONE after their last keyframe : {}",
|
||||
flips[2]
|
||||
);
|
||||
for e in &examples {
|
||||
println!(" {e}")
|
||||
}
|
||||
}
|
||||
92
crates/sylpheed-formats/examples/ptloop_leaf_extent.rs
Normal file
92
crates/sylpheed-formats/examples/ptloop_leaf_extent.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! The FULL extent of the two title sweep leaves, across their whole cycle.
|
||||
//!
|
||||
//! `ptloop_leaf_sweep_at.rs` samples t=340..540 — a window chosen to compare two
|
||||
//! competing fits — so it never showed how far the leaves travel. That gap let a
|
||||
//! claim stand that `ptloop01/02` "do not free-run", measured over the PARENT's
|
||||
//! 200x90 rect, which is a pivot anchor the leaf spends almost no time inside.
|
||||
//! `sylpheed-port` reports x tracks of -639..1521 and -839..1721 from their
|
||||
//! export; this checks that against the disc.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ptloop_leaf_extent
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
for entry in [4usize, 5, 7] {
|
||||
let Ok(by) = ar.read(&ar.entries()[entry]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("\n######## GP_TITLE entry {entry} ########");
|
||||
for parent in ["ptloop01", "ptloop02"] {
|
||||
let Some(el) = b.elements.iter().find(|e| e.name.starts_with(parent)) else {
|
||||
println!(" {parent}: not present in this build");
|
||||
continue;
|
||||
};
|
||||
let Some(&(off, size)) = b.records.get(&el.name) else {
|
||||
println!(" {}: no nested record", el.name);
|
||||
continue;
|
||||
};
|
||||
let span = u32::from_be_bytes(by[off + 8..off + 12].try_into().unwrap());
|
||||
let Some(lb) = ui_layout::parse_build(&by[off..off + size]) else {
|
||||
println!(" {}: leaf will not parse", el.name);
|
||||
continue;
|
||||
};
|
||||
println!(
|
||||
" {} parent rest ({},{}) nested cycle span {span}",
|
||||
el.name,
|
||||
el.keyframes.last().map(|k| k.x).unwrap_or(0),
|
||||
el.keyframes.last().map(|k| k.y).unwrap_or(0)
|
||||
);
|
||||
for le in &lb.elements {
|
||||
let (mut lo, mut hi) = (i64::MAX, i64::MIN);
|
||||
let (mut sxs, mut sys) = (Vec::new(), Vec::new());
|
||||
for t in 0..=span {
|
||||
if let Some(k) = le.pose_at(t) {
|
||||
lo = lo.min(k.x as i64);
|
||||
hi = hi.max(k.x as i64);
|
||||
if !sxs.contains(&k.scale_x) {
|
||||
sxs.push(k.scale_x)
|
||||
}
|
||||
if !sys.contains(&k.scale_y) {
|
||||
sys.push(k.scale_y)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A CYCLE LENGTH IS NOT A MOTION DURATION. Find the last t at
|
||||
// which x still changes: sylpheed-port reports the final segment
|
||||
// HOLDS, which would make px/unit larger than cycle-based maths.
|
||||
let mut last_move = 0u32;
|
||||
let mut prev = None;
|
||||
for t in 0..=span {
|
||||
if let Some(k) = le.pose_at(t) {
|
||||
if prev.is_some_and(|p| p != k.x) {
|
||||
last_move = t
|
||||
}
|
||||
prev = Some(k.x);
|
||||
}
|
||||
}
|
||||
let w = (le.pivot_x * 2) as i64;
|
||||
println!(
|
||||
" leaf {:<12} pivot {}x{} quad w={w} x track {lo} .. {hi} \
|
||||
(centre {} .. {}) scale_x {:?} scale_y {:?}",
|
||||
le.name,
|
||||
le.pivot_x,
|
||||
le.pivot_y,
|
||||
lo + le.pivot_x as i64,
|
||||
hi + le.pivot_x as i64,
|
||||
sxs,
|
||||
sys
|
||||
);
|
||||
println!(" motion ends at t={last_move} of a {span}-unit cycle -> {:.3} px/unit over the MOVING span (vs {:.3} over the cycle)",
|
||||
(hi - lo) as f64 / last_move.max(1) as f64,
|
||||
(hi - lo) as f64 / span as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("--- END ---");
|
||||
}
|
||||
49
crates/sylpheed-formats/examples/ptloop_leaf_keyframes.rs
Normal file
49
crates/sylpheed-formats/examples/ptloop_leaf_keyframes.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! The sweep leaves' RAW keyframes, so a segment rate can be checked not assumed.
|
||||
//!
|
||||
//! `sylpheed-port` reports `pteff03` as +4.0000 px/unit over t 0..150 and +4.0000
|
||||
//! again over 150..540 -- perfectly linear -- against `pteff03a` at -4.0667 then
|
||||
//! -4.0625. That asymmetry is what makes their "inversion" observation sharp: my
|
||||
//! linearity gate fails on the leaf whose source is exactly straight. It is their
|
||||
//! number from their export, so it is worth deriving independently.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ptloop_leaf_keyframes
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let by = ar.read(&ar.entries()[4]).expect("entry 4");
|
||||
let b = ui_layout::parse_build(&by).expect("parse");
|
||||
for parent in ["ptloop01", "ptloop02"] {
|
||||
let Some(el) = b.elements.iter().find(|e| e.name.starts_with(parent)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(&(off, size)) = b.records.get(&el.name) else {
|
||||
continue;
|
||||
};
|
||||
let Some(lb) = ui_layout::parse_build(&by[off..off + size]) else {
|
||||
continue;
|
||||
};
|
||||
for le in &lb.elements {
|
||||
println!("\n== {} -> leaf {}", el.name, le.name);
|
||||
let ks: Vec<_> = le.keyframes.iter().collect();
|
||||
for w in ks.windows(2) {
|
||||
let (a, c) = (w[0], w[1]);
|
||||
match (a.time, c.time) {
|
||||
(Some(t0), Some(t1)) if t1 > t0 => println!(
|
||||
" t {t0:>4} -> {t1:<4} x {:>6} -> {:<6} = {:+.4} px/unit",
|
||||
a.x,
|
||||
c.x,
|
||||
(c.x - a.x) as f64 / (t1 - t0) as f64
|
||||
),
|
||||
_ => println!(
|
||||
" t {:?} -> {:?} x {} -> {} (no rate)",
|
||||
a.time, c.time, a.x, c.x
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("--- END ---");
|
||||
}
|
||||
94
crates/sylpheed-formats/examples/ptloop_leaf_sweep_at.rs
Normal file
94
crates/sylpheed-formats/examples/ptloop_leaf_sweep_at.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! Where are the title's two light-sweep quads at a given time — and is a
|
||||
//! best-fit against a framebuffer PNG even measuring their position?
|
||||
//!
|
||||
//! `ui-leaf-vs-parent-alpha.md` solves the sweep instant as **t = 357.7** from a
|
||||
//! GPU per-draw capture: quad centre x measured off the submitted vertex buffer,
|
||||
//! which is a position measurement at 4 px/unit. The port agent separately
|
||||
//! best-fits the same leaf against `live-title-build4-no-plate.png` and gets
|
||||
//! **~400 units**, and asked whether the two used the same capture.
|
||||
//!
|
||||
//! Before comparing the numbers, check whether the second method can see what it
|
||||
//! claims to measure. A fit that is minimised by the quad being OFF-SCREEN is
|
||||
//! minimised by absence, and would return "best" at whatever time draws least —
|
||||
//! the same shape as the `.tbm` control that could not fail.
|
||||
//!
|
||||
//! So: print the leaves' own x, alpha, and on-screen overlap across the window.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ptloop_leaf_sweep_at
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
const SCREEN_W: i64 = 1280;
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
let by = ar.read(&ar.entries()[4]).expect("entry 4");
|
||||
let b = ui_layout::parse_build(&by).expect("parse");
|
||||
|
||||
for parent in ["ptloop01", "ptloop02"] {
|
||||
let Some(el) = b.elements.iter().find(|e| e.name.starts_with(parent)) else {
|
||||
eprintln!("no element {parent}");
|
||||
continue;
|
||||
};
|
||||
let Some(&(off, size)) = b.records.get(&el.name) else {
|
||||
eprintln!("{}: no nested record", el.name);
|
||||
continue;
|
||||
};
|
||||
// `ui-record-loop-length.md`: a nested record's header `+0x08` is its
|
||||
// CYCLE LENGTH, and its keyframes need not fill it. This is why two
|
||||
// captures of the same settled title do not share a sweep phase.
|
||||
let loop_len = u32::from_be_bytes(by[off + 8..off + 12].try_into().unwrap());
|
||||
println!(
|
||||
"\n-- {} nested record: loop length (+0x08) = {loop_len}",
|
||||
el.name
|
||||
);
|
||||
let Some(lb) = ui_layout::parse_build(&by[off..off + size]) else {
|
||||
eprintln!("{}: leaf will not parse", el.name);
|
||||
continue;
|
||||
};
|
||||
for le in &lb.elements {
|
||||
let w = (le.pivot_x * 2) as i64;
|
||||
println!(
|
||||
"\n== {} -> leaf {} (sprite {:?}, pivot {}x{}, {} keyframes, last t={:?})",
|
||||
el.name,
|
||||
le.name,
|
||||
le.sprite,
|
||||
le.pivot_x,
|
||||
le.pivot_y,
|
||||
le.keyframes.len(),
|
||||
le.keyframes.last().map(|k| k.time)
|
||||
);
|
||||
// ⚠️ A keyframe's `x` is the quad's LEFT edge, not its centre. The
|
||||
// draw-capture fit is quoted in CENTRES, so compare `centre`, which is
|
||||
// `x + pivot_x`. Printing `x` under a "centre" heading is how a
|
||||
// 200-px offset gets into a comparison unnoticed.
|
||||
println!(" t | x | centre | a | on-screen px of a {w}px-wide quad");
|
||||
for t in [
|
||||
340u32, 350, 355, 357, 358, 360, 370, 380, 390, 395, 400, 405, 410, 420, 440, 480,
|
||||
540,
|
||||
] {
|
||||
let Some(k) = le.pose_at(t) else {
|
||||
println!(" {t:>4} | (no pose)");
|
||||
continue;
|
||||
};
|
||||
let x = k.x as i64;
|
||||
let a = k.fade >> 24;
|
||||
let l = x;
|
||||
let r = l + w;
|
||||
let vis = (r.min(SCREEN_W) - l.max(0)).max(0);
|
||||
println!(
|
||||
" {t:>4} | {x:>4} | {:>6} | {a:>3} | {vis:>5} px {}",
|
||||
x + le.pivot_x as i64,
|
||||
if vis == 0 {
|
||||
"*** ENTIRELY OFF SCREEN ***"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
crates/sylpheed-formats/examples/ptloop_parent_keyframes.rs
Normal file
43
crates/sylpheed-formats/examples/ptloop_parent_keyframes.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
//! The PARENT elements that host the sweep leaves — `ptloop01`/`ptloop02` as
|
||||
//! they are declared in the title build itself, not in their nested records.
|
||||
//!
|
||||
//! The leaves' own alpha is non-zero at their t=0 (`pteff03` declares 255), yet
|
||||
//! the capture shows the sweep fading in from ~8. So the gate is the parent's
|
||||
//! alpha, and this prints it.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ptloop_parent_keyframes -- GP_TITLE 5
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = argv
|
||||
.iter()
|
||||
.find(|a| a.parse::<usize>().is_err())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "GP_TITLE".to_string());
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
let builds: Vec<usize> = argv.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
for e in if builds.is_empty() {
|
||||
vec![5usize]
|
||||
} else {
|
||||
builds
|
||||
} {
|
||||
let Ok(by) = ar.read(&ar.entries()[e]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("=== {pak} entry {e}: {} elements ===", b.elements.len());
|
||||
for el in &b.elements {
|
||||
for (i, k) in el.keyframes.iter().enumerate() {
|
||||
println!(" {:<20} kf{i:<2} t={:<5} x={:<6} y={:<6} sx={:<4} sy={:<4} fade={:08X} (alpha {:3})",
|
||||
el.name,
|
||||
k.time.map(|t| t.to_string()).unwrap_or_else(|| "-".into()),
|
||||
k.x, k.y, k.scale_x, k.scale_y, k.fade, k.fade >> 24);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ fn main() {
|
||||
if let Some(out) = std::env::args().nth(2) {
|
||||
// Dependency-free PPM (P6, RGB — alpha already composited over the backdrop).
|
||||
let mut buf = format!("P6\n{} {}\n255\n", screen.width, screen.height).into_bytes();
|
||||
for px in screen.rgba.chunks_exact(4) {
|
||||
for px in screen.rgba.as_chunks::<4>().0 {
|
||||
buf.extend_from_slice(&px[..3]);
|
||||
}
|
||||
std::fs::write(&out, buf).unwrap();
|
||||
|
||||
113
crates/sylpheed-formats/examples/record_loop_length.rs
Normal file
113
crates/sylpheed-formats/examples/record_loop_length.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Is a nested record's header `+0x08` its LOOP LENGTH — and do its keyframes
|
||||
//! have to fill it?
|
||||
//!
|
||||
//! A `*f` focus record animates forever while its button is focused, so
|
||||
//! something must say where the cycle restarts. The keyframes cannot: the plate's
|
||||
//! `ptbtn00f` runs 0→80→0 over 105 units, and looping at 105 gives a period 15 %
|
||||
//! short of every measurement of the real thing.
|
||||
//!
|
||||
//! Each record is itself a RATC bundle with its own header, and `+0x08` is a
|
||||
//! frame count. If it is the loop length then it must never be LESS than the
|
||||
//! record's largest keyframe time — an animation cannot restart before its own
|
||||
//! last pose — and it may be more, which is a hold at the final pose.
|
||||
//!
|
||||
//! Two controls, both of which a wrong reading fails:
|
||||
//! * `+0x08 < max keyframe time` must never happen. That is the falsifier.
|
||||
//! * The distribution must not be trivial: if every record had exactly
|
||||
//! `+0x08 == max t`, the field would carry nothing and "loop length" would be
|
||||
//! an unfalsifiable relabelling of the keyframes.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ratc, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
|
||||
.expect("dat/")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut total, mut exact, mut holds, mut violations) = (0usize, 0usize, 0usize, 0usize);
|
||||
let mut hold_hist: BTreeMap<i64, usize> = BTreeMap::new();
|
||||
let mut worst: Vec<(i64, String)> = Vec::new();
|
||||
for p in &paks {
|
||||
let Ok(ar) = pak::PakArchive::open(p) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
if !ratc::is_ratc(&by) {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (rn, &(o, s)) in &b.records {
|
||||
if o + 12 > by.len() || o + s > by.len() {
|
||||
continue;
|
||||
}
|
||||
if &by[o..o + 4] != b"RATC" {
|
||||
continue;
|
||||
}
|
||||
let len = u32::from_be_bytes(by[o + 8..o + 12].try_into().unwrap()) as i64;
|
||||
let Some(lb) = ui_layout::parse_build(&by[o..o + s]) else {
|
||||
continue;
|
||||
};
|
||||
let maxt = lb
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0) as i64;
|
||||
if maxt == 0 {
|
||||
continue;
|
||||
} // a static record declares no cycle
|
||||
total += 1;
|
||||
let slack = len - maxt;
|
||||
*hold_hist.entry(slack).or_default() += 1;
|
||||
if slack == 0 {
|
||||
exact += 1
|
||||
} else if slack > 0 {
|
||||
holds += 1
|
||||
} else {
|
||||
violations += 1;
|
||||
if worst.len() < 12 {
|
||||
worst.push((
|
||||
slack,
|
||||
format!(
|
||||
"{}:{rn} len={len} maxt={maxt}",
|
||||
p.file_name().unwrap().to_string_lossy()
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("nested records with timed keyframes : {total}");
|
||||
println!(
|
||||
" +08 == max keyframe time (exact) : {exact} ({:.1}%)",
|
||||
100.0 * exact as f64 / total as f64
|
||||
);
|
||||
println!(
|
||||
" +08 > max keyframe time (a hold) : {holds} ({:.1}%)",
|
||||
100.0 * holds as f64 / total as f64
|
||||
);
|
||||
println!(
|
||||
" +08 < max keyframe time 🔴 : {violations} ({:.2}%) <- the falsifier",
|
||||
100.0 * violations as f64 / total as f64
|
||||
);
|
||||
println!("\nslack (+08 - max t) distribution, most common first:");
|
||||
let mut h: Vec<_> = hold_hist.iter().collect();
|
||||
h.sort_by_key(|&(_, n)| std::cmp::Reverse(*n));
|
||||
for (k, n) in h.iter().take(14) {
|
||||
println!(" slack {k:>6} : {n}");
|
||||
}
|
||||
if !worst.is_empty() {
|
||||
println!("\nviolations:");
|
||||
for (s, w) in &worst {
|
||||
println!(" {s:>6} {w}");
|
||||
}
|
||||
}
|
||||
}
|
||||
92
crates/sylpheed-formats/examples/record_loop_length_api.rs
Normal file
92
crates/sylpheed-formats/examples/record_loop_length_api.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
//! Verify the newly-public `ui_layout::loop_length_units` against the disc.
|
||||
//!
|
||||
//! `sylpheed-port` reads a record's `+0x08` itself, guarded on the RATC magic,
|
||||
//! because the field was exposed on no public ref at all — example, test and
|
||||
//! `docs/re/` only. This checks the public function reproduces the numbers the
|
||||
//! finding was written from before the port depends on it.
|
||||
//!
|
||||
//! CONTROL FIRST: the function must return `None` for a non-RATC slice and for a
|
||||
//! slice too short to hold the field. An accessor that returns a number for
|
||||
//! anything cannot be trusted to return the right one.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example record_loop_length_api
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
// ---- controls -------------------------------------------------------
|
||||
assert_eq!(
|
||||
ui_layout::loop_length_units(b"NOTR\x00\x00\x00\x00\x00\x00\x00\x78"),
|
||||
None,
|
||||
"control FAILED: accepted a non-RATC slice"
|
||||
);
|
||||
assert_eq!(
|
||||
ui_layout::loop_length_units(b"RATC\x00\x00"),
|
||||
None,
|
||||
"control FAILED: accepted a slice too short for +0x08"
|
||||
);
|
||||
assert_eq!(
|
||||
ui_layout::loop_length_units(b"RATC\x00\x00\x00\x00\x00\x00\x00\x78"),
|
||||
Some(120),
|
||||
"control FAILED: did not read +0x08 big-endian"
|
||||
);
|
||||
println!("controls pass: rejects non-RATC, rejects short, reads BE at +0x08");
|
||||
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||
|
||||
// The records the finding names, with their published values.
|
||||
let expect: &[(&str, u32)] = &[
|
||||
("ptbtn00f.rat", 120),
|
||||
("ptloop01.rat", 600),
|
||||
("ptloop02.rat", 720),
|
||||
];
|
||||
let mut seen = 0usize;
|
||||
let (mut recs, mut viol) = (0usize, 0usize);
|
||||
|
||||
for (ei, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for (name, (off, size)) in &b.records {
|
||||
let rec = &by[*off..(*off + *size).min(by.len())];
|
||||
let Some(len) = ui_layout::loop_length_units(rec) else {
|
||||
continue;
|
||||
};
|
||||
recs += 1;
|
||||
// the disc-wide invariant the finding rests on
|
||||
let largest = ui_layout::parse_build(rec)
|
||||
.map(|l| {
|
||||
l.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if len < largest {
|
||||
viol += 1;
|
||||
}
|
||||
for (want_name, want) in expect {
|
||||
if name == want_name && seen < 16 {
|
||||
seen += 1;
|
||||
let ok = if len == *want { "OK" } else { "MISMATCH" };
|
||||
println!(
|
||||
" entry {ei:2} {name:16} +0x08 = {len:4} \
|
||||
(published {want}) largest kf {largest:4} {ok}"
|
||||
);
|
||||
assert_eq!(len, *want, "{name} disagrees with the published value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n{recs} records read through the public fn; \
|
||||
{viol} violate +0x08 >= largest keyframe time"
|
||||
);
|
||||
assert!(
|
||||
seen > 0,
|
||||
"found none of the named records — the check proved nothing"
|
||||
);
|
||||
}
|
||||
99
crates/sylpheed-formats/examples/rest_fallback_audit.rs
Normal file
99
crates/sylpheed-formats/examples/rest_fallback_audit.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
//! Does "1 697 fallback fires return a visible pose" survive being said out loud?
|
||||
//!
|
||||
//! `rest-fallback-census.txt` reports that of 2 305 elements where the dwell
|
||||
//! fallback decides, 1 697 rest at `alpha > 0`. It was written as if that number
|
||||
//! were the defect. **It is only a defect where the element is a transient.** An
|
||||
//! element that genuinely ends visible and stays visible SHOULD rest visible, and
|
||||
//! the fallback happening to be the path that got there is not an error.
|
||||
//!
|
||||
//! The port agent hit the mirror image of this: it counted a screen's own exit
|
||||
//! ramp as the end of an element's visibility, so `ptmsg` — the main menu's
|
||||
//! permanent footer — came out as "a 2-unit flash". The story collapsed when
|
||||
//! said aloud. This asks the same question of my number.
|
||||
//!
|
||||
//! Split the 1 697 by what the element's LAST keyframe does:
|
||||
//! * last alpha > 0 -> the element ends visible; resting visible is right
|
||||
//! * last alpha == 0 -> it fades out; a visible rest is a transient's peak
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example rest_fallback_audit
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let (mut fires, mut vis, mut ends_visible, mut ends_zero, mut at_peak) = (0, 0, 0, 0, 0);
|
||||
// ⚠️ The port agent's exit-ramp finding applies to THIS split too: if a
|
||||
// screen's exit ramp drives every element to a=0, then "last keyframe a=0"
|
||||
// says nothing about the element being a transient. Measure it on ALL
|
||||
// elements before using it on the 1 697.
|
||||
let (mut all_el, mut all_end_zero) = (0usize, 0usize);
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if el.keyframes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
all_el += 1;
|
||||
if (el.keyframes.last().unwrap().fade >> 24) & 0xff == 0 {
|
||||
all_end_zero += 1
|
||||
}
|
||||
if el.keyframes.windows(2).any(|w| {
|
||||
w[0].x == w[1].x
|
||||
&& w[0].y == w[1].y
|
||||
&& w[0].scale_x == w[1].scale_x
|
||||
&& w[0].scale_y == w[1].scale_y
|
||||
&& w[0].fade == w[1].fade
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
fires += 1;
|
||||
let Some(r) = el.rest() else { continue };
|
||||
let a = (r.fade >> 24) & 0xff;
|
||||
if a == 0 {
|
||||
continue;
|
||||
}
|
||||
vis += 1;
|
||||
let last = (el.keyframes.last().unwrap().fade >> 24) & 0xff;
|
||||
if last > 0 {
|
||||
ends_visible += 1
|
||||
} else {
|
||||
ends_zero += 1
|
||||
}
|
||||
let peak = el
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| (k.fade >> 24) & 0xff)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if a == peak {
|
||||
at_peak += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("fallback fires {fires}");
|
||||
println!(" of those, rest alpha > 0 {vis}");
|
||||
println!(" element's LAST keyframe alpha > 0 {ends_visible} <- ends visible; resting visible is CORRECT");
|
||||
println!(" element's LAST keyframe alpha = 0 {ends_zero} <- fades out; a visible rest is a transient's peak");
|
||||
println!(" rest alpha == the element's MAX {at_peak}");
|
||||
println!("\nCONTROL on the split itself — is 'ends at a=0' near-universal?");
|
||||
println!(" all elements with >= 2 keyframes {all_el}");
|
||||
println!(
|
||||
" of those, last keyframe alpha = 0 {all_end_zero} ({:.1} %)",
|
||||
100.0 * all_end_zero as f64 / all_el as f64
|
||||
);
|
||||
println!("\n--- END (if this line is missing, the run did not finish) ---");
|
||||
}
|
||||
91
crates/sylpheed-formats/examples/rest_fallback_census.rs
Normal file
91
crates/sylpheed-formats/examples/rest_fallback_census.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! When the resting-pose DWELL FALLBACK actually runs, does it pick a visible pose?
|
||||
//!
|
||||
//! `ui-resting-pose.md` argues the fallback is structurally unsound — the gap it
|
||||
//! maximises is time spent *interpolating*, so neither endpoint is held. Its one
|
||||
//! worked example, `GP_TITLE` build 7's `ptlogo_eff3.t32`, **no longer
|
||||
//! discriminates**: under the corrected keyframe-record layout the longest gap
|
||||
//! moved from `61→103` to `0→46`, and both ends of that are `a = 0`. The page's
|
||||
//! listing still shows the stale parser's trailing `-`.
|
||||
//!
|
||||
//! Losing the example is not the same as closing the question, so: disc-wide, how
|
||||
//! often does the fallback fire, and when it does, does it return something the
|
||||
//! player would see? An element resting at `a = 0` is harmless whichever end the
|
||||
//! rule lands on.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example rest_fallback_census
|
||||
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
let (mut elements, mut plateau, mut fallback, mut fb_visible) = (0usize, 0, 0, 0);
|
||||
let mut worst: Vec<(u32, String, String)> = Vec::new();
|
||||
let mut per_pak: std::collections::BTreeMap<String, (usize, usize)> = Default::default();
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
let name = pak.file_name().unwrap().to_string_lossy().to_string();
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if el.keyframes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
elements += 1;
|
||||
// a plateau is two ADJACENT poses that are equal — the same test
|
||||
// the plateau path makes before the fallback can run
|
||||
let has_plateau = el.keyframes.windows(2).any(|w| {
|
||||
w[0].x == w[1].x
|
||||
&& w[0].y == w[1].y
|
||||
&& w[0].scale_x == w[1].scale_x
|
||||
&& w[0].scale_y == w[1].scale_y
|
||||
&& w[0].fade == w[1].fade
|
||||
});
|
||||
if has_plateau {
|
||||
plateau += 1;
|
||||
continue;
|
||||
}
|
||||
fallback += 1;
|
||||
per_pak.entry(name.clone()).or_default().0 += 1;
|
||||
let Some(r) = el.rest() else { continue };
|
||||
let a = (r.fade >> 24) & 0xff;
|
||||
if a > 0 {
|
||||
fb_visible += 1;
|
||||
per_pak.entry(name.clone()).or_default().1 += 1;
|
||||
worst.push((a, name.clone(), format!("e{i}/{}", el.name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"POPULATION: {elements} elements with >= 2 keyframes, over {} archives",
|
||||
paks.len()
|
||||
);
|
||||
println!("COVERAGE: {plateau} have a plateau (fallback never runs)");
|
||||
println!(" {fallback} have NONE -> the dwell fallback decides");
|
||||
println!(" {fb_visible} of those rest at alpha > 0 -- i.e. VISIBLE\n");
|
||||
println!("PER ARCHIVE — fallback fires / of those, rests VISIBLE:");
|
||||
let mut rows: Vec<_> = per_pak.into_iter().collect();
|
||||
rows.sort_by_key(|a| std::cmp::Reverse(a.1 .1));
|
||||
for (pak, (fires, vis)) in &rows {
|
||||
println!(" {pak:34} {fires:5} fires {vis:5} visible");
|
||||
}
|
||||
println!();
|
||||
worst.sort_by_key(|a| std::cmp::Reverse(a.0));
|
||||
for (a, pak, el) in worst.iter().take(6) {
|
||||
println!(" a={a:3} {pak} {el}");
|
||||
}
|
||||
println!("\n--- END OF CENSUS (if this line is missing, the run did not finish) ---");
|
||||
}
|
||||
49
crates/sylpheed-formats/examples/rest_fallback_title.rs
Normal file
49
crates/sylpheed-formats/examples/rest_fallback_title.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! Which `GP_TITLE` elements does the resting-pose dwell fallback decide, and does
|
||||
//! it hand back a visible pose? The disc-wide census says 5 fires / 4 visible here.
|
||||
//! cargo run -p sylpheed-formats --example rest_fallback_title
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
if el.keyframes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
if el.keyframes.windows(2).any(|w| {
|
||||
w[0].x == w[1].x
|
||||
&& w[0].y == w[1].y
|
||||
&& w[0].scale_x == w[1].scale_x
|
||||
&& w[0].scale_y == w[1].scale_y
|
||||
&& w[0].fade == w[1].fade
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let Some(r) = el.rest() else { continue };
|
||||
let a = (r.fade >> 24) & 0xff;
|
||||
let ks: Vec<String> = el
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| {
|
||||
format!(
|
||||
"{}:a{}",
|
||||
k.time.map(|v| v.to_string()).unwrap_or("-".into()),
|
||||
(k.fade >> 24) & 0xff
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
"entry {i:2} {:24} rest a={a:3} t={:?} [{}]{}",
|
||||
el.name,
|
||||
r.time,
|
||||
ks.join(" "),
|
||||
if a > 0 { " <== VISIBLE" } else { "" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
crates/sylpheed-formats/examples/rest_scale_of.rs
Normal file
62
crates/sylpheed-formats/examples/rest_scale_of.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
//! The resting SCALE of each element, so a drawn quad's size can be predicted.
|
||||
//!
|
||||
//! One additive draw on both the main menu and `EXTRAS` measures 819.2 x 720 px
|
||||
//! and matches no sprite at 1x or 2x. Scale is the missing factor: the keyframe
|
||||
//! carries scale_x / scale_y in percent, and a sprite drawn at 200 % x 500 % is
|
||||
//! nothing like its stored size.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example rest_scale_of -- 5 6 4
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let argv: Vec<String> = std::env::args().skip(1).collect();
|
||||
let pak = argv
|
||||
.iter()
|
||||
.find(|a| a.parse::<usize>().is_err())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "GP_TITLE".to_string());
|
||||
let ar = PakArchive::open(root.join(format!("dat/{pak}.pak"))).expect("pak");
|
||||
let builds: Vec<usize> = argv.iter().filter_map(|a| a.parse().ok()).collect();
|
||||
for build in if builds.is_empty() {
|
||||
vec![5usize, 6]
|
||||
} else {
|
||||
builds
|
||||
} {
|
||||
let Ok(by) = ar.read(&ar.entries()[build]) else {
|
||||
continue;
|
||||
};
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
println!("=== {pak} entry {build} ===");
|
||||
println!(
|
||||
"{:<22} {:>10} {:>7} {:>7} {:>12} at 1x/2x of pivot*2",
|
||||
"element", "pivot(w,h)", "sx%", "sy%", "drawn px"
|
||||
);
|
||||
for e in &b.elements {
|
||||
let k = match e.rest() {
|
||||
Some(k) => k,
|
||||
None => continue,
|
||||
};
|
||||
let w = e.pivot_x * 2;
|
||||
let h = e.pivot_y * 2;
|
||||
let dw = w as f64 * k.scale_x as f64 / 100.0;
|
||||
let dh = h as f64 * k.scale_y as f64 / 100.0;
|
||||
println!(
|
||||
"{:<22} {:>4},{:<5} {:>7} {:>7} {:>6.1}x{:<5.1} {:>6.1}x{:<5.1}",
|
||||
e.name,
|
||||
w,
|
||||
h,
|
||||
k.scale_x,
|
||||
k.scale_y,
|
||||
dw,
|
||||
dh,
|
||||
dw * 2.0,
|
||||
dh * 2.0
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
134
crates/sylpheed-formats/examples/rest_vs_settle.rs
Normal file
134
crates/sylpheed-formats/examples/rest_vs_settle.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! Should the settled pose come from each element's `rest()`, or from the
|
||||
//! **screen's** settle instant?
|
||||
//!
|
||||
//! Three iterations have measured how badly `rest()`'s dwell fallback behaves —
|
||||
//! 2 305 elements where it decides, 1 457 of them handed the element's *maximum*
|
||||
//! alpha, and by construction none of those poses is held. What has been missing
|
||||
//! is a proposal.
|
||||
//!
|
||||
//! `UiBuild::settle_time()` already exists: the midpoint of the longest
|
||||
//! keyframe-free interval **across the whole build**. That is the port agent's
|
||||
//! "re-key on the screen's span rather than the element's", and its shipped path
|
||||
//! poses `pose_at(hold)` and agrees with every capture it holds at 0.01 %.
|
||||
//!
|
||||
//! ⚠️ **Control first.** On elements where `rest()` is already sound — the plateau
|
||||
//! path, a pose the element genuinely holds — `pose_at(settle)` must AGREE. If it
|
||||
//! disagrees there, it is not a better rule, it is a different one.
|
||||
//!
|
||||
//! ⚠️ `ui-settle-time.md` records that 42 % of bundles have a settle window under
|
||||
//! 10 units and never settle at all. Bundles are split on that here rather than
|
||||
//! averaged over.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example rest_vs_settle
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
// control population (plateau) and test population (fallback), each split by
|
||||
// whether the bundle settles at all
|
||||
let (mut ctl_n, mut ctl_cov, mut ctl_agree) = (0usize, 0usize, 0usize);
|
||||
let (mut fb_n, mut fb_rest_vis, mut fb_settle_vis) = (0usize, 0usize, 0usize);
|
||||
let mut narrow = 0usize;
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let Some((lo, hi)) = b.settle_window() else {
|
||||
continue;
|
||||
};
|
||||
if hi - lo < 10 {
|
||||
narrow += 1;
|
||||
continue;
|
||||
} // this bundle never settles
|
||||
let st = lo + (hi - lo) / 2;
|
||||
for el in &b.elements {
|
||||
if el.keyframes.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let plateau = el.keyframes.windows(2).any(|w| {
|
||||
w[0].x == w[1].x
|
||||
&& w[0].y == w[1].y
|
||||
&& w[0].scale_x == w[1].scale_x
|
||||
&& w[0].scale_y == w[1].scale_y
|
||||
&& w[0].fade == w[1].fade
|
||||
});
|
||||
let (Some(r), Some(s)) = (el.rest(), el.pose_at(st)) else {
|
||||
continue;
|
||||
};
|
||||
let (ra, sa) = ((r.fade >> 24) & 0xff, (s.fade >> 24) & 0xff);
|
||||
if plateau {
|
||||
// ⚠️ The first version of this control compared EVERY plateau
|
||||
// element and got 46.6 % agreement — then I asked what that
|
||||
// means physically. `rest()` finds *a* held pose; many
|
||||
// elements hold one during the build-in and then move on.
|
||||
// `pose_at(settle)` asks what is on screen WHEN THE SCREEN HAS
|
||||
// SETTLED. Those are different questions, so disagreement
|
||||
// proves nothing. The fair control is the subset where the
|
||||
// held interval actually CONTAINS the settle instant.
|
||||
ctl_n += 1;
|
||||
let covers = el.keyframes.windows(2).any(|w| {
|
||||
let held = w[0].x == w[1].x
|
||||
&& w[0].y == w[1].y
|
||||
&& w[0].scale_x == w[1].scale_x
|
||||
&& w[0].scale_y == w[1].scale_y
|
||||
&& w[0].fade == w[1].fade;
|
||||
match (w[0].time, w[1].time) {
|
||||
(Some(a), Some(bb)) => held && a <= st && st <= bb,
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
if covers {
|
||||
ctl_cov += 1;
|
||||
if ra == sa
|
||||
&& r.x == s.x
|
||||
&& r.y == s.y
|
||||
&& r.scale_x == s.scale_x
|
||||
&& r.scale_y == s.scale_y
|
||||
{
|
||||
ctl_agree += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fb_n += 1;
|
||||
if ra > 0 {
|
||||
fb_rest_vis += 1
|
||||
}
|
||||
if sa > 0 {
|
||||
fb_settle_vis += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("bundles skipped as never-settling (window < 10 units): {narrow}\n");
|
||||
println!("CONTROL — elements where rest() takes the SOUND plateau path:");
|
||||
println!(" {ctl_n} plateau elements in settling bundles");
|
||||
println!(" {ctl_cov} of them HOLD ACROSS the settle instant — the fair control");
|
||||
println!(
|
||||
" pose_at(settle) agrees with rest() on {ctl_agree} of those ({:.1} %)",
|
||||
100.0 * ctl_agree as f64 / ctl_cov.max(1) as f64
|
||||
);
|
||||
println!("\nTEST — elements where the unsound dwell fallback decides:");
|
||||
println!(" {fb_n} elements");
|
||||
println!(
|
||||
" rest() returns a VISIBLE pose on {fb_rest_vis} ({:.1} %)",
|
||||
100.0 * fb_rest_vis as f64 / fb_n.max(1) as f64
|
||||
);
|
||||
println!(
|
||||
" pose_at(settle) returns a VISIBLE pose on {fb_settle_vis} ({:.1} %)",
|
||||
100.0 * fb_settle_vis as f64 / fb_n.max(1) as f64
|
||||
);
|
||||
println!("\n--- END (if this line is missing, the run did not finish) ---");
|
||||
}
|
||||
60
crates/sylpheed-formats/examples/scale_census.rs
Normal file
60
crates/sylpheed-formats/examples/scale_census.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Every scale value on the disc's UI, parents AND nested leaves.
|
||||
//!
|
||||
//! `DECISIONS.md` records `ptlogo_eff2` at 125 % as "the single drawn element in
|
||||
//! the whole export at a scale that is not a whole multiple of 100 %". That
|
||||
//! census was over parents only -- leaves were never opened. This opens them.
|
||||
use std::collections::BTreeMap;
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let path = std::env::args().nth(1).expect("pak");
|
||||
let ar = pak::PakArchive::open(&path).expect("open");
|
||||
let mut hist: BTreeMap<(u32, u32), Vec<String>> = BTreeMap::new();
|
||||
let mut leaves_opened = 0usize;
|
||||
for (i, e) in ar.entries().to_vec().iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
for el in &b.elements {
|
||||
for k in &el.keyframes {
|
||||
hist.entry((k.scale_x, k.scale_y))
|
||||
.or_default()
|
||||
.push(format!("e{i}/{}", el.name));
|
||||
}
|
||||
if let Some(&(off, size)) = b.records.get(&el.name) {
|
||||
if let Some(lb) = ui_layout::parse_build(&bytes[off..off + size]) {
|
||||
leaves_opened += 1;
|
||||
for le in &lb.elements {
|
||||
for k in &le.keyframes {
|
||||
hist.entry((k.scale_x, k.scale_y))
|
||||
.or_default()
|
||||
.push(format!("e{i}/{}->LEAF/{}", el.name, le.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("{leaves_opened} leaves opened\n");
|
||||
println!("{:>12} {:>7} examples", "scale", "count");
|
||||
for (k, v) in &hist {
|
||||
let mut ex: Vec<&String> = v.iter().collect();
|
||||
ex.sort();
|
||||
ex.dedup();
|
||||
let odd = k.0 % 100 != 0 || k.1 % 100 != 0;
|
||||
println!(
|
||||
"{}{:>5},{:<5} {:>7} {}",
|
||||
if odd { "* " } else { " " },
|
||||
k.0,
|
||||
k.1,
|
||||
v.len(),
|
||||
ex.iter()
|
||||
.take(3)
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
println!("\n* = not a whole multiple of 100%");
|
||||
}
|
||||
111
crates/sylpheed-formats/examples/settle_midramp_census.rs
Normal file
111
crates/sylpheed-formats/examples/settle_midramp_census.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! How often does posing at the SCREEN's settle instant catch an element
|
||||
//! mid-ramp? The adversarial census of my own proposal.
|
||||
//!
|
||||
//! The port agent found `ptmsg` — the main menu's footer — at alpha **127.5 of
|
||||
//! 255** at that screen's settle instant, because the longest keyframe-free
|
||||
//! interval ends exactly as the footer starts to arrive. `screen render --settle`
|
||||
//! already prints "⚠️ narrow — this bundle may never settle" there: the window is
|
||||
//! **12 units**.
|
||||
//!
|
||||
//! ⚠️ **And my `rest_vs_settle` filter was too permissive**: it dropped bundles
|
||||
//! with a window under 10 units, so a 12-unit window passed while the tool itself
|
||||
//! was flagging it. This splits by width instead of picking one cutoff.
|
||||
//!
|
||||
//! "Mid-ramp" = at the settle instant the element sits strictly inside an interval
|
||||
//! whose two endpoint poses DIFFER — it is interpolating, not held.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example settle_midramp_census
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
// buckets by settle-window width
|
||||
let edges = [0u32, 10, 20, 30, 60, u32::MAX];
|
||||
let names = ["< 10", "10–19", "20–29", "30–59", ">= 60"];
|
||||
let mut els = [0usize; 5];
|
||||
let mut mid = [0usize; 5];
|
||||
let mut bundles = [0usize; 5];
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let Some((lo, hi)) = b.settle_window() else {
|
||||
continue;
|
||||
};
|
||||
let w = hi - lo;
|
||||
let bi = edges
|
||||
.windows(2)
|
||||
.position(|p| w >= p[0] && w < p[1])
|
||||
.unwrap_or(4);
|
||||
bundles[bi] += 1;
|
||||
let st = lo + w / 2;
|
||||
for el in &b.elements {
|
||||
let k = &el.keyframes;
|
||||
if k.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
els[bi] += 1;
|
||||
// the interval containing the settle instant
|
||||
let mut interpolating = false;
|
||||
for pair in k.windows(2) {
|
||||
if let (Some(t0), Some(t1)) = (pair[0].time, pair[1].time) {
|
||||
if t0 <= st && st <= t1 && t0 != t1 {
|
||||
let same = pair[0].fade == pair[1].fade
|
||||
&& pair[0].x == pair[1].x
|
||||
&& pair[0].y == pair[1].y
|
||||
&& pair[0].scale_x == pair[1].scale_x
|
||||
&& pair[0].scale_y == pair[1].scale_y;
|
||||
if !same && st != t0 && st != t1 {
|
||||
interpolating = true
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if interpolating {
|
||||
mid[bi] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{:8}{:>10}{:>10}{:>12}{:>10}",
|
||||
"window", "bundles", "elements", "mid-ramp", "share"
|
||||
);
|
||||
for i in 0..5 {
|
||||
if els[i] == 0 {
|
||||
continue;
|
||||
}
|
||||
println!(
|
||||
"{:8}{:>10}{:>10}{:>12}{:>9.1}%",
|
||||
names[i],
|
||||
bundles[i],
|
||||
els[i],
|
||||
mid[i],
|
||||
100.0 * mid[i] as f64 / els[i] as f64
|
||||
);
|
||||
}
|
||||
let te: usize = els.iter().sum();
|
||||
let tm: usize = mid.iter().sum();
|
||||
println!(
|
||||
"{:8}{:>10}{:>10}{:>12}{:>9.1}%",
|
||||
"ALL",
|
||||
bundles.iter().sum::<usize>(),
|
||||
te,
|
||||
tm,
|
||||
100.0 * tm as f64 / te as f64
|
||||
);
|
||||
println!("\n--- END (if this line is missing, the run did not finish) ---");
|
||||
}
|
||||
80
crates/sylpheed-formats/examples/settle_narrow_rate.rs
Normal file
80
crates/sylpheed-formats/examples/settle_narrow_rate.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
//! What share of bundles have a NARROW settle window -- and of WHICH bundles?
|
||||
//!
|
||||
//! `screen render --settle`'s help says "a narrow one means the bundle never
|
||||
//! settles (42 % of them, mostly `loop*` fragments)". The 42 % is correct and is
|
||||
//! stated precisely in ui-settle-time.md: 731 of **1 758 composable bundles
|
||||
//! carrying two or more keyframe times**. But inside `screen render`, "them"
|
||||
//! reads as the bundles you would render -- the SCREEN BUILDS -- which is a
|
||||
//! different and much smaller population. This computes both.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example settle_narrow_rate
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat/")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
// (with >=2 keyframe times, narrow) for each population
|
||||
let (mut b2, mut bn) = (0usize, 0usize); // screen builds (is_build)
|
||||
let (mut c2, mut cn) = (0usize, 0usize); // composable (is_composable)
|
||||
for pak in &paks {
|
||||
let Ok(ar) = PakArchive::open(pak) else {
|
||||
continue;
|
||||
};
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let is_b = ui_layout::is_build(&by);
|
||||
let is_c = ui_layout::is_composable(&by);
|
||||
if !is_b && !is_c {
|
||||
continue;
|
||||
}
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
let mut ts: Vec<u32> = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.collect();
|
||||
ts.sort_unstable();
|
||||
ts.dedup();
|
||||
if ts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let narrow = match b.settle_window() {
|
||||
Some((lo, hi)) => hi - lo < 10,
|
||||
None => true,
|
||||
};
|
||||
if is_b {
|
||||
b2 += 1;
|
||||
if narrow {
|
||||
bn += 1
|
||||
}
|
||||
}
|
||||
if is_c {
|
||||
c2 += 1;
|
||||
if narrow {
|
||||
cn += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("population n narrow (<10 u) share");
|
||||
println!("SCREEN BUILDS (is_build, what `screen render` renders by default)");
|
||||
println!(
|
||||
" {b2:5} {bn:9} {:.0} %",
|
||||
100.0 * bn as f64 / b2.max(1) as f64
|
||||
);
|
||||
println!("COMPOSABLE bundles (is_composable, what --all admits)");
|
||||
println!(
|
||||
" {c2:5} {cn:9} {:.0} %",
|
||||
100.0 * cn as f64 / c2.max(1) as f64
|
||||
);
|
||||
println!("\nui-settle-time.md quotes 731 / 1758 = 42 % over composable bundles.");
|
||||
println!("--- END ---");
|
||||
}
|
||||
49
crates/sylpheed-formats/examples/settle_window.rs
Normal file
49
crates/sylpheed-formats/examples/settle_window.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// The settled screen, computed from the keyframe times alone.
|
||||
//
|
||||
// `rest()` picks each element's last HOLD keyframe independently, which is right
|
||||
// for an element that ends settled and wrong for a transient: a 2-frame flash
|
||||
// holds at its PEAK, so `rest()` leaves it burning forever. The settled screen is
|
||||
// instead one INSTANT that every element is posed at, and the instant to pick is
|
||||
// inside the longest interval during which no element has a keyframe at all.
|
||||
use sylpheed_formats::{pak, ui_layout};
|
||||
fn main() {
|
||||
let mut a = std::env::args().skip(1);
|
||||
let pk = a.next().unwrap();
|
||||
let ar = pak::PakArchive::open(&pk).unwrap();
|
||||
let only: Option<usize> = a.next().and_then(|s| s.parse().ok());
|
||||
for (i, e) in ar.entries().iter().enumerate() {
|
||||
if let Some(o) = only {
|
||||
if o != i {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else {
|
||||
continue;
|
||||
};
|
||||
if b.elements.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let mut ts: Vec<u32> = b
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||
.collect();
|
||||
if ts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
ts.sort_unstable();
|
||||
ts.dedup();
|
||||
// longest gap between consecutive keyframe times
|
||||
let (mut best, mut lo, mut hi) = (0u32, 0u32, 0u32);
|
||||
for w in ts.windows(2) {
|
||||
if w[1] - w[0] > best {
|
||||
best = w[1] - w[0];
|
||||
lo = w[0];
|
||||
hi = w[1];
|
||||
}
|
||||
}
|
||||
println!("{:>4} {:<28} times {:>3} span {:>4} settle window [{lo},{hi}] = {best} units ({:.2}s) -> t={}",
|
||||
i, format!("{:08x}",e.name_hash), ts.len(), ts.last().unwrap(), best as f64/60.0, lo+best/2);
|
||||
}
|
||||
}
|
||||
39
crates/sylpheed-formats/examples/settle_window_check.rs
Normal file
39
crates/sylpheed-formats/examples/settle_window_check.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! Where does `settle_window()`'s answer come from? The port agent recomputes the
|
||||
//! publisher splash's widest keyframe-free gap as 190 units; `--settle` reports 8.
|
||||
//! One of the two readings is wrong and the file settles it.
|
||||
//! cargo run -p sylpheed-formats --example settle_window_check -- <build>
|
||||
use std::path::PathBuf;
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
fn main() {
|
||||
let b: usize = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or("10".into())
|
||||
.parse()
|
||||
.unwrap();
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE");
|
||||
let by = ar.read(&ar.entries()[b]).expect("entry");
|
||||
let build = ui_layout::parse_build(&by).expect("parse");
|
||||
println!("entry {b}: {} elements", build.elements.len());
|
||||
for el in &build.elements {
|
||||
let ts: Vec<String> = el
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| k.time.map(|v| v.to_string()).unwrap_or("-".into()))
|
||||
.collect();
|
||||
println!(" {:26} [{}]", el.name, ts.join(" "));
|
||||
}
|
||||
let mut ts: Vec<u32> = build
|
||||
.elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.time))
|
||||
.collect();
|
||||
ts.sort_unstable();
|
||||
ts.dedup();
|
||||
println!("\nunion of all element keyframe times: {ts:?}");
|
||||
let gaps: Vec<(u32, u32, u32)> = ts.windows(2).map(|w| (w[1] - w[0], w[0], w[1])).collect();
|
||||
let mut g = gaps.clone();
|
||||
g.sort_by_key(|a| std::cmp::Reverse(a.0));
|
||||
println!("widest gaps: {:?}", &g[..g.len().min(4)]);
|
||||
println!("settle_window() reports {:?}", build.settle_window());
|
||||
}
|
||||
@@ -38,7 +38,7 @@ fn main() {
|
||||
hi[k] = hi[k].max(v[k]);
|
||||
}
|
||||
}
|
||||
for t in sub.indices.chunks_exact(3) {
|
||||
for t in sub.indices.as_chunks::<3>().0 {
|
||||
tris.push([w[t[0] as usize], w[t[1] as usize], w[t[2] as usize]]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ fn main() {
|
||||
.or_default()
|
||||
.push((m.name.clone(), thin));
|
||||
}
|
||||
for (id, parts) in &by_ship {
|
||||
for parts in by_ship.values() {
|
||||
if parts.len() < 3 {
|
||||
continue; // no meaningful median
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ fn main() {
|
||||
};
|
||||
has_riff += 1;
|
||||
if ri > slb::HEADERLESS_DATA_OFFSET
|
||||
&& (ri - slb::HEADERLESS_DATA_OFFSET) % slb::XMA1_PACKET == 0
|
||||
&& (ri - slb::HEADERLESS_DATA_OFFSET).is_multiple_of(slb::XMA1_PACKET)
|
||||
{
|
||||
hybrid += 1;
|
||||
if b[slb::HEADERLESS_DATA_OFFSET..ri].iter().any(|x| *x != 0) {
|
||||
|
||||
93
crates/sylpheed-formats/examples/sound_cue_fields.rs
Normal file
93
crates/sylpheed-formats/examples/sound_cue_fields.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
//! F2 — is a per-cue or per-bus GAIN on the disc?
|
||||
//!
|
||||
//! The port has no gain value anywhere in its export: `confirm` peaks at
|
||||
//! −0.0 dBFS and sits 3 dB above the music. A cue record commonly carries a
|
||||
//! volume beside its wave index. This asks the disc directly rather than
|
||||
//! choosing a number.
|
||||
//!
|
||||
//! Method: dump every token of every `tables.pak` object whose tokens mention
|
||||
//! SOUND/BANK/SE/BGM, so a gain field would appear as a token if one exists.
|
||||
//! ⚠️ A NEGATIVE here is only as good as its coverage, so this prints the token
|
||||
//! count per object and does not filter — a field missed by a filter would read
|
||||
//! exactly like a field that is not there.
|
||||
//!
|
||||
//! cargo run --release -p sylpheed-formats --example sound_cue_fields -- $SYLPHEED_DISC
|
||||
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let root = a
|
||||
.get(1)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| std::env::var("SYLPHEED_DISC").unwrap());
|
||||
let arc = PakArchive::open(format!("{root}/dat/tables.pak")).unwrap();
|
||||
|
||||
// Anything a gain would plausibly be called, plus the audio nouns.
|
||||
const GAINY: &[&str] = &["VOL", "GAIN", "LEVEL", "DB", "ATTEN", "AMP", "MIX", "LOUD"];
|
||||
let mut audio_objs = 0usize;
|
||||
let mut gain_hits: Vec<(usize, String)> = Vec::new();
|
||||
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
let t = o.tokens();
|
||||
let up: Vec<String> = t.iter().map(|s| s.to_uppercase()).collect();
|
||||
let is_audio = up.iter().any(|s| {
|
||||
s.contains("SOUND")
|
||||
|| s.contains("BANK_")
|
||||
|| s.starts_with("SE_")
|
||||
|| s.starts_with("BGM_")
|
||||
});
|
||||
if !is_audio {
|
||||
continue;
|
||||
}
|
||||
audio_objs += 1;
|
||||
println!(
|
||||
"audio object #{i}: schema {:08x}, {} tokens",
|
||||
o.schema_hash,
|
||||
t.len()
|
||||
);
|
||||
for (j, tok) in up.iter().enumerate() {
|
||||
if GAINY.iter().any(|g| tok.contains(g)) {
|
||||
gain_hits.push((i, t[j].clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\naudio-bearing objects examined: {audio_objs}");
|
||||
println!("tokens matching {GAINY:?}: {}", gain_hits.len());
|
||||
for (i, tok) in &gain_hits {
|
||||
println!(" object #{i}: {tok}");
|
||||
}
|
||||
if gain_hits.is_empty() {
|
||||
println!("\n==> NO gain-like token in any audio object of tables.pak.");
|
||||
}
|
||||
|
||||
// CONTROL: the search must be able to FIND a token when one is present.
|
||||
// Without this, "no hits" is indistinguishable from a broken matcher.
|
||||
let mut ctrl = 0usize;
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else {
|
||||
continue;
|
||||
};
|
||||
ctrl += o
|
||||
.tokens()
|
||||
.iter()
|
||||
.filter(|s| s.to_uppercase().contains("SE_UI"))
|
||||
.count();
|
||||
}
|
||||
println!(
|
||||
"\nCONTROL — the same matcher looking for a token known to exist (\"SE_UI\"): {ctrl} hits"
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
if ctrl > 0 {
|
||||
"PASS: the matcher finds tokens that are there"
|
||||
} else {
|
||||
"FAIL: matcher is broken, the negative above means nothing"
|
||||
}
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user