From 126eeec4378cfba5c889a0d25790f9165dc3fc71 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Mon, 31 Aug 2026 06:16:09 +0000 Subject: [PATCH] re: kind bit 0x2 is the FOCUSABLE flag -- decoded, 0 violations in 15493 entries The declaration entry's kind word (+0x28) and its focus/nav index (+0x2C) are the same fact twice: kind & 0x2 is set iff the focus index is >= 0. Checked over 24 UI paks and every parseable build in each -- 1062 focusable elements, 14431 not, zero exceptions. The test is two-sided, so it would fail if any focusable element lacked the bit or any non-focusable element carried it. Consequence: kind == 0x3002 is not the test for a button. It catches 778 of 1062 and misses 284 (26.7 %) at 0x2, 0x2002, 0x3003, 0x73002, 0x73003 -- including ptbtn00.rat on GP_TITLE's PRESS (A) plate, which is 0x73002. And 0x3000, 817 elements, looks like a button and is not focusable. This is also the refutation attempt on sylpheed-port's kind census. Their claim -- every decoration 0x0, every button 0x3002 -- is exactly right on the two screens they checked, reproduced here independently, and fails one build over on the title they have not run yet. The other kind bits are reported as observed structure and explicitly not claimed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v --- .../examples/kind_census_five_screens.rs | 135 ++++++++++++++++++ docs/re/data/kind-focus-bit-census.txt | 89 ++++++++++++ docs/re/structures/ui-kind-focus-bit.md | 90 ++++++++++++ 3 files changed, 314 insertions(+) create mode 100644 crates/sylpheed-formats/examples/kind_census_five_screens.rs create mode 100644 docs/re/data/kind-focus-bit-census.txt create mode 100644 docs/re/structures/ui-kind-focus-bit.md 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.