using System; using Robust.Client.GameStates; using Robust.Client.Input; using Robust.Client.Player; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Players; using Robust.Shared.Utility; namespace Robust.Client.GameObjects { /// /// Client-side processing of all input commands through the simulation. /// public class InputSystem : SharedInputSystem { [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IClientGameStateManager _stateManager = default!; private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates(); /// /// Current states for all of the keyFunctions. /// public IPlayerCommandStates CmdStates => _cmdStates; /// /// If the input system is currently predicting input. /// public bool Predicted { get; private set; } /// /// Inserts an Input Command into the simulation. /// /// Player session that raised the command. On client, this is always the LocalPlayer session. /// Function that is being changed. /// Arguments for this event. /// if true, current cmd state will not be checked or updated - use this for "replaying" an /// old input that was saved or buffered until further processing could be done public bool HandleInputCommand(ICommonSession? session, BoundKeyFunction function, FullInputCmdMessage message, bool replay = false) { #if DEBUG var funcId = _inputManager.NetworkBindMap.KeyFunctionID(function); DebugTools.Assert(funcId == message.InputFunctionId, "Function ID in message does not match function."); #endif if (!replay) { // set state, state change is updated regardless if it is locally bound if (_cmdStates.GetState(function) == message.State) { return false; } _cmdStates.SetState(function, message.State); } // handle local binds before sending off foreach (var handler in BindRegistry.GetHandlers(function)) { // local handlers can block sending over the network. if (handler.HandleCmdMessage(session, message)) { return true; } } // send it off to the server DispatchInputCommand(message); return false; } /// /// Handle a predicted input command. /// /// Input command to handle as predicted. public void PredictInputCommand(FullInputCmdMessage inputCmd) { DebugTools.AssertNotNull(_playerManager.LocalPlayer); var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId); Predicted = true; var session = _playerManager.LocalPlayer!.Session; foreach (var handler in BindRegistry.GetHandlers(keyFunc)) { if (handler.HandleCmdMessage(session, inputCmd)) break; } Predicted = false; } private void DispatchInputCommand(FullInputCmdMessage message) { _stateManager.InputCommandDispatched(message); EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, message.InputSequence); } public override void Initialize() { SubscribeLocalEvent(OnAttachedEntityChanged); } private void OnAttachedEntityChanged(PlayerAttachSysMessage message) { if (message.AttachedEntity != default) // attach { SetEntityContextActive(_inputManager, message.AttachedEntity); } else // detach { _inputManager.Contexts.SetActiveContext(InputContextContainer.DefaultContextName); } } private void SetEntityContextActive(IInputManager inputMan, EntityUid entity) { if(entity == default || !EntityManager.EntityExists(entity)) throw new ArgumentNullException(nameof(entity)); if (!EntityManager.TryGetComponent(entity, out InputComponent? inputComp)) { Logger.DebugS("input.context", $"AttachedEnt has no InputComponent: entId={entity}, entProto={EntityManager.GetComponent(entity).EntityPrototype}. Setting default \"{InputContextContainer.DefaultContextName}\" context..."); inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName); return; } if (inputMan.Contexts.Exists(inputComp.ContextName)) { inputMan.Contexts.SetActiveContext(inputComp.ContextName); } else { Logger.ErrorS("input.context", $"Unknown context: entId={entity}, entProto={EntityManager.GetComponent(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context..."); inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName); } } /// /// Sets the active context to the defined context on the attached entity. /// public void SetEntityContextActive() { var controlled = _playerManager.LocalPlayer?.ControlledEntity ?? EntityUid.Invalid; if (controlled == EntityUid.Invalid) { return; } SetEntityContextActive(_inputManager, controlled); } } /// /// Entity system message that is raised when the player changes attached entities. /// public class PlayerAttachSysMessage : EntityEventArgs { /// /// New entity the player is attached to. /// public EntityUid AttachedEntity { get; } /// /// Creates a new instance of . /// /// New entity the player is attached to. public PlayerAttachSysMessage(EntityUid attachedEntity) { AttachedEntity = attachedEntity; } } public class PlayerAttachedEvent : EntityEventArgs { public PlayerAttachedEvent(EntityUid entity) { Entity = entity; } public EntityUid Entity { get; } } public class PlayerDetachedEvent : EntityEventArgs { public PlayerDetachedEvent(EntityUid entity) { Entity = entity; } public EntityUid Entity { get; } } }