1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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;
}
}
|