port: the capture has NEW GAME focused, and my harness had been rendering no focus
The Decoder attached an honest caveat to its reproduction of the tone-curve
refutation -- its bins included the focus-state mismatch, so it was "not a clean
second opinion". Removing that caveat found a defect of mine.
IDENTIFYING THE FOCUSED BUTTON, with a known-answer control. Walking focus
through all five buttons and comparing each against a capture:
vs -options-focused vs live-main-menu
ptbtn01 NEW GAME 6351 531
ptbtn02 LOAD GAME 7087 7094
ptbtn03 TUTORIAL 6230 6237
ptbtn04 OPTIONS 1292 6364
ptbtn05 EXTRAS 6073 6080
The control picks OPTIONS by 4.7x -- the answer its own filename gives -- so the
test is trustworthy, and live-main-menu.png has NEW GAME focused by 11.5x. That
is what the port already focuses. Corroboration, NOT a decode: Q5's instability
stands (four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME) and this
identifies one frame rather than a rule. Recorded in the authored entry.
THE DEFECT: `verify-capture` rendered menus with `--screen=`, which draws no
focus record, so it had been comparing main_menu to the oracle in a state the
oracle was never in.
main_menu 0.25% differing, RMSE 14.79 -> 0.06%, RMSE 13.21
extras 0.46%, RMSE 15.29 -> 0.20%, RMSE 13.38
A 4x improvement that was entirely my harness posing the port wrong rather than
the port drawing wrong. The first run of a new correctness harness reported a
discrepancy and three quarters of it was the harness.
AND THE DECODER'S CAVEAT RESOLVES TO NOTHING, measured rather than assumed:
re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 /
1.18 / 1.10 / 1.03 / 0.94 at render 8..48, unchanged from the mismatched run. Its
reproduction stands as a second opinion after all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
@@ -1,193 +1,193 @@
|
||||
{
|
||||
"format": "sylpheed.flow/1",
|
||||
"_": [
|
||||
"The boot sequence. AUTHORED, and it has to be: HANDOFF Q6 closed this with a",
|
||||
"negative -- the order is in none of the four places it could have been. It is",
|
||||
"not in config.ini's empty [SYSTEM], not in the movie manifest (which carries",
|
||||
"assets, not transitions), not in a persistent GamePart field (the requested id",
|
||||
"lives only as a stack argument in flight), and `GP_ADVERTISE_DEMO` has zero",
|
||||
"xrefs of any kind. A transition is a call with a name argument, chosen by code.",
|
||||
"",
|
||||
"So this file REPRODUCES AN OBSERVATION. The sequence below is what the RE",
|
||||
"agent watched the game do, not what any file on the disc says it does. Nothing",
|
||||
"here may be presented as decoded."
|
||||
],
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"screen": "developer_logos",
|
||||
"why": "GAME ARTS / SETA / studio anima, after the publisher wordmark. HANDOFF Q2."
|
||||
},
|
||||
{
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"screen": "title",
|
||||
"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=238. `clock: \"shared\"` is that, spelled out rather than implied by the absence of a delay field.",
|
||||
"arithmetic_why": "Why one clock reproduces the measurement, checked against this export rather than taken on trust: build 4's visible build-in ends at t=118, where `pteff01`, `pteff02` and `ptlogoall_eff` finish together; `ptbtn00` reaches alpha 255 at t=238; the difference is 120 units = 2.000 s at 60 units/s. The oracle measured 2.138 s and 2.132 s. The gap is presentation rate: 120 units in 2.135 s is 56.2 units/s, i.e. the emulator presenting at 28.1 fps against a nominal 30, and the corpus had independently measured the idle title at 28.5 fps before these runs.",
|
||||
"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. `ptlogo1` has rest.t=251 and stops MOVING at t=42, then creeps 5 px and 31 alpha steps over the next 209 units. Reading rest.t 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.",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"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.",
|
||||
"",
|
||||
"When a capture times the real boot, the extra hold per screen goes here."
|
||||
]
|
||||
"format": "sylpheed.flow/1",
|
||||
"_": [
|
||||
"The boot sequence. AUTHORED, and it has to be: HANDOFF Q6 closed this with a",
|
||||
"negative -- the order is in none of the four places it could have been. It is",
|
||||
"not in config.ini's empty [SYSTEM], not in the movie manifest (which carries",
|
||||
"assets, not transitions), not in a persistent GamePart field (the requested id",
|
||||
"lives only as a stack argument in flight), and `GP_ADVERTISE_DEMO` has zero",
|
||||
"xrefs of any kind. A transition is a call with a name argument, chosen by code.",
|
||||
"",
|
||||
"So this file REPRODUCES AN OBSERVATION. The sequence below is what the RE",
|
||||
"agent watched the game do, not what any file on the disc says it does. Nothing",
|
||||
"here may be presented as decoded."
|
||||
],
|
||||
"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."
|
||||
},
|
||||
"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).",
|
||||
"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.",
|
||||
"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."
|
||||
{
|
||||
"screen": "developer_logos",
|
||||
"why": "GAME ARTS / SETA / studio anima, after the publisher wordmark. HANDOFF Q2."
|
||||
},
|
||||
"screens": {
|
||||
"_": [
|
||||
"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",
|
||||
"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, HANDOFF Q5: (B) on the title does nothing."
|
||||
},
|
||||
"main_menu": {
|
||||
"initial_focus": "ptbtn01",
|
||||
"initial_focus_kind": "authored",
|
||||
"initial_focus_why": "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.",
|
||||
"on_cancel": {
|
||||
"goto": "title",
|
||||
"goto_name": "TITLE_SCREEN",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"kind": "authored -- likely but UNPROVEN",
|
||||
"why": "HANDOFF Q5 states (B) on the main menu returns to the title. That is the ONE navigation rule on this screen with no capture behind it: it was seen once, without a capture, and the title also returns on its own after ~8-10 s idle, so an observer could not have told the two apart (docs/re/menu-navigation-semantics.md). Independently, the main menu is the ONLY screen whose footer does not advertise (B) -- confirmed here from the disc art, see docs/port/DECISIONS.md. The port implements it because a menu with no way out is worse than a menu with a plausible one, and records that it is authored."
|
||||
},
|
||||
"buttons": {
|
||||
"ptbtn01": {
|
||||
"label": "NEW GAME",
|
||||
"goto": null,
|
||||
"goto_name": "DIFFICULTY",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
"blocked": "The lesson list is not a GP_TITLE build. Destination MEASURED."
|
||||
},
|
||||
"ptbtn04": {
|
||||
"label": "OPTIONS",
|
||||
"goto": null,
|
||||
"goto_name": null,
|
||||
"blocked": "The settings menu is GP_OPTIONS, not in this export. Destination MEASURED."
|
||||
},
|
||||
"ptbtn05": {
|
||||
"label": "EXTRAS",
|
||||
"goto": "extras",
|
||||
"goto_name": "EXTRA_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"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",
|
||||
"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.",
|
||||
"on_cancel": {
|
||||
"goto": "main_menu",
|
||||
"goto_name": "TITLE_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"screen": "title",
|
||||
"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=238. `clock: \"shared\"` is that, spelled out rather than implied by the absence of a delay field.",
|
||||
"arithmetic_why": "Why one clock reproduces the measurement, checked against this export rather than taken on trust: build 4's visible build-in ends at t=118, where `pteff01`, `pteff02` and `ptlogoall_eff` finish together; `ptbtn00` reaches alpha 255 at t=238; the difference is 120 units = 2.000 s at 60 units/s. The oracle measured 2.138 s and 2.132 s. The gap is presentation rate: 120 units in 2.135 s is 56.2 units/s, i.e. the emulator presenting at 28.1 fps against a nominal 30, and the corpus had independently measured the idle title at 28.5 fps before these runs.",
|
||||
"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. `ptlogo1` has rest.t=251 and stops MOVING at t=42, then creeps 5 px and 31 alpha steps over the next 209 units. Reading rest.t 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.",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"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.",
|
||||
"",
|
||||
"When a capture times the real boot, the extra hold per screen goes here."
|
||||
]
|
||||
},
|
||||
"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).",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"screens": {
|
||||
"_": [
|
||||
"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",
|
||||
"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, HANDOFF Q5: (B) on the title does nothing."
|
||||
},
|
||||
"main_menu": {
|
||||
"initial_focus": "ptbtn01",
|
||||
"initial_focus_kind": "authored",
|
||||
"initial_focus_why": "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.",
|
||||
"on_cancel": {
|
||||
"goto": "title",
|
||||
"goto_name": "TITLE_SCREEN",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"kind": "authored -- likely but UNPROVEN",
|
||||
"why": "HANDOFF Q5 states (B) on the main menu returns to the title. That is the ONE navigation rule on this screen with no capture behind it: it was seen once, without a capture, and the title also returns on its own after ~8-10 s idle, so an observer could not have told the two apart (docs/re/menu-navigation-semantics.md). Independently, the main menu is the ONLY screen whose footer does not advertise (B) -- confirmed here from the disc art, see docs/port/DECISIONS.md. The port implements it because a menu with no way out is worse than a menu with a plausible one, and records that it is authored."
|
||||
},
|
||||
"buttons": {
|
||||
"ptbtn01": {
|
||||
"label": "NEW GAME",
|
||||
"goto": null,
|
||||
"goto_name": "DIFFICULTY",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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",
|
||||
"blocked": "The lesson list is not a GP_TITLE build. Destination MEASURED."
|
||||
},
|
||||
"ptbtn04": {
|
||||
"label": "OPTIONS",
|
||||
"goto": null,
|
||||
"goto_name": null,
|
||||
"blocked": "The settings menu is GP_OPTIONS, not in this export. Destination MEASURED."
|
||||
},
|
||||
"ptbtn05": {
|
||||
"label": "EXTRAS",
|
||||
"goto": "extras",
|
||||
"goto_name": "EXTRA_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"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",
|
||||
"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.",
|
||||
"on_cancel": {
|
||||
"goto": "main_menu",
|
||||
"goto_name": "TITLE_MENU",
|
||||
"goto_name_kind": "name match, not measured",
|
||||
"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",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3748,3 +3748,59 @@ extrapolating past a reach the measurement's own authors had written down.
|
||||
**Extrapolating a measurement past its stated reach is how this tool got it wrong
|
||||
twice**, and the answer was not a better fit but a different instrument: a table
|
||||
somebody can argue with.
|
||||
|
||||
## Identifying the capture's focused button — and my harness was posing the port wrong
|
||||
|
||||
The Decoder attached an honest caveat to its reproduction of the tone-curve
|
||||
refutation: its bins included the focus-state mismatch, so it was *"not a clean
|
||||
second opinion"*. That is removable, and removing it found a defect of mine.
|
||||
|
||||
### The method, with a known-answer control
|
||||
|
||||
`--menu=main_menu --script=down,down,down,down` walks focus through all five
|
||||
buttons and shoots each. Compare every one against a capture; the minimum
|
||||
identifies the focused button. **The control is the capture whose answer is in
|
||||
its own filename:**
|
||||
|
||||
| render focus | vs `live-main-menu-options-focused` | vs `live-main-menu` |
|
||||
|---|---|---|
|
||||
| `ptbtn01` NEW GAME | 6 351 | **531** |
|
||||
| `ptbtn02` LOAD GAME | 7 087 | 7 094 |
|
||||
| `ptbtn03` TUTORIAL | 6 230 | 6 237 |
|
||||
| `ptbtn04` OPTIONS | **1 292** | 6 364 |
|
||||
| `ptbtn05` EXTRAS | 6 073 | 6 080 |
|
||||
|
||||
✅ **The control picks `OPTIONS`, by 4.7×** — the answer the filename gives.
|
||||
✅ **So the test is trustworthy, and `live-main-menu.png` has NEW GAME focused, by
|
||||
11.5×.**
|
||||
|
||||
**Which is what the port already focuses.** `authored/flow.json`'s
|
||||
`initial_focus: ptbtn01` was chosen because HANDOFF Q5 measured focus as
|
||||
*unstable* across boots — four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW GAME
|
||||
— and it is one of the two observed states. It is now also the state of the
|
||||
committed capture. ⚠️ That is corroboration, **not** a decode: Q5's instability
|
||||
stands, and this identifies one frame rather than a rule.
|
||||
|
||||
### The defect: `verify-capture` rendered menus with no focus at all
|
||||
|
||||
`--screen=` draws no focus record, so the harness had been comparing `main_menu`
|
||||
to the oracle **in a state the oracle was never in**. Rendered properly, with
|
||||
`--menu=`:
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| `main_menu` | 0.25 % differing, RMSE 14.79 | **0.06 %**, RMSE 13.21 |
|
||||
| `extras` | 0.46 %, RMSE 15.29 | **0.20 %**, RMSE 13.38 |
|
||||
|
||||
A 4× improvement on `main_menu` that was **entirely my harness posing the port
|
||||
wrong**, not the port drawing wrong. Worth stating plainly: the first run of a
|
||||
new correctness harness reported a discrepancy, and three quarters of it was the
|
||||
harness.
|
||||
|
||||
### And the Decoder's caveat resolves to nothing — measured, not assumed
|
||||
|
||||
Re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 /
|
||||
1.18 / 1.10 / 1.03 / 0.94 at render 8…48 — **unchanged** from the mismatched run.
|
||||
So the focus-state contamination it flagged really did not move the trend. Its
|
||||
reproduction stands as a second opinion after all, and that is now a measurement
|
||||
rather than a hope.
|
||||
|
||||
@@ -40,12 +40,21 @@ OUT="${OUT:-$(mktemp -d)}"; mkdir -p "$OUT"
|
||||
CAPS=docs/re/captures/title-builds
|
||||
|
||||
# screen : capture : how to pose it
|
||||
# 🔴 A MENU CAPTURE HAS A BUTTON FOCUSED, AND THE FIRST VERSION OF THIS TOOL
|
||||
# RENDERED WITH NONE. `--screen=` draws no focus record at all, so `main_menu`
|
||||
# was being compared to the oracle in a state the oracle was never in: 2 159
|
||||
# differing pixels, of which 74 % sat inside the focus signature. Rendered with
|
||||
# focus it is 531 -- 0.06 % of the frame, a 4x improvement that was entirely my
|
||||
# harness posing the port wrong.
|
||||
#
|
||||
# `--menu=` applies `authored/flow.json`'s initial focus and `--script=wait`
|
||||
# shoots one settled frame and exits.
|
||||
MAP=(
|
||||
"main_menu:$CAPS/live-main-menu.png:--time=99"
|
||||
"extras:$CAPS/live-extras.png:--time=99"
|
||||
"title:$CAPS/live-title-build4-no-plate.png:--time=99"
|
||||
"publisher_logo:$CAPS/live-splash-publisher.png:--time=99"
|
||||
"developer_logos:$CAPS/live-splash-developer.png:--time=99"
|
||||
"main_menu:$CAPS/live-main-menu.png:menu"
|
||||
"extras:$CAPS/live-extras.png:menu"
|
||||
"title:$CAPS/live-title-build4-no-plate.png:screen"
|
||||
"publisher_logo:$CAPS/live-splash-publisher.png:screen"
|
||||
"developer_logos:$CAPS/live-splash-developer.png:screen"
|
||||
)
|
||||
CURVE=""
|
||||
if [ "${1:-}" = "--curve" ]; then CURVE=1; shift; fi
|
||||
@@ -59,8 +68,14 @@ for row in "${MAP[@]}"; do
|
||||
IFS=: read -r name cap pose <<<"$row"
|
||||
if [ ${#want[@]} -gt 0 ] && ! printf '%s\n' "${want[@]}" | grep -qx "$name"; then continue; fi
|
||||
[ -f "$cap" ] || { printf '%-17s %s\n' "$name" "no capture"; continue; }
|
||||
godot --path port --resolution 1280x720 -- "--screen=$name" "$pose" \
|
||||
"--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true
|
||||
if [ "$pose" = menu ]; then
|
||||
godot --path port --resolution 1280x720 -- "--menu=$name" --script=wait \
|
||||
"--shots=$OUT/$name" >"$OUT/$name.log" 2>&1 || true
|
||||
[ -f "$OUT/${name}_00_start.png" ] && cp "$OUT/${name}_00_start.png" "$OUT/$name.render.png"
|
||||
else
|
||||
godot --path port --resolution 1280x720 -- "--screen=$name" --time=99 \
|
||||
"--capture=$OUT/$name.render.png" >"$OUT/$name.log" 2>&1 || true
|
||||
fi
|
||||
[ -f "$OUT/$name.render.png" ] || { printf '%-17s %s\n' "$name" "render failed"; continue; }
|
||||
# Crop the render to the capture's frame. The capture is the crop, not a scale.
|
||||
convert "$OUT/$name.render.png" -crop 1279x675+0+0 +repage "$OUT/$name.crop.png"
|
||||
@@ -68,7 +83,7 @@ for row in "${MAP[@]}"; do
|
||||
raw=$(python3 -c "print('%.2f' % (float('$raw')*255))" 2>/dev/null || echo "?")
|
||||
note=""
|
||||
case "$name" in
|
||||
main_menu|extras) note="focus state may differ from the capture" ;;
|
||||
main_menu|extras) note="rendered with authored initial focus" ;;
|
||||
title) note="ptloop sweeps never stop -- one moment vs another" ;;
|
||||
esac
|
||||
# Where the difference lives. This comes FIRST because it is what the gamma
|
||||
|
||||
Reference in New Issue
Block a user