using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Toolkit.Serialization; namespace StardewModdingAPI.Utilities { /// A set of multi-key bindings which can be triggered by the player. public class KeybindList { /********* ** Accessors *********/ /// The individual keybinds. public Keybind[] Keybinds { get; } /// Whether any keys are bound. public bool IsBound { get; } /********* ** Public methods *********/ /// Construct an instance. /// The underlying keybinds. /// See or 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. public KeybindList(params Keybind[] keybinds) { this.Keybinds = keybinds.Where(p => p.IsBound).ToArray(); this.IsBound = this.Keybinds.Any(); } /// Construct an instance. /// A single-key binding. public KeybindList(SButton singleKey) : this(new Keybind(singleKey)) { } /// Parse a keybind list from a string, and throw an exception if it's not valid. /// The keybind string. See remarks on for format details. /// The format is invalid. 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)}"); } /// Try to parse a keybind list from a string. /// The keybind string. See remarks on for format details. /// The parsed keybind list, if valid. /// The errors that occurred while parsing the input, if any. public static bool TryParse(string? input, [NotNullWhen(true)] out KeybindList? parsed, out string[] errors) { // empty input if (string.IsNullOrWhiteSpace(input)) { parsed = new KeybindList(); errors = Array.Empty(); return true; } // parse buttons var rawErrors = new List(); var keybinds = new List(); 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.Distinct().ToArray(); return false; } else { parsed = new KeybindList(keybinds.ToArray()); errors = Array.Empty(); return true; } } /// Get a keybind list for a single keybind. /// The buttons that must be down to activate the keybind. public static KeybindList ForSingle(params SButton[] buttons) { return new KeybindList( new Keybind(buttons) ); } /// Get the overall keybind list state relative to the previous tick. /// States are transitive across keybind. For example, if one keybind is 'released' and another is 'pressed', the state of the keybind list is 'held'. 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; } /// Get whether any of the button sets are pressed. public bool IsDown() { SButtonState state = this.GetState(); return state is SButtonState.Pressed or SButtonState.Held; } /// Get whether the input binding was just pressed this tick. public bool JustPressed() { return this.GetState() == SButtonState.Pressed; } /// Get the keybind which is currently down, if any. If there are multiple keybinds down, the first one is returned. public Keybind? GetKeybindCurrentlyDown() { return this.Keybinds.FirstOrDefault(p => p.GetState().IsDown()); } /// Get a string representation of the input binding. /// A keybind list is serialized to a string like LeftControl + S, LeftAlt + S, where each multi-key binding is separated with , and the keys within each keybind are separated with +. The key order is commutative, so LeftControl + S and S + LeftControl are identical. public override string ToString() { return this.Keybinds.Length > 0 ? string.Join(", ", this.Keybinds.Select(p => p.ToString())) : SButton.None.ToString(); } } }