Skip to content

Commit

Permalink
Click Migration + Input Contexts (space-wizards#656)
Browse files Browse the repository at this point in the history
* Migrated click handling from old `ClickParser` to new `InputSystem`. Resolves space-wizards#631.
* Input Context system added. Resolves space-wizards#630.
* `PlacementManager` now uses its own input context.
* Added addon `Entrypoint.PostInit` function.
* Moved EntitySystemManager initialization from after the first state update, to right after the EntitySystem init.
* Made the EntitySystems only run Update/FrameUpdate/events after the map has been initialized.
  • Loading branch information
Acruid authored and PJB3005 committed Aug 16, 2018
1 parent 900e2c9 commit 0c66313
Show file tree
Hide file tree
Showing 29 changed files with 860 additions and 249 deletions.
3 changes: 3 additions & 0 deletions Resources/Prototypes/Engine/Entities/Mobs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@

- type: Collidable
DebugColor: "#0000FF"

- type: Input
context: "human"

17 changes: 17 additions & 0 deletions Resources/keybinds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,20 @@ binds:
- function: FocusChatWindow
key: T
type: State
- function: EditorLinePlace
key: MouseLeft
mod1: Shift
type: State
- function: EditorGridPlace
key: MouseLeft
mod1: Control
type: State
- function: EditorPlaceObject
key: MouseLeft
type: State
- function: EditorCancelPlace
key: MouseRight
type: State
- function: EditorRotateObject
key: MouseMiddle
type: State
2 changes: 2 additions & 0 deletions SS14.Client/GameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ public override void Main(Godot.SceneTree tree)

_client.Initialize();

AssemblyLoader.BroadcastRunLevel(AssemblyLoader.RunLevel.PostInit);

_stateManager.RequestStateChange<MainScreen>();
}

Expand Down
5 changes: 4 additions & 1 deletion SS14.Client/GameObjects/ClientComponentFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using SS14.Client.Interfaces.GameObjects;
using SS14.Client.GameObjects.Components;
using SS14.Client.Interfaces.GameObjects;
using SS14.Client.Interfaces.GameObjects.Components;
using SS14.Shared.GameObjects;
using SS14.Shared.GameObjects.Components.BoundingBox;
Expand All @@ -20,6 +21,8 @@ public ClientComponentFactory()
Register<TransformComponent>();
RegisterReference<TransformComponent, ITransformComponent>();

Register<InputComponent>();

Register<PlayerInputMoverComponent>();
RegisterReference<PlayerInputMoverComponent, IMoverComponent>();

Expand Down
11 changes: 9 additions & 2 deletions SS14.Client/GameObjects/ClientEntityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ public sealed class ClientEntityManager : EntityManager, IClientEntityManager, I
{
[Dependency]
readonly IMapManager _mapManager;

[Dependency]
private readonly IEntitySystemManager _systemManager;

private int NextClientEntityUid = EntityUid.ClientUid + 1;

public override void Initialize()
{
base.Initialize();

_systemManager.Initialize();
}

public IEnumerable<IEntity> GetEntitiesInRange(GridLocalCoordinates position, float Range)
{
var AABB = new Box2(position.Position - new Vector2(Range / 2, Range / 2), position.Position + new Vector2(Range / 2, Range / 2));
Expand Down Expand Up @@ -110,7 +118,6 @@ public override void Startup()
}

InitializeEntities();
EntitySystemManager.Initialize();
Started = true;
}

Expand Down
29 changes: 29 additions & 0 deletions SS14.Client/GameObjects/Components/Input/InputComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using SS14.Client.GameObjects.EntitySystems;
using SS14.Shared.GameObjects;
using SS14.Shared.Input;
using SS14.Shared.Serialization;

namespace SS14.Client.GameObjects.Components
{
/// <summary>
/// Defines data fields used in the <see cref="InputSystem"/>.
/// </summary>
class InputComponent : Component
{
/// <inheritdoc />
public override string Name => "Input";

/// <summary>
/// The context that will be made active for a client that attaches to this entity.
/// </summary>
public string ContextName { get; set; }

/// <inheritdoc />
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);

serializer.DataReadWriteFunction("context", InputContextContainer.DefaultContextName, value => ContextName = value, () => ContextName);
}
}
}
101 changes: 100 additions & 1 deletion SS14.Client/GameObjects/EntitySystems/InputSystem.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
using System;
using SS14.Client.GameObjects.Components;
using SS14.Client.Interfaces.Input;
using SS14.Client.Interfaces.Player;
using SS14.Shared.GameObjects;
using SS14.Shared.GameObjects.Systems;
using SS14.Shared.Input;
using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Players;

