// Provenance: own design — shared run scripts (spec §4.1) using System.Globalization; namespace Godosa.Core.Runs.Scripts; public static class Positions { /// x,y or a locator @name(args) (args comma separated). public static (int X, int Y) Resolve(string token, IRunGame game) { if (token.StartsWith('@')) { var paren = token.IndexOf('('); var name = paren > 0 ? token[1..paren] : token[1..]; var args = paren > 0 && token.EndsWith(')') ? token[(paren + 1)..^1].Split(',', StringSplitOptions.TrimEntries).Where(a => a.Length > 0).ToArray() : []; if (!game.Locators.TryGetValue(name, out var locator)) throw new ScriptFault($"unknown locator @{name}"); var r = locator.Locate(args); return r.IsOk ? (r.X, r.Y) : throw new ScriptFault($"locator {token}: {r.Error}"); } var parts = token.Split(','); if (parts.Length == 2 && int.TryParse(parts[0], NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var x) && int.TryParse(parts[1], NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var y)) return (x, y); throw new ScriptFault($"bad position '{token}'"); } } public static class InputPlans { public static bool IsInputVerb(string verb) => verb is "click" or "rclick" or "dclick" or "drag" or "move" or "wheel" or "key" or "hold" or "chord" or "type"; public static InputPlan For(string verb, IReadOnlyList args, Func position) { List e = []; void Add(int frame, InputEvent ev) => e.Add(new PlannedInput(frame, ev)); switch (verb) { case "click" or "rclick" or "dclick": { var (x, y) = position(args[0].Text); var button = verb == "rclick" ? MouseButton.Right : MouseButton.Left; Add(0, new InputEvent(InputKind.Move, x, y)); Add(0, new InputEvent(InputKind.Down, x, y, button)); Add(1, new InputEvent(InputKind.Up, x, y, button)); if (verb == "dclick") { Add(2, new InputEvent(InputKind.Down, x, y, button)); Add(3, new InputEvent(InputKind.Up, x, y, button)); } break; } case "drag": { var (ax, ay) = position(args[0].Text); var (bx, by) = position(args[1].Text); Add(0, new InputEvent(InputKind.Move, ax, ay)); Add(0, new InputEvent(InputKind.Down, ax, ay)); for (var step = 1; step <= 3; step++) Add(step, new InputEvent(InputKind.Move, ax + (bx - ax) * step / 3, ay + (by - ay) * step / 3)); Add(4, new InputEvent(InputKind.Up, bx, by)); break; } case "move": { var (x, y) = position(args[0].Text); Add(0, new InputEvent(InputKind.Move, x, y)); break; } case "wheel": { var delta = int.TryParse(args[0].Text, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var d) ? d : throw new ScriptFault($"bad wheel delta '{args[0].Text}'"); var (x, y) = args.Count > 1 ? position(args[1].Text) : (0, 0); if (args.Count > 1) Add(0, new InputEvent(InputKind.Move, x, y)); Add(0, new InputEvent(InputKind.Wheel, x, y, Delta: delta)); break; } case "key": Add(0, new InputEvent(InputKind.KeyDown, Key: args[0].Text)); Add(1, new InputEvent(InputKind.KeyUp, Key: args[0].Text)); break; case "hold": { var frames = Durations.Frames(args[1].Text) is { } f and > 0 ? f : throw new ScriptFault($"bad hold frames '{args[1].Text}'"); Add(0, new InputEvent(InputKind.KeyDown, Key: args[0].Text)); Add(frames, new InputEvent(InputKind.KeyUp, Key: args[0].Text)); break; } case "chord": { var keys = args[0].Text.Split('+', StringSplitOptions.RemoveEmptyEntries); foreach (var k in keys) Add(0, new InputEvent(InputKind.KeyDown, Key: k)); foreach (var k in keys.Reverse()) Add(1, new InputEvent(InputKind.KeyUp, Key: k)); break; } case "type": Add(0, new InputEvent(InputKind.Text, Text: args[0].Text)); break; default: throw new ScriptFault($"not an input verb: {verb}"); } return new InputPlan(e); } }