formats: a primitive layer is not in the file - measure it, table it, prove it

Two places checked for the key a .prm element sorts by, both empty:

  * the declaration entry's four unread words are constant across every element
    of all three measured screens (+28=0, +36=0xffffffff, +56=0, and +44 is a
    button ordinal 1-5, 0xffffffff elsewhere);
  * the bundle carries NO data for a primitive at all - the menu build declares
    pteff00.prm, pteff02.prm and pteff05.t32 and has zero RATC children for any
    of them, its 34 children being 21 T8aD sprites and 13 .rat records.

So the layer comes from the game code. But it is consistent, which is what makes
a per-name table a measurement rather than a fudge. Bracketing each unkeyed
element between the keys of its measured neighbours: pteff02.prm falls in
(0x8010, 0x8040) on BOTH screens it appears on, pteff00.prm is past the maximum
on both, and palogo_eff0.prm is below the minimum on the splash.

implied_layer_key records exactly those and nothing else; an unlisted primitive
keeps u32::MAX and still sorts last. With it, derived_paint_order produces the
same layer-key sequence as the order read off the running game on all three
measured screens - primitives included - and matches element-for-element on four
of the five bundle instances. The fifth is the title, differing only inside its
tied groups, which is a separate open question.

This does not make include_primitives safe by default: the 36 builds that come
out one colour are wiped by pzeff00.prm and pceff00.prm, never measured, hence
not in the table.

14 disc tests green.
This commit is contained in:
Sylpheed RE agent
2026-08-19 09:15:55 +00:00
parent a1624af24e
commit 610b21c434
3 changed files with 159 additions and 3 deletions

View File

@@ -667,6 +667,45 @@ pub fn sprite_layer_key(build: &UiBuild, bundle: &[u8], el: &Element) -> Option<
Some(be32(bundle, off + 8)) Some(be32(bundle, off + 8))
} }
/// Layer keys for elements the bundle gives no key for, **measured** from the
/// running game rather than read from a file.
///
/// A `.prm` primitive has no `T8aD` header and therefore no layer key, and the
/// bundle carries no data for it at all — the menu build declares
/// `pteff00.prm`, `pteff02.prm` and `pteff05.t32` and has **zero** RATC children
/// for any of them. The four unread words of the 60-byte declaration entry are
/// constant across every element (`+28`=0, `+36`=0xffffffff, `+56`=0, and `+44`
/// is a button ordinal 15), so the key is not there either. It comes from the
/// game's own code.
///
/// But it is *consistent*, which is what makes a table honest rather than a
/// fudge. Bracketing each unkeyed element by its measured neighbours' keys:
///
/// | element | title screen | main menu | splash |
/// |---|---|---|---|
/// | `pteff05.t32` | — | (0x8010, 0x8040) | — |
/// | `pteff04.t32` | (0x8010, 0x8040) | — | — |
/// | `pteff02.prm` | (0x8010, 0x8040) | (0x8010, 0x8040) | — |
/// | `pteff00.prm` | (0x8100, end) | (0x8110, end) | — |
/// | `palogo_eff0.prm` | — | — | (start, 0xa100) |
///
/// `pteff02.prm` lands in the **same interval on both** screens it appears on;
/// the fade quad is past the maximum on both; the splash backdrop is below the
/// minimum. So these behave exactly like fixed per-name layers.
///
/// Only names whose position has actually been measured are listed. An unlisted
/// primitive keeps `u32::MAX` and sorts last, which is where `compose` leaves it
/// — and why `ComposeOptions::include_primitives` is still off by default.
pub fn implied_layer_key(name: &str) -> Option<u32> {
Some(match name {
"palogo_eff0.prm" => 0x0000,
"pteff04.t32" | "pteff05.t32" => 0x8020,
"pteff02.prm" => 0x8030,
"pteff00.prm" => 0xffff_fffe,
_ => return None,
})
}
/// The paint order derived from the bundle: a stable sort of the elements by /// The paint order derived from the bundle: a stable sort of the elements by
/// their sprite's layer key. /// their sprite's layer key.
/// ///
@@ -675,11 +714,14 @@ pub fn sprite_layer_key(build: &UiBuild, bundle: &[u8], el: &Element) -> Option<
/// they land does not affect a composite. Ties keep declaration order — the game /// they land does not affect a composite. Ties keep declaration order — the game
/// breaks them some other way, which is unexplained and looks harmless because /// breaks them some other way, which is unexplained and looks harmless because
/// tied elements are same-layer. /// tied elements are same-layer.
fn derived_paint_order(build: &UiBuild, bundle: &[u8]) -> Vec<usize> { pub fn derived_paint_order(build: &UiBuild, bundle: &[u8]) -> Vec<usize> {
let mut idx: Vec<usize> = (0..build.elements.len()).collect(); let mut idx: Vec<usize> = (0..build.elements.len()).collect();
idx.sort_by_key(|&i| { idx.sort_by_key(|&i| {
let el = &build.elements[i];
( (
sprite_layer_key(build, bundle, &build.elements[i]).unwrap_or(u32::MAX), sprite_layer_key(build, bundle, el)
.or_else(|| implied_layer_key(&el.name))
.unwrap_or(u32::MAX),
i, i,
) )
}); });

View File

@@ -674,3 +674,73 @@ fn applying_the_fade_alpha_blanks_no_screen() {
); );
eprintln!("fade alpha: {noop} no-op, {hidden} hidden, 0 blank builds of {builds}"); eprintln!("fade alpha: {noop} no-op, {hidden} hidden, 0 blank builds of {builds}");
} }
/// **The derived order places every element in the right layer group, on all
/// three measured screens — primitives included.**
///
/// This is the strong form of the layer-key result. The weaker test above only
/// asserts the measured order never inverts a key, which skips the elements that
/// have none at all. With `implied_layer_key` supplying measured keys for the
/// primitives, the derived sort must produce the **same key at every position**
/// as the order read off the running game.
///
/// It is stated as key-sequence equality and not as list equality on purpose:
/// elements that share a key are ordered by something still unknown (the title's
/// `0x8083` ×5 group paints `14,15,18,16,17`, the derived sort gives
/// `14,15,16,17,18`), and that tie-break is a separate open question. Anything
/// crossing a group boundary is a real regression and fails here.
#[test]
fn the_derived_order_puts_every_element_in_the_right_layer_group() {
skip_without_disc!(root);
let cases: [(usize, &str, &[usize]); 3] = [
(
24,
"ptlogo1.t32",
&[9, 11, 12, 10, 13, 6, 20, 19, 14, 15, 18, 16, 17, 0, 2, 4, 7, 1, 3, 5, 22, 23, 21, 8],
),
(7, "palogo_eff0.prm", &[0, 2, 4, 6, 1, 3, 5]),
(
16,
"pteff00.prm",
&[1, 3, 4, 2, 5, 8, 9, 6, 7, 15, 10, 11, 12, 13, 14, 0],
),
];
let arc = PakArchive::open(root.join("dat").join("GP_TITLE.pak")).expect("open pak");
let mut seen = [false; 3];
let mut exact = 0usize;
for e in arc.entries() {
let Ok(bundle) = arc.read(e) else { continue };
let Some(build) = ui_layout::parse_build(&bundle) else {
continue;
};
for (ci, (n, first, measured)) in cases.iter().enumerate() {
if build.elements.len() != *n || build.elements[0].name != *first {
continue;
}
seen[ci] = true;
let key = |i: usize| {
let el = &build.elements[i];
ui_layout::sprite_layer_key(&build, &bundle, el)
.or_else(|| ui_layout::implied_layer_key(&el.name))
.unwrap_or(u32::MAX)
};
let derived = ui_layout::derived_paint_order(&build, &bundle);
let dk: Vec<u32> = derived.iter().map(|&i| key(i)).collect();
let mk: Vec<u32> = measured.iter().map(|&i| key(i)).collect();
assert_eq!(
dk, mk,
"the {n}-element build starting {first}: the derived order's \
layer-key sequence differs from the one measured off the \
running game\n derived {derived:?}\n measured {measured:?}"
);
if derived == measured.to_vec() {
exact += 1;
}
}
}
assert!(
seen.iter().all(|&b| b),
"not every measured screen was found in GP_TITLE.pak: {seen:?}"
);
eprintln!("layer groups match on all 3 measured screens; {exact} match element-for-element");
}

