formats: expose loop_length_units -- the port was reading +0x08 itself
sylpheed-port reports that a record's loop length is on no public ref at all (example, test and docs/re/ only), so their screen.rs parses the four bytes with its own RATC guard. That is my field to publish. One function serves both levels, since a nested .rat leaf is itself a RATC bundle with the same header shape. Returns None for a non-RATC or short slice so callers need no guard of their own. Verified against the disc, controls first: rejects a non-RATC slice, rejects one too short for the field, reads big-endian at +0x08 -- then reproduces every published value (ptbtn00f 120, ptloop01 600, ptloop02 720) over 65 GP_TITLE records with 0 violations of +0x08 >= largest keyframe time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
63
crates/sylpheed-formats/examples/record_loop_length_api.rs
Normal file
63
crates/sylpheed-formats/examples/record_loop_length_api.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! 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 sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use std::path::PathBuf;
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -498,6 +498,30 @@ fn opt_link(rec: &[u8]) -> Option<String> {
|
||||
(!s.is_empty()).then_some(s)
|
||||
}
|
||||
|
||||
/// A RATC record's animation **loop length** in keyframe units — its `+0x08`.
|
||||
///
|
||||
/// Works at either level: a nested `.rat` leaf is itself a RATC bundle with the
|
||||
/// same header shape as the one containing it, so this reads a whole screen
|
||||
/// build's length and a single record's length through one path.
|
||||
///
|
||||
/// **Why it is public.** The loop length is not the largest keyframe time —
|
||||
/// `ptbtn00f.rat`, the `PRESS Ⓐ` plate glow, declares **120** while its last
|
||||
/// keyframe is at **105**, and that 15-unit slack is the plate holding dark
|
||||
/// between cycles. A consumer that infers the period from the keyframes gets
|
||||
/// 105 (1.750 s) against a real pulse measured four times at 2.12–2.34 s.
|
||||
/// Decoded disc-wide: 1 781 records, **0** violations of
|
||||
/// `+0x08 >= largest keyframe time` — see
|
||||
/// `docs/re/structures/ui-record-loop-length.md`.
|
||||
///
|
||||
/// Returns `None` for anything that is not a RATC record, so it is safe to call
|
||||
/// on an arbitrary slice; callers do not need their own magic guard.
|
||||
pub fn loop_length_units(rec: &[u8]) -> Option<u32> {
|
||||
if rec.len() < 0x0c || rec[0..4] != *b"RATC" {
|
||||
return None;
|
||||
}
|
||||
Some(be32(rec, 0x08))
|
||||
}
|
||||
|
||||
/// The sprite a `.rat` record places: a NUL-terminated name at `0x20`.
|
||||
///
|
||||
/// The field is **not** 16 bytes. Capping it there truncates every longer name —
|
||||
|
||||
Reference in New Issue
Block a user