summaryrefslogtreecommitdiff
path: root/src/SMAPI/Utilities/Keybind.cs
blob: 3532620d3bb38488c9c8f214895e042c622b8a47 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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();
        }
    }
}