re: sweep the disc for the ordinal foot-gun -- GP_TITLE was the mildest case

Last iteration I retracted three claims because `--build 10/11` on GP_TITLE are
entries 12/15, and named the untested remainder in my own report: how much else
in the corpus used a build ordinal as an entry index. This is that sweep.

`screen --build N` indexes a predicate-filtered list, so every rejected entry
shifts every later ordinal. Disc-wide: 21 of 24 build-bearing archives diverge,
18 of them at ordinal 0 -- `--build 0` is entry 108 in each GP_MAIN_GAME_*2D,
24/26 in GP_HANGAR_ARSENAL/GP_READY_ROOM. GP_TITLE is the ONLY archive whose
first ten ordinals are the identity, which is the sole reason 207 of the
corpus's 226 build citations are safe. Second foot-gun: `--all` swaps the
predicate and renumbers 18 archives, so `--build N` and `--build N --all` are
not the same object.

The instrument failed its control first. A version using parse_build as the
predicate 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 predicates screen_builds() uses and reproduces `screen list` on
GP_TITLE exactly.

Audited all 226 citations. One real defect: a five-row table in
ui-keyframe-time-unit.md headed "declared element (build 11)" spans builds 10
and 11 -- palogo_sqex is in 10. All five placements re-verified and correct, so
the linear-ramp measurement is untouched; only the label was wrong. Fixed with a
per-row bundle column. GP_DIALOG --build 0 and GP_DEBRIEFING_PILOTLOG --build 10
re-run and reproduce.

Refutation attempted: sylpheed-port's corrected mid-ramp test rests on
ptlogo_all_eff holding a=127 from t=112 to t=246. Their quote is exact and it is
a plateau. The refutation fails; their correction stands.

METHOD already carried the rule I broke, and ui-splash-addressing already said
the splashes need --all. The failure was not missing knowledge -- it was
addressing a bundle by index without grepping for the index first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 13:20:41 +00:00
parent f102cf9209
commit c364cde476
7 changed files with 375 additions and 7 deletions

View File

@@ -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<T> 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 09 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

View File

@@ -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 09. 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.

View File

@@ -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 ---

View File

@@ -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<Vec<(usize, Vec<u8>)>> {
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 09 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.

View File

@@ -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.)