Implement forced BGM opcodes
This commit is contained in:
@@ -117,6 +117,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
| `0x9f274` | `mounted_aai_catalogs` | `void*` | base of 256-entry selector-keyed AAI catalog-pointer table inside the embedded FileDB; op 0x143 scans slots 1..255 from +0x9f278 |
|
||||
| `0x9f278` | `mounted_aai_catalog_selector_1` | `void*` | selector-one cell and op 0x143 scan start; subsequent dwords are selector 2..255 |
|
||||
| `0xa0b84` | `current_bgm_track_id` | `int` | direct-name BGM track id retained by the music facade; op 0xbf starts/replaces it, ops 0xc0/0xc3 get/set it, and numbered-save layouts restore it |
|
||||
| `0xa0b8c` | `current_bgm_start_mode` | `int` | logical playback mode retained by the music facade and forwarded to its backend: zero stops at decoder EOF, nonzero rewinds; ordinary op 0xbf and forced op 0xb7 use one, forced op 0xb9 uses zero |
|
||||
| `0xa0cc0` | `screen_w` | `int` | logical screen width; constructor/default registry uses 640, then the SYS4INI SCREENX setting overrides it (Himegari 800) |
|
||||
| `0xa0cc4` | `screen_h` | `int` | logical screen height; constructor/default registry uses 480, then the SYS4INI SCREENY setting overrides it (Himegari 600) |
|
||||
| `0xa0cc8` | `screen_bpp` | `int` | screen bpp (8) |
|
||||
|
||||
@@ -2826,6 +2826,39 @@ move and repeatedly throughout enemy turns. The VM now treats a current-track re
|
||||
A different id still starts a new track, and `0xc2` target zero clears the retained id so a later request
|
||||
can start that track normally.
|
||||
|
||||
**Forced BGM lifecycle siblings (mapped 2026-07-29).** The remaining `0xb7`/`0xb8`/`0xb9` gap is one
|
||||
coherent BGM tranche:
|
||||
|
||||
- `op_0xb7_force_play_bgm_loop@0x4202d0` calls `bgm_force_play_track@0x464a90` with start mode one.
|
||||
A nonzero operand replaces and starts that track; zero restarts the retained current track. Unlike
|
||||
ordinary `0xbf`, the force worker has no same-track early return. CALLBACK_LOAD uses zero after a
|
||||
numbered-load callback, and CONFIG uses BGM029 as its music-preview sample.
|
||||
- `op_0xb8_stop_bgm@0x416ba0` calls `bgm_stop_release@0x464690`, clearing the retained track and stopping
|
||||
the active backend. CONFIG uses it when leaving the preview path; MMODE uses it before constructing the
|
||||
music-room screen.
|
||||
- `op_0xb9_force_play_bgm_once@0x420330` calls the same force worker with start mode zero. Its sole
|
||||
Himegari site starts BGM025 as STAGECLEAR's one-shot fanfare.
|
||||
|
||||
All three first complete and cancel an active `0xc2` fade by clearing run-state bit `0x200` and calling
|
||||
`bgm_fade_tick(...,100)`. Both start variants store their mode at
|
||||
`EngineCtx.current_bgm_start_mode` (`ctx+0xa0b8c`) before invoking the backend. Himegari's OGG backend
|
||||
forwards that value through `FUN_00482f00` to `sound_buffer_start`; the already-decoded
|
||||
`sound_stream_fill_quarter@0x483b70` rewinds at EOF only when the mode is nonzero. Thus the distinction is
|
||||
logical loop versus one-shot, not a mixer flag. A compatible implementation must extend the existing BGM
|
||||
host seam with explicit restart and loop ownership, preserve `0xbf`'s current idempotence, treat operand
|
||||
zero through the VM's retained track, and make `0xb8` clear that retained state.
|
||||
|
||||
The port now implements that split directly. `IHost.RestartBgm(track,startMode)` is distinct from ordinary
|
||||
`PlayBgm`, so `0xbf` remains same-track-idempotent while `0xb7`/`0xb9` always replace the stream. The VM
|
||||
resolves operand zero through `_currentBgmTrackId`; zero with no retained track calls the same idempotent
|
||||
stop path as native. `StopBgm` clears the VM track and makes Godot cancel voice ducking and any deferred
|
||||
fade, stop the player, release its stream, and restore neutral gain. Godot sets `AudioStreamOggVorbis.Loop`
|
||||
from the native start mode. The already-mapped `0xc2` target-zero endpoint now also calls this seam after
|
||||
its blocking fade, replacing the former silent-but-still-bound Godot stream with native release behavior.
|
||||
A focused regression covers same-track restart, zero aliasing, loop/one-shot selection, track replacement,
|
||||
stop/clear, fade-to-zero release, and restart after stop; the threaded self-test exercises both loop modes
|
||||
and real stop/release.
|
||||
|
||||
`0xc2` is BGM rather than SFX: `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`, arms the service timer,
|
||||
and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates current to target percent;
|
||||
durations at least 1000 ms take 100 steps, shorter durations take 10, and target zero releases the source.
|
||||
|
||||
@@ -201,6 +201,27 @@ The requested CP932 face is copied into the primary LOGFONT lfFaceName and AGE a
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra op 0xb6 handler 0x420250 -> sfx_channel_release@0x482600 -> sound_buffer_destroy@0x4831a0, which releases the per-channel object under its critical section and clears the slot. Native trace captured SC0000's channels 0..9 release sweep in consecutive calls.
|
||||
|
||||
### 0xb7 `restart-bgm-loop` (restart-bgm-loop, argc 1)
|
||||
- **summary:** (track_or_zero) — force-start looping BGM even when the requested nonzero track already matches the retained current track. A zero operand restarts the retained track; when no track is retained it resolves to an idempotent stop. Any active BGM fade is completed/cancelled first.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0xb7_force_play_bgm_loop@0x4202d0 first clears run-state bit 0x200 and forces bgm_fade_tick(...,100) when a fade is active, then calls bgm_force_play_track@0x464a90 with operand 1 and logical start mode 1. Unlike ordinary bgm_play_track@0x464750 (op 0xbf), the force worker has no same-track early return. A nonzero operand replaces EngineCtx.current_bgm_track_id; zero reuses it, or releases when it is also zero. The OGG backend forwards mode 1 through sound_buffer_start; sound_stream_fill_quarter rewinds at EOF only for nonzero mode. Corpus: CALLBACK_LOAD uses zero to restart restored music; CONFIG uses BGM029 for its music preview.
|
||||
|
||||
Implemented: the VM resolves zero through its retained current track and calls the explicit forced-restart host seam with mode 1. Godot replaces the OGG stream with Loop=true and cancels any deferred fade; ordinary op 0xbf keeps its same-track idempotence.
|
||||
|
||||
### 0xb8 `stop-bgm` (stop-bgm, argc 0)
|
||||
- **summary:** Stop and release the active BGM source and clear the retained current track. Any active BGM fade is completed/cancelled first.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0xb8_stop_bgm@0x416ba0 clears run-state bit 0x200 and forces bgm_fade_tick(...,100) when a fade is active, then calls bgm_stop_release@0x464690. The worker clears music-facade current track/status fields and invokes the active backend's stop/release method. Corpus: CONFIG stops the BGM029 preview while closing; MMODE stops current music before building the music-room screen.
|
||||
|
||||
Implemented: the VM clears its retained track and calls StopBgm. Godot cancels voice ducking and any fade, stops playback, releases the stream, and restores neutral gain.
|
||||
|
||||
### 0xb9 `restart-bgm-once` (restart-bgm-once, argc 1)
|
||||
- **summary:** (track_or_zero) — force-start one-shot BGM even when the requested nonzero track already matches the retained current track. A zero operand restarts the retained track once; when no track is retained it resolves to an idempotent stop. Any active BGM fade is completed/cancelled first.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0xb9_force_play_bgm_once@0x420330 shares op 0xb7's fade-finalization and bgm_force_play_track@0x464a90 path but supplies logical start mode 0. The OGG backend forwards that mode through sound_buffer_start; sound_stream_fill_quarter pads/stops at decoder EOF instead of rewinding. Corpus: STAGECLEAR's sole site force-starts BGM025 as its one-shot clear fanfare.
|
||||
|
||||
Implemented: the VM shares op 0xb7's retained-track/zero handling but supplies mode 0. Godot replaces the OGG stream with Loop=false, so BGM025 naturally stops at EOF.
|
||||
|
||||
### 0xba `sfx-start-loop` (sfx-start-loop, argc 1)
|
||||
- **summary:** (channel) — start the already-loaded sound-effect channel with logical loop mode 1; the streaming decoder rewinds at EOF until the channel is replaced or released.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -220,6 +241,8 @@ The requested CP932 face is copied into the primary LOGFONT lfFaceName and AGE a
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms.
|
||||
|
||||
Implemented through the blocking host fade clock. Target zero clears the VM's retained track after the wait and invokes the shared BGM stop/release seam, so Godot does not retain a silent bound stream.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by universal packed SYS4INI/AAI id with native playback/history variant 0. While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Uses the same packed-id opener as textures/scripts/movies, unlike play-bgm's direct BGM{id:03d} naming.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -1729,18 +1752,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table.
|
||||
|
||||
### 0xb7 `u0041D0E0` (u0041D0E0, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xb8 `u00415520` (u00415520, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xb9 `u0041D140` (u0041D140, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0xbb `u0041D250` (u0041D250, argc 1)
|
||||
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -874,6 +874,41 @@ effectful inventory from 15 distinct opcodes / 22 instructions to 12 / 16.
|
||||
|
||||
**NEXT:** rerank the remaining 12 effectful opcode gaps and investigate the widest coherent slice.
|
||||
|
||||
**Remaining-gap rerank and forced-BGM investigation (2026-07-29):** a fresh scan of all 481 scripts
|
||||
confirms 12 effectful gaps totaling 16 instructions. `0xb7`, `0xb8`, `0x142`, and `0x24d` occur twice
|
||||
each; `0xb9`, `0x137`, `0x144`, `0x149`, `0x241`, `0x248`, `0x2c6`, and `0x2c8` are singletons. Raw
|
||||
frequency no longer identifies a subsystem, but native RE does: `0xb7`/`0xb8`/`0xb9` are the complete
|
||||
forced-BGM lifecycle around the already-implemented `0xbf`/`0xc2` path.
|
||||
|
||||
`0xb7(track_or_zero)` force-restarts looping playback, including the same retained track and the zero
|
||||
operand's "restart current" form; `0xb9(track_or_zero)` is the corresponding one-shot start; and `0xb8`
|
||||
stops/releases and clears the retained track. Each first completes/cancels a pending BGM fade. CONFIG's
|
||||
BGM029 preview needs the force-loop/stop pair, CALLBACK_LOAD uses zero to restart restored music, MMODE
|
||||
stops current music on entry, and STAGECLEAR uses the one-shot form for BGM025. The existing Godot music
|
||||
player and retained VM track provide the whole implementation seam; it needs an explicit loop/restart
|
||||
request while ordinary `0xbf` remains same-track-idempotent.
|
||||
|
||||
**NEXT:** implement and test `0xb7`/`0xb8`/`0xb9` together. The likely follow-up is INPUTNAME's CP932
|
||||
string/UI cluster (`0x144`, `0x2c6`, `0x2c8`), but its modal host contract should be finished only after
|
||||
the bounded BGM tranche lands.
|
||||
|
||||
**Forced-BGM lifecycle implemented (2026-07-29):** `0xb7` and `0xb9` now force-replace the active BGM
|
||||
stream with logical loop and one-shot mode respectively, including zero's native alias to the retained
|
||||
track; `0xb8` stops/releases and clears that track. Ordinary `0xbf` remains idempotent for same-track
|
||||
FIELD cleanup. Godot cancels deferred fades on replacement, applies the start mode through the OGG loop
|
||||
flag, and makes stop release the stream rather than leaving a silent source. The same stop seam now closes
|
||||
`0xc2` target zero after its blocking fade, matching native source release instead of retaining a muted
|
||||
Godot stream.
|
||||
|
||||
All five sites now dispatch, reducing the effectful-gap inventory from 12 opcodes / 16 instructions to
|
||||
9 / 11. A focused VM regression covers forced same-track restart, zero reuse, both modes, replacement,
|
||||
stop/clear, and restart after stop. All 483 engine tests, opcode/EngineCtx lint, the zero-warning Godot
|
||||
build, and the Himegari-targeted threaded self-test pass; the self-test now also validates OGG loop mode
|
||||
and true stop/release.
|
||||
|
||||
**NEXT:** investigate INPUTNAME's `0x144`/`0x2c6`/`0x2c8` cluster as one modal name-entry and CP932
|
||||
string slice.
|
||||
|
||||
## Later Phase B breadth
|
||||
|
||||
**INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static
|
||||
|
||||
@@ -255,7 +255,7 @@ root/call-script parsing). Generated asset/callscript JSON remains a tooling and
|
||||
| `run <file.BIN>` | Execute a script; print steps, show-text count, **call-script dispatch count**, the first 30 lines (each tagged with its source script), and the distinct source scripts. | `CaptureHost` (headless); **executes call-script**. |
|
||||
| `trace <out.json>` | Trace every SC/SP scene → offsets + halt + steps. **Provider-less** (call-script stubbed) = a base-ISA offset dump. | writes JSON. (Was the vm0 differential oracle; vm0 is retired from oracle duty — `TraceDiffTests` removed.) |
|
||||
| `trace <SCENE.BIN> [--boot] [--state <f>] [0xADDR=VAL…] --trace-json <out>` | ★ Emit the **full per-op executed-offset path** of one scene (not just show-text), filtered to the scene's own frame — the VM side of the differential offset-path oracle (`diff_optrace.py`). `--boot` runs the SYSTEM4 state prefix; **`--state <f>` loads a captured scene-entry snapshot** (`capture_global_writes.py`) = the engine's real pre-scene state; `0xADDR=VAL` hand-seeds. | `JsonOffsetTraceSink` (observe-only, parity held) → `{scene, offsets:[…]}` JSON. |
|
||||
| `audio <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `play-bgm`/`play-voice` in order + resolved catalog record. | optional seeds. provider-less (stub) for now. |
|
||||
| `audio <SCENE.BIN> [0xADDR=VAL…]` | Dump executed ordinary/forced/stopped BGM and `play-voice` in order + resolved catalog record; forced BGM events distinguish loop and one-shot mode. | optional seeds. provider-less (stub) for now. |
|
||||
| `gfx [--boot] <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `set-texture`/`get-texture-size`/`draw-texture` (resolved file + computed geometry) **plus the per-object gfx slots** — the headless geometry oracle. **`--boot`** runs SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` first (so INIT2's gfx handle array is present) and runs the target with call-script on; without it, seeds-only + provider-less. | gfx ops now execute against `GfxState`. |
|
||||
| `play [--boot] [--state <f>] [--save-state <f>] <SCENE.BIN…> [0xADDR=VAL…]` | ★ Cross-scene **state runner**: run a scene sequence carrying persistent globals. `--boot` first runs the 9 `*INIT` data scripts (real skill/item/unit/map/stage state). `--state`/`--save-state` load/persist a JSON snapshot. | `GameSession`; **executes call-script**. |
|
||||
| `sweep [--boot] [0xADDR=VAL…]` | Corpus-scale run. **With call-script execution on: 284/297 exit, 13 STEP-LIMIT** (input/state-gated ADV scenes spin headless once subroutine global-writes drive their loops — state divergence, not a bug; 0 depth-cap/unresolved). **With seeds = a story-state explorer**: reports which scenes' dialogue changes ±seed (e.g. form flag `0xa57=1` → 34/297 scenes). | |
|
||||
|
||||
Reference in New Issue
Block a user