From 30bd1d70b543fc24281d278c4e63dea388eacbef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Aug 2017 16:08:43 +0900 Subject: [PATCH] ActionMapping doesn't support concurrent actions by default But can when required. Also supports key combination bindings now. --- osu.Game.Rulesets.Catch/CatchInputManager.cs | 4 +- osu.Game/Input/ActionMappingInputManager.cs | 90 +++++++++++++------ osu.Game/Input/Binding.cs | 28 +++++- osu.Game/Input/BindingStore.cs | 18 ++++ .../Input/GlobalActionMappingInputManager.cs | 2 +- osu.Game/Input/KeyCombination.cs | 58 ++++++++++++ osu.Game/osu.Game.csproj | 1 + 7 files changed, 167 insertions(+), 34 deletions(-) create mode 100644 osu.Game/Input/KeyCombination.cs diff --git a/osu.Game.Rulesets.Catch/CatchInputManager.cs b/osu.Game.Rulesets.Catch/CatchInputManager.cs index 98d9e7f505..40020e8649 100644 --- a/osu.Game.Rulesets.Catch/CatchInputManager.cs +++ b/osu.Game.Rulesets.Catch/CatchInputManager.cs @@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Catch { public class CatchInputManager : ActionMappingInputManager { - public CatchInputManager(RulesetInfo ruleset) : base(ruleset) + public CatchInputManager(RulesetInfo ruleset) : base(ruleset, allowConcurrentActions: true) { } - protected override IDictionary CreateDefaultMappings() => new Dictionary + protected override IDictionary CreateDefaultMappings() => new Dictionary { { Key.Z, CatchAction.MoveLeft }, { Key.Left, CatchAction.MoveLeft }, diff --git a/osu.Game/Input/ActionMappingInputManager.cs b/osu.Game/Input/ActionMappingInputManager.cs index b329d69ccd..db6561849c 100644 --- a/osu.Game/Input/ActionMappingInputManager.cs +++ b/osu.Game/Input/ActionMappingInputManager.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Input; using osu.Game.Rulesets; -using OpenTK.Input; namespace osu.Game.Input { @@ -21,58 +21,92 @@ namespace osu.Game.Input private readonly int? variant; + private readonly bool allowConcurrentActions; + + private readonly List mappings = new List(); + /// /// Create a new instance. /// /// A reference to identify the current . Used to lookup mappings. Null for global mappings. /// An optional variant for the specified . Used when a ruleset has more than one possible keyboard layouts. - protected ActionMappingInputManager(RulesetInfo ruleset = null, int? variant = null) + /// Allow concurrent actions to be actuated at once. Note that this disables chord bindings. + protected ActionMappingInputManager(RulesetInfo ruleset = null, int? variant = null, bool allowConcurrentActions = false) { this.ruleset = ruleset; this.variant = variant; - - Mappings = CreateDefaultMappings(); + this.allowConcurrentActions = allowConcurrentActions; } - protected IDictionary Mappings { get; private set; } + protected abstract IDictionary CreateDefaultMappings(); - protected abstract IDictionary CreateDefaultMappings(); + private BindingStore store; [BackgroundDependencyLoader] private void load(BindingStore bindings) { - var rulesetId = ruleset?.ID; - foreach (var b in bindings.Query(b => b.RulesetID == rulesetId && b.Variant == variant)) - Mappings[b.Key] = (T)(object)b.Action; + store = bindings; + ReloadMappings(); } + protected void ReloadMappings() + { + var rulesetId = ruleset?.ID; + + mappings.Clear(); + + foreach (var kvp in CreateDefaultMappings()) + mappings.Add(new Binding(kvp.Key, kvp.Value)); + + if (store != null) + { + foreach (var b in store.Query(b => b.RulesetID == rulesetId && b.Variant == variant)) + mappings.Add(b); + } + + if (allowConcurrentActions) + { + // ensure we have no overlapping bindings. + foreach (var m in mappings) + foreach (var colliding in mappings.Where(k => !k.Keys.Equals(m.Keys) && k.Keys.CheckValid(m.Keys.Keys))) + throw new InvalidOperationException($"Multiple partially overlapping bindings are not supported ({m} and {colliding} are colliding)!"); + } + } + + private readonly List pressedBindings = new List(); + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - mapKey(state, args.Key); + if (!args.Repeat && (allowConcurrentActions || pressedBindings.Count == 0)) + { + Binding validBinding; + + if ((validBinding = mappings.Except(pressedBindings).LastOrDefault(m => m.Keys.CheckValid(state.Keyboard.Keys))) != null) + { + // store both the pressed combination and the resulting action, just in case the assignments change while we are actuated. + pressedBindings.Add(validBinding); + state.Data = validBinding.GetAction(); + } + } + return base.OnKeyDown(state, args); } protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) { - mapKey(state, args.Key); + foreach (var binding in pressedBindings.ToList()) + { + if (!binding.Keys.CheckValid(state.Keyboard.Keys)) + { + // set data as KeyUp. + state.Data = binding.GetAction(); + + // and clear the no-longer-valid combination/action. + pressedBindings.Remove(binding); + } + } + return base.OnKeyUp(state, args); } - - private void mapKey(InputState state, Key key) - { - T mappedData; - if (Mappings.TryGetValue(key, out mappedData)) - state.Data = mappedData; - } - - private T parseStringRepresentation(string str) - { - T res; - - if (Enum.TryParse(str, out res)) - return res; - - return default(T); - } } } diff --git a/osu.Game/Input/Binding.cs b/osu.Game/Input/Binding.cs index e887d15a65..11db78ec93 100644 --- a/osu.Game/Input/Binding.cs +++ b/osu.Game/Input/Binding.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets; -using OpenTK.Input; using SQLite.Net.Attributes; using SQLiteNetExtensions.Attributes; @@ -16,8 +15,31 @@ namespace osu.Game.Input [Indexed] public int? Variant { get; set; } - public Key Key { get; set; } + [Column("Keys")] + public string KeysString + { + get { return Keys.ToString(); } + set { Keys = value; } + } - public int Action { get; set; } + [Ignore] + public KeyCombination Keys { get; private set; } + + public int Action { get; private set; } + + public Binding() + { + + } + + public Binding(KeyCombination keys, object action) + { + Keys = keys; + Action = (int)action; + } + + public virtual T GetAction() => (T)(object)Action; + + public override string ToString() => $"{KeysString}=>{Action}"; } } \ No newline at end of file diff --git a/osu.Game/Input/BindingStore.cs b/osu.Game/Input/BindingStore.cs index aa47bee068..f66d481a74 100644 --- a/osu.Game/Input/BindingStore.cs +++ b/osu.Game/Input/BindingStore.cs @@ -15,6 +15,24 @@ namespace osu.Game.Input { } + protected override int StoreVersion => 2; + + protected override void PerformMigration(int currentVersion, int targetVersion) + { + base.PerformMigration(currentVersion, targetVersion); + + while (currentVersion++ < targetVersion) + { + switch (currentVersion) + { + case 1: + // cannot migrate; breaking underlying changes. + Reset(); + break; + } + } + } + protected override void Prepare(bool reset = false) { Connection.CreateTable(); diff --git a/osu.Game/Input/GlobalActionMappingInputManager.cs b/osu.Game/Input/GlobalActionMappingInputManager.cs index 968d092f8d..cd601ac944 100644 --- a/osu.Game/Input/GlobalActionMappingInputManager.cs +++ b/osu.Game/Input/GlobalActionMappingInputManager.cs @@ -8,7 +8,7 @@ namespace osu.Game.Input { public class GlobalActionMappingInputManager : ActionMappingInputManager { - protected override IDictionary CreateDefaultMappings() => new Dictionary + protected override IDictionary CreateDefaultMappings() => new Dictionary { { Key.F8, OsuAction.ToggleChat }, { Key.F9, OsuAction.ToggleSocial }, diff --git a/osu.Game/Input/KeyCombination.cs b/osu.Game/Input/KeyCombination.cs new file mode 100644 index 0000000000..ef1d578dee --- /dev/null +++ b/osu.Game/Input/KeyCombination.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenTK.Input; + +namespace osu.Game.Input +{ + /// + /// Represent a combination of more than one s. + /// + public class KeyCombination : IEquatable + { + public readonly IEnumerable Keys; + + public KeyCombination(params Key[] keys) + { + Keys = keys; + } + + public KeyCombination(IEnumerable keys) + { + Keys = keys; + } + + public KeyCombination(string stringRepresentation) + { + Keys = stringRepresentation.Split(',').Select(s => (Key)int.Parse(s)); + } + + public bool CheckValid(IEnumerable keys) => !Keys.Except(keys).Any(); + + public bool Equals(KeyCombination other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Keys.SequenceEqual(other.Keys); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((KeyCombination)obj); + } + + public override int GetHashCode() => Keys != null ? Keys.Select(k => k.GetHashCode()).Aggregate((h1, h2) => h1 + h2) : 0; + + public static implicit operator KeyCombination(Key singleKey) => new KeyCombination(singleKey); + + public static implicit operator KeyCombination(string stringRepresentation) => new KeyCombination(stringRepresentation); + + public override string ToString() => Keys.Select(k => ((int)k).ToString()).Aggregate((s1, s2) => $"{s1},{s2}"); + } +} \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 539e77f142..6aff6734e0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -94,6 +94,7 @@ +