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); EntityNetworkManager.SendSystemNetworkMessage(message, message.InputSequence); } public override void Initialize() { SubscribeLocalEvent(OnAttachedEntityChanged); } private void OnAttachedEntityChanged(PlayerAttachSysMessage message) { if (message.AttachedEntity != null) // attach { SetEntityContextActive(_inputManager, message.AttachedEntity); } else // detach { _inputManager.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}. 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.Uid}, entProto={entity.Prototype}, 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() { if (_playerManager.LocalPlayer?.ControlledEntity == null) { return; } SetEntityContextActive(_inputManager, _playerManager.LocalPlayer.ControlledEntity); } } /// /// Entity system message that is raised when the player changes attached entities. /// public class PlayerAttachSysMessage : EntitySystemMessage { /// /// New entity the player is attached to. /// public IEntity? AttachedEntity { get; } /// /// Creates a new instance of . /// /// New entity the player is attached to. public PlayerAttachSysMessage(IEntity? attachedEntity) { AttachedEntity = attachedEntity; } } }