port: P3 -- the boot title gets its PRESS (A) plate, and two of the RE agent's numbers do not agree
ScreenView now draws two builds at once, which it never had to before. It is a second ScreenView in the same SubViewport rather than a subordinate screen inside one: each build has its own timeline, its own textures and its own hold, which is the entire content of the finding, and Node2D siblings already paint in tree order. `paint_order` still means what it meant -- an ordering WITHIN a build. The delay is authored in flow.json on the BOOT STEP, not on the `title` screen. What was measured is the boot title; whether the plate is there when the title is reached again -- (B) from the menu, or after the attract movie -- is not, and hanging it on the screen would quietly claim that it is. REFUTATION, and it is the substance of this commit: the RE agent's authoring instruction does not reproduce the RE agent's own measurement, and the gap is 3.97 s. The instruction is "when build 4 has settled, wait 2.13 s, composite build 2". But build 2 has a group and this port plays groups -- ptbtn00 is alpha 0x00 at t=214, still 0x00 at t=236 while it slides 10 px up, and 0xff only at t=238, which is 3.967 s at 60 units/s. So the plate is first VISIBLE at settle+6.10 s, while what was measured -- the glyph counter leaving 154 -- is visibility at settle+2.13 s. Both groups starting together puts it 0.38 s BEFORE settle; build 2 starting at settle puts it at settle+3.97 s; landing on the measurement needs build 2's group to start 2.51 s after build 4's, which is not a landmark of anything. The measurement is untouched -- it is an observation of the running game and I have no standing to doubt it. What is refuted is the step that turns it into an authoring rule. So the port ships the instruction, prints the discrepancy on every boot, and files the row. Same call as the BGM sub-waves: a port that quietly picks the number that looks right destroys the evidence, because a corrected boot looks exactly like a correct one. Also refuted, and it was mine: BLOCKED.md has said since P2 that "no element's alpha reverses direction anywhere in this export, so nothing pulses". ptbtn00 reverses -- 0x00 -> 0xff -> 0x00 -- and it was in the export the whole time. The claim had been checked against the screens P2 happened to be animating. The port still draws no pulse, because no reading of this group yields the measured 2.24 s: the whole group is 4.47 s and from its first keyframe 0.90 s. Gate: `--boot --capture=` writes one frame of the composited end state, instead of the 600-PNG filmstrip that was previously the only boot artifact. `--screen=title --overlay=press_start` raises the same composite in two seconds for anyone who does not want to sit through 137 s of Theora. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WM5XL4HfrHuxz8RiMWdCMC
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
# godot --path port -- --screen=main_menu --capture=/tmp/godot.png
|
||||
# godot --path port -- --screen=main_menu --time=0.5 --capture=/tmp/at-half.png
|
||||
# godot --path port -- --screen=main_menu --pose=rest --capture=/tmp/rest.png
|
||||
# godot --path port -- --screen=title --overlay=press_start --time=4
|
||||
# godot --path port -- --boot # the whole boot sequence
|
||||
# godot --path port -- --boot --film=/tmp/boot # ...and a frame every 0.25 s
|
||||
# godot --path port -- --menu # P5: navigate the menus
|
||||
@@ -49,6 +50,20 @@ var view: ScreenView = null
|
||||
var viewport: SubViewport = null
|
||||
var audio: MenuAudio = null
|
||||
|
||||
## The second build, drawn OVER `view`. The boot title is the only place in this
|
||||
## port where two builds are on screen at once (`authored/flow.json`, the boot's
|
||||
## `title` step): build 4 presents alone, and the `PRESS Ⓐ BUTTON` plate --
|
||||
## build 2 -- is composited over it 2.13 s after build 4 settles. MEASURED, two
|
||||
## boots agreeing to 6 ms; see the step's `why`.
|
||||
##
|
||||
## A second ScreenView rather than a second screen inside one, because that is
|
||||
## what "two builds at once" actually is: each has its own timeline, its own
|
||||
## textures and its own hold, and Node2D siblings already draw in tree order.
|
||||
## Teaching ScreenView about a subordinate screen would have been the same
|
||||
## information expressed less directly, and would have put an `if overlay` in
|
||||
## every method that walks elements.
|
||||
var overlay: ScreenView = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var args := _args()
|
||||
@@ -85,6 +100,10 @@ func _ready() -> void:
|
||||
if _record_to != "":
|
||||
_start_recording()
|
||||
|
||||
# In `--boot` the capture is taken at the END, not in `_ready`: the frame
|
||||
# worth having is the composited title, and `_ready` runs 150 s before it.
|
||||
if args.has("boot"):
|
||||
_capture_to = args.get("capture", "")
|
||||
_film = args.get("film", "")
|
||||
_shots = args.get("shots", "")
|
||||
if args.has("script"):
|
||||
@@ -171,7 +190,25 @@ func _ready() -> void:
|
||||
set_process(true)
|
||||
_film_capture()
|
||||
|
||||
if args.has("capture"):
|
||||
# `--overlay=<screen>` composites a second build immediately, without waiting
|
||||
# for a boot. It exists because the only other way to see two builds at once
|
||||
# is a 156 s `--boot`, of which 137 s is the intro movie -- which under Xvfb's
|
||||
# software Theora decode is several minutes to answer "is the plate on top of
|
||||
# the title". This raises the same second `ScreenView` by the same code path,
|
||||
# so what it photographs is the real composite and not a mock-up. It applies
|
||||
# NO delay: the delay is a measurement and lives in `authored/flow.json`,
|
||||
# where the boot reads it.
|
||||
if args.has("overlay") and not args.has("boot"):
|
||||
_overlay_spec = {"screen": args["overlay"]}
|
||||
_overlay_due = 0.0
|
||||
_overlay_process(0.0)
|
||||
if overlay != null and args.has("time"):
|
||||
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.
|
||||
if args.has("capture") and _capture_to == "":
|
||||
await _capture(args["capture"])
|
||||
get_tree().quit(0)
|
||||
|
||||
@@ -200,6 +237,7 @@ func _process(delta: float) -> void:
|
||||
view.time_units += delta * view.units_per_second
|
||||
_elapsed += delta
|
||||
view.queue_redraw()
|
||||
_overlay_process(delta)
|
||||
|
||||
if _player != null:
|
||||
return
|
||||
@@ -228,20 +266,32 @@ func _process(delta: float) -> void:
|
||||
view.holding = false
|
||||
elif not _boot_done:
|
||||
_boot_done = true
|
||||
print("boot sequence complete after %.2f s, holding on %s" % [_elapsed, _sequence[_step]])
|
||||
print("boot sequence complete after %.2f s, holding on %s" % [_elapsed, _sequence[_step].get("screen", _sequence[_step])])
|
||||
# The plate is timed from HERE -- the moment the screen reaches its
|
||||
# own hold -- and not from the frame it first appeared. That is the
|
||||
# finding, not a detail: measured from first-draw the two oracle
|
||||
# runs disagree by 0.48 s, because the build-in's own duration is
|
||||
# the emulator's frame pacing rather than the game's clock.
|
||||
var spec: Variant = _sequence[_step].get("overlay", null)
|
||||
if typeof(spec) == TYPE_DICTIONARY:
|
||||
_overlay_spec = spec
|
||||
_overlay_due = _elapsed + float(spec.get("after_settle_seconds", 0.0))
|
||||
print(" overlay %s due at %.2f s (+%.2f s after settle)"
|
||||
% [spec.get("screen", "?"), _overlay_due, _overlay_due - _elapsed])
|
||||
# P5 takes over here: the boot ends on the title and the title has
|
||||
# somewhere to go. Without `--play` the run still stops, because a
|
||||
# boot that ends by waiting for a key it will never get is worse
|
||||
# than one that exits.
|
||||
if _play:
|
||||
_menu_enter(String(_sequence[_step].get("screen", "")), true)
|
||||
elif _film == "":
|
||||
elif _film == "" and _overlay_spec.is_empty():
|
||||
get_tree().quit(0)
|
||||
elif not view.holding and view.time_units >= view.exit_time():
|
||||
_advance()
|
||||
|
||||
|
||||
func _advance() -> void:
|
||||
_drop_overlay()
|
||||
_step += 1
|
||||
var next: Dictionary = _sequence[_step]
|
||||
if next.has("video"):
|
||||
@@ -394,6 +444,8 @@ func _menu_enter(name: String, fresh: bool) -> void:
|
||||
|
||||
## The moment a screen has finished fading out and the next one takes over.
|
||||
func _menu_arrive() -> void:
|
||||
# The plate goes with the screen it was measured on. See `_drop_overlay`.
|
||||
_drop_overlay()
|
||||
var action: Dictionary = _pending
|
||||
_pending = null
|
||||
var name := String(action["goto"])
|
||||
@@ -456,6 +508,15 @@ func _capture(path: String) -> void:
|
||||
print("drew %d: %s" % [view.drawn.size(), ", ".join(view.drawn)])
|
||||
if not view.skipped.is_empty():
|
||||
print("not drawn %d: %s" % [view.skipped.size(), ", ".join(view.skipped)])
|
||||
# The overlay is a second build in the same frame, so it needs its own line.
|
||||
# Folding its elements into the list above would make the capture report a
|
||||
# screen that does not exist; leaving it out entirely made the first
|
||||
# composited capture read as though the plate had not been drawn at all.
|
||||
if overlay != null:
|
||||
print("overlay %s at t = %.2f units (%.3f s), drew %d: %s" % [
|
||||
overlay.screen.get("name", "?"), overlay.time_units,
|
||||
overlay.time_units / overlay.units_per_second,
|
||||
overlay.drawn.size(), ", ".join(overlay.drawn)])
|
||||
var err := img.save_png(path)
|
||||
if err != OK:
|
||||
push_error("cannot write %s (%d)" % [path, err])
|
||||
@@ -643,3 +704,118 @@ func _exit_tree() -> void:
|
||||
print("recorded %.3f s of Master bus -> %s (driver %s)"
|
||||
% [float(wav.data.size()) / float(wav.mix_rate * 2 * (2 if wav.stereo else 1)),
|
||||
_record_to, MenuAudio.driver()])
|
||||
|
||||
|
||||
# ── The second build ─────────────────────────────────────────────────────────
|
||||
|
||||
## The overlay the current boot step owes, if it has not been raised yet.
|
||||
var _overlay_spec: Dictionary = {}
|
||||
## Wall-clock second at which it is raised, measured from the screen's settle.
|
||||
var _overlay_due: float = 0.0
|
||||
|
||||
|
||||
func _overlay_process(delta: float) -> void:
|
||||
if overlay != null:
|
||||
overlay.time_units += delta * overlay.units_per_second
|
||||
overlay.queue_redraw()
|
||||
if _overlay_quit_at >= 0.0 and _elapsed >= _overlay_quit_at:
|
||||
print("boot ends on %s + %s at %.2f s"
|
||||
% [view.screen.get("name", "?"), overlay.screen.get("name", "?"), _elapsed])
|
||||
_overlay_quit_at = -1.0
|
||||
_finish_boot()
|
||||
return
|
||||
if _overlay_spec.is_empty() or _elapsed < _overlay_due:
|
||||
return
|
||||
_raise_overlay(String(_overlay_spec.get("screen", "")))
|
||||
|
||||
|
||||
## Composite a second build over the first.
|
||||
##
|
||||
## It starts at `time_units = 0` and plays its OWN group, so the plate rises and
|
||||
## fades in exactly as the disc declares -- alpha 0x00 at t=214, 0xff by t=238 --
|
||||
## rather than appearing as a cut. `holding` then parks it at its settle, which
|
||||
## for this build is the visible pose.
|
||||
##
|
||||
## ⚠️ It does NOT pulse, and that is a decision with arithmetic behind it rather
|
||||
## than an omission. See `authored/flow.json`, `no_pulse_why`.
|
||||
func _raise_overlay(name: String) -> void:
|
||||
var spec := _overlay_spec
|
||||
_overlay_spec = {}
|
||||
if name == "":
|
||||
return
|
||||
overlay = ScreenView.new()
|
||||
overlay.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
overlay.units_per_second = view.units_per_second
|
||||
overlay.exit_ramp_units = view.exit_ramp_units
|
||||
overlay.holding = true
|
||||
overlay.time_units = 0.0
|
||||
if not overlay.load_screen(view.tree, name):
|
||||
push_error(view.tree.error)
|
||||
overlay.queue_free()
|
||||
overlay = null
|
||||
return
|
||||
# After `view`, so it draws over it: Node2D siblings paint in tree order and
|
||||
# the export's own `paint_order` only orders WITHIN a build.
|
||||
viewport.add_child(overlay)
|
||||
print(" overlay %s raised at %.2f s, %d element(s), settles at t=%d"
|
||||
% [name, _elapsed, overlay.screen.get("elements", []).size(), int(overlay.settle_time())])
|
||||
# A boot with no menu to hand over to has now finished: it was held open for
|
||||
# this. Give the plate its own group time to play before leaving, so the
|
||||
# artifact shows the composited state rather than the frame it began on.
|
||||
var visible_at := overlay.settle_time() / overlay.units_per_second
|
||||
# The two lines below describe a BOOT. `--screen --overlay=` raises the same
|
||||
# composite with no delay, as a fast check, and must not narrate a sequence
|
||||
# it is not running -- a log line that lies is worse than no log line.
|
||||
if _sequence.is_empty():
|
||||
return
|
||||
# 🔴 SAY THE NUMBER OUT LOUD. `authored/flow.json` implements the RE agent's
|
||||
# instruction literally -- "when build 4 has settled, wait 2.13 s, composite
|
||||
# build 2" -- and build 2 then takes its OWN declared 238 units to fade in.
|
||||
# So the plate is first VISIBLE at settle + 2.13 + 3.97 s, while the thing
|
||||
# that was measured is the plate becoming visible at settle + 2.13 s. The two
|
||||
# cannot both be right and the port is not the one to choose. Printed on
|
||||
# every boot so the disagreement cannot go quiet.
|
||||
print(" ⚠ plate raised at settle+%.2f s but its own group reaches full alpha %.2f s later, \
|
||||
so it is first VISIBLE at settle+%.2f s -- the measurement is settle+%.2f s. See docs/port/BLOCKED.md."
|
||||
% [float(spec.get("after_settle_seconds", 0.0)), visible_at,
|
||||
float(spec.get("after_settle_seconds", 0.0)) + visible_at,
|
||||
float(spec.get("after_settle_seconds", 0.0))])
|
||||
if not _play and _film == "":
|
||||
_overlay_quit_at = _elapsed + visible_at
|
||||
print(" boot ends at %.2f s, once the plate has settled" % _overlay_quit_at)
|
||||
# `spec` is read only for the log; the reasoning lives in flow.json where a
|
||||
# reader looking for a decision will find it.
|
||||
if spec.has("why"):
|
||||
print(" why: %s" % String(spec["why"]).substr(0, 96))
|
||||
|
||||
|
||||
var _overlay_quit_at: float = -1.0
|
||||
|
||||
|
||||
## Take the overlay away with the screen it belongs to.
|
||||
##
|
||||
## The plate was measured on the BOOT title only. Whether it is there when the
|
||||
## title is reached again -- (B) from the main menu, or after the attract movie
|
||||
## -- is not measured, so leaving it up would be claiming something nobody has
|
||||
## watched. `authored/flow.json` says the same thing in the step's `scope_why`.
|
||||
func _drop_overlay() -> void:
|
||||
_overlay_spec = {}
|
||||
_overlay_quit_at = -1.0
|
||||
if overlay != null:
|
||||
overlay.queue_free()
|
||||
overlay = null
|
||||
|
||||
|
||||
## End a `--boot` run, photographing the composited end state first if asked.
|
||||
##
|
||||
## `--capture` used to be a `--screen`-only flag, taken in `_ready`. The boot had
|
||||
## no artifact of its own except a whole `--film` filmstrip, which is 600+ PNGs
|
||||
## to answer one question: is the plate on top of the title at the end. This
|
||||
## takes that one frame.
|
||||
func _finish_boot() -> void:
|
||||
if _capture_to != "":
|
||||
await _capture(_capture_to)
|
||||
get_tree().quit(0)
|
||||
|
||||
|
||||
var _capture_to := ""
|
||||
|
||||
Reference in New Issue
Block a user