From 09188ccbb252c6e3b6550249be7be417920e1303 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sat, 29 Aug 2026 23:59:54 +0000 Subject: [PATCH] port: fix two capture bugs, and the second was hiding a missing PRESS (A) plate 1. --capture with --script photographed before the first press (t=0.133 s, 10 of 16 elements transparent). Two runs differing by two presses came out bit-identical and I read it as 'runtime focus never changes'. Deferred to the end of the script; verified max 235 and t=82 units. 2. --boot --capture= wrote NO FILE: _finish_boot() is reachable only from the overlay-quit branch, but line 412 quit first because _overlay_spec is cleared when the overlay is raised. Pre-existing, confirmed by stashing. Fixed by also requiring _overlay_quit_at < 0.0. 3. The artifact that now exists shows the boot's end frame is bit-identical to the title alone -- no plate. ptbtn00 is opaque for 8 units (236-244) and the boot captures at 246.54, because it waits for build 4 to finish fading at t=261. Both halves of that are sound and they are incompatible. NOT changed; filed, since what settles it is what the game does after t=244. Defect 3 was invisible while defect 2 existed: a capture flag that writes nothing cannot show a missing element. Also records that runtime focus is FINE -- my contrary reading came from 410 files whose names did not match the flag I passed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF --- docs/port/BLOCKED.md | 27 +++++++++++++++ docs/port/DECISIONS.md | 74 ++++++++++++++++++++++++++++++++++++++++++ port/scripts/boot.gd | 31 +++++++++++++++--- 3 files changed, 127 insertions(+), 5 deletions(-) diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 352bdc6b..992ec05b 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -512,3 +512,30 @@ shipped game would do. That reasoning is what makes an invented answer feel safe ⚠️ Related but separate, and already authored with a `why`: the bed **loops by restart**, seam and all (`authored/audio.json`, `loop_why`). That is a known-ugly deliberate choice pending a loop point, not this question. + +--- + +## Does the `PRESS Ⓐ` plate stay up, or blink once and go? + +*Derived from HANDOFF `9ca1eb5`. Raised 2026-08-29 by the port.* + +`ptbtn00`'s group makes the plate opaque for **8 units only** — alpha 0 until +t=214, 255 at t=236 and t=238, back to 0 by t=244 — and its group then ends. So +in the port the plate flashes for 0.133 s and is gone, and the boot's end-state +capture (taken at t=246.54, once build 4 has finished fading up at t=261) contains +**no plate at all**. + +🔴 The two states cannot share a frame: the plate's window closes 17 units before +the title stops presenting. The port captures the title, by an earlier and sound +decision it is not reversing. + +What would settle it: **a capture of the real title held for several seconds after +the plate first appears.** Three outcomes, all decisive — the plate stays up; it +pulses on a period (`authored/timing.json` already carries a *speculative* +`looping_focus_records` entry for `press_start/ptbtn00` at 120 units, which is +what a pulse would need); or it genuinely blinks once and vanishes, in which case +the port is right and this closes ✅. + +⚠️ Note the port already draws a looping focus record for this element. If the +answer is "blinks once", **that entry is wrong and should be deleted**, not kept +as harmless — it is the only thing making the plate reappear at all. diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 31483e98..539c8db1 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -5754,3 +5754,77 @@ silently ran 39 % long would have put every such observation at the wrong instan and the bed-loop result above would have been the first casualty. It survived only because the seam was read off the **recording's** clock, which the bed's own known length calibrates. + +## Two harness bugs, and the defect the second one was hiding + +### 1. `--capture` with `--script` photographed the frame *before* the script + +`--capture` fired in `_ready` and quit. With `--script` that is **before the +first press**: at t=0.133 s, with 10 of 16 elements still transparent. Two runs +differing by two `down` presses came out **bit-identical**, and I read that as +*"runtime focus never changes"* — a confident wrong finding that `--shots` +contradicted within a minute. + +Fixed: with a script, the capture defers to the end of the run, through the same +`_capture_to` member the boot path already used. Verified — the two runs now +differ at max 235, and the capture lands at t=82 units instead of t=8. + +### 2. `--boot --capture=` wrote no file at all + +`_finish_boot()` is reachable only from the overlay-quit branch, but the boot +quit first: line 412 exits when `_film == "" and _overlay_spec.is_empty()`, and +`_overlay_spec` is **cleared when the overlay is raised**. So a plain `--boot` +ended at 10.99 s, 1.2 s before its own scheduled 12.21 s, and the capture never +happened. + +Confirmed pre-existing by stashing my changes and re-running. Fixed by also +requiring `_overlay_quit_at < 0.0`. The boot now runs to 12.19 s, prints *"boot +ends on title + press_start"*, and writes the file. + +⚠️ The flag has a doc comment explaining that it exists so the boot has an +artifact of its own instead of a 600-PNG filmstrip. **It has been producing +nothing.** A flag that silently writes no file is the same failure as +`ScreenView.skipped` and `stop_bed`: provided, plausible, and never exercised. + +### 3. 🔴 And the artifact it now produces shows the plate is missing + +The boot's end frame is **bit-identical** to `--screen=title` at the same instant +— max difference 0. The `PRESS Ⓐ` plate is not in the port's end state. + +`ptbtn00`'s own fade explains it exactly: + +| t | 0 | 214 | **236** | **238** | 244 | +|---|---|---|---|---|---| +| alpha | 0 | 0 | **255** | **255** | 0 | + +The plate is visible for **8 units — 0.133 s** — and the boot captures at +**t=246.54**, two and a half units after it has gone. + +That is not an accident of frame timing; the code chose it. `_overlay_quit_at` +takes `max(view.settle_time(), overlay.settle_time())`, and its comment says why: +*"the plate arrives at t=238 and build 4 is still fading up from black until +t=261 … quitting when the plate lands photographs a title that has not finished +presenting."* Both halves are true, and together they mean **the two states +cannot both be in one frame.** The port picked the title, and the consequence — +that the plate is in no artifact at all — was never written down. + +⚠️ I am **not** moving the trigger. The earlier reasoning is sound and the +measurement it protects is real; picking the other instant would trade a missing +plate for a visibly dark title, which is the swap that was already made once and +regretted. What settles it is what the *game* does with the plate after t=244 — +filed. + +📌 Worth naming: defect 3 was **invisible while defect 2 existed**. A capture flag +that writes nothing cannot show you a missing element. The broken tool was hiding +the thing the tool was built to find, and neither was noticed because the absence +of a file looks exactly like not having run the command. + +### What was *not* wrong, and how I nearly recorded that it was + +Runtime focus works. Per-step `--shots` across `down,down` differ at max 232–233, +with the differing boxes tracking down the button column. My contrary reading came +from analysing 410 `f_NNN` files after asking for `--shots=…/s` — **the filenames +did not match the flag I passed, and that was visible in my first `ls`.** A +verbatim re-run produces six correctly-named per-step shots. I do not know where +those 410 files came from, which is itself the point: I drew a conclusion from a +file set whose provenance I never checked. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index 82faf79a..3a262e6e 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -274,11 +274,28 @@ func _ready() -> void: overlay.time_units = float(args["time"]) * overlay.units_per_second overlay.queue_redraw() - # `--boot --capture=` is deferred to the end of the sequence (`_finish_boot`); - # in every other mode the frame worth having is this one. + # `--boot --capture=` is deferred to the end of the sequence (`_finish_boot`), + # and so is `--script --capture=`. + # + # 🔴 The second half of that was missing, and the comment here used to assert + # the opposite -- "in every other mode the frame worth having is this one". + # With `--script` it is emphatically not: the capture fired **before the + # first press**, at t=0.133 s, with 10 of 16 elements still transparent, and + # then quit. Two runs differing by two `down` presses came out BIT-IDENTICAL, + # because neither had run its script when it was photographed. + # + # That is not a harmless default. It is a well-formed answer to a different + # question, and it produced a confident wrong finding -- "runtime focus never + # changes" -- that `--shots` immediately contradicted. A flag combination + # that silently photographs the wrong instant is worse than one that errors. if args.has("capture") and _capture_to == "": - await _capture(args["capture"]) - get_tree().quit(0) + if not _script.is_empty(): + # Defer to the end of the script, through the SAME member the boot + # path already uses, rather than adding a second mechanism. + _capture_to = String(args["capture"]) + else: + await _capture(args["capture"]) + get_tree().quit(0) var _frozen := false @@ -392,7 +409,7 @@ func _process(delta: float) -> void: # than one that exits. if _play: _menu_enter(String(_sequence[_step].get("screen", "")), true) - elif _film == "" and _overlay_spec.is_empty(): + elif _film == "" and _overlay_spec.is_empty() and _overlay_quit_at < 0.0: get_tree().quit(0) elif not view.holding and view.time_units >= view.exit_time() + _black_hold: # 🔴 THE BLACK HOLD, which this port had never implemented. A transition @@ -836,6 +853,10 @@ func _run_script() -> void: await _shoot("%02d_%s" % [i + 1, token]) print("script complete after %.2f s on %s, focus %s" % [_elapsed, _menu.current(), _focus_label(view.focused_id)]) + # The frame worth having from a scripted run is the one the script ARRIVED + # at, not the one it started from. See the note beside the early capture. + if _capture_to != "": + await _capture(_capture_to) get_tree().quit(0)