This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/crates/sylpheed-formats/tests/ui_header_time_disc.rs
Sylpheed RE agent a40a0b7b01 docs: looking closer at the frame-rate word weakened it, and that is the result
The 16.16 reading of +0x04 rested on twelve bundles at 30.0. They are not twelve
witnesses: they are TWO bundles - pghud_range_main_em/emeff and
pghud_range_nose_em/emeff, both dur 30 - repeated across six language PAKs. A
30-frame flash is equally consistent with 0.5 s at 60 and 1 s at 30, so the
observation that "30.0 bundles are shorter" says almost nothing.

Worse for the clean fixed-point reading: the four bundles at 0x3C0001 would be
60.0000152 fps, which nobody authors. They are py_ranking_jump/py_ranking_next
dialogs, all dur 60. So the better reading is <rate:16>.<flag:16> - a rate-like
number in the high half and a small low field that is 0 on 2843 bundles and 1 on
four, meaning unknown.

Also stated: nothing in this container can settle it by timing, because the
emulator runs on software Vulkan far from real time - a stopwatch would measure
lavapipe rather than the game.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
2026-08-24 07:34:56 +00:00

159 lines
5.9 KiB
Rust

//! Is the bundle header's `+0x08` really an animation **duration in frames**?
//!
//! The header sweep found `+0x04` takes only `0x3C0000` and `0x1E0000` — exactly
//! `60.0` and `30.0` in 16.16 — and `+0x08` takes 30 / 1200 / 120 / 60, which
//! *looks* like a frame rate and a length. That reading came from the values
//! alone, and this checks it against something the file states independently:
//! the **keyframe times** in the placement region.
//!
//! If `+0x08` is the length of the bundle's animation, the largest keyframe time
//! in the bundle should never exceed it, and should reach it on bundles that
//! animate all the way through. If instead the times run past it, the reading is
//! wrong and the amber has to come off the other way.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
fn disc_root() -> Option<PathBuf> {
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
let p = PathBuf::from(p);
if p.join("dat").is_dir() {
return Some(p);
}
}
let default = Path::new(
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
);
if default.join("dat").is_dir() {
return Some(default.to_path_buf());
}
None
}
fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) {
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
.expect("dat/")
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
.collect();
paks.sort();
for p in &paks {
let name = p.file_name().unwrap().to_string_lossy().to_string();
let Ok(arc) = PakArchive::open(p) else { continue };
for e in arc.entries() {
let Ok(bytes) = arc.read(e) else { continue };
if ratc::is_ratc(&bytes) {
f(&name, &bytes);
}
}
}
}
fn be32(b: &[u8], at: usize) -> u32 {
u32::from_be_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}
#[test]
fn header_0x08_against_the_keyframe_times() {
let Some(root) = disc_root() else {
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
return;
};
let (mut animated, mut within, mut exact, mut over) = (0usize, 0, 0, 0);
let mut worst: Vec<String> = Vec::new();
// How the ratio max_time / header_0x08 is distributed, in tenths.
let mut ratio: HashMap<u32, usize> = HashMap::new();
// Does the rate word co-vary with anything?
let mut by_rate: HashMap<u32, (usize, u32)> = HashMap::new(); // rate -> (bundles, max seen 0x08)
// The 16.16 frame-rate reading rests on twelve bundles at 30.0. If those are
// VARIANTS of 60.0 bundles - same elements, different rate - the reading
// gains a real discriminator; if they are unrelated one-offs it does not.
let mut odd_rate: Vec<String> = Vec::new();
for_each_build(&root, |pak, bytes| {
if bytes.len() < 0x20 {
return;
}
let Some(build) = ui_layout::parse_build(bytes) else {
return;
};
if build.from_fallback {
return;
}
let dur = be32(bytes, 0x08);
let rate = be32(bytes, 0x04);
let e = by_rate.entry(rate).or_default();
e.0 += 1;
e.1 = e.1.max(dur);
if rate != 0x3C_0000 && odd_rate.len() < 20 {
let names: Vec<&str> = build.elements.iter().map(|e| e.name.as_str()).take(6).collect();
odd_rate.push(format!(
"{pak}: rate {rate:#x} dur {dur} elements {} {:?}",
build.elements.len(),
names
));
}
let max_time = build
.elements
.iter()
.flat_map(|el| el.keyframes.iter())
.filter_map(|k| k.time)
.max();
let Some(max_time) = max_time else { return };
if dur == 0 {
return;
}
animated += 1;
if max_time <= dur {
within += 1;
if max_time == dur {
exact += 1;
}
} else {
over += 1;
if worst.len() < 10 {
worst.push(format!("{pak}: max keyframe {max_time} > header {dur}"));
}
}
let r = ((max_time as f64 / dur as f64) * 10.0).round() as u32;
*ratio.entry(r.min(30)).or_default() += 1;
});
eprintln!("bundles with keyframe times and a non-zero +0x08: {animated}");
eprintln!(" max keyframe time <= +0x08: {within} of which EXACTLY equal: {exact}");
eprintln!(" max keyframe time > +0x08: {over}");
let mut r: Vec<_> = ratio.iter().collect();
r.sort();
eprintln!(" ratio max_time/+0x08 (tenths -> bundles): {r:?}");
let mut br: Vec<_> = by_rate.iter().collect();
br.sort();
eprintln!(" +0x04 rate word -> (bundles, largest +0x08 seen): {br:?}");
for w in &worst {
eprintln!(" over: {w}");
}
for o in &odd_rate {
eprintln!(" non-60 rate: {o}");
}
assert!(animated > 0, "no animated bundles — the sweep is broken");
// MEASURED 2026-08-24. +0x08 bounds the keyframe times in EVERY one of the
// 2313 bundles that have both, and 444 of them reach it exactly. The
// spread-out ratio histogram is what rules out the boring explanation: a
// large unrelated constant would bound everything too, but then the ratios
// would pile up near zero instead of peaking at 1.0.
assert_eq!(over, 0, "a keyframe time runs past the header's +0x08");
assert!(exact > 400, "the bound is never attained — it may be unrelated");
let near_one = ratio.get(&10).copied().unwrap_or(0);
let near_zero = ratio.get(&0).copied().unwrap_or(0);
assert!(
near_one > near_zero * 4,
"the max_time/+0x08 ratio does not peak at 1.0 — the bound may be vacuous"
);
}