From d148bde02bc08b495fa68dbd68eb54f3580ca26f Mon Sep 17 00:00:00 2001 From: chairbender Date: Sun, 31 May 2020 11:44:16 -0700 Subject: [PATCH] Command Binding System supporting multiple bindings (#1089) --- .../EntitySystems/EyeUpdateSystem.cs | 13 +- .../GameObjects/EntitySystems/InputSystem.cs | 25 +- Robust.Client/Placement/PlacementManager.cs | 126 +++++----- .../GameObjects/EntitySystems/InputSystem.cs | 25 +- .../GameObjects/EntitySystemManager.cs | 54 +++- .../GameObjects/Systems/SharedInputSystem.cs | 8 +- Robust.Shared/Input/Binding/CommandBind.cs | 58 +++++ .../Input/Binding/CommandBindRegistry.cs | 224 +++++++++++++++++ Robust.Shared/Input/Binding/CommandBinds.cs | 175 +++++++++++++ .../Input/Binding/ICommandBindRegistry.cs | 59 +++++ Robust.Shared/Input/CommandBindMapping.cs | 55 ---- Robust.Shared/Robust.Shared.csproj | 3 + .../GameObjects/EntitySystemManager_Tests.cs | 71 ++++++ .../Input/Binding/CommandBindRegistry_Test.cs | 236 ++++++++++++++++++ 14 files changed, 969 insertions(+), 163 deletions(-) create mode 100644 Robust.Shared/Input/Binding/CommandBind.cs create mode 100644 Robust.Shared/Input/Binding/CommandBindRegistry.cs create mode 100644 Robust.Shared/Input/Binding/CommandBinds.cs create mode 100644 Robust.Shared/Input/Binding/ICommandBindRegistry.cs delete mode 100644 Robust.Shared/Input/CommandBindMapping.cs create mode 100644 Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs create mode 100644 Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs diff --git a/Robust.Client/GameObjects/EntitySystems/EyeUpdateSystem.cs b/Robust.Client/GameObjects/EntitySystems/EyeUpdateSystem.cs index f32c14a0c..6326eae93 100644 --- a/Robust.Client/GameObjects/EntitySystems/EyeUpdateSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/EyeUpdateSystem.cs @@ -5,6 +5,7 @@ using Robust.Client.ViewVariables; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Input; +using Robust.Shared.Input.Binding; using Robust.Shared.Interfaces.GameObjects.Systems; using Robust.Shared.IoC; using Robust.Shared.Maths; @@ -36,19 +37,17 @@ namespace Robust.Client.GameObjects.EntitySystems EntityQuery = new TypeEntityQuery(typeof(EyeComponent)); //WARN: Tightly couples this system with InputSystem, and assumes InputSystem exists and is initialized - var inputSystem = EntitySystemManager.GetEntitySystem(); - inputSystem.BindMap.BindFunction(EngineKeyFunctions.CameraRotateRight, new NullInputCmdHandler()); - inputSystem.BindMap.BindFunction(EngineKeyFunctions.CameraRotateLeft, new NullInputCmdHandler()); + CommandBinds.Builder + .Bind(EngineKeyFunctions.CameraRotateRight, new NullInputCmdHandler()) + .Bind(EngineKeyFunctions.CameraRotateLeft, new NullInputCmdHandler()) + .Register(); } /// public override void Shutdown() { //WARN: Tightly couples this system with InputSystem, and assumes InputSystem exists and is initialized - var inputSystem = EntitySystemManager.GetEntitySystem(); - inputSystem.BindMap.UnbindFunction(EngineKeyFunctions.CameraRotateRight); - inputSystem.BindMap.UnbindFunction(EngineKeyFunctions.CameraRotateLeft); - + CommandBinds.Unregister(); base.Shutdown(); } diff --git a/Robust.Client/GameObjects/EntitySystems/InputSystem.cs b/Robust.Client/GameObjects/EntitySystems/InputSystem.cs index a2054ff62..b427acbda 100644 --- a/Robust.Client/GameObjects/EntitySystems/InputSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/InputSystem.cs @@ -7,6 +7,7 @@ using Robust.Client.Player; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Input; +using Robust.Shared.Input.Binding; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -18,7 +19,7 @@ namespace Robust.Client.GameObjects.EntitySystems /// /// Client-side processing of all input commands through the simulation. /// - public class InputSystem : EntitySystem + public class InputSystem : SharedInputSystem { #pragma warning disable 649 [Dependency] private readonly IInputManager _inputManager; @@ -27,18 +28,12 @@ namespace Robust.Client.GameObjects.EntitySystems #pragma warning restore 649 private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates(); - private readonly CommandBindMapping _bindMap = new CommandBindMapping(); /// /// Current states for all of the keyFunctions. /// public IPlayerCommandStates CmdStates => _cmdStates; - /// - /// Holds the keyFunction -> handler bindings for the simulation. - /// - public ICommandBindMapping BindMap => _bindMap; - /// /// If the input system is currently predicting input. /// @@ -68,11 +63,10 @@ namespace Robust.Client.GameObjects.EntitySystems _cmdStates.SetState(function, message.State); // handle local binds before sending off - if (_bindMap.TryGetHandler(function, out var handler)) + foreach (var handler in BindRegistry.GetHandlers(function)) { // local handlers can block sending over the network. - if (handler.HandleCmdMessage(session, message)) - return; + if (handler.HandleCmdMessage(session, message)) return; } // send it off to the client @@ -87,15 +81,14 @@ namespace Robust.Client.GameObjects.EntitySystems { var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId); - if (!_bindMap.TryGetHandler(keyFunc, out var handler)) - return; - Predicted = true; - var session = _playerManager.LocalPlayer.Session; - handler.HandleCmdMessage(session, inputCmd); - + foreach (var handler in BindRegistry.GetHandlers(keyFunc)) + { + if (handler.HandleCmdMessage(session, inputCmd)) break; + } Predicted = false; + } private void DispatchInputCommand(FullInputCmdMessage message) diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index fb0c8dd5a..7602c7bf7 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -28,6 +28,7 @@ using Robust.Client.Interfaces.Graphics; using Robust.Client.Interfaces.Graphics.Overlays; using Robust.Client.Player; using Robust.Shared.Input; +using Robust.Shared.Input.Binding; using Robust.Shared.Utility; using Robust.Shared.Serialization; using Robust.Shared.Timing; @@ -194,87 +195,80 @@ namespace Robust.Client.Placement _overlayManager.AddOverlay(_drawOverlay); // a bit ugly, oh well - _baseClient.PlayerJoinedServer += (sender, args) => SetupInput(_entitySystemManager); - _baseClient.PlayerLeaveServer += (sender, args) => TearDownInput(_entitySystemManager); + _baseClient.PlayerJoinedServer += (sender, args) => SetupInput(); + _baseClient.PlayerLeaveServer += (sender, args) => TearDownInput(); } - private void SetupInput(IEntitySystemManager entSysMan) + private void SetupInput() { - var inputSys = entSysMan.GetEntitySystem(); - - inputSys.BindMap.BindFunction(EngineKeyFunctions.EditorLinePlace, InputCmdHandler.FromDelegate( - session => - { - if (IsActive && !Eraser) ActivateLineMode(); - })); - inputSys.BindMap.BindFunction(EngineKeyFunctions.EditorGridPlace, InputCmdHandler.FromDelegate( - session => - { - if (IsActive && !Eraser) ActivateGridMode(); - })); - - inputSys.BindMap.BindFunction(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler( - (session, coords, uid) => - { - if (!IsActive) - return false; - - if (Eraser) + CommandBinds.Builder + .Bind(EngineKeyFunctions.EditorLinePlace, InputCmdHandler.FromDelegate( + session => { - if (uid == EntityUid.Invalid) - { + if (IsActive && !Eraser) ActivateLineMode(); + })) + .Bind(EngineKeyFunctions.EditorGridPlace, InputCmdHandler.FromDelegate( + session => + { + if (IsActive && !Eraser) ActivateGridMode(); + })) + .Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler( + (session, coords, uid) => + { + if (!IsActive) return false; + + if (Eraser) + { + if (uid == EntityUid.Invalid) + { + return false; + } + + HandleDeletion(_entityManager.GetEntity(uid)); } - HandleDeletion(_entityManager.GetEntity(uid)); - } - else + else + { + _placenextframe = true; + } + + return true; + }, + (session, coords, uid) => { - _placenextframe = true; - } + if (!IsActive || Eraser || !_placenextframe) + return false; - return true; - }, - (session, coords, uid) => - { - if (!IsActive || Eraser || !_placenextframe) - return false; + //Places objects for non-tile entities + if (!CurrentPermission.IsTile) + HandlePlacement(); - //Places objects for non-tile entities - if (!CurrentPermission.IsTile) - HandlePlacement(); - - _placenextframe = false; - return true; - })); - inputSys.BindMap.BindFunction(EngineKeyFunctions.EditorRotateObject, InputCmdHandler.FromDelegate( - session => - { - if (IsActive && !Eraser) Rotate(); - })); - inputSys.BindMap.BindFunction(EngineKeyFunctions.EditorCancelPlace, InputCmdHandler.FromDelegate( - session => - { - if (!IsActive || Eraser) - return; - if (DeactivateSpecialPlacement()) - return; - Clear(); - })); + _placenextframe = false; + return true; + })) + .Bind(EngineKeyFunctions.EditorRotateObject, InputCmdHandler.FromDelegate( + session => + { + if (IsActive && !Eraser) Rotate(); + })) + .Bind(EngineKeyFunctions.EditorCancelPlace, InputCmdHandler.FromDelegate( + session => + { + if (!IsActive || Eraser) + return; + if (DeactivateSpecialPlacement()) + return; + Clear(); + })) + .Register(); var localPlayer = PlayerManager.LocalPlayer; localPlayer.EntityAttached += OnEntityAttached; } - private void TearDownInput(IEntitySystemManager entSysMan) + private void TearDownInput() { - if (entSysMan.TryGetEntitySystem(out InputSystem inputSys)) - { - inputSys.BindMap.UnbindFunction(EngineKeyFunctions.EditorLinePlace); - inputSys.BindMap.UnbindFunction(EngineKeyFunctions.EditorGridPlace); - inputSys.BindMap.UnbindFunction(EngineKeyFunctions.EditorPlaceObject); - inputSys.BindMap.UnbindFunction(EngineKeyFunctions.EditorRotateObject); - inputSys.BindMap.UnbindFunction(EngineKeyFunctions.EditorCancelPlace); - } + CommandBinds.Unregister(); if (PlayerManager.LocalPlayer != null) { diff --git a/Robust.Server/GameObjects/EntitySystems/InputSystem.cs b/Robust.Server/GameObjects/EntitySystems/InputSystem.cs index 37c904ee5..bff443ce4 100644 --- a/Robust.Server/GameObjects/EntitySystems/InputSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/InputSystem.cs @@ -6,6 +6,7 @@ using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Input; +using Robust.Shared.Input.Binding; using Robust.Shared.IoC; namespace Robust.Server.GameObjects.EntitySystems @@ -20,15 +21,10 @@ namespace Robust.Server.GameObjects.EntitySystems #pragma warning restore 649 private readonly Dictionary _playerInputs = new Dictionary(); - private readonly CommandBindMapping _bindMap = new CommandBindMapping(); + private readonly Dictionary _lastProcessedInputCmd = new Dictionary(); - /// - /// Server side input command binds. - /// - public override ICommandBindMapping BindMap => _bindMap; - /// public override void Initialize() { @@ -36,11 +32,6 @@ namespace Robust.Server.GameObjects.EntitySystems _playerManager.PlayerStatusChanged += OnPlayerStatusChanged; } - private void HandleCommandMessage(IPlayerSession session, InputCmdHandler cmdHandler, FullInputCmdMessage msg) - { - cmdHandler.HandleCmdMessage(session, msg); - } - /// public override void Shutdown() { @@ -65,15 +56,15 @@ namespace Robust.Server.GameObjects.EntitySystems if (_lastProcessedInputCmd[session] < msg.InputSequence) _lastProcessedInputCmd[session] = msg.InputSequence; + // set state, only bound key functions get state changes + var states = GetInputStates(session); + states.SetState(function, msg.State); + // route the cmdMessage to the proper bind //Client Sanitization: unbound command, just ignore - if (_bindMap.TryGetHandler(function, out var cmdHandler)) + foreach (var handler in BindRegistry.GetHandlers(function)) { - // set state, only bound key functions get state changes - var states = GetInputStates(session); - states.SetState(function, msg.State); - - HandleCommandMessage(session, cmdHandler, msg); + if (handler.HandleCmdMessage(session, msg)) return; } } diff --git a/Robust.Shared/GameObjects/EntitySystemManager.cs b/Robust.Shared/GameObjects/EntitySystemManager.cs index 8fa81ddea..adebb7da2 100644 --- a/Robust.Shared/GameObjects/EntitySystemManager.cs +++ b/Robust.Shared/GameObjects/EntitySystemManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects.Systems; using Robust.Shared.Interfaces.Reflection; @@ -21,6 +22,10 @@ namespace Robust.Shared.GameObjects /// Maps system types to instances. /// private readonly Dictionary _systems = new Dictionary(); + /// + /// Maps system supertypes to instances. + /// + private readonly Dictionary _supertypeSystems = new Dictionary(); [ViewVariables] private IReadOnlyCollection AllSystems => _systems.Values; @@ -29,9 +34,17 @@ namespace Robust.Shared.GameObjects where T : IEntitySystem { var type = typeof(T); + // check using exact match first, then check using the supertype if (!_systems.ContainsKey(type)) { - throw new InvalidEntitySystemException(); + if (!_supertypeSystems.ContainsKey(type)) + { + throw new InvalidEntitySystemException(); + } + else + { + return (T) _supertypeSystems[type]; + } } return (T)_systems[type]; @@ -47,6 +60,12 @@ namespace Robust.Shared.GameObjects return true; } + if (_supertypeSystems.TryGetValue(typeof(T), out var systemFromSupertype)) + { + entitySystem = (T) systemFromSupertype; + return true; + } + entitySystem = default; return false; } @@ -54,6 +73,8 @@ namespace Robust.Shared.GameObjects /// public void Initialize() { + HashSet excludedTypes = new HashSet(); + foreach (var type in _reflectionManager.GetAllChildren()) { Logger.DebugS("go.sys", "Initializing entity system {0}", type); @@ -61,6 +82,27 @@ namespace Robust.Shared.GameObjects var instance = _typeFactory.CreateInstance(type); _systems.Add(type, instance); + + // also register systems under their supertypes, so they can be retrieved by their supertype. + // We don't do this if there are multiple subtype systems of that supertype though, otherwise + // it wouldn't be clear which instance to return when asking for the supertype + foreach (var baseType in GetBaseTypes(type)) + { + // already known that there are multiple subtype systems of this type, + // so don't register under the supertype because it would be unclear + // which instance to return if we retrieved it by the supertype + if (excludedTypes.Contains(baseType)) continue; + if (_supertypeSystems.ContainsKey(baseType)) + { + _supertypeSystems.Remove(baseType); + excludedTypes.Add(baseType); + } + else + { + _supertypeSystems.Add(baseType, instance); + } + } + } foreach (var system in _systems.Values) @@ -69,6 +111,15 @@ namespace Robust.Shared.GameObjects } } + private static IEnumerable GetBaseTypes(Type type) { + if(type.BaseType == null) return type.GetInterfaces(); + + return Enumerable.Repeat(type.BaseType, 1) + .Concat(type.GetInterfaces()) + .Concat(type.GetInterfaces().SelectMany(GetBaseTypes)) + .Concat(GetBaseTypes(type.BaseType)); + } + /// public void Shutdown() { @@ -80,6 +131,7 @@ namespace Robust.Shared.GameObjects } _systems.Clear(); + _supertypeSystems.Clear(); } /// diff --git a/Robust.Shared/GameObjects/Systems/SharedInputSystem.cs b/Robust.Shared/GameObjects/Systems/SharedInputSystem.cs index 64f632c87..3ad55d9c9 100644 --- a/Robust.Shared/GameObjects/Systems/SharedInputSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedInputSystem.cs @@ -1,9 +1,15 @@ using Robust.Shared.Input; +using Robust.Shared.Input.Binding; namespace Robust.Shared.GameObjects.Systems { public abstract class SharedInputSystem : EntitySystem { - public abstract ICommandBindMapping BindMap { get; } + private readonly CommandBindRegistry _bindRegistry = new CommandBindRegistry(); + + /// + /// Holds the keyFunction -> handler bindings for the simulation. + /// + public ICommandBindRegistry BindRegistry => _bindRegistry; } } diff --git a/Robust.Shared/Input/Binding/CommandBind.cs b/Robust.Shared/Input/Binding/CommandBind.cs new file mode 100644 index 000000000..2bc447673 --- /dev/null +++ b/Robust.Shared/Input/Binding/CommandBind.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Robust.Shared.Input.Binding +{ + /// + /// An individual binding of a given handler to a given key function, with associated + /// dependency information to resolve handlers bound to the same key function from different types. + /// + public class CommandBind + { + private readonly BoundKeyFunction _boundKeyFunction; + private readonly IEnumerable _after; + private readonly IEnumerable _before; + private readonly InputCmdHandler _handler; + + + /// + /// Key function the handler should be triggered on + /// + public BoundKeyFunction BoundKeyFunction => _boundKeyFunction; + + /// + /// If other types register bindings for this key function, this handler will always fire + /// after them if they appear in this list. + /// + public IEnumerable After => _after; + /// + /// If other types register bindings for this key function, this handler will always fire + /// before them if they appear in this list. + /// + public IEnumerable Before => _before; + + /// + /// Handler which should handle inputs for the key function + /// + public InputCmdHandler Handler => _handler; + + /// + /// A binding of a handler to the indicated key function, with the indicated dependencies. + /// + /// key function this handler should handle + /// handler to handle the input + /// If other types register bindings for this key function, this handler will always fire + /// before them if they appear in this list. + /// If other types register bindings for this key function, this handler will always fire + /// after them if they appear in this list. + public CommandBind(BoundKeyFunction boundKeyFunction, InputCmdHandler handler, IEnumerable before = null, + IEnumerable after = null) + { + _boundKeyFunction = boundKeyFunction; + _after = after ?? Enumerable.Empty(); + _before = before ?? Enumerable.Empty(); + _handler = handler; + } + } +} diff --git a/Robust.Shared/Input/Binding/CommandBindRegistry.cs b/Robust.Shared/Input/Binding/CommandBindRegistry.cs new file mode 100644 index 000000000..c200c04d1 --- /dev/null +++ b/Robust.Shared/Input/Binding/CommandBindRegistry.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.IoC; +using Robust.Shared.Log; + +namespace Robust.Shared.Input.Binding +{ + /// + public class CommandBindRegistry : ICommandBindRegistry + { + // all registered bindings + private List _bindings = new List(); + // handlers in the order they should be resolved for the given key function. + // internally we use a graph to construct this but we render it down to a flattened + // list so we don't need to do any graph traversal at query time + private Dictionary> _bindingsForKey = + new Dictionary>(); + + /// + public void Register(CommandBinds commandBinds) + { + Register(commandBinds, typeof(TOwner)); + } + + /// + public void Register(CommandBinds commandBinds, Type owner) + { + if (_bindings.Any(existing => existing.ForType == owner)) + { + // feel free to delete this if there's an actual need for registering multiple + // bindings for a given type in separate calls to Register() + Logger.Warning("Command binds already registered for type {0}, but you are trying" + + " to register more. This may " + + "be a programming error. Did you register these under the wrong type, or " + + "did you forget to unregister these bindings when" + + " your system / manager is shutdown?", owner.Name); + } + + foreach (var binding in commandBinds.Bindings) + { + _bindings.Add(new TypedCommandBind(owner, binding)); + } + + RebuildGraph(); + } + + /// + public IEnumerable GetHandlers(BoundKeyFunction function) + { + if (_bindingsForKey.TryGetValue(function, out var handlers)) + { + return handlers; + } + return Enumerable.Empty(); + } + + /// + public void Unregister(Type owner) + { + _bindings.RemoveAll(binding => binding.ForType == owner); + RebuildGraph(); + } + + /// + public void Unregister() + { + Unregister(typeof(TOwner)); + } + + private void RebuildGraph() + { + _bindingsForKey.Clear(); + + foreach (var functionBindings in FunctionToBindings()) + { + _bindingsForKey[functionBindings.Key] = ResolveDependencies(functionBindings.Key, functionBindings.Value); + + } + + } + + private Dictionary> FunctionToBindings() + { + var functionToBindings = new Dictionary>(); + foreach (var typeBinding in _bindings) + { + if (!functionToBindings.ContainsKey(typeBinding.CommandBind.BoundKeyFunction)) + { + functionToBindings[typeBinding.CommandBind.BoundKeyFunction] = new List(); + } + + functionToBindings[typeBinding.CommandBind.BoundKeyFunction].Add(typeBinding); + } + + return functionToBindings; + } + + + /// + /// Determines the order in which the indicated bindings handlers should be resolved for a + /// particular bound key function + /// + private List ResolveDependencies(BoundKeyFunction function, List bindingsForFunction) + { + //TODO: Probably could be optimized if needed! Generally shouldn't be a big issue since there is a relatively + // tiny amount of bindings + + List allNodes = new List(); + Dictionary> typeToNode = new Dictionary>(); + // build the dict for quick lookup on type + foreach (var binding in bindingsForFunction) + { + if (!typeToNode.ContainsKey(binding.ForType)) + { + typeToNode[binding.ForType] = new List(); + } + var newNode = new GraphNode(binding); + typeToNode[binding.ForType].Add(newNode); + allNodes.Add(newNode); + } + + //add the graph edges + foreach (var curBinding in allNodes) + { + foreach (var afterType in curBinding.TypedCommandBind.CommandBind.After) + { + // curBinding should always fire after bindings associated with this afterType, i.e. + // this binding DEPENDS ON afterTypes' bindings + if (typeToNode.TryGetValue(afterType, out var afterBindings)) + { + foreach (var afterBinding in afterBindings) + { + curBinding.DependsOn.Add(afterBinding); + } + } + } + foreach (var beforeType in curBinding.TypedCommandBind.CommandBind.Before) + { + // curBinding should always fire before bindings associated with this beforeType, i.e. + // beforeTypes' bindings DEPENDS ON this binding + if (typeToNode.TryGetValue(beforeType, out var beforeBindings)) + { + foreach (var beforeBinding in beforeBindings) + { + beforeBinding.DependsOn.Add(curBinding); + } + } + } + } + + //TODO: Log graph structure for debugging + + //use toposort to build the final result + var topoSorted = TopologicalSort(allNodes, function); + List result = new List(); + + foreach (var node in topoSorted) + { + result.Add(node.TypedCommandBind.CommandBind.Handler); + } + + return result; + } + + //Adapted from https://stackoverflow.com/a/24058279 + private static IEnumerable TopologicalSort(IEnumerable nodes, BoundKeyFunction function) + { + var elems = nodes.ToDictionary(node => node, + node => new HashSet(node.DependsOn)); + while (elems.Count > 0) + { + var elem = + elems.FirstOrDefault(x => x.Value.Count == 0); + if (elem.Key == null) + { + throw new InvalidOperationException("Found circular dependency when resolving" + + $" command binding handler order for key function {function.FunctionName}." + + $" Please check the systems which register bindings for" + + $" this function and eliminate the circular dependency."); + } + elems.Remove(elem.Key); + foreach (var selem in elems) + { + selem.Value.Remove(elem.Key); + } + yield return elem.Key; + } + } + + /// + /// node in our temporary dependency graph + /// + private class GraphNode + { + public List DependsOn = new List(); + public readonly TypedCommandBind TypedCommandBind; + + public GraphNode(TypedCommandBind typedCommandBind) + { + TypedCommandBind = typedCommandBind; + } + } + + /// + /// Command bind which has an associated type. + /// The only time a client should need to think about the type for a binding is when they are + /// registering a set of bindings, so we don't include this information in CommandBind + /// + private class TypedCommandBind + { + public readonly Type ForType; + public readonly CommandBind CommandBind; + + public TypedCommandBind(Type forType, CommandBind commandBind) + { + ForType = forType; + CommandBind = commandBind; + } + } + + + } +} diff --git a/Robust.Shared/Input/Binding/CommandBinds.cs b/Robust.Shared/Input/Binding/CommandBinds.cs new file mode 100644 index 000000000..0b1ef1391 --- /dev/null +++ b/Robust.Shared/Input/Binding/CommandBinds.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.IoC; + +namespace Robust.Shared.Input.Binding +{ + /// + /// Represents a set of bindings from BoundKeyFunctions to InputCmdHandlers + /// + /// Immutable. Use Bindings.Builder() to create. + /// + public class CommandBinds + { + private readonly List _bindings; + + public IEnumerable Bindings => _bindings; + + private CommandBinds(List bindings) + { + _bindings = bindings; + } + + /// + /// Builder to build a new set of Bindings + /// + /// + public static BindingsBuilder Builder => new BindingsBuilder(); + + /// + /// Unregisters from the current InputSystem's BindRegistry all bindings currently registered under + /// indicated owner type so they will no longer receive / handle inputs. No effect if input system + /// no longer exists. + /// + /// owner type whose bindings should be unregistered, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + public static void Unregister() + { + if (EntitySystem.TryGet(out var inputSystem)) + { + Unregister(inputSystem.BindRegistry); + } + } + + /// + /// Unregisters from the given BindRegistry all bindings currently registered under + /// indicated owner type so they will no longer receive / handle inputs. No effect if input system + /// no longer exists. + /// + /// owner type whose bindings should be unregistered, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + public static void Unregister(ICommandBindRegistry bindRegistry) + { + bindRegistry.Unregister(); + } + + /// + /// For creating Bindings. + /// + public class BindingsBuilder + { + private readonly List _bindings = new List(); + + public static BindingsBuilder Create() + { + return new BindingsBuilder(); + } + + /// + /// Bind the indicated handler to the indicated function, with no + /// particular dependency on bindings from other owner types. If multiple + /// handlers in this builder are registered to the same key function, + /// the handlers will fire in the order in which they were added to this builder. + /// + public BindingsBuilder Bind(BoundKeyFunction function, InputCmdHandler command) + { + return Bind(new CommandBind(function, command)); + } + + /// + /// Bind the indicated handlers to the indicated function, with no + /// particular dependency on bindings from other owner types. + /// + /// If multiple + /// handlers in this builder are registered to the same key function, + /// the handlers will fire in the order in which they were added to this builder. + /// + public BindingsBuilder Bind(BoundKeyFunction function, IEnumerable commands) + { + foreach (var command in commands) + { + Bind(new CommandBind(function, command)); + } + + return this; + } + + /// + /// Bind the indicated handler to the indicated function. If other owner types register bindings for this key + /// function, this handler will always fire after them if they appear in the "after" list. + /// + /// If multiple handlers in this builder are registered to the same key function, + /// the handlers will fire in the order in which they were added to this builder. + /// + /// If other owner types register bindings for this key + /// function, this handler will always fire after them if they appear in this list + public BindingsBuilder BindAfter(BoundKeyFunction function, InputCmdHandler command, params Type[] after) + { + return Bind(new CommandBind(function, command, after: after)); + } + + /// + /// Bind the indicated handler to the indicated function. If other owner types register bindings for this key + /// function, this handler will always fire before them if they appear in the "before" list. + /// + /// If multiple handlers in this builder are registered to the same key function, + /// the handlers will fire in the order in which they were added to this builder. + /// + /// If other owner types register bindings for this key + /// function, this handler will always fire before them if they appear in this list + public BindingsBuilder BindBefore(BoundKeyFunction function, InputCmdHandler command, params Type[] before) + { + return Bind(new CommandBind(function, command, before)); + } + + /// + /// Add the binding to this set of bindings. If other bindings in this set + /// are bound to the same key function, they will be resolved in the order they were added + /// to this builder. + /// + public BindingsBuilder Bind(CommandBind commandBind) + { + _bindings.Add(commandBind); + return this; + } + + /// + /// Create the Bindings based on the current configuration. + /// + public CommandBinds Build() + { + return new CommandBinds(_bindings); + } + + + /// + /// Create the Bindings based on the current configuration and register + /// with the indicated mappings so they will be allowed to handle inputs. + /// + /// type that owns these bindings, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + /// mappings to register these bindings with + public CommandBinds Register(ICommandBindRegistry registry) + { + var bindings = Build(); + registry.Register(bindings); + return bindings; + } + + /// + /// Create the Bindings based on the current configuration and register + /// with the indicated mappings to the active InputSystem's BindRegistry + /// so they will be allowed to handle inputs. + /// + /// type that owns these bindings, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + public CommandBinds Register() + { + return Register(EntitySystem.Get().BindRegistry); + } + } + } +} diff --git a/Robust.Shared/Input/Binding/ICommandBindRegistry.cs b/Robust.Shared/Input/Binding/ICommandBindRegistry.cs new file mode 100644 index 000000000..768348664 --- /dev/null +++ b/Robust.Shared/Input/Binding/ICommandBindRegistry.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace Robust.Shared.Input.Binding +{ + /// + /// Allows registering bindings so that they will receive and handle inputs. Each set of bindings + /// is registered to a particular owner Type, which is typically a system or a manager. + /// + /// This association of bindings with owner types allows allows the bindings to declare + /// dependencies on each other - for example to ensure that one system's handlers will always + /// fire after another system's handlers. This also allows easy unregistering of all bindings + /// for a given system / manager. + /// + public interface ICommandBindRegistry + { + /// CO + /// Registers the indicated bindings, under the given owner type. + /// The handlers in the bindings will receive input events. + /// + /// Bindings to register. + /// type that owns these bindings, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + void Register(CommandBinds commandBinds); + + /// + /// Registers the indicated bindings, under the given type. + /// The handlers in the bindings will receive input events. + /// + /// Bindings to register. + /// type that owns these bindings, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + void Register(CommandBinds commandBinds, Type owner); + + /// + /// Gets the command handlers bound to the indicated function, in the order + /// in which they should be fired based on the dependency graph. Empty enumerable + /// if no handlers are bound. + /// + /// Key function to get the input handlers of. + IEnumerable GetHandlers(BoundKeyFunction function); + + /// + /// Unregisters all bindings currently registered under indicated type so they will + /// no longer receive / handle inputs. + /// + /// owner type whose bindings should be unregistered, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + void Unregister(Type owner); + + /// + /// Unregisters all bindings currently registered under indicated type so they will + /// no longer receive / handle inputs. + /// + /// owner type whose bindings should be unregistered, typically a system / manager, + /// should usually be typeof(this) - same type as the calling class. + void Unregister(); + } +} diff --git a/Robust.Shared/Input/CommandBindMapping.cs b/Robust.Shared/Input/CommandBindMapping.cs deleted file mode 100644 index 9dcb7dc65..000000000 --- a/Robust.Shared/Input/CommandBindMapping.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; - -namespace Robust.Shared.Input -{ - /// - /// Contains a mapping of to . - /// - public interface ICommandBindMapping - { - /// - /// Binds an input command handler to a key function. - /// - /// Key function being bound. - /// Input command handler to bind. - void BindFunction(BoundKeyFunction function, InputCmdHandler command); - - /// - /// Tries to get the command handler of a key function. - /// - /// Key function to get the input handler of. - /// command handler that was bound to the key function (if any). - /// True if the key function had a handler to return. - bool TryGetHandler(BoundKeyFunction function, out InputCmdHandler handler); - - /// - /// Unbinds the command handler from a key function. - /// - /// Key function being unbound. - void UnbindFunction(BoundKeyFunction function); - } - - /// - public class CommandBindMapping : ICommandBindMapping - { - private readonly Dictionary _commandBinds = new Dictionary(); - - /// - public void BindFunction(BoundKeyFunction function, InputCmdHandler command) - { - _commandBinds.Add(function, command); - } - - /// - public bool TryGetHandler(BoundKeyFunction function, out InputCmdHandler handler) - { - return _commandBinds.TryGetValue(function, out handler); - } - - /// - public void UnbindFunction(BoundKeyFunction function) - { - _commandBinds.Remove(function); - } - } -} diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index 2e1cd104b..ae2c51f2c 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -33,5 +33,8 @@ Robust.Shared.Utility.TypeAbbreviations.yaml + + + diff --git a/Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs b/Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs new file mode 100644 index 000000000..6597c5fd7 --- /dev/null +++ b/Robust.UnitTesting/Shared/GameObjects/EntitySystemManager_Tests.cs @@ -0,0 +1,71 @@ +using NUnit.Framework; +using Robust.Client.Reflection; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects.Systems; +using Robust.Shared.Interfaces.Log; +using Robust.Shared.Interfaces.Reflection; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.UnitTesting.Shared.Reflection; + +namespace Robust.UnitTesting.Shared.GameObjects +{ + [TestFixture, TestOf(typeof(EntitySystemManager))] + public class EntitySystemManager_Tests: RobustUnitTest + { + + public abstract class ESystemBase : IEntitySystem + { + public void Initialize() { } + public void Shutdown() { } + public void Update(float frameTime) { } + public void FrameUpdate(float frameTime) { } + } + public class ESystemA : ESystemBase { } + public class ESystemC : ESystemA { } + public abstract class ESystemBase2 : ESystemBase { } + public class ESystemB : ESystemBase2 { } + + /* + ESystemBase (Abstract) + - ESystemA + - ESystemC + - EsystemBase2 (Abstract) + - ESystemB + + */ + + [Test] + public void GetsByTypeOrSupertype() + { + var esm = IoCManager.Resolve(); + esm.Initialize(); + + // getting type by the exact type should work fine + Assert.AreEqual(esm.GetEntitySystem().GetType(), typeof(ESystemB)); + + // getting type by an abstract supertype should work fine + // because there are no other subtypes of that supertype it would conflict with + // it should return the only concrete subtype + Assert.AreEqual(esm.GetEntitySystem().GetType(), typeof(ESystemB)); + + // getting ESystemA type by its exact type should work fine, + // even though EsystemC is a subtype - it should return an instance of ESystemA + var esysA = esm.GetEntitySystem(); + Assert.AreEqual(esysA.GetType(), typeof(ESystemA)); + Assert.AreNotEqual(esysA.GetType(), typeof(ESystemC)); + + var esysC = esm.GetEntitySystem(); + Assert.AreEqual(esysC.GetType(), typeof(ESystemC)); + + // this should not work - it's abstract and there are multiple + // concrete subtypes + Assert.Throws(() => + { + esm.GetEntitySystem(); + }); + } + + } +} diff --git a/Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs b/Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs new file mode 100644 index 000000000..54de9ad51 --- /dev/null +++ b/Robust.UnitTesting/Shared/Input/Binding/CommandBindRegistry_Test.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Robust.Shared.Input; +using Robust.Shared.Input.Binding; +using Robust.Shared.Interfaces.Log; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Players; + +namespace Robust.UnitTesting.Shared.Input.Binding +{ + [TestFixture, TestOf(typeof(CommandBindRegistry))] + public class CommandBindRegistry_Test : RobustUnitTest + { + + private class TypeA { } + private class TypeB { } + private class TypeC { } + + private class TestInputCmdHandler : InputCmdHandler + { + // these vars only for tracking / debugging during testing + private readonly string name; + public readonly Type ForType; + + public TestInputCmdHandler(Type forType = null, string name = "") + { + this.ForType = forType; + this.name = name; + } + + public override string ToString() + { + return name; + } + + public override bool HandleCmdMessage(ICommonSession session, InputCmdMessage message) + { + return false; + } + } + + [TestCase(1,1)] + [TestCase(10,10)] + public void ResolvesHandlers_WhenNoDependencies(int handlersPerType, int numFunctions) + { + var registry = new CommandBindRegistry(); + var allHandlers = new Dictionary>(); + for (int i = 0; i < numFunctions; i++) + { + var bkf = new BoundKeyFunction(i.ToString()); + var theseHandlers = new List(); + allHandlers[bkf] = theseHandlers; + + var aHandlers = new List(); + var bHandlers = new List(); + var cHandlers = new List(); + for (int j = 0; j < handlersPerType; j++) + { + aHandlers.Add(new TestInputCmdHandler(typeof(TypeA))); + bHandlers.Add(new TestInputCmdHandler(typeof(TypeB))); + cHandlers.Add(new TestInputCmdHandler(typeof(TypeC))); + } + theseHandlers.AddRange(aHandlers); + theseHandlers.AddRange(bHandlers); + theseHandlers.AddRange(cHandlers); + + CommandBinds.Builder + .Bind(bkf, aHandlers) + .Register(registry); + CommandBinds.Builder + .Bind(bkf, bHandlers) + .Register(registry); + CommandBinds.Builder + .Bind(bkf, cHandlers) + .Register(registry); + } + + + //order doesn't matter, just verify that all handlers are returned + foreach (var bkfToExpectedHandlers in allHandlers) + { + var bkf = bkfToExpectedHandlers.Key; + var expectedHandlers = bkfToExpectedHandlers.Value; + HashSet returnedHandlers = registry.GetHandlers(bkf).ToHashSet(); + + CollectionAssert.AreEqual(returnedHandlers, expectedHandlers); + } + + // type b stuff should no longer fire + CommandBinds.Unregister(registry); + + foreach (var bkfToExpectedHandlers in allHandlers) + { + var bkf = bkfToExpectedHandlers.Key; + var expectedHandlers = bkfToExpectedHandlers.Value; + expectedHandlers.RemoveAll(handler => ((TestInputCmdHandler) handler).ForType == typeof(TypeB)); + HashSet returnedHandlers = registry.GetHandlers(bkf).ToHashSet(); + CollectionAssert.AreEqual(returnedHandlers, expectedHandlers); + } + } + + + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(true, true)] + public void ResolvesHandlers_WithDependency(bool before, bool after) + { + var registry = new CommandBindRegistry(); + var bkf = new BoundKeyFunction("test"); + + var aHandler1 = new TestInputCmdHandler( ); + var aHandler2 = new TestInputCmdHandler(); + var bHandler1 = new TestInputCmdHandler(); + var bHandler2 = new TestInputCmdHandler(); + var cHandler1 = new TestInputCmdHandler(); + var cHandler2 = new TestInputCmdHandler(); + + // a handler 2 should run after both b and c handlers for all the below cases + if (before && after) + { + CommandBinds.Builder + .Bind(bkf, aHandler1) + .BindAfter(bkf, aHandler2, typeof(TypeB), typeof(TypeC)) + .Register(registry); + CommandBinds.Builder + .BindBefore(bkf, bHandler1, typeof(TypeA)) + .BindBefore(bkf, bHandler2, typeof(TypeA)) + .Register(registry); + CommandBinds.Builder + .BindBefore(bkf, cHandler1, typeof(TypeA)) + .BindBefore(bkf, cHandler2, typeof(TypeA)) + .Register(registry); + } + else if (before) + { + CommandBinds.Builder + .Bind(bkf, aHandler1) + .Bind(bkf, aHandler2) + .Register(registry); + CommandBinds.Builder + .BindBefore(bkf, bHandler1, typeof(TypeA)) + .BindBefore(bkf, bHandler2, typeof(TypeA)) + .Register(registry); + CommandBinds.Builder + .BindBefore(bkf, cHandler1, typeof(TypeA)) + .BindBefore(bkf, cHandler2, typeof(TypeA)) + .Register(registry); + } + else if (after) + { + CommandBinds.Builder + .Bind(bkf, aHandler1) + .BindAfter(bkf, aHandler2, typeof(TypeB), typeof(TypeC)) + .Register(registry); + CommandBinds.Builder + .Bind(bkf, bHandler1) + .Bind(bkf, bHandler2) + .Register(registry); + CommandBinds.Builder + .Bind(bkf, cHandler1) + .Bind(bkf, cHandler2) + .Register(registry); + } + + + var returnedHandlers = registry.GetHandlers(bkf); + + // b1 , b2, c1, c2 should be fired before a2 + bool foundB1 = false, foundB2 = false, foundC1 = false, foundC2 = false; + foreach (var returnedHandler in returnedHandlers) + { + if (returnedHandler == bHandler1) + { + foundB1 = true; + } + else if (returnedHandler == bHandler2) + { + foundB2 = true; + } + else if (returnedHandler == cHandler1) + { + foundC1= true; + } + else if (returnedHandler == cHandler2) + { + foundC2 = true; + } + else if (returnedHandler == aHandler2) + { + Assert.True(foundB1 && foundB2 && foundC1 && foundC2, "bind registry didn't respect" + + " handler dependency order"); + } + } + + var expectedHandlers = + new []{aHandler1, aHandler2, bHandler1, bHandler2, cHandler1, cHandler2}; + var returnedHandlerSet = new HashSet(returnedHandlers); + foreach (var expectedHandler in expectedHandlers) + { + Assert.True(returnedHandlerSet.Contains(expectedHandler)); + } + } + + [Test] + public void ThrowsError_WhenCircularDependency() + { + var registry = new CommandBindRegistry(); + var bkf = new BoundKeyFunction("test"); + + var aHandler1 = new TestInputCmdHandler(); + var aHandler2 = new TestInputCmdHandler(); + var bHandler1 = new TestInputCmdHandler(); + var bHandler2 = new TestInputCmdHandler(); + var cHandler1 = new TestInputCmdHandler(); + var cHandler2 = new TestInputCmdHandler(); + + CommandBinds.Builder + .Bind(bkf, aHandler1) + .BindAfter(bkf, aHandler2, typeof(TypeB), typeof(TypeC)) + .Register(registry); + CommandBinds.Builder + .Bind(bkf, bHandler1) + .Bind(bkf, bHandler2) + .Register(registry); + + Assert.Throws(() => + CommandBinds.Builder + .Bind(bkf, cHandler1) + .BindAfter(bkf, cHandler2, typeof(TypeA)) + .Register(registry)); + } + } +}