From dad3ed4a4b489f0047df4f69b90b139a65046383 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 28 Jul 2026 23:45:48 -0400 Subject: [PATCH] Implement looping SFX and immediate rotation opcodes --- docs/engine-re.md | 15 ++++++++++++--- docs/opcode-reference.md | 11 ++++++----- docs/phase-b-framework.md | 20 ++++++++++++++++++++ engine/Age.Engine.Tests/GfxAnimationTests.cs | 19 +++++++++++++++++++ engine/Age.Engine.Tests/SfxOpsTests.cs | 7 +++++-- engine/Age.Engine.Tests/TestSupport.cs | 8 +++++++- engine/Age.Engine/Hosting/IHost.cs | 3 +++ engine/Age.Engine/Model/GfxState.cs | 11 +++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 9 +++++++-- godot/GodotAdvHost.cs | 8 +++++--- godot/Main.cs | 12 +++++++++--- tools/age_opcodes_himegari.py | 1 - vm-map/opcodes.toml | 16 ++++++++-------- 13 files changed, 112 insertions(+), 28 deletions(-) diff --git a/docs/engine-re.md b/docs/engine-re.md index 7aeff8b..7546a81 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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. 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: - `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 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 - 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` -> `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. +- `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 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. 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) **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 36faf5f..d2195dc 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -201,6 +201,10 @@ The requested CP932 face is copied into the primary LOGFONT lfFaceName and AGE a - **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. +### 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) - **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 @@ -702,7 +706,7 @@ The handler clears the map embedded at retained-gfx owner+0x408, resets its coun - **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. -### 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. - **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). @@ -1203,6 +1207,7 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting ### 0x2 `exit` (exit, argc 0) - **summary:** — - **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) - **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:** — - **grounding:** source=kelebek, confidence=low -### 0xba `u0041D0B0` (u0041D0B0, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0xbb `u0041D250` (u0041D250, argc 1) - **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 81d947d..59b2566 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -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 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 **INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static diff --git a/engine/Age.Engine.Tests/GfxAnimationTests.cs b/engine/Age.Engine.Tests/GfxAnimationTests.cs index f077710..61ebb00 100644 --- a/engine/Age.Engine.Tests/GfxAnimationTests.cs +++ b/engine/Age.Engine.Tests/GfxAnimationTests.cs @@ -384,6 +384,25 @@ public class GfxAnimationTests 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()); + 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] public void ResetAnimClock_DispatchClearsGlobalServiceClock() { diff --git a/engine/Age.Engine.Tests/SfxOpsTests.cs b/engine/Age.Engine.Tests/SfxOpsTests.cs index fec270e..f4fb57c 100644 --- a/engine/Age.Engine.Tests/SfxOpsTests.cs +++ b/engine/Age.Engine.Tests/SfxOpsTests.cs @@ -41,6 +41,8 @@ public class SfxOpsTests { (0xb4, new[] { new Operand(0, 0x28), 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) }), (0x2bf, new[] { new Operand(0, 4), new Operand(0, 0), new Operand(0, 100) }), (0xb6, new[] { new Operand(0, 0) }), @@ -54,8 +56,9 @@ public class SfxOpsTests vm.Run(); Assert.Equal("exit", vm.HaltReason); - Assert.Equal((0x28L, 0), Assert.Single(host.SfxLoads)); - Assert.Equal(0, Assert.Single(host.SfxStarts)); + Assert.Equal([(0x28L, 0), (0x2aeaL, 9)], host.SfxLoads); + Assert.Equal([0, 9], host.SfxStarts); + Assert.Equal([(0, 0), (9, 1)], host.SfxStartRequests); Assert.Equal(1, Assert.Single(host.VoiceBgmDuckControls)); Assert.Equal((4, 0, 100L), Assert.Single(host.ScheduledSfxStarts)); Assert.Equal(0, Assert.Single(host.SfxReleases)); diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index bc15f60..af512a9 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -39,6 +39,7 @@ internal class RecordingHost : IHost public readonly List<(long Id, int PlaybackVariant, long DelayMs)> ScheduledVoiceRequests = new(); public readonly List VoiceBgmDuckControls = new(); public readonly List 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 SfxReleases = new(); public readonly List BgmTracks = new(); @@ -171,7 +172,12 @@ internal class RecordingHost : IHost public void ScheduleVoicePlayback(long id, int playbackVariant, long delayMs) => ScheduledVoiceRequests.Add((id, playbackVariant, delayMs)); 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) => ScheduledSfxStarts.Add((channel, startMode, delayMs)); public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel); diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs index 2232453..40dc3df 100644 --- a/engine/Age.Engine/Hosting/IHost.cs +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -133,6 +133,9 @@ public interface IHost void SetVoiceBgmDuckControl(long flags) { } void LoadSoundEffect(long resourceId, 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. // startMode is forwarded to the same worker used by immediate SFX starts. void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) { } diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index f79b05b..9cc48af 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -979,6 +979,17 @@ public sealed class GfxState } } + /// 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. + 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); + } + } + /// Op 0x1ff: immediately replace the object's current translation matrix at obj+0x16c. public void SetCurrentTranslation(long handle, (long X, long Y, long Z) translation) { diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index b16d8f7..3e6c7cb 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -2398,8 +2398,10 @@ public sealed class VirtualMachine _host.LoadSoundEffect(resourceId, channel); return pc + 1; } - case "u0041D050": // 0xb5 / semantics: sfx-start - _host.StartSoundEffect((int)Read(a[0])); 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]); @@ -2554,6 +2556,9 @@ public sealed class VirtualMachine case "u00420620": // upstream ABI label 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; + 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) { TextHistory.SetWaitIndicatorObjectHandle((int)Read(a[0]), Read(a[1])); diff --git a/godot/GodotAdvHost.cs b/godot/GodotAdvHost.cs index e70f6fc..fdb3819 100644 --- a/godot/GodotAdvHost.cs +++ b/godot/GodotAdvHost.cs @@ -1608,12 +1608,14 @@ public sealed class GodotAdvHost : IHost 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; _timeline?.Event("sfx-start", new() { ["channel"] = channel, - ["file"] = _sfxNames[channel] }); - _main.CallDeferred("StartSoundEffect", channel); + ["start_mode"] = startMode, ["file"] = _sfxNames[channel] }); + _main.CallDeferred("StartSoundEffect", channel, startMode); } public void ScheduleSoundEffectStart(int channel, int startMode, long delayMs) diff --git a/godot/Main.cs b/godot/Main.cs index 9bb3aac..65943e1 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -1890,10 +1890,16 @@ public partial class Main : Godot.Control _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) - _sfx[channel].Play(); + if ((uint)channel >= (uint)_sfx.Length || _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(); } public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds) diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index 00b9339..bc04e53 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -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'), 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."), - 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."), 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.'), diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index fa47662..f49edd1 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -67,7 +67,7 @@ noop_headless = false source = "kelebek" confidence = "med" 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]] op = 0x3 @@ -1860,17 +1860,17 @@ observed_types = ["imm"] [[opcode]] op = 0xba -label = "u0041D0B0" +label = "sfx-start-loop" argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041D0B0" -category = "unknown" -summary = "" +name = "sfx-start-loop" +category = "audio" +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 -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] evidence = "" @@ -4794,7 +4794,7 @@ observed_types = ["imm"] [[opcode]] op = 0x1fe -label = "u004206C0" +label = "set-current-rotation-axis-angle" argc = 5 abi_source = "kelebek+decode-validated"