diff --git a/crates/sylpheed-formats/examples/kind_census_five_screens.rs b/crates/sylpheed-formats/examples/kind_census_five_screens.rs new file mode 100644 index 00000000..271bc0a2 --- /dev/null +++ b/crates/sylpheed-formats/examples/kind_census_five_screens.rs @@ -0,0 +1,135 @@ +//! Refutation check on `sylpheed-port`'s kind census: *"Every sprite decoration +//! on both screens is `0x0` — `ptframe1`…`ptframe4` included — and every button +//! is `0x3002`."* +//! +//! Their exporter decodes the field independently; this reads it from the other +//! side. The check is deliberately WIDER than their claim in two ways, because a +//! census that only looks where the claim looks cannot fail: +//! +//! * it covers every element, not only `.t32` sprites, so a decoration with an +//! unexpected kind cannot hide behind the word "sprite"; +//! * it covers every build of `GP_TITLE`, not the two screens they checked, so +//! the claim's *reach* gets tested and not just its instances. +//! +//! It also cross-checks `kind` against the focus/nav index at `+0x2C`, which is +//! `-1` on anything that cannot take the cursor. That turns a census into a +//! decode: if the two fields agree everywhere, the bit that separates them is +//! identified rather than guessed. Run over EVERY UI pak on the disc, not just +//! `GP_TITLE`, so the claim is disc-wide. +//! +//! cargo run -p sylpheed-formats --example kind_census_five_screens +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::collections::BTreeMap; +use std::path::PathBuf; + +fn suffix(n: &str) -> &str { + match n.rfind('.') { Some(i) => &n[i..], None => "(none)" } +} + +fn main() { + 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"); + let n = ar.entries().len(); + // kind -> suffix -> count, and the exceptions we care about by name + let mut table: BTreeMap> = BTreeMap::new(); + let mut t32_nonzero: Vec<(usize, String, u32)> = Vec::new(); + let mut btn_nonstd: Vec<(usize, String, u32)> = Vec::new(); + let mut builds = 0usize; + for e in 0..n { + let by = match ar.read(&ar.entries()[e]) { Ok(b) => b, Err(_) => continue }; + let b = match ui_layout::parse_build(&by) { Some(b) => b, None => continue }; + builds += 1; + for el in &b.elements { + *table.entry(el.kind).or_default().entry(suffix(&el.name).to_string()).or_default() += 1; + if el.name.ends_with(".t32") && !el.name.contains("btn") && el.kind != 0 { + t32_nonzero.push((e, el.name.clone(), el.kind)); + } + if el.name.contains("btn") && el.kind != 0x3002 { + btn_nonstd.push((e, el.name.clone(), el.kind)); + } + } + } + println!("GP_TITLE: {builds} parseable builds of {n} entries\n"); + println!("{:<10} {}", "kind", "elements by file suffix"); + for (k, m) in &table { + let s: Vec = m.iter().map(|(sfx, c)| format!("{sfx}x{c}")).collect(); + println!("0x{k:<8X} {}", s.join(" ")); + } + println!("\nNON-BUTTON .t32 elements with kind != 0: {}", t32_nonzero.len()); + for (e, nm, k) in t32_nonzero.iter().take(30) { println!(" entry {e:>2} {nm:<24} kind 0x{k:X}"); } + println!("\n*btn* elements with kind != 0x3002: {}", btn_nonstd.len()); + for (e, nm, k) in btn_nonstd.iter().take(30) { println!(" entry {e:>2} {nm:<24} kind 0x{k:X}"); } + + // Is `kind` a bitfield, and does bit 0x2 mean "focusable"? The focus/nav + // index at +0x2C is -1 on everything that cannot take the cursor, so the two + // fields cross-check each other. Printed rather than asserted: this is the + // evidence for the reading, not the reading itself. + println!("\nkind vs the focus index at +0x2C (-1 = not focusable), GP_TITLE:"); + // +0x2C is not in `Element`, so it is read straight out of the 60-byte + // declaration entry: table at 0x20, 0x14 = count, entry stride 60. + const AT: usize = 0x20; + const STRIDE: usize = 60; + let mut cross: BTreeMap<(u32, i32), usize> = BTreeMap::new(); + for e in 0..n { + let by = match ar.read(&ar.entries()[e]) { Ok(b) => b, Err(_) => continue }; + if ui_layout::parse_build(&by).is_none() { continue } + if by.len() < 0x18 { continue } + let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize; + for i in 0..count { + let at = AT + i * STRIDE; + if at + STRIDE > by.len() { break } + let kind = u32::from_be_bytes([by[at+0x28], by[at+0x29], by[at+0x2A], by[at+0x2B]]); + let foc = i32::from_be_bytes([by[at+0x2C], by[at+0x2D], by[at+0x2E], by[at+0x2F]]); + *cross.entry((kind, if foc < 0 { -1 } else { 1 })).or_default() += 1; + } + } + for ((k, f), c) in &cross { + println!(" kind 0x{k:<6X} focus {:<10} {c:>4} elements", + if *f < 0 { "= -1" } else { ">= 0" }); + } + + // ── the same test, every UI pak on the disc ────────────────────────────── + let mut all: BTreeMap<(u32, i32), usize> = BTreeMap::new(); + let mut paks = 0usize; + let mut violations: Vec = Vec::new(); + let mut dir: Vec<_> = std::fs::read_dir(root.join("dat")).expect("dat") + .filter_map(|d| d.ok()).map(|d| d.path()).collect(); + dir.sort(); + for path in dir { + let name = path.file_name().unwrap().to_string_lossy().to_string(); + if !name.ends_with(".pak") { continue } + let ar = match PakArchive::open(&path) { Ok(a) => a, Err(_) => continue }; + let mut used = false; + for i in 0..ar.entries().len() { + let by = match ar.read(&ar.entries()[i]) { Ok(b) => b, Err(_) => continue }; + if ui_layout::parse_build(&by).is_none() { continue } + if by.len() < 0x18 { continue } + used = true; + let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize; + for e in 0..count { + let at = AT + e * STRIDE; + if at + STRIDE > by.len() { break } + let kind = u32::from_be_bytes([by[at+0x28], by[at+0x29], by[at+0x2A], by[at+0x2B]]); + let foc = i32::from_be_bytes([by[at+0x2C], by[at+0x2D], by[at+0x2E], by[at+0x2F]]); + let f = if foc < 0 { -1 } else { 1 }; + *all.entry((kind, f)).or_default() += 1; + if ((kind & 0x2) != 0) != (f > 0) { + violations.push(format!("{name} entry {i} elem {e}: kind 0x{kind:X} focus {foc}")); + } + } + } + if used { paks += 1 } + } + let total: usize = all.values().sum(); + println!("\nDISC-WIDE — {paks} UI paks, {total} declaration entries"); + println!("{:<12} {:>12} {:>12}", "kind", "focus = -1", "focus >= 0"); + let kinds: std::collections::BTreeSet = all.keys().map(|(k, _)| *k).collect(); + for k in kinds { + println!("0x{k:<10X} {:>12} {:>12}", + all.get(&(k, -1)).copied().unwrap_or(0), + all.get(&(k, 1)).copied().unwrap_or(0)); + } + println!("\nHYPOTHESIS: bit 0x2 of kind == (focus index >= 0)"); + println!("violations: {} of {total}", violations.len()); + for v in violations.iter().take(20) { println!(" {v}"); } +} diff --git a/docs/re/data/kind-focus-bit-census.txt b/docs/re/data/kind-focus-bit-census.txt new file mode 100644 index 00000000..829371b9 --- /dev/null +++ b/docs/re/data/kind-focus-bit-census.txt @@ -0,0 +1,89 @@ +# `kind` bit 0x2 is the FOCUSABLE flag -- decoded, disc-wide. 2026-08-31. +# +# cargo run -p sylpheed-formats --example kind_census_five_screens (SYLPHEED_DISC=/disc) +# +# The declaration entry's kind word (+0x28) and its focus/nav index (+0x2C) are +# two fields nobody had cross-checked. The index is -1 on anything that cannot +# take the cursor, so if the two agree everywhere, the bit that separates them is +# IDENTIFIED rather than guessed. +# +# bit 0x2 of kind == (focus index >= 0) 0 violations in 15493 entries +# +# 24 UI paks, every parseable build in each. 1062 focusable elements, 14431 not. +# +# WHY IT MATTERS: `kind == 0x3002` is NOT the test for a button. It catches 778 +# of 1062 focusable elements and MISSES 284 -- 26.7 % -- at 0x2, 0x2002, 0x3003, +# 0x73002 and 0x73003. On GP_TITLE that is `ptbtn00.rat` on the PRESS (A) plate +# (entries 2 and 3), which is 0x73002. And 0x3000 (817 elements) resembles +# 0x3002 and is NOT focusable. +# +# The low bits look like independent flags -- 0x1 with a parent, 0x4 a repeated +# template instance, 0x10 a primitive -- and 0x2000/0x3000/0x70000 like a group +# in the high half. Only bit 0x2 is decoded here; the rest is observed structure +# and is NOT claimed. + +GP_TITLE: 16 parseable builds of 16 entries + +kind elements by file suffix +0x0 .ratx27 .t32x102 +0x1 .t32x2 +0x4 .t32x8 +0x10 .prmx18 +0x3000 .t32x3 +0x3002 .ratx16 +0x73002 .ratx2 + +NON-BUTTON .t32 elements with kind != 0: 13 + entry 4 ptlogo1.t32 kind 0x4 + entry 4 ptlogo2.t32 kind 0x4 + entry 4 ptlogo1.t32 kind 0x4 + entry 4 ptlogo2.t32 kind 0x4 + entry 4 ptlogoall_eff.t32 kind 0x3000 + entry 4 ptlogoall_eff2.t32 kind 0x3000 + entry 7 ptlogo1.t32 kind 0x4 + entry 7 ptlogo2.t32 kind 0x4 + entry 7 ptlogo1.t32 kind 0x4 + entry 7 ptlogo2.t32 kind 0x4 + entry 7 ptlogo_all_eff.t32 kind 0x3000 + entry 10 palogo_sqex_eff.t32 kind 0x1 + entry 13 palogo_sqex_eff.t32 kind 0x1 + +*btn* elements with kind != 0x3002: 2 + entry 2 ptbtn00.rat kind 0x73002 + entry 3 ptbtn00.rat kind 0x73002 + +kind vs the focus index at +0x2C (-1 = not focusable), GP_TITLE: + kind 0x0 focus = -1 129 elements + kind 0x1 focus = -1 2 elements + kind 0x4 focus = -1 8 elements + kind 0x10 focus = -1 18 elements + kind 0x3000 focus = -1 3 elements + kind 0x3002 focus >= 0 16 elements + kind 0x73002 focus >= 0 2 elements + +DISC-WIDE — 24 UI paks, 15493 declaration entries +kind focus = -1 focus >= 0 +0x0 7459 0 +0x1 1093 0 +0x2 0 16 +0x4 2964 0 +0x5 282 0 +0x8 650 0 +0x9 6 0 +0xC 48 0 +0x10 329 0 +0x14 2 0 +0x2002 0 16 +0x3000 817 0 +0x3001 10 0 +0x3002 0 778 +0x3003 0 192 +0x3004 426 0 +0x3008 72 0 +0x300C 135 0 +0x3010 38 0 +0x73002 0 64 +0x73003 0 96 + +HYPOTHESIS: bit 0x2 of kind == (focus index >= 0) +violations: 0 of 15493 diff --git a/docs/re/structures/ui-kind-focus-bit.md b/docs/re/structures/ui-kind-focus-bit.md new file mode 100644 index 00000000..fb75385d --- /dev/null +++ b/docs/re/structures/ui-kind-focus-bit.md @@ -0,0 +1,90 @@ +# `kind` bit `0x2` is the FOCUSABLE flag — ✅ decoded, 0 violations disc-wide + +**Status:** ✅ `CONFIRMED`, classification **decoded** — the field, plus a +disc-wide check. 2026-08-31. + +## The claim + +In the 60-byte UI declaration entry, `+0x28` is the **kind** word and `+0x2C` is +the **focus/nav index** (`−1` on anything that cannot take the cursor). They are +the same fact twice: + +> **`kind & 0x2` is set if and only if the focus index is ≥ 0.** + +**0 violations in 15 493 declaration entries across 24 UI paks** — every parseable +build in each. 1 062 focusable elements, 14 431 not. + +[`data/kind-focus-bit-census.txt`](../data/kind-focus-bit-census.txt) · +[`examples/kind_census_five_screens.rs`](../../../crates/sylpheed-formats/examples/kind_census_five_screens.rs) + +| kind | focus `= −1` | focus `≥ 0` | | kind | focus `= −1` | focus `≥ 0` | +|---|---|---|---|---|---|---| +| `0x0` | 7 459 | 0 | | `0x2` | 0 | **16** | +| `0x1` | 1 093 | 0 | | `0x2002` | 0 | **16** | +| `0x4` | 2 964 | 0 | | `0x3002` | 0 | **778** | +| `0x5` | 282 | 0 | | `0x3003` | 0 | **192** | +| `0x8` | 650 | 0 | | `0x73002` | 0 | **64** | +| `0x9` | 6 | 0 | | `0x73003` | 0 | **96** | +| `0xC` | 48 | 0 | | | | | +| `0x10` | 329 | 0 | | | | | +| `0x14` | 2 | 0 | | | | | +| `0x3000` | **817** | 0 | | | | | +| `0x3001` | 10 | 0 | | | | | +| `0x3004` | 426 | 0 | | | | | +| `0x3008` | 72 | 0 | | | | | +| `0x300C` | 135 | 0 | | | | | +| `0x3010` | 38 | 0 | | | | | + +Every value in the right-hand column has bit `0x2`; no value in the left-hand +column does. + +## 🔴 So `kind == 0x3002` is not the test for a button + +It catches **778 of 1 062** focusable elements and **misses 284 — 26.7 %**, at +`0x2`, `0x2002`, `0x3003`, `0x73002` and `0x73003`. And `0x3000`, which looks +like a button and appears **817** times, is **not** focusable. + +**On the screens the port ships this is not hypothetical.** `GP_TITLE` entries 2 +and 3 — the `PRESS Ⓐ BUTTON` plate composited over the title — declare +`ptbtn00.rat` as **`0x73002`**. An equality test drops it. So does the title's +`ptlogoall_eff.t32` / `ptlogoall_eff2.t32` at `0x3000` go the other way: an +equality test correctly leaves them out, but a `kind >= 0x3000` test would not. + +## Why this is a decode and not a correlation + +The two fields are independent bytes four apart in a record nobody wrote them +into together, and the test is **two-sided**: it would fail if any focusable +element lacked the bit *or* if any non-focusable element carried it. A +one-directional check ("every button has the bit") would have been satisfied by +the bit simply being common. + +⚠️ **What is NOT claimed.** The other bits look like independent flags — `0x1` +where the crate reads "has a parent", `0x4` "a repeated instance of a template", +`0x10` a primitive — and `0x2000` / `0x3000` / `0x70000` like a group in the high +half. That is **observed structure, not decoded**: nothing here tests them, and +the `0x5`/`0x9`/`0xC`/`0x14`/`0x3001`/`0x300C` combinations are consistent with +flags without establishing them. + +⚠️ **Reach.** Every `.pak` under `dat/` that `parse_build` accepts — 24 archives. +It says nothing about whether a *focusable* element is reachable by the cursor at +run time, only that the declaration marks it. + +## Refutation attempt on `sylpheed-port` — survives in its scope, fails past it + +Their `DECISIONS.md` records: *"Every sprite decoration on both screens is `0x0` — +`ptframe1`…`ptframe4` included — and every button is `0x3002`."* + +✅ **On `GP_TITLE` entries 5 and 6, both halves are exactly right**, and this +census reproduces them independently — a third reading of the same field after +their exporter and my earlier declaration walk. + +🔴 **One build over, both halves fail.** The census above is deliberately wider +than the claim in two ways, because a census that only looks where the claim +looks cannot fail it: it covers every element rather than only `.t32` sprites, and +every build rather than the two screens. Entry 4 — the **title**, which they say +they have not run yet — puts `ptlogo1`/`ptlogo2` at `0x4` and +`ptlogoall_eff`/`ptlogoall_eff2` at `0x3000`, and entries 2/3 put a button at +`0x73002`. + +The claim was true of what it examined. What is refuted is its **reach**, and the +reach is what was about to be used.