diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index c2eaa164..fefc82e2 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -6220,3 +6220,67 @@ full-frame and **0.000 %** across the band. A general floor could not coexist wi either number. So the 0.301 % is specific to the attract band capture, and my 0.01–0.2 % rows are not sitting on a hidden floor. ⚠️ What that does *not* settle is why those two frames differ — still theirs, and still worth an answer. + +## Five authored values had no reader — including the one I asked for measurements into + +Applying the prior from the last six findings — *a rule or capability nothing +exercises turns out broken or inert when someone looks* — to `authored/` itself. +Grepping every authored key for a reader in the exporter or the runtime: + +| key | file | status | +|---|---|---| +| `dwell` | `flow.json` | 🔴 **no reader** — now wired | +| `ramp` | `timing.json` | no reader — now asserted | +| `left_right` | `flow.json` | no reader — now asserted | +| `input_during_transition` | `flow.json` | no reader — now asserted | +| `stems` | `audio.json` | no reader (`stems_why` is carried; the sum is hardcoded) | + +Everything else — `se`, `bgm`, `voice`, `boot`, `screens`, `navigation.wrap`, +`draw_leaf_for`, `loop_leaf_on_screens`, `keyframe_units_per_second`, +`black_hold_units`, `archives`, `also_export`, `presentation`, `loop_mode`, +`initial_focus`, `skippable`, `then_video`, `after_video` — is read. + +### 🔴 `dwell` is the one that mattered + +Its own text says *"when a capture times the real boot, the extra hold per screen +goes here."* **A number placed there did nothing.** Two iterations ago I asked +the Decoder for measurements destined for exactly that slot; had they arrived, +they would have been filed into a value with no reader and the boot would have +been unchanged, silently — and I would have reported the boot as matching. + +It is wired now, and **stays empty**. Nothing is authored into it: the splash +dwells are declared on the disc and measured to agree. Wiring the slot so that a +future number has an effect is the opposite of adopting one. + +⚠️ **I wired it to the wrong branch first, and it did nothing — silently.** Holding +longer after settle changes nothing, because the screen still leaves when +`exit_time() + black_hold` arrives and the extra hold is absorbed. A dwell has to +delay the **departure**. I found it only because I tested the control: ++120 units moved the transition 4.46 s → **6.43 s**, +1.97 s, with the video +following by the same amount. Reproducing the exact defect I was removing, inside +the fix for it, is the strongest argument I have for testing that a wire carries +current rather than that it exists. + +### The other three are asserted, not implemented + +`ramp`, `left_right` and `input_during_transition` describe behaviour the port +**hardcodes**. That is fine for a record and dangerous for a switch, and they are +written like switches — setting `left_right` to `"move"` would change nothing and +warn nobody. + +Rather than invent the missing implementations, `_check_authored_invariants` +**asserts the value the port was built against**, naming the file. Changing one +now produces an error instead of silence. + +That is precisely the distinction `left_right`'s own `why` claims to be making — +*"written out rather than left unhandled so that 'the game ignores it' and 'we +never wired it' are different lines of code"* — and which was not in fact being +made, because nothing read the value that was supposed to make it. + +✅ The validator is **called**, not merely defined. A validator nobody invokes is +the same defect it exists to catch, and this file now documents six other +instances of exactly that. + +Verified: clean boot with no invariant errors and unchanged timings; setting +`left_right: "move"` produces the error; all five MODDING rules still pass; the +oracle rows are unmoved. diff --git a/port/scripts/boot.gd b/port/scripts/boot.gd index 86f63c7d..3e820e7d 100644 --- a/port/scripts/boot.gd +++ b/port/scripts/boot.gd @@ -202,6 +202,9 @@ func _ready() -> void: # own logic on purpose -- see `looping_focus` there for the census that says # this cannot be a rule. _looping = timing.get("looping_focus_records", {}) + # Called, not merely defined. A validator nobody invokes is the same defect + # it exists to catch. + _check_authored_invariants(timing) # Which decoded rules apply where. See `authored/rendering.json`. var rendering: Variant = export_tree.authored("rendering.json") _draw_leaf_for = [] if rendering == null else rendering.get("draw_leaf_for", []) @@ -391,6 +394,25 @@ func _process(delta: float) -> void: # # `_advance` is CAUSED by the next screen arriving, never scheduled off a # timer, which is what the draw stream says the game does. + # `authored/flow.json` `dwell` is applied at the EXIT below, not here. + # + # 🔴 I wired it here first and it did nothing, silently -- which is the + # defect it exists to remove, reproduced while removing it. Holding longer + # after settle changes nothing, because the screen still leaves when + # `exit_time() + black_hold` arrives and the extra hold is absorbed. A dwell + # has to delay the DEPARTURE. + # + # It was read NOWHERE for eight milestones. The + # block's own text says "when a capture times the real boot, the extra hold + # per screen goes here" -- and a number placed there did nothing at all. Two + # iterations ago I asked the Decoder for measurements destined for that slot; + # had they arrived, they would have been filed into a value with no reader + # and the boot would have been unchanged, silently. + # + # It stays EMPTY. Nothing is authored into it, because the splash dwells are + # declared on the disc and measured to agree. This wires the slot so the day + # a number belongs there it has an effect, which is the opposite of adopting + # one now. if view.holding and view.time_units >= view.settle_time(): # The LAST screen in the sequence keeps holding. A screen plays itself # out because something is taking its place; nothing is taking the @@ -420,7 +442,8 @@ func _process(delta: float) -> void: _menu_enter(String(_sequence[_step].get("screen", "")), true) 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: + elif not view.holding and view.time_units >= view.exit_time() + _black_hold \ + + _dwell_for(String(_sequence[_step].get("screen", ""))): # 🔴 THE BLACK HOLD, which this port had never implemented. A transition # is a fade THROUGH black (HANDOFF Q7), and the pure-black plateau # between one screen leaving and the next arriving was measured at @@ -1165,3 +1188,45 @@ func _looping_for(screen_name: String) -> Dictionary: if parts.size() == 2 and parts[0] == screen_name: out[parts[1]] = _looping[key] return out + + +## Extra hold for one screen, in units, from `authored/flow.json` `dwell`. +## +## Zero unless a measurement is authored. A value here is an ADDITION to the +## screen's own declared group, not a replacement for it. +func _dwell_for(screen_name: String) -> float: + if _flow == null or not (_flow is Dictionary): + return 0.0 + var table: Variant = (_flow as Dictionary).get("dwell", {}) + if not (table is Dictionary): + return 0.0 + var v: Variant = (table as Dictionary).get(screen_name, 0.0) + return float(v) if (v is float or v is int) else 0.0 + + +## Check the authored values this port CANNOT act on, and fail loudly if one +## changes. +## +## `left_right`, `input_during_transition` and `ramp` are authored with reasons +## and read by nothing -- the behaviour they describe is hardcoded. That is +## defensible for a record and dangerous for a switch, and they are written like +## switches: someone setting `left_right` to "move" would change nothing and get +## no warning. +## +## So rather than invent the missing implementations, the port ASSERTS the value +## it was built against. Changing one now produces an error naming the file +## instead of silence, which is the distinction the `why` for `left_right` +## claims to be making -- "the game ignores it" and "we never wired it" are +## different lines of code -- and which was not actually being made. +func _check_authored_invariants(timing: Dictionary) -> void: + var nav: Variant = (_flow as Dictionary).get("navigation", {}) if _flow is Dictionary else {} + if nav is Dictionary: + var lr := String((nav as Dictionary).get("left_right", "nothing")) + if lr != "nothing": + push_error("authored/flow.json navigation.left_right is \"%s\"; this port implements only \"nothing\"" % lr) + var idt := String((nav as Dictionary).get("input_during_transition", "ignored")) + if idt != "ignored": + push_error("authored/flow.json navigation.input_during_transition is \"%s\"; this port implements only \"ignored\"" % idt) + var ramp := String(timing.get("ramp", "linear")) + if ramp != "linear": + push_error("authored/timing.json ramp is \"%s\"; ScreenView interpolates linearly and has no other mode" % ramp)