From b7185bbb211a78c88937e7b54fcd7fedfe158400 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 27 Jul 2026 11:40:54 -0400 Subject: [PATCH] Fix repeated FIELD BGM restarts --- docs/engine-re.md | 9 +++++++++ docs/opcode-reference.md | 4 ++-- engine/Age.Engine.Tests/SfxOpsTests.cs | 25 +++++++++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 13 +++++++++++-- vm-map/opcodes.toml | 4 ++-- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 85d6a94..3e7c5f8 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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; 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, 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. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 7d7083e..be7ebe3 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -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. ### 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 -- **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) - **summary:** (track_out) - return the direct-name BGM track id retained by the music facade; the same value is restored from numbered saves. diff --git a/engine/Age.Engine.Tests/SfxOpsTests.cs b/engine/Age.Engine.Tests/SfxOpsTests.cs index 563099b..fec270e 100644 --- a/engine/Age.Engine.Tests/SfxOpsTests.cs +++ b/engine/Age.Engine.Tests/SfxOpsTests.cs @@ -7,6 +7,31 @@ using Xunit; 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()), + }, Array.Empty()); + 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] public void Sc0000AudioLifecycleDelayAndDuckControlReachHost() { diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 6ea035c..c2a6ab8 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -2330,9 +2330,18 @@ public sealed class VirtualMachine Gfx.ReleaseSurfaceRange(42, 1000 - 42); _host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1; 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; + } case "get-current-bgm-track": Write(a[0], _currentBgmTrackId); return pc + 1; diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 45d487b..da670f4 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1887,12 +1887,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "play-bgm" 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 source = "investigation" confidence = "high" 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]] i = 1