diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index 5f2f956..bb828ab 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -61,6 +61,8 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x6dbcc` | `coroutine_resume_off` | `int` | op 0x7c resume PC offset | | `0x6dbd4` | `adv_read_skip_state` | `int` | current ADV read/click-skip service state; queried by op 0x1cc, maintained from ReadTextSkip + per-PC read history | | `0x6dbe4` | `adv_auto_voice_pending` | `int` | set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets | +| `0x6dbe8` | `voice_bgm_duck_saved_volume` | `int` | BGM volume saved by the voice-start duck helper before applying MusicFadeOnVoicePlayingVol | +| `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation | | `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active | | `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero | | `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) | diff --git a/docs/engine-re.md b/docs/engine-re.md index 7c698b1..10e5a3a 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1640,6 +1640,17 @@ resource `0x28` to `E0808.WAV`, loads channel 0, and `0xb5@0xc2e` starts it in t scratch global `G[0x6242d]` is maintained outside script-visible writes; the SC0000 port profile exposes it as an external value of 4 rather than pretending the script assigned it. +That later service start is opcode `0x2bf`, now identified as the facade's `SetDelay` operation. +`op_0x2bf_schedule_sfx_start@0x425240` passes `(channel,start_mode,delay_ms)` to +`sfx_set_delay@0x482720` on the inline sound facade at `ctx+0x14024`. The worker accepts channels `0..9` +and stores five pieces of transient state: active at facade `+0x418`, elapsed/progress at `+0x440`, delay +at `+0x468`, start mode at `+0x490`, and an aggregate-active marker at `+0x4b8`. Its native error text is +literally `Function: SetDelay ... invalid Sound number`. The existing trace supplies the consumer proof: +after SC0000 executes `(channel 4, mode 0, 100 ms)`, the ordinary `sfx_channel_start` worker enters for +channel 4 about 109 ms later while the interpreter is parked at its `0x21c` presentation boundary. There +is no intervening `0xb5`. The port schedules the same already-loaded channel and invalidates the pending +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. @@ -1649,6 +1660,19 @@ durations at least 1000 ms take 100 steps, shorter durations take 10, and target The VM is parked for the requested duration. `0xd9` is adjacent startup control, not audio data: it clears run/service bit `0x1000` in the primary and, when active, secondary context and has no VM-visible result. +Opcode `0x1cf` belongs to the voice/BGM envelope rather than SFX. Handler +`op_0x1cf_set_voice_bgm_duck_control@0x4209f0` replaces the transient mask at +`EngineCtx.voice_bgm_duck_control_flags` (`ctx+0x6dbf0`). Before ordinary or History voice playback, +`voice_bgm_duck_begin@0x406de0` checks that BGM is active, `MusicFadeOnVoicePlaying` is enabled, no explicit +`0xc2` fade is active, and mask bit 0 is clear. It then saves the current BGM volume at +`ctx+0x6dbe8` and applies `MusicFadeOnVoicePlayingVol`. The native settings registry supplies defaults of +enabled=`1` and target=`50` percent; SC0000 writes only control masks `0` and `1`. + +The port therefore retains `0x1cf` as script-owned runtime state, applies the native registered default +attenuation when an unsuppressed voice begins, and restores the saved BGM level when that voice completes +or is stopped by message Skip. This introduces no profile record, boot seed, or persistence backend. A +future unified settings backend can replace the registered defaults without changing the opcode/host seam. + ### Scene-entry state snapshot — auto-seeding single-scene runs (2026-07-09) **Problem the oracle surfaced:** single-scene VM runs diverge from the engine because they lack the diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index b3456df..e10c5c7 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -162,6 +162,16 @@ - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice and calls voice_play_indexed_asset@0x488330 with operand 1 plus playback variant 1 when Skip is inactive (or queues that pair while Skip is active). When text-history recording is enabled it appends voice pair {id,1}; HISTORY.BIN suppresses recording with op 0x1bb while its UI is active, preventing replay clicks from recursively entering the backlog. The handler also sets adv_auto_voice_pending when playback exists. voice_play_indexed_asset retains the variant in sound-buffer state before starting channel 12; its exact audible meaning remains unproven. +### 0x1cf `set-voice-bgm-duck-control` (set-voice-bgm-duck-control, argc 1) +- **summary:** (flags) - replace the transient voice/BGM-duck control mask. Bit 0 suppresses automatic BGM attenuation when voice playback starts; zero permits the configured duck behavior. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1cf_set_voice_bgm_duck_control@0x4209f0 fetches operand 1 and writes it verbatim to ctx+0x6dbf0. The voice-start helper voice_bgm_duck_begin@0x406de0 tests bit 0 before arming BGM attenuation; native settings defaults register MusicFadeOnVoicePlaying=1 and MusicFadeOnVoicePlayingVol=50. SC0000 uses values 0 and 1. + +### 0x2bf `schedule-sfx-start` (schedule-sfx-start, argc 3) +- **summary:** (channel)(start_mode)(delay_ms) - schedule the already-loaded SFX channel to enter the ordinary start worker after the requested delay. Channels are limited to 0..9. +- **grounding:** source=frida, confidence=high +- **evidence:** Ghidra /v2: op_0x2bf_schedule_sfx_start@0x425240 calls sfx_set_delay@0x482720 on the ctx+0x14024 sound facade. The worker's native error text names Function:SetDelay, validates channel<=9, and stores active/progress/delay/start-mode state. Existing SC0000 trace: SetDelay(channel 4, mode 0, 100) is followed about 109 ms later by the ordinary sfx_channel_start worker on channel 4 with mode 0 and no intervening 0xb5. + ## compute ### 0x61 `lookup-array` (lookup-array, argc 3) @@ -1078,10 +1088,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=med -### 0x1cf `u0041DA10` (u0041DA10, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1f6 `u00416170` (u00416170, argc 0) - **summary:** — - **grounding:** source=kelebek, confidence=low @@ -1167,10 +1173,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x2bf `u00423180` (u00423180, argc 3) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x2c0 `u004231C0` (u004231C0, argc 3) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index daa720b..f523540 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -1952,3 +1952,28 @@ decision. Validation: all 206 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the Godot build has zero warnings, and threaded `SELFTEST OK`. + +### SC0000 deferred SFX start and voice/BGM duck control implemented (2026-07-20) + +Native RE and the saved SFX trace resolve two high-frequency audio gaps. Opcode `0x2bf` is the sound +facade's `SetDelay(channel,start_mode,delay_ms)`: SC0000's first `(4,0,100)` call starts the preloaded +secondary E0808 channel about 109 ms later through the same worker used by `0xb5`. The port now schedules +that start against the presentation clock and attaches it to a per-channel generation, so a later load or +release cancels an obsolete callback instead of starting replacement audio. + +Opcode `0x1cf` replaces a transient voice/BGM-duck control mask; bit 0 suppresses automatic ducking. +The native engine registers ducking enabled with a 50-percent target, and its voice-start helper saves the +current BGM level before applying that target. Godot now follows that contract and restores the saved level +when voice playback completes or message Skip stops it. This is runtime audio state only: no profile/save +field, boot seed, or storage-backend decision was introduced. + +The canonical opcode and EngineCtx sources are regenerated, the expanded 65-field `EngineCtx` is applied, +and the `/v2` handlers/workers are named, commented, and saved. SC0000 is now 119/129 distinct opcodes and +16,238/16,257 instructions handled or safe-noop (92.2% distinct; 99.9% instruction-weighted); its ten +remaining effectful gaps total 19 instructions. + +**Next:** investigate the six-call `0x1ad` cluster first. It is now the largest remaining SC0000 gap and +should be classified before choosing between it and the smaller two-call support pairs. + +Validation: all 206 engine tests pass; opcode and EngineCtx lints, vm0 RECOVER, the zero-warning Godot +build, and threaded `SELFTEST OK` are clean. diff --git a/engine/Age.Engine.Tests/SfxOpsTests.cs b/engine/Age.Engine.Tests/SfxOpsTests.cs index 2294e6f..563099b 100644 --- a/engine/Age.Engine.Tests/SfxOpsTests.cs +++ b/engine/Age.Engine.Tests/SfxOpsTests.cs @@ -8,7 +8,7 @@ using Xunit; public class SfxOpsTests { [Fact] - public void Sc0000SfxLifecycleAndBgmFadeReachHost() + public void Sc0000AudioLifecycleDelayAndDuckControlReachHost() { var table = OpcodeTableJson.Load(Paths.OpcodesJson); var script = ScriptAssembler.Assemble(table, "SFX", @@ -16,6 +16,8 @@ public class SfxOpsTests { (0xb4, new[] { new Operand(0, 0x28), new Operand(0, 0) }), (0xb5, new[] { new Operand(0, 0) }), + (0x1cf, new[] { new Operand(0, 1) }), + (0x2bf, new[] { new Operand(0, 4), new Operand(0, 0), new Operand(0, 100) }), (0xb6, new[] { new Operand(0, 0) }), (0xc2, new[] { new Operand(0, 25), new Operand(0, 3000) }), (0xd9, Array.Empty()), @@ -29,6 +31,8 @@ public class SfxOpsTests Assert.Equal("exit", vm.HaltReason); Assert.Equal((0x28L, 0), Assert.Single(host.SfxLoads)); Assert.Equal(0, Assert.Single(host.SfxStarts)); + Assert.Equal(1, Assert.Single(host.VoiceBgmDuckControls)); + Assert.Equal((4, 0, 100L), Assert.Single(host.ScheduledSfxStarts)); Assert.Equal(0, Assert.Single(host.SfxReleases)); Assert.Equal((25, 3000L), Assert.Single(host.BgmFades)); } diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index a63421d..3acd360 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -30,7 +30,9 @@ internal class RecordingHost : IHost public readonly List<(long Resource, int Channel)> SfxLoads = new(); public readonly List Voices = new(); public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new(); + public readonly List VoiceBgmDuckControls = new(); public readonly List SfxStarts = new(); + public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new(); public readonly List SfxReleases = new(); public readonly List<(int Target, long Duration)> BgmFades = new(); public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new(); @@ -113,8 +115,11 @@ internal class RecordingHost : IHost Voices.Add(id); VoiceRequests.Add((id, playbackVariant)); } + public void SetVoiceBgmDuckControl(long flags) => VoiceBgmDuckControls.Add(flags); public void LoadSoundEffect(long resourceId, int channel) => SfxLoads.Add((resourceId, channel)); public void StartSoundEffect(int channel) => SfxStarts.Add(channel); + public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) + => ScheduledSfxStarts.Add((channel, startMode, delayMs)); public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel); public void FadeBgm(int targetPercent, long durationMs) => BgmFades.Add((targetPercent, durationMs)); public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs index 5cebc91..13be7e6 100644 --- a/engine/Age.Engine/Hosting/IHost.cs +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -79,8 +79,14 @@ public interface IHost // 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. void PlayVoice(long id, int playbackVariant) => PlayVoice(id); + // Native op 0x1cf stores a transient control mask. Bit 0 suppresses the automatic + // BGM attenuation normally applied when a voice starts. + void SetVoiceBgmDuckControl(long flags) { } void LoadSoundEffect(long resourceId, int channel) { } void StartSoundEffect(int channel) { } + // Native SetDelay (op 0x2bf) starts an already-loaded channel after delayMs. + // startMode is forwarded to the same worker used by immediate SFX starts. + void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) { } void ReleaseSoundEffect(int channel) { } void FadeBgm(int targetPercent, long durationMs) { } // Native op 0x236 binds a DirectShow movie decoder to an existing retained texture surface. diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index e3b5809..76b7365 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -972,12 +972,16 @@ public sealed class VirtualMachine _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 "play-sound-effect": // 0xb4 / semantics: sfx-load _host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1; case "u0041D050": // 0xb5 / semantics: sfx-start _host.StartSoundEffect((int)Read(a[0])); return pc + 1; case "u0041D080": // 0xb6 / semantics: sfx-release _host.ReleaseSoundEffect((int)Read(a[0])); 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 _host.FadeBgm((int)Read(a[0]), Read(a[1])); return pc + 1; case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000 diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index ceb3918..355c3ec 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -40,6 +40,7 @@ public sealed class GodotAdvHost : IHost private bool _advTextForceComplete; private readonly Dictionary _waitIndicators = new(); private volatile bool _messageSkipActive; + private int _voiceBgmDuckControl; private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice; private int _activeWaitLayout; private long _waitIndicatorStartedMs; @@ -663,9 +664,16 @@ public sealed class GodotAdvHost : IHost private void DispatchVoice(AudioPayload audio, int playbackVariant) { int generation = _main.QueueVoicePlayback(); + bool duckBgm = (System.Threading.Volatile.Read(ref _voiceBgmDuckControl) & 1) == 0; // Godot's stream player has no matching AGE start-mode control. Retain the native // variant through dispatch/timeline so that distinction is not erased at the VM seam. - _main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation); + _main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation, duckBgm, 50); + } + + public void SetVoiceBgmDuckControl(long flags) + { + System.Threading.Volatile.Write(ref _voiceBgmDuckControl, unchecked((int)flags)); + _timeline?.State("voice-bgm-duck-control", new() { ["flags"] = flags }); } public void LoadSoundEffect(long resourceId, int channel) @@ -687,6 +695,16 @@ public sealed class GodotAdvHost : IHost _main.CallDeferred("StartSoundEffect", channel); } + public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) + { + if ((uint)channel >= (uint)_sfxNames.Length || _sfxNames[channel] == null) return; + long ms = System.Math.Clamp(delayMs, 0, 60_000); + double realSeconds = ms / 1000.0 / System.Math.Max(0.05, _clock.Speed); + _timeline?.Event("sfx-start-scheduled", new() { ["channel"] = channel, + ["start_mode"] = startMode, ["delay_ms"] = ms, ["file"] = _sfxNames[channel] }); + _main.CallDeferred("ScheduleSoundEffectStart", channel, startMode, realSeconds); + } + public void ReleaseSoundEffect(int channel) { if ((uint)channel >= (uint)_sfxNames.Length) return; diff --git a/godot/Main.cs b/godot/Main.cs index 3330471..1c3a353 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -38,7 +38,11 @@ public partial class Main : Godot.Control private int _voiceQueuedGeneration; private int _voiceStartedGeneration; private int _voiceCompletedGeneration; + private bool _voiceBgmDuckActive; + private int _voiceBgmDuckGeneration; + private float _voiceBgmDuckRestoreDb; private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9 + private readonly int[] _sfxGenerations = new int[10]; private VirtualMachine _vm = null!; private GodotAdvHost _host = null!; private readonly Age.Engine.Hosting.FrameClock _clock = new(); @@ -850,7 +854,7 @@ public partial class Main : Godot.Control => System.Threading.Volatile.Read(ref _voiceCompletedGeneration) < System.Threading.Volatile.Read(ref _voiceQueuedGeneration); - public void PlayVoice(byte[] oggBytes, string assetName, int generation) + public void PlayVoice(byte[] oggBytes, string assetName, int generation, bool duckBgm, int duckTargetPercent) { var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes); if (stream == null) @@ -859,6 +863,10 @@ public partial class Main : Godot.Control CompleteVoiceGeneration(generation); return; } + if (duckBgm) + BeginVoiceBgmDuck(generation, duckTargetPercent); + else + RestoreVoiceBgmDuck(); stream.Loop = false; _voice.Stream = stream; System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation); @@ -888,15 +896,37 @@ public partial class Main : Godot.Control } while (System.Threading.Interlocked.CompareExchange( ref _voiceCompletedGeneration, generation, current) != current); + if (_voiceBgmDuckActive && generation >= _voiceBgmDuckGeneration) + RestoreVoiceBgmDuck(); + } + + private void BeginVoiceBgmDuck(int generation, int targetPercent) + { + if (!_voiceBgmDuckActive) + _voiceBgmDuckRestoreDb = _bgm.VolumeDb; + _voiceBgmDuckActive = true; + _voiceBgmDuckGeneration = generation; + float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f); + _bgm.VolumeDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear); + } + + private void RestoreVoiceBgmDuck() + { + if (!_voiceBgmDuckActive) return; + _bgm.VolumeDb = _voiceBgmDuckRestoreDb; + _voiceBgmDuckActive = false; + _voiceBgmDuckGeneration = 0; } public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel) { if ((uint)channel >= (uint)_sfx.Length) return; + _sfxGenerations[channel]++; + _sfx[channel].Stop(); + _sfx[channel].Stream = null; var stream = AudioStreamWav.LoadFromBuffer(wavBytes); if (stream == null) { GD.Print($"WAV load failed {assetName}"); return; } stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled; - _sfx[channel].Stop(); _sfx[channel].VolumeDb = 0; _sfx[channel].Stream = stream; } @@ -907,9 +937,31 @@ public partial class Main : Godot.Control _sfx[channel].Play(); } + public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds) + { + if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return; + int generation = _sfxGenerations[channel]; + void StartIfCurrent() + { + if (_sfxGenerations[channel] != generation || _sfx[channel].Stream == null) return; + if (_sfx[channel].Stream is AudioStreamWav wav) + wav.LoopMode = startMode == 0 + ? AudioStreamWav.LoopModeEnum.Disabled + : AudioStreamWav.LoopModeEnum.Forward; + _sfx[channel].Play(); + } + if (realDelaySeconds <= 0) + { + StartIfCurrent(); + return; + } + GetTree().CreateTimer(realDelaySeconds).Timeout += StartIfCurrent; + } + public void ReleaseSoundEffect(int channel) { if ((uint)channel >= (uint)_sfx.Length) return; + _sfxGenerations[channel]++; _sfx[channel].Stop(); _sfx[channel].Stream = null; } diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index 9b0b227..efc9412 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -283,6 +283,16 @@ name = "adv_auto_voice_pending" type = "int" note = "set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets" [[field]] +offset = 0x6dbe8 +name = "voice_bgm_duck_saved_volume" +type = "int" +note = "BGM volume saved by the voice-start duck helper before applying MusicFadeOnVoicePlayingVol" +[[field]] +offset = 0x6dbf0 +name = "voice_bgm_duck_control_flags" +type = "uint" +note = "transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation" +[[field]] offset = 0x6dbf4 name = "message_skip_queued_voice_id" type = "int" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 29dbffb..2107d72 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -4249,23 +4249,23 @@ observed_types = ["imm"] [[opcode]] op = 0x1cf -label = "u0041DA10" +label = "set-voice-bgm-duck-control" argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041DA10" -category = "unknown" -summary = "" +name = "set-voice-bgm-duck-control" +category = "audio" +summary = "(flags) - replace the transient voice/BGM-duck control mask. Bit 0 suppresses automatic BGM attenuation when voice playback starts; zero permits the configured duck behavior." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1cf_set_voice_bgm_duck_control@0x4209f0 fetches operand 1 and writes it verbatim to ctx+0x6dbf0. The voice-start helper voice_bgm_duck_begin@0x406de0 tests bit 0 before arming BGM attenuation; native settings defaults register MusicFadeOnVoicePlaying=1 and MusicFadeOnVoicePlayingVol=50. SC0000 uses values 0 and 1." [[opcode.semantics.args]] i = 1 -role = "" +role = "control flags (bit 0 = suppress voice-triggered BGM duck)" observed_types = ["imm", "g-int"] [[opcode]] @@ -6688,33 +6688,33 @@ observed_types = ["imm"] [[opcode]] op = 0x2bf -label = "u00423180" +label = "schedule-sfx-start" argc = 3 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00423180" -category = "unknown" -summary = "" +name = "schedule-sfx-start" +category = "audio" +summary = "(channel)(start_mode)(delay_ms) - schedule the already-loaded SFX channel to enter the ordinary start worker after the requested delay. Channels are limited to 0..9." noop_headless = false -source = "kelebek" -confidence = "low" +source = "frida" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x2bf_schedule_sfx_start@0x425240 calls sfx_set_delay@0x482720 on the ctx+0x14024 sound facade. The worker's native error text names Function:SetDelay, validates channel<=9, and stores active/progress/delay/start-mode state. Existing SC0000 trace: SetDelay(channel 4, mode 0, 100) is followed about 109 ms later by the ordinary sfx_channel_start worker on channel 4 with mode 0 and no intervening 0xb5." [[opcode.semantics.args]] i = 1 -role = "" +role = "sound channel (0..9)" observed_types = ["imm", "g-int", "l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "start mode forwarded to SFX start worker" observed_types = ["imm"] [[opcode.semantics.args]] i = 3 -role = "" +role = "delay in milliseconds" observed_types = ["imm", "l-int"] [[opcode]]