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