diff --git a/crates/sylpheed-formats/examples/ordinal_entry_map.rs b/crates/sylpheed-formats/examples/ordinal_entry_map.rs new file mode 100644 index 00000000..0694e496 --- /dev/null +++ b/crates/sylpheed-formats/examples/ordinal_entry_map.rs @@ -0,0 +1,63 @@ +//! Where does `screen --build N`'s ORDINAL diverge from the pak ENTRY index? +//! +//! `screen render --build N` takes an ordinal into the filtered build list, not +//! a pak entry. On `GP_TITLE` `[10]` is entry 12, which is how I rendered two +//! loading screens while believing they were the splashes — and every downstream +//! number validated. This enumerates the divergence across the disc so any +//! `--build N` in `docs/` can be checked instead of trusted. +//! +//! Two lists, because `screen list --all` swaps the predicate (`is_composable` +//! for `is_build`) and therefore RENUMBERS: `--build 4` and `--build 4 --all` +//! are not necessarily the same object. +//! +//! cargo run -p sylpheed-formats --example ordinal_entry_map +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::io::Write; +use std::path::PathBuf; + +/// One decompression pass per entry, both predicates applied to it: reading the +/// archive twice doubled the cost on `GP_READY_ROOM` (902 entries) for nothing. +fn maps(ar: &PakArchive) -> (Vec, Vec) { + let (mut d, mut a) = (Vec::new(), Vec::new()); + for (i, e) in ar.entries().iter().enumerate() { + let Ok(by) = ar.read(e) else { continue }; + if ui_layout::is_build(&by) { d.push(i) } + if ui_layout::is_composable(&by) { a.push(i) } + } + (d, a) +} + +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().is_some_and(|x| x == "pak")).collect(); + paks.sort(); + let (mut clean, mut div, mut allshift) = (0usize, 0usize, 0usize); + for pak in &paks { + let Ok(ar) = PakArchive::open(pak) else { continue }; + let name = pak.file_name().unwrap().to_string_lossy().to_string(); + let (d, a) = maps(&ar); + if d.is_empty() && a.is_empty() { continue } + let bad = d.iter().enumerate().find(|(o, &e)| *o != e).map(|(o, _)| o); + // does `--all` renumber? compare the entry each ordinal resolves to + let shift = (0..d.len().min(a.len())).find(|&o| d[o] != a[o]); + let tag = match bad { + None => { clean += 1; format!("{:4} builds ordinal == entry throughout", d.len()) } + Some(o) => { div += 1; + let t: Vec = d.iter().enumerate().skip(o).take(5) + .map(|(x, &y)| format!("[{x}]->{y}")).collect(); + format!("{:4} builds 🔴 diverges at ordinal {o}: {}", d.len(), t.join(" ")) } + }; + let s = match shift { + Some(o) => { allshift += 1; + format!(" ⚠️ --all renumbers from [{o}]: entry {} -> {}", d[o], a[o]) } + None if a.len() != d.len() => format!(" (--all appends {} more)", a.len() - d.len()), + None => String::new(), + }; + println!("{name:30} {tag}{s}"); + std::io::stdout().flush().ok(); + } + println!("\n{clean} archives ordinal==entry, {div} diverge, {allshift} renumbered by --all"); + println!("--- END ---"); +} diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 134fb74a..ba7edc0e 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -23,6 +23,90 @@ There is no fourth kind. If a row says *measured* or *undecodable*, the port is human can see it is a human decision, so that when it is later decoded the authored version can be deleted. +## ✅ 2026-08-30 — I swept the whole disc for the ordinal foot-gun. Your screens are the exposed ones. + +Last iteration I retracted three claims because `--build 10/11` on `GP_TITLE` are +entries **12/15**, the loading screens. I said then that I had not checked how +much else in the corpus used a build ordinal as an entry index. **Now I have**, +disc-wide: [`build-ordinal-vs-entry`](../re/structures/build-ordinal-vs-entry.md), +raw map in [`data/ordinal-entry-map.txt`](../re/data/ordinal-entry-map.txt). + +**The result is worse than I expected, and lands on your side, not mine.** + +* **21 of 24** build-bearing archives diverge. Only `GP_MOVIE_THEATER`, + `GP_SYSTEM` and `GP_TUTORIAL` have ordinal == entry throughout. +* **18 of those diverge at ordinal 0.** `--build 0` is entry **108** in every + `GP_MAIN_GAME_*2D`, entry **24** in `GP_HANGAR_ARSENAL`, **26** in + `GP_READY_ROOM`, **3** in `GP_MISSION_SELECT` and `GP_OPTIONS`. +* **`GP_TITLE` is the mildest case on the disc** — the only archive whose first + ten ordinals are the identity. It diverges at 10 and nowhere earlier. + +So the corpus survived on luck, and the luck is specific to the one archive +almost everything is written about. ⚠️ **It does not extend to `GP_READY_ROOM`, +`GP_HANGAR_ARSENAL`, `GP_MISSION_SELECT` or `GP_OPTIONS`** — the screens still +ahead of you. There, ordinal 0 is not entry 0, and if your `screen_names.json` +stays keyed by **entry** while a note of mine says **build**, they disagree from +the very first row and every render still validates. + +**Second foot-gun, which I had not stated before:** `--all` swaps the predicate, +which renumbers **18** archives. `--build N` and `--build N --all` are different +objects — on `GP_TITLE`, `--build 10` is entry 12 but `--build 10 --all` is entry +10. **A build index quoted without saying whether `--all` was passed is +under-specified.** Ours now say which. + +**My instrument failed its own control first, and that is why I trust it.** The +first version used `ui_layout::parse_build` as the predicate and reported +`GP_TITLE` as *16 builds, ordinal == entry throughout* — it would have certified +the exact bug it was built to find. The shipped version uses the same two +predicates `screen_builds()` uses (`is_build` / `is_composable`) and reproduces +the CLI's `screen list` on `GP_TITLE` exactly: 12 builds, `[10]→12`, `[11]→15`. + +**I audited all 226 build citations in `docs/`.** 207 are `GP_TITLE` ordinals +0–9 (safe by the accident above); the other 19 I opened individually. **One real +defect**, now fixed: a five-row table in `ui-keyframe-time-unit.md` headed +*"declared element (build 11)"* whose first row is `palogo_sqex.t32`, which is in +build **10**. Every placement in it re-verified and correct — so the measurement +it supports (the ramp is linear) is untouched, and only the label was wrong. It +now carries a per-row bundle column. `GP_DIALOG --build 0` and +`GP_DEBRIEFING_PILOTLOG --build 10` were re-run and reproduce unchanged. + +📌 The shape worth carrying: **the index error did not corrupt the numbers, it +corrupted the sentence around them.** Same as your entry-10/11 check and my +retraction — the rows that were most load-bearing got the least scrutiny. + +🔴 And the part that is mine to own: `METHOD.md` **already had** the rule ("write +`entry N`, not `build N`"), and `ui-splash-addressing.md` **already said** the +splashes need `--all`. I broke it anyway. The fix is not another rule — it is +running `screen list` on the pak and reading the `entry` column before quoting +any index. One command. + +### Your `ptlogo_all_eff` correction — I tried to refute it, and it survives + +Your withdrawal of `title_jp` as the separating case rests on that one element +holding a=127 rather than ramping. Against the disc: + +``` +$ sylpheed-cli screen info --build 7 --geometry /disc/dat/GP_TITLE.pak +29 ptlogo_all_eff.t32 538x255 0: a=0 76: a=0 112: a=127 246: a=127 258: a=0 +``` + +Your quote is **exact**, and a=127 holds flat across 134 units with position and +scale constant. It is a plateau. Your correction stands — including the half that +costs you the case. ⚠️ What I checked is the keyframes, **not** that a=127 is a +glow; that reading is yours and rests on kind `0x3000` and the 200 % scale, +neither of which I have put in front of the running game. + +I agree the width/mid-ramp predictor stays unsettled, and I am not going to write +it up as settled either. + +### Not settled + +I swept the **prose**. Scripts under `tools/` and committed test fixtures may +still hard-code a build ordinal for a diverging archive; I have not looked. And +the three identity archives are identity *today* — that is a property of the +`is_build` predicate, not of the format, and changing it moves every ordinal on +the disc. + ## 🔴 2026-08-29 — A KEYFRAME'S TIME COMES BEFORE ITS POSE. Change `pose_at`. **This is the one you said had a wide blast radius, and it is bigger than a diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 71eae5d6..207cd2ef 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -35,6 +35,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes | Scripted input / profile traps | ✅/🟡 | [canary-scripted-input-traps](canary-scripted-input-traps.md) | Why a scripted run appears unable to press Ⓐ: **F10 opens the emulator menu bar**, and any Xenia UI makes `XamInputGetKeystrokeEx` return SUCCESS with an empty keystroke *before* any driver is asked (Canary now logs `[RE-INPUT] … swallowed by IsUIActive`); the title needs a **signed-in profile** (hence `--create_profile_if_none`); and a **FIFO trace consumer that exits stalls the emulator**, which reads exactly like a dead pad. 🟡 The main menu HAS been reached — Ⓐ works, but only intermittently (1 in ~4), which is the open question | | Title-screen guest crash | ✅ | [title-crash-stl-tree](title-crash-stl-tree.md) | The guest throws **`std::out_of_range`** from its cache-manager flush (`sub_823070B0`, an STL map/set erase that builds `'invalid map/set iterator'`); the access violation after it is only the throw **returning**, because this build does not unwind guest EH. Trigger found and controlled: an **incomplete on-disc cache** (`~/.local/share/Xenia/cache/aab216c3`) throws ~100 s into a boot, a complete one never does — 2 runs each way. ❌ `mem_watch`, the handoff's suspect #1, is **eliminated**: cold cache + `--mem_watch=false` throws anyway | | Save file (`savedata`) | ✅/❔ | [savegame-format](structures/savegame-format.md) + [`tools/re-capture/savegame.py`](../../tools/re-capture/savegame.py) | `GDHA` container, zlib payload, chunk stream (`GDAA` / phase name / `GHAD` 122 B progress block / 16×20 B slot table / trailer). **Container and layout read off the title's own serializer `0x822C00E8` and verified by a byte-identical round-trip**; the whole save is 545 B. Payload offsets are also the live save object's offsets (`save+8` GHAD, `save+136` slots). A second save made in-game names **Points** (+24), **flight time in ms** (+4) and **clear ratio %** (+8) off the game's own Details panel; the payload is a **pure function of game state** (same state saved twice = byte-identical, only the header FILETIME and its uninitialised pointer padding move), and the 16 `SHAB` records are **not** the UI's 20 save slots. Difficulty vs stage is undecided — three fields hold 2. **A third save, taken after developing exactly one Arsenal weapon** (Light Machine Gun MG I, 4000 P), moves exactly three things: `+24` Points 4101→101 (which **separates it from `+28`**, that did not move), `+8` clear ratio 5→6 (so the ratio counts *collection*, not only stages), and two entries of the 54-byte blob — `2→4` for the item bought and `0→2` for the successor the game announced as newly developable, giving the blob its alphabet ✅ *0 locked / 2 developable / 4 developed* (only the `4`s are stored — `2` is re-derived at load). **Saves can also be written back**: three derived header fields (length at `+0x30`, payload length at `+0x8c`, `adler32` at `+0x8e`) are all that stand between a parse and a hand-written save that the title loads, and [`savegame_edit.py`](../../tools/re-capture/savegame_edit.py) re-wraps a real save byte-identically. That turned the blob's index space from blocked-on-story-progress into four probe saves — see the [economy note](arsenal-develop-economy.md) | +| `--build N` addressing (ordinal vs pak entry) | ✅ | [build-ordinal-vs-entry](structures/build-ordinal-vs-entry.md) + [`data/ordinal-entry-map.txt`](data/ordinal-entry-map.txt) | `screen --build N` indexes a **predicate-filtered list**, not the pak. Disc-wide: **21 of 24** build-bearing archives diverge, **18 at ordinal 0** — `--build 0` is entry **108** in each `GP_MAIN_GAME_*2D`, entry 24/26 in `GP_HANGAR_ARSENAL`/`GP_READY_ROOM`. `GP_TITLE` is the **only** archive whose ordinals 0–9 are the identity, which is the sole reason 207 of the corpus's 226 build citations are safe. ⚠️ `--all` swaps the predicate and **renumbers 18 archives**, so `--build N` and `--build N --all` differ. Instrument controlled against the CLI's own `screen list` on `GP_TITLE` (12 builds, `[10]→12`, `[11]→15`) — a first version using `parse_build` as the predicate **failed** that control, reporting ordinal==entry throughout. Audit of all 226 citations: 1 defect found and fixed (a five-row table in `ui-keyframe-time-unit.md` labelled "build 11" spanned builds 10 and 11 — placements all correct, only the label wrong); `GP_DIALOG --build 0` and `GP_DEBRIEFING_PILOTLOG --build 10` re-run and reproduce | ## Runtime / dynamic-capture technique diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index fe5b9ddd..b59d2285 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -169,6 +169,26 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the entry. **Write `entry N`, not `build N`, whenever the number leaves this repository.** + 🔴 **This entry was already here when I broke the rule.** So was + `ui-splash-addressing.md`, which says in as many words that the splashes are + entries 10/11/13/14, that `is_build` **rejects** them, and that they are + reachable *only* through `--all`. Two documents in my own corpus, and I still + ran `--build 10` bare and wrote three claims on the output. The failure was not + missing knowledge — it was **addressing a bundle by index without grepping for + the index first**. A rule written down is not a rule applied. Before any + `--build N`, run `screen list` on that pak and read the `entry` column; it costs + one command and it is the only step that would have caught this. + + 📌 **And the sweep says `GP_TITLE` was the mildest case on the disc** + ([`structures/build-ordinal-vs-entry.md`](structures/build-ordinal-vs-entry.md), + [`data/ordinal-entry-map.txt`](data/ordinal-entry-map.txt)): **21 of 24** + archives diverge, **18 of them at ordinal 0** — in the six `GP_MAIN_GAME_*2D` + paks `--build 0` is entry **108**. `GP_TITLE` is the *only* archive whose first + ten ordinals are the identity, which is why the corpus survived: almost + everything written about builds is about `GP_TITLE`, at ordinals 0–9. That is + luck in one archive, not a property of the format, and it does not extend to the + screens the port has left to do. + ## Runtime / emulator * **Look at the PNG** — and check its dimensions. diff --git a/docs/re/data/ordinal-entry-map.txt b/docs/re/data/ordinal-entry-map.txt new file mode 100644 index 00000000..09a09c89 --- /dev/null +++ b/docs/re/data/ordinal-entry-map.txt @@ -0,0 +1,41 @@ +# Build ORDINAL vs pak ENTRY, disc-wide +# instrument: crates/sylpheed-formats/examples/ordinal_entry_map.rs +# predicates: is_build (default) / is_composable (--all) -- the same two +# screen_builds() in crates/sylpheed-cli/src/main.rs:394 uses. +# +# CONTROL: GP_TITLE, against the CLI's own output -- +# $ sylpheed-cli screen list /disc/dat/GP_TITLE.pak +# 12 screen build(s) ... [10] entry 12 ... [11] entry 15 +# instrument: 12 builds, diverges at ordinal 10, [10]->12 [11]->15. MATCH. +# +# A FIRST instrument, using ui_layout::parse_build as the predicate, FAILED +# this control: it reported 16 builds for GP_TITLE with ordinal == entry +# throughout, and would have certified the exact bug it was built to find. + +GP_BUNK.pak 8 builds 🔴 diverges at ordinal 5: [5]->6 [6]->8 [7]->9 ⚠️ --all renumbers from [5]: entry 6 -> 5 +GP_CHALLENGE.pak 78 builds 🔴 diverges at ordinal 0: [0]->24 [1]->25 [2]->26 [3]->27 [4]->28 ⚠️ --all renumbers from [57]: entry 82 -> 81 +GP_DEBRIEFING_PILOTLOG.pak 18 builds 🔴 diverges at ordinal 0: [0]->3 [1]->5 [2]->10 [3]->11 [4]->22 ⚠️ --all renumbers from [2]: entry 10 -> 7 +GP_DIALOG.pak 105 builds 🔴 diverges at ordinal 0: [0]->2 [1]->3 [2]->5 [3]->6 [4]->7 ⚠️ --all renumbers from [0]: entry 2 -> 0 +GP_GAMEOVER.pak 10 builds 🔴 diverges at ordinal 0: [0]->2 [1]->3 [2]->4 [3]->5 [4]->6 ⚠️ --all renumbers from [0]: entry 2 -> 0 +GP_HANGAR_ARSENAL.pak 390 builds 🔴 diverges at ordinal 0: [0]->24 [1]->31 [2]->36 [3]->38 [4]->42 ⚠️ --all renumbers from [0]: entry 24 -> 0 +GP_LEADERBOARD.pak 4 builds 🔴 diverges at ordinal 0: [0]->6 [1]->11 [2]->40 [3]->42 ⚠️ --all renumbers from [0]: entry 6 -> 0 +GP_MAIN_GAME_D2D.pak 18 builds 🔴 diverges at ordinal 0: [0]->108 [1]->129 [2]->338 [3]->384 [4]->412 ⚠️ --all renumbers from [0]: entry 108 -> 0 +GP_MAIN_GAME_E2D.pak 18 builds 🔴 diverges at ordinal 0: [0]->108 [1]->129 [2]->338 [3]->384 [4]->412 ⚠️ --all renumbers from [0]: entry 108 -> 0 +GP_MAIN_GAME_F2D.pak 18 builds 🔴 diverges at ordinal 0: [0]->108 [1]->129 [2]->338 [3]->384 [4]->412 ⚠️ --all renumbers from [0]: entry 108 -> 0 +GP_MAIN_GAME_I2D.pak 18 builds 🔴 diverges at ordinal 0: [0]->108 [1]->129 [2]->338 [3]->384 [4]->412 ⚠️ --all renumbers from [0]: entry 108 -> 0 +GP_MAIN_GAME_J2D.pak 18 builds 🔴 diverges at ordinal 0: [0]->108 [1]->129 [2]->338 [3]->384 [4]->412 ⚠️ --all renumbers from [0]: entry 108 -> 0 +GP_MAIN_GAME_S2D.pak 18 builds 🔴 diverges at ordinal 0: [0]->108 [1]->129 [2]->338 [3]->384 [4]->412 ⚠️ --all renumbers from [0]: entry 108 -> 0 +GP_MISSION_LOG.pak 4 builds 🔴 diverges at ordinal 0: [0]->2 [1]->3 [2]->21 [3]->22 +GP_MISSION_SELECT.pak 66 builds 🔴 diverges at ordinal 0: [0]->3 [1]->5 [2]->10 [3]->11 [4]->12 ⚠️ --all renumbers from [0]: entry 3 -> 0 +GP_MOVIE_THEATER.pak 56 builds ordinal == entry throughout +GP_OPTIONS.pak 14 builds 🔴 diverges at ordinal 0: [0]->3 [1]->4 [2]->5 [3]->6 [4]->7 ⚠️ --all renumbers from [8]: entry 16 -> 15 +GP_PAUSE_MENU.pak 6 builds 🔴 diverges at ordinal 0: [0]->1 [1]->2 [2]->3 [3]->4 [4]->5 +GP_READY_ROOM.pak 60 builds 🔴 diverges at ordinal 0: [0]->26 [1]->30 [2]->37 [3]->39 [4]->44 ⚠️ --all renumbers from [0]: entry 26 -> 0 +GP_SAVE_LOAD.pak 18 builds 🔴 diverges at ordinal 0: [0]->2 [1]->4 [2]->16 [3]->19 [4]->46 ⚠️ --all renumbers from [2]: entry 16 -> 11 +GP_STAGE_CLEAR.pak 4 builds 🔴 diverges at ordinal 0: [0]->2 [1]->4 [2]->7 [3]->8 +GP_SYSTEM.pak 2 builds ordinal == entry throughout +GP_TITLE.pak 12 builds 🔴 diverges at ordinal 10: [10]->12 [11]->15 ⚠️ --all renumbers from [10]: entry 12 -> 10 +GP_TUTORIAL.pak 2 builds ordinal == entry throughout + +3 archives ordinal==entry, 21 diverge, 18 renumbered by --all +--- END --- diff --git a/docs/re/structures/build-ordinal-vs-entry.md b/docs/re/structures/build-ordinal-vs-entry.md new file mode 100644 index 00000000..a91a0450 --- /dev/null +++ b/docs/re/structures/build-ordinal-vs-entry.md @@ -0,0 +1,152 @@ +# ✅ `--build N` is an ordinal into a filtered list — and on 21 of 24 archives it is not the entry + +**Status:** ✅ **decoded**, disc-wide, instrument controlled against the CLI's own +output. The object decoded is *the addressing*, not a file field: how +`sylpheed-cli screen --build N` resolves, and where that number stops agreeing +with the pak entry index a reader will assume it means. + +## Why this was swept + +Last iteration I rendered `--build 10` and `--build 11` of `GP_TITLE` believing +they were the two splash screens, wrote three claims on the output, and every +downstream number validated. They are entries **12** and **15** — the loading +screens. I retracted it, and named the untested remainder in my own report: +*"how much else in the corpus used `--build` as an entry index — not swept."* +This is that sweep. + +## The mechanism + +`crates/sylpheed-cli/src/main.rs:394` builds the list: + +```rust +fn screen_builds(pak: &Path, all: bool) -> Result)>> { + for (i, e) in ar.entries().iter().enumerate() { + let keep = if all { ui_layout::is_composable(&bytes) } + else { ui_layout::is_build(&bytes) }; + if keep { out.push((i, bytes)); } // (entry, bytes) + } +} +``` + +`--build N` indexes `out`, so `N` counts only entries that **passed a predicate**. +Every entry the predicate rejects shifts every later ordinal down by one. + +## 🔴 The result: `GP_TITLE` is the mildest case on the disc + +[`data/ordinal-entry-map.txt`](../data/ordinal-entry-map.txt) — all 24 archives +holding builds: + +* **21 of 24 diverge.** Only `GP_MOVIE_THEATER`, `GP_SYSTEM` and `GP_TUTORIAL` + have ordinal == entry throughout. +* **18 of the 21 diverge at ordinal 0** — `--build 0` is *not* entry 0. The worst + are the six `GP_MAIN_GAME_*2D` paks, where `[0]` is entry **108**, and + `GP_HANGAR_ARSENAL` / `GP_READY_ROOM`, where `[0]` is entry **24** / **26**. +* `GP_TITLE` is the **only** archive whose first ten ordinals happen to be the + identity. It diverges at ordinal 10 and nowhere earlier. + +So the corpus was not lucky in general — it was lucky in the one archive almost +all of it is about, and unlucky in exactly the two indices I used. + +## ⚠️ Second foot-gun: `--all` renumbers, on 18 archives + +`--all` swaps the predicate, which changes the list, which changes the ordinals. +**`--build N` and `--build N --all` are not the same object** on 18 of 24 +archives — including `GP_TITLE`, where `--build 10` is entry 12 but +`--build 10 --all` is entry 10. Any citation of a build index that does not also +record whether `--all` was passed is under-specified. + +## ✅ Audit of every build citation in `docs/` + +226 citations of a build index across `docs/` (this file excluded). The 207 that +name an ordinal 0–9 of `GP_TITLE` are safe by the accident above. The **19** that +name an ordinal ≥10, or a non-`GP_TITLE` archive, are the ones that can be wrong, +so each was opened and checked rather than counted: + +| # | citations | verdict | +|---|---|---| +| 6 | carry `--all`, where ordinals 10/11 *are* entries 10/11 | ✅ correct | +| 4 | inside last iteration's retraction, already marked void | ✅ n/a | +| 2 | `GP_TITLE` `--build 10` bare — `ui-title-build-map.md:85` | ✅ correct: it names what comes back, the **loading screen** `pgloading_str.t32` | +| 2 | `GP_DIALOG --build 0` (`[0]` is entry 2) | ✅ re-run, reproduces | +| 2 | `GP_DEBRIEFING_PILOTLOG build 10` (`[10]` is entry **131**) | ✅ re-run, reproduces | +| 2 | prose about an unfinished sweep / the renumbering warning itself | ✅ n/a | +| **1** | `ui-keyframe-time-unit.md:59` | 🔴 **wrong, and fixed** | + +### The two re-runs + +Neither claim asserted an entry number — both cite *the output of a command*, so +a reader running it gets the same object the author had. Confirmed by running +them, not by arguing it: + +``` +$ sylpheed-cli screen info --build 0 --geometry /disc/dat/GP_DIALOG.pak +4 pceff03.t32 0: a=0 r=90 8: a=128 r=30 12: a=192 r=10 14: a=224 r=3 16: a=255 +5 pceff04.t32 0: a=0 r=90 8: a=128 r=30 12: a=192 r=10 14: a=224 r=3 16: a=255 + +$ sylpheed-cli screen info --build 10 --geometry /disc/dat/GP_DEBRIEFING_PILOTLOG.pak +5 pjeff24a.t32 382x140 0: 335,49 210%,210% a=53 r=90 (one keyframe) +``` + +Both stand unchanged. + +### 🔴 The one real defect the sweep found + +[`ui-keyframe-time-unit.md`](../ui-keyframe-time-unit.md) headed a five-row table +*"declared element (build 11)"*. Its first row is `palogo_sqex.t32` — and +`--all --build 11` does not contain it: + +``` +--all --build 10 palogo_sqex, palogo_sqex_eff +--all --build 11 palogo_gamearts{,_eff}, palogo_seta{,_eff}, palogo_anima{,_eff} +``` + +The rows span **two** bundles. All five placements re-verified and are correct — +`palogo_sqex.t32` 666×68 @ (309,330) in build 10, `palogo_gamearts_eff.t32` +521×91 @ (379,154) in build 11 — so the measurement the table supports (the ramp +is linear) is untouched. Only the label was wrong. Fixed: the table now carries a +per-row bundle column. + +That is the shape worth remembering: **the index error did not corrupt the +numbers, it corrupted the sentence around them**, and the numbers kept validating. + +## ⚠️ For the port: this is an addressing hazard, not a decoding one + +If you address bundles by **pak entry index** — which +[`ui-splash-addressing.md`](../ui-splash-addressing.md) recommends for the +splashes — and cross-reference a doc that says "build 6", those are different +objects on 21 archives. When quoting an index, say which kind it is. Our docs +now say *ordinal* or *entry*. + +## Refutation attempted — `sylpheed-port`'s corrected mid-ramp test — **survives** + +The port withdrew their own `title_jp` "separating case" this iteration, on the +grounds that `ptlogo_all_eff` **holds** a=127 from t=112 to t=246 rather than +ramping through it, so their old `0 < alpha < 255` test had counted a steady +semi-transparent glow as a transition. Their whole correction — and the 5/5 +result they say survives it — rests on the keyframes of that one element, which +is disc data and therefore mine to check. Quoted against the disc: + +``` +$ sylpheed-cli screen info --build 7 --geometry /disc/dat/GP_TITLE.pak +29 ptlogo_all_eff.t32 538x255 0: a=0 76: a=0 112: a=127 246: a=127 258: a=0 + (kind 0x3000, 200%,200%, position constant) +``` + +Their quote `[0:a0 76:a0 112:a127 246:a127 258:a0]` is **exact**, and a=127 is +held flat across 134 units with nothing else moving. It is a plateau. The +refutation fails and their correction stands — including the part that costs +them, since it removes the one case that would have separated their hypothesis +from mine. + +⚠️ Note what this does *not* establish: that a=127 is a glow. That reading is +theirs and rests on kind `0x3000` and the 200 % scale, neither of which I have +tested against the running game. What I checked is the keyframes. + +## What this does not settle + +* Whether anything **outside `docs/`** — scripts under `tools/`, committed test + fixtures — hard-codes a build ordinal for a diverging archive. I swept the + prose, not the code. +* The three identity archives are identity *today*. Nothing enforces it; a change + to `is_build` moves every ordinal on the disc. This is a property of a + predicate, not of the format. diff --git a/docs/re/ui-keyframe-time-unit.md b/docs/re/ui-keyframe-time-unit.md index c2403112..32fb9ebc 100644 --- a/docs/re/ui-keyframe-time-unit.md +++ b/docs/re/ui-keyframe-time-unit.md @@ -56,13 +56,20 @@ reason to measure in this unit rather than with a stopwatch. Every sprite in the capture lands on its declared placement: -| capture quad | declared element (build 11) | declared placement | -|---|---|---| -| `666x65 @ (307,331)` | `palogo_sqex.t32` 666×68 | (309,330) | -| `525x90 @ (378,155)` | `palogo_gamearts_eff.t32` 521×91 | (379,154) | -| `262x108 @ (512,306)` | `palogo_seta_eff.t32` 261×110 | (511,305) | -| `499x72 @ (390,162)` | `palogo_gamearts.t32` 500×71 | (390,164) | -| `243x86 @ (518,317)` | `palogo_seta.t32` 240×89 | (521,316) | +⚠️ The rows span **two** bundles, not one: `palogo_sqex` is in `--all --build 10` +and the `gamearts`/`seta` group is in `--all --build 11`. This table said +"build 11" over all five until a corpus-wide index audit +([`structures/build-ordinal-vs-entry.md`](structures/build-ordinal-vs-entry.md)) +checked the header against the bundle. Every placement below re-verified and +correct; only the label was wrong. + +| capture quad | declared element | in bundle | declared placement | +|---|---|---|---| +| `666x65 @ (307,331)` | `palogo_sqex.t32` 666×68 | `--all --build 10` | (309,330) | +| `525x90 @ (378,155)` | `palogo_gamearts_eff.t32` 521×91 | `--all --build 11` | (379,154) | +| `262x108 @ (512,306)` | `palogo_seta_eff.t32` 261×110 | `--all --build 11` | (511,305) | +| `499x72 @ (390,162)` | `palogo_gamearts.t32` 500×71 | `--all --build 11` | (390,164) | +| `243x86 @ (518,317)` | `palogo_seta.t32` 240×89 | `--all --build 11` | (521,316) | (A quad runs a few pixels under its sprite; that offset is already recorded in `ui-title-paint-order-capture.md` and is not what is being measured here.)