aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Desktop/InputScriptPlayer.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Desktop/InputScriptPlayer.cs')
-rw-r--r--src/Godosa.Desktop/InputScriptPlayer.cs124
1 files changed, 124 insertions, 0 deletions
diff --git a/src/Godosa.Desktop/InputScriptPlayer.cs b/src/Godosa.Desktop/InputScriptPlayer.cs
new file mode 100644
index 0000000..2aad679
--- /dev/null
+++ b/src/Godosa.Desktop/InputScriptPlayer.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using Godosa.Core.Diagnostics;
+using Godosa.Core.Input;
+
+namespace Godosa.Desktop;
+
+/// <summary>Replays the game's input-script file (Core <see cref="InputScript"/>, path in the
+/// variable given to <see cref="Configure"/>) as real
+/// routed key events to the focused control and clicks on visible buttons, so smoke runs
+/// can drive the game without X key injection (which never reaches a focused window on
+/// KDE Wayland). Log category <c>script</c> (<see cref="EngineDebug"/>).</summary>
+public sealed class InputScriptPlayer
+{
+ private readonly Window _window;
+ private readonly InputScript _script;
+ private readonly Stopwatch _clock = Stopwatch.StartNew();
+ private readonly List<ScriptEvent> _due = new();
+ private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(10) };
+
+ private static string? _variable;
+ private readonly Func<Window, Interactive?>? _keyTarget;
+
+ /// <summary>True while a script drives the game: window focus no longer pauses or
+ /// releases input.</summary>
+ public static bool Active { get; private set; }
+
+ /// <summary>Scripted gamepad (<c>PadA … PadRT</c>, see <see cref="GamepadScript"/>).</summary>
+ public static GamepadState Pad { get; private set; }
+
+ /// <summary>Call first in <c>Main</c>: names the game's script variable.</summary>
+ public static void Configure(string environmentVariable)
+ {
+ _variable = environmentVariable;
+ Active = Environment.GetEnvironmentVariable(environmentVariable) is { Length: > 0 };
+ }
+
+ private InputScriptPlayer(Window window, InputScript script, Func<Window, Interactive?>? keyTarget)
+ {
+ _window = window;
+ _script = script;
+ _keyTarget = keyTarget;
+ _timer.Tick += (_, _) => Step();
+ _timer.Start();
+ }
+
+ /// <param name="keyTarget">Where keys go when nothing has focus (e.g. the game view); the window otherwise.</param>
+ public static InputScriptPlayer? Start(Window window, Func<Window, Interactive?>? keyTarget = null)
+ {
+ if (!Active) return null;
+ string path = Environment.GetEnvironmentVariable(_variable!)!;
+ var script = InputScript.Parse(File.ReadAllText(path));
+ if (EngineDebug.Script) EngineDebug.Write("script", $"{script.Events.Count} events from {path}");
+ return new InputScriptPlayer(window, script, keyTarget);
+ }
+
+ private void Step()
+ {
+ _script.Due(_clock.Elapsed.TotalSeconds, _due);
+ foreach (var e in _due)
+ {
+ bool ok = e.Action switch
+ {
+ ScriptAction.Click => Click(e.Arg),
+ ScriptAction.Type => Type(e.Arg),
+ _ => Key(e.Action == ScriptAction.Down, e.Arg),
+ };
+ if (EngineDebug.Script) EngineDebug.Write("script", $"t={_clock.Elapsed.TotalSeconds:F2} {e.Action} {e.Arg}{(ok ? "" : " (FAILED)")}");
+ }
+ _due.Clear();
+ if (_script.Finished) _timer.Stop();
+ }
+
+ private bool Key(bool down, string name)
+ {
+ if (name.StartsWith("Pad", StringComparison.OrdinalIgnoreCase))
+ {
+ if (GamepadScript.Apply(Pad, name, down) is not { } next) return false;
+ Pad = next;
+ return true;
+ }
+ if (!Enum.TryParse(name, ignoreCase: true, out PhysicalKey physical)) return false;
+ Enum.TryParse(name, ignoreCase: true, out Key key);
+ var target = _window.FocusManager?.GetFocusedElement() as Interactive
+ ?? _keyTarget?.Invoke(_window)
+ ?? _window;
+ target.RaiseEvent(new KeyEventArgs
+ {
+ RoutedEvent = down ? InputElement.KeyDownEvent : InputElement.KeyUpEvent,
+ PhysicalKey = physical,
+ Key = key,
+ Source = target,
+ });
+ return true;
+ }
+
+ private bool Type(string text)
+ {
+ if (_window.FocusManager?.GetFocusedElement() is not TextBox box) return false;
+ box.Text = text;
+ return true;
+ }
+
+ private bool Click(string label)
+ {
+ var buttons = _window.GetVisualDescendants().OfType<Button>()
+ .Where(b => b.IsEffectivelyVisible && b.IsEffectivelyEnabled).ToList();
+ string Text(Button b) => b.Content?.ToString()?.Trim() ?? "";
+ var button = buttons.FirstOrDefault(b => Text(b).Equals(label, StringComparison.OrdinalIgnoreCase)) // exact label first
+ ?? buttons.FirstOrDefault(b => b.Name == label) // steppers: fov+ / fov-
+ ?? buttons.FirstOrDefault(b => Text(b).Contains(label, StringComparison.OrdinalIgnoreCase));
+ if (button is not null) Buttons.Press(button);
+ return button is not null;
+ }
+}