From f852049d0d0e21faefe144dad1d9439eb0356d06 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Wed, 19 Aug 2026 09:15:55 +0000 Subject: [PATCH] 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. --- crates/sylpheed-formats/src/ui_layout.rs | 46 +++++++++++- .../tests/ui_paint_order_disc.rs | 70 +++++++++++++++++++ docs/re/structures/ui-prm-primitives.md | 46 +++++++++++- 3 files changed, 159 insertions(+), 3 deletions(-) diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index d5ef2dc3..2a47ec84 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -667,6 +667,45 @@ pub fn sprite_layer_key(build: &UiBuild, bundle: &[u8], el: &Element) -> Option< 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 1–5), 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 { + 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 /// 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 /// breaks them some other way, which is unexplained and looks harmless because /// tied elements are same-layer. -fn derived_paint_order(build: &UiBuild, bundle: &[u8]) -> Vec { +pub fn derived_paint_order(build: &UiBuild, bundle: &[u8]) -> Vec { let mut idx: Vec = (0..build.elements.len()).collect(); 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, ) }); diff --git a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs index 40ada1be..8f44e967 100644 --- a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs +++ b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs @@ -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}"); } + +/// **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 = derived.iter().map(|&i| key(i)).collect(); + let mk: Vec = 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"); +} diff --git a/docs/re/structures/ui-prm-primitives.md b/docs/re/structures/ui-prm-primitives.md index e918588c..b6a29cae 100644 --- a/docs/re/structures/ui-prm-primitives.md +++ b/docs/re/structures/ui-prm-primitives.md @@ -116,9 +116,53 @@ The genuinely wiped ones are a different set, wiped by an *opaque* quad. ## 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 -* ❔ **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 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