port: P5 -- the menus navigate, and the focus ring is drawn wrong on purpose
P5's gate is "a human clicks through it". The artifact is a scripted walk that
proves the wiring rather than the intent -- up (wraps 01->05), five down, (A)
into EXTRAS, down, (B) back, landing on the main menu with focus RESTORED to
EXTRAS, ten PNGs one per settled step:
xvfb-run -a godot --path port -- --menu \
--script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5
--script posts InputEventAction through Input.parse_input_event so the presses
arrive at _unhandled_input exactly as a d-pad's would. Calling MenuFlow directly
would have been shorter and would have proved nothing: the wiring between a
press and the cursor is the part most likely to be broken, and a direct call is
exactly the part that skips it.
Derived vs authored, which P5 is the easiest place to blur:
* DERIVED -- the ORDER of the items, from each screen file's `buttons`, which
the exporter already fills from button-role elements sorted by resting Y.
* AUTHORED -- destinations, initial focus, what (B) does, and left/right being
a no-op. All measured off the running game (HANDOFF Q4/Q5) or chosen, none
on the disc, all in authored/flow.json with a why.
Four of five main-menu destinations are `goto: null` with a `blocked` note. That
is a MILESTONE BOUNDARY, not an unknown -- DIFFICULTY, the save list, the lesson
list and OPTIONS were all measured and live in archives this export does not
carry. `blocked` and `none` are kept apart so nobody later "discovers" the gap.
--headless CANNOT DRAW, and the port hung instead of saying so.
Measured, not assumed: under --headless Godot's dummy renderer never emits
RenderingServer.frame_post_draw, so every capture path awaited it forever --
--capture since P1, --film since P3, --shots as of now. With stdout block-
buffered the observable behaviour was SILENCE, FOREVER, which in a loop reads as
a job still working. Isolated by `--quit` (prints, exits 0) vs `--capture` (zero
bytes, killed at 40 s). Now those three flags refuse at STARTUP naming the
xvfb-run line that works, and --script no longer waits for a frame it is not
going to photograph -- so headless walks the menus in 4.5 s as a cheap
regression check needing no X server.
REFUTATION ATTEMPT, against the Decoder's 76653ca point 2 ("the oracle confirms
the game renders the ring's rotation"). Aimed there because PROTOCOL says to aim
at a claim the port is about to build on that rests on an estimator whose own
control the Decoder reported as +/-19.8 deg. IT SURVIVES, more strongly than
claimed.
Both captures draw the SAME sprite (ptbtneff01) 240 px apart, so "is it drawn
rotated" becomes "are these two crops one image at a different angle" -- no crop
offset needed and no reference to our own renderer. 360-bin angular luminance
profile over the annulus, circularly cross-correlated. Two controls first: known
rotations 0/30/90/150/210/270/330 recovered with 0 deg error, and a ring-free
patch of the same capture peaks at 0.369, so the estimator does not manufacture
matches. Then: A vs B 134 deg (corr 0.968), sprite vs A 76 deg, sprite vs B
210 deg -- and 210-76 = 134, which nothing in the method forced.
So 0 deg is NOT A POSE THE GAME SHOWS, and screen_view.gd draws the ring at
0 deg. That is now stated in the code as known-wrong rather than suspected. The
port did NOT start spinning it: the period has two unknowns and both are the
Decoder's -- the second keyframe is untimed, and "groups hold" predicts a stop
at 360 = 0 which contradicts both captures. Two frames of one focused button a
known time apart settle it. Filed in BLOCKED.md and asked over the channel.
BLOCKED.md's staleness check was half a check. It tested whether that page is
stale relative to HANDOFF; it cannot see the other direction, and the other
direction is what happened -- 76653ca lands 27 minutes AFTER HANDOFF was last
written and answers a question HANDOFF still lists as open. Added the missing
half: `git log --oneline 0fd8e69..HEAD -- docs/re/`.
Also recorded, since the two were nearly confused: the ring's annulus centroid
lands within ~0.4 px of its design position under a ZERO crop offset, which
corroborates ORACLE-CAPTURES' "1279x675, top-left aligned" on a feature nobody
chose for the purpose. The earlier "text bands at design y + 23" is an offset
WITHIN the button sprite, not a crop offset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CtmUw5N5LJaMW1Njb8Ziey
This commit is contained in:
@@ -13,6 +13,26 @@ for, so it is deleted rather than forgotten when the answer arrives.
|
||||
Reconciled **2026-08-29** against [`docs/port/HANDOFF.md`](HANDOFF.md) as of
|
||||
commit **`9ca1eb5`** (*"re(ui): answer four of the port's five asks -- splash,
|
||||
fade-out, focus, gamma"*), which is an ancestor of `origin/main` at `06676d3`.
|
||||
Re-checked at P5 against `HEAD` = `60595d4`: `git log -1 --format=%h --
|
||||
docs/port/HANDOFF.md` still answers `9ca1eb5`, so HANDOFF itself has not moved.
|
||||
|
||||
🔴 **HANDOFF has not moved, and that is now the problem.** The check above tests
|
||||
whether *this page* is stale relative to HANDOFF. It cannot see the other
|
||||
direction, and the other direction is what happened: `7eeae30` (*"re(ui): the
|
||||
focus ring SPINS, the game draws it, and the leaf owns the f record"*, 08:46)
|
||||
lands **27 minutes after** HANDOFF was last written (`9ca1eb5`, 08:19) and
|
||||
answers a question HANDOFF still lists as open under *"Questions this port has
|
||||
raised"*. Both are ancestors of `HEAD`.
|
||||
|
||||
So the staleness check needs a second half, and this is it:
|
||||
|
||||
```sh
|
||||
git log --oneline 9ca1eb5..HEAD -- docs/re/ # RE landed since HANDOFF was written?
|
||||
```
|
||||
|
||||
Anything it lists may already answer a row below. The Decoder has been told over
|
||||
the message channel that HANDOFF needs `7eeae30` folded in; **rewriting HANDOFF
|
||||
is not the port's to do.**
|
||||
|
||||
⚠️ **The address of HANDOFF.md changed and this page did not notice.** The
|
||||
previous line here cited `/reborn` HEAD `9a0ca0d`. Two things have since made
|
||||
@@ -45,6 +65,8 @@ git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconc
|
||||
| ~~P6 audio~~ | ~~which cue fires on move / confirm / back~~ | Q8 | ✅ **answered 2026-08-28** — the RE agent retracted "cannot be extracted". The waves are located in `Static.slb` by playing them: **move `0x1ec0`** (8 192 B, 0.533 s), **confirm `0x5d6c0`** (12 288 B, 1.016 s), **back `0x0ec0`** (4 096 B, 0.344 s), and ⬅➡ play nothing. Move and back reproduce across two boots. 🟡 that the cursor's wave is the cue *named* `SE_UI_CURSOR` is still a name match, and Ⓐ's wave is not separated between `SE_UI_DECIDE` and `SE_UI_SUB_WIN_OPN`. P6 can now export real audio; the exporter has to grow an SE path. |
|
||||
| P6 audio | which BGM the menu plays | Q10 | ❔ **not on the disc.** All 32 banks are named `BGM_001`…`BGM_109` with no semantic name anywhere. The port is choosing a track, and that choice is authored. |
|
||||
| P6 looping | where a menu loop restarts | Q10 | ❔ `BGM_001` fades out at 167.663 s into 6.15 s of silence, and no loop-point field has been identified. A menu loop is authored. |
|
||||
| P5 focus marker | **the focus ring's spin PERIOD, and whether it loops** | Q1 + the 2026-08-28 *"groups hold"* answer | ❔ open, and the port is drawing a pose it knows is wrong. Derived at HANDOFF `9ca1eb5` **plus** `7eeae30`, which HANDOFF does not yet carry. `ptbtneff01` declares `t=120, rot 0` then an **untimed** `rot 360`. The port measured the two oracle captures at **~76°** and **~210°** — 134° apart, peak corr 0.968, null control 0.369 (`DECISIONS.md`) — so **0° is not a pose the game shows**, and the port draws 0° because a spin rate would be invented. Two unknowns, both the Decoder's: (a) under Q1's *replicated* reading `t=120` is when the **next** pose is reached, giving one revolution in 2.0 s, but this port's `pose_at` implements the other reading and switching it changes every screen's animation timing; (b) *"groups hold"* predicts a stop at 360 = 0, which contradicts both captures. **What settles it: two frames of one focused button a known time apart.** |
|
||||
| P5 — Ⓑ on the main menu | **is Ⓑ what returns to the title, or the idle timer?** | Q5 | 🟡 stated in HANDOFF, no capture behind it. The title self-returns after ~8–10 s idle, so one unrecorded observation cannot separate them. `authored/flow.json` implements it and marks it *authored — likely but UNPROVEN*. **Not blocking** — P5 shipped with it — but it is the only navigation rule on that screen with nothing under it. Settled by one run that presses Ⓑ well inside the idle window, timestamped. |
|
||||
|
||||
## Answered since this file was last written — no longer blocking
|
||||
|
||||
@@ -82,7 +104,7 @@ than exported:
|
||||
| Authored because it is not on the disc | HANDOFF | Where it lives |
|
||||
|---|---|---|
|
||||
| `1 keyframe unit = 1/60 s` | Q1 | not yet written — P2 |
|
||||
| initial menu focus (not stable across boots; pick one and say so) | Q5 | `authored/menu.json` — P5, this iteration |
|
||||
| initial menu focus (not stable across boots; pick one and say so) | Q5 | `authored/flow.json`, `screens.main_menu.initial_focus` — landed at P5 |
|
||||
| the ~0.4 s fade-out and the 0.17–0.23 s black hold | Q7 | not yet written — P3 |
|
||||
|
||||
## The five asks — four answered at `9ca1eb5`
|
||||
|
||||
@@ -928,3 +928,213 @@ next iteration does not rebuild the same broken tool.
|
||||
the group-loop question again — answered "groups hold" for build-in animations,
|
||||
but a 360° hold and a 0° hold are the same pose, so *this* group cannot be told
|
||||
apart by its rest pose. Not guessed; raised below.
|
||||
|
||||
---
|
||||
|
||||
## P5 — navigation, 2026-08-29
|
||||
|
||||
The gate is *"a human clicks through it"*, and the artifact is a walk that
|
||||
proves the wiring rather than the intent: `up` (which wraps 01→05), five `down`,
|
||||
Ⓐ into `EXTRAS`, `down`, Ⓑ back — landing on the main menu with focus restored
|
||||
to `EXTRAS`.
|
||||
|
||||
```sh
|
||||
xvfb-run -a godot --path port -- --menu \
|
||||
--script=up,down,down,down,down,down,accept,down,cancel --shots=/tmp/p5
|
||||
```
|
||||
|
||||
Ten PNGs, one per step, each taken after the screen it produced had settled.
|
||||
Contact sheet handed over as `share` id `1788002507-ef4468a0a33a`.
|
||||
|
||||
### The scripted walk goes through the input system, not around it
|
||||
|
||||
`--script` posts `InputEventAction` through `Input.parse_input_event` and lets
|
||||
it arrive at `_unhandled_input` exactly as a d-pad's press would. Calling
|
||||
`MenuFlow.move()`/`accept()`/`cancel()` directly would have been shorter and
|
||||
would have proved nothing: the thing most likely to be broken is the wiring
|
||||
between a press and the cursor, and a direct call is precisely the part that
|
||||
skips it. The same reasoning says the settle wait must be real — a shot taken
|
||||
before the screen stops moving photographs a fade and calls it a menu.
|
||||
|
||||
### What is authored here, and what is derived
|
||||
|
||||
Split deliberately, because P5 is where the two are easiest to blur:
|
||||
|
||||
| | where | why |
|
||||
|---|---|---|
|
||||
| the ORDER of the items | **derived** — each screen file's `buttons`, filled by the exporter from the button-role elements sorted by resting Y | it is on the disc |
|
||||
| where an item goes | **authored** — `authored/flow.json` | HANDOFF Q4 *measured* the destinations; they are not in the file |
|
||||
| which item opens focused | **authored** | Q5 measured that it is **not stable boot to boot** |
|
||||
| what Ⓑ does | **authored** | Q5, measured — except on the main menu, see below |
|
||||
| ⬅➡ do nothing | **authored**, written as an explicit no-op | so that *"the game ignores it"* and *"we never wired it"* are different lines of code |
|
||||
|
||||
Four of the five main-menu destinations are `goto: null` with a `blocked` note.
|
||||
That is **not** an unknown: `DIFFICULTY`, the save-slot list, the lesson list and
|
||||
the settings menu were all measured, and they live in archives this export does
|
||||
not carry. `blocked` and `none` are kept apart so a later reader does not
|
||||
"discover" a gap that was a milestone boundary.
|
||||
|
||||
`EXTRAS` is the only main-menu destination inside `GP_TITLE`, and therefore the
|
||||
only Ⓐ-into-a-submenu this gate can actually walk.
|
||||
|
||||
### The one navigation rule with nothing behind it
|
||||
|
||||
Ⓑ on the **main menu** → title. HANDOFF Q5 states it, and `flow.json` marks it
|
||||
*authored — likely but UNPROVEN*, because the title also self-returns after
|
||||
~8–10 s idle and a single unrecorded observation cannot separate the two. The
|
||||
port implements it anyway — a menu with no way out is worse than a menu with a
|
||||
plausible one — and says in the file that it did. Asked of the Decoder this
|
||||
iteration; see `BLOCKED.md`.
|
||||
|
||||
Independent corroboration that the main menu is different from its submenu:
|
||||
the main menu's footer advertises only `Ⓐ : OK`, while `EXTRAS`' footer
|
||||
advertises `Ⓑ : Back`. That is on the disc, in `ptmsg.png` vs `ptmsg2.png`, and
|
||||
it is visible in both the port's render and the captures.
|
||||
|
||||
### A press during a fade is dropped
|
||||
|
||||
**Authored, and not measured.** Nobody has watched what the game does with a
|
||||
button pressed mid-transition. Dropping invents less than queueing does: it
|
||||
cannot manufacture a press the game might have discarded. `flow.json` says so
|
||||
under `navigation.input_during_transition`.
|
||||
|
||||
---
|
||||
|
||||
## `--headless` cannot draw, and the port hung instead of saying so, 2026-08-29
|
||||
|
||||
`docs/port/PORT-MISSION.md` and the loop prompt both name `godot-headless` as
|
||||
how this project runs unattended. It does not work, and the way it failed was
|
||||
the worst available shape.
|
||||
|
||||
**Measured, not assumed.** Under `--headless` Godot's dummy renderer never emits
|
||||
`RenderingServer.frame_post_draw`. Every capture path in `boot.gd` awaits it —
|
||||
`--capture` since P1, `--film` since P3, `--shots` as of this milestone — so all
|
||||
three blocked forever. Isolated by the difference between two runs:
|
||||
|
||||
```
|
||||
godot --headless --path port --quit # prints, exits 0
|
||||
godot --headless --path port -- --screen=… --capture=… # no output at all, killed at 40 s
|
||||
```
|
||||
|
||||
The second produces **zero bytes of output** before it is killed, because
|
||||
Godot's stdout is block-buffered and never flushes. So the observable behaviour
|
||||
of an unattended headless capture was: silence, forever. In a loop, a job that
|
||||
waits reads as a job still working — this is the failure mode that costs a whole
|
||||
iteration and leaves nothing behind to say what happened.
|
||||
|
||||
Two changes, and deliberately not one:
|
||||
|
||||
* `--capture`, `--film` and `--shots` **refuse at startup** under `--headless`,
|
||||
naming the flag and printing the `xvfb-run` line that does work. Refusing
|
||||
early rather than at the first frame means the run does not die halfway
|
||||
through a filmstrip with some frames written.
|
||||
* `--script` **no longer waits for a drawn frame when it is not going to
|
||||
photograph one.** Navigation is checkable where nothing draws, and that is
|
||||
worth keeping: `godot --headless --path port -- --menu --script=…` now walks
|
||||
the menus and exits 0 in about four seconds, which is a cheap regression check
|
||||
that needs no X server at all.
|
||||
|
||||
The Xvfb path is unchanged and is what produced the P5 artifact.
|
||||
|
||||
---
|
||||
|
||||
## Refutation — the focus ring IS drawn rotated, and it is not at 0° in either capture
|
||||
|
||||
Attempted against the Decoder's `7eeae30` (*"re(ui): the focus ring SPINS, the
|
||||
game draws it, and the leaf owns the f record"*), point 2: that in the
|
||||
OPTIONS-focused capture the ring's bright head sits in a different angular
|
||||
position from the sprite's own, caught mid-spin. **It survives**, and the
|
||||
evidence is stronger than what was claimed.
|
||||
|
||||
Chosen for refutation because it is exactly what PROTOCOL says to aim at: a
|
||||
claim the port is about to build on, resting on an estimator (a brightest-region
|
||||
centroid) whose own control the Decoder reported as ±19.8°.
|
||||
|
||||
### The test, and why it needs no absolute registration
|
||||
|
||||
`live-main-menu.png` has `ptbtn01` focused; `live-main-menu-options-focused.png`
|
||||
has `ptbtn04` focused. Both draw **the same sprite**, `ptbtneff01.png` — the
|
||||
export confirms the two focus records name the same file. So the two captures
|
||||
contain two instances of one 42×46 image, 240 px apart in design space, and the
|
||||
question *"is it drawn rotated"* becomes *"are these two crops the same image
|
||||
at a different angle"* — which needs no crop offset and no reference to our own
|
||||
renderer.
|
||||
|
||||
Method: sample each ring into a 360-bin **angular luminance profile** over the
|
||||
annulus band (r = 9…15 px, bilinear, 0.5 px radial step) and circularly
|
||||
cross-correlate. A rotation about the centre shifts that vector and changes
|
||||
nothing else.
|
||||
|
||||
### The instrument was run through two controls before it was believed
|
||||
|
||||
| control | result |
|
||||
|---|---|
|
||||
| rotate a capture's own ring by a known 0/30/90/150/210/270/330° and recover it | **0° error on all seven**, peak corr 1.000 |
|
||||
| the same estimator on a ring-free 60×64 patch of the *same* capture | peak corr **0.369** — it does not manufacture a match |
|
||||
|
||||
### The measurement
|
||||
|
||||
On one shared centre for all three images, so a centroid difference cannot
|
||||
masquerade as a rotation:
|
||||
|
||||
| pair | best shift | peak corr | corr at 0° |
|
||||
|---|---|---|---|
|
||||
| capture A vs capture B | **134°** | 0.968 | −0.064 |
|
||||
| sprite (unrotated) vs capture A | **76°** | 0.969 | −0.295 |
|
||||
| sprite (unrotated) vs capture B | **210°** | 0.948 | −0.181 |
|
||||
|
||||
210 − 76 = 134: the three measurements are internally consistent, which nothing
|
||||
in the method forced them to be. Sweeping the centre by ±2 px moves the A-vs-B
|
||||
answer over 117…161° while the peak correlation stays 0.9+ across the middle of
|
||||
that range, so the **magnitude is ~134° ± ~15°** and the precision claim stops
|
||||
there.
|
||||
|
||||
Evidence sheet — sprite, capture A, capture B, each cropped at the declared
|
||||
`42×46+500+156` / `+500+396` — handed over as `share` id
|
||||
`1788002507-afe1ad843789`. The phase difference is obvious by eye; the numbers
|
||||
are here so it is not only obvious by eye.
|
||||
|
||||
### The two things this settles for the port
|
||||
|
||||
1. **The game draws `rotation_deg` on an element the English boot path shows.**
|
||||
This is a second, independent confirmation on a different screen and a
|
||||
different element from the `ptloop` sweeps, and it moves HANDOFF **ask 4**
|
||||
(*should the port draw rotation*) off "changes nothing at rest" — it changes
|
||||
the main menu's focus marker, in every frame.
|
||||
|
||||
2. **0° is not a pose the running game shows.** `screen_view.gd` currently draws
|
||||
the ring at its `rest` pose, which is `rotation_deg 0`, and both captures put
|
||||
it at 76° and 210°. So the port's focus marker is **known** to be wrong, not
|
||||
suspected — and the comment in `screen_view.gd` now says which two numbers it
|
||||
is wrong against.
|
||||
|
||||
### Registration, as a by-product
|
||||
|
||||
The ring's annulus centroid lands at (32.94, 36.63) and (33.30, 38.90) in
|
||||
windows whose design-space prediction under a **zero crop offset** is
|
||||
(33.0, 37.0). Within ~0.4 px on the better-thresholded of the two. That
|
||||
corroborates `ORACLE-CAPTURES.md`'s *"1279×675, top-left aligned"* directly, on
|
||||
a feature nobody chose for the purpose.
|
||||
|
||||
⚠️ Do not read the earlier P5-groundwork note *"button text bands land at design
|
||||
y + 23"* as a crop offset — it is an offset **within** the button sprite, and
|
||||
the two were nearly confused here.
|
||||
|
||||
### What the port did NOT do about it
|
||||
|
||||
It did not start spinning the ring. The period is a **guess with two unknowns**
|
||||
and both belong to the Decoder:
|
||||
|
||||
* the keyframes are `t=120, rot 0` then an **untimed** `rot 360`. Under HANDOFF
|
||||
Q1's replicated reading (*"`+36` is the time the NEXT pose is reached"*) that
|
||||
is one revolution in 120 units = **2.0 s** — but this port's `pose_at`
|
||||
implements the *other* reading, and switching it is a change to every screen's
|
||||
animation timing, not a P5 change;
|
||||
* *"groups hold"* (settled 2026-08-28) predicts the ring stops at 360° = 0°.
|
||||
Both captures show it elsewhere. That is either a spin that loops, or two
|
||||
captures both taken inside the first two seconds of focus. **The port cannot
|
||||
tell those apart**, and a wrong answer here is a visible continuous rotation
|
||||
on whichever button the player is sitting on.
|
||||
|
||||
Filed in `BLOCKED.md` and asked over the message channel. What settles it is two
|
||||
frames of one focused button a known time apart.
|
||||
|
||||
Reference in New Issue
Block a user