summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Tests/Utilities/KeybindListTests.cs152
-rw-r--r--src/SMAPI/Utilities/Keybind.cs10
-rw-r--r--src/SMAPI/Utilities/KeybindList.cs2
3 files changed, 162 insertions, 2 deletions
diff --git a/src/SMAPI.Tests/Utilities/KeybindListTests.cs b/src/SMAPI.Tests/Utilities/KeybindListTests.cs
new file mode 100644
index 00000000..0bd6ec17
--- /dev/null
+++ b/src/SMAPI.Tests/Utilities/KeybindListTests.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using NUnit.Framework;
+using StardewModdingAPI;
+using StardewModdingAPI.Utilities;
+
+namespace SMAPI.Tests.Utilities
+{
+ /// <summary>Unit tests for <see cref="KeybindList"/>.</summary>
+ [TestFixture]
+ internal class KeybindListTests
+ {
+ /*********
+ ** Unit tests
+ *********/
+ /****
+ ** TryParse
+ ****/
+ /// <summary>Assert the parsed fields when constructed from a simple single-key string.</summary>
+ [TestCaseSource(nameof(KeybindListTests.GetAllButtons))]
+ public void TryParse_SimpleValue(SButton button)
+ {
+ // act
+ bool success = KeybindList.TryParse($"{button}", out KeybindList parsed, out string[] errors);
+
+ // assert
+ Assert.IsTrue(success, "Parsing unexpectedly failed.");
+ Assert.IsNotNull(parsed, "The parsed result should not be null.");
+ Assert.AreEqual(parsed.ToString(), $"{button}");
+ Assert.IsNotNull(errors, message: "The errors should never be null.");
+ Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
+ }
+
+ /// <summary>Assert the parsed fields when constructed from multi-key values.</summary>
+ [TestCase("", ExpectedResult = "None")]
+ [TestCase(" ", ExpectedResult = "None")]
+ [TestCase(null, ExpectedResult = "None")]
+ [TestCase("A + B", ExpectedResult = "A + B")]
+ [TestCase("A+B", ExpectedResult = "A + B")]
+ [TestCase(" A+ B ", ExpectedResult = "A + B")]
+ [TestCase("a +b", ExpectedResult = "A + B")]
+ [TestCase("a +b, LEFTcontrol + leftALT + LeftSHifT + delete", ExpectedResult = "A + B, LeftControl + LeftAlt + LeftShift + Delete")]
+
+ [TestCase(",", ExpectedResult = "None")]
+ [TestCase("A,", ExpectedResult = "A")]
+ [TestCase(",A", ExpectedResult = "A")]
+ public string TryParse_MultiValues(string input)
+ {
+ // act
+ bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+
+ // assert
+ Assert.IsTrue(success, "Parsing unexpectedly failed.");
+ Assert.IsNotNull(parsed, "The parsed result should not be null.");
+ Assert.IsNotNull(errors, message: "The errors should never be null.");
+ Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
+ return parsed.ToString();
+ }
+
+ /// <summary>Assert invalid values are rejected.</summary>
+ [TestCase("+", "Invalid empty button value")]
+ [TestCase("A+", "Invalid empty button value")]
+ [TestCase("+C", "Invalid empty button value")]
+ [TestCase("A + B +, C", "Invalid empty button value")]
+ [TestCase("A, TotallyInvalid", "Invalid button value 'TotallyInvalid'")]
+ [TestCase("A + TotallyInvalid", "Invalid button value 'TotallyInvalid'")]
+ public void TryParse_InvalidValues(string input, string expectedError)
+ {
+ // act
+ bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+
+ // assert
+ Assert.IsFalse(success, "Parsing unexpectedly succeeded.");
+ Assert.IsNull(parsed, "The parsed result should be null.");
+ Assert.IsNotNull(errors, message: "The errors should never be null.");
+ Assert.AreEqual(expectedError, string.Join("; ", errors), "The errors don't match the expected ones.");
+ }
+
+
+ /****
+ ** GetState
+ ****/
+ /// <summary>Assert that <see cref="KeybindList.GetState"/> returns the expected result for a given input state.</summary>
+ // single value
+ [TestCase("A", "A:Held", ExpectedResult = SButtonState.Held)]
+ [TestCase("A", "A:Pressed", ExpectedResult = SButtonState.Pressed)]
+ [TestCase("A", "A:Released", ExpectedResult = SButtonState.Released)]
+ [TestCase("A", "A:None", ExpectedResult = SButtonState.None)]
+
+ // multiple values
+ [TestCase("A + B + C, D", "A:Released, B:None, C:None, D:Pressed", ExpectedResult = SButtonState.Pressed)] // right pressed => pressed
+ [TestCase("A + B + C, D", "A:Pressed, B:Held, C:Pressed, D:None", ExpectedResult = SButtonState.Pressed)] // left pressed => pressed
+ [TestCase("A + B + C, D", "A:Pressed, B:Pressed, C:Released, D:None", ExpectedResult = SButtonState.None)] // one key released but other keys weren't down last tick => none
+ [TestCase("A + B + C, D", "A:Held, B:Held, C:Released, D:None", ExpectedResult = SButtonState.Released)] // all three keys were down last tick and now one is released => released
+
+ // transitive
+ [TestCase("A, B", "A: Released, B: Pressed", ExpectedResult = SButtonState.Held)]
+ public SButtonState GetState(string input, string stateMap)
+ {
+ // act
+ bool success = KeybindList.TryParse(input, out KeybindList parsed, out string[] errors);
+ if (success && parsed?.Keybinds != null)
+ {
+ foreach (var keybind in parsed.Keybinds)
+#pragma warning disable 618 // method is marked obsolete because it should only be used in unit tests
+ keybind.GetButtonState = key => this.GetStateFromMap(key, stateMap);
+#pragma warning restore 618
+ }
+
+ // assert
+ Assert.IsTrue(success, "Parsing unexpected failed");
+ Assert.IsNotNull(parsed, "The parsed result should not be null.");
+ Assert.IsNotNull(errors, message: "The errors should never be null.");
+ Assert.IsEmpty(errors, message: "The input bindings incorrectly reported errors.");
+ return parsed.GetState();
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get all defined buttons.</summary>
+ private static IEnumerable<SButton> GetAllButtons()
+ {
+ foreach (SButton button in Enum.GetValues(typeof(SButton)))
+ yield return button;
+ }
+
+ /// <summary>Get the button state defined by a mapping string.</summary>
+ /// <param name="button">The button to check.</param>
+ /// <param name="stateMap">The state map.</param>
+ private SButtonState GetStateFromMap(SButton button, string stateMap)
+ {
+ foreach (string rawPair in stateMap.Split(','))
+ {
+ // parse values
+ string[] parts = rawPair.Split(new[] { ':' }, 2);
+ if (!Enum.TryParse(parts[0], ignoreCase: true, out SButton curButton))
+ Assert.Fail($"The state map is invalid: unknown button value '{parts[0].Trim()}'");
+ if (!Enum.TryParse(parts[1], ignoreCase: true, out SButtonState state))
+ Assert.Fail($"The state map is invalid: unknown state value '{parts[1].Trim()}'");
+
+ // get state
+ if (curButton == button)
+ return state;
+ }
+
+ Assert.Fail($"The state map doesn't define button value '{button}'.");
+ return SButtonState.None;
+ }
+ }
+}
diff --git a/src/SMAPI/Utilities/Keybind.cs b/src/SMAPI/Utilities/Keybind.cs
index 9d6cd6ee..dd8d2861 100644
--- a/src/SMAPI/Utilities/Keybind.cs
+++ b/src/SMAPI/Utilities/Keybind.cs
@@ -10,6 +10,14 @@ namespace StardewModdingAPI.Utilities
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>
@@ -97,7 +105,7 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get the keybind state relative to the previous tick.</summary>
public SButtonState GetState()
{
- SButtonState[] states = this.Buttons.Select(SGame.GetInputState).Distinct().ToArray();
+ SButtonState[] states = this.Buttons.Select(this.GetButtonState).Distinct().ToArray();
// single state
if (states.Length == 1)
diff --git a/src/SMAPI/Utilities/KeybindList.cs b/src/SMAPI/Utilities/KeybindList.cs
index 4ae66ab7..1845285a 100644
--- a/src/SMAPI/Utilities/KeybindList.cs
+++ b/src/SMAPI/Utilities/KeybindList.cs
@@ -72,7 +72,7 @@ namespace StardewModdingAPI.Utilities
if (rawErrors.Any())
{
parsed = null;
- errors = rawErrors.ToArray();
+ errors = rawErrors.Distinct().ToArray();
return false;
}
else