Implement looping SFX and immediate rotation opcodes

This commit is contained in:
gamer147
2026-07-28 23:45:48 -04:00
parent 82ac160271
commit 3f4124bbb8
13 changed files with 112 additions and 28 deletions

View File

@@ -2672,9 +2672,9 @@ atlas cell. Frame selection now treats the terminal value as the exclusive frame
Keeping this 30x27 overlay outside the software backbuffer preserves the static-wait compositor optimization. Keeping this 30x27 overlay outside the software backbuffer preserves the static-wait compositor optimization.
Full SYSTEM4 replay remains Phase-B work; the shortcut carries the same state in the meantime. Full SYSTEM4 replay remains Phase-B work; the shortcut carries the same state in the meantime.
### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9` (2026-07-11) ### SC0000 native SFX / BGM-fade family — `0xb4`/`0xb5`/`0xb6`/`0xba`/`0xc2`/`0xd9` (2026-07-11)
The three SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler The four SFX opcodes are a retained channel lifecycle, not immediate fire-and-forget calls. Handler
resolution and the saved `/v2` names are: resolution and the saved `/v2` names are:
- `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(packed_raw_resource_id, channel)` - `0xb4` `op_0xb4_sfx_load@0x4201d0` -> `sfx_channel_load@0x482500`: `(packed_raw_resource_id, channel)`
@@ -2682,10 +2682,13 @@ resolution and the saved `/v2` names are:
when it is nonzero, then replaces the channel decoder/buffer without starting it. The manager supports when it is nonzero, then replaces the channel decoder/buffer without starting it. The manager supports
13 slots (`0..12`); SC0000 deliberately resets and uses the `0..9` subset. 13 slots (`0..12`); SC0000 deliberately resets and uses the `0..9` subset.
- `0xb5` `op_0xb5_sfx_start_once@0x420210` -> `sfx_channel_start@0x4825d0`: starts the loaded channel with - `0xb5` `op_0xb5_sfx_start_once@0x420210` -> `sfx_channel_start@0x4825d0`: starts the loaded channel with
logical loop mode 0. Adjacent op `0xba`, not this slice, passes mode 1. logical loop mode 0.
- `0xb6` `op_0xb6_sfx_release@0x420250` -> `sfx_channel_release@0x482600` -> - `0xb6` `op_0xb6_sfx_release@0x420250` -> `sfx_channel_release@0x482600` ->
`sound_buffer_destroy@0x4831a0`: stop/release and clear the retained resource/decoder; empty release is `sound_buffer_destroy@0x4831a0`: stop/release and clear the retained resource/decoder; empty release is
safe. SC0000's `0x62b..0x646` and `0x120d..0x1228` are ten-channel reset sweeps. safe. SC0000's `0x62b..0x646` and `0x120d..0x1228` are ten-channel reset sweeps.
- `0xba` `op_0xba_sfx_start_loop@0x420290` -> the same `sfx_channel_start@0x4825d0`: starts the loaded
channel with logical loop mode 1. `sound_stream_fill_quarter` rewinds the decoder at EOF until a later
load or release replaces the channel. It is not an alias of the one-shot `0xb5`.
`sound_decode_channel@0x483360` selects the decoder by file signature, constructs a DirectSound buffer, and `sound_decode_channel@0x483360` selects the decoder by file signature, constructs a DirectSound buffer, and
installs four quarter-buffer notifications. `sound_buffer_start@0x484270` primes the ring and synchronously installs four quarter-buffer notifications. `sound_buffer_start@0x484270` primes the ring and synchronously
@@ -2768,6 +2771,12 @@ overwritten to `-80 dB`; a later voice temporarily wrote the audible 50-percent
the erroneous silent level afterward. This introduces no profile record, boot seed, or persistence backend. the erroneous silent level afterward. 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. A future unified settings backend can replace the registered defaults without changing the opcode/host seam.
**Loop-start closeout (2026-07-28).** A complete Himegari-corpus scan found `0xba` at 334 sites in 100
scripts, and every site immediately follows `0xb4` with the same literal channel 9 for persistent UI/ambient
sound. The port now forwards the native start mode through its host seam and sets
Godot's WAV loop mode before playback; `0xb5` explicitly supplies mode 0 and `0xba` supplies mode 1. The
focused VM regression covers both modes, and the renamed/commented `/v2` handler is saved.
### 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

View File

