using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace Godosa.Core.Input; public enum ScriptAction { Down, Up, Click, Type } /// Physical key name (Down/Up), button label (Click) or text (Type). public readonly record struct ScriptEvent(double Time, ScriptAction Action, string Arg); /// Timed input for smoke runs (the host replays it as real key events and button /// clicks). One per line: TIME ACTION ARGS, TIME absolute seconds or +seconds /// after the previous line; actions down K, up K, press K, /// hold K SECONDS, click LABEL, type TEXT (into the focused text box); # comments. public sealed class InputScript { public const double PressSeconds = 0.1; private int _next; public IReadOnlyList Events { get; } public bool Finished => _next >= Events.Count; private InputScript(List events) => Events = events; public static InputScript Parse(string text) { var events = new List(); double? previous = null; var lines = text.Split('\n'); for (int n = 0; n < lines.Length; n++) { string line = lines[n].Trim(); if (line.Length == 0 || line.StartsWith('#')) continue; string[] parts = line.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); FormatException Bad(string why) => new($"input script line {n + 1}: {why}: '{line}'"); if (parts.Length < 3) throw Bad("expected TIME ACTION ARG"); bool relative = parts[0].StartsWith('+'); if (!double.TryParse(parts[0].TrimStart('+'), NumberStyles.Float, CultureInfo.InvariantCulture, out double t)) throw Bad("bad time"); if (relative) { if (previous is null) throw Bad("relative time needs a line before it"); t += previous.Value; } previous = t; string arg = parts[2]; switch (parts[1]) { case "down": events.Add(new(t, ScriptAction.Down, arg)); break; case "up": events.Add(new(t, ScriptAction.Up, arg)); break; case "press": events.Add(new(t, ScriptAction.Down, arg)); events.Add(new(t + PressSeconds, ScriptAction.Up, arg)); break; case "hold": string[] hold = arg.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (hold.Length != 2 || !double.TryParse(hold[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double seconds)) throw Bad("hold needs KEY SECONDS"); events.Add(new(t, ScriptAction.Down, hold[0])); events.Add(new(t + seconds, ScriptAction.Up, hold[0])); break; case "click": events.Add(new(t, ScriptAction.Click, arg)); break; case "type": events.Add(new(t, ScriptAction.Type, arg)); break; default: throw Bad($"unknown action '{parts[1]}'"); } } return new InputScript(events.OrderBy(e => e.Time).ToList()); // stable: same-time lines keep order } /// Appends every not-yet-returned event with Time ≤ . public void Due(double now, List into) { while (_next < Events.Count && Events[_next].Time <= now) into.Add(Events[_next++]); } }