View File

@@ -116,9 +116,53 @@ The genuinely wiped ones are a different set, wiped by an *opaque* quad.
## What is not settled ## What is not settled
## Where a primitive paints — refuted in the file, measured in the game (2026-08-19)
**🔴 Refuted: the key is not in the bundle.** Two places were checked and both
are empty:
* the **declaration entry**'s four unread words are constant across every
element of all three measured screens — `+28` = 0, `+36` = `0xffffffff`,
`+56` = 0, and `+44` is a button ordinal (1…5 on the menu's five buttons,
`0xffffffff` everywhere else). No key there.
* the **bundle carries no data at all** for a primitive. The menu build declares
`pteff00.prm`, `pteff02.prm` and `pteff05.t32` and has **zero** RATC children
for any of them — its 34 children are 21 `T8aD` sprites and 13 `.rat` records,
none of them named for a primitive.
So the layer a primitive draws on comes from the game's own code, not the file.
**✅ But it is consistent, which makes a per-name table honest.** Bracketing each
unkeyed element between its measured neighbours' keys:
| element | title screen | main menu | splash |
|---|---|---|---|
| `pteff05.t32` | — | (0x8010, 0x8040) | — |
| `pteff04.t32` | (0x8010, 0x8040) | — | — |
| `pteff02.prm` | **(0x8010, 0x8040)** | **(0x8010, 0x8040)** | — |
| `pteff00.prm` | (0x8100, end) | (0x8110, end) | — |
| `palogo_eff0.prm` | — | — | (start, 0xa100) |
`pteff02.prm` falls in the **same interval on both** screens it appears on, the
fade quad is past the maximum on both, and the splash backdrop is below the
minimum. `ui_layout::implied_layer_key` records exactly these, and nothing more —
an unlisted primitive keeps `u32::MAX` and still sorts last.
With it, `derived_paint_order` produces **the same layer-key sequence as the
order read off the running game on all three measured screens**, primitives
included, and matches element-for-element on four of the five bundle instances
(the fifth is the title, which differs only inside its tied groups — a separate
open question). Pinned by
`the_derived_order_puts_every_element_in_the_right_layer_group`.
This does **not** make `include_primitives` safe by default: the 36 builds that
come out one colour are wiped by `pzeff00.prm` and `pceff00.prm`, whose positions
have never been measured, so they are not in the table.
## What is not settled ## What is not settled
***Where a primitive paints.** The blocker, described above. It has no layer ***Where an *unmeasured* primitive paints.** The blocker, unchanged for the
ones not in the table. It has no layer
key and the two measured screens rule out every constant default. The cheapest key and the two measured screens rule out every constant default. The cheapest
next step is a third measured order from a screen that carries a primitive — next step is a third measured order from a screen that carries a primitive —
the `GP_DIALOG` DIFFICULTY box is reachable from the main menu and has exactly the `GP_DIALOG` DIFFICULTY box is reachable from the main menu and has exactly