@@ -201,6 +201,10 @@ The requested CP932 face is copied into the primary LOGFONT lfFaceName and AGE a
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **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.
### 0xba `sfx-start-loop` (sfx-start-loop, argc 1)
- **summary:** (channel) — start the already-loaded sound-effect channel with logical loop mode 1; the streaming decoder rewinds at EOF until the channel is replaced or released.
- **grounding:** source=investigation, confidence=high
### 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. 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. - **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
@@ -702,7 +706,7 @@ The handler clears the map embedded at retained-gfx owner+0x408, resets its coun
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: handler gfx_op_0x1fd_set_vec_scaled@0x422650 fetches operands 2..4, divides each by the 100.0 constant, and calls gfx_object_set_scale_current@0x47e6b0. The worker gets/creates the object, marks obj+0x68, and calls matrix4_make_scale on obj+0x6c. SC0000 sets AE001D handles to 210/210/100 and 240/240/100; without this setter their 800x800 alpha circles remain below the viewport. Both functions annotated and /v2 saved 2026-07-11. - **evidence:** Ghidra /v2: handler gfx_op_0x1fd_set_vec_scaled@0x422650 fetches operands 2..4, divides each by the 100.0 constant, and calls gfx_object_set_scale_current@0x47e6b0. The worker gets/creates the object, marks obj+0x68, and calls matrix4_make_scale on obj+0x6c. SC0000 sets AE001D handles to 210/210/100 and 240/240/100; without this setter their 800x800 alpha circles remain below the viewport. Both functions annotated and /v2 saved 2026-07-11.
### 0x1fe `set-current-rotation-axis-angle` (u004206C0, argc 5) ### 0x1fe `set-current-rotation-axis-angle` (set-current-rotation-axis-angle, argc 5)
- **summary:** (handle)(axis_x)(axis_y)(axis_z)(angle_degrees) — immediately replace the retained object's current axis-angle rotation matrix. This is the direct-current companion to op 0x21f's delayed one-shot rotation target. - **summary:** (handle)(axis_x)(axis_y)(axis_z)(angle_degrees) — immediately replace the retained object's current axis-angle rotation matrix. This is the direct-current companion to op 0x21f's delayed one-shot rotation target.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1fe_set_rotation_current@0x422700 converts operands 2..5 to float and calls gfx_object_set_rotation_current@0x47e720. The worker gets/creates the handle, marks transform state at obj+0x68, stores current axis obj+0x1ec..0x1f4 and angle degrees obj+0x204, converts degrees to radians with pi/180, builds the current matrix at obj+0xec through matrix4_make_axis_angle, and sets retained-gfx redraw dirty owner+0xb558 (EngineCtx+0x51b6c). The finite-channel consumer composes this current matrix between scale and translation. Corpus: 186/187 calls use Z axis (0,0,1); the lone DEBUG call uses Y axis (0,1,0). - **evidence:** Ghidra /v2: op_0x1fe_set_rotation_current@0x422700 converts operands 2..5 to float and calls gfx_object_set_rotation_current@0x47e720. The worker gets/creates the handle, marks transform state at obj+0x68, stores current axis obj+0x1ec..0x1f4 and angle degrees obj+0x204, converts degrees to radians with pi/180, builds the current matrix at obj+0xec through matrix4_make_axis_angle, and sets retained-gfx redraw dirty owner+0xb558 (EngineCtx+0x51b6c). The finite-channel consumer composes this current matrix between scale and translation. Corpus: 186/187 calls use Z axis (0,0,1); the lone DEBUG call uses Y axis (0,1,0).
@@ -1203,6 +1207,7 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
### 0x2 `exit` (exit, argc 0) ### 0x2 `exit` (exit, argc 0)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
- **evidence:** Ghidra /v2: op_0xba_sfx_start_loop@0x420290 records a 3-dword instruction, fetches channel operand 1, and calls sfx_channel_start@0x4825d0 with logical loop mode 1. The same worker receives mode 0 from op 0xb5. sound_stream_fill_quarter@0x483b70 rewinds the decoder at EOF only for mode 1. All 334 Himegari sites pass literal channel 9 immediately after op 0xb4 loads that channel.
### 0x4 `u00417E30` (u00417E30, argc 2) ### 0x4 `u00417E30` (u00417E30, argc 2)
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. - **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
@@ -1660,10 +1665,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low
### 0xba `u0041D0B0` (u0041D0B0, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0xbb `u0041D250` (u0041D250, argc 1) ### 0xbb `u0041D250` (u0041D250, argc 1)
- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. - **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it.
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low

View File

