formats/cli: the bundle's other orderings, and a guard that neither is a paint order

The title screen needs an order that puts element 13 (`ptbase2.t32`, the
full-screen background) behind elements 0-5 (the wordmarks) — the capture shows
the wordmarks on top, so the declaration table is not it. Two other orderings
the bundle carries were the cheap candidates, and both are now dead:

* the **placement region** stores a keyframe group per element with an explicit
  element index, so it could be a second ordering. It is not — it equals the
  declaration order on every build on the disc. `UiBuild::placement_order`
  exposes it and `placement_region_order_is_never_a_second_ordering` pins it, so
  the refutation stays checkable instead of remembered.
* the **RATC child order** is the declaration order with the `.prm` elements
  absent — strictly less information, and no place to put the background other
  than where the table already puts it.

`screen info --geometry` prints both, plus each element's decoded sprite size
beside `pivot*2` and every keyframe's scale/position/time — the numbers a
placement hypothesis has to be tested against, and how the pivot/scale rule in
the previous commit was found.

`title_background_is_full_screen` pins that rule against the disc rather than a
synthetic sprite. `scaled_elements_are_a_small_and_mostly_undiscriminating_minority`
reports the scope honestly: 865 of 5 130 resting placements are scaled at all,
and only 213 of those could tell "about the pivot" from "about the sprite
centre" — which the capture did *not* settle, because `ptbase2`'s pivot is its
centre. It also counts how far `pivot*2` is from the decoded size disc-wide
(2 521 agree, 1 884 are off by more than 16 px), which demotes the "pivot is
exactly half the texture" result to a property of the tutorial bundle.
This commit is contained in:
Sylpheed RE agent
2026-08-18 16:32:40 +00:00
parent e6a55b5fd4
commit 705d9a37ba
3 changed files with 324 additions and 7 deletions

View File

@@ -137,6 +137,12 @@ enum ScreenCommands {
/// Which build (index into `screen list`); default = the largest
#[arg(long)]
build: Option<usize>,
/// Also print the bundle's other orderings and, per element, the decoded
/// sprite size beside the declared pivot and every keyframe's
/// scale/position/time — what a placement or paint-order hypothesis has
/// to be tested against.
#[arg(long)]
geometry: bool,
},
/// Composite one build to a PNG — the headless self-verify for the viewer
Render {
@@ -301,7 +307,11 @@ async fn main() -> Result<()> {
},
Commands::Screen { cmd } => match cmd {
ScreenCommands::List { pak } => cmd_screen_list(&pak),
ScreenCommands::Info { pak, build } => cmd_screen_info(&pak, build),
ScreenCommands::Info {
pak,
build,
geometry,
} => cmd_screen_info(&pak, build, geometry),
ScreenCommands::Render {
pak,
output,
@@ -379,7 +389,7 @@ fn cmd_screen_list(pak: &Path) -> Result<()> {
Ok(())
}
fn cmd_screen_info(pak: &Path, want: Option<usize>) -> Result<()> {
fn cmd_screen_info(pak: &Path, want: Option<usize>, geometry: bool) -> Result<()> {
use sylpheed_formats::ui_layout;
let builds = screen_builds(pak)?;
let idx = pick_build(&builds, want)?;
@@ -436,9 +446,82 @@ fn cmd_screen_info(pak: &Path, want: Option<usize>) -> Result<()> {
println!("{:<3} {:<30} → focus {link}", "", "");
}
}
if geometry {
print_geometry(&b, bytes);
}
Ok(())
}
/// The two other orderings the bundle carries, and each element's real drawn
/// size. Both were needed to settle the title screen's paint order — see
/// `docs/re/BACKLOG.md`.
fn print_geometry(b: &sylpheed_formats::ui_layout::UiBuild, bytes: &[u8]) {
println!();
let identity: Vec<usize> = (0..b.placement_order.len()).collect();
println!(
"placement-region group order: {:?}{}",
b.placement_order,
if b.placement_order == identity {
" (== declaration order)"
} else {
" (DIFFERS from declaration order)"
}
);
if let Some(kids) = sylpheed_formats::ratc::parse(bytes) {
println!(
"RATC child order: {:?}",
kids.iter()
.map(|c| format!("{}:{}", c.kind, c.name))
.collect::<Vec<_>>()
);
}
println!();
println!("geometry — decoded sprite size vs the declared pivot, and every keyframe");
println!(
"{:<3} {:<26} {:>11} {:>11} {:>5} keyframes t: x,y sx%,sy% a=fade-alpha",
"#", "sprite", "decoded", "pivot*2", "same"
);
for el in &b.elements {
let dims = el
.sprite
.as_ref()
.and_then(|s| b.sprites.get(s))
.and_then(|&(off, size)| sylpheed_formats::t8ad::parse(&bytes[off..off + size]))
.map(|img| (img.width, img.height));
let pv = (el.pivot_x * 2, el.pivot_y * 2);
let same = match dims {
Some(d) if d == pv => "yes",
Some(_) => "NO",
None => "-",
};
let kfs = el
.keyframes
.iter()
.map(|f| {
format!(
"{}: {},{} {}%,{}% a={}",
f.time.map(|v| v.to_string()).unwrap_or_else(|| "-".into()),
f.x,
f.y,
f.scale_x,
f.scale_y,
(f.fade >> 24) & 0xff
)
})
.collect::<Vec<_>>()
.join(" ");
println!(
"{:<3} {:<26} {:>11} {:>11} {:>5} {kfs}",
el.index,
el.sprite.as_deref().unwrap_or(""),
dims.map(|(w, h)| format!("{w}x{h}"))
.unwrap_or_else(|| "".into()),
format!("{}x{}", pv.0, pv.1),
same,
);
}
}
fn cmd_screen_render(
pak: &Path,
output: &Path,