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();
}
}
}