diff options
| author | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
|---|---|---|
| committer | godosa <godosa@godosa.eu> | 2026-10-06 23:39:36 +0200 |
| commit | 39066900773e7857faf2d02e7ed51b71d219d97d (patch) | |
| tree | ffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Desktop/InputScriptPlayer.cs | |
| download | godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip | |
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Desktop/InputScriptPlayer.cs')
| -rw-r--r-- | src/Godosa.Desktop/InputScriptPlayer.cs | 124 |
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; + } +} |
