Files
RobustToolbox/Robust.Client/GameObjects/EntitySystems/InputSystem.cs
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

266 lines
9.9 KiB
C#

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
{
/// <summary>
/// Client-side processing of all input commands through the simulation.
/// </summary>
public sealed partial class InputSystem : SharedInputSystem, IPostInjectInit
{
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
[Dependency] private IClientGameStateManager _stateManager = default!;
[Dependency] private IConsoleHost _conHost = default!;
[Dependency] private IGameTiming _timing = default!;
[Dependency] private ILogManager _logManager = default!;
[Dependency] private SharedTransformSystem _transform = default!;
private ISawmill _sawmillInputContext = default!;
private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates();
/// <summary>
/// Current states for all of the keyFunctions.
/// </summary>
public IPlayerCommandStates CmdStates => _cmdStates;
/// <summary>
/// If the input system is currently predicting input.
/// </summary>
public bool Predicted { get; private set; }
/// <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>
/// <param name="replay">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</param>
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;
}
}
var clientMsg = message switch
{
ClientFullInputCmdMessage clientInput => clientInput,
FullInputCmdMessage fullInput => new ClientFullInputCmdMessage(
fullInput.Tick,
fullInput.SubTick,
fullInput.InputFunctionId,
GetCoordinates(fullInput.Coordinates),
fullInput.ScreenCoordinates,
fullInput.State,
GetEntity(fullInput.Uid)),
_ => throw new ArgumentOutOfRangeException()
};
var fullMsg = message switch
{
FullInputCmdMessage fullInput => fullInput,
ClientFullInputCmdMessage client => new FullInputCmdMessage(
client.Tick,
client.SubTick,
client.InputFunctionId,
clientMsg.State,
GetNetCoordinates(client.Coordinates),
clientMsg.ScreenCoordinates,
GetNetEntity(clientMsg.Uid)
),
_ => throw new ArgumentOutOfRangeException()
};
DispatchInputCommand(clientMsg, fullMsg);
return false;
}
/// <summary>
/// Handle a predicted input command.
/// </summary>
/// <param name="inputCmd">Input command to handle as predicted.</param>
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<LocalPlayerAttachedEvent>(OnAttachedEntityChanged);
_conHost.RegisterCommand("incmd",
"Inserts an input command into the simulation",
"incmd <KeyFunction> <KeyState> [wxPos] [wyPos]",
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;
if (args.Length is not (2 or 4))
{
shell.WriteLine(Loc.GetString($"cmd-invalid-arg-number-error"));
return;
}
var keyFunction = new BoundKeyFunction(args[0]);
if (!Enum.TryParse<BoundKeyState>(args[1], out var state))
{
shell.WriteLine(Loc.GetString("cmd-parse-failure-enum", ("arg", args[1]), ("enum", nameof(BoundKeyState))));
return;
}
var wOffset = Vector2.Zero;
if (args.Length == 4)
{
if (!float.TryParse(args[2], out var wX))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[2])));
return;
}
if (!float.TryParse(args[3], out var wY))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-float", ("arg", args[3])));
return;
}
wOffset = new Vector2(wX, wY);
}
var coords = _transform.ToCoordinates(pent, _transform.GetMapCoordinates(pent).Offset(wOffset));
var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction);
var message = new ClientFullInputCmdMessage(_timing.CurTick,
_timing.TickFraction,
funcId,
coords,
new ScreenCoordinates(0, 0, default),
state,
EntityUid.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 || !Exists(entity))
throw new ArgumentNullException(nameof(entity));
if (!TryComp(entity, out InputComponent? inputComp))
{
_sawmillInputContext.Debug($"AttachedEnt has no InputComponent: entId={entity}, entProto={Comp<MetaDataComponent>(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={Comp<MetaDataComponent>(entity).EntityPrototype}, context={inputComp.ContextName}. . Setting default \"{InputContextContainer.DefaultContextName}\" context...");
inputMan.Contexts.SetActiveContext(InputContextContainer.DefaultContextName);
}
}
/// <summary>
/// Sets the active context to the defined context on the attached entity.
/// </summary>
public void SetEntityContextActive()
{
if (_playerManager.LocalEntity is not { } controlled)
return;
SetEntityContextActive(_inputManager, controlled);
}
protected override void PostInject()
{
base.PostInject();
_sawmillInputContext = _logManager.GetSawmill("input.context");
}
}
}