Fix repeated FIELD BGM restarts

This commit is contained in:
gamer147
2026-07-27 11:40:54 -04:00
parent 760e9663f4
commit a547fc4c11
5 changed files with 49 additions and 6 deletions

View File

@@ -2421,6 +2421,15 @@ callback if that channel is loaded again or released before its deadline.
Normal-speed windowed validation reached `wait-for-input@0x1a58` after 45.6 seconds without an audio stall; Normal-speed windowed validation reached `wait-for-input@0x1a58` after 45.6 seconds without an audio stall;
the user confirmed the opening effects were audible and sounded good. the user confirmed the opening effects were audible and sounded good.
FIELD deliberately reasserts `play-bgm(stage_bgm_id)` from its common post-action/event cleanup at
`0x884a`; ordinary unit movement reaches that cleanup once per acting unit. The BGM facade already retains
the selected direct-name track id for op `0xc0` and numbered-save state. An identical `0xbf` request must
therefore preserve the current stream and playback position rather than reopen the OGG. The former host path
decoded and assigned a fresh Godot stream on every request, resetting the stage music after every player
move and repeatedly throughout enemy turns. The VM now treats a current-track reassertion as idempotent.
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.
`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.

View File

@@ -163,9 +163,9 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que
- **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.
### 0xbf `play-bgm` (play-bgm, argc 1) ### 0xbf `play-bgm` (play-bgm, argc 1)
- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005. - **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005. Reasserting the facade's current track id is idempotent and preserves playback position; a different id starts the new track, while fade-to-zero clears the current id so a later request can start it again.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`. - **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry; the facade retains that id at EngineCtx.current_bgm_track_id for op 0xc0 and numbered saves. FIELD intentionally reasserts stage_bgm_id from its common post-action/event cleanup at 0x884a, reached after unit movement; restarting an identical host stream there resets the music once per unit during enemy turns. Op 0xc2 target zero releases the source and the VM clears the retained id. Diagnostic: `Age.Cli audio SC0000.BIN`.
### 0xc0 `get-current-bgm-track` (get-current-bgm-track, argc 1) ### 0xc0 `get-current-bgm-track` (get-current-bgm-track, argc 1)
- **summary:** (track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves. - **summary:** (track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves.

View File

@@ -7,6 +7,31 @@ using Xunit;
public class SfxOpsTests public class SfxOpsTests
{ {
[Fact]
public void ReassertingCurrentBgmPreservesPlaybackUntilTrackChangesOrStops()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "BGM",
new List<(int, Operand[])>
{
(0xbf, new[] { new Operand(0, 24) }),
(0xbf, new[] { new Operand(0, 24) }), // FIELD action cleanup: keep current playback position
(0xbf, new[] { new Operand(0, 25) }),
(0xbf, new[] { new Operand(0, 25) }),
(0xc2, new[] { new Operand(0, 0), new Operand(0, 0) }),
(0xbf, new[] { new Operand(0, 25) }), // fade-to-zero cleared the current track
(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, 25L], host.BgmTracks);
Assert.Equal((0, 0L), Assert.Single(host.BgmFades));
}
[Fact] [Fact]
public void Sc0000AudioLifecycleDelayAndDuckControlReachHost() public void Sc0000AudioLifecycleDelayAndDuckControlReachHost()
{ {

View File

@@ -2330,9 +2330,18 @@ public sealed class VirtualMachine
Gfx.ReleaseSurfaceRange(42, 1000 - 42); Gfx.ReleaseSurfaceRange(42, 1000 - 42);
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1; _host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
case "play-bgm": case "play-bgm":
_currentBgmTrackId = Read(a[0]); {
_host.PlayBgm(_currentBgmTrackId); long requestedTrackId = Read(a[0]);
// Native scripts freely reassert the stage BGM after FIELD action/event cleanup.
// The music facade retains the current track id, so an identical request keeps the
// existing stream position instead of reopening the OGG from the beginning.
if (requestedTrackId != _currentBgmTrackId)
{
_currentBgmTrackId = requestedTrackId;
_host.PlayBgm(_currentBgmTrackId);
}
return pc + 1; 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;

View File

@@ -1887,12 +1887,12 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "play-bgm" name = "play-bgm"
category = "audio" category = "audio"
summary = "Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005." summary = "Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), unlike ordinary resource operands, which are universal packed SYS4INI/AAI ids. E.g. play-bgm 5 -> BGM005. Reasserting the facade's current track id is idempotent and preserves playback position; a different id starts the new track, while fade-to-zero clears the current id so a later request can start it again."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
evidence = "By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry. Diagnostic: `Age.Cli audio SC0000.BIN`." evidence = "By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the disproven scene-section model). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (the BGM set skips 030-034). Ghidra /v2 op_0xbf_handler@0x420390 forwards the numeric track to the BGM facade rather than asset_open_indexed_entry; the facade retains that id at EngineCtx.current_bgm_track_id for op 0xc0 and numbered saves. FIELD intentionally reasserts stage_bgm_id from its common post-action/event cleanup at 0x884a, reached after unit movement; restarting an identical host stream there resets the music once per unit during enemy turns. Op 0xc2 target zero releases the source and the VM clears the retained id. Diagnostic: `Age.Cli audio SC0000.BIN`."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1