aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Desktop/InputScriptPlayer.cs
blob: 2aad6796e156243eed2a9476aee6f047da4f6095 (plain)
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;
    }
}