diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-01-19 21:20:25 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2021-01-19 21:20:25 -0500 |
commit | ff16a6567b6137b1aafed3470406d5f5884a5bdc (patch) | |
tree | 3b19d821d3f45d266914ec9fbccaa3a375c3fd0a /src/SMAPI/Utilities | |
parent | 5676d94fe655c42e50c27b5eae72b9c96cfc2476 (diff) | |
download | SMAPI-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.cs | 131 | ||||
-rw-r--r-- | src/SMAPI/Utilities/KeybindList.cs | 152 |
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(); + } + } +} |