From 738129faa108a2ab6437d2272ceee1a3840ff10c Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 2 Aug 2026 22:25:06 -0400 Subject: [PATCH] Extract VM audio opcode handler --- docs/PROJECT-STRUCTURE.md | 5 + docs/remake-architecture-and-roadmap.md | 11 +- engine/Age.Engine/Vm/VirtualMachine.Audio.cs | 145 +++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 145 +++---------------- 4 files changed, 179 insertions(+), 127 deletions(-) create mode 100644 engine/Age.Engine/Vm/VirtualMachine.Audio.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index c159cd2..f9552a2 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -172,6 +172,11 @@ the interpolation helpers consumed by retained-scene sampling. `engine/Age.Engin owns surface/movie transition queues, presentation activity and click-skip logic, diagnostics and dirty-reason accounting, and visible retained-scene snapshots. +`engine/Age.Engine/Vm/VirtualMachine.cs` retains VM lifecycle, cross-domain state, and the proven top-level +opcode dispatcher. Its partial-class companion `engine/Age.Engine/Vm/VirtualMachine.Audio.cs` owns VM audio +state, BGM restart semantics, and the BGM/voice/SFX/mixer opcode handler; `Step` retains the audio labels and +routes that family into the handler. + The disposable `build/page-map-.jsonl` files are produced by editor/development Godot runs and map runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead. diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index fa3b8c4..bd6d2bf 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -624,6 +624,12 @@ do not mix mechanical moves with semantic changes. accounting, scene reset, persistence, and locking. The planned `GfxState` decomposition is complete; runtime validation remains green. + The first bounded `VirtualMachine.Step` extraction converted the sealed VM type to a sealed partial class and + moved VM-owned audio state, BGM restart semantics, and the existing BGM/voice/SFX/mixer case bodies into + `engine/Age.Engine/Vm/VirtualMachine.Audio.cs`. The top-level dispatcher retains the exact opcode labels and + routes only that family through `StepAudio`; public VM behavior and case-body logic remain unchanged. Runtime + validation remains green. + **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after each domain move. @@ -995,7 +1001,8 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState` -domains isolated, begin thinning `VirtualMachine.Step` through existing domain handler methods without replacing -the proven dispatcher or changing public types, commands, and generated output. +domains isolated and the first `VirtualMachine.Step` family routed through a domain handler, extract the movie +opcode family next without replacing the proven dispatcher or changing public types, commands, and generated +output. Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/engine/Age.Engine/Vm/VirtualMachine.Audio.cs b/engine/Age.Engine/Vm/VirtualMachine.Audio.cs new file mode 100644 index 0000000..d5dd410 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.Audio.cs @@ -0,0 +1,145 @@ +using Age.Engine.Model; +using Age.Engine.Persistence; + +namespace Age.Engine.Vm; + +public sealed partial class VirtualMachine +{ + private readonly AudioMixerSettings _audioMixerSettings; + private long _currentBgmTrackId; + private readonly long[] _loadedSoundEffectResourceIds = + new long[NativeNumberedSaveState.SoundEffectChannelCount]; + + private void RestartBgm(long requestedTrackId, int startMode) + { + if (requestedTrackId != 0) + _currentBgmTrackId = requestedTrackId; + + if (_currentBgmTrackId == 0) + _host.StopBgm(); + else + _host.RestartBgm(_currentBgmTrackId, startMode); + } + + private int StepAudio(string label, IReadOnlyList a, int pc) + { + switch (label) + { + case "play-bgm": + { + 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 "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": + Write(a[0], _currentBgmTrackId); + return pc + 1; + case "play-voice": + _autoVoicePending = true; + TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle); + _host.PlayVoice(Read(a[0]), 0); return pc + 1; + case "play-history-voice": // 0x1bd: native voice start/history argument is one + case "u0041D910": + _autoVoicePending = true; + TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle); + _host.PlayVoice(Read(a[0]), 1); return pc + 1; + case "set-voice-bgm-duck-control": // 0x1cf: bit 0 suppresses automatic voice ducking + _host.SetVoiceBgmDuckControl(Read(a[0])); return pc + 1; + case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request + _host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1; + case "play-sound-effect": // 0xb4 / semantics: sfx-load + { + long resourceId = Read(a[0]); + int channel = (int)Read(a[1]); + if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length) + _loadedSoundEffectResourceIds[channel] = resourceId; + _host.LoadSoundEffect(resourceId, channel); + return pc + 1; + } + case "u0041D050": // 0xb5 / semantics: sfx-start-once + _host.StartSoundEffect((int)Read(a[0]), 0); return pc + 1; + case "sfx-start-loop": // 0xba / same worker, logical decoder rewind at EOF + _host.StartSoundEffect((int)Read(a[0]), 1); return pc + 1; + case "u0041D080": // 0xb6 / semantics: sfx-release + { + int channel = (int)Read(a[0]); + if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length) + _loadedSoundEffectResourceIds[channel] = 0; + _host.ReleaseSoundEffect(channel); + return pc + 1; + } + case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms) + _host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1; + case "u0041D2B0": // 0xc2 / semantics: fade-bgm + { + int targetPercent = (int)Read(a[0]); + _host.FadeBgm(targetPercent, Read(a[1])); + if (targetPercent == 0) + { + _currentBgmTrackId = 0; + _host.StopBgm(); + } + return pc + 1; + } + case "get-audio-volume": // 0xc5 (category)(out basis points) + { + int category = unchecked((int)Read(a[0])); + if (_audioMixerSettings.TryGetVolume(category, out int basisPoints)) + Write(a[1], basisPoints); + else + _host.ReportWarning($"audio volume category out of range: {category}"); + return pc + 1; + } + case "set-audio-volume": // 0xc6 (category)(basis points) + { + int category = unchecked((int)Read(a[0])); + long basisPoints = Read(a[1]); + if (_audioMixerSettings.TrySetVolume(category, basisPoints)) + _host.ApplyAudioVolume(category, checked((int)basisPoints)); + else + _host.ReportWarning($"audio volume category out of range: {category}"); + return pc + 1; + } + case "get-audio-route-enabled": // 0xc7 (category)(out boolean) + { + int category = unchecked((int)Read(a[0])); + if (_audioMixerSettings.TryGetRouteEnabled(category, out bool enabled)) + Write(a[1], enabled ? 1 : 0); + else + _host.ReportWarning($"audio route category out of range: {category}"); + return pc + 1; + } + case "set-audio-route-enabled": // 0x1ba (category)(enabled) + { + int category = unchecked((int)Read(a[0])); + bool enabled = Read(a[1]) != 0; + if (_audioMixerSettings.TrySetRouteEnabled(category, enabled, out bool changed)) + { + if (changed) _host.ApplyAudioRouteEnabled(category, enabled); + } + else + _host.ReportWarning($"audio route category out of range: {category}"); + return pc + 1; + } + default: + throw new InvalidOperationException($"Non-audio opcode routed to audio handler: {label}"); + } + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index fe7af03..17b2ed5 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -22,7 +22,7 @@ public readonly record struct SharedProfileShutdownFlushResult( SharedProfileShutdownFlushOutcome Outcome, string? Error = null); -public sealed class VirtualMachine +public sealed partial class VirtualMachine { private const long NoJump = 0xFFFFFFFF; private const int HALT = int.MinValue; @@ -42,7 +42,6 @@ public sealed class VirtualMachine private readonly Encoding _nativeStringEncoding; private readonly IScriptProvider? _provider; private readonly SharedProfile _sharedProfile; - private readonly AudioMixerSettings _audioMixerSettings; private readonly DiagnosticOutputState _diagnosticOutput; private readonly INativeDatStore? _nativeDatStore; private static readonly bool _diagSetTexture = System.Environment.GetEnvironmentVariable("AGE_DIAG_SETTEX") == "1"; @@ -58,9 +57,6 @@ public sealed class VirtualMachine private NativeNumberedSaveState? _loadedNumberedState; private NativeNumberedSaveState? _retainedNativeNumberedState; private int _restoreFrameIndex = -1; - private long _currentBgmTrackId; - private readonly long[] _loadedSoundEffectResourceIds = - new long[NativeNumberedSaveState.SoundEffectChannelCount]; private uint _accumulatedPlaySeconds; private readonly long _sessionStartTimestamp; private ExecFrame? _debugActiveFrame; @@ -822,16 +818,6 @@ public sealed class VirtualMachine _intStacks[i] = new Stack(0x100); } - 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) { @@ -1218,7 +1204,8 @@ public sealed class VirtualMachine { int op = ins.Opcode; var a = ins.Args; - switch (_t.Label(op)) + string label = _t.Label(op); + switch (label) { case "script-entry": Gfx.ClearSurfaceReloadPolicies(); return pc + 1; @@ -2555,118 +2542,26 @@ public sealed class VirtualMachine Gfx.ReleaseSurfaceRange(42, 1000 - 42); _host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1; case "play-bgm": - { - 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 "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 "restart-bgm-loop": + case "stop-bgm": + case "restart-bgm-once": case "get-current-bgm-track": - Write(a[0], _currentBgmTrackId); - return pc + 1; case "play-voice": - _autoVoicePending = true; - TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle); - _host.PlayVoice(Read(a[0]), 0); return pc + 1; - case "play-history-voice": // 0x1bd: native voice start/history argument is one + case "play-history-voice": case "u0041D910": - _autoVoicePending = true; - TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle); - _host.PlayVoice(Read(a[0]), 1); return pc + 1; - case "set-voice-bgm-duck-control": // 0x1cf: bit 0 suppresses automatic voice ducking - _host.SetVoiceBgmDuckControl(Read(a[0])); return pc + 1; - case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request - _host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1; - case "play-sound-effect": // 0xb4 / semantics: sfx-load - { - long resourceId = Read(a[0]); - int channel = (int)Read(a[1]); - if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length) - _loadedSoundEffectResourceIds[channel] = resourceId; - _host.LoadSoundEffect(resourceId, channel); - return pc + 1; - } - case "u0041D050": // 0xb5 / semantics: sfx-start-once - _host.StartSoundEffect((int)Read(a[0]), 0); return pc + 1; - case "sfx-start-loop": // 0xba / same worker, logical decoder rewind at EOF - _host.StartSoundEffect((int)Read(a[0]), 1); return pc + 1; - case "u0041D080": // 0xb6 / semantics: sfx-release - { - int channel = (int)Read(a[0]); - if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length) - _loadedSoundEffectResourceIds[channel] = 0; - _host.ReleaseSoundEffect(channel); - return pc + 1; - } - case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms) - _host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1; - case "u0041D2B0": // 0xc2 / semantics: fade-bgm - { - int targetPercent = (int)Read(a[0]); - _host.FadeBgm(targetPercent, Read(a[1])); - if (targetPercent == 0) - { - _currentBgmTrackId = 0; - _host.StopBgm(); - } - return pc + 1; - } - case "get-audio-volume": // 0xc5 (category)(out basis points) - { - int category = unchecked((int)Read(a[0])); - if (_audioMixerSettings.TryGetVolume(category, out int basisPoints)) - Write(a[1], basisPoints); - else - _host.ReportWarning($"audio volume category out of range: {category}"); - return pc + 1; - } - case "set-audio-volume": // 0xc6 (category)(basis points) - { - int category = unchecked((int)Read(a[0])); - long basisPoints = Read(a[1]); - if (_audioMixerSettings.TrySetVolume(category, basisPoints)) - _host.ApplyAudioVolume(category, checked((int)basisPoints)); - else - _host.ReportWarning($"audio volume category out of range: {category}"); - return pc + 1; - } - case "get-audio-route-enabled": // 0xc7 (category)(out boolean) - { - int category = unchecked((int)Read(a[0])); - if (_audioMixerSettings.TryGetRouteEnabled(category, out bool enabled)) - Write(a[1], enabled ? 1 : 0); - else - _host.ReportWarning($"audio route category out of range: {category}"); - return pc + 1; - } - case "set-audio-route-enabled": // 0x1ba (category)(enabled) - { - int category = unchecked((int)Read(a[0])); - bool enabled = Read(a[1]) != 0; - if (_audioMixerSettings.TrySetRouteEnabled(category, enabled, out bool changed)) - { - if (changed) _host.ApplyAudioRouteEnabled(category, enabled); - } - else - _host.ReportWarning($"audio route category out of range: {category}"); - return pc + 1; - } + case "set-voice-bgm-duck-control": + case "schedule-voice-playback": + case "play-sound-effect": + case "u0041D050": + case "sfx-start-loop": + case "u0041D080": + case "schedule-sfx-start": + case "u0041D2B0": + case "get-audio-volume": + case "set-audio-volume": + case "get-audio-route-enabled": + case "set-audio-route-enabled": + return StepAudio(label, a, pc); case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000 return pc + 1; case "get-initial-root-run": // 0x130 (out)