@@ -788,6 +788,26 @@ Linux/macOS FFmpeg builds and packaging smoke gates when work returns to cross-p
Post-cleanup validation passes all 405 engine tests, the warning-free Godot build, threaded selftest, opcode Post-cleanup validation passes all 405 engine tests, the warning-free Godot build, threaded selftest, opcode
lint/tooling, and the 213/213 installed video/audio corpus gate. lint/tooling, and the 213/213 installed video/audio corpus gate.
**CORPUS-WIDE OPCODE HARDENING PASS STARTED (2026-07-28).**
With numbered-save implementation complete and further live save testing explicitly deferred, the active
breadth pass now ranks every Himegari-observed VM fallthrough across all 481 decoded scripts. The starting
inventory has only 25 distinct effectful gaps. The first tranche closes the two widest independent holes:
- `0xba` occurs 334 times in 100 scripts. Native `op_0xba_sfx_start_loop@0x420290` calls the same retained
SFX start worker as one-shot `0xb5`, but with logical loop mode 1. The host seam now preserves that mode
and Godot rewinds the WAV stream at EOF.
- `0x1fe` occurs 187 times in 47 scripts. Native
`op_0x1fe_set_rotation_current@0x422700` immediately replaces the retained object's current axis-angle
rotation. The existing compositor already sampled that field; the VM now writes it directly.
Together these handlers close 521 formerly skipped instructions and reduce the corpus-wide effectful
fallthrough inventory from 25 to 23 distinct opcodes. Focused regressions distinguish one-shot/looping SFX
starts and prove immediate rotation does not arm the delayed `0x21f` channel. The opcode registry and
generated references are current, and the native `/v2` image is annotated and saved. **NEXT OPCODE STEP:**
rank the remaining 23 by systemic state risk, beginning with the 31-site retained-animation opcode `0x233`
and the CONFIG audio/settings cluster (`0xc5`, `0xc6`, `0x1ba`), rather than treating low-frequency
developer-only calls as equally urgent.
## Later Phase B breadth ## Later Phase B breadth
**INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static **INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static

View File

@@ -384,6 +384,25 @@ public class GfxAnimationTests
Assert.Equal((12L, 34L, 5L), o.V16c); Assert.Equal((12L, 34L, 5L), o.V16c);
} }
[Fact]
public void CurrentRotationSetter_ReplacesTheLiveAxisAngleImmediately()
{
var t = T();
var scene = ScriptAssembler.Assemble(t, "CURRENTROTATION", new List<(int, Operand[])>
{
MovGI(1, 0xcb20), MovGI(2, 0), MovGI(3, 1), MovGI(4, 0), MovGI(5, 180),
(0x1fe, new[] { G(1), G(2), G(3), G(4), G(5) }),
Exit(),
}, System.Array.Empty<string>());
var vm = new VirtualMachine(scene, t, new RecordingHost());
vm.Run();
var o = vm.Gfx.TryGet(0xcb20)!;
Assert.Equal((0.0, 1.0, 0.0, 180.0), o.RotationCurrent);
Assert.False(o.RotationChannelEnabled);
}
[Fact] [Fact]
public void ResetAnimClock_DispatchClearsGlobalServiceClock() public void ResetAnimClock_DispatchClearsGlobalServiceClock()
{ {

View File

@@ -41,6 +41,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) }),
(0xb4, new[] { new Operand(0, 0x2aea), new Operand(0, 9) }),
(0xba, new[] { new Operand(0, 9) }),
(0x1cf, new[] { new Operand(0, 1) }), (0x1cf, new[] { new Operand(0, 1) }),
(0x2bf, new[] { new Operand(0, 4), new Operand(0, 0), new Operand(0, 100) }), (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) }),
@@ -54,8 +56,9 @@ public class SfxOpsTests
vm.Run(); vm.Run();
Assert.Equal("exit", vm.HaltReason); Assert.Equal("exit", vm.HaltReason);
Assert.Equal((0x28L, 0), Assert.Single(host.SfxLoads)); Assert.Equal([(0x28L, 0), (0x2aeaL, 9)], host.SfxLoads);
Assert.Equal(0, Assert.Single(host.SfxStarts)); Assert.Equal([0, 9], host.SfxStarts);
Assert.Equal([(0, 0), (9, 1)], host.SfxStartRequests);
Assert.Equal(1, Assert.Single(host.VoiceBgmDuckControls)); Assert.Equal(1, Assert.Single(host.VoiceBgmDuckControls));
Assert.Equal((4, 0, 100L), Assert.Single(host.ScheduledSfxStarts)); Assert.Equal((4, 0, 100L), Assert.Single(host.ScheduledSfxStarts));
Assert.Equal(0, Assert.Single(host.SfxReleases)); Assert.Equal(0, Assert.Single(host.SfxReleases));

