using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Framework; namespace StardewModdingAPI.Utilities { /// A single multi-key binding which can be triggered by the player. /// NOTE: this is part of , and usually shouldn't be used directly. public class Keybind { /********* ** Fields *********/ /// Get the current input state for a button. [Obsolete("This property should only be used for unit tests.")] internal Func GetButtonState { get; set; } = SGame.GetInputState; /********* ** Accessors *********/ /// The buttons that must be down to activate the keybind. public SButton[] Buttons { get; } /// Whether any keys are bound. public bool IsBound { get; } /********* ** Public methods *********/ /// Construct an instance. /// The buttons that must be down to activate the keybind. public Keybind(params SButton[] buttons) { this.Buttons = buttons; this.IsBound = buttons.Any(p => p != SButton.None); } /// Parse a keybind string, if it's valid. /// The keybind string. See remarks on for format details. /// The parsed keybind, if valid. /// The parse errors, if any. public static bool TryParse(string input, [NotNullWhen(true)] out Keybind? parsed, out string[] errors) { // empty input if (string.IsNullOrWhiteSpace(input)) { parsed = new Keybind(SButton.None); errors = Array.Empty(); return true; } // parse buttons string[] rawButtons = input.Split('+'); SButton[] buttons = new SButton[rawButtons.Length]; List rawErrors = new List(); 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 = Array.Empty(); return true; } } /// Get the keybind state relative to the previous tick. public SButtonState GetState() { #pragma warning disable CS0618 // Type or member is obsolete: deliberate call to GetButtonState() for unit tests SButtonState[] states = this.Buttons.Select(this.GetButtonState).Distinct().ToArray(); #pragma warning restore CS0618 // 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 is SButtonState.Pressed or SButtonState.Held)) return SButtonState.Pressed; // mix of held + released => released if (states.All(p => p is SButtonState.Held or SButtonState.Released)) return SButtonState.Released; // not down last tick or now return SButtonState.None; } /// Get a string representation of the keybind. /// A keybind is serialized to a string like LeftControl + S, where each key is separated with +. The key order is commutative, so LeftControl + S and S + LeftControl are identical. public override string ToString() { return this.Buttons.Length > 0 ? string.Join(" + ", this.Buttons) : SButton.None.ToString(); } } }