Implement ADV controls and native Auto timing
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Threading;
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
private readonly Main _main;
|
||||
@@ -20,6 +23,7 @@ public sealed class GodotAdvHost : IHost
|
||||
// primary surface, but op 0x1fa releases it like any other slot; subsequent size queries must return 0x0.
|
||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||
private readonly SemaphoreSlim _gate = new(0, 1);
|
||||
private readonly AutoResetEvent _inputCallbackSignal = new(false);
|
||||
private readonly Age.Engine.Hosting.FrameClock _clock;
|
||||
private readonly GodotTimelineLog? _timeline;
|
||||
private readonly PageLocatorState _locator;
|
||||
@@ -147,6 +151,13 @@ public sealed class GodotAdvHost : IHost
|
||||
public void WaitForInput() => WaitForInput(0);
|
||||
|
||||
public void WaitForInput(int layoutSlot)
|
||||
=> WaitForInput(layoutSlot, static () => false, static () => default);
|
||||
|
||||
public void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
|
||||
=> WaitForInput(layoutSlot, serviceInputCallback, static () => default);
|
||||
|
||||
public void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
|
||||
Func<AdvAutoWaitState> autoWaitState)
|
||||
{
|
||||
Pages++;
|
||||
_locator.Wait(Pages);
|
||||
@@ -158,9 +169,22 @@ public sealed class GodotAdvHost : IHost
|
||||
_waitIndicatorStartedMs = _clock.NowMs;
|
||||
IsWaiting = true;
|
||||
_timeline?.State("input-wait", new() { ["page"] = Pages });
|
||||
_gate.Wait();
|
||||
var autoTimer = new AdvAutoAdvanceTimer();
|
||||
bool autoAdvanced = false;
|
||||
while (!_stopping)
|
||||
{
|
||||
while (serviceInputCallback()) { }
|
||||
if (autoTimer.Poll(autoWaitState(), _main.IsVoicePlaybackActive, _clock.NowMs))
|
||||
{
|
||||
autoAdvanced = true;
|
||||
break;
|
||||
}
|
||||
if (_gate.Wait(0)) break;
|
||||
WaitHandle.WaitAny(new[] { _gate.AvailableWaitHandle, _inputCallbackSignal, _frameSignal });
|
||||
if (_gate.Wait(0)) break;
|
||||
}
|
||||
IsWaiting = false;
|
||||
_timeline?.State("running", new() { ["input"] = "auto-or-user" });
|
||||
_timeline?.State("running", new() { ["input"] = autoAdvanced ? "auto" : "user" });
|
||||
lock (_textLock)
|
||||
{
|
||||
_advText = "";
|
||||
@@ -194,6 +218,11 @@ public sealed class GodotAdvHost : IHost
|
||||
if (IsWaiting && _gate.CurrentCount == 0) _gate.Release();
|
||||
}
|
||||
|
||||
public void WakeInputCallbackService() => _inputCallbackSignal.Set();
|
||||
|
||||
public void InputCallbackCompleted(GfxState gfx)
|
||||
=> Interlocked.Exchange(ref _presentRequested, 1);
|
||||
|
||||
public void WaitForForegroundTransition(GfxState gfx)
|
||||
{
|
||||
int started = gfx.StartForegroundTransitions(_clock.NowMs);
|
||||
@@ -251,6 +280,7 @@ public sealed class GodotAdvHost : IHost
|
||||
_stopping = true;
|
||||
lock (_textLock) _advTextForceComplete = true;
|
||||
if (_gate.CurrentCount == 0) _gate.Release();
|
||||
_inputCallbackSignal.Set();
|
||||
_frameSignal.Set();
|
||||
}
|
||||
|
||||
@@ -440,7 +470,11 @@ public sealed class GodotAdvHost : IHost
|
||||
{
|
||||
var asset = _res.Resolve(_scene, id);
|
||||
var audio = asset != null ? LoadAudio(asset) : null;
|
||||
if (audio != null) _main.CallDeferred("PlayVoice", audio.Bytes, audio.Name);
|
||||
if (audio != null)
|
||||
{
|
||||
int generation = _main.QueueVoicePlayback();
|
||||
_main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadSoundEffect(long resourceId, int channel)
|
||||
|
||||
@@ -30,6 +30,9 @@ public partial class Main : Godot.Control
|
||||
private Label _locatorHud = null!;
|
||||
private AudioStreamPlayer _bgm = null!; // looping background music
|
||||
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
|
||||
private int _voiceQueuedGeneration;
|
||||
private int _voiceStartedGeneration;
|
||||
private int _voiceCompletedGeneration;
|
||||
private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
|
||||
private VirtualMachine _vm = null!;
|
||||
private GodotAdvHost _host = null!;
|
||||
@@ -231,6 +234,10 @@ public partial class Main : Godot.Control
|
||||
foreach (var kv in session.GlobalStrings) _vm.GlobalStrings[kv.Key] = kv.Value;
|
||||
GD.Print($"[boot] system boot done: {session.Globals.Count} globals seeded");
|
||||
}
|
||||
// The native SYSTEM4 UI boot enables standard ADV chrome after the data-only *INIT prefix above.
|
||||
// Without this inherited value the visible SO001 strip is still drawn, but every ADV script skips
|
||||
// its five pointer rectangles and registers only the off-screen keyboard/pad records.
|
||||
if (!_selftest) _vm.Globals[0x6c1] = 1;
|
||||
// Native AGE owns this transient secondary-SFX channel outside script-visible writes.
|
||||
// The matching SC0000 trace has value 4 at 0xc31; seed only this proven profile/slice.
|
||||
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
|
||||
@@ -265,6 +272,7 @@ public partial class Main : Godot.Control
|
||||
_timelineFrame++;
|
||||
_timeline?.SetFrame(_timelineFrame, _clock.NowMs);
|
||||
_host?.PulseFrame();
|
||||
UpdateVoicePlaybackState();
|
||||
UpdateMovieFrames();
|
||||
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
|
||||
Recomposite(); // native publishes retained mutations only at present/service boundaries
|
||||
@@ -325,9 +333,32 @@ public partial class Main : Godot.Control
|
||||
_locatorHud.Text = _locator.CurrentDisplay + " · copied";
|
||||
return;
|
||||
}
|
||||
if (e.IsActionPressed("ui_accept") ||
|
||||
(e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left))
|
||||
if (e is InputEventMouseMotion motion)
|
||||
{
|
||||
var p = ToNativeScreen(motion.Position);
|
||||
_vm.UpdatePointer(p.X, p.Y);
|
||||
return;
|
||||
}
|
||||
if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
var p = ToNativeScreen(mb.Position);
|
||||
if (_vm.TryActivatePointer(p.X, p.Y))
|
||||
{
|
||||
GetViewport().SetInputAsHandled();
|
||||
return;
|
||||
}
|
||||
_host.SignalInput();
|
||||
return;
|
||||
}
|
||||
if (e.IsActionPressed("ui_accept")) _host.SignalInput();
|
||||
}
|
||||
|
||||
private (int X, int Y) ToNativeScreen(Vector2 position)
|
||||
{
|
||||
Vector2 size = GetViewportRect().Size;
|
||||
if (size.X <= 0 || size.Y <= 0) return (0, 0);
|
||||
return ((int)System.Math.Floor(position.X * ScreenWidth / size.X),
|
||||
(int)System.Math.Floor(position.Y * ScreenHeight / size.Y));
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
@@ -644,15 +675,47 @@ public partial class Main : Godot.Control
|
||||
_bgm.Play();
|
||||
}
|
||||
|
||||
public void PlayVoice(byte[] oggBytes, string assetName)
|
||||
public int QueueVoicePlayback()
|
||||
=> System.Threading.Interlocked.Increment(ref _voiceQueuedGeneration);
|
||||
|
||||
public bool IsVoicePlaybackActive
|
||||
=> System.Threading.Volatile.Read(ref _voiceCompletedGeneration)
|
||||
< System.Threading.Volatile.Read(ref _voiceQueuedGeneration);
|
||||
|
||||
public void PlayVoice(byte[] oggBytes, string assetName, int generation)
|
||||
{
|
||||
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
|
||||
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
|
||||
if (stream == null)
|
||||
{
|
||||
GD.Print($"OGG load failed {assetName}");
|
||||
CompleteVoiceGeneration(generation);
|
||||
return;
|
||||
}
|
||||
stream.Loop = false;
|
||||
_voice.Stream = stream;
|
||||
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
|
||||
_voice.Play();
|
||||
}
|
||||
|
||||
private void UpdateVoicePlaybackState()
|
||||
{
|
||||
int started = System.Threading.Volatile.Read(ref _voiceStartedGeneration);
|
||||
if (started > System.Threading.Volatile.Read(ref _voiceCompletedGeneration) && !_voice.Playing)
|
||||
CompleteVoiceGeneration(started);
|
||||
}
|
||||
|
||||
private void CompleteVoiceGeneration(int generation)
|
||||
{
|
||||
int current;
|
||||
do
|
||||
{
|
||||
current = System.Threading.Volatile.Read(ref _voiceCompletedGeneration);
|
||||
if (current >= generation) return;
|
||||
}
|
||||
while (System.Threading.Interlocked.CompareExchange(
|
||||
ref _voiceCompletedGeneration, generation, current) != current);
|
||||
}
|
||||
|
||||
public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||
|
||||
Reference in New Issue
Block a user