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 |
|
| `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 |
|
| `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 |
|
| `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) |
|
| `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) |
|
| `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) |
|
| `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
|
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.
|
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,
|
`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;
|
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.
|
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
|
- **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.
|
- **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)
|
### 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.
|
- **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
|
- **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
|
- **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.
|
- **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)
|
### 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.
|
- **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
|
- **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
|
- **grounding:** source=kelebek, confidence=low
|
||||||
- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table.
|
- **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)
|
### 0xbb `u0041D250` (u0041D250, argc 1)
|
||||||
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
|
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **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.
|
**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
|
## Later Phase B breadth
|
||||||
|
|
||||||
**INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static
|
**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**. |
|
| `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 <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. |
|
| `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`. |
|
| `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**. |
|
| `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). | |
|
| `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). | |
|
||||||
|
|||||||
@@ -352,6 +352,13 @@ sealed class AudioTraceHost : IHost
|
|||||||
var entry = _res.ResolveBgm(id);
|
var entry = _res.ResolveBgm(id);
|
||||||
Events.Add(("play-bgm", id, entry != null ? $"{entry.Archive} {entry.Name}" : $"BGM{id:D3}.OGG <missing>"));
|
Events.Add(("play-bgm", id, entry != null ? $"{entry.Archive} {entry.Name}" : $"BGM{id:D3}.OGG <missing>"));
|
||||||
}
|
}
|
||||||
|
public void RestartBgm(long id, int startMode)
|
||||||
|
{
|
||||||
|
var entry = _res.ResolveBgm(id);
|
||||||
|
string resolved = entry != null ? $"{entry.Archive} {entry.Name}" : $"BGM{id:D3}.OGG <missing>";
|
||||||
|
Events.Add((startMode == 0 ? "restart-bgm-once" : "restart-bgm-loop", id, resolved));
|
||||||
|
}
|
||||||
|
public void StopBgm() => Events.Add(("stop-bgm", 0, ""));
|
||||||
public void PlayVoice(long id) // voice: universal packed catalog id
|
public void PlayVoice(long id) // voice: universal packed catalog id
|
||||||
{
|
{
|
||||||
var e = _res.ResolveVoice(id);
|
var e = _res.ResolveVoice(id);
|
||||||
|
|||||||
@@ -30,6 +30,41 @@ public class SfxOpsTests
|
|||||||
Assert.Equal("exit", vm.HaltReason);
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
Assert.Equal([24L, 25L, 25L], host.BgmTracks);
|
Assert.Equal([24L, 25L, 25L], host.BgmTracks);
|
||||||
Assert.Equal((0, 0L), Assert.Single(host.BgmFades));
|
Assert.Equal((0, 0L), Assert.Single(host.BgmFades));
|
||||||
|
Assert.Equal(1, host.BgmStops);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ForcedBgmTrioRestartsWithNativeModesReusesCurrentAndStops()
|
||||||
|
{
|
||||||
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
const int currentAfterStop = 0x100;
|
||||||
|
const int currentAfterRestart = 0x101;
|
||||||
|
var script = ScriptAssembler.Assemble(table, "BGM-FORCE",
|
||||||
|
new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0xbf, new[] { new Operand(0, 24) }),
|
||||||
|
(0xb7, new[] { new Operand(0, 24) }), // same track still restarts
|
||||||
|
(0xb7, new[] { new Operand(0, 0) }), // zero aliases retained track
|
||||||
|
(0xb9, new[] { new Operand(0, 0) }), // retained track, one-shot mode
|
||||||
|
(0xb9, new[] { new Operand(0, 25) }), // nonzero replaces retained track
|
||||||
|
(0xb8, Array.Empty<Operand>()),
|
||||||
|
(0xc0, new[] { new Operand(3, currentAfterStop) }),
|
||||||
|
(0xb7, new[] { new Operand(0, 0) }), // no retained track: idempotent stop
|
||||||
|
(0xbf, new[] { new Operand(0, 25) }),
|
||||||
|
(0xc0, new[] { new Operand(3, currentAfterRestart) }),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
}, Array.Empty<string>());
|
||||||
|
var host = new RecordingHost();
|
||||||
|
var vm = new VirtualMachine(script, table, host);
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
|
Assert.Equal([24L, 25L], host.BgmTracks);
|
||||||
|
Assert.Equal([(24L, 1), (24L, 1), (24L, 0), (25L, 0)], host.BgmRestartRequests);
|
||||||
|
Assert.Equal(2, host.BgmStops);
|
||||||
|
Assert.Equal(0, vm.Globals[currentAfterStop]);
|
||||||
|
Assert.Equal(25, vm.Globals[currentAfterRestart]);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new();
|
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new();
|
||||||
public readonly List<int> SfxReleases = new();
|
public readonly List<int> SfxReleases = new();
|
||||||
public readonly List<long> BgmTracks = new();
|
public readonly List<long> BgmTracks = new();
|
||||||
|
public readonly List<(long Track, int StartMode)> BgmRestartRequests = new();
|
||||||
|
public int BgmStops;
|
||||||
public readonly List<(int Target, long Duration)> BgmFades = new();
|
public readonly List<(int Target, long Duration)> BgmFades = new();
|
||||||
public readonly List<(int Category, int BasisPoints)> AudioVolumeChanges = new();
|
public readonly List<(int Category, int BasisPoints)> AudioVolumeChanges = new();
|
||||||
public readonly List<(int Category, bool Enabled)> AudioRouteChanges = new();
|
public readonly List<(int Category, bool Enabled)> AudioRouteChanges = new();
|
||||||
@@ -175,6 +177,8 @@ internal class RecordingHost : IHost
|
|||||||
=> TextureDraws.Add((slot, sx, sy, w, h, dx, dy));
|
=> TextureDraws.Add((slot, sx, sy, w, h, dx, dy));
|
||||||
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
||||||
public void PlayBgm(long id) => BgmTracks.Add(id);
|
public void PlayBgm(long id) => BgmTracks.Add(id);
|
||||||
|
public void RestartBgm(long id, int startMode) => BgmRestartRequests.Add((id, startMode));
|
||||||
|
public void StopBgm() => BgmStops++;
|
||||||
public void PlayVoice(long id) => Voices.Add(id);
|
public void PlayVoice(long id) => Voices.Add(id);
|
||||||
public void PlayVoice(long id, int playbackVariant)
|
public void PlayVoice(long id, int playbackVariant)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ public interface IHost
|
|||||||
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
|
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
|
||||||
(int Width, int Height) GetTextureSize(int slot);
|
(int Width, int Height) GetTextureSize(int slot);
|
||||||
void PlayBgm(long id);
|
void PlayBgm(long id);
|
||||||
|
// Ordinary op 0xbf is VM-filtered so reasserting the current track is idempotent. Ops
|
||||||
|
// 0xb7/0xb9 deliberately bypass that guard and restart with logical loop/one-shot mode.
|
||||||
|
void RestartBgm(long id, int startMode) => PlayBgm(id);
|
||||||
|
// Op 0xb8 releases the current source rather than merely applying a zero-volume envelope.
|
||||||
|
void StopBgm() => FadeBgm(0, 0);
|
||||||
void PlayVoice(long id);
|
void PlayVoice(long id);
|
||||||
// Native voice playback retains a second start argument: ordinary dialogue passes 0,
|
// Native voice playback retains a second start argument: ordinary dialogue passes 0,
|
||||||
// while History replay (0x1bd) passes 1. Existing non-audio hosts may ignore it.
|
// while History replay (0x1bd) passes 1. Existing non-audio hosts may ignore it.
|
||||||
|
|||||||
@@ -786,6 +786,17 @@ public sealed class VirtualMachine
|
|||||||
_host.ResetSceneContext();
|
_host.ResetSceneContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RestartBgm(long requestedTrackId, int startMode)
|
||||||
|
{
|
||||||
|
if (requestedTrackId != 0)
|
||||||
|
_currentBgmTrackId = requestedTrackId;
|
||||||
|
|
||||||
|
if (_currentBgmTrackId == 0)
|
||||||
|
_host.StopBgm();
|
||||||
|
else
|
||||||
|
_host.RestartBgm(_currentBgmTrackId, startMode);
|
||||||
|
}
|
||||||
|
|
||||||
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
|
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
|
||||||
{
|
{
|
||||||
ExecFrame? previousInteractiveFrame;
|
ExecFrame? previousInteractiveFrame;
|
||||||
@@ -2421,6 +2432,16 @@ public sealed class VirtualMachine
|
|||||||
}
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
|
case "restart-bgm-loop": // 0xb7: force start, including current-track/zero alias
|
||||||
|
RestartBgm(Read(a[0]), 1);
|
||||||
|
return pc + 1;
|
||||||
|
case "stop-bgm": // 0xb8: release source and clear retained track
|
||||||
|
_currentBgmTrackId = 0;
|
||||||
|
_host.StopBgm();
|
||||||
|
return pc + 1;
|
||||||
|
case "restart-bgm-once": // 0xb9: force start without decoder rewind at EOF
|
||||||
|
RestartBgm(Read(a[0]), 0);
|
||||||
|
return pc + 1;
|
||||||
case "get-current-bgm-track":
|
case "get-current-bgm-track":
|
||||||
Write(a[0], _currentBgmTrackId);
|
Write(a[0], _currentBgmTrackId);
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
@@ -2464,7 +2485,11 @@ public sealed class VirtualMachine
|
|||||||
{
|
{
|
||||||
int targetPercent = (int)Read(a[0]);
|
int targetPercent = (int)Read(a[0]);
|
||||||
_host.FadeBgm(targetPercent, Read(a[1]));
|
_host.FadeBgm(targetPercent, Read(a[1]));
|
||||||
if (targetPercent == 0) _currentBgmTrackId = 0;
|
if (targetPercent == 0)
|
||||||
|
{
|
||||||
|
_currentBgmTrackId = 0;
|
||||||
|
_host.StopBgm();
|
||||||
|
}
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
}
|
}
|
||||||
case "get-audio-volume": // 0xc5 (category)(out basis points)
|
case "get-audio-volume": // 0xc5 (category)(out basis points)
|
||||||
|
|||||||
@@ -1584,11 +1584,33 @@ public sealed class GodotAdvHost : IHost
|
|||||||
// ---- audio ops (OGG plays natively in Godot) ----
|
// ---- audio ops (OGG plays natively in Godot) ----
|
||||||
// BGM is addressed by direct name (BGM{id:D3}.OGG); voice uses the universal packed catalog.
|
// BGM is addressed by direct name (BGM{id:D3}.OGG); voice uses the universal packed catalog.
|
||||||
public void PlayBgm(long id)
|
public void PlayBgm(long id)
|
||||||
|
=> DispatchBgm(id, 1, false);
|
||||||
|
|
||||||
|
public void RestartBgm(long id, int startMode)
|
||||||
|
=> DispatchBgm(id, startMode, true);
|
||||||
|
|
||||||
|
private void DispatchBgm(long id, int startMode, bool forceRestart)
|
||||||
{
|
{
|
||||||
var asset = _res.ResolveBgm(id);
|
var asset = _res.ResolveBgm(id);
|
||||||
var audio = asset != null ? LoadAudio(asset) : null;
|
var audio = asset != null ? LoadAudio(asset) : null;
|
||||||
_timeline?.Event("bgm", new() { ["id"] = id, ["file"] = audio?.Name });
|
_timeline?.Event("bgm", new()
|
||||||
if (audio != null) _main.CallDeferred("PlayBgm", audio.Bytes, audio.Name);
|
{
|
||||||
|
["id"] = id,
|
||||||
|
["file"] = audio?.Name,
|
||||||
|
["start_mode"] = startMode,
|
||||||
|
["force_restart"] = forceRestart,
|
||||||
|
});
|
||||||
|
if (audio == null) return;
|
||||||
|
if (forceRestart)
|
||||||
|
_main.CallDeferred("RestartBgm", audio.Bytes, audio.Name, startMode);
|
||||||
|
else
|
||||||
|
_main.CallDeferred("PlayBgm", audio.Bytes, audio.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopBgm()
|
||||||
|
{
|
||||||
|
_timeline?.Event("bgm-stop", new());
|
||||||
|
_main.CallDeferred("StopBgm");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PlayVoice(long id) => PlayVoice(id, 0);
|
public void PlayVoice(long id) => PlayVoice(id, 0);
|
||||||
|
|||||||
@@ -1829,12 +1829,19 @@ public partial class Main : Godot.Control
|
|||||||
px[i + 3] = 0;
|
px[i + 3] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode VFS-owned bytes in Godot. BGM loops; voice plays once, cutting off any prior line.
|
// Decode VFS-owned bytes in Godot. Ordinary BGM loops; forced starts retain AGE's
|
||||||
|
// loop/one-shot mode. Voice plays once, cutting off any prior line.
|
||||||
public void PlayBgm(byte[] oggBytes, string assetName)
|
public void PlayBgm(byte[] oggBytes, string assetName)
|
||||||
|
=> StartBgm(oggBytes, assetName, 1);
|
||||||
|
|
||||||
|
public void RestartBgm(byte[] oggBytes, string assetName, int startMode)
|
||||||
|
=> StartBgm(oggBytes, assetName, startMode);
|
||||||
|
|
||||||
|
private void StartBgm(byte[] oggBytes, string assetName, int startMode)
|
||||||
{
|
{
|
||||||
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
|
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
|
||||||
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
|
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
|
||||||
stream.Loop = true;
|
stream.Loop = startMode != 0;
|
||||||
// FadeBgm is marshalled from the VM thread and starts on the next Godot frame, while the VM's
|
// FadeBgm is marshalled from the VM thread and starts on the next Godot frame, while the VM's
|
||||||
// blocking deadline begins immediately. Scene startup can consequently request the replacement
|
// blocking deadline begins immediately. Scene startup can consequently request the replacement
|
||||||
// track just before the old fade tween reaches zero. Do not let that orphaned tween mute the new
|
// track just before the old fade tween reaches zero. Do not let that orphaned tween mute the new
|
||||||
@@ -1845,6 +1852,15 @@ public partial class Main : Godot.Control
|
|||||||
_bgm.Play();
|
_bgm.Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void StopBgm()
|
||||||
|
{
|
||||||
|
RestoreVoiceBgmDuck();
|
||||||
|
CancelBgmFade();
|
||||||
|
_bgm.Stop();
|
||||||
|
_bgm.Stream = null;
|
||||||
|
_bgm.VolumeDb = 0;
|
||||||
|
}
|
||||||
|
|
||||||
private void PersistAudioMixerSettings(AudioMixerSettingsSnapshot snapshot)
|
private void PersistAudioMixerSettings(AudioMixerSettingsSnapshot snapshot)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -2268,8 +2284,15 @@ public partial class Main : Godot.Control
|
|||||||
bool bgmReplacementCancelsFade = bgmFadeStarted
|
bool bgmReplacementCancelsFade = bgmFadeStarted
|
||||||
&& _bgmFadeTween == null
|
&& _bgmFadeTween == null
|
||||||
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
||||||
_bgm.Stop();
|
RestartBgm(bgm.Bytes, bgm.Name, 0);
|
||||||
_bgm.Stream = null;
|
bool bgmOneShotModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == false;
|
||||||
|
RestartBgm(bgm.Bytes, bgm.Name, 1);
|
||||||
|
bool bgmLoopModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == true;
|
||||||
|
StopBgm();
|
||||||
|
bool bgmStopReleaseOk = _bgm.Stream == null
|
||||||
|
&& !_bgm.Playing
|
||||||
|
&& _bgmFadeTween == null
|
||||||
|
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
|
||||||
AdvTextEffectTheme mode1 = ResolveAdvTextEffectTheme(AdvTextStyle.Default with
|
AdvTextEffectTheme mode1 = ResolveAdvTextEffectTheme(AdvTextStyle.Default with
|
||||||
{
|
{
|
||||||
RenderMode = 1,
|
RenderMode = 1,
|
||||||
@@ -2337,18 +2360,22 @@ public partial class Main : Godot.Control
|
|||||||
new Sys4LogicalCanvas(_screenWidth, _screenHeight));
|
new Sys4LogicalCanvas(_screenWidth, _screenHeight));
|
||||||
textEffectSmoke.QueueFree();
|
textEffectSmoke.QueueFree();
|
||||||
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
|
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
|
||||||
&& bgmReplacementCancelsFade && textEffectModesOk && fontCalibrationOk
|
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
|
||||||
|
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
|
||||||
&& logicalCanvasOk;
|
&& logicalCanvasOk;
|
||||||
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
|
||||||
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
|
||||||
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
|
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
|
||||||
$"bgm-fade-replacement=ok; text-effect-modes=ok; font-calibration=ok; " +
|
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
|
||||||
|
$"text-effect-modes=ok; font-calibration=ok; " +
|
||||||
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
|
||||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||||
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
|
||||||
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
|
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
|
||||||
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " +
|
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " +
|
||||||
$"bgm-fade-replacement={bgmReplacementCancelsFade}; " +
|
$"bgm-fade-replacement={bgmReplacementCancelsFade}; " +
|
||||||
|
$"bgm-one-shot={bgmOneShotModeOk}; bgm-loop={bgmLoopModeOk}; " +
|
||||||
|
$"bgm-stop-release={bgmStopReleaseOk}; " +
|
||||||
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
|
||||||
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
|
||||||
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
|
||||||
|
|||||||
@@ -563,6 +563,11 @@ name = "current_bgm_track_id"
|
|||||||
type = "int"
|
type = "int"
|
||||||
note = "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"
|
note = "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"
|
||||||
[[field]]
|
[[field]]
|
||||||
|
offset = 0xa0b8c
|
||||||
|
name = "current_bgm_start_mode"
|
||||||
|
type = "int"
|
||||||
|
note = "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"
|
||||||
|
[[field]]
|
||||||
offset = 0xa0cc0
|
offset = 0xa0cc0
|
||||||
name = "screen_w"
|
name = "screen_w"
|
||||||
type = "int"
|
type = "int"
|
||||||
|
|||||||
@@ -1804,60 +1804,63 @@ observed_types = ["imm"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0xb7
|
op = 0xb7
|
||||||
label = "u0041D0E0"
|
label = "restart-bgm-loop"
|
||||||
argc = 1
|
argc = 1
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u0041D0E0"
|
name = "restart-bgm-loop"
|
||||||
category = "unknown"
|
category = "audio"
|
||||||
summary = ""
|
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."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
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."
|
||||||
|
details = "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."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "track_or_zero"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0xb8
|
op = 0xb8
|
||||||
label = "u00415520"
|
label = "stop-bgm"
|
||||||
argc = 0
|
argc = 0
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u00415520"
|
name = "stop-bgm"
|
||||||
category = "unknown"
|
category = "audio"
|
||||||
summary = ""
|
summary = "Stop and release the active BGM source and clear the retained current track. Any active BGM fade is completed/cancelled first."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
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."
|
||||||
|
details = "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."
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0xb9
|
op = 0xb9
|
||||||
label = "u0041D140"
|
label = "restart-bgm-once"
|
||||||
argc = 1
|
argc = 1
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u0041D140"
|
name = "restart-bgm-once"
|
||||||
category = "unknown"
|
category = "audio"
|
||||||
summary = ""
|
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."
|
||||||
noop_headless = false
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = ""
|
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."
|
||||||
|
details = "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."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "track_or_zero"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
@@ -1938,6 +1941,7 @@ source = "investigation"
|
|||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
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."
|
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."
|
||||||
|
details = "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."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user