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; /// Replays the game's input-script file (Core , path in the /// variable given to ) 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 script (). public sealed class InputScriptPlayer { private readonly Window _window; private readonly InputScript _script; private readonly Stopwatch _clock = Stopwatch.StartNew(); private readonly List _due = new(); private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(10) }; private static string? _variable; private readonly Func? _keyTarget; /// True while a script drives the game: window focus no longer pauses or /// releases input. public static bool Active { get; private set; } /// Scripted gamepad (PadA … PadRT, see ). public static GamepadState Pad { get; private set; } /// Call first in Main: names the game's script variable. public static void Configure(string environmentVariable) { _variable = environmentVariable; Active = Environment.GetEnvironmentVariable(environmentVariable) is { Length: > 0 }; } private InputScriptPlayer(Window window, InputScript script, Func? keyTarget) { _window = window; _script = script; _keyTarget = keyTarget; _timer.Tick += (_, _) => Step(); _timer.Start(); } /// Where keys go when nothing has focus (e.g. the game view); the window otherwise. public static InputScriptPlayer? Start(Window window, Func? 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