// 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);
}
}