From 382112d63a0fddde77fff957ab59152659d04e7e Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 29 Aug 2026 18:16:20 +0000 Subject: [PATCH] re: 125 percent is not the only odd scale, and ptlogo_eff2 is a pop not a steady state Refutes a DECISIONS claim and decodes the element the port withheld on my say-so. The claim that title_jp s ptlogo_eff2 at 125 percent is the single drawn element in the whole export at a scale that is not a whole multiple of 100 percent rested on a census of PARENTS only. Opening the 45 leaves as well finds thirteen distinct non-whole-multiple scales -- 75, 96, 99, 101, 103, 112, 125, 150, 204x208, 210x220, 250, and the 75x100 / 96x100 / 99x100 pairs -- with 125 among the rarest at two occurrences. ptlogo1 and ptlogo2 carry 101/103/112 on the ENGLISH title too, so it is not a Japanese-build peculiarity. The claim s real content was "the only one the port draws", which is about the export s element set rather than the disc. And ptlogo_eff2 is decoded. The 125 percent lasts 57 units, about 0.95 s -- a scale-0 to 125 to scale-0 flash between t=50 and t=107, a transient rather than a steady state, which is why it looked anomalous in a census of resting poses. The leaf draws at 100 percent as two superimposed copies of the same sprite at alpha 160 and 80, each rotating a full 360 degrees over 960 units: a slow double-layered spin, 16 s per revolution. This is exactly the case the ptloop rule could not separate. There the parent had expired so leaf-wins and parent-ignored were indistinguishable; here the parent carries real geometry including a scale that reaches zero twice. If parent scale gates the leaf the spin is a 0.95 s flash; if the leaf runs on its own timeline it spins for 16 s. Nothing on the disc chooses between them, and title_jp has no oracle capture, so it is undecodable in this container -- the port is right to withhold it, and the Japanese-locale capture MISSION has parked would settle it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd --- crates/sylpheed-formats/examples/leaf_dump.rs | 42 +++++++++++++ .../sylpheed-formats/examples/scale_census.rs | 48 +++++++++++++++ docs/re/data/ui-scale-census-with-leaves.txt | 30 ++++++++++ docs/re/structures/ui-leaf-vs-parent-alpha.md | 60 ++++++++++++++++++- 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 crates/sylpheed-formats/examples/leaf_dump.rs create mode 100644 crates/sylpheed-formats/examples/scale_census.rs create mode 100644 docs/re/data/ui-scale-census-with-leaves.txt diff --git a/crates/sylpheed-formats/examples/leaf_dump.rs b/crates/sylpheed-formats/examples/leaf_dump.rs new file mode 100644 index 00000000..40dff320 --- /dev/null +++ b/crates/sylpheed-formats/examples/leaf_dump.rs @@ -0,0 +1,42 @@ +//! Dump a record's parent keyframes and its nested `.rat` leaf's, for any entry. +//! +//! cargo run -p sylpheed-formats --example leaf_dump -- +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), + } + } +} diff --git a/crates/sylpheed-formats/examples/scale_census.rs b/crates/sylpheed-formats/examples/scale_census.rs new file mode 100644 index 00000000..e55a8f94 --- /dev/null +++ b/crates/sylpheed-formats/examples/scale_census.rs @@ -0,0 +1,48 @@ +//! 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 sylpheed_formats::{pak, ui_layout}; +use std::collections::BTreeMap; + +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> = 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::>().join(", ")); + } + println!("\n* = not a whole multiple of 100%"); +} diff --git a/docs/re/data/ui-scale-census-with-leaves.txt b/docs/re/data/ui-scale-census-with-leaves.txt new file mode 100644 index 00000000..53277b9a --- /dev/null +++ b/docs/re/data/ui-scale-census-with-leaves.txt @@ -0,0 +1,30 @@ +45 leaves opened + + scale count examples + 0,0 12 e0/pgloading_loop4.rat, e1/pgloading_loop4.rat, e12/pgloading_loop4.rat + 0,100 4 e0/pgloading_line.t32, e1/pgloading_line.t32, e12/pgloading_line.t32 +* 75,75 4 e0/pgloading_loop4.rat, e1/pgloading_loop4.rat, e12/pgloading_loop4.rat +* 75,100 4 e0/pgloading_line.t32, e1/pgloading_line.t32, e12/pgloading_line.t32 +* 96,96 4 e0/pgloading_loop4.rat, e1/pgloading_loop4.rat, e12/pgloading_loop4.rat +* 96,100 4 e0/pgloading_line.t32, e1/pgloading_line.t32, e12/pgloading_line.t32 +* 99,99 4 e0/pgloading_loop4.rat, e1/pgloading_loop4.rat, e12/pgloading_loop4.rat +* 99,100 4 e0/pgloading_line.t32, e1/pgloading_line.t32, e12/pgloading_line.t32 + 100,100 759 e0/pgloading_eff01.t32, e0/pgloading_eff02.t32, e0/pgloading_line.t32 + 100,600 24 e4/ptloop01.rat->LEAF/pteff03.t32, e5/ptloop01.rat->LEAF/pteff03.t32, e6/ptloop01.rat->LEAF/pteff03.t32 + 100,800 24 e4/ptloop02.rat->LEAF/pteff03a.t32, e5/ptloop02.rat->LEAF/pteff03a.t32, e6/ptloop02.rat->LEAF/pteff03a.t32 +* 101,101 12 e4/ptlogo1.t32, e4/ptlogo2.t32, e7/ptlogo1.t32 +* 103,103 12 e4/ptlogo1.t32, e4/ptlogo2.t32, e7/ptlogo1.t32 +* 112,112 12 e4/ptlogo1.t32, e4/ptlogo2.t32, e7/ptlogo1.t32 +* 125,125 2 e7/ptlogo_eff2.rat +* 150,150 28 e0/pgloading_loop1.rat, e1/pgloading_loop1.rat, e12/pgloading_loop1.rat + 200,200 46 e12/pgloading_baseeff.t32, e15/pgloading_baseeff.t32, e4/ptbase2.t32 + 200,500 20 e5/pteff10.t32, e6/pteff10.t32, e8/pteff10.t32 +* 204,208 1 e4/ptlogoall_eff.t32 +* 210,220 1 e4/ptlogoall_eff.t32 +* 250,250 2 e12/pgloading_loop5.rat->LEAF/pgloading_ring.t32, e15/pgloading_loop5.rat->LEAF/pgloading_ring.t32 + 300,100 2 e6/pteff21.t32, e9/pteff21.t32 + 400,400 5 e4/ptlogoall_eff2.t32 + 800,800 2 e12/pgloading_loop5.rat->LEAF/pgloading_ring.t32, e15/pgloading_loop5.rat->LEAF/pgloading_ring.t32 + 1000,1000 4 e12/pgloading_loop5.rat->LEAF/pgloading_ring.t32, e15/pgloading_loop5.rat->LEAF/pgloading_ring.t32 + +* = not a whole multiple of 100% diff --git a/docs/re/structures/ui-leaf-vs-parent-alpha.md b/docs/re/structures/ui-leaf-vs-parent-alpha.md index a9f46468..749d7232 100644 --- a/docs/re/structures/ui-leaf-vs-parent-alpha.md +++ b/docs/re/structures/ui-leaf-vs-parent-alpha.md @@ -110,4 +110,62 @@ one.** ❔ The residual **11.5 px** between 980.5 and 992.0 is not explained by this and is left open — a rotation about a pivot rather than the centre would displace by -about that much, and nothing here measures it. \ No newline at end of file +about that much, and nothing here measures it. + +--- + +## 🔴 Refutation — "125 % is the only non-whole-multiple scale" is WRONG, and by a lot + +`DECISIONS.md` records `title_jp`'s `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**. Opening the 45 leaves as well +(`--example scale_census`, output at +[`data/ui-scale-census-with-leaves.txt`](../data/ui-scale-census-with-leaves.txt)): + +| scale | count | where | +|---|---|---| +| **75,75** · **96,96** · **99,99** | 4 each | `pgloading_loop4.rat` on all four loading screens | +| **75,100** · **96,100** · **99,100** | 4 each | `pgloading_line.t32` | +| **101,101** · **103,103** · **112,112** | 12 each | `ptlogo1` / `ptlogo2`, entries 4 **and** 7 | +| **150,150** | 28 | `pgloading_loop1.rat` | +| **204,208** · **210,220** | 1 each | `ptlogo_eff2` … no: `ptlogoall_eff.t32`, entry 4 | +| **250,250** | 2 | `pgloading_loop5.rat` → **LEAF** `pgloading_ring.t32` | +| **125,125** | **2** | `ptlogo_eff2.rat`, entry 7 | + +**Thirteen distinct non-whole-multiple scales, and 125 % is among the rarest at +2 occurrences.** `ptlogo1`/`ptlogo2` carry 101/103/112 on the **English** title +too, so this is not a Japanese-build peculiarity. ⚠️ The claim's real content was +"the only one *the port draws*", which is a statement about the export's element +set, not about the disc. + +## ✅ And `ptlogo_eff2` itself is decoded — the 125 % is a POP, not a steady scale + +``` +PARENT ptlogo_eff2.rat pivot (169,169) + t=0 a=0 scale (100,100) pos (412,96) + t=50 a=0 scale (0,0) + t=59 a=255 scale (125,125) + t=71 a=255 scale (125,125) + t=107 a=0 scale (0,0) +LEAF ptlogo_eff2.t32 kind 0x8 a=160 scale (100,100) rot 0 → 360 over t=0…960 +LEAF ptlogo_eff2.t32 kind 0xc a=80 scale (100,100) rot 0 → 360 over t=0…960 +``` + +* The **125 % lasts 57 units (~0.95 s)** — a scale-0 → 125 % → scale-0 flash + between t=50 and t=107. It is a transient, not a steady state, which is why it + looks anomalous in a census of resting poses. +* The **leaf draws at 100 %**, as **two superimposed copies** of the same sprite + at alpha **160** and **80**, each rotating **a full 360° over 960 units** — a + slow double-layered spin, 16 s per revolution at 60 units/s. + +🔴 **And this is exactly the case my `ptloop` rule could NOT separate.** There the +parent had expired (alpha 0, no sprite) so "leaf wins" and "parent ignored" were +indistinguishable. Here the parent carries **real geometry** — a scale that +reaches 0 twice. If parent scale gates the leaf, the spin is a 0.95 s flash; if +the leaf runs on its own timeline, it spins continuously for 16 s. **The two +readings differ enormously and nothing on the disc chooses between them.** + +❔ **Undecodable here, with reach:** `title_jp` has **no oracle capture**, so this +cannot be adjudicated in this container at all. The port is right to withhold it. +A capture of the Japanese title would settle it — and that is the same +Japanese-locale capture MISSION has parked as 🟡 since 2026-08-29. \ No newline at end of file