using System; using System.Numerics; using Robust.Client.GameStates; using Robust.Client.Input; using Robust.Client.Player; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Client.GameObjects { /// /// Client-side processing of all input commands through the simulation. /// public sealed class InputSystem : SharedInputSystem, IPostInjectInit { [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IClientGameStateManager _stateManager = default!; [Dependency] private readonly IConsoleHost _conHost = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private ISawmill _sawmillInputContext = 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, IFullInputCmdMessage 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)) { if (!_stateManager.IsPredictionEnabled && !handler.FireOutsidePrediction) continue; // local handlers can block sending over the network. if (handler.HandleCmdMessage(EntityManager, session, message)) { return true; } } // send it off to the server var clientMsg = (ClientFullInputCmdMessage)message; var fullMsg = new FullInputCmdMessage( clientMsg.Tick, clientMsg.SubTick, (int)clientMsg.InputSequence, clientMsg.InputFunctionId, clientMsg.State, GetNetCoordinates(clientMsg.Coordinates), clientMsg.ScreenCoordinates) { Uid = GetNetEntity(clientMsg.Uid) }; DispatchInputCommand(clientMsg, fullMsg); return false; } /// /// Handle a predicted input command. /// /// Input command to handle as predicted. public void PredictInputCommand(IFullInputCmdMessage inputCmd) { var keyFunc = _inputManager.NetworkBindMap.KeyFunctionName(inputCmd.InputFunctionId); Predicted = true; var session = _playerManager.LocalSession; foreach (var handler in BindRegistry.GetHandlers(keyFunc)) { if (handler.HandleCmdMessage(EntityManager, session, inputCmd)) break; } Predicted = false; } private void DispatchInputCommand(ClientFullInputCmdMessage clientMsg, FullInputCmdMessage message) { _stateManager.InputCommandDispatched(clientMsg, message); EntityManager.EntityNetManager?.SendSystemNetworkMessage(message, message.InputSequence); } public override void Initialize() { SubscribeLocalEvent(OnAttachedEntityChanged); _conHost.RegisterCommand("incmd", "Inserts an input command into the simulation", "incmd ", GenerateInputCommand); } public override void Shutdown() { base.Shutdown(); _conHost.UnregisterCommand("incmd"); } private void GenerateInputCommand(IConsoleShell shell, string argstr, string[] args) { if (_playerManager.LocalEntity is not { } pent) return; BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]); BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down; var pxform = Transform(pent); var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3])); var coords = EntityCoordinates.FromMap(pent, new MapCoordinates(wPos, pxform.MapID), _transform, EntityManager); var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction); var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state, GetNetCoordinates(coords), new ScreenCoordinates(0, 0, default), NetEntity.Invalid); HandleInputCommand(_playerManager.LocalSession, keyFunction, message); } private void OnAttachedEntityChanged(LocalPlayerAttachedEvent message) { if (message.Entity != default) // attach { SetEntityContextActive(_inputManager, message.Entity); } 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)) { _sawmillInputContext.Debug($"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 { _sawmillInputContext.Error($"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() { if (_playerManager.LocalEntity is not { } controlled) return; SetEntityContextActive(_inputManager, controlled); } protected override void PostInject() { base.PostInject(); _sawmillInputContext = _logManager.GetSawmill("input.context"); } } }