Answers the question the port agent asked: does the `PRESS A` plate's pulse group loop from its start, or hold at alpha 0 between cycles? It holds. A nested record is itself a RATC bundle with its own header, and that header's `+0x08` is the loop length -- the same field ui_header_time_disc already tests as an animation length at the top level. Its keyframes need not fill it, and the slack is a hold at the final pose. `ptbtn00f` is 105 units of ramp inside a 120-unit cycle, so the glow rests dark for 15 units between pulses. The five main-menu focus records fill their 120 exactly, which is what shows the slack belongs to this record rather than to the format. Disc-wide over 1 781 timed nested records: 92.3% declare exactly their last keyframe time, 7.7% declare more, and 0 declare less. That last row is the falsifier -- a cycle cannot restart before its own last pose -- and it never fires; the 7.7% is what keeps the reading from being an unfalsifiable relabelling of the keyframes. Falsification against the running game, using a pacing factor measured INDEPENDENTLY on the main menu's focus ring (declared 120 units, measured 2.177 s, factor 1.0885): to reach the corpus's four measurements of the plate pulse (2.12/2.19/2.34/2.31 s), a 105-unit period needs a factor of 1.211-1.337, which EXCLUDES the ring's; a 120-unit period needs 1.060-1.170, which CONTAINS it. Predicted 2.177 s against a measured 2.12-2.34. The two elements are in different bundles and were measured in separate runs; the only thing tying them together is that both declare 120. So the port should stop shipping 105. Its 123-vs-129 ambiguity straddled the right answer without containing it, and 129 only fitted because it was 105 + the exit_ramp_units constant it has since correctly deleted. Reach is stated: this says where a cycle ends, not which records cycle, and the TOP-level +0x08 is a different field left untouched -- every GP_TITLE entry declares 300 while its elements end at 244-269. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
28 lines
1.3 KiB
Rust
28 lines
1.3 KiB
Rust
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});
|
|
}
|
|
}
|
|
}
|