View File

@@ -39,6 +39,7 @@ internal class RecordingHost : IHost
public readonly List<(long Id, int PlaybackVariant, long DelayMs)> ScheduledVoiceRequests = new(); public readonly List<(long Id, int PlaybackVariant, long DelayMs)> ScheduledVoiceRequests = new();
public readonly List<long> VoiceBgmDuckControls = 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)> SfxStartRequests = new();
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = 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<long> BgmTracks = new(); public readonly List<long> BgmTracks = new();
@@ -171,7 +172,12 @@ internal class RecordingHost : IHost
public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs) public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs)
=> ScheduledVoiceRequests.Add((id, playbackVariant, delayMs)); => ScheduledVoiceRequests.Add((id, playbackVariant, delayMs));
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) => StartSoundEffect(channel, 0);
public void StartSoundEffect(int channel, int startMode)
{
SfxStarts.Add(channel);
SfxStartRequests.Add((channel, startMode));
}
public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs)
=> ScheduledSfxStarts.Add((channel, startMode, delayMs)); => ScheduledSfxStarts.Add((channel, startMode, delayMs));
public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel); public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel);

View File

@@ -133,6 +133,9 @@ public interface IHost
void SetVoiceBgmDuckControl(long flags) { } void SetVoiceBgmDuckControl(long flags) { }
void LoadSoundEffect(long resourceId, int channel) { } void LoadSoundEffect(long resourceId, int channel) { }
void StartSoundEffect(int channel) { } void StartSoundEffect(int channel) { }
// Ops 0xb5/0xba share the native channel-start worker. Mode 0 plays once; mode 1
// rewinds the decoder at EOF. The one-argument seam remains for simple hosts.
void StartSoundEffect(int channel, int startMode) => StartSoundEffect(channel);
// Native SetDelay (op 0x2bf) starts an already-loaded channel after delayMs. // Native SetDelay (op 0x2bf) starts an already-loaded channel after delayMs.
// startMode is forwarded to the same worker used by immediate SFX starts. // startMode is forwarded to the same worker used by immediate SFX starts.
void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) { } void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) { }

View File

@@ -979,6 +979,17 @@ public sealed class GfxState
} }
} }
/// <summary>Op 0x1fe: immediately replace the object's current axis-angle rotation matrix at
/// obj+0xec. Axis components and angle are retained as native floating-point values.</summary>
public void SetCurrentRotation(long handle, (long X, long Y, long Z) axis, long angleDegrees)
{
lock (_lock)
{
var o = GetOrCreate(handle);
o.RotationCurrent = (axis.X, axis.Y, axis.Z, angleDegrees);
}
}
/// <summary>Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c.</summary> /// <summary>Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c.</summary>
public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation) public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation)
{ {

View File

@@ -2398,8 +2398,10 @@ public sealed class VirtualMachine
_host.LoadSoundEffect(resourceId, channel); _host.LoadSoundEffect(resourceId, channel);
return pc + 1; return pc + 1;
} }
case "u0041D050": // 0xb5 / semantics: sfx-start case "u0041D050": // 0xb5 / semantics: sfx-start-once
_host.StartSoundEffect((int)Read(a[0])); return pc + 1; _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 case "u0041D080": // 0xb6 / semantics: sfx-release
{ {
int channel = (int)Read(a[0]); int channel = (int)Read(a[0]);
@@ -2554,6 +2556,9 @@ public sealed class VirtualMachine
case "u00420620": // upstream ABI label case "u00420620": // upstream ABI label
case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix case "gfx-set-scale-current": // 0x1fd (handle)(sx%)(sy%)(sz%) -> current scale matrix
Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1; Gfx.SetCurrentScale(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
case "set-current-rotation-axis-angle": // 0x1fe (handle)(axis x/y/z)(angle degrees)
Gfx.SetCurrentRotation(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3])), Read(a[4]));
return pc + 1;
case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle) case "set-adv-wait-indicator-handle": // 0x212 (layout)(retained handle)
{ {
TextHistory.SetWaitIndicatorObjectHandle((int)Read(a[0]), Read(a[1])); TextHistory.SetWaitIndicatorObjectHandle((int)Read(a[0]), Read(a[1]));

View File

@@ -1608,12 +1608,14 @@ public sealed class GodotAdvHost : IHost
if (audio != null) _main.CallDeferred("LoadSoundEffect", audio.Bytes, audio.Name, channel); if (audio != null) _main.CallDeferred("LoadSoundEffect", audio.Bytes, audio.Name, channel);
} }
public void StartSoundEffect(int channel) public void StartSoundEffect(int channel) => StartSoundEffect(channel, 0);
public void StartSoundEffect(int channel, int startMode)
{ {
if ((uint)channel >= (uint)_sfxNames.Length || _sfxNames[channel] == null) return; if ((uint)channel >= (uint)_sfxNames.Length || _sfxNames[channel] == null) return;
_timeline?.Event("sfx-start", new() { ["channel"] = channel, _timeline?.Event("sfx-start", new() { ["channel"] = channel,
["file"] = _sfxNames[channel] }); ["start_mode"] = startMode, ["file"] = _sfxNames[channel] });
_main.CallDeferred("StartSoundEffect", channel); _main.CallDeferred("StartSoundEffect", channel, startMode);
} }
public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs)

