Compare commits
1 Commits
chore/agen
...
recover/op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57eda14e4 |
@@ -15,7 +15,7 @@
|
||||
"boot": [
|
||||
{
|
||||
"screen": "publisher_logo",
|
||||
"why": "The SQUARE ENIX wordmark is the first thing the boot shows -- RE agent, 2026-08-29. Entry 10 of the pair; 13 is its region twin and the port shows one, not both."
|
||||
"why": "The SQUARE ENIX wordmark is the first thing the boot shows -- RE agent, 2026-08-29. Entry 10 of the pair; 13 is its region twin and the port shows one, not both. \ud83d\udccc SOURCE, added 2026-09-01: the boot's screen order and dwells are derived from GP_TITLE's own entries -- see docs/port/FORMAT.md for the export shape and docs/re/ui-title-build-map.md for which entry is which screen. The order here is not authored; it is what the archive declares."
|
||||
},
|
||||
{
|
||||
"screen": "developer_logos",
|
||||
@@ -25,31 +25,651 @@
|
||||
"video": "ADV",
|
||||
"why": "HANDOFF Q9, DECODED from the movie manifest: ADVERTISE_MOVIE -> ADV.wmv, and the boot intro and the attract movie are the SAME asset -- there is no separate boot slot. Its POSITION here (after the developer logos, before the title) is measured, not decoded: it is the order the RE agent watched the game boot in.",
|
||||
"skippable": true,
|
||||
"skippable_why": "HANDOFF Q9: one (A) press skips a movie -- measured, title reached at 57 s against a 193 s baseline."
|
||||
"skippable_why": "HANDOFF Q9: one (A) press skips a movie -- measured, title reached at 57 s against a 193 s baseline.",
|
||||
"skippable_kind": "measured"
|
||||
},
|
||||
{
|
||||
"screen": "title",
|
||||
"why": "HANDOFF Q2/Q6: the boot reaches the title after the intro movie. The port holds here -- nothing takes the title's place until P5 gives it somewhere to go."
|
||||
"overlay": {
|
||||
"screen": "press_start",
|
||||
"clock": "shared",
|
||||
"why": "MEASURED, 2026-08-29, docs/re/title-plate-delay-measured.md on branch auto/no-disc-and-menu-captures at 5b0a6e6 (NOT on main when this was written). The boot title shows build 4 ALONE and the `PRESS (A) BUTTON` plate -- build 2 -- arrives later. This is the ONE case in the port where two builds are drawn at once.",
|
||||
"no_constant_why": "THERE IS NO AUTHORED DELAY HERE, AND THERE WAS ONE FOR ONE ITERATION. The first version of this block carried `after_settle_seconds: 2.13`, taken from the RE agent's instruction. The port refuted that instruction with arithmetic off the disc -- build 2 has a group of its own, and starting it at settle put the plate 3.97 s late -- and the corrected answer needs no constant at all: BOTH BUILDS RUN ON ONE CLOCK, STARTED TOGETHER, and the plate arrives at its own declared t=236 (CORRECTED 2026-09-01 from t=238, which is the last opaque frame rather than the arrival). `clock: \"shared\"` is that, spelled out rather than implied by the absence of a delay field. \ud83d\udccc SOURCES, added 2026-09-01 in the uncited-why backfill: the plate's arrival is docs/re/title-plate-delay-measured.md and its pulse is docs/re/structures/plate-pulse-measured.md. \ud83d\udd34 AND `clock: \"shared\"` IS AUTHORED FROM OUR OWN ARITHMETIC, NOT MEASURED. Nobody has watched whether build 2's group starts with build 4's; it is the reading that reconciles the oracle's 2.13 s IF the settle anchor is t=118. See docs/port/plate-arrival-halves.md and BLOCKED.md H3.",
|
||||
"arithmetic_why": "Why one clock reproduces the measurement, checked against this export rather than taken on trust: build 4's effect quads `pteff01`, `pteff02` and `ptlogoall_eff` end their ramps together at t=118; `ptbtn00` reaches alpha 255 at t=236; the difference is 118 units = 1.967 s at 60 units/s. The oracle measured 2.138 s and 2.132 s. The gap is presentation rate: the emulator presents at 28.1 fps against a nominal 30, and the corpus had independently measured the idle title at 28.5 fps before these runs. \ud83d\udd34 CORRECTED 2026-09-01: this said `ptbtn00` reaches 255 at t=238 and that the difference is 120 units = 2.000 s. It reaches 255 at t=236 and HOLDS to 238, so 238 is the last opaque frame, not the arrival; 236 - 118 = 118. The port printed the contradiction in one sentence on every boot. The correction moves the reconciliation by 0.033 s and overturns nothing -- see docs/port/plate-arrival-halves.md. \ud83d\udd34 AND THE ANCHOR IS NOW OPEN. The oracle defines \"title settled\" operationally, as its glyph counter first reading the no-plate value 154. This export offers TWO anchors 42 units apart: t=118 (the effect quads) and t=160 (`ptcopyright` at full alpha -- the LAST element to finish building in, and the only one made of glyphs). This line picked 118, while `ScreenView.settle_time()` returns 160 and the boot prints `settles at t=160`, so one binary holds both. Asked in BLOCKED.md H3; not guessed here. \ud83d\udccc SOURCE: the pulse period and its phase behaviour are in docs/re/structures/plate-pulse-measured.md and docs/re/structures/plate-pulse-phase-lock.md, with the raw series in docs/re/data/plate-pulse-timeseries.txt. \u2705 AUDITED 2026-09-01: the corpus's 28.5 fps is a genuinely independent leg -- a different quantity (idle-title presentation rate), measured BEFORE these runs, so it could have come out disagreeing. It agrees to 1.4 %.",
|
||||
"the_premise_that_failed_why": "The port's own, and it is worth keeping because it will bite again: `rest.t` IS NOT WHEN A SCREEN SETTLES. It is the last hold keyframe before the exit. Reading it as the settle put build 4's arrival at 4.35 s instead of 1.97 s, and every reconciliation computed from it came out wrong by exactly that error. `ScreenView.settle_time()` still uses rest.t -- see docs/port/BLOCKED.md. \ud83d\udd34 THE EXAMPLE THIS CITED IS GONE, THOUGH THE CONCLUSION IS NOT. It read \"`ptlogo1` has rest.t=251 and stops MOVING at t=42\". In the CURRENT export `ptlogo1.rest.t` is 42 -- equal to when it stops moving. The record-layout fix repaired precisely that element, and the entry was never re-derived under it (REFUTED.md now carries this at \ud83d\udfe1 \u27e8our-reader\u27e9). rest.t is still wrong for transients -- `ptlogo_back2eff1` is a two-frame flash whose rest.t=54 is the flash PEAK -- and for `pteff00`, whose rest.t=16 sits at the end of the fade-FROM-black while a fade-TO-black runs 261..269. Re-derived 2026-09-01: docs/port/plate-arrival-halves.md. \ud83d\udd34 AND IT IS NOT THIS DEFECT'S CAUSE. The plate's ARRIVAL is a declared keyframe (transparent to t=214, opaque at t=236), not a rest pose; rest.t=236 only chooses where `holding` parks it, and 236 is that ramp's own peak. Confirmed on a filmed boot with rest.t untouched: the onset is bracketed within one frame of 214.",
|
||||
"scope_why": "Attached to the BOOT STEP, not to the `title` screen, and that is deliberate. What was measured is the boot title. Whether the title shows the plate when it is REACHED AGAIN -- by (B) from the main menu, or after the attract movie -- is not measured, and putting the overlay on the screen would quietly claim it is. \ud83d\udccc SOURCE, added 2026-09-01: the plate belongs to the boot's overlay step rather than to the title screen because its arrival is measured against the boot clock -- docs/re/title-plate-delay-measured.md. \ud83d\udd34 STALE CLAUSE, CORRECTED 2026-09-01: this said \"Whether the title shows the plate when it is REACHED AGAIN -- by (B) from the main menu, or after the attract movie -- is not measured\". It IS measured now, and has been since 2026-08-30: after (B) from the menu the plate is re-drawn, pressed at 351.2 s with its pulse back at 358.5 s (the Decoder, nav-autorepeat-and-settled-b data). The port re-arms the overlay on arrival at the title by any path, and that is correct. What stayed true is the structural half -- the declaration lives on the boot STEP and is looked up from there, so a screen that gains an overlay gets it on both paths at once. \u26a0\ufe0f What is STILL not measured is whether the returned plate FADES or appears at once; the 7.3 s between press and pulse is consistent with a transition plus the declared 214->236 fade, but that is consistency, not a measurement of the ramp on this path.",
|
||||
"no_pulse_why": "The port draws the plate arriving and then holding. It does not pulse it. The RE agent identifies the pulse as the plate's FOCUS RECORD `ptbtn00f` -- a glow ramping 0x00 to 0x50 and back, t=6..105 -- not as a loop of `ptbtn00`'s own group, which was the port's earlier reading and was wrong. Looping that record is a candidate the port has NOT taken: its group is 105 timed units plus an AUTHORED 24-unit exit ramp, and hitting the measured 2.24 s mean requires composing that authored constant with a loop assumption, which is tuning rather than measuring. Filed in BLOCKED.md. \ud83d\udccc SOURCE, added 2026-09-01: docs/re/structures/plate-pulse-measured.md, and the phase-lock caveat that bounds what a gated capture can show is docs/re/structures/plate-pulse-phase-lock.md."
|
||||
},
|
||||
"why": "HANDOFF Q2/Q6: the boot reaches the title after the intro movie. This is the LAST step, and a last step is where the sequence stops rather than fading out -- a boot that ends by fading to black looks like a boot that crashed. P5 gave the title somewhere to go, but that is a HANDOVER and not another boot step: `--boot` still stops here, and `--boot --play` hands the same held title to the menu flow, where (A) opens TITLE_MENU. Kept as a stop rather than folded into `screens` because what the boot does is authored from a measured sequence, and what (A) does is a separate measurement."
|
||||
}
|
||||
],
|
||||
"dwell": {
|
||||
"_": [
|
||||
"DELIBERATELY EMPTY. Each screen's dwell is its own keyframe group -- the",
|
||||
"publisher wordmark reaches its hold at t=235 (3.92 s) and the developer",
|
||||
"logos at t=190 (3.17 s), both read from the disc. Holding beyond that would",
|
||||
"be a number nobody has measured, so the sequencer holds for zero extra time",
|
||||
"and the pacing is the disc's own.",
|
||||
"NOT SET -- because the dwell is DECLARED, and the port already plays it.",
|
||||
"",
|
||||
"When a capture times the real boot, the extra hold per screen goes here."
|
||||
"This key has now been wrong in two opposite directions, and the second was",
|
||||
"mine, so both are recorded.",
|
||||
"",
|
||||
"It first said 'a screen's dwell is its OWN keyframe group'. Then GP_TITLE",
|
||||
"build 4 was measured dwelling ~1100 presented frames against a declared ~120,",
|
||||
"and I generalised that into 'the boot is KNOWN TOO FAST [refuted] on both splashes'.",
|
||||
"\ud83d\udd34 THAT WAS AN OVER-CORRECTION and it is withdrawn. Build 4 is the title: its",
|
||||
"exit is caused by something outside its timeline, so it holds. A splash's exit",
|
||||
"is caused by nothing, so it plays its declared timeline and leaves. The title",
|
||||
"is the exception, not the rule, and one screen was never enough to overturn",
|
||||
"the other two.",
|
||||
"",
|
||||
"MEASURED 2026-08-29 by the Decoder over 3 cold boots",
|
||||
"(docs/re/structures/boot-splash-dwells-are-declared.md):",
|
||||
"",
|
||||
" publisher declared t=0..255 = 4.250 s corpus 4.30 / 4.60 / 4.37",
|
||||
" developer declared t=0..210 = 3.500 s corpus 3.51 / 3.50 / 3.37",
|
||||
"",
|
||||
"The developer agrees to 1.1 %, two of its three runs to 0.3 %. The port emits",
|
||||
"4.400 s and 3.650 s -- each declared value plus the 9-unit black hold, exactly.",
|
||||
"So the pacing was right all along and nothing changes in the code.",
|
||||
"",
|
||||
"\ud83d\udd34 AND THE UNIT STAYS UNITS, NOT SECONDS. The same two dwells timed in the",
|
||||
"Decoder's own container came out 15-20 % LONGER than both the declared values",
|
||||
"and the corpus -- same disc, same timeline -- and three independent readings",
|
||||
"of that container's rate disagree with each other. A seconds figure records",
|
||||
"one emulator's pacing on one run. The units are on the disc. If anything ever",
|
||||
"goes in `dwell` it is an extra hold in UNITS, and only for a screen that is",
|
||||
"measured to wait beyond its group."
|
||||
]
|
||||
},
|
||||
"navigation": {
|
||||
"_": [
|
||||
"MEASURED off the running game, HANDOFF Q5 -- none of it is on the disc.",
|
||||
"It lives here rather than in GDScript so that a reader can see it is a",
|
||||
"measurement and delete it the day a field on the disc states it."
|
||||
],
|
||||
"wrap": true,
|
||||
"wrap_why": "HANDOFF Q5: up/down move one item and WRAP at both ends. Measured on the 5-item main menu AND the 3-item EXTRAS, so it is a menu rule and not a per-screen one (docs/game/navigation.md, branch auto/no-disc-and-menu-captures 3a87a26).",
|
||||
"wrap_kind": "measured",
|
||||
"left_right": "nothing",
|
||||
"left_right_why": "HANDOFF Q5: left/right do nothing. Measured. Implemented as an explicit no-op rather than by omission, so that 'we never wired it' and 'the game ignores it' are distinguishable in the code.",
|
||||
"left_right_kind": "measured",
|
||||
"input_during_transition": "ignored",
|
||||
"input_during_transition_why": "AUTHORED, and NOT measured -- nobody has watched what the game does with a button pressed mid-fade. Ignoring is the choice that invents the least: it cannot queue a press the game might have dropped. Ask the RE agent before relying on it. \ud83d\udccc WHERE THE ASK LIVES, added 2026-09-01: docs/port/BLOCKED.md carries it, and until now this why said \"ask the RE agent\" without naming where the question is recorded -- a pointer with no destination. An `authored` kind still needs a citation, because the thing to cite is the OPEN QUESTION the choice stands in for; without it, an invented value and a placeholder for a measurement read the same.",
|
||||
"input_during_transition_kind": "authored",
|
||||
"auto_repeat": false,
|
||||
"auto_repeat_why": "MEASURED 2026-08-30, Decoder daf8f47: a 2.0 s held (down) moves the cursor EXACTLY ONCE. Their counter passes its own control first -- a single 0.12 s tap gives exactly 1 spike, the hold gives 1, move spike 0.0202-0.0220 against a 0.0003-0.0038 floor. The port's edge-triggered _input already behaved this way; what changed is that it is now a MEASUREMENT rather than an unexamined consequence of how the handler was written. HANDOFF Q5's 'up / down' row is split at the source: one-item-per-press (evidenced by the 4-press wrap count) from no-auto-repeat (which had nothing until this run).",
|
||||
"auto_repeat_kind": "measured"
|
||||
},
|
||||
"screens": {
|
||||
"_": [
|
||||
"What each button does. NOT FILLED IN -- that is P5. HANDOFF Q4 measured the",
|
||||
"destination screens and the RE agent later decoded that a transition is a",
|
||||
"lookup by NAME, giving a candidate vocabulary (TITLE_SCREEN, TITLE_MENU,",
|
||||
"LOADING, DIFFICULTY, EXTRA_MENU, TUTORIAL_MENU). Those are the right `goto`",
|
||||
"targets when this is written, marked as the name match they are."
|
||||
]
|
||||
"What each button does. The NAVIGATION ORDER is not here -- it is derived,",
|
||||
"in each screen file's `buttons` (button-role elements sorted by resting Y).",
|
||||
"Only the destinations, the initial focus and the cancel target are",
|
||||
"authored, because only those are measurements or decisions.",
|
||||
"",
|
||||
"`goto` is an EXPORTED SCREEN NAME or null. `goto_name` is the game's own",
|
||||
"screen vocabulary from the decoded transition lookup -- carried so the",
|
||||
"binding is not lost, and marked below as the NAME MATCH it is, never as a",
|
||||
"measurement (HANDOFF: the strings are what the call sites reference, not",
|
||||
"proven arguments, and the same list mixes in TEXT_FONT and GAMMA_RGB).",
|
||||
"",
|
||||
"`goto: null` with a `blocked` note means the destination screen is real and",
|
||||
"measured but is NOT IN THIS EXPORT -- it lives in another archive. That is a",
|
||||
"milestone boundary, not an unknown."
|
||||
],
|
||||
"title": {
|
||||
"on_accept": {
|
||||
"goto": "main_menu",
|
||||
"goto_name": "TITLE_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
"screens are measured; the ids are a name match onto the executable's class",
|
||||
"names.\" So `TITLE_MENU` is a string that exists in the executable and plausibly",
|
||||
"denotes this screen -- nothing observed binds it to this transition.",
|
||||
"",
|
||||
"It is carried so a reader can search for it and so the port never has to",
|
||||
"invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
"a screen file, and this field is documentation.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
"labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
"claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
"as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
"argument reads as evidenced and is not."
|
||||
],
|
||||
"why": "MEASURED, HANDOFF: (A) on the title opens the main menu, with (A) on the boot title as the control in the same run."
|
||||
},
|
||||
"on_cancel": null,
|
||||
"on_cancel_why": "MEASURED 2026-08-30, Decoder daf8f47, docs/re/data/nav-autorepeat-and-settled-b.txt: twenty seconds after a delivery-confirmed B the screen is still the title with PRESS (A) BUTTON up. The run waited for the PLATE PULSE -- the title's own settled signature -- before pressing, which is exactly what the earlier confounded attempt did not. This cell briefly said 'MEASURED, HANDOFF Q5' on no evidence, then said AUTHORED once that was caught; it is now measured for real. Value unchanged throughout: null.",
|
||||
"on_cancel_kind": "measured"
|
||||
},
|
||||
"main_menu": {
|
||||
"initial_focus": "ptbtn01",
|
||||
"initial_focus_kind": "measured",
|
||||
"focus_persists": true,
|
||||
"focus_persists_kind": "measured",
|
||||
"focus_persists_why": [
|
||||
"MEASURED 2026-08-30, Decoder: the main menu REMEMBERS ITS CURSOR across a",
|
||||
"round trip through the title. (B) out and (A) back returns to the item you",
|
||||
"left, not to a default. Their control passed first -- two delivery-confirmed",
|
||||
"DOWNs moved the cursor exactly two items before the round trip, so the",
|
||||
"cursor demonstrably was not where it started.",
|
||||
"",
|
||||
"The port reset to `initial_focus` on every entry, so this was a real defect",
|
||||
"and not a refinement: a player who moved to EXTRAS, pressed (B), then (A),",
|
||||
"landed back on NEW GAME.",
|
||||
"",
|
||||
"\ud83d\udd34 SCOPED TO THIS SCREEN ON PURPOSE, and the scope is the authored part.",
|
||||
"The measurement is of the MAIN MENU. Making it a menu-wide rule would be",
|
||||
"n=1 wearing a rule's clothes -- and here it would actively contradict a",
|
||||
"measurement, because `extras` opens on MISSION SELECT as a MEASURED initial",
|
||||
"focus, and a remembered cursor would override it on re-entry. `wrap` is a",
|
||||
"menu rule because it was measured on two screens; this was measured on one.",
|
||||
"",
|
||||
"\u26a0\ufe0f WHAT IS NOT KNOWN: whether the memory survives a return to the BOOT",
|
||||
"(as opposed to the title), and whether any other screen has it. Ask before",
|
||||
"widening this.",
|
||||
"",
|
||||
"\ud83d\udd34 CORRECTED 2026-08-30, SAME DAY, by the Decoder: the paragraph above argued",
|
||||
"the scope from `extras` having a MEASURED initial focus that a remembered",
|
||||
"cursor would override. That is a good reason to be CAUTIOUS and NOT a finding",
|
||||
"that `extras` resets. Nothing has measured what a submenu's own cursor does on",
|
||||
"re-entry: the corpus has EXTRAS' opening item from ONE entry, and (B) restoring",
|
||||
"the PARENT's focus 4/4, and neither answers it.",
|
||||
"",
|
||||
"So `focus_persists: false` everywhere else is THE PORT'S DEFAULT, not the",
|
||||
"game's behaviour. It invents the least and it preserves the one measurement",
|
||||
"there is. `tools/port/contract-check` asserts only the main-menu half against",
|
||||
"the contract and reports the scope as a GUARD, because for one iteration it",
|
||||
"asserted non-persistence as though it had been measured -- which would have",
|
||||
"held the port to the wrong behaviour and passed while doing it.",
|
||||
"",
|
||||
"\u2754 The Decoder is measuring EXTRAS re-entry now. Do not build on the",
|
||||
"non-persistence half until it returns.",
|
||||
"",
|
||||
"\ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: docs/re/data/focus-persists-across-title.txt carries the round trip, and docs/re/data/extras-focus-resets.txt carries the contrasting submenu result that keeps this scoped to one screen."
|
||||
],
|
||||
"initial_focus_why": [
|
||||
"MEASURED 2026-08-30 (later) -- `NEW GAME` on a fresh boot, 2/2 fresh boots,",
|
||||
"both the FIRST menu entry. Decoder, HANDOFF `bf9e07f`, section \"correcting",
|
||||
"today's focus delivery\"; ring row y=225.5 against a measured 79.25 px step,",
|
||||
"data in docs/re/data/menu-focus-reader-offset.txt.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS FIELD WAS `authored` UNTIL NOW AND THE UPGRADE IS NOT BECAUSE IT",
|
||||
"AGREES WITH ME. The value did not change; its standing did. The confirmation",
|
||||
"is a direct reading of a fresh boot's first menu entry, independent of the",
|
||||
"reasoning that chose NEW GAME here -- and the Decoder had said explicitly that",
|
||||
"my agreeing with their records was no evidence, which was correct at the time.",
|
||||
"",
|
||||
"\u2705 AND IT SURVIVES A REBOOT -- MEASURED 2026-08-31. Six fresh boots all",
|
||||
"opened on NEW GAME, and THREE of them followed a session that ended with the",
|
||||
"cursor on EXTRAS or OPTIONS. That is what makes it a test of persistence",
|
||||
"rather than six repetitions of the same start.",
|
||||
"",
|
||||
"\u26a0\ufe0f REACH, and it is the Decoder's own caveat rather than mine: every one of",
|
||||
"those sessions ended with the emulator KILLED, not shut down cleanly. A game",
|
||||
"that writes menu state on a clean exit never gets the chance, so this",
|
||||
"measures 'does not survive a KILLED session'. If a real console remembers a",
|
||||
"cursor across a power cycle, that does not contradict this.",
|
||||
"",
|
||||
"\u26a0\ufe0f WHY 'FIRST ENTRY' IS LOAD-BEARING: the menu REMEMBERS ITS CURSOR (see",
|
||||
"`focus_persists`), so any reading not taken on a fresh boot's first entry is",
|
||||
"measuring HISTORY, not what the screen opens on. That objection is what",
|
||||
"invalidated the earlier TUTORIAL/NEW GAME disagreement, and this measurement",
|
||||
"is the one that is immune to it.",
|
||||
"",
|
||||
"The superseded reasoning is kept below, because it is what made the wait cheap:",
|
||||
"the field existed and was labelled honestly, so arriving at a measurement was a",
|
||||
"label change and not an archaeology problem.",
|
||||
"",
|
||||
" (was) AUTHORED, standing in for HANDOFF Q5, which measured that initial focus is NOT STABLE: four boots of the same harness opened on TUTORIAL, TUTORIAL, NEW GAME, NEW GAME. A port has to open on something. ptbtn01 (NEW GAME) is picked because it is one of the two states actually observed and it is the top item, so a reader can predict it. It is a CHOICE. Delete this the day the RE agent finds what selects it. CORROBORATED 2026-08-29, and still not decoded: the committed capture live-main-menu.png has NEW GAME focused. Identified by rendering all five focus states and taking the minimum difference -- 531 differing pixels against 6080-7094 for the others, an 11.5x margin -- with the method controlled on live-main-menu-options-focused.png, whose answer is in its filename and which it picks by 4.7x. That means the port's choice matches the state of one committed frame. It does NOT make focus stable: Q5's four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME, and this identifies one frame rather than a rule. Delete this entry the day something says what SELECTS it. TIGHTENED 2026-08-29: Q5 now has SIX boots, and the shape is sharper than 'unstable' -- TUTORIAL x3, NEW GAME x3, and NO OTHER ITEM EVER OBSERVED. So it is not uniform over five buttons; whatever selects it has to explain a two-way split. That does not change this choice (NEW GAME remains one of exactly two observed states, and it is the state of the committed capture) but it does change what would REFUTE it: a boot opening on LOAD GAME, OPTIONS or EXTRAS would break the two-way shape, and a rule that predicts the split would delete this entry outright.",
|
||||
" (was) ",
|
||||
" (was) \u2705 CONSISTENT WITH THE ONE CAPTURE, measured 2026-08-30. Rendering each of the",
|
||||
" (was) five buttons focused against `live-main-menu.png` gives 0.0705 % for ptbtn01",
|
||||
" (was) and 0.72-0.84 % for the other four -- a 10x discrimination. So that capture",
|
||||
" (was) shows NEW GAME focused, and the authored choice matches it.",
|
||||
" (was) ",
|
||||
" (was) \u26a0\ufe0f THIS DOES NOT OVERTURN Q5. Q5 measured initial focus as UNSTABLE across",
|
||||
" (was) four boots; one capture showing ptbtn01 is consistent with that and does not",
|
||||
" (was) contradict it. What the measurement establishes is narrower and still worth",
|
||||
" (was) having: the port's focus rendering is distinctive enough that a capture",
|
||||
" (was) identifies which button is focused, and this authored value is not at odds",
|
||||
" (was) with the only frame we can check it against. It stays AUTHORED."
|
||||
],
|
||||
"on_cancel": {
|
||||
"goto": "title",
|
||||
"goto_name": "TITLE_SCREEN",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
"screens are measured; the ids are a name match onto the executable's class",
|
||||
"names.\" So `TITLE_SCREEN` is a string that exists in the executable and plausibly",
|
||||
"denotes this screen -- nothing observed binds it to this transition.",
|
||||
"",
|
||||
"It is carried so a reader can search for it and so the port never has to",
|
||||
"invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
"a screen file, and this field is documentation.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
"labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
"claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
"as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
"argument reads as evidenced and is not."
|
||||
],
|
||||
"kind": "measured",
|
||||
"why": "MEASURED 2026-08-30, delivery-confirmed (B = 0x5801), 73.5 % of pixels changed, and both captures name themselves. Latency <= 0.4 s and NO loading screen in between, which matters because the disc carries four pgloading_* screens. This entry previously read 'likely but UNPROVEN': it had been seen once without a capture, and the title ALSO returns on its own after ~8-10 s idle, so an observer could not tell a response from a timeout. The <= 0.4 s latency is what kills that confound -- it is twenty times faster than the idle return. Decoder 86a8ce7, menu-navigation-semantics.md row 'B on the main menu', docs/re/data/b-on-main-menu.txt."
|
||||
},
|
||||
"buttons": {
|
||||
"ptbtn01": {
|
||||
"label": "NEW GAME",
|
||||
"goto": null,
|
||||
"goto_name": "DLG_SELECT_DIFFICULTY",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"\u2705 CORRECTED 2026-08-31: this read `DIFFICULTY`, and the destination is a",
|
||||
"DIALOG rather than a GamePart -- `DLG_SELECT_DIFFICULTY`, `GP_DIALOG.pak`",
|
||||
"entries 2/3 [see the withdrawal below]. Decoder, TWO arguments [corrected below]; the geometry one is",
|
||||
"re-derived here with this port's own reader: entries 2 and 3 are the ONLY",
|
||||
"builds in that archive carrying `pcbtn00`-`pcbtn03`, at design rows",
|
||||
"259/329/399/469, spacing exactly 70. See",
|
||||
"`crates/sylpheed-export/examples/dialog_rows.rs`.",
|
||||
"",
|
||||
"\ud83d\udd34 SO THE FOUR EXTERNAL DESTINATIONS ARE NOT UNIFORM: three open GameParts",
|
||||
"and this one opens a dialog. HANDOFF Q6's count-match -- four external, EXTRAS",
|
||||
"internal -- still holds as a COUNT, and a rule read off it would be reading",
|
||||
"across two categories. The Decoder sent that count with disc support",
|
||||
"yesterday and weakened it themselves today; recorded at the weaker strength.",
|
||||
"",
|
||||
"\u2705 THE REACH IS NOW BOUNDED -- 2026-08-31, and both agents scanned for it.",
|
||||
"",
|
||||
"It read: \"another four-button dialog with the same rows would be",
|
||||
"indistinguishable by this evidence\". The Decoder searched every build in",
|
||||
"every pak for four buttons within 6 px of those rows and found ZERO rivals.",
|
||||
"Re-run here with this port's reader and a BROADER filter -- any element",
|
||||
"whose name contains `btn`, not only `pcbtn`, so a rival under a different",
|
||||
"naming convention would still be caught: 2 859 builds across 33 paks,",
|
||||
"EXACTLY 2 matches, entries 2 and 3. The run carries its own known positive:",
|
||||
"fewer than 2 would mean the reader cannot see the incumbents and its zero",
|
||||
"would mean nothing.",
|
||||
"",
|
||||
"\u2705 And the name is now backed by a TABLE ENTRY rather than an inference",
|
||||
"from a string list: every `DLG_` name in the image sits in a 12-byte record",
|
||||
"(id, name pointer, handler [corrected]) spanning 0x820A0A2C-0x820A0D68 -- 70 names,",
|
||||
"70 records, none unmatched. `DLG_SELECT_DIFFICULTY` is **id 2000**.",
|
||||
"",
|
||||
"\ud83d\udd34 \"THREE INDEPENDENT ROUTES\" CORRECTED TO TWO -- 2026-08-31, by the Decoder,",
|
||||
"and I had relayed the count unchecked for the second time from one delivery.",
|
||||
"",
|
||||
"The image leg says DIFFICULTY is a dialog and names no entry, so alone it",
|
||||
"identifies nothing. The disc and oracle legs are ONE COMPOUND ARGUMENT: the",
|
||||
"capture is compared against the disc's rows. What makes that discriminating is",
|
||||
"the EXCLUSION SCAN -- zero rivals within 6 px anywhere on the disc -- and that",
|
||||
"is what the word \"three\" was taking credit for. The conclusion is unchanged;",
|
||||
"the evidence is two arguments, one of them compound, and was never three.",
|
||||
"",
|
||||
"\ud83d\udccc The test that falls out of it, theirs: ask of an n-routes claim not whether",
|
||||
"the routes are correct but whether ANY COULD HAVE COME OUT DIFFERENTLY GIVEN",
|
||||
"THE OTHERS. That is an exclusion argument, and it is usually absent.",
|
||||
"",
|
||||
"\ud83d\udd34 WITHDRAWN 2026-08-31 -- \"AN EN/JP PAIR\", AND I RELAYED IT.",
|
||||
"",
|
||||
"The Decoder stated entries 2/3 as a language pair in the same HANDOFF row that",
|
||||
"identifies DIFFICULTY, as a fact, and has withdrawn it: nothing established the",
|
||||
"pairing. I copied it into this `why` -- twice -- in the SAME SENTENCE where I",
|
||||
"was careful to say my re-derivation confirms the geometry and does not name the",
|
||||
"screen. The unchecked half rode along inside the clause I had checked.",
|
||||
"",
|
||||
"What the scan actually shows is that adjacent GP_DIALOG entries are UNRELATED",
|
||||
"DIALOGS: 26 of 65 adjacent pairs differ in BUTTON COUNT, which no language pair",
|
||||
"can. Identical element sets is the language signature in GP_TITLE; here it is",
|
||||
"equally consistent with a duplicate. So `2/3` are two builds with the same four",
|
||||
"buttons at the same rows, and calling them EN and JP is an assumption.",
|
||||
"",
|
||||
"\u26a0\ufe0f THE IDENTIFICATION DOES NOT REST ON IT -- unique four-button geometry with",
|
||||
"zero rivals disc-wide, plus the oracle capture. The pairing was decoration on a",
|
||||
"conclusion that stands without it, which is exactly why it travelled unchecked.",
|
||||
"",
|
||||
"\u2705 RESTORED 2026-08-31, ON A MEASUREMENT RATHER THAN A RELAY. The Decoder took",
|
||||
"the `ja` capture of DIFFICULTY that was missing and 2/3 ARE English/Japanese:",
|
||||
"EN vs JP differ in 1.82 % of pixels in FOUR BANDS AND NOWHERE ELSE -- the",
|
||||
"heading (DIFFICULTY -> the JP heading), the ring by 2 px, the BACK label, and",
|
||||
"the footer. EASY/NORMAL/HARD are NOT in the differing set: the Japanese release",
|
||||
"leaves the three difficulty names in Latin script, which is why the disc figure",
|
||||
"is only 2.77 % of bytes against 1.82 % of pixels.",
|
||||
"",
|
||||
"\ud83d\udccc MY OBJECTION WAS NOT WRONG AND IS NOT WITHDRAWN. It was that IDENTICAL",
|
||||
"ELEMENT SETS DO NOT IMPLY A LANGUAGE PAIR -- 26 of 65 adjacent pairs differ in",
|
||||
"button count, so adjacency proves nothing. That argument still holds; what has",
|
||||
"changed is that the conclusion now rests on a direct locale capture instead of",
|
||||
"on that inference. A bad argument for a true claim is still a bad argument, and",
|
||||
"the claim was correctly out of this file until somebody went and looked.",
|
||||
"",
|
||||
"\u26a0\ufe0f REACH, THEIRS: one JP boot, one screen, does not generalise. GP_TITLE 4/7 is",
|
||||
"known to differ by MORE than text -- entry 7 carries nine sprites entry 4 lacks.",
|
||||
"Nothing in the port keys off locale today; this is recorded, not consumed.",
|
||||
"",
|
||||
"\ud83d\udd34 RECORD LAYOUT CORRECTED 2026-09-01, and I had copied the wrong one. I wrote",
|
||||
"\"(handler, id, name pointer)\"; it is {id, name_ptr, handler} -- the same three",
|
||||
"fields shifted one word, so every record was being credited with the PREVIOUS",
|
||||
"record's handler. The Decoder caught it with a control dump: under the old",
|
||||
"alignment record 0 had a handler of 0x10000000, which is not a code address.",
|
||||
"ids and names are unaffected and DLG_SELECT_DIFFICULTY is still 2000, so",
|
||||
"nothing here moves except the sentence.",
|
||||
"",
|
||||
"\ud83d\udccc FOURTH aside of theirs relayed into this file. The first three were an EN/JP",
|
||||
"pairing, a leg count and an independence claim -- all decorative. This one is a",
|
||||
"STRUCTURE, which is worse: a wrong field order is the kind of thing a later",
|
||||
"reader builds on, and it carried no weight here only by luck.",
|
||||
"",
|
||||
"\u2754 AND THE JOIN IS NOT REACHABLE BY THAT ROUTE -- their negative, with their",
|
||||
"reach. All three handlers load the same global at 0x828E2B14 and take addresses",
|
||||
"at 0x828E45E0/4640/467C, every one inside a 364 601-byte contiguous zero run:",
|
||||
"BSS, populated only at runtime. Controlled, because an all-zero read is also",
|
||||
"what a wrong address gives, and the dialog table itself reads non-zero through",
|
||||
"the same arithmetic.",
|
||||
"",
|
||||
"\u26a0\ufe0f That closes the DIALOG HANDLERS, not the image. The archive loader and any",
|
||||
"id-keyed table elsewhere are unexamined, so \"not in the image\" is NOT",
|
||||
"established. Recorded as a route rather than an answer, which is how they sent",
|
||||
"it.",
|
||||
"",
|
||||
"\u2754 STILL UNBOUND, and it is what would make this airtight: nothing connects",
|
||||
"id 2000 to a pak entry. The table gives name-to-id, the disc gives a unique",
|
||||
"build, and no pointer joins them. The tie is UNIQUENESS PLUS THE ORACLE",
|
||||
"CAPTURE, not a binding -- so if a rival build ever appeared, this",
|
||||
"identification would go with it.",
|
||||
"",
|
||||
"button count and geometry, NOT by a binding from the `DLG_` name to a pak",
|
||||
"entry. No such binding was found. Another four-button dialog with the same",
|
||||
"rows would be indistinguishable by this evidence -- my re-derivation",
|
||||
"confirms the geometry and does not name the screen.",
|
||||
"",
|
||||
" (was) NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
" (was) screens are measured; the ids are a name match onto the executable's class",
|
||||
" (was) names.\" So `DIFFICULTY` is a string that exists in the executable and plausibly",
|
||||
" (was) denotes this screen -- nothing observed binds it to this transition.",
|
||||
" (was) ",
|
||||
" (was) It is carried so a reader can search for it and so the port never has to",
|
||||
" (was) invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
" (was) a screen file, and this field is documentation.",
|
||||
" (was) ",
|
||||
" (was) \ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
" (was) labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
" (was) claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
" (was) as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
" (was) argument reads as evidenced and is not."
|
||||
],
|
||||
"blocked": "DIFFICULTY is not in this export. MEASURED destination (EASY/NORMAL/HARD/BACK, opening on NORMAL, then SELECT DATA) but it is not a GP_TITLE build, so there is no screen file to go to yet.",
|
||||
"skipped_chain": [
|
||||
"DIFFICULTY",
|
||||
"SELECT DATA"
|
||||
],
|
||||
"skipped_chain_why": "THE PORT SKIPS TWO MEASURED SCREENS HERE, AND IT SAYS SO OUT LOUD RATHER THAN PRETENDING. The real chain is NEW GAME -> DIFFICULTY -> SELECT DATA -> (A) on a save slot -> ~4.5 s -> S00A. DIFFICULTY and SELECT DATA are MEASURED destinations (HANDOFF Q4) but neither is a GP_TITLE build, so there is no screen file to go to. The port jumps from NEW GAME to the one thing in that chain it has, and the runtime prints what it skipped on every run. This is a GAP, not a sequence: nobody may read the port's behaviour here as what the game does.",
|
||||
"skipped_chain_kind": "measured",
|
||||
"then_video": "S00A",
|
||||
"then_video_why": "P7. HANDOFF Q9, DECODED from the movie manifest: MS00A -> S00A.wmv is the new-game intro, 93.9 s. Its POSITION is measured as well -- the movie starts ~4.5 s after (A) on the save slot, matched off the running game at 0.96-1.000 with a strictly monotone playhead over 25 consecutive 0.5 s samples.",
|
||||
"then_video_kind": "decoded",
|
||||
"unobserved_why": "WHAT FILLS THE ~4.5 s between the save slot and the movie is NOT KNOWN. The oracle run that would have shown it hit the already-documented sub_823070B0 cache crash after SELECT DATA. GP_TITLE does carry a LOADING screen -- entries 0/1 and 12/15, whose elements are every one of them named pgloading_* -- and LOADING is in the game's own screen vocabulary, but nobody has watched it appear here and the port does NOT put it in the chain on that basis. \ud83d\udccc WHERE THE OPEN QUESTION LIVES, added 2026-09-01: docs/port/BLOCKED.md carries the row -- 'what fills the 4.5 s before S00A'. An explicit unknown still needs a citation, or it cannot be distinguished from an unexamined one.",
|
||||
"skippable": true,
|
||||
"skippable_why": "HANDOFF Q9, MEASURED: one (A) press skips a movie -- the title was reached at 57 s against a 193 s baseline. Same rule the boot intro already uses.",
|
||||
"skippable_kind": "measured",
|
||||
"after_video": {
|
||||
"goto": "title",
|
||||
"kind": "authored",
|
||||
"why": "AUTHORED, and it has to be: the game goes into MISSION 1, and gameplay is out of scope (PORT-MISSION section 7). P7's gate asks for 'plays, then returns to a defined state' -- this is that state. The title is chosen over the main menu because the boot's own end state is the title, so a run that finishes the new-game intro lands somewhere a player can start again from. Nothing measured says the game does this."
|
||||
}
|
||||
},
|
||||
"ptbtn02": {
|
||||
"label": "LOAD GAME",
|
||||
"goto": null,
|
||||
"goto_name": null,
|
||||
"blocked": "The save-slot list is GP_SAVE_LOAD, not in this export. Destination MEASURED."
|
||||
},
|
||||
"ptbtn03": {
|
||||
"label": "TUTORIAL",
|
||||
"goto": null,
|
||||
"goto_name": "TUTORIAL_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
"screens are measured; the ids are a name match onto the executable's class",
|
||||
"names.\" So `TUTORIAL_MENU` is a string that exists in the executable and plausibly",
|
||||
"denotes this screen -- nothing observed binds it to this transition.",
|
||||
"",
|
||||
"It is carried so a reader can search for it and so the port never has to",
|
||||
"invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
"a screen file, and this field is documentation.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
"labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
"claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
"as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
"argument reads as evidenced and is not."
|
||||
],
|
||||
"blocked": "The lesson list is not a GP_TITLE build. Destination MEASURED."
|
||||
},
|
||||
"ptbtn04": {
|
||||
"label": "OPTIONS",
|
||||
"goto": "options",
|
||||
"goto_name": null,
|
||||
"goto_why": "UNBLOCKED 2026-09-03. This read goto:null with blocked: 'The settings menu is GP_OPTIONS, not in this export.' GP_OPTIONS is in the export now -- authored/screen_names.json export_archives -- and entry 19 is its root, named `options` from the text it renders: GAME SETTINGS, CONTROL SETTINGS, SOUND SETTINGS, SCREEN SETTINGS, BACK. docs/port/options-screens.md. The destination itself was always MEASURED; only the screen file was missing.",
|
||||
"goto_kind": "measured-destination-newly-exported",
|
||||
"limits": [
|
||||
"\ud83d\udd34 THE SCREEN OPENS BUT DOES NOT NAVIGATE. Its five rows are kind 0x3003 and the exporter only treats 0x3002 as a button, so export buttons[] is empty and up/down move nothing. \u24b7 backs out correctly.",
|
||||
"What 0x3003 MEANS is a decode question and is with the Decoder -- not widened here on the port's authority. The circumstantial case is strong (five rows, each with a focus record, on a screen whose own text lists five options) and circumstantial is exactly the standard this project keeps getting burned by.",
|
||||
"None of the sub-screens is wired. Which row leads where is read off content, not measured navigation."
|
||||
]
|
||||
},
|
||||
"ptbtn05": {
|
||||
"label": "EXTRAS",
|
||||
"goto": "extras",
|
||||
"goto_name": "EXTRA_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
"screens are measured; the ids are a name match onto the executable's class",
|
||||
"names.\" So `EXTRA_MENU` is a string that exists in the executable and plausibly",
|
||||
"denotes this screen -- nothing observed binds it to this transition.",
|
||||
"",
|
||||
"It is carried so a reader can search for it and so the port never has to",
|
||||
"invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
"a screen file, and this field is documentation.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
"labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
"claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
"as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
"argument reads as evidenced and is not."
|
||||
],
|
||||
"why": "MEASURED, HANDOFF Q4: EXTRAS opens GP_TITLE build 6. It is the ONLY main-menu destination inside this archive, and therefore the only (A)-into-a-submenu the P5 gate can actually walk."
|
||||
}
|
||||
},
|
||||
"labels_why": "The five labels are read off live-main-menu.png, a capture of the running game (docs/game/navigation.md, branch auto/no-disc-and-menu-captures 3a87a26). They are carried for logs and for a human reading this file; nothing draws them -- the button sprite already has its own text."
|
||||
},
|
||||
"extras": {
|
||||
"initial_focus": "ptbtn11",
|
||||
"initial_focus_kind": "measured",
|
||||
"focus_persists": false,
|
||||
"focus_persists_kind": "measured",
|
||||
"focus_persists_why": [
|
||||
"MEASURED 2026-08-30 -- EXTRAS RESETS. HANDOFF `4ed75e6`: ring back to",
|
||||
"y=347.5 on re-entry after a confirmed DOWN, frame 0.0 % different from the",
|
||||
"first entry, and the screen confirmed by eye as EXTRAS because an earlier",
|
||||
"run was fooled about which screen it was on.",
|
||||
"",
|
||||
"\ud83d\udccc WRITTEN EXPLICITLY, THOUGH THE PORT'S DEFAULT IS ALREADY false. The",
|
||||
"absent key and the measured false behave identically and mean completely",
|
||||
"different things: one is 'nobody looked', the other is 'the game was",
|
||||
"watched doing it'. `tools/port/audit-kinds` can see the second and not the",
|
||||
"first, which is the whole reason for spending a key on it.",
|
||||
"",
|
||||
"\ud83d\udd34 AND THIS IS NOT A VINDICATION OF HOW IT GOT HERE. For one iteration the",
|
||||
"port ASSERTED non-persistence for EXTRAS in `contract-check` while nothing",
|
||||
"had measured it; the Decoder flagged that, and it turned out right. Being",
|
||||
"right by luck does not retroactively make it evidence -- declining to",
|
||||
"generalise the memory was the correct move, and encoding 'not measured",
|
||||
"here' as a positive claim was a different and wrong one that happened to",
|
||||
"land. The measurement is what makes it true; the assertion never did.",
|
||||
"",
|
||||
"\u26a0\ufe0f Do NOT generalise in either direction: main_menu persists, EXTRAS resets,",
|
||||
"and OPTIONS / LOAD GAME / TUTORIAL are untouched."
|
||||
],
|
||||
"initial_focus_why": [
|
||||
"MEASURED, unlike the main menu's: EXTRAS opens focused on MISSION SELECT (live-extras.png). It is authored here only because there is nowhere else to put a measurement -- it is not a choice.",
|
||||
"",
|
||||
"",
|
||||
"\u2705 CAVEAT LIFTED 2026-08-30 -- MEASURED, not a single-entry reading any more.",
|
||||
"HANDOFF `4ed75e6`, docs/re/data/extras-focus-resets.txt: EXTRAS opens at ring",
|
||||
"y=347.5 on MISSION SELECT, moves to 427.5 after one delivery-confirmed DOWN,",
|
||||
"and returns to 347.5 on re-entry with the frame 0.0 % different from the first",
|
||||
"entry. Because this screen RESETS, a single-entry reading of it is not",
|
||||
"measuring history -- which is precisely what made the caveat necessary while",
|
||||
"persistence here was unknown.",
|
||||
"",
|
||||
"\u2705 THE AMBIGUITY IS RESOLVED -- MEASURED 2026-08-31, and it went the way",
|
||||
"that makes `ptbtn11` right for a REASON rather than by coincidence.",
|
||||
"",
|
||||
"A submenu resets to ITS OWN OPENING ITEM, and that item is a per-screen",
|
||||
"default which need NOT be the first. Decoder, docs/re/data/",
|
||||
"difficulty-resets-to-named-item.txt: DIFFICULTY opens on NORMAL (second of",
|
||||
"four); after one confirmed DOWN to HARD, (B) out and (A) back returns to",
|
||||
"NORMAL -- in-cursor 1.0 from where it opened against 93.9 from where it was",
|
||||
"left. Reproduced on a FRESH BOOT and confirmed by eye, not read off the",
|
||||
"2026-08-29 capture.",
|
||||
"",
|
||||
"So the port's `initial_focus` is the reset target, and `buttons[0]` in",
|
||||
"`MenuFlow.initial_focus` is a REPAIR rather than a default -- which is how",
|
||||
"it was already documented, and is now measured rather than principled.",
|
||||
"",
|
||||
"\u2754 STILL OPEN, and not leaned on: whether the reset target MOVES once a",
|
||||
"difficulty has actually been confirmed. A game that remembered your last",
|
||||
"choice would behave differently, and the probe never confirms one -- the",
|
||||
"same SELECT DATA crash that constrains the run prevents testing it.",
|
||||
"",
|
||||
"",
|
||||
"\ud83d\udd34 CORRECTED 2026-08-31. This read \"it matters IF another screen is ever",
|
||||
"authored\" whose opening item is not its first. Such a screen exists and is",
|
||||
"recorded IN THIS FILE: DIFFICULTY, under `main_menu/buttons/ptbtn01`, is",
|
||||
"EASY/NORMAL/HARD/BACK and opens on NORMAL -- the SECOND of four. Measured:",
|
||||
"driven with no d-pad, unchanged for 90 s, matching the committed capture at",
|
||||
"r=+0.999 (Decoder, docs/re/captures/newgame-path/newgame-difficulty.png).",
|
||||
"",
|
||||
"So \"a screen opens on its first item\" is REFUTED as a general description of",
|
||||
"this game. On EXTRAS, TUTORIAL and OPTIONS the named item and the top item",
|
||||
"coincide BY ACCIDENT. A top-item rule would be wrong on DIFFICULTY.",
|
||||
"",
|
||||
"nobody can separate \"resets to MISSION SELECT\" from \"resets to the TOP ITEM\".",
|
||||
"They coincide here -- ptbtn11 is both. The port's value is correct under either",
|
||||
"reading, and the REASON is not established.",
|
||||
"",
|
||||
"The superseded caveat is kept below.",
|
||||
" (was) \u26a0\ufe0f WEAKENED 2026-08-30 -- the OBSERVATION stands, its reading as an INITIAL",
|
||||
" (was) focus does not. It was taken on a single entry. Now that the main menu is known",
|
||||
" (was) to remember its cursor across a round trip, a one-entry reading of any screen",
|
||||
" (was) may be measuring HISTORY rather than what the screen opens on -- the same",
|
||||
" (was) objection that reframed the main menu's TUTORIAL/NEW GAME disagreement.",
|
||||
" (was) ",
|
||||
" (was) Kept as `measured` because the frame really does show MISSION SELECT focused,",
|
||||
" (was) and kept as the port's opening item because it is the only reading there is.",
|
||||
" (was) \ud83d\udd34 If EXTRAS turns out to persist, this becomes history and the kind must",
|
||||
" (was) change with it.",
|
||||
"",
|
||||
"\ud83d\udd34 CHECKED AGAINST THE BYTES 2026-08-31 by both agents -- and NOT independently.",
|
||||
"Settled by fact, not by my inference: the Decoder's 282/362/442 came from",
|
||||
"`crates/sylpheed-formats/examples/extras_button_order.rs`, which calls",
|
||||
"`ui_layout::parse_build` -- THE SAME CRATE this port's export uses. The",
|
||||
"Python RATC parsers in their tree exist and did not produce that number.",
|
||||
"So the two legs are ONE READER USED TWICE, and the agreement carries no",
|
||||
"information about the reader being right; it carries information only about",
|
||||
"two callers of it agreeing, which they could not fail to do.",
|
||||
"",
|
||||
"\u26a0\ufe0f The VALUE is unaffected -- `ptbtn11` is decided by the DIFFICULTY",
|
||||
"measurement and by the reset finding. What died is a word I used about the",
|
||||
"evidence, which is the third such word in three iterations.",
|
||||
"",
|
||||
"is WEAKENED, by my own audit rather than by theirs.",
|
||||
"",
|
||||
"Applying their test to my own sentence: could my reading have come out",
|
||||
"differently given theirs? Only if the implementations differ. Mine is",
|
||||
"`sylpheed_formats::ui_layout::parse_build` via this port's export. Their tree",
|
||||
"does carry separate Python RATC parsers (`kf_record_census.py` and others),",
|
||||
"so a second implementation EXISTS -- but which reader produced their",
|
||||
"282/362/442 is not established by me, and if they used the same crate the",
|
||||
"two legs are one reader used twice.",
|
||||
"",
|
||||
"So: the values agreeing is still evidence, and calling it INDEPENDENT was a",
|
||||
"claim about their tooling that I did not check. Recorded at the strength I",
|
||||
"can support. \u26a0\ufe0f Nothing rests on it -- the row order is also decided by the",
|
||||
"DIFFICULTY measurement -- which is exactly why it went unexamined.",
|
||||
"",
|
||||
"Decoder attempted to refute this value and it survives: `ptbtn11` is the TOP",
|
||||
"button on this screen -- y 282 against 362 and 442 -- so the port is right",
|
||||
"whichever reading of the reset target applies. Confirmed from THIS port's own",
|
||||
"export, a different reader of the same disc: extras 282/362/442, and the main",
|
||||
"menu as a control at 162/242/322/401/482.",
|
||||
"",
|
||||
"\ud83d\udd34 WHICH ALSO MEANS EXTRAS CANNOT SEPARATE the two readings -- named item and",
|
||||
"top item coincide here. It was DIFFICULTY, opening on its second of four, that",
|
||||
"settled it."
|
||||
],
|
||||
"on_cancel": {
|
||||
"goto": "main_menu",
|
||||
"goto_name": "TITLE_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
"screens are measured; the ids are a name match onto the executable's class",
|
||||
"names.\" So `TITLE_MENU` is a string that exists in the executable and plausibly",
|
||||
"denotes this screen -- nothing observed binds it to this transition.",
|
||||
"",
|
||||
"It is carried so a reader can search for it and so the port never has to",
|
||||
"invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
"a screen file, and this field is documentation.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
"labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
"claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
"as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
"argument reads as evidenced and is not."
|
||||
],
|
||||
"why": "MEASURED, HANDOFF Q5: (B) goes up one level and RESTORES FOCUS to the item you came from. EXTRAS advertises (B) in its own footer -- the red glyph is in ptmsg2.png and absent from the main menu's ptmsg.png."
|
||||
},
|
||||
"buttons": {
|
||||
"ptbtn11": {
|
||||
"label": "MISSION SELECT",
|
||||
"goto": null,
|
||||
"goto_name": null,
|
||||
"blocked": "The stage list is GP_MISSION_SELECT, not in this export. Destination MEASURED."
|
||||
},
|
||||
"ptbtn12": {
|
||||
"label": "MOVIE THEATER",
|
||||
"goto": null,
|
||||
"goto_name": null,
|
||||
"blocked": "NEVER OPENED. docs/game/navigation.md marks this one unknown -- not merely unexported. Do not assume it opens GP_MOVIE_THEATER; that would be a name match dressed as a destination."
|
||||
},
|
||||
"ptbtn13": {
|
||||
"label": "BACK",
|
||||
"goto": "main_menu",
|
||||
"goto_name": "TITLE_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"goto_name_why": [
|
||||
"NOT MEASURED, and the label says so. HANDOFF Q4 states it exactly: \"the",
|
||||
"screens are measured; the ids are a name match onto the executable's class",
|
||||
"names.\" So `TITLE_MENU` is a string that exists in the executable and plausibly",
|
||||
"denotes this screen -- nothing observed binds it to this transition.",
|
||||
"",
|
||||
"It is carried so a reader can search for it and so the port never has to",
|
||||
"invent one. THE PORT NEVER BRANCHES ON IT: navigation uses `goto`, which is",
|
||||
"a screen file, and this field is documentation.",
|
||||
"",
|
||||
"\ud83d\udd34 THIS `why` DID NOT EXIST UNTIL 2026-08-30. All seven `goto_name_kind`",
|
||||
"labels rested on a sibling `why` that argues the DESTINATION -- a different",
|
||||
"claim from where the NAME came from. `tools/port/audit-kinds` reports that",
|
||||
"as BORROWED rather than ok, because a label resting on a neighbour's",
|
||||
"argument reads as evidenced and is not."
|
||||
],
|
||||
"same_as_cancel": true,
|
||||
"why": "MEASURED: EXTRAS' third item is BACK (live-extras.png). Treated as (B): it pops the stack, so focus is restored on the main menu exactly as (B) does. Whether the game distinguishes them is untested and there is no reason here to invent a difference."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,55 +16,151 @@
|
||||
"",
|
||||
"Delete an entry here the day the RE agent decodes a name field."
|
||||
],
|
||||
"export_archives": [
|
||||
"dat/GP_TITLE.pak",
|
||||
"dat/GP_OPTIONS.pak",
|
||||
"dat/GP_SAVE_LOAD.pak"
|
||||
],
|
||||
"export_archives_why": [
|
||||
"WHICH disc archives the export reads screen builds from.",
|
||||
"",
|
||||
"GP_TITLE was the only one for the whole project, hardcoded in the",
|
||||
"exporter. That single constant is why four of the five main-menu",
|
||||
"destinations are dead: authored/flow.json records LOAD GAME, TUTORIAL,",
|
||||
"OPTIONS and NEW GAME's difficulty chain as MEASURED destinations,",
|
||||
"blocked only because 'there is no screen file to go to'.",
|
||||
"",
|
||||
"GP_OPTIONS ADDED 2026-09-03, and deliberately alone. The probe",
|
||||
"(crates/sylpheed-export/examples/probe_archives.rs) finds screen builds",
|
||||
"in 24 archives with the EXISTING detector -- GP_OPTIONS 14,",
|
||||
"GP_SAVE_LOAD 18, GP_DIALOG 105, GP_TUTORIAL 2. Adding all four at once",
|
||||
"would land 139 new screens together and make any regression",
|
||||
"unattributable, so this takes the smallest archive first.",
|
||||
"",
|
||||
"\u26a0\ufe0f is_build() PARSING IS NOT RENDERING. It says the record is a build,",
|
||||
"not that its sprites resolve or that anyone has identified the screen.",
|
||||
"Unnamed builds export as build_NN by entry index. Expect names to be",
|
||||
"wrong-looking until someone drives the game to them; that is a naming",
|
||||
"gap, not a decode failure.",
|
||||
"",
|
||||
"GP_SAVE_LOAD ADDED 2026-09-03, again alone. 18 builds. It is main_menu",
|
||||
"ptbtn02 (LOAD GAME)'s destination, recorded in authored/flow.json as a",
|
||||
"MEASURED destination blocked only by 'not a GP_TITLE build'. It may also",
|
||||
"hold SELECT DATA, the second screen of the NEW GAME chain, but that is a",
|
||||
"guess from the name until the screens are rendered and read.",
|
||||
"",
|
||||
"\u26a0\ufe0f OUT OF SCOPE ON PURPOSE: GP_HANGAR_ARSENAL (390 builds), the",
|
||||
"GP_MAIN_GAME_* set and the rest of the gameplay archives. MISSION",
|
||||
"section 7 scopes gameplay out, and a screen that parses is not a screen",
|
||||
"this milestone wants."
|
||||
],
|
||||
"archives": {
|
||||
"dat/GP_TITLE.pak": {
|
||||
"2": {
|
||||
"name": "press_start",
|
||||
"why": "HANDOFF Q2: builds 2/3 are the PRESS (A) BUTTON plate -- a build of its own, composited over the title and faded in a beat later. English of the EN/JP pair. Measured against a live capture."
|
||||
"why": "HANDOFF Q2: builds 2/3 are the PRESS (A) BUTTON plate -- a build of its own, composited over the title and faded in a beat later. English of the EN/JP pair. Measured against a live capture. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"3": {
|
||||
"name": "press_start_jp",
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 2. Out of scope for this milestone; named so it is not mistaken for a screen we need."
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 2. Out of scope for this milestone; named so it is not mistaken for a screen we need. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"4": {
|
||||
"name": "title",
|
||||
"why": "HANDOFF Q2: build 4 is the English title art. Measured against a live capture."
|
||||
"why": "HANDOFF Q2: build 4 is the English title art. Measured against a live capture. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"5": {
|
||||
"name": "main_menu",
|
||||
"why": "HANDOFF Q2: builds 5/8 are the five-button main menu; 5 is English. Measured against a live capture. (An earlier reading called 8 a submenu and was withdrawn -- 8 is the Japanese main menu.)"
|
||||
"why": "HANDOFF Q2: builds 5/8 are the five-button main menu; 5 is English. Measured against a live capture. (An earlier reading called 8 a submenu and was withdrawn -- 8 is the Japanese main menu.) \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"6": {
|
||||
"name": "extras",
|
||||
"why": "HANDOFF Q2: builds 6/9 are the EXTRAS submenu, the only submenu inside this archive. Measured against a fresh EXTRAS capture."
|
||||
"why": "HANDOFF Q2: builds 6/9 are the EXTRAS submenu, the only submenu inside this archive. Measured against a fresh EXTRAS capture. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"7": {
|
||||
"name": "title_jp",
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 4."
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 4. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"8": {
|
||||
"name": "main_menu_jp",
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 5."
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 5. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"9": {
|
||||
"name": "extras_jp",
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 6."
|
||||
"why": "HANDOFF Q2: the Japanese twin of build 6. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"10": {
|
||||
"name": "publisher_logo",
|
||||
"why": "The SQUARE ENIX PUBLISHER wordmark -- the FIRST thing the boot sequence shows, before the developer logos. Measured by the RE agent 2026-08-29, render grid at docs/re/captures/title-builds/splash-both-halves-rendered.png. Entries 10/13 are region twins distinguished by the trademark glyph; 10 carries the (TM)."
|
||||
"why": "The SQUARE ENIX PUBLISHER wordmark -- the FIRST thing the boot sequence shows, before the developer logos. Measured by the RE agent 2026-08-29, render grid at docs/re/captures/title-builds/splash-both-halves-rendered.png. Entries 10/13 are region twins distinguished by the trademark glyph; 10 carries the (TM). \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"13": {
|
||||
"name": "publisher_logo_r",
|
||||
"why": "The region twin of entry 10, carrying (R) where 10 carries (TM). Named so it is not mistaken for a second screen the boot path needs."
|
||||
"why": "The region twin of entry 10, carrying (R) where 10 carries (TM). Named so it is not mistaken for a second screen the boot path needs. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"11": {
|
||||
"name": "developer_logos",
|
||||
"why": "The GAME ARTS / SETA / studio anima logos -- the developer splash, shown after the publisher wordmark. HANDOFF Q2 and the RE agent's 2026-08-29 render grid; draws 7/7 elements."
|
||||
"why": "The GAME ARTS / SETA / studio anima logos -- the developer splash, shown after the publisher wordmark. HANDOFF Q2 and the RE agent's 2026-08-29 render grid; draws 7/7 elements. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"14": {
|
||||
"name": "developer_logos_r",
|
||||
"why": "The region twin of entry 11, as 13 is to 10."
|
||||
"why": "The region twin of entry 11, as 13 is to 10. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
}
|
||||
},
|
||||
"dat/GP_OPTIONS.pak": {
|
||||
"3": {
|
||||
"name": "sound_settings",
|
||||
"why": "SOUND SETTINGS -- Music/Movie/Voice/SFX Volume. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"4": {
|
||||
"name": "control_settings",
|
||||
"why": "CONTROL SETTINGS -- Control Type, Throttle, sensitivities, Vibration. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"5": {
|
||||
"name": "sound_settings_jp",
|
||||
"why": "\u30b5\u30a6\u30f3\u30c9\u8a2d\u5b9a, the JP pair of entry 3. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"6": {
|
||||
"name": "screen_settings",
|
||||
"why": "Gamma Correction with R/G/B and a NEXT PAGE affordance. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"7": {
|
||||
"name": "screen_settings_page2",
|
||||
"why": "White Level / Black Level Adjust, PREVIOUS PAGE. Page 2 of entry 6. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"8": {
|
||||
"name": "control_settings_jp",
|
||||
"why": "\u64cd\u4f5c\u8a2d\u5b9a, the JP pair of entry 4. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"9": {
|
||||
"name": "screen_settings_jp",
|
||||
"why": "\u30ac\u30f3\u30de\u88dc\u6b63\u30ec\u30d9\u30eb, the JP pair of entry 6. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"10": {
|
||||
"name": "screen_settings_page2_jp",
|
||||
"why": "\u767d\u30ec\u30d9\u30eb/\u9ed2\u30ec\u30d9\u30eb\u8abf\u6574, the JP pair of entry 7. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"16": {
|
||||
"name": "game_settings",
|
||||
"why": "GAME SETTINGS -- Auto-Save, View Point, Radio Log, Subtitles. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"18": {
|
||||
"name": "game_settings_jp",
|
||||
"why": "\u30b2\u30fc\u30e0\u8a2d\u5b9a, the JP pair of entry 16. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"19": {
|
||||
"name": "options",
|
||||
"why": "\ud83d\udd34 THE OPTIONS ROOT. Rows: GAME SETTINGS, CONTROL SETTINGS, SOUND SETTINGS, SCREEN SETTINGS, BACK -- the four screens named here plus a back row. This is main_menu ptbtn04's destination. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"20": {
|
||||
"name": "control_customize",
|
||||
"why": "CUSTOMIZE -- per-action key remapping, reached from CONTROL SETTINGS. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"21": {
|
||||
"name": "options_jp",
|
||||
"why": "The JP OPTIONS root, pair of entry 19. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
},
|
||||
"22": {
|
||||
"name": "control_customize_jp",
|
||||
"why": "\u30ad\u30fc\u30ab\u30b9\u30bf\u30de\u30a4\u30ba, the JP pair of entry 20. Named from the screen's OWN RENDERED TEXT. Each was exported, rendered by the port at rest and read: the title and row labels are legible English or Japanese. That is identification by CONTENT THE SCREEN STATES ABOUT ITSELF, not by position, size or ordinal -- the three this project has been burned by. Contact sheets were reviewed 2026-09-03; docs/port/options-screens.md."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -75,19 +171,19 @@
|
||||
"dat/GP_TITLE.pak": {
|
||||
"10": {
|
||||
"name": "publisher_logo",
|
||||
"why": "LOCATED BY ENTRY INDEX, not by a rule. These four bundles declare their sprites directly and have no .rat layout child, so `is_build` cannot see them -- and the RE agent established that NO content rule can: design size fails (every extra composable bundle sampled is 1280x720, the same as every screen) and element count fails (fragments run 2..15 elements in GP_OPTIONS/GP_SAVE_LOAD while these are 3 and 7 -- the ranges overlap). Safe here and not in general: in GP_TITLE the widened set adds exactly these four and all four are real screens, zero fragments."
|
||||
"why": "LOCATED BY ENTRY INDEX, not by a rule. These four bundles declare their sprites directly and have no .rat layout child, so `is_build` cannot see them -- and the RE agent established that NO content rule can: design size fails (every extra composable bundle sampled is 1280x720, the same as every screen) and element count fails (fragments run 2..15 elements in GP_OPTIONS/GP_SAVE_LOAD while these are 3 and 7 -- the ranges overlap). Safe here and not in general: in GP_TITLE the widened set adds exactly these four and all four are real screens, zero fragments. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"11": {
|
||||
"name": "developer_logos",
|
||||
"why": "As entry 10: located by index because no content rule distinguishes a splash from a fragment. 7 elements, all drawn."
|
||||
"why": "As entry 10: located by index because no content rule distinguishes a splash from a fragment. 7 elements, all drawn. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"13": {
|
||||
"name": "publisher_logo_r",
|
||||
"why": "As entry 10, region twin."
|
||||
"why": "As entry 10, region twin. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
},
|
||||
"14": {
|
||||
"name": "developer_logos_r",
|
||||
"why": "As entry 11, region twin."
|
||||
"why": "As entry 11, region twin. \ud83d\udccc SOURCE, added 2026-09-01 in the uncited-why backfill: the four bundles are identified in docs/re/ui-title-build-map.md, and the ordinal-versus-entry distinction this entry depends on is docs/re/structures/build-ordinal-vs-entry.md. \u26a0\ufe0f The sibling references below (\"as entry 10, region twin\") are a citation form too -- they point at another entry in this file rather than at a document, and forcing a path onto them would be mislabelling to satisfy a counter."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
crates/sylpheed-export/examples/probe_archives.rs
Normal file
42
crates/sylpheed-export/examples/probe_archives.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
//! Which disc archives contain UI screen builds?
|
||||
//!
|
||||
//! The exporter reads `dat/GP_TITLE.pak` and nothing else, so four of the five
|
||||
//! main-menu destinations have no screen file to go to: `authored/flow.json`
|
||||
//! records LOAD GAME as `GP_SAVE_LOAD`, OPTIONS as `GP_OPTIONS`, and NEW GAME's
|
||||
//! chain as `DLG_SELECT_DIFFICULTY` -> `SELECT DATA`, all measured destinations
|
||||
//! that this export cannot reach.
|
||||
//!
|
||||
//! This asks the cheap question before anyone refactors the exporter: does the
|
||||
//! EXISTING build detector find anything in those archives? It changes nothing
|
||||
//! and writes nothing.
|
||||
//!
|
||||
//! cargo run --release -p sylpheed-export --example probe_archives
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
|
||||
let mut names: Vec<String> = std::fs::read_dir(format!("{disc}/dat"))?
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| n.ends_with(".pak"))
|
||||
.collect();
|
||||
names.sort();
|
||||
println!("{:<32} {:>7} {:>8}", "archive", "entries", "builds");
|
||||
for n in names {
|
||||
let path = format!("{disc}/dat/{n}");
|
||||
let Ok(ar) = PakArchive::open(&path) else {
|
||||
println!("{n:<32} {:>7} {:>8}", "-", "open failed");
|
||||
continue;
|
||||
};
|
||||
let total = ar.entries().len();
|
||||
let builds = ar
|
||||
.entries()
|
||||
.iter()
|
||||
.filter(|e| ar.read(e).map(|b| ui_layout::is_build(&b)).unwrap_or(false))
|
||||
.count();
|
||||
if builds > 0 || n.contains("OPTIONS") || n.contains("SAVE") || n.contains("DIALOG") {
|
||||
println!("{n:<32} {total:>7} {builds:>8}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
//!
|
||||
//! See `docs/FORMAT.md` for the schema and `docs/MISSION.md` for scope.
|
||||
|
||||
mod audio;
|
||||
mod check;
|
||||
mod video;
|
||||
mod screen;
|
||||
@@ -20,7 +21,7 @@ use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use serde::Serialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||
use sylpheed_formats::{media, pak::PakArchive, ui_layout};
|
||||
|
||||
/// The revision of `sylpheed-formats` this exporter is pinned to, recorded in
|
||||
/// every file it writes. Keep in step with `Cargo.toml` — it is what makes an
|
||||
@@ -76,6 +77,48 @@ struct ManifestVideo {
|
||||
/// dislikes the quality re-runs one line rather than reverse-engineering it.
|
||||
command: String,
|
||||
why: &'static str,
|
||||
/// What the runtime should have played, so it can report what it did.
|
||||
/// See `video::Transcoded::duration_s` — the port measured its player
|
||||
/// presenting 28–47 % of a stream's frames, and seconds alone hide that.
|
||||
duration_s: f64,
|
||||
fps: f64,
|
||||
}
|
||||
|
||||
/// One exported audio file. Carries the same provenance a video does, plus the
|
||||
/// measured peak and duration: silence and clipping are the two audio failures
|
||||
/// that pass every check that is not looking for them.
|
||||
#[derive(Serialize)]
|
||||
struct ManifestAudio {
|
||||
/// `se` or `bgm`. The runtime dispatches on it, so it is a field rather
|
||||
/// than a prefix on `name` that a consumer would have to parse.
|
||||
kind: &'static str,
|
||||
name: String,
|
||||
file: String,
|
||||
command: String,
|
||||
why: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
peak_dbfs: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
duration_s: Option<f32>,
|
||||
/// 🔴 One line saying what this asset is KNOWN to be missing, for the
|
||||
/// runtime to announce. Absent means nothing is known to be missing --
|
||||
/// never that the asset was checked and is complete.
|
||||
///
|
||||
/// It exists because the export could already say this and the RUNTIME
|
||||
/// could not. `why` carries the full account, but it is a paragraph aimed
|
||||
/// at a reader of the manifest; a player hears clean dialogue and has no
|
||||
/// way to learn that a stream is absent from it. This port already
|
||||
/// announces the two measured screens NEW GAME jumps over, on the principle
|
||||
/// that a gap is announced before it is opened. Audio had no equivalent.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
incomplete: Option<String>,
|
||||
/// The game's own cue identifier where one is a NAME MATCH. Absent means
|
||||
/// nobody has claimed one -- never that the binding is unknown.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name_match: Option<String>,
|
||||
/// What the runtime does at the end of the file, where that was authored.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
loop_mode: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -88,6 +131,8 @@ struct Manifest {
|
||||
screens: Vec<ManifestScreen>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
videos: Vec<ManifestVideo>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
audio: Vec<ManifestAudio>,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -163,6 +208,54 @@ fn load_also_export(authored: &Path) -> Result<AlsoExport> {
|
||||
/// exactly four bundles and all four are real screens, with zero fragments. In
|
||||
/// another archive it would not be, which is why this is an allow-list and not
|
||||
/// a widened predicate.
|
||||
/// Which archives the export reads, from `authored/screen_names.json`
|
||||
/// `export_archives`.
|
||||
///
|
||||
/// 🔴 THIS WAS ONE HARDCODED CONSTANT AND IT COST FOUR MENU DESTINATIONS.
|
||||
/// `authored/flow.json` records LOAD GAME, TUTORIAL, OPTIONS and NEW GAME's
|
||||
/// difficulty chain as MEASURED destinations that are `blocked` because "not a
|
||||
/// GP_TITLE build, so there is no screen file to go to". The blocker was never
|
||||
/// the disc or the reader -- `examples/probe_archives.rs` finds screen builds in
|
||||
/// 24 archives using the EXISTING detector. It was this line.
|
||||
///
|
||||
/// Absent from the authored file, it stays exactly what it was, so an old
|
||||
/// `authored/` tree exports what it always did.
|
||||
fn load_export_archives(authored: &Path) -> Result<Vec<String>> {
|
||||
let path = authored.join("screen_names.json");
|
||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||
return Ok(vec!["dat/GP_TITLE.pak".into()]);
|
||||
};
|
||||
let v: serde_json::Value = serde_json::from_str(&text)
|
||||
.with_context(|| format!("parse {}", path.display()))?;
|
||||
match v.get("export_archives").and_then(|a| a.as_array()) {
|
||||
None => Ok(vec!["dat/GP_TITLE.pak".into()]),
|
||||
Some(list) => Ok(list
|
||||
.iter()
|
||||
.filter_map(|e| e.as_str().map(str::to_owned))
|
||||
.collect()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The sprite subdirectory for an archive: `dat/GP_OPTIONS.pak` -> `options`.
|
||||
///
|
||||
/// ⚠️ NOT cosmetic. Sprites are written to `sprites/<group>/<screen>/`, so two
|
||||
/// archives sharing a group would collide by screen name -- and unnamed builds
|
||||
/// are named `build_NN` by ENTRY INDEX, which restarts at 0 in every archive.
|
||||
/// `GP_TITLE` keeps its historical `title` so no existing path moves.
|
||||
fn group_for(archive: &str) -> &'static str {
|
||||
match archive {
|
||||
"dat/GP_TITLE.pak" => "title",
|
||||
"dat/GP_OPTIONS.pak" => "options",
|
||||
"dat/GP_SAVE_LOAD.pak" => "save_load",
|
||||
"dat/GP_TUTORIAL.pak" => "tutorial",
|
||||
"dat/GP_DIALOG.pak" => "dialog",
|
||||
// Deliberately not derived from the filename: a new archive should be a
|
||||
// decision someone made, not a directory that appears because a string
|
||||
// parsed. An unmapped archive is rejected below.
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn screen_builds(ar: &PakArchive, also: Option<&std::collections::BTreeMap<String, NameEntry>>)
|
||||
-> Vec<(usize, Vec<u8>)>
|
||||
{
|
||||
@@ -195,86 +288,275 @@ fn main() -> Result<()> {
|
||||
fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
let names = load_names(authored_dir)?;
|
||||
|
||||
// Built up as the export runs. A warning is a thing a CONSUMER of the tree
|
||||
// has to know about; it is not an error, and it is not a log line, because
|
||||
// the person who needs it reads `manifest.json` and never sees stdout.
|
||||
let mut warnings: Vec<String> = vec![
|
||||
String::new(), // replaced below once the archive list is known
|
||||
"The four splash bundles (entries 10/13 publisher, 11/14 developer) have no .rat \
|
||||
layout child, so `is_build` cannot see them and no content rule can: element \
|
||||
count and design size both overlap with two-element fragments in other archives. \
|
||||
They are located by ENTRY INDEX from authored/screen_names.json `also_export`, \
|
||||
which is a locator and not a claim -- see each one's name_why."
|
||||
.into(),
|
||||
];
|
||||
|
||||
// Derived output is regenerated wholesale: clear it, so a screen that stops
|
||||
// being exported stops existing rather than lingering as a stale file that
|
||||
// still validates.
|
||||
//
|
||||
// 🔴 EXCEPT `video/`, and leaving it out was a bug that hid in plain sight.
|
||||
// `video::transcode` has always carried a cache -- it writes a `.cmd`
|
||||
// sidecar with the exact command, the source size and the channel count, and
|
||||
// skips the encode when all three still match. Its own doc comment says
|
||||
// "without it every re-export pays ~4 minutes to produce a byte-identical
|
||||
// file". **This wipe deleted the sidecar and the output immediately before
|
||||
// the check, so the cache had never hit once.** Six exports in one session
|
||||
// paid ~48 minutes of Theora to produce five byte-identical files, and
|
||||
// nothing reported it: the cache is silent when it works and silent when it
|
||||
// does not.
|
||||
//
|
||||
// The wholesale guarantee is kept rather than weakened -- everything else is
|
||||
// still cleared outright, and `prune_videos` below deletes any file in
|
||||
// `video/` that this run did not claim, so a movie that stops being exported
|
||||
// still stops existing.
|
||||
if out.exists() {
|
||||
std::fs::remove_dir_all(&out).context("clear the output tree")?;
|
||||
for entry in std::fs::read_dir(&out).context("clear the output tree")? {
|
||||
let entry = entry?;
|
||||
if entry.file_name() == "video" {
|
||||
continue;
|
||||
}
|
||||
if entry.file_type()?.is_dir() {
|
||||
std::fs::remove_dir_all(entry.path())
|
||||
} else {
|
||||
std::fs::remove_file(entry.path())
|
||||
}
|
||||
.with_context(|| format!("clear {}", entry.path().display()))?;
|
||||
}
|
||||
}
|
||||
std::fs::create_dir_all(&out)?;
|
||||
|
||||
let archive = "dat/GP_TITLE.pak";
|
||||
let pak = disc.join(archive);
|
||||
let ar = PakArchive::open(&pak).with_context(|| format!("open {}", pak.display()))?;
|
||||
let also = load_also_export(authored_dir)?;
|
||||
let archive_also = also.get(archive);
|
||||
let builds = screen_builds(&ar, archive_also);
|
||||
println!("{archive}: {} screen build(s)", builds.len());
|
||||
|
||||
let archive_names = names.get(archive);
|
||||
let archives = load_export_archives(authored_dir)?;
|
||||
warnings[0] = format!(
|
||||
"Screen builds from {} only ({}). Other archives on the disc also contain UI \
|
||||
builds and are not exported. Only the two movies MISSION section 6 puts in scope.",
|
||||
archives.len(),
|
||||
archives.join(", ")
|
||||
);
|
||||
let mut screens = Vec::new();
|
||||
for (build_idx, (entry, bytes)) in builds.iter().enumerate() {
|
||||
// Keyed by ENTRY, not by the ordinal: widening the enumeration to reach
|
||||
// the splash renumbers ordinals, and a name that moves when the rule
|
||||
// changes is not a name.
|
||||
let key = entry.to_string();
|
||||
let named = archive_names
|
||||
.and_then(|m| m.get(&key))
|
||||
.or_else(|| archive_also.and_then(|m| m.get(&key)));
|
||||
let (name, name_source, why) = match named {
|
||||
Some(e) => (e.name.clone(), "authored", e.why.clone()),
|
||||
// Nobody has identified this build. Emit a stable synthetic id and
|
||||
// say in the file that the name is not a recovered one.
|
||||
None => (format!("build_{entry:02}"), "index", None),
|
||||
};
|
||||
let ex = screen::export_build(
|
||||
&out,
|
||||
archive,
|
||||
*entry,
|
||||
build_idx,
|
||||
bytes,
|
||||
&name,
|
||||
name_source,
|
||||
why,
|
||||
"title",
|
||||
EXPORTER,
|
||||
FORMATS_REV,
|
||||
)
|
||||
.with_context(|| format!("export build {build_idx} of {archive}"))?;
|
||||
println!(
|
||||
" [{build_idx}] entry {entry:<3} -> {} ({} sprites{})",
|
||||
ex.json_path,
|
||||
ex.sprites,
|
||||
if ex.missing.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(", {} missing", ex.missing.len())
|
||||
}
|
||||
);
|
||||
screens.push(ManifestScreen {
|
||||
name: ex.name,
|
||||
file: ex.json_path,
|
||||
sprites: ex.sprites,
|
||||
missing_sprites: ex.missing,
|
||||
});
|
||||
for archive in archives.iter().map(String::as_str) {
|
||||
let group = group_for(archive);
|
||||
if group.is_empty() {
|
||||
anyhow::bail!(
|
||||
"authored/screen_names.json export_archives lists {archive}, which has no \
|
||||
sprite group in group_for(). Add one deliberately -- deriving it from the \
|
||||
filename would let a typo create a directory."
|
||||
);
|
||||
}
|
||||
let pak = disc.join(archive);
|
||||
let ar = PakArchive::open(&pak).with_context(|| format!("open {}", pak.display()))?;
|
||||
let archive_also = also.get(archive);
|
||||
let builds = screen_builds(&ar, archive_also);
|
||||
println!("{archive}: {} screen build(s) -> sprites/{group}/", builds.len());
|
||||
|
||||
let archive_names = names.get(archive);
|
||||
for (build_idx, (entry, bytes)) in builds.iter().enumerate() {
|
||||
// Keyed by ENTRY, not by the ordinal: widening the enumeration to reach
|
||||
// the splash renumbers ordinals, and a name that moves when the rule
|
||||
// changes is not a name.
|
||||
let key = entry.to_string();
|
||||
let named = archive_names
|
||||
.and_then(|m| m.get(&key))
|
||||
.or_else(|| archive_also.and_then(|m| m.get(&key)));
|
||||
let (name, name_source, why) = match named {
|
||||
Some(e) => (e.name.clone(), "authored", e.why.clone()),
|
||||
// Nobody has identified this build. Emit a stable synthetic id and
|
||||
// say in the file that the name is not a recovered one.
|
||||
None => (format!("build_{entry:02}"), "index", None),
|
||||
};
|
||||
let ex = screen::export_build(
|
||||
&out,
|
||||
archive,
|
||||
*entry,
|
||||
build_idx,
|
||||
bytes,
|
||||
&name,
|
||||
name_source,
|
||||
why,
|
||||
group,
|
||||
EXPORTER,
|
||||
FORMATS_REV,
|
||||
)
|
||||
.with_context(|| format!("export build {build_idx} of {archive}"))?;
|
||||
println!(
|
||||
" [{build_idx}] entry {entry:<3} -> {} ({} sprites{})",
|
||||
ex.json_path,
|
||||
ex.sprites,
|
||||
if ex.missing.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(", {} missing", ex.missing.len())
|
||||
}
|
||||
);
|
||||
screens.push(ManifestScreen {
|
||||
name: ex.name,
|
||||
file: ex.json_path,
|
||||
sprites: ex.sprites,
|
||||
missing_sprites: ex.missing,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// MISSION §6: the boot intro and the one new-game intro only.
|
||||
let mut videos = Vec::new();
|
||||
let mut movie_lengths: Vec<(&'static str, Option<f32>)> = Vec::new();
|
||||
// 🔴 The export deviates from a HUMAN decision, and until this warning
|
||||
// existed nobody could tell. MISSION §6 pins the 5.1 fold; `video.rs` ships
|
||||
// that matrix scaled by 0.4142, i.e. 7.65 dB quieter. The deviation is
|
||||
// justified for one of the two movies and over-broad for the other, and
|
||||
// which of the three options to take is not the exporter's call -- so it is
|
||||
// reported on every run rather than left in a doc comment nobody opens.
|
||||
if video::MOVIES.iter().any(|m| disc.join(m.src).exists()) {
|
||||
warnings.push(
|
||||
"video/*.ogv: the 5.1->stereo fold is NOT the matrix MISSION §6 pins. §6 fixes it at FL = 1.0*FL + 0.707*FC + 0.707*BL (a human decision, 2026-08-29); this export ships that matrix scaled by 0.4142 -- same weighting, 7.65 dB quieter. Measured over the whole of both movies, float-decoded so nothing is pre-clamped: under the PINNED matrix ADV peaks at +4.26 dBFS with 4406 samples at or over full scale (1874 more than 1 dB over, longest clamped run 0.333 ms), while S00A peaks at -1.34 dBFS and never clips. So the pin overloads ADV and this constant is over-broad for S00A; the smallest single scalar under which neither clamps is 1/1.6339 = 0.612. NOT changed on the exporter's own authority -- the level of a mix is what §6 reserves to a human. See docs/port/DECISIONS.md."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
for m in video::MOVIES {
|
||||
match video::transcode(disc, out, m)? {
|
||||
Some(t) => {
|
||||
println!(" video {} -> {}", m.src, t.file);
|
||||
movie_lengths.push((m.stem, audio::probe_duration(&out.join(&t.file))));
|
||||
videos.push(ManifestVideo {
|
||||
name: t.name,
|
||||
file: t.file,
|
||||
command: t.command,
|
||||
why: t.why,
|
||||
duration_s: t.duration_s,
|
||||
fps: t.fps,
|
||||
});
|
||||
}
|
||||
None => println!(" video {} not on this disc -- skipped", m.src),
|
||||
}
|
||||
}
|
||||
prune_videos(out, &videos)?;
|
||||
|
||||
// P6. Both tables are AUTHORED, for two different reasons -- the cue offsets
|
||||
// because they were measured off the running game and are on the disc in no
|
||||
// findable form, the BGM choice because HANDOFF Q10 is a negative and
|
||||
// nothing states which track a menu plays. See `authored/audio.json`.
|
||||
let mut audio = Vec::new();
|
||||
let audio_cfg = audio::load(authored_dir)?;
|
||||
match &audio_cfg {
|
||||
None => println!(" no authored/audio.json -- no audio exported"),
|
||||
Some(cfg) => {
|
||||
let source = media::DirectorySource::new(disc);
|
||||
for a in audio::export_cues(&source, out, &cfg.se)? {
|
||||
println!(
|
||||
" se {:<8} -> {} ({})",
|
||||
a.name,
|
||||
a.file,
|
||||
describe(&a)
|
||||
);
|
||||
audio.push(ManifestAudio::from(a));
|
||||
}
|
||||
for (role, spec) in &cfg.bgm {
|
||||
match audio::export_bgm(&source, out, role, spec)? {
|
||||
Some(a) => {
|
||||
println!(
|
||||
" bgm {:<8} -> {} ({}, bank {}, {} sub-wave(s))",
|
||||
a.name,
|
||||
a.file,
|
||||
describe(&a),
|
||||
spec.bank,
|
||||
a.sub_waves
|
||||
);
|
||||
// HANDOFF Q10's census is "exactly two waves of
|
||||
// identical duration, 32/32 banks on the disc". When
|
||||
// `media` hands back a different number, SAY SO -- the
|
||||
// port does not get to decide that one of them is not a
|
||||
// stem, and silently summing an extra region into the
|
||||
// music is precisely the media-assembly mistake MISSION
|
||||
// section 2 names. The decoder's answer is what ships;
|
||||
// the disagreement is what gets reported.
|
||||
if a.sub_waves != 2 {
|
||||
warnings.push(format!(
|
||||
"audio/bgm/{role}.ogg: sylpheed_formats::media::sound_bank_riffs \
|
||||
returned {} sub-wave(s) for `{}`, but HANDOFF Q10's bank census \
|
||||
says a music bank is EXACTLY TWO waves of identical duration \
|
||||
(32/32 banks). All {} are summed, because choosing which to drop \
|
||||
is a decoding question and this exporter does not answer those. \
|
||||
See docs/port/BLOCKED.md.",
|
||||
a.sub_waves, spec.bank, a.sub_waves
|
||||
));
|
||||
}
|
||||
audio.push(ManifestAudio::from(a));
|
||||
}
|
||||
// Not an error: the authored bank may simply not be on this
|
||||
// disc, and the export of everything else is still good.
|
||||
None => warnings.push(format!(
|
||||
"authored/audio.json bgm.{role} names bank `{}`, which is not in \
|
||||
this disc's sound.pak -- no BGM exported for that role.",
|
||||
spec.bank
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The cutscene voices are DERIVED, not authored, so this runs outside the
|
||||
// `authored/audio.json` block above: the binding comes off the disc (the
|
||||
// movie manifest in `tables.pak`), and an export with no authored audio
|
||||
// should still carry the dialogue for the movies it ships.
|
||||
//
|
||||
// A movie that resolves to no region is genuinely unvoiced and gets a
|
||||
// warning rather than a substitute -- for both movies in scope this port
|
||||
// expects a region, so a warning here is a real signal and not noise.
|
||||
{
|
||||
let source = media::DirectorySource::new(disc);
|
||||
for (stem, len) in &movie_lengths {
|
||||
// The presentation choice is AUTHORED and this block runs even when
|
||||
// there is no `authored/audio.json` -- the voice binding is decoded,
|
||||
// so the dialogue exports either way and only the choice defaults.
|
||||
let want = audio_cfg.as_ref().map(|c| c.voice).unwrap_or_default();
|
||||
let weights = audio_cfg.as_ref().map(|c| c.stream_weights.clone()).unwrap_or_default();
|
||||
match audio::export_voice(&source, out, stem, *len, want, &weights)? {
|
||||
Some(a) => {
|
||||
// 🔴 A TOP-LEVEL WARNING, not just a `why` on the entry. The
|
||||
// export is known to be missing audio the game plays, and
|
||||
// the failure sounds like success: one stream decodes to
|
||||
// clean dialogue, so nobody listening finds out.
|
||||
if a.kept_waves < a.content_waves {
|
||||
warnings.push(format!(
|
||||
"{}: KNOWN INCOMPLETE. This region holds {} streams and the RUNNING \
|
||||
GAME DECODES ALL OF THEM CONCURRENTLY (Canary --xma_param_probe: \
|
||||
three XMA contexts, byte sizes matching the disc payloads exactly). \
|
||||
The export carries ONE. Nothing in the audio reveals this -- a \
|
||||
single stream is clean audible dialogue. Held rather than summed \
|
||||
because an equal-gain sum of channel pairs is not a downmix and \
|
||||
would be a second guess, not a fix. See authored/audio.json voice \
|
||||
and docs/port/BLOCKED.md.",
|
||||
a.file, a.sub_waves
|
||||
));
|
||||
}
|
||||
println!(
|
||||
" voice {:<8} -> {} ({}, {} of {} stream(s){})",
|
||||
a.name,
|
||||
a.file,
|
||||
describe(&a),
|
||||
a.kept_waves,
|
||||
a.sub_waves,
|
||||
if a.kept_waves < a.content_waves { " -- KNOWN INCOMPLETE, see warnings" } else { "" }
|
||||
);
|
||||
audio.push(ManifestAudio::from(a));
|
||||
}
|
||||
None => warnings.push(format!(
|
||||
"movie `{stem}`: the movie manifest binds it to no voice region, so no dialogue was exported. That is a real answer for an unvoiced cutscene -- nothing is substituted, because resolving an unbound movie through a shared demo line was measured to play the WRONG recording."
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let manifest = Manifest {
|
||||
format: "sylpheed.manifest/1",
|
||||
@@ -283,16 +565,8 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
disc: disc.display().to_string(),
|
||||
screens,
|
||||
videos,
|
||||
warnings: vec![
|
||||
"P0 scope: GP_TITLE screen builds only. No audio, no video, no other archive."
|
||||
.into(),
|
||||
"The four splash bundles (entries 10/13 publisher, 11/14 developer) have no .rat \
|
||||
layout child, so `is_build` cannot see them and no content rule can: element \
|
||||
count and design size both overlap with two-element fragments in other archives. \
|
||||
They are located by ENTRY INDEX from authored/screen_names.json `also_export`, \
|
||||
which is a locator and not a claim -- see each one's name_why."
|
||||
.into(),
|
||||
],
|
||||
audio,
|
||||
warnings,
|
||||
};
|
||||
std::fs::write(
|
||||
out.join("manifest.json"),
|
||||
@@ -301,3 +575,81 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
|
||||
println!("wrote {}/manifest.json", out.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
impl From<audio::Exported> for ManifestAudio {
|
||||
fn from(a: audio::Exported) -> Self {
|
||||
ManifestAudio {
|
||||
kind: a.kind,
|
||||
name: a.name,
|
||||
file: a.file,
|
||||
command: a.command,
|
||||
why: a.why,
|
||||
peak_dbfs: a.peak_dbfs,
|
||||
duration_s: a.duration_s,
|
||||
incomplete: (a.kept_waves < a.content_waves).then(|| {
|
||||
format!(
|
||||
"{} of {} streams. The running game decodes all {} concurrently. \
|
||||
Nothing in the audio reveals the gap -- what plays is clean dialogue. \
|
||||
WHICH streams are dropped and why differs per asset; the manifest \
|
||||
entry's `why` says, and it is not the same story twice.",
|
||||
a.kept_waves, a.sub_waves, a.sub_waves
|
||||
)
|
||||
}),
|
||||
name_match: a.name_match,
|
||||
loop_mode: a.loop_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The two numbers worth reading on an audio line, in the console.
|
||||
///
|
||||
/// Printed rather than left to the manifest because the failure this catches is
|
||||
/// a SILENT file: the right duration, the right channel count, the right size,
|
||||
/// and nothing in it. `-inf dB` on stdout is the one form of that failure a
|
||||
/// person notices without being told to look.
|
||||
fn describe(a: &audio::Exported) -> String {
|
||||
let peak = match a.peak_dbfs {
|
||||
Some(p) => format!("peak {p:.1} dBFS"),
|
||||
None => "peak unmeasured".into(),
|
||||
};
|
||||
match a.duration_s {
|
||||
Some(d) => format!("{d:.3} s, {peak}"),
|
||||
None => peak,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Delete anything in `video/` this run did not produce.
|
||||
///
|
||||
/// `video/` is the one directory the wholesale wipe spares, so that the
|
||||
/// transcode cache survives to be consulted. This restores the guarantee the
|
||||
/// wipe exists for: a movie that stops being exported stops existing, rather
|
||||
/// than lingering as a file the manifest no longer lists.
|
||||
fn prune_videos(out: &Path, kept: &[ManifestVideo]) -> Result<()> {
|
||||
let dir = out.join("video");
|
||||
if !dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut keep: Vec<String> = Vec::new();
|
||||
for v in kept {
|
||||
if let Some(name) = Path::new(&v.file).file_name() {
|
||||
let name = name.to_string_lossy().into_owned();
|
||||
keep.push(name.clone());
|
||||
// The cache sidecar goes with the file it stamps.
|
||||
if let Some(stem) = Path::new(&name).file_stem() {
|
||||
keep.push(format!("{}.cmd", stem.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for entry in std::fs::read_dir(&dir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if keep.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
println!(" video {name} is no longer exported -- removed");
|
||||
let _ = std::fs::remove_file(entry.path());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -17,13 +17,27 @@ use sylpheed_formats::{t8ad, ui_layout};
|
||||
///
|
||||
/// ⚠️ `0x3002` is one member of a `0x3000` family and is **not** a general
|
||||
/// button test — `GP_READY_ROOM` uses `0x3000`/`0x3004`/`0x300c`/`0x3008` and
|
||||
/// has zero `0x3002`. Every screen in this milestone is `GP_TITLE`, where the
|
||||
/// mapping is decoded; anything else exports as `unknown` with its raw kind.
|
||||
/// has zero `0x3002`. The mapping is decoded for the kinds listed; anything else
|
||||
/// exports as `unknown` with its raw kind visible.
|
||||
fn role_of(kind: u32, has_sprite: bool) -> &'static str {
|
||||
// 🔴 BIT 0 IS THE PARENT FLAG AND CARRIES NO ROLE INFORMATION. Decoded
|
||||
// disc-wide: `kind & 1` agrees with "has a parent" on 15 493 elements with
|
||||
// zero disagreements (`docs/re/ui-kind-bit0-is-has-parent.md`). So a role
|
||||
// table keyed on the raw kind splits every class in two and calls the
|
||||
// parented half `unknown` -- which is how the OPTIONS menu's rows came out
|
||||
// roleless while the exporter had already accepted them as buttons.
|
||||
//
|
||||
// ⚠️ APPLIED TO EVERY PAIR, NOT JUST THE ONE THAT FAILED. Fixing only
|
||||
// `0x3003` would have left `0x1` as `unknown` while `0x0` is `decoration`,
|
||||
// i.e. the same inconsistency one kind along -- and half-applying this
|
||||
// decode is exactly what produced the failure this is fixing.
|
||||
//
|
||||
// ⚠️ `0x73002`/`0x73003` are NOT folded in. Their `0x70000` bits are
|
||||
// undecoded, so they stay `unknown` with their raw kind visible.
|
||||
match kind {
|
||||
0x3002 => "button",
|
||||
0x10 if !has_sprite => "primitive",
|
||||
0x0 => "decoration",
|
||||
0x3002 | 0x3003 => "button",
|
||||
0x10 | 0x11 if !has_sprite => "primitive",
|
||||
0x0 | 0x1 => "decoration",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
@@ -103,6 +117,14 @@ pub struct FocusElement {
|
||||
pub id: String,
|
||||
pub declared: String,
|
||||
pub sprite: Option<String>,
|
||||
/// `true` when the game draws this sprite ADDITIVE — `T8aD +0x04` bit
|
||||
/// `0x02`, decoded. Absent when the sprite resolves to no `T8aD` header.
|
||||
///
|
||||
/// A leaf's sprite may live in the leaf's own table or in the parent
|
||||
/// bundle's, so the bit is looked up in the same two places, in the same
|
||||
/// order, that the PNG is written from.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blend_additive: Option<bool>,
|
||||
pub pivot: [u32; 2],
|
||||
pub rest: Rest,
|
||||
pub keyframes: Vec<Keyframe>,
|
||||
@@ -112,6 +134,34 @@ pub struct FocusElement {
|
||||
pub struct Focus {
|
||||
/// The `.rat` leaf this came from, e.g. `ptbtn01f.rat`.
|
||||
pub record: String,
|
||||
/// The record header's `+0x08`: **where the cycle restarts**, in keyframe
|
||||
/// units — which is not the same thing as the last keyframe's time.
|
||||
///
|
||||
/// `ptbtn00f`, the `PRESS Ⓐ` plate's glow, ramps 0→80→0 over **105** units
|
||||
/// inside a **120**-unit cycle and rests dark for the remaining 15. Deriving
|
||||
/// the period from the largest keyframe time — what the port did until now —
|
||||
/// runs it 14 % fast and deletes the dark rest entirely.
|
||||
///
|
||||
/// Decoded by the Decoder (`07e93ce`, `docs/re/structures/ui-record-loop-length.md`,
|
||||
/// delivered in HANDOFF `27938aa`) and **re-run here before adoption**, with
|
||||
/// their falsifier and their non-triviality control (⚠️ the 92.3 % below is
|
||||
/// "of records where the question is meaningful" -- 1 643 of the 1 781 with a
|
||||
/// timed keyframe. 3 311 nested records exist; the other 1 530 have no
|
||||
/// keyframe time at all, so `+0x08 == max t` is not a question there. Quoted
|
||||
/// bare until 2026-09-01, which is a population-scoped statistic reported
|
||||
/// without its population):
|
||||
/// `cargo run -p sylpheed-export --example record_loop_control`. Disc-wide
|
||||
/// 1 781 timed records, 92.3 % exact, 7.7 % hold, **0 declaring less than
|
||||
/// their own last pose**; on the eight records this port animates, seven
|
||||
/// exact and `ptbtn00f` the one hold.
|
||||
///
|
||||
/// ✅ **The port no longer owns this reading.** For one iteration `screen.rs`
|
||||
/// held its own guard and byte read, because the field was decoded in an
|
||||
/// example and a test and exposed in no public API on any ref. It is now
|
||||
/// `ui_layout::loop_length_units`, taken at `formats-pin-2026-08-30b`, and
|
||||
/// the local copy is deleted — the doc comment that promised that deletion
|
||||
/// is the only reason it did not quietly become permanent.
|
||||
pub loop_length_units: Option<u32>,
|
||||
/// Back-to-front, in the leaf's own declaration order.
|
||||
pub elements: Vec<FocusElement>,
|
||||
}
|
||||
@@ -141,6 +191,59 @@ pub struct Element {
|
||||
/// convention and a consumer may still want the bare highlight texture.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub focus: Option<Focus>,
|
||||
|
||||
/// This element's own `.rat` leaf, when its declared name is itself a
|
||||
/// record in the bundle.
|
||||
///
|
||||
/// 🔴 **DECODED DATA THE EXPORTER USED TO DROP.** `ptloop01`/`ptloop02` on
|
||||
/// the title declare scale 100 % and rotation 0 at the parent, and their
|
||||
/// leaves declare **(100, 600) at +30°** and **(100, 800) at −45°** — and
|
||||
/// the leaves *move*, x from −639 → 1521 and 1721 → −839. `ui_layout`'s own
|
||||
/// note says so: *"the rotated quads come from its two nested `.rat` leaf
|
||||
/// records, which the census never opened."* Neither did this exporter: it
|
||||
/// opened a leaf only for a FOCUS record, via `highlight_name`.
|
||||
///
|
||||
/// That omission is measurable. It is the whole of the title's 1.82 %
|
||||
/// disagreement with the oracle — the port draws two 400 px sprites upright
|
||||
/// and static at (441, 270) where the game sweeps two ~1080 and ~1440 px
|
||||
/// quads across the frame at opposite leans.
|
||||
///
|
||||
/// ⚠️ **Emitted, not yet drawn.** Parent and leaf each carry their own alpha
|
||||
/// ramp on a different span — parent 0→255 over t=70…238, leaf
|
||||
/// 255→0x80→255 over t=150…600 — so how the two compose is a *decoding*
|
||||
/// question and not the port's to answer. The data is exported so it stops
|
||||
/// being invisible; `ScreenView` ignores it until the composition rule is
|
||||
/// known.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub leaf: Option<Focus>,
|
||||
|
||||
/// True when the leaf's geometry DIFFERS from the parent's, so the leaf is
|
||||
/// what the game draws.
|
||||
///
|
||||
/// Decided here rather than in the runtime because it is disc knowledge.
|
||||
/// The Decoder's rule: *"the discriminator is which record carries the
|
||||
/// geometry, not a fixed order"* — and the census over this export splits
|
||||
/// cleanly, with no ambiguous middle:
|
||||
///
|
||||
/// * **30 of 46** leaf elements duplicate the parent's scale and rotation
|
||||
/// exactly. That is the BASE-record case `screen.rs` already handled: the
|
||||
/// leaf may differ by a unit of position (`ptbtn04`: parent y=401, leaf
|
||||
/// y=402) and the parent wins. Flag is false; nothing changes.
|
||||
/// * **16 differ**, and all of them differ in scale or rotation, not by a
|
||||
/// rounding unit: the ten `ptloop01`/`ptloop02` sweeps ((100,600) at +30°
|
||||
/// and (100,800) at −45° against an identity parent), two
|
||||
/// `pgloading_ring` (leaf scale **(0,0)**), and `title_jp`'s
|
||||
/// `ptlogo_eff2` (**parent 125 %, leaf 100 %**).
|
||||
///
|
||||
/// ⚠️ **Only the `ptloop` case is decoded.** The Decoder fitted the game's
|
||||
/// own composed alpha — vertex colours `C3FFFFFF`/`B6FFFFFF`, i.e. 195 and
|
||||
/// 182 — against the two leaf ramps and got one consistent time, t=355, then
|
||||
/// *predicted* the quad centres at 981 and 478 against 992.0 and 467.2
|
||||
/// measured. The other two are the same shape and are **not** separately
|
||||
/// confirmed; they are flagged so the harness can adjudicate them rather
|
||||
/// than being asserted.
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub leaf_carries_geometry: bool,
|
||||
/// The raw `opt ` link inside this element's `.rat` record.
|
||||
///
|
||||
/// ⚠️ **This is not a focus link.** It was read as one, and that was
|
||||
@@ -161,6 +264,23 @@ pub struct Element {
|
||||
/// Paint-order key. `"sprite"` = read from the `T8aD` header at `+0x0A`.
|
||||
/// `"implied"` = **measured off the running game**, for elements that carry
|
||||
/// no header. `"none"` = neither; sorts last.
|
||||
/// `true` when the game draws this element ADDITIVE — `T8aD +0x04` bit
|
||||
/// `0x02`.
|
||||
///
|
||||
/// 🔴 **DECODED, and it replaces an authored map.** The port carried an
|
||||
/// `additive_elements` table in `authored/rendering.json`, keyed by SCREEN
|
||||
/// NAME and transcribed from the Decoder's per-draw `RB_BLENDCONTROL0` log.
|
||||
/// A name-keyed map cannot answer for a screen nobody drove the game to,
|
||||
/// which is why the port was drawing the English menus additive and the
|
||||
/// Japanese ones alpha-over — asserting by omission that the JP build
|
||||
/// blends differently. The bit is on the disc for every screen at once.
|
||||
///
|
||||
/// ⚠️ `kind_raw` is NOT this field. `kind` is `+40` of the RATC declaration
|
||||
/// entry; this is `+0x04` of the sprite's own `T8aD` header. Tested over
|
||||
/// four screens: `kind & 0x2` is *anti*-correlated with the measured map —
|
||||
/// 0 of 14 additive elements set it and 9 non-additive ones do.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blend_additive: Option<bool>,
|
||||
pub layer_source: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub layer: Option<String>,
|
||||
@@ -201,6 +321,35 @@ pub struct Screen {
|
||||
/// **Geometric, not a decoded neighbour graph** — right for a vertical menu
|
||||
/// and not to be trusted for anything else.
|
||||
pub buttons: Vec<String>,
|
||||
|
||||
/// The instant every element of this screen is settled at, and the width of
|
||||
/// the interval it was taken from — `[start, end, midpoint]` in keyframe
|
||||
/// units, absent when the screen has fewer than two keyframe times.
|
||||
///
|
||||
/// 🔴 **A SETTLED SCREEN IS ONE INSTANT, AND THE DISC SAYS WHICH.** Posing
|
||||
/// each element at its own `rest()` is right for anything that ends the
|
||||
/// screen settled and **exactly wrong for a transient**: the title's
|
||||
/// `ptlogo_back2eff1` is a two-frame flash — 0 until t52, 255 at t54–56, 0
|
||||
/// again by t58 — so its last *hold* is the flash peak and `rest()` leaves
|
||||
/// it burning forever. There are five of these, and `rest()` draws all five
|
||||
/// at once, saturating the light arc.
|
||||
///
|
||||
/// The window is the **longest interval containing no keyframe time**, over
|
||||
/// this bundle's TOP-LEVEL elements only. Nested leaves are excluded, and
|
||||
/// that exclusion is what reproduces the Decoder's independently computed
|
||||
/// `[160, 236]` for the title: including the `ptloop` leaves gives
|
||||
/// `[269, 540]` instead.
|
||||
///
|
||||
/// ⚠️ **Emitted for every screen; USABLE only where it is wide.** Across this
|
||||
/// export the widths split with nothing in between — `press_start` 214,
|
||||
/// `publisher_logo` 190, `developer_logos` 145, `title` 76, then
|
||||
/// `main_menu` 12, `extras` 12, the loading screens 8 and 4. A 12-unit
|
||||
/// "settle" on a menu that builds in until t=70 is not a settled pose, it is
|
||||
/// a gap between staggered ramps. The Decoder's disc-wide census agrees on
|
||||
/// the shape: only 30 % of bundles have a window ≥ 30 units and 42 % have
|
||||
/// one under 10, the latter mostly `loop*` fragments meant to be in motion.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub settle_window: Option<[i64; 3]>,
|
||||
/// What this file does not answer. A consumer needing one of these must get
|
||||
/// it from `authored/`.
|
||||
pub unresolved: Vec<&'static str>,
|
||||
@@ -316,6 +465,76 @@ pub fn export_build(
|
||||
// Contrast with a BASE record, where the leaf duplicates the parent's
|
||||
// placement and the two can differ by a unit (ptbtn04: parent y=401,
|
||||
// leaf y=402). There the parent wins. Here there is no parent.
|
||||
// Reads one record in the bundle as a nested build and returns its
|
||||
// elements. Used twice: for a FOCUS record (`ptbtn0Nf.rat`) and for an
|
||||
// element whose OWN declared name is a record (`ptloop01.rat`). One
|
||||
// implementation, because the second case was missing for eight
|
||||
// milestones and a second copy is how it would go missing again.
|
||||
let read_leaf = |rec: &str,
|
||||
written: &mut std::collections::BTreeMap<String, ()>,
|
||||
missing: &mut Vec<String>|
|
||||
-> Result<Option<Focus>> {
|
||||
let Some(&(off, size)) = b.records.get(rec) else { return Ok(None) };
|
||||
let Some(leaf) = ui_layout::parse_build(&bundle[off..off + size]) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let mut fes = Vec::new();
|
||||
for fe in &leaf.elements {
|
||||
let sp: &str = fe.sprite.as_deref().unwrap_or(&fe.name);
|
||||
let mut fsprite = None;
|
||||
if write_from(&sprite_dir, written, sp, &bundle[off..off + size], &leaf.sprites)?
|
||||
|| write_from(&sprite_dir, written, sp, bundle, &b.sprites)?
|
||||
{
|
||||
fsprite = Some(sprite_rel(sp));
|
||||
} else if sp.ends_with(".t32") {
|
||||
missing.push(sp.to_string());
|
||||
}
|
||||
let Some(r) = fe.rest() else { continue };
|
||||
fes.push(FocusElement {
|
||||
id: id_of(&fe.name),
|
||||
declared: fe.name.clone(),
|
||||
sprite: fsprite,
|
||||
blend_additive: ui_layout::blend_additive_by_name(
|
||||
&leaf, &bundle[off..off + size], sp)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
pivot: [fe.pivot_x, fe.pivot_y],
|
||||
rest: Rest {
|
||||
pos: [r.x, r.y],
|
||||
scale: [r.scale_x, r.scale_y],
|
||||
tint_rgba: hex32(r.tint),
|
||||
fade_argb: hex32(r.fade),
|
||||
rotation_deg: r.rotation_deg,
|
||||
t: r.time,
|
||||
},
|
||||
keyframes: fe
|
||||
.keyframes
|
||||
.iter()
|
||||
.map(|k| Keyframe {
|
||||
t: k.time,
|
||||
pos: [k.x, k.y],
|
||||
scale: [k.scale_x, k.scale_y],
|
||||
tint_rgba: hex32(k.tint),
|
||||
fade_argb: hex32(k.fade),
|
||||
rotation_deg: k.rotation_deg,
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
Ok(if fes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Focus {
|
||||
record: rec.to_string(),
|
||||
loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]),
|
||||
elements: fes,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// An element whose own declared name is a record in this bundle carries
|
||||
// its geometry THERE, not in its parent entry. See `Element::leaf`.
|
||||
let leaf = read_leaf(&el.name, &mut written, &mut missing)?;
|
||||
|
||||
let mut focus = None;
|
||||
if let Some(rec) = highlight_name(&el.name) {
|
||||
if let Some(&(off, size)) = b.records.get(&rec) {
|
||||
@@ -341,6 +560,9 @@ pub fn export_build(
|
||||
id: id_of(&fe.name),
|
||||
declared: fe.name.clone(),
|
||||
sprite: fsprite,
|
||||
blend_additive: ui_layout::blend_additive_by_name(
|
||||
&leaf, &bundle[off..off + size], sp)
|
||||
.or_else(|| ui_layout::blend_additive_by_name(&b, bundle, sp)),
|
||||
pivot: [fe.pivot_x, fe.pivot_y],
|
||||
rest: Rest {
|
||||
pos: [r.x, r.y],
|
||||
@@ -365,7 +587,11 @@ pub fn export_build(
|
||||
});
|
||||
}
|
||||
if !fes.is_empty() {
|
||||
focus = Some(Focus { record: rec, elements: fes });
|
||||
focus = Some(Focus {
|
||||
record: rec,
|
||||
loop_length_units: ui_layout::loop_length_units(&bundle[off..off + size]),
|
||||
elements: fes,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,10 +623,21 @@ pub fn export_build(
|
||||
sprite: sprite_out,
|
||||
focus_sprite,
|
||||
focus,
|
||||
leaf_carries_geometry: leaf.as_ref().is_some_and(|l| {
|
||||
let p = el.rest();
|
||||
l.elements.iter().any(|le| {
|
||||
p.is_none_or(|p| {
|
||||
le.rest.scale != [p.scale_x, p.scale_y]
|
||||
|| le.rest.rotation_deg != p.rotation_deg
|
||||
})
|
||||
})
|
||||
}),
|
||||
leaf,
|
||||
opt_link: el.focus_link.clone(),
|
||||
pivot: [el.pivot_x, el.pivot_y],
|
||||
size: (role == "primitive").then(|| [el.pivot_x * 2, el.pivot_y * 2]),
|
||||
parent: el.parent,
|
||||
blend_additive: ui_layout::sprite_blend_additive(&b, bundle, el),
|
||||
layer_source,
|
||||
layer,
|
||||
focused: el.focused,
|
||||
@@ -419,14 +656,33 @@ pub fn export_build(
|
||||
|
||||
// Navigation order is geometric: buttons top-to-bottom by resting Y. A
|
||||
// focused-state record is not itself a menu item.
|
||||
//
|
||||
// 🔴 `0x3003` IS `0x3002`. Bit 0 of `kind` is the PARENT FLAG and carries no
|
||||
// role information: decoded disc-wide over every `.pak` in `dat/`, `kind & 1`
|
||||
// agrees with "has a parent" on 15 493 elements with ZERO disagreements
|
||||
// (`docs/re/ui-kind-bit0-is-has-parent.md`). Matching only `0x3002` meant the
|
||||
// OPTIONS menu's five rows -- parented, hence `0x3003` -- were not buttons,
|
||||
// so the screen opened and could not be navigated.
|
||||
//
|
||||
// ⚠️ TWO VALUES, LISTED, NOT A MASK. `kind & 0xFFFE == 0x3002` would also
|
||||
// match `0x73002`/`0x73003` -- 160 elements whose `0x70000` bits nobody has
|
||||
// decoded -- and it would do it silently, on screens neither agent has
|
||||
// looked at. Those are excluded by construction until somebody decides about
|
||||
// them deliberately.
|
||||
let mut buttons: Vec<(i32, String)> = b
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| e.kind == 0x3002 && !e.focused)
|
||||
.filter(|e| matches!(e.kind, 0x3002 | 0x3003) && !e.focused)
|
||||
.filter_map(|e| e.rest().map(|k| (k.y, id_of(&e.name))))
|
||||
.collect();
|
||||
buttons.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
|
||||
|
||||
let window = settle_window(&elements);
|
||||
let order = forced_backdrop_first(
|
||||
ui_layout::derived_paint_order(&b, bundle),
|
||||
&elements,
|
||||
[b.design_w, b.design_h],
|
||||
);
|
||||
let screen = Screen {
|
||||
format: "sylpheed.screen/3",
|
||||
exporter: exporter.to_string(),
|
||||
@@ -441,8 +697,9 @@ pub fn export_build(
|
||||
name_why,
|
||||
design: [b.design_w, b.design_h],
|
||||
elements,
|
||||
paint_order: ui_layout::derived_paint_order(&b, bundle),
|
||||
paint_order: order,
|
||||
buttons: buttons.into_iter().map(|(_, n)| n).collect(),
|
||||
settle_window: window,
|
||||
unresolved: vec![
|
||||
// The time unit is measured off the running game, not on the disc.
|
||||
"keyframe_time_unit",
|
||||
@@ -473,3 +730,234 @@ pub fn export_build(
|
||||
missing,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// The longest interval containing no keyframe time, over TOP-LEVEL elements.
|
||||
///
|
||||
/// See [`Screen::settle_window`] for why this is the settled instant and why
|
||||
/// nested leaves are excluded. Returns `[start, end, midpoint]`.
|
||||
fn settle_window(elements: &[Element]) -> Option<[i64; 3]> {
|
||||
let mut times: Vec<i64> = elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.t.map(i64::from)))
|
||||
.collect();
|
||||
times.sort_unstable();
|
||||
times.dedup();
|
||||
if times.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
// 🔴 A GAP IN WHICH NOTHING IS VISIBLE IS NOT A SETTLE WINDOW.
|
||||
//
|
||||
// The widest keyframe-free interval is only a settled state if the screen is
|
||||
// actually PRESENTING something across it. `press_start` is the case that
|
||||
// proves it: its keyframes are 0, 214, 236, 238, 244, so the widest gap is
|
||||
// 0..214 -- the dead stretch BEFORE the plate appears, where `ptbtn00` is
|
||||
// alpha 0 throughout. Taking its midpoint gave a settle instant of t=107,
|
||||
// and the runtime then answered every question about that screen at t=107.
|
||||
// The result was that the PRESS (A) plate could not be drawn at any instant
|
||||
// at all, including the boot's own end state, whose entire purpose is to
|
||||
// show it.
|
||||
//
|
||||
// The fix is not a tuned threshold: it is that the heuristic was reading an
|
||||
// interval where the screen is BLANK as the interval where it has arrived.
|
||||
// Rejecting those leaves `press_start` with 214..236 (22 units), which is
|
||||
// under the runtime's 30-unit bar, so it falls back to each element's own
|
||||
// hold -- which is the plate, opaque, exactly as the disc declares it.
|
||||
//
|
||||
// ⚠️ This does not disturb the windows the settle instant was measured on.
|
||||
// `title` keeps [160, 236]: elements are visible across it, and the
|
||||
// Decoder's draw stream independently found the game's clock freezing in
|
||||
// that same interval.
|
||||
let visible_at = |t: i64| elements.iter().any(|e| alpha_at(e, t) > 0);
|
||||
let (a, b) = times
|
||||
.windows(2)
|
||||
.map(|w| (w[0], w[1]))
|
||||
.filter(|(a, b)| visible_at((a + b) / 2))
|
||||
.max_by_key(|(a, b)| b - a)?;
|
||||
Some([a, b, (a + b) / 2])
|
||||
}
|
||||
|
||||
|
||||
/// Alpha of one element at instant `t`, under the linear ramp the port uses.
|
||||
fn alpha_at(e: &Element, t: i64) -> u8 {
|
||||
let ks = &e.keyframes;
|
||||
let a = |k: &Keyframe| (u32::from_str_radix(k.fade_argb.trim_start_matches("0x"), 16)
|
||||
.unwrap_or(0) >> 24) as i64;
|
||||
let timed: Vec<&Keyframe> = ks.iter().filter(|k| k.t.is_some()).collect();
|
||||
if timed.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
if t <= timed[0].t.unwrap() as i64 {
|
||||
return a(timed[0]) as u8;
|
||||
}
|
||||
for w in timed.windows(2) {
|
||||
let (t0, t1) = (w[0].t.unwrap() as i64, w[1].t.unwrap() as i64);
|
||||
if t < t1 {
|
||||
if t1 <= t0 {
|
||||
return a(w[0]) as u8;
|
||||
}
|
||||
let f = (t - t0) as f64 / (t1 - t0) as f64;
|
||||
return (a(w[0]) as f64 + (a(w[1]) - a(w[0])) as f64 * f).round() as u8;
|
||||
}
|
||||
}
|
||||
a(timed[timed.len() - 1]) as u8
|
||||
}
|
||||
|
||||
/// Scale of one element at instant `t`, in percent per axis, under the same
|
||||
/// linear ramp as the fade. Interpolated rather than stepped, because a scale
|
||||
/// that animates passes through every value between its keyframes.
|
||||
fn scale_at(e: &Element, t: i64) -> [f64; 2] {
|
||||
let timed: Vec<&Keyframe> = e.keyframes.iter().filter(|k| k.t.is_some()).collect();
|
||||
if timed.is_empty() {
|
||||
return [100.0, 100.0];
|
||||
}
|
||||
let g = |k: &Keyframe, i: usize| k.scale[i] as f64;
|
||||
if t <= timed[0].t.unwrap() as i64 {
|
||||
return [g(timed[0], 0), g(timed[0], 1)];
|
||||
}
|
||||
for w in timed.windows(2) {
|
||||
let (t0, t1) = (w[0].t.unwrap() as i64, w[1].t.unwrap() as i64);
|
||||
if t < t1 {
|
||||
if t1 <= t0 {
|
||||
return [g(w[0], 0), g(w[0], 1)];
|
||||
}
|
||||
let f = (t - t0) as f64 / (t1 - t0) as f64;
|
||||
return [
|
||||
g(w[0], 0) + (g(w[1], 0) - g(w[0], 0)) * f,
|
||||
g(w[0], 1) + (g(w[1], 1) - g(w[0], 1)) * f,
|
||||
];
|
||||
}
|
||||
}
|
||||
let l = timed[timed.len() - 1];
|
||||
[g(l, 0), g(l, 1)]
|
||||
}
|
||||
|
||||
/// Move a full-screen opaque primitive to the FRONT of the paint order when the
|
||||
/// file forces it there.
|
||||
///
|
||||
/// 🔴 **The rule is a constraint, not a preference**, and it is the Decoder's:
|
||||
/// *an element that covers the screen and is fully opaque at some instant cannot
|
||||
/// paint above anything visible at that instant; where the elements visible
|
||||
/// during its opaque span are ALL of them, its position is forced to first.*
|
||||
///
|
||||
/// It was found because `build_12`/`build_15` are **black at every instant** of
|
||||
/// their declared timeline under the old rule — `pgloading_eff00` is opaque for
|
||||
/// 39 instants while all 9 other elements live and die inside that span. A
|
||||
/// screen that is black for its whole life is impossible on its face, which is
|
||||
/// the only kind of check that survives two renderers sharing an assumption:
|
||||
/// `sylpheed-cli` agreed with the port here because it agreed about
|
||||
/// `implied_layer_key`.
|
||||
///
|
||||
/// Two measured controls, both prior orders off the running game:
|
||||
///
|
||||
/// | primitive | measured | opaque instants | forced below | |
|
||||
/// |---|---|---|---|---|
|
||||
/// | `palogo_eff0.prm` | **first** | 211 | 6 of 6 | ✅ forced |
|
||||
/// | `pteff00.prm` | **last** | 2 | 3 of 23 | ✅ permitted on top |
|
||||
///
|
||||
/// ⚠️ **Do NOT reduce this to a name heuristic.** `*base*` first / `*eff*` last
|
||||
/// matches 77 of 80 and fails on exactly the three families that cross it —
|
||||
/// `palogo_eff0`, `pgloading_eff00`, `pzeff00`. `palogo_eff0.prm` is *named like
|
||||
/// an overlay* and is measured painting first. The name is not the rule.
|
||||
///
|
||||
/// 🔴 **And it is restricted to elements with NO SPRITE**, which is the limit
|
||||
/// that the rule's own disc-wide test caught: applied to sprites it claimed 22
|
||||
/// `.t32` textures must sort first *against their own layer keys*. **An
|
||||
/// element's alpha says nothing about whether its texture covers the screen** —
|
||||
/// most of a sprite may be transparent.
|
||||
///
|
||||
/// ⚠️ Reach: assumes straight alpha-over. Blend mode is undecoded, and an
|
||||
/// additive quad at alpha 255 would not occlude. It is a lower bound, not an
|
||||
/// ordering — it says nothing about elements that are constrained but not
|
||||
/// forced. Delete this when a pinned `sylpheed-formats` does it.
|
||||
fn forced_backdrop_first(order: Vec<usize>, elements: &[Element], design: [u32; 2]) -> Vec<usize> {
|
||||
let screen_end: i64 = elements
|
||||
.iter()
|
||||
.flat_map(|e| e.keyframes.iter().filter_map(|k| k.t))
|
||||
.map(i64::from)
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let forced: Vec<usize> = elements
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, e)| {
|
||||
// 🔴 UNTEXTURED SOLID QUAD, tested positively -- NOT merely "has no
|
||||
// sprite". Those coincide in GP_TITLE and the distinction is still
|
||||
// the whole point, because the negative test guards a SYMPTOM.
|
||||
//
|
||||
// The rule needs the element's alpha to BE its pixels' alpha. That
|
||||
// is true of a `.prm` solid quad and of nothing else. The Decoder
|
||||
// found this the expensive way twice: first `.t32` sprites (an
|
||||
// element's alpha says nothing about a texture that is mostly
|
||||
// transparent), guarded with "no sprite" -- and then `.tbm`, which
|
||||
// is 38 of their 80 forced-first verdicts and declares fade
|
||||
// `ffffffff`. A solid WHITE quad painted first at alpha 255 would
|
||||
// make the screen white; no screen is white, so a `.tbm`'s white is
|
||||
// a modulation ON a texture and its element alpha proves nothing
|
||||
// about coverage either.
|
||||
//
|
||||
// "No sprite" would keep admitting a `.tbm` that this exporter
|
||||
// happens not to emit a sprite for. `role == "primitive"` cannot.
|
||||
// GP_TITLE has no full-screen `.tbm` at all -- every layerless
|
||||
// full-screen element here is `.prm` and pure black, checked -- so
|
||||
// this changes no verdict today and is a guard against a corpus
|
||||
// that grows.
|
||||
// Cheap prefilter only -- the binding coverage test is per-instant,
|
||||
// in `covers` below. An element scaled ABOVE 100 could cover the
|
||||
// screen from a smaller declared size, so this deliberately does
|
||||
// not reject on size.
|
||||
e.role == "primitive" && e.sprite.is_none() && e.size.is_some()
|
||||
})
|
||||
.filter(|(i, e)| {
|
||||
let span: Vec<i64> = e
|
||||
.keyframes
|
||||
.iter()
|
||||
.filter_map(|k| k.t)
|
||||
.map(i64::from)
|
||||
.collect();
|
||||
let Some(&lo) = span.first() else { return false };
|
||||
// 🔴 COVERAGE IS TESTED AT EACH INSTANT, NOT ONCE FROM `size`.
|
||||
// Declared size alone is not what the element draws: scale is a
|
||||
// percent per axis and it animates. `pbafc.prm` is the disc's own
|
||||
// counterexample -- declared 844x600, scaled 2 % x 3 %, so it draws
|
||||
// about 17x18 px, a moving glint rather than a wash. A rule that
|
||||
// read its declared size would call it screen-covering.
|
||||
//
|
||||
// Nothing in GP_TITLE needs this: every layerless full-screen
|
||||
// element here is at scale 100 on every keyframe, so no verdict
|
||||
// moves. It is in because the data that would break it exists on
|
||||
// this disc, which is a better reason than a failure would have been.
|
||||
let covers = |t: i64| {
|
||||
let sc = scale_at(e, t);
|
||||
e.size.is_some_and(|s| {
|
||||
s[0] as f64 * sc[0] / 100.0 >= design[0] as f64
|
||||
&& s[1] as f64 * sc[1] / 100.0 >= design[1] as f64
|
||||
})
|
||||
};
|
||||
// An element HOLDS ITS FINAL POSE to the end of the screen -- it does
|
||||
// not vanish at its own last keyframe. `palogo_eff0.prm` is the case
|
||||
// that shows why: it declares ONE keyframe, opaque black full-screen
|
||||
// at t=0, and reading its span as `0..=0` makes the splash's backdrop
|
||||
// a single-instant event instead of the thing that is on screen for
|
||||
// the whole splash. So the span runs to the SCREEN's last keyframe.
|
||||
let hi = screen_end.max(*span.last().unwrap());
|
||||
let opaque: Vec<i64> = (lo..=hi)
|
||||
.filter(|&t| alpha_at(e, t) == 255 && covers(t))
|
||||
.collect();
|
||||
if opaque.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Every OTHER element must be visible somewhere inside that span.
|
||||
elements.iter().enumerate().all(|(j, o)| {
|
||||
j == *i || opaque.iter().any(|&t| alpha_at(o, t) > 0)
|
||||
})
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
if forced.is_empty() {
|
||||
return order;
|
||||
}
|
||||
let mut out = forced.clone();
|
||||
out.extend(order.into_iter().filter(|i| !forced.contains(i)));
|
||||
out
|
||||
}
|
||||
|
||||
597
docs/port/f6-what-starts-the-sweep.md
Normal file
597
docs/port/f6-what-starts-the-sweep.md
Normal file
@@ -0,0 +1,597 @@
|
||||
# F6 unit a — what the port currently uses to start the title sweep
|
||||
|
||||
**Status:** ✅ answered. **No behaviour changed** — this unit exists so that when
|
||||
the Decoder says *when* the glow should start, the edit is one line.
|
||||
Port at `937f055`, 2026-09-02.
|
||||
|
||||
## The answer, in one line
|
||||
|
||||
`port/scripts/screen_view.gd:684`
|
||||
|
||||
```gdscript
|
||||
var t := leaf_time_units if leaf_time_units >= 0.0 else time_units
|
||||
```
|
||||
|
||||
**That is the whole start mechanism, and it is not a start mechanism.**
|
||||
`leaf_time_units` is set in exactly one place — `boot.gd:359`, the `--leaf-time`
|
||||
diagnostic flag — and is `-1.0` on every real boot. So the travelling glow runs
|
||||
on `view.time_units`, the title screen's own clock, which `_advance` sets to
|
||||
`0.0` when the title is raised. **Zero offset, no gate.**
|
||||
|
||||
## 🔴 And the obvious gate does not exist
|
||||
|
||||
The natural reading — mine, before checking — is that the parents gate it:
|
||||
`ptloop01`/`ptloop02` declare `0:0 70:0 100:255 238:255 250:0`, invisible until
|
||||
t=70. **That is not what happens**, because what reaches the screen is the LEAF,
|
||||
and `screen_view.gd` records as decoded that *"the leaf runs on its OWN timeline
|
||||
and the parent's alpha is NOT multiplied in"*. The parent ramp gates nothing.
|
||||
|
||||
The leaves' own declarations:
|
||||
|
||||
| leaf | alpha | x position |
|
||||
|---|---|---|
|
||||
| `pteff03` | **`0:255`** 150:128 540:255 600:255 | −639 → −39 (t=150) → 1521 (t=540) |
|
||||
| `pteff03a` | 0:0 150:128 630:255 720:255 | 1721 → 1111 (t=150) → −839 (t=630) |
|
||||
|
||||
**`pteff03` is at full alpha from title t=0** and is travelling from t=0. It
|
||||
clears the left edge (sprite is 399 wide) at around t≈60 and is well inside the
|
||||
frame by t=150.
|
||||
|
||||
The plate arrives at **t=214–236**. So the port starts the sweep roughly
|
||||
**150+ units ≈ 2.5 s early** — which is the size and the direction of what the
|
||||
human reported.
|
||||
|
||||
## Corroborated on a film, not only read
|
||||
|
||||
Filmed a real boot at 0.05 s and measured frame-to-frame change in the title art
|
||||
band `1280x420+0+90`, which **excludes the plate's own rectangle** (y 550–600) so
|
||||
the plate cannot be what registers:
|
||||
|
||||
```
|
||||
view_units 22 54 69 86 118 134 214 341 406
|
||||
delta 31.8 39.9 30.8 9.4 12.6 0.2 0.2 0.1 0.4
|
||||
```
|
||||
|
||||
Motion is heavy through the build-in and the band is quiet by t≈134 — consistent
|
||||
with `pteff03` having already crossed the measured band and with the coarse
|
||||
resize washing a thin glow out. **The film neither adds to nor contradicts the
|
||||
declaration; the declaration is the evidence here.**
|
||||
|
||||
## Where the sweep actually is, computed from the leaf's own translation
|
||||
|
||||
The port positions the leaf **from the leaf's own clock** — it does not draw the
|
||||
parent's pose and ignore the translation. Sprite 399 wide on a 1280 screen:
|
||||
|
||||
| t | 0 | **61** | 70 | 100 | 150 | **236** | 250 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `pteff03` x | −639 | **−395** | −359 | −239 | −39 | **305** | 361 |
|
||||
| `pteff03a` x | 1721 | 1477 | 1436 | 1314 | 1111 | 762 | 705 |
|
||||
|
||||
**`pteff03` enters the frame at t=61 and is mid-screen at t=305 when the plate
|
||||
reaches full alpha at t=236** — visible and travelling for ~175 units ≈ 2.9 s
|
||||
before the plate. `pteff03a` enters much later.
|
||||
|
||||
## 🔴 The open question in this file sits exactly inside F6's window
|
||||
|
||||
`screen_view.gd` flags its own limit on the leaf-vs-parent alpha decode:
|
||||
|
||||
> *"Every observation behind this has parent alpha 0, so 'the leaf wins' and 'the
|
||||
> parent is ignored because it draws nothing' are NOT separated. **A capture
|
||||
> during t=100…238 would separate them.**"*
|
||||
|
||||
The parent is non-zero exactly on `t=100…238`, and the plate arrives at 236. So
|
||||
that unresolved ambiguity is **the same interval F6 is about**, and it is
|
||||
load-bearing for the first stretch: the parent ramps 70→100, so applying it would
|
||||
hide the sweep until t=70 and dim it to t=100, while the port shows it at full
|
||||
alpha from t=61.
|
||||
|
||||
That accounts for ~40 units of the earliness. **It does not account for the other
|
||||
~175**, which is the leaf clock starting at title t=0 with no offset.
|
||||
|
||||
📌 **One capture in `t=100…238` would settle both** — F6's start time and the
|
||||
leaf/parent alpha rule — rather than two.
|
||||
|
||||
## What changes when the answer lands
|
||||
|
||||
A start time is an **offset**, and `leaf_time_units` is an absolute override —
|
||||
they are not the same field. The one-line edit at 684 becomes a subtraction, fed
|
||||
by one authored value. Nothing else moves.
|
||||
|
||||
## What this does NOT do
|
||||
|
||||
* **It does not choose a start time.** That is the Decoder's, and this unit was
|
||||
scoped to exclude it deliberately.
|
||||
* It does not touch the glow. A boot looks exactly as it did.
|
||||
* It says nothing about whether the *speed* or the *path* is right — only when it
|
||||
begins.
|
||||
|
||||
---
|
||||
|
||||
# 🔴 Unit b, HELD: the parent-alpha refutation may be right, but its identification step cannot carry it
|
||||
|
||||
**Status:** ⏸️ **the renderer is NOT changed.** The Decoder's
|
||||
`f6-unit2-parent-alpha-multiplies.md` refutes `screen_view.gd`'s *"the parent's
|
||||
alpha is NOT multiplied in"* using a bound. The bound's shape is sound and its
|
||||
premise checks out against this export. **The step that assigns the measurement
|
||||
to an element does not.**
|
||||
|
||||
## The premise holds
|
||||
|
||||
`pteff03`'s leaf declares `0:255 150:128 540:255 600:255` — **minimum 128**,
|
||||
confirmed off `export/screens/title/title.json`. A drawn alpha below 128 cannot
|
||||
come from that leaf alone. That part is right.
|
||||
|
||||
## 🔴 But the two strips are the SAME SIZE, so size cannot say which is which
|
||||
|
||||
The identification is stated as *"by size against the corpus's independently
|
||||
measured AABB height of 1134 px"*. Measured off this export:
|
||||
|
||||
| sprite | dimensions | leaf alpha range | travel |
|
||||
|---|---|---|---|
|
||||
| `pteff03` | **399 × 180** | **128 … 255** | left → right (−639 → 1521) |
|
||||
| `pteff03a` | **399 × 180** | **0 … 255** | right → left (1721 → −839) |
|
||||
|
||||
**They are byte-identical in size**, which is consistent with the two reported
|
||||
rows measuring `1.38 × 3.15` and `1.39 × 3.15` — a 0.7 % difference. Size
|
||||
separates the sweeps from everything else on the screen; it cannot separate them
|
||||
from **each other**, and that is the distinction the argument needs.
|
||||
|
||||
## Why it matters — the assignment flips the conclusion
|
||||
|
||||
The quoted row that reaches **8** is the one the argument leans on. But
|
||||
`pteff03a`'s leaf alpha floors at **0**, not 128, and ramps `0 → 128` across
|
||||
t=0…150. Values of 8, 24, 33, 50 … are exactly what **that leaf alone** produces.
|
||||
So if the 8-row is `pteff03a`, the bound is satisfied with no parent at all.
|
||||
|
||||
⚠️ **And the conclusion may still be correct via the OTHER row.** The row
|
||||
reported as `16 41 67 91 116 128 129 130 131` contains values below 128 *and* a
|
||||
dense cluster at 128–131 — the signature of `pteff03`'s floor. If that row is
|
||||
`pteff03`, then 16 < 128 refutes no-multiply exactly as claimed. **The finding
|
||||
may be right and the cited row wrong.**
|
||||
|
||||
## The discriminator is free and already in their capture
|
||||
|
||||
The two leaves travel in **opposite directions**: `pteff03` left→right,
|
||||
`pteff03a` right→left, separated by ~1 000 px for most of their run. One frame
|
||||
pair settles it. Nothing needs re-capturing.
|
||||
|
||||
## ✅ Resolved: the discriminator worked, and MY proposed repair was wrong
|
||||
|
||||
The Decoder ran the travel-direction check on the capture they already had:
|
||||
|
||||
```
|
||||
1.38x3.15 n=1140 x centre -1.690 -> +0.500 LEFT->RIGHT
|
||||
1.39x3.15 n= 614 x centre -1.685 -> +0.495 LEFT->RIGHT
|
||||
```
|
||||
|
||||
**Both rows travel the same way, so both are `pteff03`** — the 0.7 % size
|
||||
difference is per-frame rounding splitting **one** element across two keys.
|
||||
`pteff03a` is not in the capture at all.
|
||||
|
||||
🔴 **So my "the finding may still be correct via the OTHER row" does not apply —
|
||||
there is no other row.** The identification was broken a third way that neither
|
||||
of us named: not two elements confused for each other, but one element counted
|
||||
twice. I was right that size could not carry the assignment and wrong about what
|
||||
the assignment actually was.
|
||||
|
||||
⚠️ And the part I could not see from here, which is the larger one: the quoted
|
||||
`8 24 33 50 58 …` were the nine **lowest distinct** values, not a series — a
|
||||
tail, presented in a shape that reads as a ramp. **14 sub-floor samples out of
|
||||
1754.** The bound survives in shape and collapses in weight: one sample below 128
|
||||
is still unexplainable by a leaf flooring at 128, but 14 of 1754 is where a
|
||||
grouping slip or a partial frame would show up.
|
||||
|
||||
**Status: 🟡 not refuted, not established.** The next unit is a re-read of data
|
||||
already on disk — the same capture as a *series* across t=70…100, where a
|
||||
multiplying parent must ramp on nearly every frame rather than in 14 stragglers.
|
||||
That unit is the Decoder's and has not been run.
|
||||
|
||||
## What the port does about it: nothing yet
|
||||
|
||||
Adopting a decode whose element assignment is unresolved is how this project has
|
||||
been burned before — and the port would be changing a renderer rule on it.
|
||||
`screen_view.gd` keeps its current behaviour until the row is identified by
|
||||
travel direction. **This is not scepticism about the conclusion**, which the
|
||||
port's own flagged limit predicted would fall this way; it is about which row.
|
||||
|
||||
---
|
||||
|
||||
# 📌 What was actually holding this up: we both picked the wrong observable
|
||||
|
||||
The human asked what was blocking us and whether the approach needed revising.
|
||||
**It did, and the diagnosis is short: two agents spent three exchanges on ALPHA
|
||||
while the answer sat in a POSITION series neither of us compared to anything.**
|
||||
|
||||
## The arithmetic that should have been step one
|
||||
|
||||
The Decoder's capture reports the sweep's x-centre running `−1.690 → +0.500`.
|
||||
Read as NDC (`x_ndc = 2·x_px/W − 1`, **an assumption, stated as one**):
|
||||
|
||||
| | ndc | centre px | implied leaf t |
|
||||
|---|---|---|---|
|
||||
| first sample | −1.690 | −441.6 | **−0.5** |
|
||||
| last sample | +0.500 | +960.0 | 349.9 |
|
||||
| **declared at leaf t=0** | **−1.687** | **−439.5** | — |
|
||||
|
||||
**A 2 px agreement on a 2 160 px travel.** So the game's sweep begins travelling
|
||||
at leaf t≈0, from off-screen left — **the same as the port.**
|
||||
|
||||
🔴 **That contradicts my own earlier framing on this page**, which attributed
|
||||
~135 units of the earliness to "the leaf clock starting at title t=0 with no
|
||||
offset". If the game's leaf clock also starts at 0, that is not a defect and F6
|
||||
is a **visibility** question — alpha, or draw order, or something not yet named —
|
||||
rather than a clock question. I am flagging it rather than rewriting the section:
|
||||
this rests on two numbers relayed in a message, which is exactly the thing that
|
||||
should be read from the repository instead.
|
||||
|
||||
## Why alpha was the wrong tool, stated generally
|
||||
|
||||
| | alpha | position |
|
||||
|---|---|---|
|
||||
| dynamic range | 8 bits, quantised | **2 160 px** |
|
||||
| shape | non-monotone, ramps and holds | **monotone** |
|
||||
| failure mode that bit us | a 14-sample tail out of 1754 looks like signal | a wrong shape raises the residual |
|
||||
| yields the clock? | no | **origin AND rate together** |
|
||||
|
||||
**When something moves, its position carries the clock and its alpha carries
|
||||
almost nothing.** Neither of us reached for a trajectory comparison because
|
||||
neither of us had one.
|
||||
|
||||
## So: `tools/port/fit-trajectory`
|
||||
|
||||
Solves `x_measured(frame) ≈ declared(t0 + rate·frame)` for the pair, and reports
|
||||
the **residual**, which is the part that matters: it says whether the model was
|
||||
right at all, where a value-at-an-instant never can.
|
||||
|
||||
Its `--selftest` runs both directions — recovers a known clock from a synthesised
|
||||
series to 0.09 px, and **rejects** a wrong-shape series at 81.9 px against a 20 px
|
||||
bar — because a fit that cannot fail is a curve-fitter, not a measurement. Wired
|
||||
into `check-all`.
|
||||
|
||||
⚠️ It fits a **constant** rate. A stalling guest clock or uneven capture drops
|
||||
raise the residual rather than being absorbed, which is deliberate.
|
||||
|
||||
---
|
||||
|
||||
# ❌ WITHDRAWN — Unit c: "the port draws a sweep the game does not"
|
||||
|
||||
> 🔴 **This whole section is refuted, and the port was right.** `pteff03a` **is**
|
||||
> drawn by the game. The two strips are batched into a **single additive draw of
|
||||
> eight vertices — two quads** — and the Decoder's log reader took the first
|
||||
> vertex match per draw line and discarded the rest, so every analysis saw quad A
|
||||
> and never quad B. No new capture was needed; `pteff03a` was in the same logs
|
||||
> that were read as declaring it absent
|
||||
> (`docs/re/f6-unit11-pteff03a-IS-drawn.md`). Measured on both sides: the strips
|
||||
> travel in opposite directions with a size ratio of 1.301 against the declared
|
||||
> 800/600 = 1.333.
|
||||
>
|
||||
> ✅ **Nothing in the port changed on the strength of it.** I proposed gating
|
||||
> `pteff03a` and held, because absence in one capture read by one probe is a lead
|
||||
> and not a finding, and because the check I asked for was a human's look rather
|
||||
> than another measurement. That hold is the only reason this cost nothing.
|
||||
>
|
||||
> ⚠️ **And the absence claim cited the port as evidence against itself** — "the
|
||||
> port draws it, the game does not" — so a defect was inferred in my renderer
|
||||
> from a gap in a reader. Kept in place rather than deleted: the reasoning below
|
||||
> is sound given its premise, and the premise is exactly the kind that looks like
|
||||
> data.
|
||||
|
||||
## The original section, kept for its shape
|
||||
|
||||
## First, the correction: my refutation was right in outcome and WRONG in its reason
|
||||
|
||||
I challenged the Decoder's by-size identification on the ground that *"both
|
||||
sweep sprites are 399×180, so size cannot separate them"*. **That was wrong.** I
|
||||
compared the source PNGs and never looked at the leaf declarations:
|
||||
|
||||
| leaf | sprite | declared scale | **drawn height** |
|
||||
|---|---|---|---|
|
||||
| `pteff03` | 399×180 | `[100, 600]` | **1080 px** |
|
||||
| `pteff03a` | 399×180 | `[100, 800]` | **1440 px** |
|
||||
|
||||
The *drawn* quads differ by a third, which is exactly the 3.15 vs 3.62 NDC the
|
||||
Decoder was separating by. **Size distinguishes them fine.** The hold was still
|
||||
correct and the check I asked for still found a real defect — but it found a
|
||||
different one (one element double-counted, and a set presented as a series), and
|
||||
my stated reason did not survive. Recorded because a right answer reached by a
|
||||
wrong argument is the kind that gets cited later for the wrong reason.
|
||||
|
||||
## And it makes the real finding sharper
|
||||
|
||||
Because size *does* separate them, the Decoder's line — *"`pteff03a` does **not**
|
||||
appear in this capture at all"* — is well-evidenced rather than incidental. They
|
||||
looked for a distinct size and found nothing.
|
||||
|
||||
**The port draws it.** Asked directly, at three instants:
|
||||
|
||||
```
|
||||
t=120 drew 9: ptbase2, pteff03, pteff03a, pteff04, ...
|
||||
t=180 drew 10: ptbase2, pteff03, pteff03a, pteff04, ...
|
||||
t=240 drew 10: ptbase2, pteff03, pteff03a, pteff04, ...
|
||||
```
|
||||
|
||||
`pteff03a` is on screen in the port from t≈108 (it crosses x=1280 there) until
|
||||
t≈521, travelling **right-to-left** at 800 % vertical scale while `pteff03` runs
|
||||
left-to-right at 600 %. The capture covers that window and contains only
|
||||
`pteff03`.
|
||||
|
||||
> ~~**So the port appears to render a second light sweep, larger and travelling
|
||||
> the opposite way, that the game does not draw during the title build-in.**~~
|
||||
> ❌ **False.** The game draws both, batched into one eight-vertex draw.
|
||||
|
||||
⚠️ **Absence in one capture is not absence in the game**, and this is one
|
||||
capture, read by one probe, identified by size. It is a lead, not a finding. But
|
||||
it is the first thing in F6 that is *visible*, *port-side*, and *checkable by a
|
||||
person in seconds* — which is what this whole exchange has been missing.
|
||||
|
||||
## What did NOT work, recorded so nobody repeats it
|
||||
|
||||
I tried to isolate the two sweeps visually by differencing title frames at
|
||||
several `--time` values. **It failed and the output is not evidence**: at those
|
||||
instants the whole title is still animating — logo, effects, copyright — so the
|
||||
difference is the entire screen rather than the sweeps. Frame-differencing
|
||||
isolates motion only when everything else is still, and during a build-in nothing
|
||||
is.
|
||||
|
||||
## The unit, and it is one question for a person
|
||||
|
||||
> **On the real game's title screen, is there ONE light streak sweeping across,
|
||||
> or TWO travelling in opposite directions?**
|
||||
|
||||
Pass for the port as it stands: two. If the game shows one, `pteff03a` is drawn
|
||||
here and should not be — and an extra glow arriving at t≈108 is a very good
|
||||
candidate for *"the glow starts too early"*.
|
||||
|
||||
**Not covered:** the start time of `pteff03` itself, which is still open; and the
|
||||
parent-alpha question, still 🟡.
|
||||
|
||||
---
|
||||
|
||||
# ❌ Unit d — the "variant link" explanation, raised and killed in one pass
|
||||
|
||||
The Decoder's second candidate for why the game submits `ptloop01` and not
|
||||
`ptloop02` was *"a focus/variant link means only one of the pair is ever
|
||||
active"*. **That is answerable from the export, and the answer is no.**
|
||||
|
||||
## What looked like a smoking gun
|
||||
|
||||
`ptloop01` carries **`opt_link = "ptloop02.rat"`**, `ptloop02` carries none, and
|
||||
it is the only linked element on the title screen. The field is exported straight
|
||||
from `el.focus_link` (`crates/sylpheed-export/src/screen.rs:622`), and
|
||||
**`port/scripts/` never reads it.** An ignored variant link would have explained
|
||||
the extra sweep exactly.
|
||||
|
||||
## ❌ And it is not a variant link
|
||||
|
||||
Surveying `opt_link` across the whole export splits it into two populations:
|
||||
|
||||
| target | example | is the target also a top-level element? |
|
||||
|---|---|---|
|
||||
| `*f.rat` | `ptbtn00 → ptbtn00f` | **no** — variant only |
|
||||
| everything else | `ptloop01 → ptloop02` | **yes** — both are drawn |
|
||||
|
||||
And the second population **chains across unrelated element kinds**. On
|
||||
`main_menu`:
|
||||
|
||||
```
|
||||
index 3 ptloop01 -> ptloop02.rat
|
||||
index 4 ptloop02 -> ptbtn01.rat
|
||||
index 10 ptbtn01 -> ptbtn01f.rat
|
||||
```
|
||||
|
||||
**A light sweep points at a button.** A variant selector cannot do that, so
|
||||
`opt_link` is a chain pointer that happens to land on the focus variant when the
|
||||
element is a button — which is why it was exported under the name `focus_link`.
|
||||
|
||||
> So the field does not select between `ptloop01` and `ptloop02`, and the port
|
||||
> ignoring it is not what draws the extra sweep. **Candidate eliminated.**
|
||||
|
||||
## The smaller finding that survives
|
||||
|
||||
**`focus_link` is carrying two different things** and the exporter names it after
|
||||
only one of them. The `*f` population is a variant; the rest is a chain. Nothing
|
||||
depends on this today — the port reads neither — but the name asserts a meaning
|
||||
the data does not support, and the next person to reach for it will reach for the
|
||||
wrong one. Worth renaming when something actually needs it; not worth a
|
||||
re-export on its own.
|
||||
|
||||
## Where that leaves F6
|
||||
|
||||
The lead is unchanged and unexplained: **the port draws `pteff03a`, the game's
|
||||
capture never does** — now confirmed by an exhaustive scan of every tall quad in
|
||||
1..2499 rather than a filtered subset. One of the two candidate causes is now
|
||||
eliminated from the export side, which leaves the Decoder's first: a zero-alpha
|
||||
skip suppressing the opening frames. ⚠️ That one does not obviously survive
|
||||
either — it would explain `pteff03a`'s *opening* frames, not its whole run, and
|
||||
its leaf reaches α=128 well inside the captured window.
|
||||
|
||||
**Nothing is deleted and the renderer is unchanged**, pending one five-second
|
||||
human look: one streak, or two?
|
||||
|
||||
---
|
||||
|
||||
# Unit e — the port draws exactly TWO travelling lights, and the human reports more
|
||||
|
||||
The human, watching the real game: *"I think multiple, possible more than two…
|
||||
The lights move on blue lines looking like PCB board lines. And frankly I cannot
|
||||
tell if the game renders a light per line or uses a light that is shown around
|
||||
multiple, close lines."*
|
||||
|
||||
That is a different question from the one both agents had been asking, and it is
|
||||
worth having the port's own number first.
|
||||
|
||||
## Census of every element on the title that travels
|
||||
|
||||
| element | x travel | note |
|
||||
|---|---|---|
|
||||
| `ptlogo1` / `ptlogo2` (×3 instances) | 300 px | the **logo** sliding in, t=34…251 — not a light |
|
||||
| **`pteff03`** (leaf of `ptloop01`) | **2 160 px** | left → right |
|
||||
| **`pteff03a`** (leaf of `ptloop02`) | **2 560 px** | right → left |
|
||||
|
||||
Every other title element — `pteff00`, `pteff01`, `pteff02`, `pteff04`,
|
||||
`ptlogo_back2eff` and `…eff1…5`, `ptlogoall_eff`, `ptlogoall_eff2`,
|
||||
`ptcopyright`, `ptbase2` — **declares no positional travel at all.** They fade in
|
||||
and out in place.
|
||||
|
||||
> **The port renders exactly two moving lights.** The human describes multiple,
|
||||
> possibly more than two, running along individual PCB traces.
|
||||
|
||||
## What that reframes
|
||||
|
||||
Both agents had been asking *when* the sweep starts. If the game's effect is a
|
||||
population of small lights on separate traces and the port's is two full-height
|
||||
streaks crossing the screen, then **the port may have the wrong effect
|
||||
altogether**, and "starts too early" is what a wrong effect looks like to someone
|
||||
who is not reading keyframes.
|
||||
|
||||
⚠️ **And it puts a limit on the capture result.** The Decoder's scan that found
|
||||
`pteff03a` absent covered every quad **taller than 1.2 NDC**. Small per-trace
|
||||
lights are far below that, so that scan cannot count them — it is exhaustive over
|
||||
full-height streaks and silent about the population in question. `pteff03a`'s
|
||||
absence stands (it would be 3.62 NDC); *"only one travelling quad exists"* does
|
||||
not generalise beyond tall quads.
|
||||
|
||||
## ⚠️ A limit of this census
|
||||
|
||||
It reads **declared** keyframes. An element with a single keyframe shows as
|
||||
"visible 0…0" here and is in fact held and drawn — `ptbase2`, the background, is
|
||||
the obvious case. So the visibility column understates; **the travel column is
|
||||
the load-bearing one**, and travel is what a moving light needs.
|
||||
|
||||
It also cannot see motion that is not positional — a scrolling UV, a texture
|
||||
animation, or a shader would move light along a trace while declaring no travel
|
||||
at all. **Nothing in this export declares such a thing**, but the port would not
|
||||
know if the game did it that way, and that is now a live possibility rather than
|
||||
a remote one.
|
||||
|
||||
## Not covered
|
||||
|
||||
Whether the game's lights are one-per-trace or one glow spanning several — the
|
||||
human says they cannot tell, and it is the Decoder's screenshots to settle.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# 📌 What the withdrawal is worth, since the port lost nothing
|
||||
|
||||
Three of my own claims rested on `pteff03a` being absent and all three fall with
|
||||
it: that the port renders a sweep the game does not, that this was "the first
|
||||
thing in F6 that is visible and port-side", and — in a report to the human — that
|
||||
"the port draws two, the game's capture has one." **The port draws two and so
|
||||
does the game.** The census on this page stands unchanged; what changed is that
|
||||
it now agrees with the capture rather than contradicting it.
|
||||
|
||||
**The one thing that made this free was refusing to act on it.** The evidence was
|
||||
an exhaustive scan, from an agent with the oracle, corroborated by a mechanism
|
||||
and by two candidate causes. It was still an *absence*, measured once, by one
|
||||
reader — and the check I asked for was a human's look, not another measurement.
|
||||
|
||||
⚠️ **An absence is a claim about an instrument, not about the world.** A count of
|
||||
zero says only that nothing got through the reader. Every positive result on that
|
||||
same capture — the alpha decomposition, the press-vs-control comparisons, the
|
||||
pulse ratio — is untouched, because those compare like with like on the same
|
||||
quad. Only the absence compared a count against zero, and that is precisely where
|
||||
a truncating reader is fatal.
|
||||
|
||||
📌 The Decoder notes this is the third time this corpus has been bitten by an
|
||||
under-reading dump, and that `REFUTED.md` already recorded a draw carrying two
|
||||
rotated parallelograms — **the general fact was written down before the reader
|
||||
contradicted it.** Their cheap check is worth repeating here because it applies to
|
||||
anything the port ever reads: *read one raw record in full before trusting any
|
||||
count derived from it.* The batch size was printed on every one of those lines.
|
||||
|
||||
## 📌 And the same error recurred, which makes it a pattern rather than a slip
|
||||
|
||||
The withdrawn alpha bound on this page failed because nine values quoted as a
|
||||
series were `sorted(set(...))[:9]` — the lowest distinct values, a tail wearing
|
||||
the shape of a trajectory. The Decoder has since found the same thing in a second
|
||||
finding: an implied-parent range quoted as 254.0–256.9 turned out to be *the rows
|
||||
they had printed*, every twentieth frame, standing in for a population whose real
|
||||
first-cycle spread was 250.9–260.5.
|
||||
|
||||
**Twice, and both times the output looked fine.** That is the tell: a summary
|
||||
drawn from a subset does not look like an error, it looks like a result. The
|
||||
conclusion survived on both occasions, so nothing here needs undoing — but a
|
||||
conclusion surviving is not evidence the number under it was sound, and this port
|
||||
has now inherited two numbers that were not.
|
||||
|
||||
⚠️ **Neither was reachable by reasoning**, which is the part worth keeping. In
|
||||
both cases the argument was valid and the *inputs to the summary* were wrong. No
|
||||
amount of re-reading the claim finds that; only re-running it does. It is the
|
||||
argument for re-running over re-checking, and it is why the two findings flagged
|
||||
as unverified above were re-run rather than defended.
|
||||
|
||||
---
|
||||
|
||||
# ❌ A refutation aimed at this renderer, measured and NOT landed
|
||||
|
||||
The Decoder raised it and could not test it from their side: *"if your renderer
|
||||
runs both leaves on a single rate, the two strips stay locked together and drift
|
||||
from the game by ~118 units per cycle, growing without bound."* The two leaves
|
||||
declare **600** and **720** unit loops.
|
||||
|
||||
**Pre-registered, then measured on a real boot** via `--probe-leaf`. At a raw leaf
|
||||
clock of 4873:
|
||||
|
||||
| leaf | span | measured `leaf_t` | `fposmod(4873, span)` |
|
||||
|---|---|---|---|
|
||||
| `pteff03` | 600 | **72.6** | 73 |
|
||||
| `pteff03a` | 720 | **552.6** | 553 |
|
||||
|
||||
The port takes each leaf's span from **its own keyframes** — `span = max(k.t)`
|
||||
over `fe.keyframes` — so the two were never locked. **17 748 probe samples, title
|
||||
clock reaching 9 745**, i.e. the sweep is still looping 162 seconds in.
|
||||
|
||||
## 🔴 Two false alarms of my own on the way there, both from the same mistake
|
||||
|
||||
1. **I used `--time` to ask a question about running behaviour.** It sets
|
||||
`frozen`, which by design bypasses the `holding` clamp, so the title read as
|
||||
*empty* past t=250 and I nearly reported the whole title vanishing. On a real
|
||||
boot it does not: `settle_window` is `[160, 236, 198]`, the elements clamp to
|
||||
t=198, and a filmed frame at `view_units 6733` shows the complete title.
|
||||
2. **I read a probe stopping as the feature stopping.** Two runs ended at
|
||||
u≈236 and I took that as the sweep dying at settle. It was the run ending —
|
||||
without `--film` the boot exits sooner. With a film attached the same probe
|
||||
reaches 9 745.
|
||||
|
||||
📌 Both are the frozen-sweep lesson wearing new clothes: *the diagnostic that
|
||||
pins the clock cannot answer a question about the clock running*, and *an
|
||||
instrument going quiet is not the subject going quiet*. The second is the same
|
||||
shape as the Decoder's own absence-of-a-quad bug — a count of zero says only that
|
||||
nothing reached the reader.
|
||||
|
||||
---
|
||||
|
||||
# ✅ Out-of-sample: what the port ships was in the passing half
|
||||
|
||||
The Decoder pre-registered six predictions and tested them on a fresh boot that
|
||||
had no hand in deriving them. **Three failed.** Audited here against what this
|
||||
port actually authors, and the answer is **nothing to change**:
|
||||
|
||||
| their prediction | fresh boot | does the port carry it? |
|
||||
|---|---|---|
|
||||
| leaf period ratio 1.200 | 1.1753 ✅ | **yes** — this is `rate = 0.5` |
|
||||
| strip size ratio 1.333 | 1.3009 ✅ | yes, as element identity |
|
||||
| pulse / sweep loop 0.100 | 0.0963 ✅ | yes, `looping_focus_records` 120 |
|
||||
| pulse amplitude ≤3 levels | 8.73 🔴 | no |
|
||||
| `ptcopyright` ramp ratio 0.733 | 0.550 🔴 | no |
|
||||
| sweep leads plate 0.138–0.141 | **0.0996** 🔴 | **no** — grepped, absent |
|
||||
|
||||
`authored/rendering.json` `leaf_clock` is `{start_units: null, rate: 0.5}` and
|
||||
nothing else. No separation constant exists in `authored/`, `tools/port/` or
|
||||
`port/scripts/`.
|
||||
|
||||
📌 **That split is not luck and is worth naming.** Everything the port adopted is
|
||||
either **declared on the disc** (the parent gate, the 120-unit pulse loop, the
|
||||
600/720 leaf periods) or **corroborated by three independent legs** (the rate).
|
||||
Every failed prediction is a figure derived from *relationships between elements
|
||||
measured in a capture* — the class with no declared counterpart, which
|
||||
`check-authored-vs-declared` says out loud it cannot arbitrate. The rule "adopt
|
||||
what the disc declares, or what three unrelated things agree on" selected exactly
|
||||
the surviving half without anyone knowing which half that would be.
|
||||
|
||||
⚠️ And the Decoder reports that `check_labels.py` — offered last iteration as the
|
||||
mechanism for capture-only labels — **fails its first independent test**: two of
|
||||
its four checks fire on a third capture, having been validated on the two that
|
||||
produced the labels. An instrument validated on its own training data. Nothing
|
||||
here depends on it, but it is not a mechanism this port should lean on either.
|
||||
70
docs/port/menu-destinations-are-one-line-away.md
Normal file
70
docs/port/menu-destinations-are-one-line-away.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Four of five main-menu destinations are blocked on ONE hardcoded archive
|
||||
|
||||
**Status:** ✅ feasibility established, nothing changed yet. 2026-09-03.
|
||||
|
||||
## The gap, in player terms
|
||||
|
||||
| button | destination | today |
|
||||
|---|---|---|
|
||||
| NEW GAME | `DLG_SELECT_DIFFICULTY` → SELECT DATA → video | **jumps straight to the video** |
|
||||
| LOAD GAME | `GP_SAVE_LOAD` | **dead** |
|
||||
| TUTORIAL | — | **dead** |
|
||||
| OPTIONS | `GP_OPTIONS` | **dead** |
|
||||
| EXTRAS | `extras` | works |
|
||||
|
||||
All four are recorded in `authored/flow.json` as **measured destinations** —
|
||||
somebody drove the real game to them. They are `blocked` for one structural
|
||||
reason, stated there: *"not a GP_TITLE build, so there is no screen file to go
|
||||
to."*
|
||||
|
||||
## The cause is one line
|
||||
|
||||
`crates/sylpheed-export/src/main.rs` hardcodes `let archive = "dat/GP_TITLE.pak"`.
|
||||
|
||||
## And the reader already works on the rest
|
||||
|
||||
`examples/probe_archives.rs` runs the **existing** `ui_layout::is_build` over
|
||||
every `.pak` on the disc. It decodes nothing new:
|
||||
|
||||
| archive | entries | builds |
|
||||
|---|---|---|
|
||||
| `GP_OPTIONS` | 26 | **14** |
|
||||
| `GP_SAVE_LOAD` | 108 | **18** |
|
||||
| `GP_DIALOG` | 140 | **105** |
|
||||
| `GP_TUTORIAL` | 2 | **2** |
|
||||
| `GP_TITLE` | 16 | 12 |
|
||||
|
||||
**24 archives contain UI screen builds. The exporter reads one.**
|
||||
|
||||
> So this is not blocked on the Decoder and needs no new format work. It is an
|
||||
> exporter scope limit, and the exporter is the port's.
|
||||
|
||||
## Why this is worth doing before the queued items
|
||||
|
||||
Measured against *"if this is wrong, what does a player experience?"* — the
|
||||
filter this port adopted after spending two rounds on a plate pulse that turned
|
||||
out not to be a defect:
|
||||
|
||||
* **four dead menu entries** and a missing difficulty screen: a player hits them
|
||||
immediately and three of them do nothing at all;
|
||||
* the audio mix (F2): a player notices, but the menu still works;
|
||||
* the repeat rate (F1) and the title track (F3): both blocked on measurement.
|
||||
|
||||
## ⚠️ What this does NOT establish
|
||||
|
||||
* **That the screens will render.** `is_build` says the record parses as a build,
|
||||
not that its sprites resolve, its names are known, or its layout is complete.
|
||||
`GP_HANGAR_ARSENAL` reports 390 builds and is squarely gameplay, out of scope.
|
||||
* **Which entry is the difficulty dialog.** `GP_DIALOG` has 105 builds and none
|
||||
of them is named yet; `DLG_SELECT_DIFFICULTY` is a name from the flow, not an
|
||||
entry index.
|
||||
* **That more screens are free.** Every screen the export gains is a screen
|
||||
`check-all`'s comparisons iterate over, and screen names are authored per
|
||||
archive+entry — unnamed screens need a naming decision, not just a loop bound.
|
||||
|
||||
## Next unit
|
||||
|
||||
Widen the exporter to **one** further archive — `GP_OPTIONS`, the smallest at 26
|
||||
entries — as data rather than a second hardcoded constant, and see what actually
|
||||
comes out. Not all four at once: 139 new screens arriving together would make any
|
||||
regression unattributable.
|
||||
118
docs/port/options-screens.md
Normal file
118
docs/port/options-screens.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# The OPTIONS menu tree exists, renders, and is named
|
||||
|
||||
**2026-09-03.** `GP_OPTIONS` joined `export_archives` and produced 14 screen
|
||||
builds. All 14 render; all 14 are now named.
|
||||
|
||||
## What they are
|
||||
|
||||
| entry | name | English | | entry | name |
|
||||
|---|---|---|---|---|---|
|
||||
| 19 | **`options`** | **the root** — GAME / CONTROL / SOUND / SCREEN SETTINGS, BACK | | 21 | `options_jp` |
|
||||
| 16 | `game_settings` | Auto-Save, View Point, Radio Log, Subtitles | | 18 | `game_settings_jp` |
|
||||
| 4 | `control_settings` | Control Type, Throttle, sensitivities, Vibration | | 8 | `control_settings_jp` |
|
||||
| 3 | `sound_settings` | Music / Movie / Voice / SFX Volume | | 5 | `sound_settings_jp` |
|
||||
| 6 | `screen_settings` | Gamma Correction, R/G/B, NEXT PAGE | | 9 | `screen_settings_jp` |
|
||||
| 7 | `screen_settings_page2` | White / Black Level Adjust, PREVIOUS PAGE | | 10 | `screen_settings_page2_jp` |
|
||||
| 20 | `control_customize` | per-action key remapping | | 22 | `control_customize_jp` |
|
||||
|
||||
A clean EN/JP pair for every screen, which is itself a check: 14 builds, 7
|
||||
pairs, no leftovers.
|
||||
|
||||
## How they were identified, and why that is stronger than usual here
|
||||
|
||||
**By the text the screen renders about itself.** Each was exported, drawn by the
|
||||
port at rest, and read: the titles and row labels are legible.
|
||||
|
||||
📌 That matters because this project has been bitten three times by
|
||||
identification via **position, size or ordinal** — the sweep strips confused by
|
||||
size, the plate identified by screen position, `ptcopyright` mistaken for the
|
||||
plate. A screen that renders the words `SOUND SETTINGS` above four volume rows is
|
||||
not that kind of inference.
|
||||
|
||||
⚠️ **What it still does not establish:** which screen the *game* navigates to
|
||||
from which. The tree above is read off content, so `control_customize` being
|
||||
"reached from CONTROL SETTINGS" is a reading of its own legend
|
||||
(`Ⓨ : Customize` on `control_settings`), not a measured transition. Wiring
|
||||
anything beyond `main_menu → options` needs the real navigation.
|
||||
|
||||
## Not yet done
|
||||
|
||||
* **Nothing is reachable yet.** `main_menu` `ptbtn04` still has `goto: null`.
|
||||
* **`po_pad_slider1` has no sprite** in the export and reports NOT DRAWN.
|
||||
* **All 14 are `NEVER COMPARED`** by `verify-screen` — reported, not asserted;
|
||||
both its allowance and the reference renderer were calibrated on `GP_TITLE`.
|
||||
* The screens are static: no navigation, no focus movement, no value editing.
|
||||
|
||||
---
|
||||
|
||||
# ✅ OPTIONS is reachable — and navigation inside it is blocked on a kind
|
||||
|
||||
`main_menu` `ptbtn04` now has `goto: "options"`. Walked with the menu harness:
|
||||
main_menu → ⬇⬇⬇ → Ⓐ → the OPTIONS root renders. Ⓑ backs out.
|
||||
|
||||
## ✅ RESOLVED — the rows move. `0x3003` is `0x3002` with the parent bit set
|
||||
|
||||
The Decoder decoded it disc-wide: **bit 0 of `kind` is the PARENT FLAG**, and it
|
||||
carries no role information. Over every `.pak` in `dat/`, `kind & 1` agrees with
|
||||
"has a parent" on **15 493 elements with zero disagreements**
|
||||
(`docs/re/ui-kind-bit0-is-has-parent.md`). The OPTIONS rows are parented; the
|
||||
main-menu buttons are not. Same record class.
|
||||
|
||||
So the detector now matches `0x3002 | 0x3003` — **two values listed, not a
|
||||
mask**. `kind & 0xFFFE == 0x3002` would also catch `0x73002`/`0x73003`, 160
|
||||
elements whose high bits nobody has decoded, silently and on screens neither
|
||||
agent has seen.
|
||||
|
||||
**Impact measured before re-exporting, not after:** exactly two screens gain
|
||||
buttons — `options` and `options_jp`, five rows each. No existing screen changes.
|
||||
|
||||
Verified by walking it: `main_menu` → ⬇⬇⬇ → Ⓐ → OPTIONS, then ⬇⬇ moves
|
||||
`po_menu_btn2` → `po_menu_btn3` with the focus ring rendering on the highlighted
|
||||
row.
|
||||
|
||||
📌 **The port was right to wait.** The rejected rule — "carries a focus record ⇒
|
||||
menu item" — would have reached the same answer here by a second inference from
|
||||
structure, and would have reclassified elements on screens nobody had looked at.
|
||||
The field cost one question and needed no inference at all.
|
||||
|
||||
## The original section, kept for the shape of the block
|
||||
|
||||
The exporter's button detector is `kind == 0x3002 && !focused`. The OPTIONS rows
|
||||
are **`kind_raw = 0x3003`**, so `role` comes out `unknown`, the export's
|
||||
`buttons[]` is empty, and up/down move nothing.
|
||||
|
||||
| screen | element | kind | focus record | in `buttons[]` |
|
||||
|---|---|---|---|---|
|
||||
| `main_menu` | `ptbtn01` | `0x3002` | yes | yes |
|
||||
| `extras` | `ptbtn11` | `0x3002` | yes | yes |
|
||||
| **`options`** | **`po_menu_btn1`** | **`0x3003`** | **yes** | **no** |
|
||||
|
||||
**What `0x3003` means is not the port's to decide**, so the rule was not widened
|
||||
here. The circumstantial case is strong — five rows, each carrying a focus
|
||||
record, on a screen whose own text lists five options — and *circumstantial* is
|
||||
precisely the standard that has cost this project three separate retractions.
|
||||
Asked of the Decoder.
|
||||
|
||||
⚠️ A tempting alternative rule is "an element with a focus record is a menu item",
|
||||
which fits both screens. It is still an inference about semantics from structure,
|
||||
and it would silently reclassify elements on every screen in the export. Not
|
||||
taken.
|
||||
|
||||
## The workflow cost this exposed, worth knowing before repeating it
|
||||
|
||||
**A screen name is authored data, but it only reaches the port through a full
|
||||
re-export** — which re-transcodes both movies. Renaming one screen costs the
|
||||
whole tree. Not worth fixing today; worth knowing before anyone plans a naming
|
||||
pass.
|
||||
|
||||
## 🔴 And a genuinely dangerous mistake, recorded because it nearly cost the session
|
||||
|
||||
Killing a background check with `pkill -f "check-all"` matched **the container's
|
||||
own entrypoint**, whose command line contains the loop prompt — and that prompt
|
||||
mentions `check-all`. `pgrep` duly reported the process as still running after it
|
||||
had stopped, and a `pkill -9` on that pattern could have killed the session
|
||||
itself.
|
||||
|
||||
**Match a process by its actual `comm`, or list with `ps` and check, before
|
||||
sending a signal.** A pattern that appears in your own instructions is not a
|
||||
pattern that identifies a process.
|
||||
376
tools/port/check-all
Executable file
376
tools/port/check-all
Executable file
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run every check this port has, and say which ones assert.
|
||||
#
|
||||
# tools/port/check-all
|
||||
#
|
||||
# There are fourteen tools under `tools/port/` (eleven when this was written --
|
||||
# the count is stated because it dates the sentence) and nothing ran them
|
||||
# together, so
|
||||
# each had to be remembered individually. That is the ninth instance of this
|
||||
# port's recurring shape -- something correct, documented and unexercised -- one
|
||||
# level up: the checks themselves were the thing nobody was running.
|
||||
#
|
||||
# ⚠️ It runs the tools that ASSERT. The exploratory ones -- `screen-strip`,
|
||||
# `which-focus`, `strip-padding`, `verify-dwell`, `check-capture`,
|
||||
# `verify-video-audio` -- produce artifacts for a person to look at and have no
|
||||
# verdict to collect. Listing them here as passes would be inventing six.
|
||||
set -euo pipefail
|
||||
cd "${PROJECT_DIR:-/work}"
|
||||
export DISPLAY="${DISPLAY:-:97}"
|
||||
OUT="${OUT:-${TMPDIR:-/tmp}/check-all}"; mkdir -p "$OUT"
|
||||
BIN="${CARGO_TARGET_DIR:-/sylph-home/port/target-container}/debug/sylpheed-export"
|
||||
fail=0
|
||||
|
||||
step() { # name, expectation, command...
|
||||
local name="$1" expect="$2"; shift 2
|
||||
local log="$OUT/${name}.log" rc=0
|
||||
"$@" >"$log" 2>&1 || rc=$?
|
||||
case "$expect" in
|
||||
must-pass)
|
||||
[ $rc -eq 0 ] && printf ' %-24s ok\n' "$name" \
|
||||
|| { printf ' %-24s 🔴 FAILED (rc=%d) -- %s\n' "$name" "$rc" "$log"; fail=1; }
|
||||
;;
|
||||
report-only)
|
||||
printf ' %-24s ran (no verdict -- see below)\n' "$name"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 🔴 THE DISPLAY CAN BE GONE, AND EVERY GODOT STEP THEN FAILS FOR ONE REASON.
|
||||
#
|
||||
# Xvfb does not survive a container restart, and its socket does: /tmp/.X11-unix
|
||||
# keeps `X97` after the server is gone, so Godot reports
|
||||
#
|
||||
# ERROR: X11 Display is not available
|
||||
#
|
||||
# rather than "no such display", falls back to Wayland, fails that too, and
|
||||
# exits non-zero. Every Godot-backed step below would then report red, and all of
|
||||
# it would mean one thing -- there is no display -- which is exactly the wall of
|
||||
# meaningless failures a check suite exists to avoid. Cost one run on 2026-09-01
|
||||
# before it was noticed.
|
||||
#
|
||||
# Checked with `xdpyinfo` rather than by looking for the socket, because the
|
||||
# stale socket is what makes the failure confusing in the first place.
|
||||
if ! DISPLAY="$DISPLAY" timeout 10 xdpyinfo >/dev/null 2>&1; then
|
||||
echo "🔴 no X display on $DISPLAY -- every Godot step below would fail for that one reason."
|
||||
echo " Xvfb does not survive a container restart and leaves its socket behind. Start it with:"
|
||||
echo " rm -f /tmp/.X11-unix/X\${DISPLAY#:} /tmp/.X\${DISPLAY#:}-lock"
|
||||
echo " Xvfb $DISPLAY -screen 0 1280x720x24 -nolisten tcp &"
|
||||
echo " 🔴 THE LOCK FILE IS NOT OPTIONAL and this recipe omitted it until"
|
||||
echo " 2026-09-03. Removing only the socket leaves /tmp/.X<n>-lock behind,"
|
||||
echo " Xvfb exits 1 immediately, and the next command still reports no"
|
||||
echo " display -- which reads as the restart having failed for some deeper"
|
||||
echo " reason. Cost three occurrences before anyone read Xvfb's own stderr."
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# 🔴 GODOT'S SCRIPT CLASS LIST IS A BUILD CACHE, AND IT IS GITIGNORED.
|
||||
#
|
||||
# `port/.godot/global_script_class_cache.cfg` is what resolves a `class_name`,
|
||||
# and `.gitignore` excludes `port/.godot/` -- correctly, it is derived. So a
|
||||
# checkout that MERGES a commit adding a new `class_name` keeps a cache that
|
||||
# does not list it, and every script referencing the new class fails to parse:
|
||||
#
|
||||
# SCRIPT ERROR: Parse Error: Identifier "Gamepad" not declared in the current scope.
|
||||
# ERROR: Failed to load script "res://scripts/boot.gd" with error "Parse error".
|
||||
#
|
||||
# The whole project then refuses to load, from `--screen` to `--boot`, and the
|
||||
# error names the symbol rather than the cache -- so it reads as a missing file
|
||||
# or a bad merge. This is exactly what merging the human's input fix did on
|
||||
# 2026-09-01: `gamepad.gd` arrived with `class_name Gamepad`, the cache in this
|
||||
# container was warm and predated it, and the port did not run at all.
|
||||
#
|
||||
# A fresh clone has no `.godot/` and Godot builds one on first run, so nobody
|
||||
# hits this until they merge into a working tree -- which is every iteration of
|
||||
# this loop. Reimporting is cheap and idempotent, so it runs unconditionally
|
||||
# rather than behind a staleness test that would itself need to be right.
|
||||
echo "godot: reimporting so class_name resolves against a fresh cache"
|
||||
DISPLAY="$DISPLAY" godot --headless --path port --import >"$OUT/godot-import.log" 2>&1 \
|
||||
|| { echo " 🔴 godot --import FAILED -- see $OUT/godot-import.log"; fail=1; }
|
||||
for c in $(grep -ho '^class_name [A-Za-z_][A-Za-z0-9_]*' port/scripts/*.gd | awk '{print $2}'); do
|
||||
grep -q "\"$c\"" port/.godot/global_script_class_cache.cfg 2>/dev/null \
|
||||
|| { printf ' %-24s 🔴 class_name %s is not in the class cache\n' class-cache "$c"; fail=1; }
|
||||
done
|
||||
echo
|
||||
|
||||
echo "asserting checks:"
|
||||
step format-validator must-pass "$BIN" check
|
||||
# The contract lives on a branch this checkout does not merge: HANDOFF on `main`
|
||||
# is frozen at 926 lines while the live one is 4 111. Reading 70 unread sections
|
||||
# by hand is how two days of deliveries went unread. These are the values that
|
||||
# have been reduced to a check; the rest are still read by eye, or not at all.
|
||||
step contract-values must-pass tools/port/contract-check
|
||||
step contract-control must-pass tools/port/contract-check --control
|
||||
# 🔴 The control harness itself is asserted. Every --control run says "each check
|
||||
# fails on a perturbed contract"; none of them said "a broken control reports
|
||||
# broken". A harness that silently approves a dead check is exactly as useless as
|
||||
# a check that silently approves a dead value.
|
||||
step control-harness must-pass tools/port/contract-check --selftest
|
||||
step modding-rules must-pass tools/port/check-modding
|
||||
# Every `kind` in authored/ is a claim about where a value came from, and until
|
||||
# 2026-08-30 nothing checked what any of them rested on -- seven were resting on
|
||||
# a sibling `why` that argued a different claim.
|
||||
step authored-kinds must-pass tools/port/audit-kinds
|
||||
# The classifier is asked whether it can tell grounded from ungrounded at all,
|
||||
# rather than only what it found. Exit 2 = the harness is broken.
|
||||
step kinds-harness must-pass tools/port/audit-kinds --selftest
|
||||
# Band levels are alignment-free and carry their own known negative on every run;
|
||||
# the difference-signal half of the same tool stays report-only and asserts
|
||||
# nothing. See docs/port/DECISIONS.md -- the waveform question is still open.
|
||||
step transcode-bands must-pass tools/port/verify-transcode-fidelity
|
||||
# Asks whether the band measurement is LIVE, not just what it found. An empty
|
||||
# band list makes every comparison read 0.0 dB and pass; that now exits 2.
|
||||
step bands-harness must-pass tools/port/verify-transcode-fidelity --selftest
|
||||
step capture-controls must-pass tools/port/check-capture-controls
|
||||
step menu-audio must-pass env OUT="$OUT/audio" tools/port/verify-menu-audio
|
||||
# 🔴 ADDED 2026-09-02, because `menu-audio` above SPENT WEEKS UNABLE TO FAIL. It
|
||||
# computed its verdict, printed a red line when a cue was silent, and its python
|
||||
# had no exit path -- so it returned 0 while registered `must-pass` here. Every
|
||||
# other assertion in this file has a control for exactly this reason and audio
|
||||
# was the one that did not. It costs a second set of runs and that is the price.
|
||||
step menu-audio-ctl must-pass env OUT="$OUT/audioctl" tools/port/verify-menu-audio --control
|
||||
# 🔴 ADDED 2026-09-01 after a human found Ⓐ dead on a real controller while the
|
||||
# unattended P5 walk passed. `--script` sends `InputEventAction`, which BYPASSES
|
||||
# the input map, so every check here asserted the code BELOW the map and nothing
|
||||
# about the map -- which was missing a joypad binding for `ui_accept` and
|
||||
# `ui_cancel` entirely. The same blind spot hid a second defect: an
|
||||
# `InputEventAction` is not an analog axis, so nothing could see that a held
|
||||
# stick fired once per jitter.
|
||||
step input-map must-pass tools/port/verify-input
|
||||
step input-control must-pass tools/port/verify-input --control
|
||||
# 🔴 ADDED 2026-09-02 after a human found the splash frozen while THREE checks
|
||||
# here were green. The frozen sweep proved a pose could be drawn, the settled
|
||||
# comparison scored 0.01 % against the oracle (a frozen screen matches a settled
|
||||
# reference perfectly -- that is what frozen means), and the fps counter counted
|
||||
# frames drawn. All three measured throughput or a pose; none measured CHANGE.
|
||||
# Same shape as InputEventAction bypassing the input map, two rows above.
|
||||
step boot-motion must-pass tools/port/verify-motion
|
||||
step motion-control must-pass tools/port/verify-motion --control
|
||||
# A stale index is worse than none: it answers "is this already decided?" with a
|
||||
# confident no. That is not hypothetical -- see the entry it was built after.
|
||||
step decisions-index must-pass tools/port/index-decisions --check
|
||||
# `audit-kinds` checks citations in `authored/`; nothing checked the PROSE, and
|
||||
# prose is where this port explains itself. A first run found 37 of 91
|
||||
# non-resolving -- 7 of them pointing at NOTHING on any ref, left behind by the
|
||||
# monorepo move and the `export/` rename. Only that class fails; a citation that
|
||||
# is merely on a peer's unmerged branch is reported, because the fix is a merge
|
||||
# and nobody in this container can make it.
|
||||
step trajectory-fit must-pass tools/port/fit-trajectory --selftest
|
||||
step linked-records must-pass tools/port/check-linked-records
|
||||
step linked-rec-ctl must-pass tools/port/check-linked-records --selftest
|
||||
step authored-declared must-pass tools/port/check-authored-vs-declared
|
||||
step authored-decl-ctl must-pass tools/port/check-authored-vs-declared --selftest
|
||||
step doc-citations must-pass tools/port/check-citations
|
||||
step citations-control must-pass tools/port/check-citations --selftest
|
||||
# A refuted claim asserted outside its correction is a lie the corpus tells a
|
||||
# reader who greps for it. Registered claims must carry an explicit `[refuted]`.
|
||||
# 🔴 The register check had NO executable control until 2026-08-31 -- every
|
||||
# "planted a revival and it failed" in DECISIONS was done by hand, once. Four
|
||||
# cases now drive it as a subprocess and read its real exit code, including an
|
||||
# EMPTY REGISTER, which used to report clean forever.
|
||||
step claims-control must-pass tools/port/check-claims --control
|
||||
step refuted-claims must-pass tools/port/check-claims
|
||||
echo
|
||||
echo "reported, not asserted:"
|
||||
# Not an assertion: being behind a peer's topic branch is the normal state, and a
|
||||
# red line for it would be scenery within a day. It is here so the affordance is
|
||||
# visible on every run -- reading a peer's head needs no merge and no human.
|
||||
step peer-heads report-only tools/port/peer-head
|
||||
step oracle-captures report-only env OUT="$OUT/oracle" tools/port/verify-capture
|
||||
sed -n '/^screen /,$p' "$OUT/oracle-captures.log" | sed 's/^/ /'
|
||||
# 🔴 `verify-capture` prints and always exits 0. Its own header is right that the
|
||||
# numbers are not a target -- the captures carry the game's tone ramp, so RMSE has
|
||||
# a floor and driving it lower is fitting the ramp. But "not a target" is not the
|
||||
# same as "not a regression detector", and nothing here would notice `title_plate`
|
||||
# moving off 0.00 %. Asserting it needs a stored baseline per row, which is a real
|
||||
# design decision about what a baseline means when the pose is fitted. NAMED, not
|
||||
# quietly skipped.
|
||||
|
||||
echo
|
||||
echo "consistency (expected to differ, for a stated reason):"
|
||||
rc=0; env OUT="$OUT/screens" tools/port/verify-screen >"$OUT/verify-screen.log" 2>&1 || rc=$?
|
||||
differs=$(grep -c DIFFERS "$OUT/verify-screen.log" || true)
|
||||
# 🔴 THE ALLOWANCE IS DERIVED NOW, NOT LISTED, and that is strictly stronger.
|
||||
#
|
||||
# Six screens joined this set on 2026-09-01 and the cause is diagnosed for two of
|
||||
# them: the port draws some elements ADDITIVE -- transcribed from the Decoder's
|
||||
# per-draw RB_BLENDCONTROL0 log off the running game -- and the reference has no
|
||||
# additive path at all (ui_layout.rs has exactly two blend sites, both
|
||||
# alpha-over, and line 1169 records that it tried additive and refuted it from
|
||||
# its own composite metrics). So the two renderers disagree ON PURPOSE, and the
|
||||
# size of the disagreement tracks the size of the additive set: extras has 9
|
||||
# elements and a mean of 6.74, main_menu has 5 and 3.94, and the screens with
|
||||
# none sit an order of magnitude below.
|
||||
#
|
||||
# Computing the allowance from `authored/rendering.json` rather than listing it
|
||||
# means a screen is excused BECAUSE it has additive elements the reference
|
||||
# cannot draw, and a screen that differs WITHOUT them still fails -- which a
|
||||
# literal list could not express, and which keeps this from going stale against
|
||||
# the map it is derived from. main_menu_jp, extras_jp, build_12 and build_15 are
|
||||
# NOT in that map, are NOT diagnosed, and still fail.
|
||||
# docs/port/verify-screen-blend-divergence.md
|
||||
# 🔴 THIS DERIVED FROM authored/rendering.json AND I DELETED THAT KEY MYSELF.
|
||||
# The blend is decoded now and the map is gone, so the lookup silently returned
|
||||
# an EMPTY allowance -- which would have failed main_menu and extras too, six
|
||||
# rows instead of four, for no reason anyone could have read off the output. A
|
||||
# derived allowance is only as durable as the thing it derives from, and I
|
||||
# pointed this one at a file I then emptied one iteration later.
|
||||
#
|
||||
# It now derives from the EXPORT, which is what the port actually draws from: a
|
||||
# screen may differ if any of its elements -- or any nested focus/leaf element --
|
||||
# carries `blend_additive: true`, because `ui_layout.rs` has no additive path at
|
||||
# all and cannot reproduce those draws by construction.
|
||||
#
|
||||
# ⚠️ THIS ALLOWANCE IS LOOSER THAN THE ONE IT REPLACES AND THAT IS A REAL COST.
|
||||
# The old map covered 3 screens because it was a transcription of what somebody
|
||||
# had driven the game to; the bit is disc-wide, so 12 of 16 screens now qualify
|
||||
# and verify-screen goes fully green. Measured after the swap, the two sets line
|
||||
# up exactly -- all 10 screens that DIFFER have a drawn additive element, and all
|
||||
# 6 that agree have none -- so nothing is being excused that does not have the
|
||||
# cause. But a screen that starts differing for some OTHER reason will now be
|
||||
# excused if it happens to carry an additive element anywhere, and this check
|
||||
# will not say so.
|
||||
#
|
||||
# ✅ THE REAL FIX HAS LANDED -- AT A TAG, NOT YET ON `main`, WHICH IS WHY THIS
|
||||
# CLAUSE IS STILL HERE. `ui_layout::blit` draws additive as of
|
||||
# formats-pin-2026-09-01b, so the comparison is capable again and this widening
|
||||
# has lost its justification.
|
||||
#
|
||||
# Measured at that tag, in a detached worktree, with SYLPHEED_CLI pointed at it:
|
||||
# main_menu 7.26 -> 1.21, extras 6.98 -> 1.02, both JP twins likewise, and
|
||||
# build_00/build_01 go DIFFERS -> OK (over3 3422 -> 0). A 6x collapse.
|
||||
#
|
||||
# 🔴 NOT NARROWED YET, AND ON PURPOSE. This script builds the reference from the
|
||||
# WORKSPACE crate, and the additive path is not on `main`. Narrowing now would
|
||||
# turn check-all red against a reference that still cannot draw additive -- a
|
||||
# wall of failures meaning one thing, which is the defect the display guard above
|
||||
# exists to prevent.
|
||||
#
|
||||
# TRIGGER, so this does not rot: when `grep -q additive crates/sylpheed-formats/src/ui_layout.rs`
|
||||
# succeeds, delete the export-derived clause and keep only `-e title -e title_jp`.
|
||||
# The set that should then differ is measured in
|
||||
# docs/port/verify-screen-blend-divergence.md: title, title_jp, main_menu, extras,
|
||||
# main_menu_jp, extras_jp, build_12, build_15 -- and build_00/build_01 pass.
|
||||
additive_screens=$(python3 -c "
|
||||
import json, glob, os
|
||||
out = []
|
||||
for p in sorted(glob.glob('export/screens/*/*.json')):
|
||||
d = json.load(open(p))
|
||||
def any_add(els):
|
||||
for e in els:
|
||||
if e.get('blend_additive'):
|
||||
return True
|
||||
for k in ('focus', 'leaf'):
|
||||
if any_add((e.get(k) or {}).get('elements', [])):
|
||||
return True
|
||||
return False
|
||||
if any_add(d.get('elements', [])):
|
||||
out.append(os.path.basename(p)[:-5])
|
||||
print('\n'.join(out))" 2>/dev/null)
|
||||
allow_args=(-e title -e title_jp)
|
||||
for sc in $additive_screens; do allow_args+=(-e "$sc"); done
|
||||
printf ' %-24s allowing %s (additive set + 2 legacy)\n' verify-screen \
|
||||
"$(echo $additive_screens | tr '\n' ' ')"
|
||||
unexpected=$(grep DIFFERS "$OUT/verify-screen.log" | awk '{print $1}' \
|
||||
| grep -vx "${allow_args[@]}" || true)
|
||||
|
||||
# 🔴 SCREENS FROM A NEWLY EXPORTED ARCHIVE HAVE NEVER BEEN COMPARED, AND THAT IS
|
||||
# NOT THE SAME AS DISAGREEING.
|
||||
#
|
||||
# `verify-screen` is renderer-vs-renderer, and BOTH its allowance and the
|
||||
# reference renderer itself were built against GP_TITLE. When the exporter gained
|
||||
# `GP_OPTIONS` (2026-09-03) its 14 screens all read DIFFERS at means of 10-60
|
||||
# against 0.02-7.3 for the calibrated set -- which says nothing yet, because
|
||||
# nobody has looked at a single one of them.
|
||||
#
|
||||
# They are REPORTED, not failed and NOT added to the allowed set. Failing would
|
||||
# put the suite red for a state nobody has investigated -- the wall of
|
||||
# meaningless failures the display guard exists to prevent. Allowing would assert
|
||||
# they are explained, and `verify-screen`'s own header is emphatic that the
|
||||
# allowed set means "measured, cause open", not "ignore this".
|
||||
#
|
||||
# The discriminator is the sprite group in the manifest path, so a screen becomes
|
||||
# assertable the moment somebody moves it into the calibrated population
|
||||
# deliberately, rather than by an export widening underneath the check.
|
||||
uncompared=$(python3 -c "
|
||||
import json
|
||||
m = json.load(open('export/manifest.json'))
|
||||
print('\n'.join(s['name'] for s in m['screens']
|
||||
if not s['file'].startswith('screens/title/')))" 2>/dev/null)
|
||||
if [ -n "$uncompared" ]; then
|
||||
still=$(echo "$unexpected" | grep -vxF -f <(echo "$uncompared") || true)
|
||||
newly=$(echo "$unexpected" | grep -xF -f <(echo "$uncompared") || true)
|
||||
unexpected="$still"
|
||||
[ -n "$newly" ] && printf ' %-24s %d screen(s) NEVER COMPARED (new archive, uncalibrated): %s\n' \
|
||||
verify-screen "$(echo $newly | wc -w)" "$(echo $newly | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
# 🔴 THE OLD ALLOWANCE WAS FALSE, AND MY FIRST REPLACEMENT REASON WAS ALSO
|
||||
# WRONG. Both are recorded because the second error is the more instructive.
|
||||
#
|
||||
# It said: "the pin is not on main, so this compares two decoder eras". I
|
||||
# replaced that with "the eras render identically -- 0 pixels different on three
|
||||
# screens". 🔴 **That measurement was void**: the two binaries I compared had the
|
||||
# same md5. I built one in a worktree at the pinned tag and one from the
|
||||
# workspace, and both commits carry the record-layout fix, so I compared a
|
||||
# binary with itself and reported the zero as evidence.
|
||||
#
|
||||
# Rebuilt properly against `origin/main`, which is the genuinely stale era
|
||||
# (`rest t=70 [12 70 80 -]` against the fixed `rest t=12 [0 12 70 80]`):
|
||||
#
|
||||
# title 0 px main_menu 0 px title_jp 74 507 px
|
||||
#
|
||||
# ✅ The eras DO change pixels, and `title_jp` is one of the seven bundles where
|
||||
# they do -- reproducing the Decoder's figure exactly, under their flags and
|
||||
# mine. My "--animated masks it" hypothesis was wrong too.
|
||||
#
|
||||
# ✅ BUT THE ERA STILL CANNOT EXPLAIN THIS SCRIPT'S ROWS, for a reason I had not
|
||||
# established: BOTH SIDES OF THIS COMPARISON ARE THE FIXED ERA. The exporter is
|
||||
# pinned to `formats-pin-2026-08-30` and this reference is built from the
|
||||
# workspace, and a binary built from each has the SAME md5. There is no era
|
||||
# mismatch here to explain anything. Right answer, wrong evidence, and the wrong
|
||||
# evidence was a broken experiment.
|
||||
#
|
||||
# The real reasons are per-screen and already documented:
|
||||
# title -- the ptloop SWEEP PHASE residual, max 6 / over3 790, unchanged
|
||||
# across every renderer change since P1 (DECISIONS.md).
|
||||
# title_jp -- the `--pose=rest` sparkle handling. Adjudicated against the
|
||||
# oracle: the port's SHIPPED pose scores r +0.9994 against the
|
||||
# game where the reference scores +0.8727, and `--pose=rest` is
|
||||
# what this script compares.
|
||||
# ⚠️ title_jp is ALSO an era-sensitive bundle, so if this reference is ever
|
||||
# built from a different era than the exporter's pin, that row's cause changes
|
||||
# and this note stops applying. Check the md5s before trusting it again.
|
||||
#
|
||||
# So the allowance is now a NAMED SET, not a count with an excuse. A DIFFERS on
|
||||
# any other screen fails the run, which a count never could.
|
||||
# 🔴 SIX MORE SCREENS JOINED THIS SET ON 2026-09-01 AND THE SET WAS NOT WIDENED.
|
||||
# main_menu, extras, main_menu_jp, extras_jp, build_12, build_15. Measured, not
|
||||
# diagnosed: the difference is full-frame, it is EXACTLY ZERO on unblended
|
||||
# pixels (18 081 of them agree to a hundredth of a level) and gamma-shaped on
|
||||
# every blended one, so it is a blend-SPACE divergence rather than moved content.
|
||||
# Scored against the live capture the port is 16 % closer than the reference --
|
||||
# an ordering only, since both sides share this script's --pose=rest
|
||||
# contamination. Left failing on purpose: this allowance has twice been widened
|
||||
# with a reason that turned out false, and "I measured it but cannot say which
|
||||
# renderer is right" is not a reason. docs/port/verify-screen-blend-divergence.md
|
||||
if [ -n "$unexpected" ]; then
|
||||
printf ' %-24s 🔴 DIFFERS on %s -- not in the allowed set\n' verify-screen "$(echo $unexpected | tr '\n' ' ')"
|
||||
printf ' %-24s see docs/port/verify-screen-blend-divergence.md -- measured, cause open\n' ""
|
||||
fail=1
|
||||
else
|
||||
printf ' %-24s %d DIFFERS, both named and explained per screen:\n' verify-screen "$differs"
|
||||
printf ' %-24s title = sweep phase; title_jp = rest-pose sparkles (the port is\n' ""
|
||||
printf ' %-24s closer to the GAME there than the reference is).\n' ""
|
||||
fi
|
||||
|
||||
# Separately, and unrelated to the rows above: revert to the path dependency when
|
||||
# the pin lands. Read from Cargo.toml so it cannot drift out of step again.
|
||||
pin=$(sed -n 's/.*tag = "\([^"]*\)".*/\1/p' crates/sylpheed-export/Cargo.toml | head -1)
|
||||
if [ -n "$pin" ] && git merge-base --is-ancestor "$pin" origin/main 2>/dev/null; then
|
||||
printf ' %-24s ⚠️ %s has landed on main -- revert Cargo.toml to the path dep\n' pin "$pin"
|
||||
fi
|
||||
|
||||
echo
|
||||
[ $fail -eq 0 ] && echo "every asserting check passes" || echo "🔴 a check failed"
|
||||
exit $fail
|
||||
Reference in New Issue
Block a user