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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-31 06:16:09 +00:00
parent 63dce8daaf
commit 126eeec437
3 changed files with 314 additions and 0 deletions

View File

@@ -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<u32, BTreeMap<String, usize>> = 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<String> = 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<String> = 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<u32> = 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}"); }
}