diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index 01f0a6f..fdb963c 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -75,8 +75,10 @@ + + diff --git a/BepInEx/Configuration/KeyboardShortcut.cs b/BepInEx/Configuration/KeyboardShortcut.cs new file mode 100644 index 0000000..79b2aba --- /dev/null +++ b/BepInEx/Configuration/KeyboardShortcut.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BepInEx.Logging; +using UnityEngine; +using Logger = BepInEx.Logging.Logger; + +namespace BepInEx.Configuration +{ + /// + /// A keyboard shortcut that can be used in Update method to check if user presses a key combo. The shortcut is only + /// triggered when the user presses the exact combination. For example, F + LeftCtrl will trigger only if user + /// presses and holds only LeftCtrl, and then presses F. If any other keys are pressed, the shortcut will not trigger. + /// + /// Can be used as a value of a setting in + /// to allow user to change this shortcut and have the changes saved. + /// + /// How to use: Use in this class instead of in the Update loop. + /// + public class KeyboardShortcut + { + static KeyboardShortcut() + { + TomlTypeConverter.AddConverter( + typeof(KeyboardShortcut), + new TypeConverter + { + ConvertToString = (o, type) => (o as KeyboardShortcut)?.Serialize(), + ConvertToObject = (s, type) => Deserialize(s) + }); + } + + /// + /// All KeyCode values that can be used in a keyboard shortcut. + /// + public static readonly IEnumerable AllKeyCodes = (KeyCode[])Enum.GetValues(typeof(KeyCode)); + + private readonly KeyCode[] _allKeys; + private readonly HashSet _allKeysLookup; + + /// + /// Create a new keyboard shortcut. + /// + /// Main key to press + /// Keys that should be held down before main key is registered + public KeyboardShortcut(KeyCode mainKey, params KeyCode[] modifiers) : this(new[] { mainKey }.Concat(modifiers).ToArray()) + { + if (mainKey == KeyCode.None && modifiers.Any()) + throw new ArgumentException($"Can't set {nameof(mainKey)} to KeyCode.None if there are any {nameof(modifiers)}"); + } + + private KeyboardShortcut(KeyCode[] keys) + { + _allKeys = SanitizeKeys(keys); + _allKeysLookup = new HashSet(_allKeys); + } + + public KeyboardShortcut() + { + _allKeys = SanitizeKeys(); + _allKeysLookup = new HashSet(_allKeys); + } + + private static KeyCode[] SanitizeKeys(params KeyCode[] keys) + { + if (keys.Length == 0 || keys[0] == KeyCode.None) + return new[] { KeyCode.None }; + + return new[] { keys[0] }.Concat(keys.Skip(1).Distinct().Where(x => x != keys[0]).OrderBy(x => (int)x)).ToArray(); + } + + /// + /// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered. + /// If the combination is empty, is returned. + /// + public KeyCode MainKey => _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None; + + /// + /// Modifiers of the key combination, if any. + /// + public IEnumerable Modifiers => _allKeys.Skip(1); + + /// + /// Attempt to deserialize key combination from the string. + /// + public static KeyboardShortcut Deserialize(string str) + { + try + { + var parts = str.Split(new[] { ' ', '+', ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => (KeyCode)Enum.Parse(typeof(KeyCode), x)).ToArray(); + return new KeyboardShortcut(parts); + } + catch (SystemException ex) + { + Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message); + return new KeyboardShortcut(); + } + } + + /// + /// Serialize the key combination into a user readable string. + /// + public string Serialize() + { + return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray()); + } + + /// + /// Check if the main key was just pressed (Input.GetKeyDown), and specified modifier keys are all pressed + /// + public bool IsDown() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKeyDown(mainKey) && ModifierKeyTest(); + } + + /// + /// Check if the main key is currently held down (Input.GetKey), and specified modifier keys are all pressed + /// + public bool IsPressed() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKey(mainKey) && ModifierKeyTest(); + } + + /// + /// Check if the main key was just lifted (Input.GetKeyUp), and specified modifier keys are all pressed. + /// + public bool IsUp() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKeyUp(mainKey) && ModifierKeyTest(); + } + + private bool ModifierKeyTest() + { + return AllKeyCodes.All(c => + { + if (_allKeysLookup.Contains(c)) + { + if (_allKeys[0] == c) + return true; + return Input.GetKey(c); + } + return !Input.GetKey(c); + }); + } + + /// + public override string ToString() + { + if (MainKey == KeyCode.None) return "Not set"; + + return string.Join(" + ", _allKeys.Select(c => c.ToString()).ToArray()); + } + + /// + public override bool Equals(object obj) + { + return obj is KeyboardShortcut shortcut && _allKeys.SequenceEqual(shortcut._allKeys); + } + + /// + public override int GetHashCode() + { + var hc = _allKeys.Length; + for (var i = 0; i < _allKeys.Length; i++) + hc = unchecked(hc * 31 + (int)_allKeys[i]); + return hc; + } + } +} \ No newline at end of file diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index aff8bde..2376653 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -4,15 +4,12 @@ using System.Globalization; namespace BepInEx.Configuration { - internal class TypeConverter + /// + /// Serializer/deserializer used by the config system. + /// + public static class TomlTypeConverter { - public Func ConvertToString { get; set; } - public Func ConvertToObject { get; set; } - } - - internal static class TomlTypeConverter - { - public static Dictionary TypeConverters { get; } = new Dictionary + private static Dictionary TypeConverters { get; } = new Dictionary { [typeof(string)] = new TypeConverter { @@ -91,6 +88,8 @@ namespace BepInEx.Configuration ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo), }, + //enums are special + [typeof(Enum)] = new TypeConverter { ConvertToString = (obj, type) => obj.ToString(), @@ -116,17 +115,29 @@ namespace BepInEx.Configuration { var conv = GetConverter(valueType); if (conv == null) - throw new InvalidOperationException($"Cannot convert to type {valueType}"); + throw new InvalidOperationException($"Cannot convert to type {valueType.Name}"); return conv.ConvertToObject(value, valueType); } - private static TypeConverter GetConverter(Type valueType) + public static TypeConverter GetConverter(Type valueType) { + if (valueType == null) throw new ArgumentNullException(nameof(valueType)); + if (valueType.IsEnum) return TypeConverters[typeof(Enum)]; - return TypeConverters[valueType]; + TypeConverters.TryGetValue(valueType, out var result); + return result; + } + + public static void AddConverter(Type type, TypeConverter converter) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (converter == null) throw new ArgumentNullException(nameof(converter)); + if (CanConvert(type)) throw new ArgumentException("The specified type already has a converter assigned to it", nameof(type)); + + TypeConverters.Add(type, converter); } public static bool CanConvert(Type type) diff --git a/BepInEx/Configuration/TypeConverter.cs b/BepInEx/Configuration/TypeConverter.cs new file mode 100644 index 0000000..1f1b2db --- /dev/null +++ b/BepInEx/Configuration/TypeConverter.cs @@ -0,0 +1,22 @@ +using System; + +namespace BepInEx.Configuration +{ + /// + /// A serializer/deserializer combo for some type(s). Used by the config system. + /// + public class TypeConverter + { + /// + /// Used to serialize the type into a (hopefully) human-readable string. + /// Object is the instance to serialize, Type is the object's type. + /// + public Func ConvertToString { get; set; } + + /// + /// Used to deserialize the type from a string. + /// String is the data to deserialize, Type is the object's type, should return instance to an object of Type. + /// + public Func ConvertToObject { get; set; } + } +} \ No newline at end of file diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index a65a125..96f8617 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.IO; using System.Linq; using System.Threading; +using UnityEngine; namespace BepInEx.Configuration.Tests { @@ -235,5 +236,22 @@ namespace BepInEx.Configuration.Tests w.Value = null; Assert.AreEqual("lel", w.Value); } + + [TestMethod] + public void KeyShortcutTest() + { + var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y); + var s = shortcut.Serialize(); + var d = KeyboardShortcut.Deserialize(s); + Assert.AreEqual(shortcut, d); + + var c = MakeConfig(); + var w = c.Wrap("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift)); + Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value); + + w.Value = shortcut; + c.Reload(); + Assert.AreEqual(shortcut, w.Value); + } } }