formats: the declaration entry does not mark a focused state - swept and asserted
The backlog's cheapest open question about the 60-byte entry: kind is a flags word (0x10 untextured primitive, 0x4 repeated instance, 0x3002 button record), so a focus bit would be the obvious answer and the name-pairing in mark_focused_states would be a convention standing in for a real field. It is not. Over every screen build on the disc: 54 name-paired focused/base pairs, all 54 with IDENTICAL kind (all 0x0), NO bit ever set on the focused entry and clear on its base, and the only words of the entry that ever differ are +48 and +52 - the pivot. The two entries differ in where the sprite sits and in nothing else. The test asserts all four numbers rather than just printing them, so the negative is pinned instead of decaying back into a suspicion. Also noted: these buttons carry kind = 0x0, so the documented 0x3002 "button record" belongs to the .rat records and not to the .t32 sprites a menu draws. Closes the declaration table, not the question - the focused state could still be marked in the .rat record, the RATC child stream, or only in the game's code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
171
crates/sylpheed-formats/tests/ui_focus_kind_disc.rs
Normal file
171
crates/sylpheed-formats/tests/ui_focus_kind_disc.rs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
//! Does anything in the declaration entry mark a **focused** button state, or is
|
||||||
|
//! the pairing really just a naming convention?
|
||||||
|
//!
|
||||||
|
//! `mark_focused_states` pairs `pgmenu_btn00f.t32` with `pgmenu_btn00.t32` by
|
||||||
|
//! name, and the backlog has carried the obvious suspicion ever since: `kind` is
|
||||||
|
//! a flags word — `0x10` means untextured primitive, `0x4` a repeated instance,
|
||||||
|
//! `0x3002` a button record — so a focus bit would be the cheapest possible
|
||||||
|
//! answer. This sweeps every screen build on the disc and asks the question
|
||||||
|
//! exactly, rather than plausibly.
|
||||||
|
//!
|
||||||
|
//! It is written to be able to FAIL to find anything: the interesting outcome is
|
||||||
|
//! as likely to be "no bit distinguishes them" as "here is the bit", and either
|
||||||
|
//! way the number is what goes in the docs.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
|
||||||
|
|
||||||
|
const DECL_TABLE_AT: usize = 0x20;
|
||||||
|
const DECL_ENTRY: usize = 60;
|
||||||
|
|
||||||
|
fn disc_root() -> Option<PathBuf> {
|
||||||
|
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
|
||||||
|
let p = PathBuf::from(p);
|
||||||
|
if p.join("dat").is_dir() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let default = Path::new(
|
||||||
|
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
|
||||||
|
);
|
||||||
|
if default.join("dat").is_dir() {
|
||||||
|
return Some(default.to_path_buf());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) {
|
||||||
|
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))
|
||||||
|
.expect("dat/")
|
||||||
|
.flatten()
|
||||||
|
.map(|e| e.path())
|
||||||
|
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak"))
|
||||||
|
.collect();
|
||||||
|
paks.sort();
|
||||||
|
for p in &paks {
|
||||||
|
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||||
|
for e in arc.entries() {
|
||||||
|
let Ok(bytes) = arc.read(e) else { continue };
|
||||||
|
if ratc::is_ratc(&bytes) {
|
||||||
|
f(&name, &bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_at(bundle: &[u8], index: usize, off: usize) -> Option<u32> {
|
||||||
|
let at = DECL_TABLE_AT + index * DECL_ENTRY + off;
|
||||||
|
bundle
|
||||||
|
.get(at..at + 4)
|
||||||
|
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare every name-paired (focused, base) pair on the disc, word by word.
|
||||||
|
#[test]
|
||||||
|
fn focused_and_base_declaration_entries_are_compared_word_by_word() {
|
||||||
|
let Some(root) = disc_root() else {
|
||||||
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pairs = 0usize;
|
||||||
|
// For each unread word of the 60-byte entry, how often does it differ?
|
||||||
|
let mut differs: HashMap<usize, usize> = HashMap::new();
|
||||||
|
// kind bits that are set on the focused entry and clear on its base.
|
||||||
|
let mut set_on_focused: HashMap<u32, usize> = HashMap::new();
|
||||||
|
let mut kind_equal = 0usize;
|
||||||
|
let mut examples: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
for_each_build(&root, |pak, bytes| {
|
||||||
|
let Some(build) = ui_layout::parse_build(bytes) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if build.from_fallback {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let index_of: HashMap<String, usize> = build
|
||||||
|
.elements
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, e)| (e.name.to_ascii_lowercase(), i))
|
||||||
|
.collect();
|
||||||
|
for (i, el) in build.elements.iter().enumerate() {
|
||||||
|
if !el.focused {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let l = el.name.to_ascii_lowercase();
|
||||||
|
let Some((stem, ext)) = l.rsplit_once('.') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let base_name = format!("{}.{}", &stem[..stem.len() - 1], ext);
|
||||||
|
let Some(&j) = index_of.get(&base_name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
pairs += 1;
|
||||||
|
let base = &build.elements[j];
|
||||||
|
if el.kind == base.kind {
|
||||||
|
kind_equal += 1;
|
||||||
|
} else {
|
||||||
|
let only_focused = el.kind & !base.kind;
|
||||||
|
for bit in 0..32 {
|
||||||
|
if only_focused & (1 << bit) != 0 {
|
||||||
|
*set_on_focused.entry(1 << bit).or_default() += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for off in (0..DECL_ENTRY).step_by(4) {
|
||||||
|
if off < 28 {
|
||||||
|
continue; // the name
|
||||||
|
}
|
||||||
|
let (a, b) = (word_at(bytes, i, off), word_at(bytes, j, off));
|
||||||
|
if a.is_some() && a != b {
|
||||||
|
*differs.entry(off).or_default() += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if examples.len() < 6 {
|
||||||
|
examples.push(format!(
|
||||||
|
"{pak}: {} kind {:#x} vs base {} kind {:#x}",
|
||||||
|
el.name, el.kind, base.name, base.kind
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eprintln!("focused/base pairs on the disc: {pairs}");
|
||||||
|
eprintln!("pairs whose `kind` is IDENTICAL: {kind_equal}");
|
||||||
|
let mut bits: Vec<_> = set_on_focused.iter().collect();
|
||||||
|
bits.sort();
|
||||||
|
eprintln!("kind bits set on focused but not base: {bits:?}");
|
||||||
|
let mut d: Vec<_> = differs.iter().collect();
|
||||||
|
d.sort();
|
||||||
|
eprintln!("declaration words that ever differ (offset -> pairs): {d:?}");
|
||||||
|
for e in &examples {
|
||||||
|
eprintln!(" {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(pairs > 0, "no focused/base pairs found — the sweep is broken");
|
||||||
|
|
||||||
|
// MEASURED 2026-08-24, and asserted so the answer cannot rot back into a
|
||||||
|
// suspicion: all 54 pairs on the disc carry the SAME `kind`, no bit is ever
|
||||||
|
// set on the focused entry and clear on its base, and the only words of the
|
||||||
|
// 60-byte declaration entry that ever differ are +48 and +52 — the PIVOT,
|
||||||
|
// i.e. where the sprite sits, not what it is. So the entry does not mark a
|
||||||
|
// focused state at all, and the naming pairing is not a shortcut around a
|
||||||
|
// field that exists: there is no field.
|
||||||
|
assert_eq!(pairs, 54, "the disc has 54 name-paired focused elements");
|
||||||
|
assert_eq!(kind_equal, pairs, "some pair's `kind` differs from its base");
|
||||||
|
assert!(
|
||||||
|
set_on_focused.is_empty(),
|
||||||
|
"a kind bit distinguishes focused from base: {set_on_focused:?}"
|
||||||
|
);
|
||||||
|
let mut offs: Vec<usize> = differs.keys().copied().collect();
|
||||||
|
offs.sort();
|
||||||
|
assert_eq!(
|
||||||
|
offs,
|
||||||
|
vec![48, 52],
|
||||||
|
"a declaration word other than the pivot differs between focused and base"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -422,3 +422,33 @@ decoded 265×198, declared pivot 19,18), so the rule is not merely a
|
|||||||
language-inheritance artefact. **Do not derive a texture size from a pivot**; the
|
language-inheritance artefact. **Do not derive a texture size from a pivot**; the
|
||||||
capture backs the decoded size, not the pivot — `ptcopyright` is 694 px wide in
|
capture backs the decoded size, not the pivot — `ptcopyright` is 694 px wide in
|
||||||
the framebuffer, and `pivot·2` says 618.
|
the framebuffer, and `pivot·2` says 618.
|
||||||
|
|
||||||
|
## 🔴 The declaration entry does NOT mark a focused state (2026-08-24)
|
||||||
|
|
||||||
|
The backlog carried this as the cheapest open question about the entry: `kind` is
|
||||||
|
a flags word — `0x10` is an untextured primitive, `0x4` a repeated instance,
|
||||||
|
`0x3002` a button record — so a *focus* bit would be the obvious answer, and the
|
||||||
|
name-pairing in `mark_focused_states` (`pgmenu_btn00f.t32` next to
|
||||||
|
`pgmenu_btn00.t32`) would be a convention standing in for a real field.
|
||||||
|
|
||||||
|
**It is not standing in for anything.** Swept over every screen build on the disc
|
||||||
|
(`tests/ui_focus_kind_disc.rs`, asserted so it cannot rot back into a suspicion):
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| name-paired focused/base pairs on the disc | **54** |
|
||||||
|
| pairs whose `kind` is **identical** | **54** — all of them, and all `kind = 0x0` |
|
||||||
|
| `kind` bits set on the focused entry and clear on its base | **none, on any pair** |
|
||||||
|
| words of the 60-byte entry that ever differ | **`+48` and `+52` only** — the pivot |
|
||||||
|
|
||||||
|
So the two entries differ in *where the sprite sits* and in nothing else. There
|
||||||
|
is no focus field in the declaration table, and the naming pairing is the only
|
||||||
|
signal the file gives.
|
||||||
|
|
||||||
|
Worth noting in passing: these buttons carry `kind = 0x0`, so the documented
|
||||||
|
`0x3002` "button record" belongs to the `.rat` records, not to the `.t32` sprites
|
||||||
|
that a menu draws for its buttons.
|
||||||
|
|
||||||
|
**Still open:** whether the focused state is marked anywhere *else* — the `.rat`
|
||||||
|
record, the RATC child stream, or (as with the paint order) only in the game's
|
||||||
|
code. This closes the declaration table, not the question.
|
||||||
|
|||||||
Reference in New Issue
Block a user