diff options
Diffstat (limited to 'src/Godosa.Core/Input')
| -rw-r--r-- | src/Godosa.Core/Input/GamepadState.cs | 58 | ||||
| -rw-r--r-- | src/Godosa.Core/Input/InputScript.cs | 82 | ||||
| -rw-r--r-- | src/Godosa.Core/Input/MenuCursor.cs | 21 |
3 files changed, 161 insertions, 0 deletions
diff --git a/src/Godosa.Core/Input/GamepadState.cs b/src/Godosa.Core/Input/GamepadState.cs new file mode 100644 index 0000000..5f1d5a5 --- /dev/null +++ b/src/Godosa.Core/Input/GamepadState.cs @@ -0,0 +1,58 @@ +using System.Numerics; + +namespace Godosa.Core.Input; + +[Flags] +public enum PadButtons +{ + None = 0, + A = 1 << 0, + B = 1 << 1, + X = 1 << 2, + Y = 1 << 3, + Back = 1 << 4, + Start = 1 << 5, + LeftShoulder = 1 << 6, + RightShoulder = 1 << 7, + DpadUp = 1 << 8, + DpadDown = 1 << 9, + DpadLeft = 1 << 10, + DpadRight = 1 << 11, +} + +/// <summary>Raw held pad state, no game meaning: sticks −1…1 (SDL convention, stick up = +/// −Y), triggers 0…1. Each game maps it to its own input frame.</summary> +public readonly record struct GamepadState(Vector2 LeftStick, Vector2 RightStick, float LeftTrigger, float RightTrigger, PadButtons Buttons) +{ + public bool Held(PadButtons button) => (Buttons & button) != 0; +} + +/// <summary>Input-script pad events (<c>PadA PadB PadX PadY PadBack PadStart PadUp PadDown +/// PadLeft PadRight PadLB PadRB PadLT PadRT</c>, case-insensitive): triggers go full on/off.</summary> +public static class GamepadScript +{ + public static GamepadState? Apply(GamepadState state, string name, bool down) + { + string key = name.ToLowerInvariant(); + if (key == "padlt") return state with { LeftTrigger = down ? 1f : 0f }; + if (key == "padrt") return state with { RightTrigger = down ? 1f : 0f }; + PadButtons button = key switch + { + "pada" => PadButtons.A, + "padb" => PadButtons.B, + "padx" => PadButtons.X, + "pady" => PadButtons.Y, + "padback" => PadButtons.Back, + "padstart" => PadButtons.Start, + "padup" => PadButtons.DpadUp, + "paddown" => PadButtons.DpadDown, + "padleft" => PadButtons.DpadLeft, + "padright" => PadButtons.DpadRight, + "padlb" => PadButtons.LeftShoulder, + "padrb" => PadButtons.RightShoulder, + _ => PadButtons.None, + }; + if (button == PadButtons.None) return null; + return state with { Buttons = down ? state.Buttons | button : state.Buttons & ~button }; + } +} diff --git a/src/Godosa.Core/Input/InputScript.cs b/src/Godosa.Core/Input/InputScript.cs new file mode 100644 index 0000000..481b78f --- /dev/null +++ b/src/Godosa.Core/Input/InputScript.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Godosa.Core.Input; + +public enum ScriptAction { Down, Up, Click, Type } + +/// <param name="Arg">Physical key name (Down/Up), button label (Click) or text (Type).</param> +public readonly record struct ScriptEvent(double Time, ScriptAction Action, string Arg); + +/// <summary>Timed input for smoke runs (the host replays it as real key events and button +/// clicks). One per line: <c>TIME ACTION ARGS</c>, TIME absolute seconds or <c>+seconds</c> +/// after the previous line; actions <c>down K</c>, <c>up K</c>, <c>press K</c>, +/// <c>hold K SECONDS</c>, <c>click LABEL</c>, <c>type TEXT</c> (into the focused text box); <c>#</c> comments.</summary> +public sealed class InputScript +{ + public const double PressSeconds = 0.1; + + private int _next; + + public IReadOnlyList<ScriptEvent> Events { get; } + + public bool Finished => _next >= Events.Count; + + private InputScript(List<ScriptEvent> events) => Events = events; + + public static InputScript Parse(string text) + { + var events = new List<ScriptEvent>(); + 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 + } + + /// <summary>Appends every not-yet-returned event with Time ≤ <paramref name="now"/>.</summary> + public void Due(double now, List<ScriptEvent> into) + { + while (_next < Events.Count && Events[_next].Time <= now) + into.Add(Events[_next++]); + } +} diff --git a/src/Godosa.Core/Input/MenuCursor.cs b/src/Godosa.Core/Input/MenuCursor.cs new file mode 100644 index 0000000..be6cab3 --- /dev/null +++ b/src/Godosa.Core/Input/MenuCursor.cs @@ -0,0 +1,21 @@ +namespace Godosa.Core.Input; + +/// <summary>Which action a gamepad highlights in an open window (hud spec §4): wraps at the +/// ends, stays inside a list that shrinks, −1 when there's nothing to pick.</summary> +public sealed class MenuCursor +{ + public int Index { get; private set; } = -1; + + public void Clamp(int count) + { + if (count <= 0) Index = -1; + else if (Index < 0) Index = 0; + else if (Index >= count) Index = count - 1; + } + + public void Move(int delta, int count) + { + if (count <= 0) return; + Index = ((Index + delta) % count + count) % count; + } +} |
