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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
sylph-decoder
2026-08-29 18:16:20 +00:00
parent 143912ea4f
commit 382112d63a
4 changed files with 179 additions and 1 deletions

View File

@@ -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 -- <pak> <entry> <name>
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),
}
}
}

View File

@@ -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<String>> = 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::<Vec<_>>().join(", "));
}
println!("\n* = not a whole multiple of 100%");
}

View File

@@ -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%

View File

@@ -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.
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.