aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core/Input
diff options
context:
space:
mode:
Diffstat (limited to 'src/Godosa.Core/Input')
-rw-r--r--src/Godosa.Core/Input/GamepadState.cs58
-rw-r--r--src/Godosa.Core/Input/InputScript.cs82
-rw-r--r--src/Godosa.Core/Input/MenuCursor.cs21
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;
+ }
+}