Implement held ADV fast-forward input
This commit is contained in:
@@ -2292,6 +2292,12 @@ poll/dispatch ops `0xff` and `0x100`. These calls are effectful engine configura
|
|||||||
`input_poll_joystick_action_bits@0x460380`. The first three logical actions are therefore not a generic
|
`input_poll_joystick_action_bits@0x460380`. The first three logical actions are therefore not a generic
|
||||||
Godot UI ordering; they are the native engine ABI established by the input manager and refined by scripts.
|
Godot UI ordering; they are the native engine ABI established by the input manager and refined by scripts.
|
||||||
|
|
||||||
|
The same mask is also consumed directly by `adv_interpreter_tick@0x410fb0`. Logical action 6 is bit
|
||||||
|
`0x40`, the exact bit used by the transient ADV fast-forward/run-state path. There is no Ctrl-specific
|
||||||
|
branch: SYSTEM4's C and LeftCtrl mappings, the retained native Backspace default, and any joystick mapping
|
||||||
|
that emits action 6 all reach the same hold-to-fast-forward mechanism. Persistent op-`0x88` Skip injects
|
||||||
|
that same bit independently each tick.
|
||||||
|
|
||||||
The port now models this as one process-owned `InputBindings` service. It starts with the native seven
|
The port now models this as one process-owned `InputBindings` service. It starts with the native seven
|
||||||
keyboard defaults, the four configuration handlers mutate it, and `0xff` combines live keyboard-VK,
|
keyboard defaults, the four configuration handlers mutate it, and `0xff` combines live keyboard-VK,
|
||||||
left/right mouse, joystick-axis, and joystick-button state with the narrow logical injection used by tests.
|
left/right mouse, joystick-axis, and joystick-button state with the narrow logical injection used by tests.
|
||||||
@@ -2300,8 +2306,10 @@ uses callback slot `count` only for an empty mask. Godot translates layout-indep
|
|||||||
native Win32 VK namespace and sends physical mouse/standard joy events through the service; it no longer
|
native Win32 VK namespace and sends physical mouse/standard joy events through the service; it no longer
|
||||||
assigns Godot's `ui_*` actions directly to AGE indices. Direct-scene diagnostics replay the same 16 immediate
|
assigns Godot's `ui_*` actions directly to AGE indices. Direct-scene diagnostics replay the same 16 immediate
|
||||||
SYSTEM4 configuration calls through `InputBindingBootstrap`, while natural boot executes the real opcodes.
|
SYSTEM4 configuration calls through `InputBindingBootstrap`, while natural boot executes the real opcodes.
|
||||||
This remains frontend input configuration only: it needs no profile storage, boot seed, or game-specific
|
The live action-6 state now also feeds the ADV fast-forward host channel while the ADV lifecycle service is
|
||||||
conditional.
|
enabled. That transient channel is kept separate from persistent op-`0x88` Skip, so releasing the held key
|
||||||
|
cannot clear the user's toggle; both channels share the existing text completion, wait advance, voice
|
||||||
|
deferral, and skip cadence. This needs no profile storage, boot seed, or game-specific conditional.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -824,7 +824,7 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
|||||||
- **summary:** (out) - write 1 iff ADV message-skip run-state bit 0x08000000 is set, otherwise 0.
|
- **summary:** (out) - write 1 iff ADV message-skip run-state bit 0x08000000 is set, otherwise 0.
|
||||||
- **grounding:** source=investigation, confidence=high
|
- **grounding:** source=investigation, confidence=high
|
||||||
- **depended on by:** 0x20c, 0x20d, 0x21c, 0x223
|
- **depended on by:** 0x20c, 0x20d, 0x21c, 0x223
|
||||||
- **evidence:** Ghidra handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). SC0000 label_1235a ORs it with op 0x1cc.
|
- **evidence:** Ghidra /v2: handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). adv_interpreter_tick@0x410fb0 first polls the configured logical-action mask; action 6 is bit 0x40 and therefore drives this transient run state from SYSTEM4's C/LeftCtrl bindings or the retained native Backspace default. Persistent op-0x88 state injects the same bit. SC0000 label_1235a ORs the result with op 0x1cc.
|
||||||
|
|
||||||
### 0x1ca `set-read-message-skip` (u0041B9B0, argc 1)
|
### 0x1ca `set-read-message-skip` (u0041B9B0, argc 1)
|
||||||
- **summary:** (enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.
|
- **summary:** (enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.
|
||||||
|
|||||||
@@ -2190,3 +2190,19 @@ empty-mask dispatch, simultaneous/bounded actions, and native range failures.
|
|||||||
**Next:** manually exercise TITLE, ROOM, and ADV with keyboard, left/right mouse, and—if available—a
|
**Next:** manually exercise TITLE, ROOM, and ADV with keyboard, left/right mouse, and—if available—a
|
||||||
controller. A clean check should confirm native direction order, Z/Enter/Space behavior, right-click action
|
controller. A clean check should confirm native direction order, Z/Enter/Space behavior, right-click action
|
||||||
7, and release re-arming before selecting another reached Phase-B cluster.
|
7, and release re-arming before selecting another reached Phase-B cluster.
|
||||||
|
|
||||||
|
### Slice B1 ADV held fast-forward implemented (2026-07-21)
|
||||||
|
|
||||||
|
Manual validation found the general input bindings sound but LeftCtrl did not accelerate ADV. Native tracing
|
||||||
|
resolves the missing consumer: `adv_interpreter_tick@0x410fb0` polls the configured logical mask and uses
|
||||||
|
action 6's bit `0x40` for transient fast-forward. It does not test Ctrl directly, so SYSTEM4's C/LeftCtrl
|
||||||
|
bindings and the engine's retained Backspace default intentionally share the behavior.
|
||||||
|
|
||||||
|
The VM now forwards held action 6 through an ADV-lifecycle-gated physical skip channel. Godot combines it
|
||||||
|
with, but stores it separately from, persistent op-`0x88` Skip; release therefore restores normal playback
|
||||||
|
only when the persistent channel is also off. The combined state reuses text completion, wait advance,
|
||||||
|
voice deferral/release, and skip pacing. Focused tests cover Backspace/C/Ctrl, activation on ADV resume, and
|
||||||
|
persistent-toggle survival across a physical press/release.
|
||||||
|
|
||||||
|
**Next:** manually hold LeftCtrl through text reveal and multiple waits in SC0000, then confirm release
|
||||||
|
returns to normal cadence and that the on-screen Skip toggle remains active if enabled during the test.
|
||||||
|
|||||||
@@ -68,6 +68,59 @@ public class InputBindingTests
|
|||||||
Assert.Equal(7, bindings.MouseAction(1));
|
Assert.Equal(7, bindings.MouseAction(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0x08)] // retained native Backspace default
|
||||||
|
[InlineData(0x11)] // SYSTEM4 LeftCtrl binding
|
||||||
|
[InlineData(0x43)] // SYSTEM4 C binding
|
||||||
|
public void LogicalActionSixDrivesHeldAdvFastForward(int virtualKey)
|
||||||
|
{
|
||||||
|
var script = ScriptAssembler.Assemble(Table, "ADV_FAST_FORWARD", new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0x19c, Array.Empty<Operand>()),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
}, Array.Empty<string>());
|
||||||
|
var host = new RecordingHost();
|
||||||
|
var vm = new VirtualMachine(script, Table, host);
|
||||||
|
InputBindingBootstrap.Apply(
|
||||||
|
Sys4ScriptProvider.Load(Table).RequireByName("SYSTEM4.BIN"), vm.InputBindings);
|
||||||
|
|
||||||
|
vm.UpdateKeyboardVirtualKeyState(virtualKey, true);
|
||||||
|
Assert.False(host.PhysicalMessageSkip); // ADV lifecycle is not active yet.
|
||||||
|
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
Assert.True(host.PhysicalMessageSkip);
|
||||||
|
Assert.True(host.MessageSkip);
|
||||||
|
|
||||||
|
vm.UpdateKeyboardVirtualKeyState(virtualKey, false);
|
||||||
|
|
||||||
|
Assert.False(host.PhysicalMessageSkip);
|
||||||
|
Assert.False(host.MessageSkip);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReleasingPhysicalFastForwardDoesNotClearPersistentSkip()
|
||||||
|
{
|
||||||
|
var script = ScriptAssembler.Assemble(Table, "ADV_PERSISTENT_AND_HELD_SKIP", new List<(int, Operand[])>
|
||||||
|
{
|
||||||
|
(0x19c, Array.Empty<Operand>()),
|
||||||
|
(0x88, new[] { I(1) }),
|
||||||
|
(0x2, Array.Empty<Operand>()),
|
||||||
|
}, Array.Empty<string>());
|
||||||
|
var host = new RecordingHost();
|
||||||
|
var vm = new VirtualMachine(script, Table, host);
|
||||||
|
InputBindingBootstrap.Apply(
|
||||||
|
Sys4ScriptProvider.Load(Table).RequireByName("SYSTEM4.BIN"), vm.InputBindings);
|
||||||
|
vm.Run();
|
||||||
|
|
||||||
|
vm.UpdateKeyboardVirtualKeyState(0x11, true);
|
||||||
|
vm.UpdateKeyboardVirtualKeyState(0x11, false);
|
||||||
|
|
||||||
|
Assert.True(vm.MessageSkipEnabled);
|
||||||
|
Assert.False(host.PhysicalMessageSkip);
|
||||||
|
Assert.True(host.MessageSkip);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EmptyPollDispatchesTheCallbackAtActionCount()
|
public void EmptyPollDispatchesTheCallbackAtActionCount()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ internal class RecordingHost : IHost
|
|||||||
public int TransitionWaits;
|
public int TransitionWaits;
|
||||||
public int InputCallbackFrames;
|
public int InputCallbackFrames;
|
||||||
public bool MessageSkip;
|
public bool MessageSkip;
|
||||||
|
public bool ScriptMessageSkip;
|
||||||
|
public bool PhysicalMessageSkip;
|
||||||
public bool AdvReadSkip;
|
public bool AdvReadSkip;
|
||||||
public readonly List<(int Offset, string Text)> Lines = new();
|
public readonly List<(int Offset, string Text)> Lines = new();
|
||||||
public readonly List<(int Slot, int X, int Y)> TextCursors = new();
|
public readonly List<(int Slot, int X, int Y)> TextCursors = new();
|
||||||
@@ -41,6 +43,7 @@ internal class RecordingHost : IHost
|
|||||||
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
|
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
|
||||||
public readonly List<(int Source, int Target, long Interval)> SurfaceCrossfades = new();
|
public readonly List<(int Source, int Target, long Interval)> SurfaceCrossfades = new();
|
||||||
public readonly List<bool> MessageSkipChanges = new();
|
public readonly List<bool> MessageSkipChanges = new();
|
||||||
|
public readonly List<bool> PhysicalMessageSkipChanges = new();
|
||||||
public readonly List<long> CursorResources = new();
|
public readonly List<long> CursorResources = new();
|
||||||
public readonly List<bool> AdvPagePresentationSuspended = new();
|
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||||
public int CursorClearCount;
|
public int CursorClearCount;
|
||||||
@@ -94,8 +97,15 @@ internal class RecordingHost : IHost
|
|||||||
public bool IsMessageSkipActive => MessageSkip;
|
public bool IsMessageSkipActive => MessageSkip;
|
||||||
public void SetMessageSkipActive(bool active)
|
public void SetMessageSkipActive(bool active)
|
||||||
{
|
{
|
||||||
MessageSkip = active;
|
ScriptMessageSkip = active;
|
||||||
MessageSkipChanges.Add(active);
|
MessageSkip = ScriptMessageSkip || PhysicalMessageSkip;
|
||||||
|
MessageSkipChanges.Add(MessageSkip);
|
||||||
|
}
|
||||||
|
public void SetPhysicalMessageSkipActive(bool active)
|
||||||
|
{
|
||||||
|
PhysicalMessageSkip = active;
|
||||||
|
MessageSkip = ScriptMessageSkip || PhysicalMessageSkip;
|
||||||
|
PhysicalMessageSkipChanges.Add(active);
|
||||||
}
|
}
|
||||||
public bool IsAdvReadSkipActive => AdvReadSkip;
|
public bool IsAdvReadSkipActive => AdvReadSkip;
|
||||||
public void PresentFrame(GfxState gfx)
|
public void PresentFrame(GfxState gfx)
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ public interface IHost
|
|||||||
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
|
||||||
// hosts default to normal playback; the Godot host supplies the live interactive values.
|
// hosts default to normal playback; the Godot host supplies the live interactive values.
|
||||||
void SetMessageSkipActive(bool active) { }
|
void SetMessageSkipActive(bool active) { }
|
||||||
|
// Logical action 6 is the native hold-to-fast-forward channel. Keep it separate from the
|
||||||
|
// persistent op-0x88 channel so releasing the key cannot turn off the user's Skip toggle.
|
||||||
|
void SetPhysicalMessageSkipActive(bool active) { }
|
||||||
bool IsMessageSkipActive => false;
|
bool IsMessageSkipActive => false;
|
||||||
bool IsAdvReadSkipActive => false;
|
bool IsAdvReadSkipActive => false;
|
||||||
// Normal playback reaches op 0x21c and parks until a queued 0x223 transition completes. The
|
// Normal playback reaches op 0x21c and parks until a queued 0x223 transition completes. The
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public sealed class VirtualMachine
|
|||||||
private bool _initialRootRun = true;
|
private bool _initialRootRun = true;
|
||||||
private volatile bool _messageSkipEnabled;
|
private volatile bool _messageSkipEnabled;
|
||||||
private volatile bool _messageSkipServiceActive;
|
private volatile bool _messageSkipServiceActive;
|
||||||
|
private volatile bool _advSkipServiceEnabled;
|
||||||
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
||||||
private readonly Dictionary<string, int> _valueSwitchTargets = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, int> _valueSwitchTargets = new(StringComparer.Ordinal);
|
||||||
public long CallScriptDispatches { get; private set; }
|
public long CallScriptDispatches { get; private set; }
|
||||||
@@ -157,6 +158,7 @@ public sealed class VirtualMachine
|
|||||||
public int UpdateKeyboardVirtualKeyState(int virtualKey, bool pressed)
|
public int UpdateKeyboardVirtualKeyState(int virtualKey, bool pressed)
|
||||||
{
|
{
|
||||||
InputBindings.UpdateKeyboardVirtualKey(virtualKey, pressed);
|
InputBindings.UpdateKeyboardVirtualKey(virtualKey, pressed);
|
||||||
|
RefreshPhysicalMessageSkipState();
|
||||||
_host.WakeInputCallbackService();
|
_host.WakeInputCallbackService();
|
||||||
return InputBindings.KeyboardAction(virtualKey);
|
return InputBindings.KeyboardAction(virtualKey);
|
||||||
}
|
}
|
||||||
@@ -164,6 +166,7 @@ public sealed class VirtualMachine
|
|||||||
public int UpdatePhysicalMouseButtonState(int physicalButton, bool pressed)
|
public int UpdatePhysicalMouseButtonState(int physicalButton, bool pressed)
|
||||||
{
|
{
|
||||||
InputBindings.UpdateMouseButton(physicalButton, pressed);
|
InputBindings.UpdateMouseButton(physicalButton, pressed);
|
||||||
|
RefreshPhysicalMessageSkipState();
|
||||||
_host.WakeInputCallbackService();
|
_host.WakeInputCallbackService();
|
||||||
return InputBindings.MouseAction(physicalButton);
|
return InputBindings.MouseAction(physicalButton);
|
||||||
}
|
}
|
||||||
@@ -171,6 +174,7 @@ public sealed class VirtualMachine
|
|||||||
public int UpdateJoystickButtonState(int physicalButton, bool pressed)
|
public int UpdateJoystickButtonState(int physicalButton, bool pressed)
|
||||||
{
|
{
|
||||||
InputBindings.UpdateJoystickButton(physicalButton, pressed);
|
InputBindings.UpdateJoystickButton(physicalButton, pressed);
|
||||||
|
RefreshPhysicalMessageSkipState();
|
||||||
_host.WakeInputCallbackService();
|
_host.WakeInputCallbackService();
|
||||||
return InputBindings.JoystickButtonActionMask(physicalButton);
|
return InputBindings.JoystickButtonActionMask(physicalButton);
|
||||||
}
|
}
|
||||||
@@ -211,6 +215,16 @@ public sealed class VirtualMachine
|
|||||||
} while (Interlocked.CompareExchange(ref field, after, before) != before);
|
} while (Interlocked.CompareExchange(ref field, after, before) != before);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Mirror adv_interpreter_tick's bit-0x40 path. The bit is logical action 6 from the
|
||||||
|
/// process-owned binding map, not a hardcoded Ctrl test; Himegari also binds C and retains the
|
||||||
|
/// engine's default Backspace binding. The presentation lifecycle gate prevents a held action
|
||||||
|
/// from leaking into non-ADV script execution.</summary>
|
||||||
|
private void RefreshPhysicalMessageSkipState()
|
||||||
|
{
|
||||||
|
bool active = _advSkipServiceEnabled && (InputBindings.PollActionMask() & 0x40) != 0;
|
||||||
|
_host.SetPhysicalMessageSkipActive(active);
|
||||||
|
}
|
||||||
|
|
||||||
private static long Gi(Dictionary<int, long> d, int k) => d.TryGetValue(k, out var v) ? v : 0;
|
private static long Gi(Dictionary<int, long> d, int k) => d.TryGetValue(k, out var v) ? v : 0;
|
||||||
private long ReadGlobal(int k) => ExternalGlobals.TryGetValue(k, out var v) ? v : Gi(Globals, k);
|
private long ReadGlobal(int k) => ExternalGlobals.TryGetValue(k, out var v) ? v : Gi(Globals, k);
|
||||||
private static string Gs(Dictionary<int, string> d, int k) => d.TryGetValue(k, out var v) ? v : "";
|
private static string Gs(Dictionary<int, string> d, int k) => d.TryGetValue(k, out var v) ? v : "";
|
||||||
@@ -465,9 +479,11 @@ public sealed class VirtualMachine
|
|||||||
_autoVoicePending = false;
|
_autoVoicePending = false;
|
||||||
_messageSkipEnabled = false;
|
_messageSkipEnabled = false;
|
||||||
_messageSkipServiceActive = false;
|
_messageSkipServiceActive = false;
|
||||||
|
_advSkipServiceEnabled = false;
|
||||||
_advTextStyle = AdvTextStyle.Default;
|
_advTextStyle = AdvTextStyle.Default;
|
||||||
TextHistory.SetRecordingEnabled(true);
|
TextHistory.SetRecordingEnabled(true);
|
||||||
_host.SetMessageSkipActive(false);
|
_host.SetMessageSkipActive(false);
|
||||||
|
_host.SetPhysicalMessageSkipActive(false);
|
||||||
_host.ResetSceneContext();
|
_host.ResetSceneContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1132,13 +1148,17 @@ public sealed class VirtualMachine
|
|||||||
Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1;
|
Write(a[0], _messageSkipEnabled ? 1 : 0); return pc + 1;
|
||||||
case "u00414E80":
|
case "u00414E80":
|
||||||
case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation
|
case "suspend-adv-skip-service": // 0x19b: preserve the toggle while leaving ADV presentation
|
||||||
|
_advSkipServiceEnabled = false;
|
||||||
_messageSkipServiceActive = false;
|
_messageSkipServiceActive = false;
|
||||||
_host.SetMessageSkipActive(false);
|
_host.SetMessageSkipActive(false);
|
||||||
|
_host.SetPhysicalMessageSkipActive(false);
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "u00414EC0":
|
case "u00414EC0":
|
||||||
case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry
|
case "resume-adv-skip-service": // 0x19c: recompute active fast-forward on ADV entry
|
||||||
|
_advSkipServiceEnabled = true;
|
||||||
_messageSkipServiceActive = _messageSkipEnabled || _host.IsAdvReadSkipActive;
|
_messageSkipServiceActive = _messageSkipEnabled || _host.IsAdvReadSkipActive;
|
||||||
_host.SetMessageSkipActive(_messageSkipServiceActive);
|
_host.SetMessageSkipActive(_messageSkipServiceActive);
|
||||||
|
RefreshPhysicalMessageSkipState();
|
||||||
return pc + 1;
|
return pc + 1;
|
||||||
case "get-message-skip": // 0x1c7: persistent Skip or host-supplied Ctrl fast-forward
|
case "get-message-skip": // 0x1c7: persistent Skip or host-supplied Ctrl fast-forward
|
||||||
// The native per-op tick continually re-arms the transient run-state bit while the
|
// The native per-op tick continually re-arms the transient run-state bit while the
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ public sealed class GodotAdvHost : IHost
|
|||||||
private long _advTextStartedMs;
|
private long _advTextStartedMs;
|
||||||
private bool _advTextForceComplete;
|
private bool _advTextForceComplete;
|
||||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||||
|
private readonly object _messageSkipLock = new();
|
||||||
|
private bool _scriptMessageSkipActive;
|
||||||
|
private bool _physicalMessageSkipActive;
|
||||||
private volatile bool _messageSkipActive;
|
private volatile bool _messageSkipActive;
|
||||||
private int _voiceBgmDuckControl;
|
private int _voiceBgmDuckControl;
|
||||||
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
||||||
@@ -405,19 +408,42 @@ public sealed class GodotAdvHost : IHost
|
|||||||
public bool IsMessageSkipActive => _messageSkipActive;
|
public bool IsMessageSkipActive => _messageSkipActive;
|
||||||
|
|
||||||
public void SetMessageSkipActive(bool active)
|
public void SetMessageSkipActive(bool active)
|
||||||
|
=> SetMessageSkipChannel(active, physical: false);
|
||||||
|
|
||||||
|
public void SetPhysicalMessageSkipActive(bool active)
|
||||||
|
=> SetMessageSkipChannel(active, physical: true);
|
||||||
|
|
||||||
|
private void SetMessageSkipChannel(bool active, bool physical)
|
||||||
{
|
{
|
||||||
_messageSkipActive = active;
|
bool effective;
|
||||||
_timeline?.State("message-skip", new() { ["enabled"] = active });
|
bool changed;
|
||||||
if (active)
|
(AudioPayload Audio, int PlaybackVariant)? queued = null;
|
||||||
|
lock (_messageSkipLock)
|
||||||
|
{
|
||||||
|
if (physical) _physicalMessageSkipActive = active;
|
||||||
|
else _scriptMessageSkipActive = active;
|
||||||
|
effective = _scriptMessageSkipActive || _physicalMessageSkipActive;
|
||||||
|
changed = effective != _messageSkipActive;
|
||||||
|
_messageSkipActive = effective;
|
||||||
|
if (changed && !effective)
|
||||||
|
{
|
||||||
|
queued = _queuedSkippedVoice;
|
||||||
|
_queuedSkippedVoice = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!changed) return;
|
||||||
|
_timeline?.State("message-skip", new()
|
||||||
|
{
|
||||||
|
["enabled"] = effective,
|
||||||
|
["source"] = physical ? "logical-action-6" : "script",
|
||||||
|
});
|
||||||
|
if (effective)
|
||||||
{
|
{
|
||||||
lock (_textLock) _advTextForceComplete = true;
|
lock (_textLock) _advTextForceComplete = true;
|
||||||
_frameSignal.Set();
|
_frameSignal.Set();
|
||||||
_inputCallbackSignal.Set();
|
_inputCallbackSignal.Set();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var queued = _queuedSkippedVoice;
|
|
||||||
_queuedSkippedVoice = null;
|
|
||||||
if (queued != null) DispatchVoice(queued.Value.Audio, queued.Value.PlaybackVariant);
|
if (queued != null) DispatchVoice(queued.Value.Audio, queued.Value.PlaybackVariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -628,8 +654,13 @@ public sealed class GodotAdvHost : IHost
|
|||||||
}
|
}
|
||||||
while (_gate.Wait(0)) { }
|
while (_gate.Wait(0)) { }
|
||||||
_inputCallbackSignal.WaitOne(0);
|
_inputCallbackSignal.WaitOne(0);
|
||||||
_messageSkipActive = false;
|
lock (_messageSkipLock)
|
||||||
_queuedSkippedVoice = null;
|
{
|
||||||
|
_scriptMessageSkipActive = false;
|
||||||
|
_physicalMessageSkipActive = false;
|
||||||
|
_messageSkipActive = false;
|
||||||
|
_queuedSkippedVoice = null;
|
||||||
|
}
|
||||||
System.Threading.Volatile.Write(ref _voiceBgmDuckControl, 0);
|
System.Threading.Volatile.Write(ref _voiceBgmDuckControl, 0);
|
||||||
_advPagePresentationSuspended = false;
|
_advPagePresentationSuspended = false;
|
||||||
_modalMovieCancelled = false;
|
_modalMovieCancelled = false;
|
||||||
@@ -952,10 +983,19 @@ public sealed class GodotAdvHost : IHost
|
|||||||
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
|
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
|
||||||
["playback_variant"] = playbackVariant });
|
["playback_variant"] = playbackVariant });
|
||||||
if (audio == null) return;
|
if (audio == null) return;
|
||||||
if (_messageSkipActive)
|
bool queuedForSkip;
|
||||||
|
bool firstQueued = false;
|
||||||
|
lock (_messageSkipLock)
|
||||||
|
{
|
||||||
|
queuedForSkip = _messageSkipActive;
|
||||||
|
if (queuedForSkip)
|
||||||
|
{
|
||||||
|
firstQueued = _queuedSkippedVoice == null;
|
||||||
|
_queuedSkippedVoice = (audio, playbackVariant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (queuedForSkip)
|
||||||
{
|
{
|
||||||
bool firstQueued = _queuedSkippedVoice == null;
|
|
||||||
_queuedSkippedVoice = (audio, playbackVariant);
|
|
||||||
if (firstQueued) _main.CallDeferred("StopVoiceForMessageSkip");
|
if (firstQueued) _main.CallDeferred("StopVoiceForMessageSkip");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4135,7 +4135,7 @@ noop_headless = false
|
|||||||
source = "investigation"
|
source = "investigation"
|
||||||
confidence = "high"
|
confidence = "high"
|
||||||
depends_on = []
|
depends_on = []
|
||||||
evidence = "Ghidra handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). SC0000 label_1235a ORs it with op 0x1cc."
|
evidence = "Ghidra /v2: handler 0x4272b0 reads ctx+0xa0ce4 bit 0x08000000 and vm_operand_write(1, 1|0). adv_interpreter_tick@0x410fb0 first polls the configured logical-action mask; action 6 is bit 0x40 and therefore drives this transient run state from SYSTEM4's C/LeftCtrl bindings or the retained native Backspace default. Persistent op-0x88 state injects the same bit. SC0000 label_1235a ORs the result with op 0x1cc."
|
||||||
|
|
||||||
[[opcode.semantics.args]]
|
[[opcode.semantics.args]]
|
||||||
i = 1
|
i = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user