summaryrefslogtreecommitdiff
path: root/src/SMAPI/Utilities
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Utilities')
-rw-r--r--src/SMAPI/Utilities/Keybind.cs139
-rw-r--r--src/SMAPI/Utilities/KeybindList.cs161
-rw-r--r--src/SMAPI/Utilities/PerScreen.cs53
3 files changed, 336 insertions, 17 deletions
diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs
new file mode 100644
index 00000000..dd8d2861
--- /dev/null
+++ b/src/SMAPI/Utilities/Keybind.cs
@@ -0,0 +1,139 @@
+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
+ {
+ /*********
+ ** 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, 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(this.GetButtonState).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..1845285a
--- /dev/null
+++ b/src/SMAPI/Utilities/KeybindList.cs
@@ -0,0 +1,161 @@
+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.Distinct().ToArray();
+ return false;
+ }
+ else
+ {
+ parsed = new KeybindList(keybinds.ToArray());
+ errors = new string[0];
+ return true;
+ }
+ }
+
+ /// <summary>Get a keybind list for a single keybind.</summary>
+ /// <param name="buttons">The buttons that must be down to activate the keybind.</param>
+ public static KeybindList ForSingle(params SButton[] buttons)
+ {
+ return new KeybindList(
+ new Keybind(buttons)
+ );
+ }
+
+ /// <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();
+ }
+ }
+}
diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs
index 89d08e87..20b8fbce 100644
--- a/src/SMAPI/Utilities/PerScreen.cs
+++ b/src/SMAPI/Utilities/PerScreen.cs
@@ -11,10 +11,10 @@ namespace StardewModdingAPI.Utilities
/*********
** Fields
*********/
- /// <summary>Create the initial value for a player.</summary>
+ /// <summary>Create the initial value for a screen.</summary>
private readonly Func<T> CreateNewState;
- /// <summary>The tracked values for each player.</summary>
+ /// <summary>The tracked values for each screen.</summary>
private readonly IDictionary<int, T> States = new Dictionary<int, T>();
/// <summary>The last <see cref="Context.LastRemovedScreenId"/> value for which this instance was updated.</summary>
@@ -24,8 +24,8 @@ namespace StardewModdingAPI.Utilities
/*********
** Accessors
*********/
- /// <summary>The value for the current player.</summary>
- /// <remarks>The value is initialized the first time it's requested for that player, unless it's set manually first.</remarks>
+ /// <summary>The value for the current screen.</summary>
+ /// <remarks>The value is initialized the first time it's requested for that screen, unless it's set manually first.</remarks>
public T Value
{
get => this.GetValueForScreen(Context.ScreenId);
@@ -41,47 +41,66 @@ namespace StardewModdingAPI.Utilities
: this(null) { }
/// <summary>Construct an instance.</summary>
- /// <param name="createNewState">Create the initial state for a player screen.</param>
+ /// <param name="createNewState">Create the initial state for a screen.</param>
public PerScreen(Func<T> createNewState)
{
this.CreateNewState = createNewState ?? (() => default);
}
+ /// <summary>Get all active values by screen ID. This doesn't initialize the value for a screen ID if it's not created yet.</summary>
+ public IEnumerable<KeyValuePair<int, T>> GetActiveValues()
+ {
+ this.RemoveDeadScreens();
+ return this.States.ToArray();
+ }
+
/// <summary>Get the value for a given screen ID, creating it if needed.</summary>
/// <param name="screenId">The screen ID to check.</param>
- internal T GetValueForScreen(int screenId)
+ public T GetValueForScreen(int screenId)
{
- this.RemoveDeadPlayers();
+ this.RemoveDeadScreens();
return this.States.TryGetValue(screenId, out T state)
? state
: this.States[screenId] = this.CreateNewState();
}
- /// <summary>Set the value for a given screen ID, creating it if needed.</summary>
+ /// <summary>Set the value for a given screen ID.</summary>
/// <param name="screenId">The screen ID whose value set.</param>
/// <param name="value">The value to set.</param>
- internal void SetValueForScreen(int screenId, T value)
+ public void SetValueForScreen(int screenId, T value)
{
- this.RemoveDeadPlayers();
+ this.RemoveDeadScreens();
this.States[screenId] = value;
}
+ /// <summary>Remove all active values.</summary>
+ public void ResetAllScreens()
+ {
+ this.RemoveScreens(p => true);
+ }
+
/*********
** Private methods
*********/
- /// <summary>Remove players who are no longer have a split-screen index.</summary>
- /// <returns>Returns whether any players were removed.</returns>
- private void RemoveDeadPlayers()
+ /// <summary>Remove screens which are no longer active.</summary>
+ private void RemoveDeadScreens()
{
if (this.LastRemovedScreenId == Context.LastRemovedScreenId)
return;
-
this.LastRemovedScreenId = Context.LastRemovedScreenId;
- foreach (int id in this.States.Keys.ToArray())
+
+ this.RemoveScreens(id => !Context.HasScreenId(id));
+ }
+
+ /// <summary>Remove screens matching a condition.</summary>
+ /// <param name="shouldRemove">Returns whether a screen ID should be removed.</param>
+ private void RemoveScreens(Func<int, bool> shouldRemove)
+ {
+ foreach (var pair in this.States.ToArray())
{
- if (!Context.HasScreenId(id))
- this.States.Remove(id);
+ if (shouldRemove(pair.Key))
+ this.States.Remove(pair.Key);
}
}
}