Implement deferred SFX and voice duck control
This commit is contained in:
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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) |
|
| `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) |
|
||||||
|
|||||||
@@ -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
|
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.
|
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;
|
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.
|
||||||
|
|
||||||
@@ -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
|
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.
|
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)
|
### 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
|
**Problem the oracle surfaced:** single-scene VM runs diverge from the engine because they lack the
|
||||||
|
|||||||
@@ -162,6 +162,16 @@
|
|||||||
- **grounding:** source=investigation, confidence=high
|
- **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.
|
- **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
|
## compute
|
||||||
|
|
||||||
### 0x61 `lookup-array` (lookup-array, argc 3)
|
### 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:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=med
|
- **grounding:** source=kelebek, confidence=med
|
||||||
|
|
||||||
### 0x1cf `u0041DA10` (u0041DA10, argc 1)
|
|
||||||
- **summary:** —
|
|
||||||
- **grounding:** source=kelebek, confidence=low
|
|
||||||
|
|
||||||
### 0x1f6 `u00416170` (u00416170, argc 0)
|
### 0x1f6 `u00416170` (u00416170, argc 0)
|
||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **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:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|
||||||
### 0x2bf `u00423180` (u00423180, argc 3)
|
|
||||||
- **summary:** —
|
|
||||||
- **grounding:** source=kelebek, confidence=low
|
|
||||||
|
|
||||||
### 0x2c0 `u004231C0` (u004231C0, argc 3)
|
### 0x2c0 `u004231C0` (u004231C0, argc 3)
|
||||||
- **summary:** —
|
- **summary:** —
|
||||||
- **grounding:** source=kelebek, confidence=low
|
- **grounding:** source=kelebek, confidence=low
|
||||||
|
|||||||
@@ -1952,3 +1952,28 @@ decision.
|
|||||||
|
|
||||||
Validation: all 206 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the
|
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`.
|
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.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Xunit;
|
|||||||
public class SfxOpsTests
|
public class SfxOpsTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Sc0000SfxLifecycleAndBgmFadeReachHost()
|
public void Sc0000AudioLifecycleDelayAndDuckControlReachHost()
|
||||||
{
|
{
|
||||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
var script = ScriptAssembler.Assemble(table, "SFX",
|
var script = ScriptAssembler.Assemble(table, "SFX",
|
||||||
@@ -16,6 +16,8 @@ public class SfxOpsTests
|
|||||||
{
|
{
|
||||||
(0xb4, new[] { new Operand(0, 0x28), new Operand(0, 0) }),
|
(0xb4, new[] { new Operand(0, 0x28), new Operand(0, 0) }),
|
||||||
(0xb5, new[] { 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) }),
|
(0xb6, new[] { new Operand(0, 0) }),
|
||||||
(0xc2, new[] { new Operand(0, 25), new Operand(0, 3000) }),
|
(0xc2, new[] { new Operand(0, 25), new Operand(0, 3000) }),
|
||||||
(0xd9, Array.Empty<Operand>()),
|
(0xd9, Array.Empty<Operand>()),
|
||||||
@@ -29,6 +31,8 @@ public class SfxOpsTests
|
|||||||
Assert.Equal("exit", vm.HaltReason);
|
Assert.Equal("exit", vm.HaltReason);
|
||||||
Assert.Equal((0x28L, 0), Assert.Single(host.SfxLoads));
|
Assert.Equal((0x28L, 0), Assert.Single(host.SfxLoads));
|
||||||
Assert.Equal(0, Assert.Single(host.SfxStarts));
|
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(0, Assert.Single(host.SfxReleases));
|
||||||
Assert.Equal((25, 3000L), Assert.Single(host.BgmFades));
|
Assert.Equal((25, 3000L), Assert.Single(host.BgmFades));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||||
public readonly List<long> Voices = new();
|
public readonly List<long> Voices = new();
|
||||||
public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new();
|
public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new();
|
||||||
|
public readonly List<long> VoiceBgmDuckControls = new();
|
||||||
public readonly List<int> SfxStarts = new();
|
public readonly List<int> SfxStarts = 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<(int Target, long Duration)> BgmFades = new();
|
public readonly List<(int Target, long Duration)> BgmFades = new();
|
||||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = 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);
|
Voices.Add(id);
|
||||||
VoiceRequests.Add((id, playbackVariant));
|
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 LoadSoundEffect(long resourceId, int channel) => SfxLoads.Add((resourceId, channel));
|
||||||
public void StartSoundEffect(int channel) => SfxStarts.Add(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 ReleaseSoundEffect(int channel) => SfxReleases.Add(channel);
|
||||||
public void FadeBgm(int targetPercent, long durationMs) => BgmFades.Add((targetPercent, durationMs));
|
public void FadeBgm(int targetPercent, long durationMs) => BgmFades.Add((targetPercent, durationMs));
|
||||||
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
|
||||||
|
|||||||
@@ -79,8 +79,14 @@ public interface IHost
|
|||||||
// 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.
|
||||||
void PlayVoice(long id, int playbackVariant) => PlayVoice(id);
|
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 LoadSoundEffect(long resourceId, int channel) { }
|
||||||
void StartSoundEffect(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 ReleaseSoundEffect(int channel) { }
|
||||||
void FadeBgm(int targetPercent, long durationMs) { }
|
void FadeBgm(int targetPercent, long durationMs) { }
|
||||||
// Native op 0x236 binds a DirectShow movie decoder to an existing retained texture surface.
|
// Native op 0x236 binds a DirectShow movie decoder to an existing retained texture surface.
|
||||||
|
|||||||
@@ -972,12 +972,16 @@ public sealed class VirtualMachine
|
|||||||
_autoVoicePending = true;
|
_autoVoicePending = true;
|
||||||
TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle);
|
TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle);
|
||||||
_host.PlayVoice(Read(a[0]), 1); return pc + 1;
|
_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
|
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
||||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||||
case "u0041D050": // 0xb5 / semantics: sfx-start
|
case "u0041D050": // 0xb5 / semantics: sfx-start
|
||||||
_host.StartSoundEffect((int)Read(a[0])); return pc + 1;
|
_host.StartSoundEffect((int)Read(a[0])); return pc + 1;
|
||||||
case "u0041D080": // 0xb6 / semantics: sfx-release
|
case "u0041D080": // 0xb6 / semantics: sfx-release
|
||||||
_host.ReleaseSoundEffect((int)Read(a[0])); return pc + 1;
|
_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
|
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
|
||||||
_host.FadeBgm((int)Read(a[0]), Read(a[1])); return pc + 1;
|
_host.FadeBgm((int)Read(a[0]), Read(a[1])); return pc + 1;
|
||||||
case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000
|
case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private bool _advTextForceComplete;
|
private bool _advTextForceComplete;
|
||||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||||
private volatile bool _messageSkipActive;
|
private volatile bool _messageSkipActive;
|
||||||
|
private int _voiceBgmDuckControl;
|
||||||
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
||||||
private int _activeWaitLayout;
|
private int _activeWaitLayout;
|
||||||
private long _waitIndicatorStartedMs;
|
private long _waitIndicatorStartedMs;
|
||||||
@@ -663,9 +664,16 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private void DispatchVoice(AudioPayload audio, int playbackVariant)
|
private void DispatchVoice(AudioPayload audio, int playbackVariant)
|
||||||
{
|
{
|
||||||
int generation = _main.QueueVoicePlayback();
|
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
|
// 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.
|
// 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)
|
public void LoadSoundEffect(long resourceId, int channel)
|
||||||
@@ -687,6 +695,16 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_main.CallDeferred("StartSoundEffect", channel);
|
_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)
|
public void ReleaseSoundEffect(int channel)
|
||||||
{
|
{
|
||||||
if ((uint)channel >= (uint)_sfxNames.Length) return;
|
if ((uint)channel >= (uint)_sfxNames.Length) return;
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ public partial class Main : Godot.Control
|
|||||||
private int _voiceQueuedGeneration;
|
private int _voiceQueuedGeneration;
|
||||||
private int _voiceStartedGeneration;
|
private int _voiceStartedGeneration;
|
||||||
private int _voiceCompletedGeneration;
|
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 AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
|
||||||
|
private readonly int[] _sfxGenerations = new int[10];
|
||||||
private VirtualMachine _vm = null!;
|
private VirtualMachine _vm = null!;
|
||||||
private GodotAdvHost _host = null!;
|
private GodotAdvHost _host = null!;
|
||||||
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
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 _voiceCompletedGeneration)
|
||||||
< System.Threading.Volatile.Read(ref _voiceQueuedGeneration);
|
< 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);
|
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
@@ -859,6 +863,10 @@ public partial class Main : Godot.Control
|
|||||||
CompleteVoiceGeneration(generation);
|
CompleteVoiceGeneration(generation);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (duckBgm)
|
||||||
|
BeginVoiceBgmDuck(generation, duckTargetPercent);
|
||||||
|
else
|
||||||
|
RestoreVoiceBgmDuck();
|
||||||
stream.Loop = false;
|
stream.Loop = false;
|
||||||
_voice.Stream = stream;
|
_voice.Stream = stream;
|
||||||
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
|
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
|
||||||
@@ -888,15 +896,37 @@ public partial class Main : Godot.Control
|
|||||||
}
|
}
|
||||||
while (System.Threading.Interlocked.CompareExchange(
|
while (System.Threading.Interlocked.CompareExchange(
|
||||||
ref _voiceCompletedGeneration, generation, current) != current);
|
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)
|
public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel)
|
||||||
{
|
{
|
||||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||||
|
_sfxGenerations[channel]++;
|
||||||
|
_sfx[channel].Stop();
|
||||||
|
_sfx[channel].Stream = null;
|
||||||
var stream = AudioStreamWav.LoadFromBuffer(wavBytes);
|
var stream = AudioStreamWav.LoadFromBuffer(wavBytes);
|
||||||
if (stream == null) { GD.Print($"WAV load failed {assetName}"); return; }
|
if (stream == null) { GD.Print($"WAV load failed {assetName}"); return; }
|
||||||
stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled;
|
stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled;
|
||||||
_sfx[channel].Stop();
|
|
||||||
_sfx[channel].VolumeDb = 0;
|
_sfx[channel].VolumeDb = 0;
|
||||||
_sfx[channel].Stream = stream;
|
_sfx[channel].Stream = stream;
|
||||||
}
|
}
|
||||||
@@ -907,9 +937,31 @@ public partial class Main : Godot.Control
|
|||||||
_sfx[channel].Play();
|
_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)
|
public void ReleaseSoundEffect(int channel)
|
||||||
{
|
{
|
||||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||||
|
_sfxGenerations[channel]++;
|
||||||
_sfx[channel].Stop();
|
_sfx[channel].Stop();
|
||||||
_sfx[channel].Stream = null;
|
_sfx[channel].Stream = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -283,6 +283,16 @@ name = "adv_auto_voice_pending"
|
|||||||
type = "int"
|
type = "int"
|
||||||
note = "set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets"
|
note = "set by play-voice ops when voice service exists; Auto waits for voice completion then arms AutoMessageTime0; op 0x1bc resets"
|
||||||
[[field]]
|
[[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
|
offset = 0x6dbf4
|
||||||
name = "message_skip_queued_voice_id"
|
name = "message_skip_queued_voice_id"
|
||||||
type = "int"
|
type = "int"
|
||||||
|
|||||||
@@ -4249,23 +4249,23 @@ observed_types = ["imm"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0x1cf
|
op = 0x1cf
|
||||||
label = "u0041DA10"
|
label = "set-voice-bgm-duck-control"
|
||||||
argc = 1
|
argc = 1
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u0041DA10"
|
name = "set-voice-bgm-duck-control"
|
||||||
category = "unknown"
|
category = "audio"
|
||||||
summary = ""
|
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
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "investigation"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
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]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "control flags (bit 0 = suppress voice-triggered BGM duck)"
|
||||||
observed_types = ["imm", "g-int"]
|
observed_types = ["imm", "g-int"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
@@ -6688,33 +6688,33 @@ observed_types = ["imm"]
|
|||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
op = 0x2bf
|
op = 0x2bf
|
||||||
label = "u00423180"
|
label = "schedule-sfx-start"
|
||||||
argc = 3
|
argc = 3
|
||||||
abi_source = "kelebek+decode-validated"
|
abi_source = "kelebek+decode-validated"
|
||||||
|
|
||||||
[opcode.semantics]
|
[opcode.semantics]
|
||||||
name = "u00423180"
|
name = "schedule-sfx-start"
|
||||||
category = "unknown"
|
category = "audio"
|
||||||
summary = ""
|
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
|
noop_headless = false
|
||||||
source = "kelebek"
|
source = "frida"
|
||||||
confidence = "low"
|
confidence = "high"
|
||||||
depends_on = []
|
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]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
role = ""
|
role = "sound channel (0..9)"
|
||||||
observed_types = ["imm", "g-int", "l-int"]
|
observed_types = ["imm", "g-int", "l-int"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 2
|
i = 2
|
||||||
role = ""
|
role = "start mode forwarded to SFX start worker"
|
||||||
observed_types = ["imm"]
|
observed_types = ["imm"]
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 3
|
i = 3
|
||||||
role = ""
|
role = "delay in milliseconds"
|
||||||
observed_types = ["imm", "l-int"]
|
observed_types = ["imm", "l-int"]
|
||||||
|
|
||||||
[[opcode]]
|
[[opcode]]
|
||||||
|
|||||||
Reference in New Issue
Block a user