View File

@@ -1890,10 +1890,16 @@ public partial class Main : Godot.Control
_sfx[channel].Stream = stream; _sfx[channel].Stream = stream;
} }
public void StartSoundEffect(int channel) public void StartSoundEffect(int channel) => StartSoundEffect(channel, 0);
public void StartSoundEffect(int channel, int startMode)
{ {
if ((uint)channel < (uint)_sfx.Length && _sfx[channel].Stream != null) if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return;
_sfx[channel].Play(); if (_sfx[channel].Stream is AudioStreamWav wav)
wav.LoopMode = startMode == 0
? AudioStreamWav.LoopModeEnum.Disabled
: AudioStreamWav.LoopModeEnum.Forward;
_sfx[channel].Play();
} }
public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds) public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds)

View File

@@ -66,7 +66,6 @@ INFERRED: dict[int, dict] = {
0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'), 0x1f4: dict(name='stmt-begin', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; opens scripts, pairs with stmt-end 0x1f5'),
0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'), 0x1f5: dict(name='stmt-end', category='marker', noop=True, confidence='high', source='investigation', summary='zero-arg; precedes exit/next-stmt, pairs with 0x1f4'),
0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."), 0x1fd: dict(name='gfx-set-scale-current', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the retained object's current scale matrix at obj+0x6c. The handler divides each integer operand by 100.0 before calling matrix4_make_scale; this is distinct from 0x21e's delayed one-shot target scale."),
0x1fe: dict(name='set-current-rotation-axis-angle', category='draw', noop=False, confidence='high', source='investigation', summary="(handle)(axis_x)(axis_y)(axis_z)(angle_degrees) — immediately replace the retained object's current axis-angle rotation matrix. This is the direct-current companion to op 0x21f's delayed one-shot rotation target."),
0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."), 0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."),
0x205: dict(name='draw-formatted-integer', category='draw', noop=False, confidence='high', source='investigation', summary='Format an integer into a fixed-width decimal field and rasterize it immediately into a numbered graphics surface using the current font/color/effect state.'), 0x205: dict(name='draw-formatted-integer', category='draw', noop=False, confidence='high', source='investigation', summary='Format an integer into a fixed-width decimal field and rasterize it immediately into a numbered graphics surface using the current font/color/effect state.'),
0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'), 0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'),

View File

@@ -67,7 +67,7 @@ noop_headless = false
source = "kelebek" source = "kelebek"
confidence = "med" confidence = "med"
depends_on = [] depends_on = []
evidence = "" evidence = "Ghidra /v2: op_0xba_sfx_start_loop@0x420290 records a 3-dword instruction, fetches channel operand 1, and calls sfx_channel_start@0x4825d0 with logical loop mode 1. The same worker receives mode 0 from op 0xb5. sound_stream_fill_quarter@0x483b70 rewinds the decoder at EOF only for mode 1. All 334 Himegari sites pass literal channel 9 immediately after op 0xb4 loads that channel."
[[opcode]] [[opcode]]
op = 0x3 op = 0x3
@@ -1860,17 +1860,17 @@ observed_types = ["imm"]
[[opcode]] [[opcode]]
op = 0xba op = 0xba
label = "u0041D0B0" label = "sfx-start-loop"
argc = 1 argc = 1
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041D0B0" name = "sfx-start-loop"
category = "unknown" category = "audio"
summary = "" summary = "(channel) — start the already-loaded sound-effect channel with logical loop mode 1; the streaming decoder rewinds at EOF until the channel is replaced or released."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" evidence = ""
@@ -4794,7 +4794,7 @@ observed_types = ["imm"]
[[opcode]] [[opcode]]
op = 0x1fe op = 0x1fe
label = "u004206C0" label = "set-current-rotation-axis-angle"
argc = 5 argc = 5
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"