summaryrefslogtreecommitdiff
path: root/src/SMAPI/Utilities
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-01-19 21:20:25 -0500
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2021-01-19 21:20:25 -0500
commitff16a6567b6137b1aafed3470406d5f5884a5bdc (patch)
tree3b19d821d3f45d266914ec9fbccaa3a375c3fd0a /src/SMAPI/Utilities
parent5676d94fe655c42e50c27b5eae72b9c96cfc2476 (diff)
downloadSMAPI-ff16a6567b6137b1aafed3470406d5f5884a5bdc.tar.gz
SMAPI-ff16a6567b6137b1aafed3470406d5f5884a5bdc.tar.bz2
SMAPI-ff16a6567b6137b1aafed3470406d5f5884a5bdc.zip
add multi-key binding API (#744)
Diffstat (limited to 'src/SMAPI/Utilities')
-rw-r--r--src/SMAPI/Utilities/Keybind.cs131
-rw-r--r--src/SMAPI/Utilities/KeybindList.cs152
2 files changed, 283 insertions, 0 deletions
diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs
new file mode 100644
index 00000000..9d6cd6ee
--- /dev/null
+++ b/src/SMAPI/Utilities/Keybind.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Framework;
+
+namespace StardewModdingAPI.Utilities
+{
+ /// <summary>A single multi-key binding which can be triggered by the player.</summary>
+ /// <remarks>NOTE: this is part of <see cref="KeybindList"/>, and usually shouldn't be used directly.</remarks>
+ public class Keybind
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The buttons that must be down to activate the keybind.</summary>
+ public SButton[] Buttons { get; }
+
+ /// <summary>Whether any keys are bound.</summary>
+ public bool IsBound { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="buttons">The buttons that must be down to activate the keybind.</param>
+ public Keybind(params SButton[] buttons)
+ {
+ this.Buttons = buttons;
+ this.IsBound = buttons.Any(p => p != SButton.None);
+ }
+
+ /// <summary>Parse a keybind string, if it's valid.</summary>
+ /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
+ /// <param name="parsed">The parsed keybind, if valid.</param>
+ /// <param name="errors">The parse errors, if any.</param>
+ public static bool TryParse(string input, out Keybind parsed, out string[] errors)
+ {
+ // empty input
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ parsed = new Keybind(SButton.None);
+ errors = new string[0];
+ return true;
+ }
+
+ // parse buttons
+ string[] rawButtons = input.Split('+');
+ SButton[] buttons = new SButton[rawButtons.Length];
+ List<string> rawErrors = new List<string>();
+ for (int i = 0; i < buttons.Length; i++)
+ {
+ string rawButton = rawButtons[i].Trim();
+ if (string.IsNullOrWhiteSpace(rawButton))
+ rawErrors.Add("Invalid empty button value");
+ else if (!Enum.TryParse(rawButton, ignoreCase: true, out SButton button))
+ {
+ string error = $"Invalid button value '{rawButton}'";
+
+ switch (rawButton.ToLower())
+ {
+ case "shift":
+ error += $" (did you mean {SButton.LeftShift}?)";
+ break;
+
+ case "ctrl":
+ case "control":
+ error += $" (did you mean {SButton.LeftControl}?)";
+ break;
+
+ case "alt":
+ error += $" (did you mean {SButton.LeftAlt}?)";
+ break;
+ }
+
+ rawErrors.Add(error);
+ }
+ else
+ buttons[i] = button;
+ }
+
+ // build result
+ if (rawErrors.Any())
+ {
+ parsed = null;
+ errors = rawErrors.ToArray();
+ return false;
+ }
+ else
+ {
+ parsed = new Keybind(buttons);
+ errors = new string[0];
+ return true;
+ }
+ }
+
+ /// <summary>Get the keybind state relative to the previous tick.</summary>
+ public SButtonState GetState()
+ {
+ SButtonState[] states = this.Buttons.Select(SGame.GetInputState).Distinct().ToArray();
+
+ // single state
+ if (states.Length == 1)
+ return states[0];
+
+ // if any key has no state, the whole set wasn't enabled last tick
+ if (states.Contains(SButtonState.None))
+ return SButtonState.None;
+
+ // mix of held + pressed => pressed
+ if (states.All(p => p == SButtonState.Pressed || p == SButtonState.Held))
+ return SButtonState.Pressed;
+
+ // mix of held + released => released
+ if (states.All(p => p == SButtonState.Held || p == SButtonState.Released))
+ return SButtonState.Released;
+
+ // not down last tick or now
+ return SButtonState.None;
+ }
+
+ /// <summary>Get a string representation of the keybind.</summary>
+ /// <remarks>A keybind is serialized to a string like <c>LeftControl + S</c>, where each key is separated with <c>+</c>. The key order is commutative, so <c>LeftControl + S</c> and <c>S + LeftControl</c> are identical.</remarks>
+ public override string ToString()
+ {
+ return this.Buttons.Length > 0
+ ? string.Join(" + ", this.Buttons)
+ : SButton.None.ToString();
+ }
+ }
+}
diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs
new file mode 100644
index 00000000..f6933af3
--- /dev/null
+++ b/src/SMAPI/Utilities/KeybindList.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Serialization;
+
+namespace StardewModdingAPI.Utilities
+{
+ /// <summary>A set of multi-key bindings which can be triggered by the player.</summary>
+ public class KeybindList
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The individual keybinds.</summary>
+ public Keybind[] Keybinds { get; }
+
+ /// <summary>Whether any keys are bound.</summary>
+ public bool IsBound { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="keybinds">The underlying keybinds.</param>
+ /// <remarks>See <see cref="Parse"/> or <see cref="TryParse"/> to parse it from a string representation. You can also use this type directly in your config or JSON data models, and it'll be parsed by SMAPI.</remarks>
+ public KeybindList(params Keybind[] keybinds)
+ {
+ this.Keybinds = keybinds.Where(p => p.IsBound).ToArray();
+ this.IsBound = this.Keybinds.Any();
+ }
+
+ /// <summary>Parse a keybind list from a string, and throw an exception if it's not valid.</summary>
+ /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
+ /// <exception cref="FormatException">The <paramref name="input"/> format is invalid.</exception>
+ public static KeybindList Parse(string input)
+ {
+ return KeybindList.TryParse(input, out KeybindList parsed, out string[] errors)
+ ? parsed
+ : throw new SParseException($"Can't parse {nameof(Keybind)} from invalid value '{input}'.\n{string.Join("\n", errors)}");
+ }
+
+ /// <summary>Try to parse a keybind list from a string.</summary>
+ /// <param name="input">The keybind string. See remarks on <see cref="ToString"/> for format details.</param>
+ /// <param name="parsed">The parsed keybind list, if valid.</param>
+ /// <param name="errors">The errors that occurred while parsing the input, if any.</param>
+ public static bool TryParse(string input, out KeybindList parsed, out string[] errors)
+ {
+ // empty input
+ if (string.IsNullOrWhiteSpace(input))
+ {
+ parsed = new KeybindList();
+ errors = new string[0];
+ return true;
+ }
+
+ // parse buttons
+ var rawErrors = new List<string>();
+ var keybinds = new List<Keybind>();
+ foreach (string rawSet in input.Split(','))
+ {
+ if (string.IsNullOrWhiteSpace(rawSet))
+ continue;
+
+ if (!Keybind.TryParse(rawSet, out Keybind keybind, out string[] curErrors))
+ rawErrors.AddRange(curErrors);
+ else
+ keybinds.Add(keybind);
+ }
+
+ // build result
+ if (rawErrors.Any())
+ {
+ parsed = null;
+ errors = rawErrors.ToArray();
+ return false;
+ }
+ else
+ {
+ parsed = new KeybindList(keybinds.ToArray());
+ errors = new string[0];
+ return true;
+ }
+ }
+
+ /// <summary>Get the overall keybind list state relative to the previous tick.</summary>
+ /// <remarks>States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'.</remarks>
+ public SButtonState GetState()
+ {
+ bool wasPressed = false;
+ bool isPressed = false;
+
+ foreach (Keybind keybind in this.Keybinds)
+ {
+ switch (keybind.GetState())
+ {
+ case SButtonState.Pressed:
+ isPressed = true;
+ break;
+
+ case SButtonState.Held:
+ wasPressed = true;
+ isPressed = true;
+ break;
+
+ case SButtonState.Released:
+ wasPressed = true;
+ break;
+ }
+ }
+
+ if (wasPressed == isPressed)
+ {
+ return wasPressed
+ ? SButtonState.Held
+ : SButtonState.None;
+ }
+
+ return wasPressed
+ ? SButtonState.Released
+ : SButtonState.Pressed;
+ }
+
+ /// <summary>Get whether any of the button sets are pressed.</summary>
+ public bool IsDown()
+ {
+ SButtonState state = this.GetState();
+ return state == SButtonState.Pressed || state == SButtonState.Held;
+ }
+
+ /// <summary>Get whether the input binding was just pressed this tick.</summary>
+ public bool JustPressed()
+ {
+ return this.GetState() == SButtonState.Pressed;
+ }
+
+ /// <summary>Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned.</summary>
+ public Keybind GetKeybindCurrentlyDown()
+ {
+ return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown());
+ }
+
+ /// <summary>Get a string representation of the input binding.</summary>
+ /// <remarks>A keybind list is serialized to a string like <c>LeftControl + S, LeftAlt + S</c>, where each multi-key binding is separated with <c>,</c> and the keys within each keybind are separated with <c>+</c>. The key order is commutative, so <c>LeftControl + S</c> and <c>S + LeftControl</c> are identical.</remarks>
+ public override string ToString()
+ {
+ return this.Keybinds.Length > 0
+ ? string.Join(", ", this.Keybinds.Select(p => p.ToString()))
+ : SButton.None.ToString();
+ }
+ }
+}