using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; 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 { /********* ** Fields *********/ /// <summary>Get the current input state for a button.</summary> [Obsolete("This property should only be used for unit tests.")] internal Func<SButton, SButtonState> GetButtonState { get; set; } = SGame.GetInputState; /********* ** 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, [NotNullWhen(true)] out Keybind? parsed, out string[] errors) { // empty input if (string.IsNullOrWhiteSpace(input)) { parsed = new Keybind(SButton.None); errors = Array.Empty<string>(); return true; } // parse buttons string[] rawButtons = input.Split('+', StringSplitOptions.TrimEntries); SButton[] buttons = new SButton[rawButtons.Length]; List<string> rawErrors = new List<string>(); for (int i = 0; i < buttons.Length; i++) { string rawButton = rawButtons[i]; 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<string>(); return true; } } /// <summary>Get the keybind state relative to the previous tick.</summary> 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; } /// <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(); } } }