Implement ROOM fades and character voices

This commit is contained in:
gamer147
2026-07-21 00:47:29 -04:00
parent 2fa42b085a
commit cffc75c64d
18 changed files with 442 additions and 71 deletions

View File

@@ -2135,13 +2135,64 @@ animation contract. No boot seed, script-offset branch, or persistence state is
---
### ROOM character selection, profile voice defaults, and legacy screen crossfade — `0x60` / `0x6c` / `0x25` (2026-07-21)
ROOM's apparently connected missing greeting and instant menu presentation were three separate native
contracts. Opcode `0x60` is the random selector, `0x6c` establishes the default voice-setting array, and
`0x25` owns the blocking transition between two complete offscreen frames.
`op_0x60_handler@0x426970` calls the imported CRT `rand`, fetches operand 2, and writes
`rand() % bound` to operand 1. Bound zero first writes zero and then raises script error `0x10005`.
`ROOM.BIN@0x5` passes bound 4; the result selects four character resource sets. Results 0, 1, and 2 also
select six voice ids each (`0x3365..0x3376`), including their greeting/farewell pair. Result 3 deliberately
leaves those locals zero in the release script, so that presentation variant is silent by script design.
The voice-enabled default was being defeated earlier in boot. `op_0x6c_handler@0x426d90` resolves operand
1 as a writable integer address and fills operand-2 consecutive dwords with `EngineCtx::anti_tamper_b`, the
native encoded representation of logical zero. It is a zero-range operation, not the scalar
`copy-to-global` suggested by Kelebek's label. `INITCONFIG@0x30` therefore clears the 13-cell character
voice-setting table `G[0x2e49..0x2e55]`, after which `0x1a2` registers each cell with the shared profile
service. CONFIG indexes the same table for voice preview and its 0/1 enable/suppress choices. The old port
instead wrote the scalar count 13 into `G[0x2e49]`; ROOM treats a nonzero value in cell 0 as voice suppression and
skipped all greeting/farewell ids. Correct zero-range execution fixes the boot default without implementing
or choosing a persistence backend for `0x1a2`/`0x1a3`.
`op_0x25_handler@0x41ce00` starts the legacy transition manager at `EngineCtx+0x1c38`, sets run-state bit
8, and keeps the script parked until the transition completes. Its three operands are source surface,
target surface, and a timing argument. For arguments up to 64, the handler arms that many milliseconds per
tick and advances the 8-bit alpha by 16. Larger arguments use `argument/16` milliseconds and alpha step
1. The branch is expressed in decompilation as `((arg < 65) - 1 & 0xfffffff1) + 0x10`; evaluating both
outcomes is significant (`true -> 16`, `false -> 1`). `interval_timer_poll_elapsed_steps@0x44d080` returns any missed timer intervals, and
`screen_transition_tick@0x43a7a0` mode 4 composites source first and target over it at the accumulated alpha;
at `0x100` it commits the target. `screen_transition_begin@0x439da0` and
`screen_transition_finalize@0x4399c0` own the endpoints. Thus an argument 10 lasts about 160 ms, while 30
lasts about 480 ms; input can force the endpoint through the same transition-abort service. Manual ROOM
validation caught the initially inverted alpha-step branch.
ROOM explicitly constructs those full frames. It selects/clears surface 1 and calls `0x20c`, mutates the
retained room presentation, captures surface 2 through another `0x20c`, restores the backbuffer, and calls
`0x25(1,2,10)`. The same form fades its menu away at `0x856`; after the farewell and one-second hold,
`0x25(1,2,30)@0x8dc` fades to the frame prepared for TITLE return. The interactive port now snapshots
retained objects when `0x20c` targets an offscreen surface, composites the captured target over the captured
source on the shared frame clock, blocks the VM through the endpoint, and only then permits the following
surface release/root reload. Headless hosts retain their non-rendering no-op policy.
The same manual pass exposed a separate resource-addressing issue after `0x6c` was fixed: ROOM did execute
`play-voice`, but `GodotAdvHost` attempted only SC-section resolution. ROOM is a frontend script without an
SC section, and its voice operands `0x3365..0x3376` are universal raw SYS4INI indices (for example raw
`0x3365` is `EUA0016.OGG`). Voice resolution now matches the already-required texture rule: try the active
SC section first, then a type-checked raw-catalog fallback. SC0000's local `0x24 -> MAN999.OGG` mapping is
unchanged.
---
## Native walls backlog (targets for this loop)
- ~~**call-script dispatch**~~ — **SOLVED** (above): `call-script <id>` = raw SYS4INI file index.
- **decision→scene** — how `0x62ccf`/the decision selects the next `SCxxxx`. Now narrower: scenes load
via `call-script`/the same SYS4INI-index loader, so the open question is only where the decision
value is turned into a scene *id* (a caller of SCJUMP; re-aimed away from `u00428010`).
- **op `0x60`** (`u0041A270`) — the rand-like value gating 1732/1755 SCJUMP decisions.
- ~~**op `0x60`**~~ — **SOLVED**: CRT `rand() % bound`, including the zero-bound script-error path.
- ~~**gfx command-buffer**~~ — **DONE** (the `0x2120x21a` positioned-object subsystem = the rendering
drift): all 14 ops reversed + implemented against a host-side `GfxState`, and the missing INIT2 boot
state supplied via `--boot`. CGs render (screenshot-confirmed). See the op `0x215` finding + "The render