diff --git a/crates/sylpheed-export/examples/static_with_cycle.rs b/crates/sylpheed-export/examples/static_with_cycle.rs new file mode 100644 index 00000000..0e8b114c --- /dev/null +++ b/crates/sylpheed-export/examples/static_with_cycle.rs @@ -0,0 +1,46 @@ +//! Do any screens THIS PORT SHIPS carry a record that declares a cycle while all +//! its poses sit at t = 0? +//! +//! The substantive finding from the denominator thread: 1 530 nested records +//! disc-wide are timed with every pose at t = 0 and still declare a nonzero +//! `+0x08`. A static record that declares a cycle length is a real thing, not a +//! counting artefact — so the question for the port is whether it holds one of +//! those still while the disc says it cycles. +//! +//! Scoped to `GP_TITLE`, because that is the archive the port exports. +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_TITLE.pak")).expect("GP_TITLE.pak"); + let (mut total, mut hits, mut multipose) = (0usize, 0usize, 0usize); + 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 }; + for (name, &(o, s)) in &b.records { + if o + 12 > by.len() || o + s > by.len() || &by[o..o + 4] != b"RATC" { continue } + 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); + let len = ui_layout::loop_length_units(&by[o..o + s]).unwrap_or(0); + total += 1; + if maxt == 0 && len > 0 { + hits += 1; + // A cycle can only produce motion if there is more than one pose + // to move between. All-at-t=0 with a single keyframe per element + // is visually inert however it is played. + let kf: usize = lb.elements.iter().map(|el| el.keyframes.len()).sum(); + let multi = lb.elements.iter().filter(|el| el.keyframes.len() > 1).count(); + if multi > 0 { multipose += 1 } + println!(" entry {i:>2} {name:<16} {len}-unit cycle, {kf} keyframe(s) \ +across {} element(s), {multi} with >1 pose", lb.elements.len()); + } + } + } + println!("\n {total} nested record(s) in GP_TITLE; {hits} declare a cycle while static."); + println!(" Of those, {multipose} have an element with MORE THAN ONE pose -- the only"); + println!(" ones where looping could differ visibly from holding. A record whose"); + println!(" elements each carry a single pose renders identically either way, so a"); + println!(" declared cycle there is inert rather than a defect."); +} diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index ecd3ae72..a09a09e6 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -310 sections. Search this before re-deriving anything. +311 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -321,6 +321,7 @@ dies, which is what this file is for. * [🔴 My falsifier never identified the offset — the half I called a formality did](#my-falsifier-never-identified-the-offset--the-half-i-called-a-formality-did) * [The 92.3 %-versus-49.6 % gap: same numerator, and their filter is not applied](#the-923--versus-496--gap-same-numerator-and-their-filter-is-not-applied) * [🔴 Correcting my own correction: none of the 1 530 is a question without content](#correcting-my-own-correction-none-of-the-1-530-is-a-question-without-content) +* [The one load-bearing thing in the denominator thread, checked against the port](#the-one-load-bearing-thing-in-the-denominator-thread-checked-against-the-port) ## P0 — the exporter, 2026-08-28 @@ -15302,3 +15303,40 @@ throughout; every disagreement has been about what they were counting.** equals the largest keyframe time exactly where that time is nonzero, `+0x04` does so **0 %** of the time under either denominator, and the offset identification stands on both scans. + +## The one load-bearing thing in the denominator thread, checked against the port + +Their substantive point was not about counting: **a static record still declares a +cycle length**, and a nonzero `+0x08` against a largest keyframe time of 0 is a +real disagreement. That is a *rendering* question for this port, and it had not +been asked. + +Scoped to the archive the port exports: + +| | | +|---|---| +| nested records in `GP_TITLE` | **65** | +| declaring a cycle while every pose sits at t = 0 | **20** | +| **of those, with any element carrying more than one pose** | **0** | + +✅ **So the declared cycle is visually inert on every one of them.** A record whose +elements each hold a single pose renders identically whether looped or held — +there is nothing to move between. **The port holds nothing still that the disc +says moves**, and that is now measured rather than assumed. + +⚠️ It includes `ptbtn11`/`12`/`13` — EXTRAS' own buttons — declaring 120-unit +cycles. Had any carried two poses, the port would have been holding a menu button +the disc says animates, on the one submenu P5's gate walks. **The check cost one +scan and the answer could have gone the other way.** + +📌 **And this is the thread's whole yield stated honestly.** Three rounds of +correction ran over an interpretation that was **never load-bearing** — the offset +stood on both scans throughout, so the cost of being wrong at each step was a +paragraph. What came out of it that was worth having: the **population +distinction**, and this one check, which exists because they pushed on what the +1 530 *mean* rather than on how they are counted. + +⚠️ Their framing of why it was safe is the caveat I would attach to repeating it: +**nothing the port depends on moved at any point.** That made three rounds cheap. +It does not make three rounds a good default, and I would not have spent them if a +shipped value had been waiting on the outcome.