namespace SS14.Client.GameObjects.EntitySystems
{
/// <summary>
/// Client-side processing of all input commands through the simulation.
/// </summary>
class InputSystem : EntitySystem
{
private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates();
private readonly CommandBindMapping _bindMap = new CommandBindMapping();

/// <summary>
/// Current states for all of the keyFunctions.
/// </summary>
public IPlayerCommandStates CmdStates => _cmdStates;
public ICommandBindMapping BindMap => _bindMap;

/// <summary>
/// Holds the keyFunction -> handler bindings for the simulation.
/// </summary>
public ICommandBindMapping BindMap => _bindMap;

/// <summary>
/// Inserts an Input Command into the simulation.
/// </summary>
/// <param name="session">Player session that raised the command. On client, this is always the LocalPlayer session.</param>
/// <param name="function">Function that is being changed.</param>
/// <param name="message">Arguments for this event.</param>
public void HandleInputCommand(ICommonSession session, BoundKeyFunction function, FullInputCmdMessage message)
{
// set state, state change is updated regardless if it is locally bound
Expand All @@ -29,5 +51,82 @@ public void HandleInputCommand(ICommonSession session, BoundKeyFunction function

RaiseNetworkEvent(message);
}

/// <inheritdoc />
public override void SubscribeEvents()
{
base.SubscribeEvents();

SubscribeEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
}

private static void OnAttachedEntityChanged(object sender, EntitySystemMessage message)
{
if(!(message is PlayerAttachSysMessage msg))
return;

var inputMan = IoCManager.Resolve<IInputManager>();

if (msg.AttachedEntity != null) // attach
{
SetEntityContextActive(inputMan, msg.AttachedEntity);
}
else // detach
{
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
}
}

private static void SetEntityContextActive(IInputManager inputMan, IEntity entity)
{
if(entity == null || !entity.IsValid())
throw new ArgumentNullException(nameof(entity));

if (!entity.TryGetComponent(out InputComponent inputComp))
{
Logger.DebugS("input.context", $"AttachedEnt has no InputComponent: entId={entity.Uid}, entProto={entity.Prototype}");
return;
}

if (inputMan.Contexts.Exists(inputComp.ContextName))
{
inputMan.Contexts.SetActiveContext(inputComp.ContextName);
}
else
{
Logger.ErrorS("input.context", $"Unknown context: entId={entity.Uid}, entProto={entity.Prototype}, context={inputComp.ContextName}");
}
}

/// <summary>
/// Sets the active context to the defined context on the attached entity.
/// </summary>
public void SetEntityContextActive()
{
var inputMan = IoCManager.Resolve<IInputManager>();
var localPlayer = IoCManager.Resolve<IPlayerManager>().LocalPlayer;

SetEntityContextActive(inputMan, localPlayer.ControlledEntity);
}
}

/// <summary>
/// Entity system message that is raised when the player changes attached entities.
/// </summary>
public class PlayerAttachSysMessage : EntitySystemMessage
{
/// <summary>
/// New entity the player is attached to.
/// </summary>
public IEntity AttachedEntity { get; }

/// <summary>
/// Creates a new instance of <see cref="PlayerAttachSysMessage"/>.
/// </summary>
/// <param name="attachedEntity">New entity the player is attached to.</param>
public PlayerAttachSysMessage(IEntity attachedEntity)
{
AttachedEntity = attachedEntity;
}
}
}
36 changes: 36 additions & 0 deletions SS14.Client/Input/EngineContexts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using SS14.Shared.Input;

namespace SS14.Client.Input
{
/// <summary>
/// Contains a helper function for setting up all default engine contexts.
/// </summary>
internal static class EngineContexts
{
/// <summary>
/// Adds the default set of engine contexts to a context container.
/// </summary>
/// <param name="contexts">Default contexts will be set up inside this container.</param>
public static void SetupContexts(IInputContextContainer contexts)
{
var common = contexts.GetContext(InputContextContainer.DefaultContextName);
common.AddFunction(EngineKeyFunctions.EscapeMenu);
common.AddFunction(EngineKeyFunctions.FocusChat);
common.AddFunction(EngineKeyFunctions.ShowDebugMonitors);

var editor = contexts.New("editor", common);
editor.AddFunction(EngineKeyFunctions.EditorLinePlace);
editor.AddFunction(EngineKeyFunctions.EditorGridPlace);
editor.AddFunction(EngineKeyFunctions.EditorPlaceObject);
editor.AddFunction(EngineKeyFunctions.EditorCancelPlace);
editor.AddFunction(EngineKeyFunctions.EditorRotateObject);

var human = contexts.New("human", common);
human.AddFunction(EngineKeyFunctions.MoveUp);
human.AddFunction(EngineKeyFunctions.MoveDown);
human.AddFunction(EngineKeyFunctions.MoveLeft);
human.AddFunction(EngineKeyFunctions.MoveRight);
human.AddFunction(EngineKeyFunctions.Run);
}
}
}
30 changes: 29 additions & 1 deletion SS14.Client/Input/InputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,20 @@ public class InputManager : IInputManager
/// <inheritdoc />
public BoundKeyMap NetworkBindMap { get; private set; }

/// <inheritdoc />
public IInputContextContainer Contexts { get; } = new InputContextContainer();

public event Action<BoundKeyEventArgs> KeyBindStateChanged;

public void Initialize()
{
NetworkBindMap = new BoundKeyMap(_reflectionManager);
NetworkBindMap.PopulateKeyFunctionsMap();

EngineContexts.SetupContexts(Contexts);

Contexts.ContextChanged += OnContextChanged;

LoadKeyFile(new ResourcePath("/keybinds.yml"));
var path = new ResourcePath("/keybinds_content.yml");
if (_resourceMan.ContentFileExists(path))
Expand All @@ -59,6 +66,19 @@ public void Initialize()
_netManager.RegisterNetMessage<MsgKeyFunctionStateChange>(MsgKeyFunctionStateChange.NAME);
}

private void OnContextChanged(object sender, ContextChangedEventArgs args)
{
// keyup any commands that are not in the new contexts, because it will not exist in the new context and get filtered. Luckily
// the diff does not have to be symmetrical, otherwise instead of 'A \ B' we allocate all the things with '(A \ B) ∪ (B \ A)'
// It should be OK to artificially keyup these, because in the future the organic keyup will be blocked (either the context
// does not have the binding, or the double keyup check in UpBind will block it).
foreach (var function in args.OldContext.Except(args.NewContext))
{
var bind = _bindings.Find(binding => binding.Function == function);
SetBindState(bind, BoundKeyState.Up);
}
}

public void KeyDown(KeyEventArgs args)
{
if (!Enabled || UIBlocked() || args.Key == Keyboard.Key.Unknown)
Expand All @@ -69,11 +89,15 @@ public void KeyDown(KeyEventArgs args)
var internalKey = KeyToInternal(args.Key);
_keysPressed[internalKey] = true;

int matchedCombo = 0;
var matchedCombo = 0;

// bindings are ordered with larger combos before single key bindings so combos have priority.
foreach (var binding in _bindings)
{
// check if our binding is even in the active context
if(!Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
continue;

if (PackedMatchesPressedState(binding.PackedKeyCombo))
{
// this statement *should* always be true first
Expand Down Expand Up @@ -101,6 +125,10 @@ public void KeyUp(KeyEventArgs args)
var internalKey = KeyToInternal(args.Key);
foreach (var binding in _bindings)
{
// check if our binding is even in the active context
if (!Contexts.ActiveContext.FunctionExistsHierarchy(binding.Function))
continue;

if (PackedContainsKey(binding.PackedKeyCombo, internalKey) && PackedMatchesPressedState(binding.PackedKeyCombo))
{
UpBind(binding);
Expand Down
2 changes: 2 additions & 0 deletions SS14.Client/Interfaces/Input/IInputManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public interface IInputManager

BoundKeyMap NetworkBindMap { get; }

IInputContextContainer Contexts { get; }

void Initialize();

void KeyDown(KeyEventArgs e);
Expand Down
6 changes: 0 additions & 6 deletions SS14.Client/Interfaces/Placement/IPlacementManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using SS14.Client.Placement;
using SS14.Shared;
using SS14.Shared.Enums;
using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.Map;

namespace SS14.Client.Interfaces.Placement
Expand All @@ -18,16 +17,11 @@ public interface IPlacementManager

event EventHandler PlacementCanceled;

void HandleDeletion(IEntity entity);
void HandlePlacement();
void BeginPlacing(PlacementInformation info);
void Render();
void Clear();
void ToggleEraser();
void Rotate();

bool MouseDown(MouseButtonEventArgs e);
bool MouseUp(MouseButtonEventArgs e);
void FrameUpdate(RenderFrameEventArgs e);
}
}
Loading

0 comments on commit 0c66313

Please sign in to comment.