diff --git a/crates/sylpheed-formats/src/ui_layout.rs b/crates/sylpheed-formats/src/ui_layout.rs index 2a47ec84..728a02da 100644 --- a/crates/sylpheed-formats/src/ui_layout.rs +++ b/crates/sylpheed-formats/src/ui_layout.rs @@ -698,7 +698,11 @@ pub fn sprite_layer_key(build: &UiBuild, bundle: &[u8], el: &Element) -> Option< /// — 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, + // Full-screen backdrops, measured painting FIRST on their screens. + // `pfbase.tbm` is not a `.prm` at all — a `.tbm` with no sprite and no + // key — but it behaves identically: the save/load screen paints it and + // its `kind = 0x3004` instance before everything else. + "palogo_eff0.prm" | "pfbase.tbm" => 0x0000, "pteff04.t32" | "pteff05.t32" => 0x8020, "pteff02.prm" => 0x8030, "pteff00.prm" => 0xffff_fffe, diff --git a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs index e2ee539f..37ac6f97 100644 --- a/crates/sylpheed-formats/tests/ui_paint_order_disc.rs +++ b/crates/sylpheed-formats/tests/ui_paint_order_disc.rs @@ -900,3 +900,67 @@ fn order_disagreements_that_change_pixels_are_pinned() { "the set of order disagreements that actually change pixels has moved" ); } + +/// Two more paint orders read off the running game, from `GP_SAVE_LOAD` — and +/// the first **independent confirmation** of the derived rule. +/// +/// The three orders the rule was built from all live in `GP_TITLE.pak`. These do +/// not, and one of them the sort gets exactly right without having been told +/// anything about it: +/// +/// * the **slot list header** (9 elements) composites `[7,8,0,1,2,3,4,5,6]`, +/// which is what sorting by layer key gives — including **two tied groups** +/// (`0xb102` ×2 and `0xb210` ×5) that both come out in declaration order, and +/// the unkeyed `pfeff00.prm` fade quad last. Exact on all five bundle +/// instances of it. +/// * the **save/load frame** (13 elements) does not, in exactly two ways, and +/// both are already-known open questions rather than new ones: two unkeyed +/// `pfbase.tbm` background elements paint **first** where the sort puts the +/// keyless last, and the `0xb100` group of four paints `10,11,8,12` where +/// declaration order is `8,10,11,12`. +#[test] +fn the_save_load_screens_match_what_the_running_game_paints() { + skip_without_disc!(root); + let header: [usize; 9] = [7, 8, 0, 1, 2, 3, 4, 5, 6]; + let frame: [usize; 13] = [0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 8, 12, 9]; + let arc = PakArchive::open(root.join("dat").join("GP_SAVE_LOAD.pak")).expect("pak"); + let (mut headers, mut frames) = (0usize, 0usize); + for e in arc.entries() { + let Ok(bundle) = arc.read(e) else { continue }; + let Some(b) = ui_layout::parse_build(&bundle) else { + continue; + }; + let names: Vec<&str> = b.elements.iter().map(|e| e.name.as_str()).collect(); + let derived = ui_layout::derived_paint_order(&b, &bundle); + if b.elements.len() == 9 && names[0] == "pftitlebase.t32" && names[6] == "pfeff00.prm" { + headers += 1; + assert_eq!( + derived, + header.to_vec(), + "the slot-list header no longer composites in the order the game paints" + ); + } + if b.elements.len() == 13 && names[0] == "pfbase.tbm" { + frames += 1; + // The two known gaps, stated as the measured difference rather than + // asserted away: the sort must at least agree on everything else. + let key = |i: usize| { + let el = &b.elements[i]; + ui_layout::sprite_layer_key(&b, &bundle, el) + .or_else(|| ui_layout::implied_layer_key(&el.name)) + .unwrap_or(u32::MAX) + }; + let dk: Vec = derived.iter().map(|&i| key(i)).collect(); + let mk: Vec = frame.iter().map(|&i| key(i)).collect(); + assert_eq!( + dk, mk, + "the save/load frame's layer-key sequence differs from the game's \ + — the only accepted differences are WITHIN a key group\n \ + derived {derived:?}\n measured {frame:?}" + ); + } + } + assert!(headers >= 3, "found {headers} slot-list headers, expected several"); + assert!(frames >= 1, "found {frames} save/load frames"); + eprintln!("GP_SAVE_LOAD: {headers} headers exact, {frames} frames agree by layer group"); +} diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index fefe1167..24b9a4eb 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -139,9 +139,16 @@ See [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) and it is pinned by a test. The third pair (`ptlogo2` vs `ptlogo_tm`) overlaps by bounding box but shares no opaque pixel; a box test called it a defect and the alpha says otherwise. - **Next:** it is one element on one screen, so the cheap move is a fourth - measured screen with a tied group — not more static guessing at fields that - have all now been checked. 🔴 **Attempted 2026-08-19 and blocked:** advancing + ✅ **Fourth and fifth screens measured (2026-08-19)**, from `GP_SAVE_LOAD`, + reachable now that the Canary threading fix makes the menu dependable. The + 9-element slot-list header is **EXACT** under the derived rule — two tied + groups both in declaration order, unkeyed `.prm` last — and it is the first + screen outside `GP_TITLE.pak`, so it *confirms* the rule rather than being + fitted to it. The 13-element save/load frame differs in exactly the two known + ways: unkeyed `pfbase.tbm` backgrounds paint **first** (now covered by + `implied_layer_key`), and the `0xb100` group of four paints `10,11,8,12`. + 🔴 **Refuted: the tie-break is not `kind`.** "Descending kind" reproduces + `10,11,8,12` exactly but fails both title groups. Seven candidates refuted now. 🔴 **Attempted 2026-08-19 and blocked:** advancing past the title is intermittent — **1 success in 3 attempts**, same binary, same profile, same procedure. ✅ **And now diagnosed one layer deeper:** the title *does* act on Ⓐ — the press spawns a slot-`(1F)` loader thread (exactly diff --git a/docs/re/structures/ui-paint-order-key.md b/docs/re/structures/ui-paint-order-key.md index 7fb1823d..18008415 100644 --- a/docs/re/structures/ui-paint-order-key.md +++ b/docs/re/structures/ui-paint-order-key.md @@ -257,3 +257,69 @@ it is not. That is why the test reads pixels. So the residual error in the derived order is confined to **one element's blend on one screen**, and it is pinned: a change that makes it worse fails the test. + + +## Two more measured orders — and the first independent confirmation (2026-08-19) + +The three orders the derived rule was built from all live in `GP_TITLE.pak`, so +they cannot confirm it: the rule was fitted to them. These two are from +`GP_SAVE_LOAD.pak`, read off the running game after the Canary threading fix made +the main menu reachable ([capture](../captures/load-game-oracle.png)). + +### ✅ The slot-list header — exact, without being told anything + +`GP_SAVE_LOAD` ratc-indices 1/4/5/21/27, 9 elements: + +``` +measured 7 8 0 1 2 3 4 5 6 +derived 7 8 0 1 2 3 4 5 6 EXACT, on all 6 instances +``` + +| element | key | +|---|---| +| 7 `pffocus1.rat`, 8 `pfslot1f_scr.rat` | `0xb102` **tied ×2** | +| 0 `pftitlebase` | `0xb200` | +| 1–5 `pfeff11`…`pftitle1` | `0xb210` **tied ×5** | +| 6 `pfeff00.prm` | none → last | + +Both tied groups come out in **declaration order**, which is what the stable sort +gives, and the unkeyed fade quad lands last. Nothing about this screen was fed +into the rule. It is the first evidence that the layer key is a real ordering +mechanism rather than a description of three screens in one pak. + +### 🟡 The save/load frame — differs in exactly the two known ways + +`GP_SAVE_LOAD` ratc-index 18/24, 13 elements (`is_build` rejects it, so the +compositor never sees it — but it is just as good as *evidence*): + +``` +measured 0 1 2 3 4 5 6 7 10 11 8 12 9 +derived 2 3 4 5 6 7 8 10 11 12 9 0 1 +``` + +1. **Unkeyed elements paint first, not last.** Elements 0 and 1 are + `pfbase.tbm` — a `.tbm`, not a `.prm`, with no sprite and no key — and the + game paints them before everything else. Exactly the splash's + `palogo_eff0.prm` behaviour in a different file type, so `implied_layer_key` + now covers it and the layer-key sequences agree. +2. **A tied group in a non-declaration order.** `0xb100` holds 8 `pfwin`, + 10 `pfbtn0`, 11 `pfbtn1`, 12 `pfmsg`, and the game paints + **10, 11, 8, 12**. + +That second point is the tie-break question again — and it is now on a *second* +screen, which is what it needed. + +### 🔴 Refuted: the tie-break is not `kind` + +The new group is suggestive: 10 and 11 are `kind = 0x2002` and 8 and 12 are +`0x0000`, so "descending `kind`, then declaration index" reproduces +`10, 11, 8, 12` exactly. It does not survive the title: + +* `0x8083` ×5 — every element is `kind = 0x0000`, so the rule predicts + declaration order `14,15,16,17,18`; the game paints `14,15,18,16,17`. +* `0x80a0` ×7 — kinds are `0x0` and `0x4`, so the rule predicts + `2,3,4,5` then `0,1,7`; the game paints `0,2,4,7,1,3,5`. + +So `kind` is not it either, and the count of refuted candidates is now seven: +declaration order, RATC child order, first keyframe time, resting time, resting +X/Y, the `T8aD` header words, and `kind`.