diff --git a/authored/flow.json b/authored/flow.json index 3deb807c..f8e84e6c 100644 --- a/authored/flow.json +++ b/authored/flow.json @@ -29,6 +29,15 @@ }, { "screen": "title", + "overlay": { + "screen": "press_start", + "after_settle_seconds": 2.13, + "why": "MEASURED, 2026-08-29, docs/re/title-plate-delay-measured.md on branch auto/no-disc-and-menu-captures at fb536df (NOT yet on main when this was written). The boot title shows build 4 ALONE, and the `PRESS (A) BUTTON` plate -- build 2 -- is composited over it 2.13 s later. Two independent boots of the real game agree to 6 ms. This is the ONE case in the port where two builds are drawn at once.", + "measured_from_why": "The 2.13 s is timed from the frame build 4's own build-in animation FINISHES, not from the frame the title art first appears. That distinction is the finding, not a detail: measured the other way the two runs differ by 0.48 s, because the build-in itself ran 1.64 s and 2.13 s and the emulator's frame pacing during an animation is not the game's clock. A number taken from the wrong instant here looks exactly like a measurement.", + "not_a_fade_in_why": "The alternative -- that the plate was there all along, pulsing too dim to see -- was tested and refuted on two observables. The plate's declared alpha never exceeds 0x50, so a thresholded glyph counter could in principle miss a dim phase; it does not. The count sits at EXACTLY 154 with zero variation for 1.99 s / 2.13 s and then swings 714-1520, and the surface mean is flat to +/-0.03 across the plateau. A pulsing overlay moves the frame mean.", + "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. See docs/port/BLOCKED.md.", + "no_pulse_why": "The port draws the plate arriving and then HOLDING VISIBLE. It does not pulse it. The plate's own group encodes exactly one cycle -- alpha 0x00 at t=214, 0xff at t=238-244, 0x00 on the final untimed keyframe -- and the port holds at the settle (t=238, alpha 0xff) as it does on every other screen. The running game was measured pulsing continuously at peak intervals averaging 2.24 s, and NO reading of this group produces 2.24 s: looping the whole group gives 268 units = 4.47 s, looping from the first keyframe gives 54 units = 0.90 s. So repeating the group here would be inventing a period, and the port does not. 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." } ], diff --git a/docs/port/BLOCKED.md b/docs/port/BLOCKED.md index 72a8cf31..0736e5da 100644 --- a/docs/port/BLOCKED.md +++ b/docs/port/BLOCKED.md @@ -83,7 +83,11 @@ git log -1 --format=%h -- docs/port/HANDOFF.md # newer than 9ca1eb5? re-reconc | 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. | | P6 BGM โ€” the sub-wave count | **is a music bank's LEADING REGION a stem, or a decoder artefact?** | Q10 | ๐Ÿ”ด **HANDOFF and the decoders disagree, and P6 ships the disagreement.** `media::sound_bank_riffs("BGM_103.slb")` returns **three** sub-waves; HANDOFF Q10's census says a music bank is *"exactly two waves of identical duration (32/32 banks on the disc)"*. The third comes from `slb.rs:380` `to_xma_riffs`, whose hybrid branch emits a leading headerless packet region ahead of the `RIFF` waves โ€” and `docs/re/REFUTED.md` already records that region as what makes `BGM_106`โ€“`BGM_109` *"break the two-wave rule"*. Derived at HANDOFF `9ca1eb5`. **The exporter sums all three and writes a manifest warning**, because choosing which sub-wave to drop is a decoding question and MISSION ยง2 forbids this exporter answering one. So the menu currently plays a sum of three things where the census predicts two. What settles it: whether that leading region carries music. Raised with the Decoder 2026-08-29. | -| P3/P5 โ€” the title plate | ~~does the idle title show `PRESS โ’ถ`~~ | Q2 | โœ… **answered 2026-08-29, NOT ON `main` YET, and it is the expensive answer.** Pointer from the Decoder, unverified by me at the time of writing: branch `auto/no-disc-and-menu-captures` at **`fb536df`**, `docs/re/title-plate-delay-measured.md`, per-frame traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It confirms the ๐Ÿ”ด row below: `ScreenView` **does** have to draw two builds at once, and `--boot`'s plate-free end state is wrong. โš ๏ธ The Decoder flags a wrong way to read the number โ€” time it from the frame build 4 *stops animating*, not from the frame it first appears; the two runs differ by half a second on the latter and 6 ms on the former. **Read the file, not this row.** Not taken at P6; this is the next iteration's work. | +| P3 โ€” the plate's ONSET | **does the plate become visible 2.13 s after settle, or does its GROUP START then?** | Q2 | ๐Ÿ”ด **new, and it is a disagreement between two of the Decoder's own numbers.** `docs/re/title-plate-delay-measured.md` (`auto/no-disc-and-menu-captures` at `fb536df`, **not on `main`**) measures the plate *becoming visible* at settle + **2.13 s**, and instructs the port to *"wait 2.13 s, composite build 2"*. Those are 3.97 s apart, because build 2 has a group of its own and the port plays it: `press_start`'s `ptbtn00` is alpha `0x00` at t=214, `0x00` at t=236 (it slides y 560โ†’550 invisible), `0xff` at t=238 โ€” **3.967 s** from group start to full alpha at 60 u/s. Neither landmark reconciles them: both groups starting together puts full alpha 0.38 s *before* build 4 settles (`title` settles at t=261 = 4.350 s); build 2 starting at settle puts it at settle + 3.97 s. Landing on settle + 2.13 s needs build 2's group to start 2.51 s after build 4's, which is neither. **The port ships the instruction, not its own arithmetic**, prints the discrepancy on every boot, and is not choosing. First thing to check, and it is about the instrument rather than the game: is *"title settled"* (the glyph counter first reading 154) the same instant as the port's last-element settle? Raised 2026-08-29. | +| ~~P3/P5 โ€” the title plate~~ | ~~does the idle title show `PRESS โ’ถ`~~ | Q2 | โœ… **answered and TAKEN at this iteration.** `auto/no-disc-and-menu-captures` at `fb536df`, `docs/re/title-plate-delay-measured.md`, traces in `docs/re/data/plate-timing-run{1,2}.tsv`. It is the third case: build 4 alone, then the plate composited over it. โš ๏ธ The delay is timed from where build 4 **stops animating**, not from where it first appears โ€” measured the other way the two runs differ by 0.48 s against 6 ms. `ScreenView` now draws two builds at once, as a second `ScreenView` in the same `SubViewport` rather than a subordinate screen inside one. The onset question above is what is left. | + +| P3 โ€” the plate's PULSE | **what period does the plate pulse at, and which group boundary repeats?** | Q2 | โ” **open, and the port draws no pulse.** The same finding measures the running plate pulsing continuously โ€” peak/trough intervals 2.12 / 2.19 / 2.34 / 2.31 s, mean **2.24 s**. `press_start`'s group encodes exactly one cycle (`0x00` โ†’ `0xff` at t=238โ€“244 โ†’ `0x00` untimed), and **no reading of it produces 2.24 s**: the whole group is 268 units = 4.47 s, and from its first keyframe 54 units = 0.90 s. So repeating the group would be inventing a period. The port holds the plate at its settle (t=238, alpha `0xff`), which is what it does on every other screen and what the static oracle capture `live-title-press-a.png` shows. What settles it: which instant the group restarts from. | +| P5 focus ring โ€” implementation | ~~the ring's spin period~~ | Q1 | โœ… answered (2.177 s, `4fa3099`) and **still not implemented** โ€” the port draws 0ยฐ, a pose the corpus says the game never shows. Not blocked on anybody; it is queued behind P3's plate this iteration. | ## Answered since this file was last written โ€” no longer blocking @@ -236,9 +240,7 @@ right for an entry animation (the main menu settles at t=80, 1.33 s) and is proven on the screen P2 gates. The **title** runs to t=269 โ€” 4.48 s โ€” and there the port's settled pose and the decoders' `rest` disagree badly (max 142/255). -What is known: no element's alpha reverses direction anywhere in this export, so -nothing pulses, which removes the obvious reason to expect a loop without -disproving one. What would settle it: **a capture of build 4 alone**. The one +๐Ÿ”ด **REFUTED 2026-08-29, by the port, against its own export.** This paragraph said: *"no element's alpha reverses direction anywhere in this export, so nothing pulses, which removes the obvious reason to expect a loop without disproving one."* `press_start`'s `ptbtn00` reverses: `fade_argb` is `0x00ffffff` at t=214 and t=236, `0xffffffff` at t=238 and t=244, and `0x00ffffff` again on the final untimed keyframe. It was in the export the whole time and the claim was never checked against it โ€” it was checked against the screens P2 happened to be looking at. The reason to expect a loop is back, and the running game pulses this exact element. What would settle it: **a capture of build 4 alone**. The one live title capture composites the `PRESS โ’ถ` plate (build 2) over it, so it cannot be diffed against the title by itself. diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index ca35bda1..ca90dfed 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -1591,3 +1591,162 @@ dispatch on the extension, so a temp name must preserve it.** The second is the more dangerous shape, and it is the one this project has already warned itself about: a confident line of output pointing at nothing. The rename's return is now checked and the failure is loud. + +## P3, reopened โ€” the boot title was missing the `PRESS โ’ถ` plate, 2026-08-29 + +P3 passed its gate with a boot that ended on build 4 alone. `BLOCKED.md` carried +that as ๐Ÿ”ด from the start: both states were captured, so the art was never the +question โ€” the *sequence* was, and it is behavioural, so the port had no oracle +for it. + +It is answered. `docs/re/title-plate-delay-measured.md` +(`auto/no-disc-and-menu-captures` at `fb536df`, **not on `main`** at the time of +writing) measures two independent boots: the title presents **without** the +plate, and the plate arrives **2.13 s** later, the two runs agreeing to 6 ms. + +### Two builds at once, as two `ScreenView`s + +`ScreenView` draws one screen. The obvious change was to teach it about a +subordinate overlay screen; the change made was to put a **second `ScreenView` +in the same `SubViewport`**, after the first. + +That is what "two builds at once" actually is. Each build has its own timeline, +its own textures and its own hold โ€” the plate's group runs independently of the +title's, which is the entire content of the finding โ€” and Node2D siblings already +paint in tree order. The alternative would have put an `if overlay` in every +method that walks elements, and would have expressed the same information less +directly. The export's `paint_order` still means what it always meant: an +ordering *within* a build. + +### The delay is timed from where build 4 stops animating + +Not from where the title first appears. This is the finding rather than a detail: +measured from first-draw the two oracle runs differ by **0.48 s**, because the +build-in itself ran 1.64 s and 2.13 s and the emulator's frame pacing during an +animation is not the game's clock. Measured from settle they differ by 6 ms. + +So `_boot_done` โ€” the moment the sequencer already had for "this screen has +reached its hold" โ€” is the landmark, and the overlay is due `after_settle_seconds` +later. A number taken from the wrong instant here looks exactly like a +measurement. + +### The overlay is attached to the BOOT STEP, not to the `title` screen + +What was measured is the boot title. Whether the plate is there when the title is +reached *again* โ€” by โ’ท from the main menu, or after the attract movie โ€” is not +measured, and hanging the overlay on the screen would quietly claim that it is. +So it lives on the boot step in `authored/flow.json`, and `_drop_overlay` takes +it away with the screen it belongs to. `BLOCKED.md` carries the gap. + +### Refutation โ€” the RE agent's instruction contradicts the RE agent's measurement + +**The claim under test**, quoted from the finding's *"What the port should +author"*: draw build 4, *"when build 4 has settled, wait **2.13 s**, composite +build 2 over it"*. + +**It does not reproduce the measurement it came from**, and the gap is 3.97 s. +Build 2 is not a static plate: it has a group, and this port plays groups. +`press_start` has one element, `ptbtn00`, and its `fade_argb` reads + +``` +t=214 0x00ffffff pos (383, 560) invisible +t=236 0x00ffffff pos (383, 550) still invisible, having slid 10 px up +t=238 0xffffffff full alpha +t=244 0xffffffff holds + โ€” 0x00ffffff the exit, untimed +``` + +At the measured 60 units/s that is **3.967 s** from the group's start to full +alpha. Compose the instruction with the group and the plate is first *visible* at +settle + 2.13 + 3.97 = **settle + 6.10 s**. What was measured โ€” the glyph counter +leaving its no-plate value of 154 โ€” is the plate becoming visible at **settle + +2.13 s**. + +Neither obvious reconciliation works: + +| reading | plate visible at | measured | +|---|---|---| +| both groups start together | 3.97 s (build 4 settles at **4.350 s**) โ€” i.e. 0.38 s *before* settle | settle + 2.13 s | +| build 2's group starts at settle | settle + 3.97 s | settle + 2.13 s | +| build 2's group starts at settle + 2.13 s (the instruction) | settle + 6.10 s | settle + 2.13 s | + +To land on the measurement, build 2's group has to start **2.51 s** after build +4's, which is not a landmark of anything. + +**Verdict: the instruction is refuted as written; the measurement is untouched.** +The measurement is an observation of the running game and this port has no +standing to doubt it. What is refuted is the step that turns it into an +authoring rule, and that step is an interpretation. + +**So the port ships the instruction, not its own arithmetic**, prints the +discrepancy on every boot, and files the row. This is the same call as the BGM +sub-waves and for the same reason: reconciling two of the RE agent's numbers is +a decoding question, and a port that quietly picks the one that looks right +destroys the evidence โ€” a corrected boot looks exactly like a correct one. + +The first thing to check is about the instrument rather than the game: is *"title +settled"*, the glyph counter first reading 154, the same instant as the port's +last-element settle (t=261, 4.350 s into the group)? If that landmark is earlier, +the gap closes with nothing else moving. + +### Refuting the port's own claim: things in this export DO pulse + +`BLOCKED.md` has carried this since P2, under the port's own raised question +about whether groups loop: + +> no element's alpha reverses direction anywhere in this export, so nothing +> pulses, which removes the obvious reason to expect a loop without disproving +> one. + +**`ptbtn00` reverses.** `0x00` โ†’ `0xff` โ†’ `0x00`, in the table above, in the +export, the whole time. The claim was never checked against `press_start`; it was +checked against the screens P2 happened to be animating. The RE agent has now +measured the running game pulsing this exact element at a mean 2.24 s. + +So the reason to expect a loop is back โ€” and the port still does not draw one, +because **no reading of this group produces 2.24 s**: the whole group is 268 +units = 4.47 s, and from its first keyframe 54 units = 0.90 s. The plate is drawn +arriving and then holding at its settle (t=238, alpha `0xff`), which is what +every other screen does and what the static oracle capture +`live-title-press-a.png` shows. Which instant a repeat restarts from is filed, +not guessed. + +### `--boot --capture=` โ€” one frame instead of six hundred + +The boot had no artifact of its own except `--film`, a PNG every 0.25 s for the +whole 156 s run, to answer one question: is the plate on top of the title at the +end. `--capture` was a `--screen`-only flag taken in `_ready`, which for a boot +run is 150 s too early. It is now deferred to the end of the sequence when +`--boot` is given. + +### P3 gate โ€” the boot ends on two builds + +``` +godot --path port -- --boot --capture=โ€ฆ/p3-plate.png + โ†’ boot sequence complete after 155.86 s, holding on title + overlay press_start due at 157.99 s (+2.13 s after settle) + overlay press_start raised at 158.00 s, 1 element(s), settles at t=238 + โš  plate raised at settle+2.13 s but its own group reaches full alpha 3.97 s + later, so it is first VISIBLE at settle+6.10 s -- the measurement is + settle+2.13 s. + boot ends on title + press_start at 161.99 s + drew 16: ptbase2, ptloop01, โ€ฆ, ptcopyright + overlay press_start at t = 261.00 units, drew 1: ptbtn00 +``` + +The PNG shows the title logo with **`PRESS โ’ถ BUTTON`** under it โ€” build 4 and +build 2 in one frame, which this port had never drawn. + +Two things the run made obvious and that are now fixed: + +* **The capture reported only the base build's elements.** The first composited + capture printed `drew 16` and no mention of the plate, which reads as though + the overlay had not drawn at all. The overlay gets its own line; folding its + elements into the first list would have reported a screen that does not exist. +* **`--screen= --overlay=`** raises the same composite immediately, by the + same code path, with no delay. It exists because the only other way to see two + builds was a 156 s boot of which 137 s is the intro movie โ€” and under Xvfb's + software Theora decode that is several minutes to answer "is the plate on top + of the title". It applies **no** delay: the delay is a measurement and lives in + `authored/flow.json`. The boot-mode narration is suppressed there, because a + log line that describes a sequence it is not running is worse than no log line. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index 07556af8..6593d58f 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -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=` 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 := ""