re: a .t32 element carries no blend/alpha mode -- undecodable, with reach
Answers the port's ask about ptframe1/ptframe2, whose residual is uniquely higher on flat pixels than edges and signed one direction -- a body-intensity difference. Prior work covers .prm primitives and a refuted T8aD +0x04 bit; neither covers a .t32 element. All 15 words of the 60-byte declaration entry are read: 3 are the name, 8 constant, the rest kind, focus index, position and pivot. The frames are kind 0, identical to every other plain sprite. One candidate found and refuted by myself: T8aD +0x08 is the only word where both frames agree uniquely on that screen, at 0x8050 -- but 38 sprites carry it disc-wide, only 8 named frame, and the high byte tracks the archive. It is an atlas/format word, not a mode. Also records a false positive of my own test: +0x00 and +0x08 first read as 'separating the frames' because those words are the name string. So any blend the port picks is authored. Reach: not looked at the executable's draw path, where a mode selected in code rather than data would live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
48
crates/sylpheed-formats/examples/decl_entry_diff.rs
Normal file
48
crates/sylpheed-formats/examples/decl_entry_diff.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Does a textured element's declaration carry anything that distinguishes the
|
||||
//! two FRAME elements from every other element on the main menu?
|
||||
//!
|
||||
//! `sylpheed-port` asks for the blend/alpha mode of `ptframe1`/`ptframe2`. Prior
|
||||
//! work is on `.prm` PRIMITIVES (`ui-prm-blend-mode.md`, undecodable with reach —
|
||||
//! no field, the declaration words are constant) and on a refuted `T8aD +0x04`
|
||||
//! bit. Neither covers a `.t32` element's own declaration entry, which is 60
|
||||
//! bytes and mostly unread.
|
||||
//!
|
||||
//! This dumps every declaration entry on the menu and reports, per 4-byte word,
|
||||
//! whether the two frames share a value that no other element has. A word that
|
||||
//! separates exactly those two is a candidate; one that does not, is not.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_entry_diff
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const AT: usize = 0x20;
|
||||
const N: usize = 60;
|
||||
|
||||
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 by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let names: Vec<String> = b.elements.iter().map(|e| e.name.clone()).collect();
|
||||
let frames: Vec<usize> = names.iter().enumerate()
|
||||
.filter(|(_, n)| n.starts_with("ptframe")).map(|(i, _)| i).collect();
|
||||
println!("{} elements; frames at indices {:?}", names.len(), frames);
|
||||
|
||||
let word = |i: usize, w: usize| -> u32 {
|
||||
let o = AT + i * N + w * 4;
|
||||
u32::from_be_bytes([by[o], by[o + 1], by[o + 2], by[o + 3]])
|
||||
};
|
||||
println!("\nper-word: does a value separate EXACTLY the two frames?");
|
||||
for w in 0..N / 4 {
|
||||
let fv: Vec<u32> = frames.iter().map(|&i| word(i, w)).collect();
|
||||
let same_in_frames = fv.windows(2).all(|p| p[0] == p[1]);
|
||||
let others: Vec<u32> = (0..names.len()).filter(|i| !frames.contains(i))
|
||||
.map(|i| word(i, w)).collect();
|
||||
let unique = same_in_frames && !others.contains(&fv[0]);
|
||||
let distinct = { let mut v: Vec<u32> = (0..names.len()).map(|i| word(i, w)).collect();
|
||||
v.sort_unstable(); v.dedup(); v.len() };
|
||||
println!(" +0x{:02X} frames {:?} distinct values {distinct:2}{}",
|
||||
w * 4, fv.iter().map(|v| format!("{v:08X}")).collect::<Vec<_>>(),
|
||||
if unique { " <- SEPARATES THE FRAMES" } else { "" });
|
||||
}
|
||||
}
|
||||
32
crates/sylpheed-formats/examples/decl_flag_words.rs
Normal file
32
crates/sylpheed-formats/examples/decl_flag_words.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Which elements carry which values at the low-cardinality declaration words?
|
||||
//!
|
||||
//! `decl_entry_diff` found nothing separating `ptframe1`/`ptframe2` except their
|
||||
//! NAME — +0x00 and +0x08 are the name string ("ptfr", ".t32"), so those two hits
|
||||
//! are a false positive of that test, not a field.
|
||||
//!
|
||||
//! The remaining candidates for a per-element mode flag are the words with few
|
||||
//! distinct values: +0x28 (3) and +0x2C (6). This prints who has what.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example decl_flag_words
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const AT: usize = 0x20;
|
||||
const N: usize = 60;
|
||||
|
||||
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 by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let w = |i: usize, off: usize| -> u32 {
|
||||
let o = AT + i * N + off;
|
||||
u32::from_be_bytes([by[o], by[o + 1], by[o + 2], by[o + 3]])
|
||||
};
|
||||
println!("{:<22} {:>10} {:>10} {:>10} {:>10}", "element", "+0x28", "+0x2C", "+0x34", "kind");
|
||||
for (i, e) in b.elements.iter().enumerate() {
|
||||
let mark = if e.name.starts_with("ptframe") { " <- FRAME" } else { "" };
|
||||
println!("{:<22} {:>10} {:>10} {:>10} {:>#10x}{mark}",
|
||||
e.name, w(i, 0x28), w(i, 0x2C) as i32, w(i, 0x34), e.kind);
|
||||
}
|
||||
}
|
||||
48
crates/sylpheed-formats/examples/t8ad_header_compare.rs
Normal file
48
crates/sylpheed-formats/examples/t8ad_header_compare.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Does a `.t32` sprite's own T8aD header carry a per-sprite blend/alpha mode?
|
||||
//!
|
||||
//! The declaration entry does not: `ptframe1`/`ptframe2` are kind 0, identical to
|
||||
//! every other plain sprite on the menu. The remaining place a mode could live is
|
||||
//! the sprite's own T8aD child. ⚠️ `REFUTED.md` already kills one reading of it —
|
||||
//! "`T8aD +0x04` bit `0x02` selects an additive blend" — so this is not that
|
||||
//! claim; it asks whether ANY header word separates the two frames from the
|
||||
//! sprites the port measures as ordinary.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example t8ad_header_compare
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use std::path::PathBuf;
|
||||
|
||||
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 by = ar.read(&ar.entries()[5]).expect("entry 5");
|
||||
let b = ui_layout::parse_build(&by).expect("build");
|
||||
let mut names: Vec<&String> = b.sprites.keys().collect();
|
||||
names.sort();
|
||||
println!("{:<20} {:>8} first 12 header words", "sprite", "size");
|
||||
let mut rows: Vec<(String, Vec<u32>)> = Vec::new();
|
||||
for n in names {
|
||||
let (off, size) = b.sprites[n];
|
||||
let s = &by[off..(off + size).min(by.len())];
|
||||
if s.len() < 48 { continue }
|
||||
let ws: Vec<u32> = (0..12)
|
||||
.map(|k| u32::from_be_bytes([s[k*4], s[k*4+1], s[k*4+2], s[k*4+3]]))
|
||||
.collect();
|
||||
let mark = if n.starts_with("ptframe") { " <- FRAME" } else { "" };
|
||||
println!("{n:<20} {size:>8} {}{mark}",
|
||||
ws.iter().map(|v| format!("{v:08X}")).collect::<Vec<_>>().join(" "));
|
||||
rows.push((n.clone(), ws));
|
||||
}
|
||||
// which words take a value the two frames share and nobody else does?
|
||||
let fr: Vec<&(String, Vec<u32>)> = rows.iter().filter(|(n, _)| n.starts_with("ptframe")).collect();
|
||||
if fr.len() == 2 {
|
||||
println!("\nwords where BOTH frames agree and no other sprite has that value:");
|
||||
let mut any = false;
|
||||
for w in 0..12 {
|
||||
let a = fr[0].1[w]; let bb = fr[1].1[w];
|
||||
if a != bb { continue }
|
||||
if rows.iter().any(|(n, v)| !n.starts_with("ptframe") && v[w] == a) { continue }
|
||||
println!(" word {w} (+0x{:02X}) = {a:08X}", w * 4); any = true;
|
||||
}
|
||||
if !any { println!(" NONE"); }
|
||||
}
|
||||
}
|
||||
50
crates/sylpheed-formats/examples/t8ad_word8_census.rs
Normal file
50
crates/sylpheed-formats/examples/t8ad_word8_census.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
//! Is T8aD `+0x08` a per-sprite MODE, or a texture FORMAT word?
|
||||
//!
|
||||
//! On the main menu the two frames share `+0x08 = 0x8050` and no other sprite has
|
||||
//! it — a candidate for the blend/alpha mode `sylpheed-port` asked for. Before
|
||||
//! offering it, refute it: if 0x8050 is common disc-wide on ordinary sprites, it
|
||||
//! is not frame-specific and not a mode.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example t8ad_word8_census
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||
let mut paks: Vec<_> = std::fs::read_dir(root.join("dat"))
|
||||
.expect("dat")
|
||||
.filter_map(|e| e.ok().map(|e| e.path()))
|
||||
.filter(|p| p.extension().map(|x| x == "pak").unwrap_or(false))
|
||||
.collect();
|
||||
paks.sort();
|
||||
let mut hist: BTreeMap<u32, usize> = BTreeMap::new();
|
||||
let mut frames_like: BTreeMap<u32, usize> = BTreeMap::new();
|
||||
let mut examples: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||
for p in &paks {
|
||||
let Ok(ar) = PakArchive::open(p) else { continue };
|
||||
for e in ar.entries() {
|
||||
let Ok(by) = ar.read(e) else { continue };
|
||||
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||
for (n, (off, size)) in &b.sprites {
|
||||
let s = &by[*off..(*off + *size).min(by.len())];
|
||||
if s.len() < 48 { continue }
|
||||
let w = u32::from_be_bytes([s[8], s[9], s[10], s[11]]);
|
||||
*hist.entry(w).or_default() += 1;
|
||||
if n.contains("frame") { *frames_like.entry(w).or_default() += 1 }
|
||||
let ex = examples.entry(w).or_default();
|
||||
if ex.len() < 3 && !ex.contains(n) { ex.push(n.clone()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
let total: usize = hist.values().sum();
|
||||
println!("{total} sprites disc-wide; distinct +0x08 values: {}\n", hist.len());
|
||||
println!("{:>10} {:>8} {:>10} examples", "value", "count", "of which 'frame'");
|
||||
for (v, c) in hist.iter().filter(|(_, c)| **c >= 20) {
|
||||
println!("{:>#10x} {c:>8} {:>10} {}", v, frames_like.get(v).copied().unwrap_or(0),
|
||||
examples[v].join(", "));
|
||||
}
|
||||
println!("\n0x8050 specifically: {} sprites, {} of them named *frame*",
|
||||
hist.get(&0x8050).copied().unwrap_or(0),
|
||||
frames_like.get(&0x8050).copied().unwrap_or(0));
|
||||
}
|
||||
59
docs/re/structures/t32-blend-mode-not-on-disc.md
Normal file
59
docs/re/structures/t32-blend-mode-not-on-disc.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# A textured element carries no blend/alpha mode — ❔ undecodable, with reach
|
||||
|
||||
**Answers `sylpheed-port`'s ask**: what blend/alpha mode do `ptframe1` and
|
||||
`ptframe2` use on the main menu? They measure those two elements as the only ones
|
||||
whose residual is **higher on flat pixels than on edges** (25.41/16.54 against
|
||||
19.85/9.82), signed one direction, `ptframe1` rendering at 88.4 against the
|
||||
capture's 129.1 with 0.1 % of pixels render-brighter — a body-intensity difference,
|
||||
not a geometric one.
|
||||
|
||||
## The answer: it is not on the disc, and the port is authoring
|
||||
|
||||
⚠️ Prior work covers **`.prm` primitives**
|
||||
([`ui-prm-blend-mode.md`](ui-prm-blend-mode.md), ❔ undecodable with reach) and a
|
||||
**refuted** `T8aD +0x04` bit. Neither covers a `.t32` element, which is what was
|
||||
asked. This does.
|
||||
|
||||
### Reach 1 — the declaration entry (60 bytes, all 15 words read)
|
||||
|
||||
| words | what they are |
|
||||
|---|---|
|
||||
| `+0x00`–`+0x0B` | the **name** string (`ptfr`, `ame1`, `.t32`) |
|
||||
| `+0x0C`–`+0x1F` | **constant 0** across every element on the screen |
|
||||
| `+0x20`, `+0x24` | constant `FFFFFFFF` |
|
||||
| `+0x28` | **kind** — 0 plain, `0x10` primitive, `0x3002` button |
|
||||
| `+0x2C` | **focus/nav index** — `−1` for non-buttons, 1…5 for the five buttons |
|
||||
| `+0x30`, `+0x34` | position / pivot |
|
||||
| `+0x38` | constant 0 |
|
||||
|
||||
🔴 **The two frames are `kind 0` — identical to `ptbase`, `pteff05`, `pteff10`,
|
||||
`pteff12` and `ptmsg`.** Nothing in the declaration distinguishes them.
|
||||
|
||||
⚠️ A first pass reported `+0x00` and `+0x08` as "separating the frames". **False
|
||||
positive of my own test**: those words are the name, and the frames share a prefix
|
||||
and a suffix no other element has.
|
||||
|
||||
### Reach 2 — the sprite's own `T8aD` header, and a candidate I refuted
|
||||
|
||||
`+0x08` is the one word where both frames agree on a value no other menu sprite
|
||||
has: **`0x8050`**. Tested disc-wide before offering it:
|
||||
|
||||
* **38 sprites carry `0x8050`, only 8 named `*frame*`** — not frame-specific;
|
||||
* the **high byte tracks the archive** — `0x80xx` in `GP_TITLE`, `0xb1xx` in
|
||||
`GP_OPTIONS`, `0xd8xx` in `GP_GAMEOVER`, `0xf0xx` in `GP_DIALOG`.
|
||||
|
||||
❌ So `+0x08` is an atlas/format word, **not a mode**. The candidate is dead.
|
||||
|
||||
## What this means for the port
|
||||
|
||||
**Any blend the port picks for these elements is authored, not transcribed**, and
|
||||
must be labelled that way. ✅ Their refusal to brighten the frames until they match
|
||||
is the right call for the reason they gave: a blend invented on their side is
|
||||
indistinguishable from a decoded one in a month — and this page is the evidence
|
||||
that there is nothing to decode it *from*.
|
||||
|
||||
⚠️ **Reach of the negative**: one screen's declaration table read exhaustively, the
|
||||
`T8aD` header's low words compared across every sprite on that screen and one
|
||||
candidate word censused disc-wide. **Not looked at**: the executable's draw path,
|
||||
which is where a mode selected in *code* rather than *data* would live. That is a
|
||||
route, and it is the one left.
|
||||
Reference in New Issue
Block a user